diff --git a/public/app.js b/public/app.js index fcb3c3e..925ccf3 100755 --- a/public/app.js +++ b/public/app.js @@ -1,120 +1,119 @@ -let INDEX, CURRENT_PATH=null, PATH_TO_EL=new Map(); -const treeEl=document.getElementById("tree"); -const mdView=document.getElementById("mdView"); -const iframe=document.getElementById("htmlView"); -const metaLine=document.getElementById("meta"); -const sortSel=document.getElementById("sort"); -const filterSel=document.getElementById("filter"); -const searchBox=document.getElementById("search"); -const prevBtn=document.getElementById("prev"); -const nextBtn=document.getElementById("next"); +let INDEX, CURRENT_PATH = null, PATH_TO_EL = new Map(); +const treeEl = document.getElementById("tree"); +const mdView = document.getElementById("mdView"); +const htmlView = document.getElementById("htmlView"); +const metaLine = document.getElementById("meta"); +const sortSel = document.getElementById("sort"); +const filterSel = document.getElementById("filter"); +const searchBox = document.getElementById("search"); +const prevBtn = document.getElementById("prev"); +const nextBtn = document.getElementById("next"); -async function loadIndex(){ - const res=await fetch("/index.json",{cache:"no-store"}); - INDEX=await res.json(); +async function loadIndex() { + const res = await fetch("/index.json", { cache: "no-store" }); + INDEX = await res.json(); populateFilters(); rebuildTree(); - window.addEventListener("popstate",()=>{const hp=location.hash.slice(2);if(hp)openPath(hp);}); - const init=location.hash.slice(2)||INDEX.flat.sort((a,b)=>b.mtime-a.mtime)[0].path; + window.addEventListener("popstate", () => { const hp = location.hash.startsWith("#=") ? location.hash.slice(2) : null; if (hp) openPath(hp); }); + const init = location.hash.startsWith("#=") ? location.hash.slice(2) : INDEX.flat.sort((a, b) => b.mtime - a.mtime)[0]?.path; openPath(init); } -function populateFilters(){ - const cats=new Set(INDEX.sections); - filterSel.innerHTML=''; - for(const c of cats){const o=document.createElement("option");o.value=c;o.textContent=c;filterSel.appendChild(o);} -} -function rebuildTree(){ - treeEl.innerHTML=""; - const filter=filterSel.value; - const sort=sortSel.value; - const query=searchBox.value.toLowerCase(); - for(const dir of INDEX.tree){ - if(filter!=="all"&&dir.name!==filter)continue; - treeEl.appendChild(renderNode(dir,sort,query)); +function populateFilters() { + filterSel.innerHTML = ''; + for (const cat of INDEX.sections) { + const opt = document.createElement("option"); + opt.value = opt.textContent = cat; + filterSel.appendChild(opt); } } -function renderNode(node,sort,query){ - if(node.type==="dir"){ - const div=document.createElement("div"); - div.className="dir";div.setAttribute("aria-expanded","false"); - const lbl=document.createElement("span"); - lbl.className="label";lbl.textContent=node.name; - lbl.addEventListener("click",()=>{ - const idx=node.children.find(c=>/^index\.(md|html)$/.test(c.name)); - if(idx)openPath(idx.path); +function rebuildTree() { + treeEl.innerHTML = ""; + PATH_TO_EL.clear(); + const filter = filterSel.value; + const sort = sortSel.value; + const query = searchBox.value.trim().toLowerCase(); + const root = { type: "dir", children: INDEX.tree }; + const pruned = filterTree(root, f => (filter === "all" || f.path.split("/")[0] === filter) && (!query || (f.title || f.name).toLowerCase().includes(query))); + sortDir(pruned, sort); + for (const c of pruned.children) treeEl.appendChild(renderNode(c)); + treeEl.querySelectorAll(".dir").forEach(d => d.classList.add("open")); // Auto-open tops +} +function filterTree(node, keep) { + if (node.type === "file") return keep(node) ? node : null; + const kids = node.children.map(c => filterTree(c, keep)).filter(Boolean); + return kids.length ? { ...node, children: kids } : null; +} +function sortDir(node, sort) { + const cmp = sort === "name" ? (a, b) => a.name.localeCompare(b.name) : + sort === "old" ? (a, b) => a.mtime - b.mtime : (a, b) => b.mtime - a.mtime; + node.children.sort((a, b) => (a.type === "dir" && b.type !== "dir") ? -1 : (a.type !== "dir" && b.type === "dir") ? 1 : cmp(a, b)); + node.children.forEach(c => c.type === "dir" && sortDir(c, sort)); +} +function renderNode(node) { + if (node.type === "dir") { + const div = document.createElement("div"); + div.className = "dir"; + div.setAttribute("aria-expanded", "false"); + const lbl = document.createElement("span"); + lbl.className = "label"; + lbl.textContent = node.name || "/"; + lbl.addEventListener("click", () => { + const idx = node.children.find(c => c.type === "file" && /^index\.(md|html)$/i.test(c.name)); + if (idx) openPath(idx.path); else div.classList.toggle("open"); }); div.appendChild(lbl); - const kids=document.createElement("div");kids.className="children"; - const sorted=[...node.children].sort((a,b)=>{ - if(sort==="name")return a.name.localeCompare(b.name); - return sort==="old"?a.mtime-b.mtime:b.mtime-a.mtime; - }); - for(const c of sorted){ - if(c.type==="file"){ - if(query&&!c.title.toLowerCase().includes(query))continue; - const a=document.createElement("a"); - a.className="file";a.textContent=c.title; - a.addEventListener("click",e=>{e.preventDefault();openPath(c.path);}); - PATH_TO_EL.set(c.path,a); - kids.appendChild(a); - }else kids.appendChild(renderNode(c,sort,query)); - } + const kids = document.createElement("div"); + kids.className = "children"; + node.children.forEach(c => kids.appendChild(renderNode(c))); div.appendChild(kids); return div; } - return document.createTextNode(""); + const a = document.createElement("a"); + a.className = "file"; + a.innerHTML = `${node.pinned ? '๐Ÿ“Œ' : ''}${iconForExt(node.ext)} ${node.title} (${fmtDate(node.mtime)} ยท ${node.name})`; + a.addEventListener("click", e => { e.preventDefault(); openPath(node.path); }); + PATH_TO_EL.set(node.path, a); + return a; } - -async function openPath(path){ - if(path===CURRENT_PATH)return; - CURRENT_PATH=path; - if(location.hash!==`#=${path}`)history.pushState(null,"",`#=${path}`); - let f=INDEX.flat.find(x=>x.path===path); - if(!f){metaLine.textContent="Not found";return;} - metaLine.textContent=`${f.pinned?"๐Ÿ“Œ ":""}${new Date(f.mtime).toISOString().slice(0,10)} โ€ข ${f.name}`; - if(f.ext===".md")await renderMarkdown(f.path); +function iconForExt(ext) { return ext === ".md" ? "๐Ÿ“" : "๐Ÿงฉ"; } +function fmtDate(ms) { return new Date(ms).toISOString().slice(0, 10); } +function findDir(path) { + path = path.replace(/\/$/, ''); + function search(node) { + if (node.type === "dir" && node.path === path) return node; + for (const c of node.children || []) { + const found = search(c); + if (found) return found; + } + } + return search({ children: INDEX.tree }); +} +async function openPath(path) { + if (path === CURRENT_PATH) return; + CURRENT_PATH = path; + if (location.hash !== `#=${path}`) history.pushState(null, "", `#=${path}`); + let f = INDEX.flat.find(x => x.path === path); + if (!f) { + const dir = findDir(path); + if (dir) { + const idx = dir.children.find(c => c.type === "file" && /^index\.(md|html)$/i.test(c.name)); + if (idx) return openPath(idx.path); + } + metaLine.textContent = "Path not found: " + path; + return; + } + metaLine.textContent = `${f.pinned ? "๐Ÿ“Œ " : ""}${fmtDate(f.mtime)} โ€ข ${f.name}`; + if (f.ext === ".md") await renderMarkdown(f.path); else renderHTML(f.path); setActive(path); updatePager(); - if(window.innerWidth<900)document.querySelector(".sidebar").classList.remove("open"); + if (window.innerWidth < 900) document.querySelector(".sidebar").classList.remove("open"); } -async function renderMarkdown(path){ - const res=await fetch(path); - if(!res.ok){mdView.innerHTML="

File not found

";return;} - const text=await res.text(); - const html=(window.marked?window.marked.parse(text):text); - const safe=(window.DOMPurify?window.DOMPurify.sanitize(html):html); - mdView.innerHTML=safe; - iframe.style.display="none";mdView.style.display="block"; -} -function renderHTML(path){ - iframe.src=path; - iframe.style.display="block";mdView.style.display="none"; -} -function setActive(path){ - document.querySelectorAll(".file.active").forEach(el=>el.classList.remove("active")); - const el=PATH_TO_EL.get(path); - if(el){el.classList.add("active");let p=el.parentElement;while(p&&p!==treeEl){if(p.classList.contains("children"))p.parentElement.classList.add("open");p=p.parentElement;}} -} -function updatePager(){ - const list=INDEX.flat.filter(f=>f.ext===".md"||f.ext===".html").sort((a,b)=>b.mtime-a.mtime); - const i=list.findIndex(x=>x.path===CURRENT_PATH); - prevBtn.disabled=i<=0;nextBtn.disabled=i>=list.length-1; - prevBtn.onclick=()=>{if(i>0)openPath(list[i-1].path);}; - nextBtn.onclick=()=>{if(i{clearTimeout(searchTimer);searchTimer=setTimeout(rebuildTree,300);}); -sortSel.addEventListener("change",rebuildTree); -filterSel.addEventListener("change",rebuildTree); -document.body.addEventListener("click",e=>{ - const a=e.target.closest("a[href]"); - if(!a)return; - const href=a.getAttribute("href"); - if(href.startsWith("/")&&!href.startsWith("//")&&!a.target){ - e.preventDefault(); - openPath(href.replace(/^\//,"")); - } -}); -window.addEventListener("DOMContentLoaded",loadIndex); \ No newline at end of file +async function renderMarkdown(path) { + const res = await fetch("/" + path); + if (!res.ok) { mdView.innerHTML = "

File not found: " + path + "

"; return; } + const text = await res.text(); + const html = window.marked ? window.marked.parse(text) : text.replace(/&/g, '&').replace(/