; 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 = `Markdown renderer failed to load.
`;
return;
}
let manifest;
try {
manifest = await fetchJson("./manifest.json");
} catch (e) {
contentEl.innerHTML = `Missing manifest.json. Deploy the site via CI.
`;
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 = `Nothing lives at ${escapeHtml(
activeRoute,
)}.
`;
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();