diff --git a/public/app.js b/public/app.js index 9ddcced..5e28f2f 100755 --- a/public/app.js +++ b/public/app.js @@ -1,195 +1,246 @@ +/* ============================================================ + Self-Organizing Static Site Framework v2.4 (Local Libs) + ============================================================ */ + 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 treeEl = document.getElementById("tree"); +const mdView = document.getElementById("mdView"); +const mdWarn = document.getElementById("mdWarn"); +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"); -const sidebar = document.querySelector(".sidebar"); +const prevBtn = document.getElementById("prev"); +const nextBtn = document.getElementById("next"); +const sidebar = document.querySelector(".sidebar"); const navToggle = document.getElementById("navToggle"); -const overlay = document.querySelector(".overlay"); +const overlay = document.querySelector(".overlay"); +/* Sidebar toggle */ navToggle.addEventListener("click", () => sidebar.classList.toggle("open")); -overlay.addEventListener("click", () => sidebar.classList.remove("open")); +overlay.addEventListener("click", () => sidebar.classList.remove("open")); +/* Load index */ async function loadIndex() { + // No CDN race now; libs are local. Still, surface diagnostics: + if (!window.marked) console.warn("⚠️ marked.js not detected."); + if (!window.DOMPurify) console.warn("⚠️ DOMPurify not detected."); + const res = await fetch("/index.json", { cache: "no-store" }); INDEX = await res.json(); populateFilters(); rebuildTree(); - 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; + + 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() { filterSel.innerHTML = ''; for (const cat of INDEX.sections) { - const opt = document.createElement("option"); - opt.value = opt.textContent = cat; - filterSel.appendChild(opt); + const o = document.createElement("option"); + o.value = o.textContent = cat; + filterSel.appendChild(o); } } + 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))); + 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")); } + 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; + 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)); + 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"); +function renderNode(n) { + if (n.type==="dir") { + const d = document.createElement("div"); + d.className="dir"; d.setAttribute("aria-expanded","false"); + const lbl=document.createElement("span"); + lbl.className="label"; lbl.textContent=n.name||"/"; + lbl.addEventListener("click",()=>{ + const idx=n.children.find(c=>c.type==="file"&&/^index\.(md|html)$/i.test(c.name)); + if(idx) openPath(idx.path); else d.classList.toggle("open"); }); - div.appendChild(lbl); - const kids = document.createElement("div"); - kids.className = "children"; - node.children.forEach(c => kids.appendChild(renderNode(c))); - div.appendChild(kids); - return div; + d.appendChild(lbl); + const kids=document.createElement("div"); + kids.className="children"; + n.children.forEach(c=>kids.appendChild(renderNode(c))); + d.appendChild(kids); + return d; } - const a = document.createElement("a"); - a.className = "file"; - a.innerHTML = `${node.pinned ? '📌' : ''}${iconForExt(node.ext)} ${node.title} `; - a.addEventListener("click", e => { e.preventDefault(); openPath(node.path); }); - PATH_TO_EL.set(node.path, a); + const a=document.createElement("a"); + a.className="file"; + a.innerHTML=`${n.pinned?'📌 ':''}${iconForExt(n.ext)} ${n.title} + `; + a.addEventListener("click",e=>{e.preventDefault();openPath(n.path);}); + PATH_TO_EL.set(n.path,a); return a; } -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; - } +function iconForExt(ext){return ext===".md"?"📝":"🧩";} +function fmtDate(ms){return new Date(ms).toISOString().slice(0,10);} + +function findDir(p){ + p=p.replace(/\/$/,''); + function search(n){ + if(n.type==="dir"&&n.path===p) return n; + for(const c of n.children||[]){const f=search(c);if(f)return f;} } - return search({ children: INDEX.tree }); + 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); + +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; + metaLine.textContent="Path not found: "+path; return; } - metaLine.textContent = `${f.pinned ? "📌 " : ""}${fmtDate(f.mtime)} • ${f.name}`; - if (f.ext === ".md") await renderMarkdown(f.path); + + 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) sidebar.classList.remove("open"); + if(window.innerWidth<900) sidebar.classList.remove("open"); } -async function renderMarkdown(path) { - mdView.innerHTML = "
Loading…
"; - mdView.style.display = "block"; - htmlView.style.display = "none"; - try { - const res = await fetch("/" + path); - if (!res.ok) throw new Error("File not found: " + path); - const text = await res.text(); - let html = text.replace(/&/g, '&').replace(/Loading…"; + htmlView.style.display="none"; + mdView.style.display="block"; + + try{ + const res=await fetch("/"+path, { cache: "no-store" }); + if(!res.ok) throw new Error("File not found: "+path); + const text=await res.text(); + + let usedFallback = false; + let html; + if (window.marked) { html = window.marked.parse(text); - usedFallback = false; + } else { + // explicit, visible signal + usedFallback = true; + html = text.replace(/&/g,"&").replace(/ { + + const safe = window.DOMPurify ? window.DOMPurify.sanitize(html) : html; + + requestAnimationFrame(()=>{ mdView.innerHTML = safe; mdView.scrollTop = 0; mdView.classList.add("fade-in"); + mdView.style.display = "block"; + if (usedFallback) mdWarn.style.display = "block"; }); - if (usedFallback) console.warn("Markdown rendered as plain text: marked.js not loaded. Check CDN or vendor locally."); - } catch (e) { - mdView.innerHTML = `${e.message}
`; + + }catch(e){ + mdView.innerHTML=`${e.message}
`; } } -function renderHTML(path) { - htmlView.style.display = "none"; - mdView.style.display = "none"; - metaLine.innerHTML += " Loading…"; - htmlView.src = "/" + path; - htmlView.onload = () => { - htmlView.style.display = "block"; - htmlView.classList.add("fade-in"); - metaLine.innerHTML = metaLine.innerHTML.replace(" Loading…", ""); - }; - htmlView.onerror = () => metaLine.innerHTML += " Load failed."; + +/* ---------- HTML ---------- */ +function renderHTML(path){ + htmlView.src="/"+path; + htmlView.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) { + +/* ---------- Active + Pager ---------- */ +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; + let p=el.parentElement; + while(p&&p!==treeEl){ + if(p.classList.contains("children")) p.parentElement.classList.add("open"); + p=p.parentElement; } } } -function updatePager() { - const query = searchBox.value.trim().toLowerCase(); - const list = INDEX.flat.filter(f => (filterSel.value === "all" || f.path.split("/")[0] === filterSel.value) && (!query || f.title.toLowerCase().includes(query))); - const cmp = sortSel.value === "name" ? (a, b) => a.name.localeCompare(b.name) : - sortSel.value === "old" ? (a, b) => a.mtime - b.mtime : (a, b) => b.mtime - a.mtime; +function updatePager(){ + const q=searchBox.value.trim().toLowerCase(); + const list=INDEX.flat.filter(f=> + (filterSel.value==="all"||f.path.split("/")[0]===filterSel.value)&& + (!q||f.title.toLowerCase().includes(q)) + ); + const cmp=sortSel.value==="name"?(a,b)=>a.name.localeCompare(b.name) + :sortSel.value==="old"?(a,b)=>a.mtime-b.mtime + :(a,b)=>b.mtime-a.mtime; list.sort(cmp); - const i = list.findIndex(x => x.path === CURRENT_PATH); - prevBtn.disabled = i <= 0; - nextBtn.disabled = i >= list.length - 1 || i < 0; - prevBtn.onclick = () => i > 0 && openPath(list[i - 1].path); - nextBtn.onclick = () => i < list.length - 1 && openPath(list[i + 1].path); + const i=list.findIndex(x=>x.path===CURRENT_PATH); + prevBtn.disabled=i<=0; + nextBtn.disabled=i>=list.length-1||i<0; + prevBtn.onclick=()=>i>0&&openPath(list[i-1].path); + nextBtn.onclick=()=>i