diff --git a/public/app.js b/public/app.js index addc49f..e3a9db4 100644 --- a/public/app.js +++ b/public/app.js @@ -1,192 +1,90 @@ -/* app.js (v2.0.0) - * - loads /index.json (generated at build) - * - builds collapsible tree (left) - * - renders HTML in sandboxed iframe OR Markdown via marked+DOMPurify - * - default sort: date (newβ†’old). toggle to alpha. - * - deep links: #=/posts/foo.md - */ - let INDEX = null; let CURRENT_PATH = null; -let PATH_TO_EL = new Map(); - const treeEl = document.getElementById("tree"); -const sortSel = document.getElementById("sort"); -const filterSel = document.getElementById("filter"); -const iframe = document.getElementById("htmlFrame"); -const mdBox = document.getElementById("mdContainer"); const metaEl = document.getElementById("meta"); +const mdView = document.getElementById("mdView"); +const htmlView = document.getElementById("htmlView"); +const sortSel = document.getElementById("sortSel"); +const filterSel = document.getElementById("filterSel"); +const navToggle = document.getElementById("navToggle"); -const ICONS = { - ".md": "πŸ“", - ".html": "🧩", - "dir": "πŸ—‚οΈ" -}; +navToggle.addEventListener("click", () => + document.querySelector(".sidebar").classList.toggle("open") +); -function icon(ext){ return ICONS[ext] || "πŸ“„"; } - -function humanDate(ms){ - const d = new Date(ms || 0); - if (!ms) return ""; - return d.toISOString().slice(0,10); -} - -function applySort(list, mode){ - const arr = list.slice(); - if (mode === "alpha") { - arr.sort((a,b)=> a.title.localeCompare(b.title)); - } else { - arr.sort((a,b)=> (b.mtime||0)-(a.mtime||0)); // new β†’ old - } - return arr; -} - -function filterFlat(list, filter){ - if (filter === "pinned") return list.filter(x=>x.pinned); - if (filter === "posts") return list.filter(x=>!x.pinned); - return list; -} - -function filterTree(node, allowedSet){ - if (node.type === "file") return allowedSet.has(node.path) ? node : null; - const kids = []; - for (const c of node.children||[]){ - const f = filterTree(c, allowedSet); - if (f) kids.push(f); - } - return { ...node, children: kids }; -} - -function buildNode(node){ - if (node.type === "dir"){ - const wrap = document.createElement("div"); - wrap.className = "tree-node dir"; - wrap.setAttribute("role","treeitem"); - wrap.setAttribute("aria-expanded","false"); - - const label = document.createElement("div"); - label.className = "dir-label"; - label.innerHTML = `${icon("dir")}${node.name}`; - label.addEventListener("click", ()=>{ - const open = wrap.classList.toggle("open"); - wrap.setAttribute("aria-expanded", open?"true":"false"); - }); - wrap.appendChild(label); - - const children = document.createElement("div"); - children.className = "children"; - for (const c of node.children || []){ - children.appendChild(buildNode(c)); - } - wrap.appendChild(children); - return wrap; - } else { - const a = document.createElement("a"); - a.className = "file"; - a.setAttribute("role","treeitem"); - a.href = `#=${node.path}`; - a.innerHTML = `${node.pinned ? 'PIN' : ''}${icon(node.ext)} ${node.title} ${humanDate(node.mtime)} Β· ${node.name}`; - a.addEventListener("click", (e)=>{ - e.preventDefault(); - openPath(node.path); - }); - PATH_TO_EL.set(node.path, a); - return a; - } -} - -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"); - // ensure ancestors open - let p = el.parentElement; - while (p && p !== treeEl){ - if (p.classList.contains("dir")){ - p.classList.add("open"); - p.setAttribute("aria-expanded","true"); - } - p = p.parentElement; - } - el.scrollIntoView({ block:"nearest", behavior:"smooth" }); - } -} - -function rebuildTree(){ - PATH_TO_EL.clear(); - treeEl.innerHTML = ""; - // compute filtered/sorted flat set - const sorted = applySort(filterFlat(INDEX.flat, filterSel.value), sortSel.value); - const allowed = new Set(sorted.map(x=>x.path)); - const filteredTree = filterTree(INDEX.tree, allowed); - treeEl.appendChild(buildNode(filteredTree)); - // expand top-level - treeEl.querySelectorAll(".dir").forEach(d=>d.classList.add("open")); -} - -async function renderMarkdown(path){ - iframe.hidden = true; - mdBox.hidden = false; - - const res = await fetch(path, { cache: "no-cache" }); - if (!res.ok) throw new Error(`fetch ${path} ${res.status}`); - const txt = await res.text(); - // marked is global when CDN loads; fallback to plain text if missing - const rawHtml = (window.marked ? window.marked.parse(txt) : `
${txt.replace(/[&<>]/g, s=>({ "&":"&","<":"<",">":">" }[s]))}
`); - const safe = (window.DOMPurify ? window.DOMPurify.sanitize(rawHtml) : rawHtml); - mdBox.innerHTML = safe; -} - -async function renderHTML(path){ - mdBox.hidden = true; - iframe.hidden = false; - iframe.src = path; -} - -async function openPath(path){ - if (!path || path === CURRENT_PATH) return; - CURRENT_PATH = path; - if (location.hash !== `#=${path}`) history.pushState(null,"",`#=${path}`); - - const entry = INDEX.flat.find(x=>x.path === path); - if (!entry) return; - - metaEl.textContent = `${entry.pinned ? "Pinned β€’ " : ""}${humanDate(entry.mtime)} β€’ ${entry.path}`; - setActive(path); - - if (entry.ext === ".md") await renderMarkdown(path); - else await renderHTML(path); -} - -async function boot(){ - try{ - const res = await fetch("/index.json", { cache: "no-cache" }); +async function loadIndex() { + try { + const res = await fetch("index.json"); INDEX = await res.json(); - } catch (e){ - treeEl.innerHTML = "

index.json missing. run the build.

"; + } catch { + treeEl.innerHTML = "

index.json missing. run the build.

"; return; } - - sortSel.addEventListener("change", rebuildTree); - filterSel.addEventListener("change", rebuildTree); - rebuildTree(); - - // choose initial route - const hashPath = location.hash.startsWith("#=") ? location.hash.slice(2) : null; - if (hashPath) { - openPath(hashPath); - } else { - // default: newest among current filter - const sorted = applySort(filterFlat(INDEX.flat, filterSel.value), "date"); - if (sorted.length) openPath(sorted[0].path); - } - - window.addEventListener("popstate", ()=>{ - const hp = location.hash.startsWith("#=") ? location.hash.slice(2) : null; - if (hp && hp !== CURRENT_PATH) openPath(hp); - }); + autoOpenLatest(); } -boot(); \ No newline at end of file +function rebuildTree() { + treeEl.innerHTML = ""; + const roots = ["pinned", "posts"]; + for (const root of roots) { + const dir = INDEX.tree.find(d => d.name === root); + if (dir) { + const h = document.createElement("div"); + h.className = "dir"; + h.textContent = root; + treeEl.appendChild(h); + dir.children.forEach(f => treeEl.appendChild(renderFile(f))); + } + } +} + +function renderFile(f) { + const a = document.createElement("a"); + a.className = "file"; + a.textContent = (f.pinned ? "πŸ“Œ " : "") + f.name; + a.addEventListener("click", e => { + e.preventDefault(); + openPath(f.path); + }); + return a; +} + +function autoOpenLatest() { + if (!INDEX.flat?.length) return; + const sorted = [...INDEX.flat].sort((a,b)=>b.mtime - a.mtime); + openPath(sorted[0].path); +} + +async function openPath(path) { + if (path === CURRENT_PATH) return; + CURRENT_PATH = path; + + const f = INDEX.flat.find(x => x.path === path); + if (!f) return; + + metaEl.textContent = `${f.pinned ? "Pinned β€’ " : ""}${new Date(f.mtime).toISOString().slice(0,10)} β€’ ${f.name}`; + + if (f.ext === ".md") await renderMarkdown(path); + else await renderHTML(path); + + if (window.innerWidth < 900) + document.querySelector(".sidebar").classList.remove("open"); +} + +async function renderMarkdown(path) { + htmlView.style.display = "none"; + mdView.style.display = "block"; + const res = await fetch(path); + const text = await res.text(); + const html = DOMPurify.sanitize(marked.parse(text)); + mdView.innerHTML = html; +} + +async function renderHTML(path) { + mdView.style.display = "none"; + htmlView.style.display = "block"; + htmlView.src = path; +} + +window.addEventListener("DOMContentLoaded", loadIndex); \ No newline at end of file