#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; const root = process.cwd(); const docsDir = path.join(root, "docs"); const outDir = path.join(root, "dist", "docs-site"); const repoEditBase = "https://github.com/openclaw/discrawl/edit/main/docs"; const siteUrl = "https://discrawl.sh"; const sections = [ ["Start", ["README.md", "install.md", "configuration.md", "bot-setup.md", "security.md", "contact.md"]], ["Guides", rels("guides")], ["Commands", rels("commands")], ]; fs.rmSync(outDir, { recursive: true, force: true }); fs.mkdirSync(outDir, { recursive: true }); const pages = allMarkdown(docsDir).map((file) => { const rel = path.relative(docsDir, file).replaceAll(path.sep, "/"); const markdown = fs.readFileSync(file, "utf8"); const title = firstHeading(markdown) || titleize(path.basename(rel, ".md")); return { file, rel, title, outRel: outPath(rel), markdown }; }); const pageMap = new Map(pages.map((page) => [page.rel, page])); const nav = sections .map(([name, rels]) => ({ name, pages: rels.map((rel) => pageMap.get(rel)).filter(Boolean), })) .filter((section) => section.pages.length); const sectionByRel = new Map(); for (const section of nav) for (const page of section.pages) sectionByRel.set(page.rel, section.name); const orderedPages = nav.flatMap((s) => s.pages); for (const page of pages) { const html = markdownToHtml(page.markdown, page.rel); const toc = tocFromHtml(html); const idx = orderedPages.findIndex((p) => p.rel === page.rel); const prev = idx > 0 ? orderedPages[idx - 1] : null; const next = idx >= 0 && idx < orderedPages.length - 1 ? orderedPages[idx + 1] : null; const sectionName = sectionByRel.get(page.rel) || "Discrawl docs"; const pageOut = path.join(outDir, page.outRel); fs.mkdirSync(path.dirname(pageOut), { recursive: true }); fs.writeFileSync(pageOut, layout({ page, html, toc, prev, next, sectionName }), "utf8"); } fs.writeFileSync(path.join(outDir, "discrawl.svg"), discrawlSvg(), "utf8"); fs.copyFileSync(path.join(docsDir, "social-card.png"), path.join(outDir, "social-card.png")); fs.writeFileSync(path.join(outDir, "CNAME"), "discrawl.sh\n", "utf8"); fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8"); console.log(`built docs site: ${path.relative(root, outDir)}`); function rels(dir) { const full = path.join(docsDir, dir); if (!fs.existsSync(full)) return []; return fs .readdirSync(full) .filter((name) => name.endsWith(".md")) .sort((a, b) => (a === "README.md" ? -1 : b === "README.md" ? 1 : a.localeCompare(b))) .map((name) => `${dir}/${name}`); } function allMarkdown(dir) { return fs .readdirSync(dir, { withFileTypes: true }) .flatMap((entry) => { const full = path.join(dir, entry.name); if (entry.isDirectory()) return allMarkdown(full); return entry.name.endsWith(".md") ? [full] : []; }) .sort(); } function outPath(rel) { if (rel === "README.md") return "index.html"; if (rel.endsWith("/README.md")) return rel.replace(/README\.md$/, "index.html"); return rel.replace(/\.md$/, ".html"); } function firstHeading(markdown) { return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim(); } function titleize(input) { return input.replaceAll("-", " ").replace(/\b\w/g, (m) => m.toUpperCase()); } function markdownToHtml(markdown, currentRel) { const lines = markdown.replace(/\r\n/g, "\n").split("\n"); const html = []; let paragraph = []; let list = null; let fence = null; const flushParagraph = () => { if (!paragraph.length) return; html.push(`

${inline(paragraph.join(" "), currentRel)}

`); paragraph = []; }; const closeList = () => { if (!list) return; html.push(``); list = null; }; const splitRow = (line) => line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((s) => s.trim()); const isDivider = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(line); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const fenceMatch = line.match(/^```(\w+)?\s*$/); if (fenceMatch) { flushParagraph(); closeList(); if (fence) { html.push(`
${escapeHtml(fence.lines.join("\n"))}
`); fence = null; } else { fence = { lang: fenceMatch[1] || "text", lines: [] }; } continue; } if (fence) { fence.lines.push(line); continue; } if (!line.trim()) { flushParagraph(); closeList(); continue; } const heading = line.match(/^(#{1,4})\s+(.+)$/); if (heading) { flushParagraph(); closeList(); const level = heading[1].length; const text = heading[2].trim(); const id = slug(text); const inner = inline(text, currentRel); if (level === 1) { html.push(`

${inner}

`); } else { html.push(`#${inner}`); } continue; } if (line.trimStart().startsWith("|") && line.includes("|", line.indexOf("|") + 1) && isDivider(lines[i + 1] || "")) { flushParagraph(); closeList(); const header = splitRow(line); const aligns = splitRow(lines[i + 1]).map((cell) => { const left = cell.startsWith(":"); const right = cell.endsWith(":"); return right && left ? "center" : right ? "right" : left ? "left" : ""; }); i += 1; const rows = []; while (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("|")) { i += 1; rows.push(splitRow(lines[i])); } const th = header.map((c, idx) => `${inline(c, currentRel)}`).join(""); const tb = rows.map((r) => `${r.map((c, idx) => `${inline(c, currentRel)}`).join("")}`).join(""); html.push(`${th}${tb}
`); continue; } const bullet = line.match(/^\s*-\s+(.+)$/); const numbered = line.match(/^\s*\d+\.\s+(.+)$/); if (bullet || numbered) { flushParagraph(); const tag = bullet ? "ul" : "ol"; if (list && list !== tag) closeList(); if (!list) { list = tag; html.push(`<${tag}>`); } html.push(`
  • ${inline((bullet || numbered)[1], currentRel)}
  • `); continue; } paragraph.push(line.trim()); } flushParagraph(); closeList(); return html.join("\n"); } function inline(text, currentRel) { const stash = []; let out = text.replace(/`([^`]+)`/g, (_, code) => { stash.push(`${escapeHtml(code)}`); return `\u0000${stash.length - 1}\u0000`; }); out = escapeHtml(out) .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `${label}`); return out.replace(/\u0000(\d+)\u0000/g, (_, i) => stash[Number(i)]); } function rewriteHref(href, currentRel) { if (/^(https?:|mailto:|#)/.test(href)) return href; const [raw, hash = ""] = href.split("#"); if (!raw) return `#${hash}`; if (!raw.endsWith(".md")) return href; const from = path.posix.dirname(currentRel); const target = path.posix.normalize(path.posix.join(from, raw)); let rewritten = outPath(target); const currentOut = outPath(currentRel); rewritten = path.posix.relative(path.posix.dirname(currentOut), rewritten) || "index.html"; return `${rewritten}${hash ? `#${hash}` : ""}`; } function tocFromHtml(html) { const items = []; const re = /([\s\S]*?)<\/h[23]>/g; let m; while ((m = re.exec(html))) { const text = m[3] .replace(/]*>.*?<\/a>/, "") .replace(/<[^>]+>/g, "") .trim(); items.push({ level: Number(m[1]), id: m[2], text }); } if (items.length < 2) return ""; return ``; } function layout({ page, html, toc, prev, next, sectionName }) { const depth = page.outRel.split("/").length - 1; const rootPrefix = depth ? "../".repeat(depth) : ""; const canonicalUrl = absolutePageUrl(page); const editUrl = `${repoEditBase}/${page.rel}`; const isHome = page.rel === "README.md"; const prevNext = !isHome && (prev || next) ? pageNavHtml(prev, next, rootPrefix) : ""; const heroBlock = isHome ? landingHero(rootPrefix) : standardHero(page, sectionName, editUrl); const articleClass = isHome ? "doc doc-home" : "doc"; const tocBlock = isHome ? "" : toc; return ` ${escapeHtml(isHome ? "Discrawl" : `${page.title} - Discrawl`)}
    ${heroBlock}
    ${html}${prevNext}
    ${tocBlock}
    `; } function absolutePageUrl(page) { if (page.outRel === "index.html") return `${siteUrl}/`; return `${siteUrl}/${page.outRel}`; } function standardHero(page, sectionName, editUrl) { return `

    ${escapeHtml(sectionName.toLowerCase())}

    ${escapeHtml(page.title)}

    `; } function landingHero(rootPrefix) { const features = [ ["bot api sync", "Fan out across every guild a bot can see. Channels, threads, members, attachments, mentions, FTS5 - all into one SQLite file."], ["desktop wiretap", "Read local Discord Desktop cache for classifiable messages and proven DMs. No user token. No selfbot. Auth tokens never extracted."], ["fts + semantic", "unicode61 tokenizer for fast literal search. Optional embeddings (OpenAI, Ollama) for semantic and hybrid recall."], ["git-backed mirrors", "Publish a sharded NDJSON snapshot to a private repo. Readers subscribe, search offline, and never need a bot token."], ["live tail", "Gateway tail keeps the archive warm. Periodic repair sweeps catch anything the live stream missed."], ["offline analysis", "digest, analytics, members, raw read-only sql against the local archive."], ]; const cards = features .map(([title, body]) => `

    ${escapeHtml(title)}

    ${body}

    `) .join(""); return `

    discord -> sqlite -> answers

    Server history you can actually search.

    Discrawl mirrors Discord guilds into local SQLite so you can grep, query, and run analytics on org memory without depending on Discord search. Bring a bot token, or read everything offline from a Git snapshot.

    ${cards}
    `; } function pageNavHtml(prev, next, rootPrefix) { const cell = (page, dir) => { if (!page) return ""; return `${dir === "prev" ? "<- prev" : "next ->"}${escapeHtml(page.title)}`; }; return ``; } function navHtml(currentRel, rootPrefix) { return nav .map((section) => `

    ${section.name.toLowerCase()}

    ${section.pages.map((page) => { const href = rootPrefix + page.outRel; const active = page.rel === currentRel ? " active" : ""; return `${escapeHtml(page.title)}`; }).join("")}
    `) .join(""); } function css() { return ` :root{--bg:#0c0f14;--panel:#11151c;--panel-2:#161b24;--ink:#e6ecf3;--ink-dim:#aab3c1;--muted:#6b7585;--line:#1f2530;--line-soft:#262d39;--cyan:#5fe3d4;--magenta:#f364a2;--amber:#f7c177;--violet:#a594ff;--shadow:0 14px 40px rgba(0,0,0,.45)} @media (prefers-color-scheme: light){:root{--bg:#f4f6fa;--panel:#ffffff;--panel-2:#f8fafc;--ink:#0f172a;--ink-dim:#3f4a5c;--muted:#64748b;--line:#dde3ec;--line-soft:#e7ecf3;--cyan:#0d9488;--magenta:#c026d3;--amber:#b45309;--violet:#6d28d9;--shadow:0 10px 30px rgba(15,23,42,.08)}} *{box-sizing:border-box} html{scroll-behavior:smooth;scroll-padding-top:24px} body{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.65;overflow-x:hidden;-webkit-font-smoothing:antialiased;font-feature-settings:"ss01","cv11"} body:before{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(1200px 600px at 90% -20%,rgba(95,227,212,.08),transparent 60%),radial-gradient(900px 500px at -10% 110%,rgba(243,100,162,.07),transparent 60%);z-index:0} ::selection{background:var(--magenta);color:var(--bg)} a{color:var(--cyan);text-decoration:none;border-bottom:1px solid transparent;transition:color .15s,border-color .15s} a:hover{color:var(--magenta);border-bottom-color:var(--magenta)} .shell{position:relative;z-index:1;display:grid;grid-template-columns:264px minmax(0,1fr);min-height:100vh} /* sidebar */ .sidebar{position:sticky;top:0;height:100vh;overflow:auto;padding:22px 18px 14px;background:var(--panel);border-right:1px solid var(--line);scrollbar-width:thin;scrollbar-color:var(--line) transparent;display:flex;flex-direction:column} .sidebar::-webkit-scrollbar{width:6px} .sidebar::-webkit-scrollbar-thumb{background:var(--line);border-radius:6px} .brand{display:flex;align-items:center;gap:10px;color:var(--ink);text-decoration:none;border:0;margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid var(--line)} .brand:hover{color:var(--ink)} .brand img{width:32px;height:32px;border-radius:7px} .brand strong{display:block;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:1.05rem;line-height:1;letter-spacing:-.01em;font-weight:700} .brand small{display:block;color:var(--muted);font-size:.66rem;margin-top:5px;font-family:"JetBrains Mono",ui-monospace,monospace;letter-spacing:.06em} .search{display:block;margin:0 0 18px} .search span{display:block;color:var(--muted);font-size:.62rem;font-weight:600;text-transform:uppercase;letter-spacing:.18em;margin-bottom:6px;font-family:"JetBrains Mono",ui-monospace,monospace} .search input{width:100%;border:1px solid var(--line);background:var(--panel-2);border-radius:6px;padding:8px 10px;font:500 .82rem/1.4 "JetBrains Mono",ui-monospace,monospace;color:var(--ink);outline:none;transition:border-color .15s,box-shadow .15s} .search input::placeholder{color:var(--muted)} .search input:focus{border-color:var(--cyan);box-shadow:0 0 0 2px rgba(95,227,212,.15)} nav{flex:1} nav section{margin:0 0 18px} nav h2{font-size:.6rem;color:var(--muted);text-transform:uppercase;letter-spacing:.2em;margin:0 0 6px;font-weight:700;font-family:"JetBrains Mono",ui-monospace,monospace;padding:0 4px} .nav-link{display:block;color:var(--ink-dim);text-decoration:none;border:0;border-radius:5px;padding:5px 10px;margin:1px 0;font-size:.86rem;line-height:1.4;font-family:"JetBrains Mono",ui-monospace,monospace;transition:background .12s,color .12s} .nav-link:hover{background:var(--panel-2);color:var(--cyan)} .nav-link.active{background:linear-gradient(90deg,rgba(95,227,212,.14),rgba(95,227,212,.04));color:var(--cyan);position:relative} .nav-link.active:before{content:"";position:absolute;left:-1px;top:6px;bottom:6px;width:2px;background:var(--cyan);border-radius:2px} .side-foot{display:flex;gap:14px;padding-top:14px;margin-top:8px;border-top:1px solid var(--line);font-size:.74rem;font-family:"JetBrains Mono",ui-monospace,monospace} .side-foot a{color:var(--muted);border:0} .side-foot a:hover{color:var(--magenta)} /* main */ main{min-width:0;padding:30px clamp(20px,4.5vw,56px) 80px;max-width:1180px;margin:0 auto;width:100%;position:relative} .hero{display:flex;align-items:flex-end;justify-content:space-between;gap:22px;border-bottom:1px solid var(--line);padding:14px 0 20px;position:relative;flex-wrap:wrap} .hero:after{content:"";position:absolute;left:0;bottom:-1px;width:64px;height:2px;background:linear-gradient(90deg,var(--cyan),var(--magenta));border-radius:2px} .hero-text{min-width:0;flex:1 1 320px} .eyebrow{margin:0 0 10px;color:var(--magenta);font-weight:700;text-transform:uppercase;letter-spacing:.18em;font-size:.66rem;font-family:"JetBrains Mono",ui-monospace,monospace} .hero h1{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:clamp(1.8rem,3.2vw,2.6rem);line-height:1.05;letter-spacing:-.02em;margin:0;font-weight:700;color:var(--ink)} .hero-meta{display:flex;gap:6px;flex:0 0 auto} .repo,.edit{border:1px solid var(--line);color:var(--ink-dim);text-decoration:none;border-radius:6px;padding:6px 12px;font-weight:500;font-size:.78rem;background:var(--panel);font-family:"JetBrains Mono",ui-monospace,monospace;transition:border-color .15s,color .15s} .repo:hover,.edit:hover{border-color:var(--cyan);color:var(--cyan)} /* landing hero */ .hero-home{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);gap:40px;align-items:center;border-bottom:0;padding:32px 0 14px} .hero-home:after{display:none} .hero-home .eyebrow{margin-bottom:16px;color:var(--cyan)} .hero-home h1{font-size:clamp(2.1rem,4.6vw,3.6rem);line-height:1.02;letter-spacing:-.025em;font-weight:700;margin:0 0 18px;max-width:18ch} .hero-home h1 em{font-style:normal;color:var(--magenta);font-weight:700;background:linear-gradient(180deg,transparent 60%,rgba(243,100,162,.18) 60%);padding:0 .12em} .lede{margin:0 0 24px;color:var(--ink-dim);font-size:clamp(1rem,1.2vw,1.06rem);line-height:1.6;max-width:48ch} .cta{display:flex;gap:10px;flex-wrap:wrap} .cta-primary,.cta-secondary{display:inline-flex;align-items:center;border-radius:7px;padding:10px 18px;font-weight:600;font-size:.86rem;text-decoration:none;font-family:"JetBrains Mono",ui-monospace,monospace;transition:transform .15s,box-shadow .15s,background .15s,border-color .15s,color .15s;border:1px solid transparent} .cta-primary{background:var(--cyan);color:var(--bg);border-color:var(--cyan)} .cta-primary:hover{background:var(--magenta);border-color:var(--magenta);color:var(--bg);transform:translateY(-1px);box-shadow:0 8px 22px rgba(243,100,162,.25)} .cta-secondary{border-color:var(--line);color:var(--ink);background:transparent} .cta-secondary:hover{border-color:var(--cyan);color:var(--cyan);transform:translateY(-1px)} .hero-snippet{margin:0;background:var(--panel);color:var(--ink);border-radius:10px;padding:20px 22px;font:500 .84rem/1.7 "JetBrains Mono",ui-monospace,monospace;border:1px solid var(--line);box-shadow:var(--shadow);overflow:hidden;position:relative} .hero-snippet:before{content:"$ wiretap";position:absolute;top:8px;right:14px;font-size:.62rem;color:var(--muted);letter-spacing:.14em;text-transform:uppercase} .hero-snippet code{background:transparent;border:0;padding:0;color:inherit;font:inherit;display:block;white-space:pre} .hero-snippet .prompt{color:var(--cyan)} .hero-snippet .comment{color:var(--muted)} /* feature grid */ .features{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin:32px 0 8px} .feature{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:18px 18px 16px;transition:border-color .15s,transform .15s,box-shadow .15s;position:relative;overflow:hidden} .feature:before{content:"";position:absolute;top:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,var(--cyan),var(--magenta));opacity:0;transition:opacity .15s} .feature:hover{border-color:var(--cyan);transform:translateY(-2px);box-shadow:var(--shadow)} .feature:hover:before{opacity:1} .feature h3{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:.95rem;margin:0 0 8px;font-weight:600;letter-spacing:-.01em;line-height:1.2;color:var(--ink)} .feature p{margin:0;color:var(--ink-dim);font-size:.9rem;line-height:1.55} .feature code{font-size:.86em;background:var(--panel-2);border:1px solid var(--line-soft);border-radius:4px;padding:.04em .3em;color:var(--cyan)} /* layout: doc + toc */ .doc-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:36px;margin-top:32px} .doc-grid-home{margin-top:14px} .doc-home{background:transparent;box-shadow:none;border:0;padding:8px clamp(18px,3vw,30px) 0;max-width:74ch;margin-inline:auto;width:100%} .doc-home>:first-child{margin-top:0} @media(min-width:1180px){.doc-grid{grid-template-columns:minmax(0,72ch) 200px;justify-content:start}.doc-grid-home{grid-template-columns:minmax(0,1fr)}} .doc{min-width:0;max-width:74ch;background:var(--panel);box-shadow:var(--shadow);border:1px solid var(--line);border-radius:10px;padding:clamp(22px,3.6vw,42px);overflow-wrap:break-word} .doc-home{max-width:none} .doc h1{display:none} .doc h2{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:1.45rem;line-height:1.2;margin:1.9em 0 .6em;font-weight:600;letter-spacing:-.015em;position:relative;color:var(--ink)} .doc h3{font-size:1.08rem;margin:1.6em 0 .35em;position:relative;font-weight:600;font-family:"JetBrains Mono",ui-monospace,monospace;color:var(--ink)} .doc h4{font-size:.92rem;margin:1.3em 0 .2em;color:var(--cyan);position:relative;font-weight:600;font-family:"JetBrains Mono",ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em} .doc h2:first-child,.doc h3:first-child,.doc h4:first-child{margin-top:0} .doc :is(h2,h3,h4) .anchor{position:absolute;left:-1em;top:0;color:var(--muted);opacity:0;text-decoration:none;font-weight:400;padding-right:.3em;transition:opacity .12s,color .12s;border:0} .doc :is(h2,h3,h4):hover .anchor{opacity:.55} .doc :is(h2,h3,h4) .anchor:hover{opacity:1;color:var(--magenta)} .doc p{margin:0 0 1.05em;color:var(--ink-dim)} .doc ul,.doc ol{padding-left:1.35rem;margin:0 0 1.2em;color:var(--ink-dim)} .doc li{margin:.25em 0} .doc li>p{margin:0 0 .4em} .doc strong{font-weight:600;color:var(--ink)} .doc code{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:.84em;background:var(--panel-2);border:1px solid var(--line-soft);border-radius:4px;padding:.08em .34em;color:var(--cyan)} .doc pre{position:relative;overflow:auto;background:var(--panel-2);color:var(--ink);border-radius:8px;padding:18px 22px;border:1px solid var(--line);margin:1.35em 0;font-size:.86em;scrollbar-width:thin;scrollbar-color:var(--line) transparent} .doc pre::-webkit-scrollbar{height:8px} .doc pre::-webkit-scrollbar-thumb{background:var(--line);border-radius:8px} .doc pre code{display:block;background:transparent;border:0;color:inherit;padding:0;font-size:1em;white-space:pre-wrap;overflow-wrap:anywhere} .doc pre .copy{position:absolute;top:8px;right:8px;background:var(--panel);color:var(--ink-dim);border:1px solid var(--line);border-radius:5px;padding:3px 9px;font:600 .68rem/1 "JetBrains Mono",monospace;cursor:pointer;opacity:0;transition:opacity .15s,background .15s,border-color .15s,color .15s} .doc pre:hover .copy,.doc pre .copy:focus{opacity:1} .doc pre .copy:hover{border-color:var(--cyan);color:var(--cyan)} .doc pre .copy.copied{background:var(--cyan);border-color:var(--cyan);color:var(--bg);opacity:1} .doc blockquote{margin:1.4em 0;padding:12px 16px;border-left:2px solid var(--magenta);background:var(--panel-2);border-radius:0 6px 6px 0;color:var(--ink-dim)} .doc blockquote p:last-child{margin-bottom:0} .doc table{width:100%;border-collapse:collapse;margin:1.2em 0;font-size:.92em} .doc th,.doc td{border-bottom:1px solid var(--line);padding:9px 10px;text-align:left} .doc th{font-weight:600;color:var(--cyan);font-family:"JetBrains Mono",ui-monospace,monospace;font-size:.85em;text-transform:uppercase;letter-spacing:.06em} .doc td{color:var(--ink-dim)} .doc hr{border:0;border-top:1px solid var(--line);margin:2em 0} /* toc */ .toc{position:sticky;top:24px;align-self:start;font-size:.82rem;padding-left:14px;border-left:1px solid var(--line);max-height:calc(100vh - 48px);overflow:auto;scrollbar-width:thin;scrollbar-color:var(--line) transparent;font-family:"JetBrains Mono",ui-monospace,monospace} .toc::-webkit-scrollbar{width:5px} .toc::-webkit-scrollbar-thumb{background:var(--line);border-radius:5px} .toc h2{font-size:.6rem;color:var(--muted);text-transform:uppercase;letter-spacing:.2em;margin:0 0 10px;font-weight:700} .toc a{display:block;color:var(--muted);text-decoration:none;padding:4px 0 4px 10px;line-height:1.4;border-left:2px solid transparent;margin-left:-12px;transition:color .12s,border-color .12s;border-bottom:0} .toc a:hover{color:var(--cyan)} .toc a.active{color:var(--cyan);border-left-color:var(--cyan);font-weight:600} .toc-l3{padding-left:22px!important;font-size:.92em} @media(max-width:1179px){.toc{display:none}} /* prev/next pager */ .page-nav{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:48px} .page-nav>a{display:block;border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:14px 18px;text-decoration:none;color:var(--ink);transition:border-color .15s,transform .15s,box-shadow .15s;border-bottom:1px solid var(--line)} .page-nav>a:hover{border-color:var(--cyan);transform:translateY(-1px);box-shadow:var(--shadow)} .page-nav small{display:block;color:var(--muted);font-size:.66rem;text-transform:uppercase;letter-spacing:.16em;margin-bottom:5px;font-weight:700;font-family:"JetBrains Mono",ui-monospace,monospace} .page-nav span{display:block;font-weight:600;line-height:1.3;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:.92rem} .page-nav-prev{text-align:left} .page-nav-next{text-align:right;grid-column:2} .page-nav-prev:only-child{grid-column:1} /* mobile nav toggle */ .nav-toggle{display:none;position:fixed;top:14px;right:14px;top:calc(14px + env(safe-area-inset-top, 0px));right:calc(14px + env(safe-area-inset-right, 0px));z-index:20;width:40px;height:40px;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--ink);cursor:pointer;padding:10px 9px;flex-direction:column;align-items:stretch;justify-content:space-between;box-shadow:var(--shadow)} .nav-toggle span{display:block;width:100%;height:2px;flex:0 0 2px;background:currentColor;border-radius:2px;transition:transform .2s,opacity .2s} .nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(8px) rotate(45deg)} .nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0} .nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-8px) rotate(-45deg)} /* mobile */ @media(max-width:900px){ .shell{display:block} .sidebar{position:fixed;inset:0 30% 0 0;max-width:300px;height:100vh;z-index:15;transform:translateX(-100%);transition:transform .25s ease;box-shadow:var(--shadow);background:var(--panel);pointer-events:none} .sidebar.open{transform:translateX(0);pointer-events:auto} .nav-toggle{display:flex} main{padding:64px 18px 56px} .hero{padding-top:8px} .hero h1{font-size:clamp(1.6rem,7vw,2rem)} .hero-meta{width:100%;justify-content:flex-start} .hero-home{grid-template-columns:1fr;gap:24px;padding-top:8px} .hero-home h1{font-size:clamp(1.95rem,8vw,2.5rem);max-width:none} .hero-snippet{font-size:.76rem;padding:16px 16px} .features{grid-template-columns:1fr;margin-top:22px} .doc{padding:20px;border-radius:8px} .doc-home{padding:0 18px} .doc-grid{margin-top:22px;gap:24px} .doc :is(h2,h3,h4) .anchor{display:none} } @media(max-width:520px){ main{padding:60px 14px 48px} .doc{padding:18px 16px} .doc-home{padding-inline:16px} .doc pre{margin-left:-16px;margin-right:-16px;border-radius:0;border-left:0;border-right:0} } `; } function js() { return ` const sidebar=document.querySelector('.sidebar'); const toggle=document.querySelector('.nav-toggle'); const mobileNav=window.matchMedia('(max-width: 900px)'); const sidebarFocusable='a[href],button,input,select,textarea,[tabindex]'; function setSidebarFocusable(enabled){ sidebar?.querySelectorAll(sidebarFocusable).forEach((el)=>{ if(enabled){ if(el.dataset.sidebarTabindex!==undefined){ if(el.dataset.sidebarTabindex)el.setAttribute('tabindex',el.dataset.sidebarTabindex); else el.removeAttribute('tabindex'); delete el.dataset.sidebarTabindex; } }else if(el.dataset.sidebarTabindex===undefined){ el.dataset.sidebarTabindex=el.getAttribute('tabindex')??''; el.setAttribute('tabindex','-1'); } }); } function setSidebarOpen(open){ if(!sidebar||!toggle)return; sidebar.classList.toggle('open',open); toggle.setAttribute('aria-expanded',open?'true':'false'); if(mobileNav.matches){ sidebar.inert=!open; if(open)sidebar.removeAttribute('aria-hidden'); else sidebar.setAttribute('aria-hidden','true'); setSidebarFocusable(open); }else{ sidebar.inert=false; sidebar.removeAttribute('aria-hidden'); setSidebarFocusable(true); } } setSidebarOpen(false); toggle?.addEventListener('click',()=>setSidebarOpen(!sidebar?.classList.contains('open'))); document.addEventListener('click',(e)=>{if(!sidebar?.classList.contains('open'))return;if(sidebar.contains(e.target)||toggle?.contains(e.target))return;setSidebarOpen(false)}); document.addEventListener('keydown',(e)=>{if(e.key==='Escape')setSidebarOpen(false)}); const syncSidebarForViewport=()=>setSidebarOpen(sidebar?.classList.contains('open')??false); if(mobileNav.addEventListener)mobileNav.addEventListener('change',syncSidebarForViewport); else mobileNav.addListener?.(syncSidebarForViewport); const input=document.getElementById('doc-search'); input?.addEventListener('input',()=>{const q=input.value.trim().toLowerCase();document.querySelectorAll('nav section').forEach(sec=>{let any=false;sec.querySelectorAll('.nav-link').forEach(a=>{const m=!q||a.textContent.toLowerCase().includes(q);a.style.display=m?'block':'none';if(m)any=true});sec.style.display=any?'block':'none'})}); document.querySelectorAll('.doc pre').forEach(pre=>{const btn=document.createElement('button');btn.type='button';btn.className='copy';btn.textContent='copy';btn.addEventListener('click',async()=>{const code=pre.querySelector('code')?.textContent??'';try{await navigator.clipboard.writeText(code);btn.textContent='copied';btn.classList.add('copied');setTimeout(()=>{btn.textContent='copy';btn.classList.remove('copied')},1400)}catch{btn.textContent='failed';setTimeout(()=>{btn.textContent='copy'},1400)}});pre.appendChild(btn)}); const tocLinks=document.querySelectorAll('.toc a'); if(tocLinks.length){const map=new Map();tocLinks.forEach(a=>{const id=a.getAttribute('href').slice(1);const el=document.getElementById(id);if(el)map.set(el,a)});const setActive=l=>{tocLinks.forEach(x=>x.classList.remove('active'));l.classList.add('active')};const obs=new IntersectionObserver(entries=>{const visible=entries.filter(e=>e.isIntersecting).sort((a,b)=>a.boundingClientRect.top-b.boundingClientRect.top);if(visible.length){const link=map.get(visible[0].target);if(link)setActive(link)}},{rootMargin:'-15% 0px -65% 0px',threshold:0});map.forEach((_,el)=>obs.observe(el))} `; } function discrawlSvg() { return ` SELECT * FROM msgs _ `; } function slug(text) { return text.toLowerCase().replace(/`/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } function escapeHtml(value) { return String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]); } function escapeAttr(value) { return escapeHtml(value); }