From 9f759c7e13dab4085c79e35236371de2ce805ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Randall=20Havens=20=E2=96=B3=20The=20Empathic=20Tec?= =?UTF-8?q?hnologist=20=E2=9F=81=20Doctor=20Who=2042?= Date: Sat, 8 Nov 2025 14:17:27 -0600 Subject: [PATCH] Update app.js --- public/app.js | 355 +++++++++++++++++++------------------------------- 1 file changed, 134 insertions(+), 221 deletions(-) diff --git a/public/app.js b/public/app.js index da9211e..a549834 100755 --- a/public/app.js +++ b/public/app.js @@ -1,241 +1,154 @@ /* ============================================================ - The Fold Within β€” Framework v2.6 Stable Layout Build + The Fold Within β€” app.js v2.6.2 ============================================================ */ -let INDEX, CURRENT_PATH = null, PATH_TO_EL = new Map(); +document.addEventListener("DOMContentLoaded", () => { + const sidebar = document.querySelector(".sidebar"); + const overlay = document.querySelector(".overlay"); + const navToggle = document.getElementById("navToggle"); + const content = document.querySelector(".content"); + const mdView = document.getElementById("mdView"); + const htmlView = document.getElementById("htmlView"); -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 navToggle = document.getElementById("navToggle"); -const overlay = document.querySelector(".overlay"); + let currentPath = ""; + let usedFallback = false; -/* Sidebar toggle */ -navToggle.addEventListener("click", () => sidebar.classList.toggle("open")); -overlay.addEventListener("click", () => sidebar.classList.remove("open")); - -/* Load index and initialize */ -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.startsWith("#=") ? location.hash.slice(2) : null; - if (hp) openPath(hp); + /* ============================================================ + Sidebar Toggle (Desktop + Mobile) + ============================================================ */ + navToggle.addEventListener("click", () => { + const isDesktop = window.innerWidth >= 900; + if (isDesktop) { + sidebar.classList.toggle("collapsed"); + content.classList.toggle("full"); + } else { + sidebar.classList.toggle("open"); + overlay.classList.toggle("active"); + } }); - const init = location.hash.startsWith("#=") - ? location.hash.slice(2) - : INDEX.flat.sort((a,b)=>b.mtime-a.mtime)[0]?.path; + overlay.addEventListener("click", () => { + sidebar.classList.remove("open"); + overlay.classList.remove("active"); + }); - openPath(init); -} - -function populateFilters() { - filterSel.innerHTML = ''; - for (const cat of INDEX.sections) { - const o = document.createElement("option"); - o.value = o.textContent = cat; - filterSel.appendChild(o); + /* ============================================================ + Load External Libraries (Marked + DOMPurify) + ============================================================ */ + async function ensureLibsReady(timeoutMs = 4000) { + const start = Date.now(); + while ( + (!window.marked || !window.DOMPurify) && + Date.now() - start < timeoutMs + ) { + await new Promise((r) => setTimeout(r, 100)); + } + if (!window.marked || !window.DOMPurify) { + console.warn("Markdown libraries not loaded β€” fallback to plain text."); + usedFallback = true; + } else usedFallback = false; } -} -/* Tree building */ -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")); -} + /* ============================================================ + File Loading + Rendering + ============================================================ */ + async function loadFile(path) { + if (!path) return; + currentPath = path; -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(n) { - if (n.type==="dir") { - const d = document.createElement("div"); - d.className="dir"; - 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"); - }); - 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=`${n.pinned?'πŸ“Œ ':''}${iconForExt(n.ext)} ${n.title} - (${fmtDate(n.mtime)} Β· ${n.name})`; - 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);} + try { + const res = await fetch(path); + if (!res.ok) throw new Error(`Path not found: ${path}`); -/* File open */ -async function openPath(path){ - if(path===CURRENT_PATH) return; - CURRENT_PATH=path; - if(location.hash!==`#=${path}`) history.pushState(null,"",`#=${path}`); + const ext = path.split(".").pop().toLowerCase(); + const text = await res.text(); - const f=INDEX.flat.find(x=>x.path===path); - if(!f){ 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) sidebar.classList.remove("open"); -} - -/* Markdown renderer */ -async function renderMarkdown(path){ - mdWarn.style.display = "none"; - mdView.innerHTML="

Loading…

"; - htmlView.style.display="none"; - mdView.style.display="block"; - - try{ - const res=await fetch("/"+path,{cache:"no-store"}); - const text=await res.text(); - - let html, usedFallback=false; - if(window.marked) html=window.marked.parse(text); - else {usedFallback=true; html=text.replace(/&/g,"&").replace(/{ - const content=document.querySelector(".content"); - if(content){ - const vh=window.innerHeight; - content.style.minHeight=`${vh-48}px`; + if (ext === "md" || ext === "markdown") { + await ensureLibsReady(); + renderMarkdown(text); + } else if (ext === "html" || ext === "htm") { + renderHTML(text); + } else { + mdView.innerHTML = `
Unsupported file type: ${ext}
`; + htmlView.innerHTML = ""; } - mdView.scrollIntoView({behavior:"instant",block:"start"}); - },80); - }catch(e){ mdView.innerHTML=`

${e.message}

`; } -} - -/* HTML renderer */ -function renderHTML(path){ - htmlView.src="/"+path; - htmlView.style.display="block"; - mdView.style.display="none"; - htmlView.classList.remove("fade-in"); - htmlView.offsetHeight; - htmlView.classList.add("fade-in"); - - setTimeout(()=>{ - const content=document.querySelector(".content"); - if(content){ - const vh=window.innerHeight; - content.style.minHeight=`${vh-48}px`; - } - htmlView.scrollIntoView({behavior:"instant",block:"start"}); - },120); -} - -/* 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; + } catch (err) { + console.error(err); + mdView.innerHTML = `
${err.message}
`; + htmlView.innerHTML = ""; } } -} -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{ - clearTimeout(searchTimer); - searchTimer=setTimeout(rebuildTree,300); -}); -sortSel.addEventListener("change",rebuildTree); -filterSel.addEventListener("change",rebuildTree); - -/* Internal link interception */ -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(/^\//,"")); + /* ============================================================ + Renderers + ============================================================ */ + function renderMarkdown(text) { + const safe = window.DOMPurify + ? DOMPurify.sanitize(window.marked.parse(text)) + : text + .replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c])); + mdView.innerHTML = usedFallback + ? `
Markdown fallback to plain text (libs failed). Check console.
${safe}
` + : safe; + fadeIn(mdView); } -}); -/* Resize */ -window.addEventListener("resize",()=>{ - const vh=window.innerHeight; - const c=document.querySelector(".content"); - if(c) c.style.minHeight=`${vh-48}px`; -}); + function renderHTML(text) { + htmlView.srcdoc = text; + fadeIn(htmlView); + } -window.addEventListener("DOMContentLoaded",loadIndex); \ No newline at end of file + /* ============================================================ + Tree Navigation (dynamic) + ============================================================ */ + document.querySelectorAll(".file").forEach((fileEl) => { + fileEl.addEventListener("click", () => { + const path = fileEl.dataset.path; + loadFile(path); + + // highlight active + document + .querySelectorAll(".file.active") + .forEach((el) => el.classList.remove("active")); + fileEl.classList.add("active"); + + // auto-collapse mobile + if (window.innerWidth < 900) { + sidebar.classList.remove("open"); + overlay.classList.remove("active"); + } + }); + }); + + /* ============================================================ + Fade Animation Helper + ============================================================ */ + function fadeIn(el) { + if (!el) return; + el.classList.remove("fade-in"); + void el.offsetWidth; // reflow + el.classList.add("fade-in"); + } + + /* ============================================================ + Hash-Based Routing (supports #=posts/file.md) + ============================================================ */ + function handleHashChange() { + const hash = window.location.hash.replace(/^#=+/, ""); + if (hash && hash !== currentPath) loadFile(hash); + } + + window.addEventListener("hashchange", handleHashChange); + handleHashChange(); + + /* ============================================================ + Keyboard Shortcuts (optional) + ============================================================ */ + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + sidebar.classList.remove("open", "collapsed"); + content.classList.remove("full"); + overlay.classList.remove("active"); + } + }); +}); \ No newline at end of file