Something went wrong. Try again.
about things notes.zzstoatzz.io
notes
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326import { createNavigationCoordinator, internalDocRoute } from "./navigation.js";import { renderTreeNodes, subtreeContains } from "./sidebar-tree.js";import { initSearch } from "./search.js";
const navEl = document.getElementById("nav");const sidebarEl = document.getElementById("sidebar");const contentEl = document.getElementById("content");const themeToggle = document.querySelector(".theme-toggle");
// Theme togglefunction getPreferredTheme() { const stored = localStorage.getItem("theme"); if (stored) return stored; return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";}
function applyTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("theme", theme); themeToggle?.setAttribute( "aria-label", theme === "dark" ? "Switch to light theme" : "Switch to dark theme", );}
applyTheme(getPreferredTheme());
themeToggle?.addEventListener("click", () => { const current = document.documentElement.getAttribute("data-theme") || getPreferredTheme(); applyTheme(current === "dark" ? "light" : "dark");});
const buildId = new URL(import.meta.url).searchParams.get("v") || "";
function withBuild(url) { if (!buildId) return url; const sep = url.includes("?") ? "&" : "?"; return `${url}${sep}v=${encodeURIComponent(buildId)}`;}
function escapeHtml(text) { return text .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'");}
// Routes are extensionless and file-layout-free: a note's route is its doc// path without ".md", a directory's route is the directory (its README is// the page), and home is "". ".md"-suffixed and trailing-slash inputs are// accepted for backwards compatibility but never emitted.function routeFor(input) { let p = String(input || "").trim(); p = p.replaceAll("\\", "/"); p = p.replace(/^\/+/, "").replace(/\/+$/, ""); p = p.replace(/\.\.\//g, ""); p = p.replace(/\.md$/i, ""); p = p.replace(/(^|\/)README$/i, "$1").replace(/\/+$/, ""); if (p === "index") p = ""; return p;}
// candidate files for a route, tried in order: a note, then a directory indexfunction docCandidates(route) { if (!route) return ["index.md"]; return [`${route}.md`, `${route}/README.md`];}
function getSelectedRoute() { const hash = (location.hash || "").replace(/^#/, ""); if (!hash) return null; // Decode defensively so literal and percent-encoded hashes resolve alike. let decoded = hash; try { decoded = decodeURIComponent(hash); } catch { // malformed escape — fall back to the raw hash } return routeFor(decoded);}
async function fetchJson(path) { const res = await fetch(withBuild(path), { cache: "no-store" }); if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`); return res.json();}
async function fetchText(path, signal) { const res = await fetch(withBuild(path), { cache: "no-store", signal }); if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`); return res.text();}
function renderNav(pages, activePath) { if (!pages.length) { navEl.innerHTML = ""; return; }
navEl.innerHTML = pages .filter((p) => routeFor(p.path) !== "") .map((p) => { const route = routeFor(p.path); const title = escapeHtml(p.title || route); const current = activePath === route ? ` aria-current="page"` : ""; return `<a href="${pageHref(p.path)}"${current}>${title}</a>`; }) .join("");}
function slugify(text) { return String(text) .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-");}
function pageHref(docPath) { return "#" + routeFor(docPath).split("/").map(encodeURIComponent).join("/");}
function sidebarLabel(page) { return routeFor(page.path) === "" ? "Overview" : page.title || page.path;}
// The left tree is progressively disclosed: every parent is a <details>, and// only the active page's ancestor chain opens automatically. Directories link// to their README when they have one; notes are leaves. Page headings live in// the right-hand #toc column, never in this tree.function renderLeaf(node, activePath, cls) { const p = routeFor(node.path); const active = p === activePath; return `<a class="${cls}${active ? " active" : ""}"${ active ? ' aria-current="page"' : "" } href="${pageHref(p)}">${escapeHtml(node.title || p)}</a>`;}
function renderSidebar(pages, activePath) { const items = pages .map((p) => { const route = routeFor(p.path); const active = route === activePath; const kids = Array.isArray(p.children) ? p.children : []; const link = `<a class="s-page${active ? " active" : ""}"${ active ? ' aria-current="page"' : "" } href="${pageHref(p.path)}">${escapeHtml(sidebarLabel(p))}</a>`; if (!kids.length) return `<li>${link}</li>`; const open = active || subtreeContains(p, activePath, routeFor); return ( `<li><details class="s-section"${open ? " open" : ""}>` + `<summary>${link}</summary>` + `<ul class="s-children">${renderTreeNodes(kids, activePath, { routeFor, renderLeaf, escapeHtml, })}</ul>` + `</details></li>` ); }) .join(""); sidebarEl.innerHTML = `<ul class="s-list">${items}</ul>`;}
// Fill the right-hand "on this page" column from the rendered doc's h2/h3.// Hidden when a page has fewer than 2 headings — nothing to orient by.const tocEl = document.getElementById("toc");
function renderHeadingToc() { if (!tocEl) return; const heads = [...contentEl.querySelectorAll("h2, h3")]; if (heads.length < 2) { tocEl.innerHTML = ""; return; } tocEl.innerHTML = `<div class="toc-label">on this page</div><ul>` + heads .map((h) => { if (!h.id) h.id = slugify(h.textContent); const sub = h.tagName === "H3" ? " s-toc-sub" : ""; return `<li><a class="s-toc-link${sub}" href="#" data-target="${h.id}">${escapeHtml( h.textContent, )}</a></li>`; }) .join("") + `</ul>`;}
// a link inside a <summary> should navigate, not toggle the section —// route it by hand and suppress the summary's default toggle.sidebarEl.addEventListener("click", (e) => { const summaryLink = e.target?.closest?.("summary a[href]"); if (!summaryLink) return; e.preventDefault(); location.hash = summaryLink.getAttribute("href").replace(/^#/, "");});
// Heading anchors scroll in-page without touching location.hash (the hash is// the SPA's doc route, so a real #id anchor would break navigation).tocEl?.addEventListener("click", (e) => { const a = e.target?.closest?.("a[data-target]"); if (!a) return; e.preventDefault(); const target = document.getElementById(a.getAttribute("data-target")); if (!target) return; // A heading can sit inside a closed <details>; scrollIntoView on a hidden // element is a no-op, so open the ancestors first. for (let d = target.closest("details"); d; d = d.parentElement?.closest("details")) { d.open = true; } const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches; target.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "start", });});
function titleForPath(nodes, path) { for (const n of nodes) { if (n.path && routeFor(n.path) === path) return n.title || path; const found = titleForPath(n.children || [], path); if (found !== path) return found; } return path;}
// Skip link: move focus to the content region without polluting the route hash.document.querySelector(".skip-link")?.addEventListener("click", (e) => { e.preventDefault(); contentEl.focus();});
// Rewrite document-shaped links to the SPA route they actually target. This// keeps browser link previews truthful and lets normal anchor behavior handle// navigation instead of repairing relative paths after a click.function routeContentLinks(fromDocPath) { for (const anchor of contentEl.querySelectorAll("a[href]")) { const route = internalDocRoute(anchor.getAttribute("href") || "", fromDocPath, routeFor); if (route !== null) anchor.setAttribute("href", pageHref(route)); }}
async function main() { // SPA fallback: convert pathname to hash route so deep links work if (location.pathname !== "/" && !location.hash) { const path = location.pathname.replace(/^\/+/, ""); if (path) { location.replace("#" + routeFor(path).split("/").map(encodeURIComponent).join("/")); return; } }
if (!globalThis.marked) { contentEl.innerHTML = `<p class="empty">Markdown renderer failed to load.</p>`; return; }
let manifest; try { manifest = await fetchJson("./manifest.json"); } catch (e) { contentEl.innerHTML = `<p class="empty">Missing <code>manifest.json</code>. Deploy the site via CI.</p>`; navEl.innerHTML = ""; console.error(e); return; }
const pages = Array.isArray(manifest.pages) ? manifest.pages : [];
const navigations = createNavigationCoordinator();
initSearch({ pages, routeFor, pageHref, escapeHtml });
async function render(isNav) { const navigation = navigations.begin(); const activeRoute = getSelectedRoute() ?? ""; renderNav(pages, activeRoute); renderSidebar(pages, activeRoute);
// a route can be a note (route.md) or a directory (route/README.md) — // try in order; the legacy "#foo.md" form was normalized by routeFor. let md = null; let docPath = null; for (const candidate of docCandidates(activeRoute)) { try { md = await fetchText(`./docs/${candidate}`, navigation.signal); if (!navigation.isCurrent()) return; docPath = candidate; break; } catch { if (!navigation.isCurrent()) return; // try the next candidate } }
if (!navigation.isCurrent()) return; if (md === null) { contentEl.innerHTML = `<p class="empty">Nothing lives at <code>${escapeHtml( activeRoute, )}</code>.</p>`; return; }
contentEl.innerHTML = globalThis.marked.parse(md); routeContentLinks(docPath); globalThis.Prism?.highlightAllUnder(contentEl); renderHeadingToc(); window.scrollTo(0, 0);
// On navigation, announce the new page and move focus into it so screen // reader and keyboard users aren't stranded on stale/orphaned focus. const liveEl = document.getElementById("a11y-live"); if (liveEl) liveEl.textContent = titleForPath(pages, activeRoute) || "notes"; if (isNav) contentEl.focus(); }
window.addEventListener("hashchange", () => render(true)); await render(false);}
main();