atproto utils for zig zat.dev
atproto sdk zig
at main 4.7 kB view raw
1const navEl = document.getElementById("nav"); 2const contentEl = document.getElementById("content"); 3const menuToggle = document.querySelector(".menu-toggle"); 4const sidebar = document.querySelector(".sidebar"); 5const overlay = document.querySelector(".overlay"); 6 7function toggleMenu(open) { 8 const isOpen = open ?? !sidebar.classList.contains("open"); 9 sidebar.classList.toggle("open", isOpen); 10 overlay?.classList.toggle("open", isOpen); 11 menuToggle?.setAttribute("aria-expanded", isOpen); 12 document.body.style.overflow = isOpen ? "hidden" : ""; 13} 14 15menuToggle?.addEventListener("click", () => toggleMenu()); 16overlay?.addEventListener("click", () => toggleMenu(false)); 17 18// Close menu when nav link clicked (mobile) 19navEl?.addEventListener("click", (e) => { 20 if (e.target.closest("a")) toggleMenu(false); 21}); 22 23const buildId = new URL(import.meta.url).searchParams.get("v") || ""; 24 25function withBuild(url) { 26 if (!buildId) return url; 27 const sep = url.includes("?") ? "&" : "?"; 28 return `${url}${sep}v=${encodeURIComponent(buildId)}`; 29} 30 31function escapeHtml(text) { 32 return text 33 .replaceAll("&", "&amp;") 34 .replaceAll("<", "&lt;") 35 .replaceAll(">", "&gt;") 36 .replaceAll('"', "&quot;") 37 .replaceAll("'", "&#039;"); 38} 39 40function normalizeDocPath(docPath) { 41 let p = String(docPath || "").trim(); 42 p = p.replaceAll("\\", "/"); 43 p = p.replace(/^\/+/, ""); 44 p = p.replace(/\.\.\//g, ""); 45 if (!p.endsWith(".md")) p += ".md"; 46 return p; 47} 48 49function getSelectedPath() { 50 const hash = (location.hash || "").replace(/^#/, ""); 51 if (!hash) return null; 52 return normalizeDocPath(hash); 53} 54 55function setSelectedPath(docPath) { 56 location.hash = normalizeDocPath(docPath); 57} 58 59async function fetchJson(path) { 60 const res = await fetch(withBuild(path), { cache: "no-store" }); 61 if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`); 62 return res.json(); 63} 64 65async function fetchText(path) { 66 const res = await fetch(withBuild(path), { cache: "no-store" }); 67 if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`); 68 return res.text(); 69} 70 71function renderNav(pages, activePath) { 72 if (!pages.length) { 73 navEl.innerHTML = ""; 74 return; 75 } 76 77 navEl.innerHTML = pages 78 .map((p) => { 79 const path = normalizeDocPath(p.path); 80 const title = escapeHtml(p.title || path); 81 const current = activePath === path ? ` aria-current="page"` : ""; 82 return `<a href="#${encodeURIComponent(path)}"${current}>${title}</a>`; 83 }) 84 .join(""); 85} 86 87function installContentLinkHandler() { 88 contentEl.addEventListener("click", (e) => { 89 const a = e.target?.closest?.("a"); 90 if (!a) return; 91 92 const href = a.getAttribute("href") || ""; 93 if ( 94 href.startsWith("http://") || 95 href.startsWith("https://") || 96 href.startsWith("mailto:") || 97 href.startsWith("#") 98 ) { 99 return; 100 } 101 102 // Route relative markdown links through the SPA. 103 if (href.endsWith(".md")) { 104 e.preventDefault(); 105 setSelectedPath(href); 106 return; 107 } 108 }); 109} 110 111async function main() { 112 if (!globalThis.marked) { 113 contentEl.innerHTML = `<p class="empty">Markdown renderer failed to load.</p>`; 114 return; 115 } 116 117 installContentLinkHandler(); 118 119 let manifest; 120 try { 121 manifest = await fetchJson("./manifest.json"); 122 } catch (e) { 123 contentEl.innerHTML = `<p class="empty">Missing <code>manifest.json</code>. Deploy the site via CI.</p>`; 124 navEl.innerHTML = ""; 125 console.error(e); 126 return; 127 } 128 129 const pages = Array.isArray(manifest.pages) ? manifest.pages : []; 130 const defaultPath = pages[0]?.path ? normalizeDocPath(pages[0].path) : null; 131 132 async function render() { 133 const activePath = getSelectedPath() || defaultPath; 134 renderNav(pages, activePath); 135 136 if (!activePath) { 137 contentEl.innerHTML = `<p class="empty">No docs yet. Add markdown files under <code>zat/docs/</code> and push to <code>main</code>.</p>`; 138 return; 139 } 140 141 try { 142 const md = await fetchText(`./docs/${activePath}`); 143 const html = globalThis.marked.parse(md); 144 contentEl.innerHTML = html; 145 146 // Update current marker after navigation re-render. 147 for (const a of navEl.querySelectorAll("a")) { 148 const href = decodeURIComponent((a.getAttribute("href") || "").slice(1)); 149 a.toggleAttribute("aria-current", normalizeDocPath(href) === activePath); 150 } 151 } catch (e) { 152 contentEl.innerHTML = `<p class="empty">Failed to load <code>${escapeHtml( 153 activePath, 154 )}</code>.</p>`; 155 console.error(e); 156 } 157 } 158 159 window.addEventListener("hashchange", () => render()); 160 161 if (!getSelectedPath() && defaultPath) setSelectedPath(defaultPath); 162 await render(); 163} 164 165main();