Update app.js

This commit is contained in:
Mark Randall Havens △ The Empathic Technologist ⟁ Doctor Who 42 2025-11-08 09:59:51 -06:00 committed by GitHub
parent 7d4d9471b5
commit 9f55ba2a7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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 (newold). toggle to alpha.
* - deep links: #=/posts/foo.md
*/
let INDEX = null; let INDEX = null;
let CURRENT_PATH = null; let CURRENT_PATH = null;
let PATH_TO_EL = new Map();
const treeEl = document.getElementById("tree"); 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 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 = { navToggle.addEventListener("click", () =>
".md": "📝", document.querySelector(".sidebar").classList.toggle("open")
".html": "🧩", );
"dir": "🗂️"
};
function icon(ext){ return ICONS[ext] || "📄"; } async function loadIndex() {
try {
function humanDate(ms){ const res = await fetch("index.json");
const d = new Date(ms || 0); INDEX = await res.json();
if (!ms) return ""; } catch {
return d.toISOString().slice(0,10); treeEl.innerHTML = "<p style='color:red'>index.json missing. run the build.</p>";
} return;
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 = `<span>${icon("dir")}</span><strong>${node.name}</strong>`;
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 ? '<span class="pin">PIN</span>' : ''}${icon(node.ext)} ${node.title} <span class="meta">${humanDate(node.mtime)} · ${node.name}</span>`;
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" });
} }
rebuildTree();
autoOpenLatest();
} }
function rebuildTree() { function rebuildTree() {
PATH_TO_EL.clear();
treeEl.innerHTML = ""; treeEl.innerHTML = "";
// compute filtered/sorted flat set const roots = ["pinned", "posts"];
const sorted = applySort(filterFlat(INDEX.flat, filterSel.value), sortSel.value); for (const root of roots) {
const allowed = new Set(sorted.map(x=>x.path)); const dir = INDEX.tree.find(d => d.name === root);
const filteredTree = filterTree(INDEX.tree, allowed); if (dir) {
treeEl.appendChild(buildNode(filteredTree)); const h = document.createElement("div");
// expand top-level h.className = "dir";
treeEl.querySelectorAll(".dir").forEach(d=>d.classList.add("open")); h.textContent = root;
treeEl.appendChild(h);
dir.children.forEach(f => treeEl.appendChild(renderFile(f)));
}
}
} }
async function renderMarkdown(path){ function renderFile(f) {
iframe.hidden = true; const a = document.createElement("a");
mdBox.hidden = false; a.className = "file";
a.textContent = (f.pinned ? "📌 " : "") + f.name;
const res = await fetch(path, { cache: "no-cache" }); a.addEventListener("click", e => {
if (!res.ok) throw new Error(`fetch ${path} ${res.status}`); e.preventDefault();
const txt = await res.text(); openPath(f.path);
// marked is global when CDN loads; fallback to plain text if missing });
const rawHtml = (window.marked ? window.marked.parse(txt) : `<pre>${txt.replace(/[&<>]/g, s=>({ "&":"&amp;","<":"&lt;",">":"&gt;" }[s]))}</pre>`); return a;
const safe = (window.DOMPurify ? window.DOMPurify.sanitize(rawHtml) : rawHtml);
mdBox.innerHTML = safe;
} }
async function renderHTML(path){ function autoOpenLatest() {
mdBox.hidden = true; if (!INDEX.flat?.length) return;
iframe.hidden = false; const sorted = [...INDEX.flat].sort((a,b)=>b.mtime - a.mtime);
iframe.src = path; openPath(sorted[0].path);
} }
async function openPath(path) { async function openPath(path) {
if (!path || path === CURRENT_PATH) return; if (path === CURRENT_PATH) return;
CURRENT_PATH = path; CURRENT_PATH = path;
if (location.hash !== `#=${path}`) history.pushState(null,"",`#=${path}`);
const entry = INDEX.flat.find(x=>x.path === path); const f = INDEX.flat.find(x => x.path === path);
if (!entry) return; if (!f) return;
metaEl.textContent = `${entry.pinned ? "Pinned • " : ""}${humanDate(entry.mtime)}${entry.path}`; metaEl.textContent = `${f.pinned ? "Pinned • " : ""}${new Date(f.mtime).toISOString().slice(0,10)}${f.name}`;
setActive(path);
if (entry.ext === ".md") await renderMarkdown(path); if (f.ext === ".md") await renderMarkdown(path);
else await renderHTML(path); else await renderHTML(path);
if (window.innerWidth < 900)
document.querySelector(".sidebar").classList.remove("open");
} }
async function boot(){ async function renderMarkdown(path) {
try{ htmlView.style.display = "none";
const res = await fetch("/index.json", { cache: "no-cache" }); mdView.style.display = "block";
INDEX = await res.json(); const res = await fetch(path);
} catch (e){ const text = await res.text();
treeEl.innerHTML = "<p style='color:#f66'>index.json missing. run the build.</p>"; const html = DOMPurify.sanitize(marked.parse(text));
return; mdView.innerHTML = html;
} }
sortSel.addEventListener("change", rebuildTree); async function renderHTML(path) {
filterSel.addEventListener("change", rebuildTree); mdView.style.display = "none";
htmlView.style.display = "block";
rebuildTree(); htmlView.src = path;
// 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", ()=>{ window.addEventListener("DOMContentLoaded", loadIndex);
const hp = location.hash.startsWith("#=") ? location.hash.slice(2) : null;
if (hp && hp !== CURRENT_PATH) openPath(hp);
});
}
boot();