From 58fd4a1c512c394ff1d9c17dcd0dfae1ad2b7233 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 12:27:31 -0600
Subject: [PATCH] Update app.js
---
public/app.js | 299 ++++++++++++++++++++++++++++++--------------------
1 file changed, 178 insertions(+), 121 deletions(-)
diff --git a/public/app.js b/public/app.js
index 2c2b3e1..efae277 100755
--- a/public/app.js
+++ b/public/app.js
@@ -1,179 +1,236 @@
+/* ============================================================
+ Self-Organizing Static Site Framework v2.3.2
+ ============================================================ */
+
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 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");
+/* --- Navigation toggle --- */
navToggle.addEventListener("click", () => sidebar.classList.toggle("open"));
-overlay.addEventListener("click", () => sidebar.classList.remove("open"));
+overlay.addEventListener("click", () => sidebar.classList.remove("open"));
+/* --- Index load --- */
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); });
- 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);
}
}
+
+/* --- Tree build --- */
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} (${fmtDate(node.mtime)} Β· ${node.name})`;
- 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}
+ (${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); }
-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);}
+
+/* --- Path openers --- */
+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.style.display = "none";
- const res = await fetch("/" + path);
- if (!res.ok) { mdView.innerHTML = "File not found: " + path + "
"; requestAnimationFrame(() => mdView.style.display = "block"); return; }
- 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);
+ if(!res.ok) throw new Error("File not found: "+path);
+ const text=await res.text();
+
+ let html=text.replace(/&/g,"&").replace(/{
+ mdView.innerHTML=safe;
+ mdView.classList.add("fade-in");
+ mdView.style.display="block";
+ });
+
+ if(usedFallback) console.warn("Markdown rendered as plain text (marked.js missing).");
+ }catch(e){
+ mdView.innerHTML=`${e.message}
`;
}
- let safe = html;
- if (window.DOMPurify) safe = window.DOMPurify.sanitize(html);
- mdView.innerHTML = safe;
- requestAnimationFrame(() => { mdView.style.display = "block"; htmlView.style.display = "none"; });
- if (usedFallback) console.warn("Markdown rendered as plain text: marked.js not loaded. Check CDN/SRI.");
}
-function renderHTML(path) {
- htmlView.src = "/" + path;
- htmlView.style.display = "block";
- mdView.style.display = "none";
+
+/* --- HTML viewer --- */
+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 { 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) {
+searchBox.addEventListener("input",()=>{
+ 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(/^\//, ""));
+ openPath(href.replace(/^\//,""));
}
});
-window.addEventListener("resize", () => { if (window.innerWidth < 900) sidebar.classList.remove("open"); });
-window.addEventListener("DOMContentLoaded", loadIndex);
\ No newline at end of file
+
+window.addEventListener("resize",()=>{ if(window.innerWidth<900) sidebar.classList.remove("open"); });
+window.addEventListener("DOMContentLoaded",loadIndex);
\ No newline at end of file