#!/usr/bin/env nodeimport fs from "node:fs";import path from "node:path"; import { css, faviconSvg, js, themeInitJs, themeToggleHtml } from "./docs-site-assets.mjs"; const root = process.cwd();const docsDir = path.join(root, "docs");const outDir = path.join(root, "dist", "docs-site");const repoBase = "https://github.com/openclaw/gitcrawl";const repoEditBase = `${repoBase}/edit/main/docs`;const cname = readCname();const siteBase = cname ? `https://${cname}` : ""; const sections = [ ["Start", ["index.md", "installation.md", "quickstart.md", "concepts.md"]], ["Configure", ["configuration.md", "sync.md", "refresh-and-embed.md"]], ["Use", ["search.md", "clustering.md", "governance.md", "tui.md", "gh-shim.md"]], ["Operate", ["portable-stores.md", "maintainer-archive.md", "automation.md", "releasing.md"]], ["Reference", ["commands.md", "reference.md"]],]; 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 raw = fs.readFileSync(file, "utf8"); const { frontmatter, body } = parseFrontmatter(raw); const cleaned = cleanKramdown(body); const title = frontmatter.title || firstHeading(cleaned) || titleize(path.basename(rel, ".md")); return { file, rel, title, outRel: outPath(rel, frontmatter), markdown: cleaned, frontmatter };}); const pageMap = new Map(pages.map((page) => [page.rel, page]));const permalinkMap = new Map();for (const page of pages) { if (page.frontmatter.permalink) { permalinkMap.set(normalizePermalink(page.frontmatter.permalink), 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) || "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, "favicon.svg"), faviconSvg(), "utf8");copyStaticAsset("social-card.svg");copyStaticAsset("social-card.png");fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8");if (cname) fs.writeFileSync(path.join(outDir, "CNAME"), cname, "utf8");validateLinks(outDir);fs.writeFileSync(path.join(outDir, "llms.txt"), llmsTxt(), "utf8");console.log(`built docs site: ${path.relative(root, outDir)}`); function llmsTxt() { const origin = docsOrigin(); const source = docsSourceUrl(); const name = typeof productName !== "undefined" ? productName : path.basename(root); const description = typeof productDescription !== "undefined" ? productDescription : `${name} documentation index.`; const install = docsInstallHint(); const docPages = docsLlmsPages().map((page) => `- ${page.title}: ${pageUrl(origin, page.outRel)}`); const lines = [ `# ${name}`, "", description, "", "Canonical documentation:", ...docPages, ]; if (install) { lines.push("", "Install:", `- ${install}`); } if (source) { lines.push("", `Source: ${source}`); } lines.push("", "Guidance for agents:", "- Prefer the canonical documentation URLs above over README excerpts or package metadata.", "- Fetch only the pages needed for the current task; this is an index, not a full-site corpus."); return `${lines.join("\n")}\n`;} function docsLlmsPages() { const seen = new Set(); const ordered = typeof orderedPages !== "undefined" ? orderedPages : []; return [...ordered, ...pages].filter((page) => page.outRel && !seen.has(page.outRel) && seen.add(page.outRel));} function docsOrigin() { const value = (typeof siteBase !== "undefined" && siteBase) || (typeof siteUrl !== "undefined" && siteUrl) || (typeof customDomain !== "undefined" && customDomain ? `https://${customDomain}` : ""); return value.replace(/\/$/, "");} function docsSourceUrl() { if (typeof repoBase !== "undefined") return repoBase; if (typeof repoUrl !== "undefined") return repoUrl; if (typeof repoEditBase !== "undefined") return repoEditBase.replace(/\/edit\/main\/docs\/?$/, ""); return "";} function docsInstallHint() { if (typeof installCommand !== "undefined") return installCommand; if (typeof installLine !== "undefined") return installLine; if (typeof installCmd !== "undefined") return installCmd; if (typeof installSnippet !== "undefined") return installSnippet; if (typeof brewInstall !== "undefined") return brewInstall; return "";} function pageUrl(origin, outRel) { const normalized = outRel === "index.html" ? "" : outRel.replace(/(?:^|\/)index\.html$/, (match) => match === "index.html" ? "" : "/"); if (!origin) return normalized || "index.html"; return normalized ? `${origin}/${normalized}` : `${origin}/`;} function readCname() { for (const candidate of [path.join(docsDir, "CNAME"), path.join(root, "CNAME")]) { if (fs.existsSync(candidate)) return fs.readFileSync(candidate, "utf8").trim(); } return "";} function copyStaticAsset(name) { const source = path.join(docsDir, name); if (fs.existsSync(source)) fs.copyFileSync(source, path.join(outDir, name));} function parseFrontmatter(raw) { const match = raw.match(/^---\n([\s\S]*?)\n---\n?/); if (!match) return { frontmatter: {}, body: raw }; const fm = {}; for (const line of match[1].split("\n")) { const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/); if (!m) continue; let value = m[2]; if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } fm[m[1]] = value; } return { frontmatter: fm, body: raw.slice(match[0].length) };} function cleanKramdown(body) { const lines = body.replace(/\r\n/g, "\n").split("\n"); const out = []; let kramdownDivDepth = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (/^\s*\{:\s*[^}]*\}\s*$/.test(line)) continue; if (/^\s*1\.\s+TOC\s*$/.test(line) && /^\s*\{:toc\}\s*$/.test(lines[i + 1] || "")) { i += 1; continue; } if (/^\s*<div\b[^>]*\bmarkdown\s*=\s*"1"[^>]*>\s*$/.test(line)) { kramdownDivDepth++; continue; } if (kramdownDivDepth > 0 && /^\s*<\/div>\s*$/.test(line)) { kramdownDivDepth--; continue; } out.push(line.replace(/\s*\{:\s*[^}]*\}\s*$/, "")); } return out.join("\n");} function normalizePermalink(value) { let v = value.trim(); if (!v) return "/"; if (!v.startsWith("/")) v = `/${v}`; if (v.length > 1 && v.endsWith("/")) v = v.slice(0, -1); return v;} 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, frontmatter = {}) { if (frontmatter.permalink) { const permalink = normalizePermalink(frontmatter.permalink); if (permalink === "/") return "index.html"; return `${permalink.slice(1)}/index.html`; } if (rel === "index.md") return "index.html"; 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; let blockquote = []; const flushParagraph = () => { if (!paragraph.length) return; const text = paragraph.join(" "); const className = currentRel === "index.md" && /^\[Quickstart\]\([^)]*\)\s+\[View on GitHub\]\(/.test(text) ? ' class="home-actions"' : ""; html.push(`<p${className}>${inline(text, currentRel)}</p>`); paragraph = []; }; const closeList = () => { if (!list) return; html.push(`</${list}>`); list = null; }; const flushBlockquote = () => { if (!blockquote.length) return; const inner = markdownToHtml(blockquote.join("\n"), currentRel); html.push(`<blockquote>${inner}</blockquote>`); blockquote = []; }; const splitRow = (line) => { let trimmed = line.trim(); if (trimmed.startsWith("|")) trimmed = trimmed.slice(1); if (trimmed.endsWith("|") && !trimmed.endsWith("\\|")) trimmed = trimmed.slice(0, -1); const cells = []; let current = ""; for (let idx = 0; idx < trimmed.length; idx++) { const char = trimmed[idx]; if (char === "\\" && trimmed[idx + 1] === "|") { current += "\\|"; idx += 1; continue; } if (char === "|") { cells.push(current.trim().replace(/\\\|/g, "|")); current = ""; continue; } current += char; } cells.push(current.trim().replace(/\\\|/g, "|")); return cells; }; 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(); flushBlockquote(); if (fence) { html.push(`<pre><code class="language-${escapeAttr(fence.lang)}">${highlightCode(fence.lines.join("\n"), fence.lang)}</code></pre>`); fence = null; } else { fence = { lang: fenceMatch[1] || "text", lines: [] }; } continue; } if (fence) { fence.lines.push(line); continue; } if (/^>\s?/.test(line)) { flushParagraph(); closeList(); blockquote.push(line.replace(/^>\s?/, "")); continue; } flushBlockquote(); if (!line.trim()) { flushParagraph(); closeList(); continue; } if (/^\s*---+\s*$/.test(line)) { flushParagraph(); closeList(); html.push("<hr>"); 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(`<h1 id="${id}">${inner}</h1>`); } else { html.push(`<h${level} id="${id}"><a class="anchor" href="#${id}" aria-label="Anchor link">#</a>${inner}</h${level}>`); } 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) => `<th${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ""}>${inline(c, currentRel)}</th>`).join(""); const tb = rows.map((r) => `<tr>${r.map((c, idx) => `<td${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ""}>${inline(c, currentRel)}</td>`).join("")}</tr>`).join(""); html.push(`<table><thead><tr>${th}</tr></thead><tbody>${tb}</tbody></table>`); 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(`<li>${inline((bullet || numbered)[1], currentRel)}</li>`); continue; } paragraph.push(line.trim()); } flushParagraph(); closeList(); flushBlockquote(); return html.join("\n");} function highlightCode(code, lang) { const normalized = String(lang || "text").toLowerCase(); if (["bash", "sh", "shell", "zsh"].includes(normalized)) return highlightBash(code); if (normalized === "json") return highlightJSON(code); if (normalized === "toml") return highlightConfig(code, "toml"); if (["yaml", "yml"].includes(normalized)) return highlightConfig(code, "yaml"); if (normalized === "cron") return highlightCron(code); return escapeHtml(code);} function highlightBash(code) { return code.split("\n").map((line) => { if (/^\s*#/.test(line)) return span("comment", line); return highlightSegments(line, /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`[^`]*`|\$\{?[A-Za-z_][A-Za-z0-9_]*\}?|--?[A-Za-z0-9][A-Za-z0-9_-]*|\b(?:brew|case|cd|curl|do|done|else|esac|export|fi|for|gh|git|gitcrawl|go|if|in|jq|ln|local|mkdir|set|then|while)\b|#.*)/g, (token) => { if (token.startsWith("#")) return span("comment", token); if (/^["'`]/.test(token)) return span("string", token); if (token.startsWith("$")) return span("variable", token); if (token.startsWith("-")) return span("option", token); return span("keyword", token); }); }).join("\n");} function highlightJSON(code) { return highlightSegments(code, /("(?:\\.|[^"\\])*"\s*:)|("(?:\\.|[^"\\])*")|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g, (token) => { if (token.endsWith(":")) return `${span("key", token.slice(0, -1))}:`; if (token.startsWith('"')) return span("string", token); if (/^(?:true|false|null)$/.test(token)) return span("literal", token); return span("number", token); });} function highlightConfig(code, lang) { return code.split("\n").map((line) => { if (/^\s*#/.test(line)) return span("comment", line); const commentMatch = line.match(/(^|[^"'])#.*/); const commentStart = commentMatch ? commentMatch.index + commentMatch[1].length : -1; const body = commentStart >= 0 ? line.slice(0, commentStart) : line; const comment = commentStart >= 0 ? line.slice(commentStart) : ""; const highlighted = lang === "toml" ? highlightSegments(body, /(^\s*[A-Za-z0-9_.-]+(?=\s*=))|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\b(?:true|false)\b|-?\b\d+(?:\.\d+)?\b/g, configToken) : highlightSegments(body, /(^\s*[A-Za-z0-9_.-]+(?=\s*:))|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?\b/g, configToken); return highlighted + (comment ? span("comment", comment) : ""); }).join("\n");} function configToken(token) { if (/^\s*[A-Za-z0-9_.-]+$/.test(token)) { const leading = token.match(/^\s*/)[0]; return `${escapeHtml(leading)}${span("key", token.slice(leading.length))}`; } if (/^["']/.test(token)) return span("string", token); if (/^(?:true|false|null)$/.test(token)) return span("literal", token); return span("number", token);} function highlightCron(code) { return code.split("\n").map((line) => { if (/^\s*#/.test(line)) return span("comment", line); return highlightSegments(line, /(\*|(?:\d+)(?:[-/,]\d+)*)|("[^"]*"|'[^']*')|#.*|\b[A-Z_][A-Z0-9_]*=/g, (token) => { if (token.startsWith("#")) return span("comment", token); if (/^["']/.test(token)) return span("string", token); if (token.endsWith("=")) return span("key", token.slice(0, -1)) + "="; return span("number", token); }); }).join("\n");} function highlightSegments(text, pattern, classify) { let out = ""; let last = 0; for (const match of text.matchAll(pattern)) { out += escapeHtml(text.slice(last, match.index)); out += classify(match[0]); last = match.index + match[0].length; } return out + escapeHtml(text.slice(last));} function span(kind, value) { return `<span class="hl-${kind}">${escapeHtml(value)}</span>`;} function inline(text, currentRel) { const stash = []; let out = text.replace(/`([^`]+)`/g, (_, code) => { stash.push(`<code>${escapeHtml(code)}</code>`); return `\u0000${stash.length - 1}\u0000`; }); out = escapeHtml(out) .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") .replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, "$1<em>$2</em>") .replace(/(^|[^_])_([^_\s][^_]*?)_(?!_)/g, "$1<em>$2</em>") .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${escapeAttr(rewriteHref(href, currentRel))}">${label}</a>`) .replace(/<(https?:\/\/[^\s<>]+)>/g, '<a href="$1">$1</a>'); 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 ? `#${hash}` : ""; if (raw.startsWith("/")) { const target = permalinkMap.get(normalizePermalink(raw)); if (target) { const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel); const out = hrefToOutRel(target.outRel, currentOut); return hash ? `${out}#${hash}` : out; } return href; } if (!raw.endsWith(".md")) return href; const from = path.posix.dirname(currentRel); const target = path.posix.normalize(path.posix.join(from, raw)); let rewritten = pageMap.get(target)?.outRel || outPath(target); const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel); rewritten = hrefToOutRel(rewritten, currentOut); return `${rewritten}${hash ? `#${hash}` : ""}`;} function tocFromHtml(html) { const items = []; const re = /<h([23]) id="([^"]+)">([\s\S]*?)<\/h[23]>/g; let m; while ((m = re.exec(html))) { const text = htmlTextContent(m[3]).replace(/^#/, "").trim(); items.push({ level: Number(m[1]), id: m[2], text }); } if (items.length < 2) return ""; return `<nav class="toc" aria-label="On this page"><h2>On this page</h2>${itemsLines 1–500 of 714. Load more
#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { css, faviconSvg, js, themeInitJs, themeToggleHtml } from "./docs-site-assets.mjs"; const root = process.cwd(); const docsDir = path.join(root, "docs"); const outDir = path.join(root, "dist", "docs-site"); const repoBase = "https://github.com/openclaw/gitcrawl"; const repoEditBase = `${repoBase}/edit/main/docs`; const cname = readCname(); const siteBase = cname ? `https://${cname}` : ""; const sections = [ ["Start", ["index.md", "installation.md", "quickstart.md", "concepts.md"]], ["Configure", ["configuration.md", "sync.md", "refresh-and-embed.md"]], ["Use", ["search.md", "clustering.md", "governance.md", "tui.md", "gh-shim.md"]], ["Operate", ["portable-stores.md", "maintainer-archive.md", "automation.md", "releasing.md"]], ["Reference", ["commands.md", "reference.md"]], ]; 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 raw = fs.readFileSync(file, "utf8"); const { frontmatter, body } = parseFrontmatter(raw); const cleaned = cleanKramdown(body); const title = frontmatter.title || firstHeading(cleaned) || titleize(path.basename(rel, ".md")); return { file, rel, title, outRel: outPath(rel, frontmatter), markdown: cleaned, frontmatter }; }); const pageMap = new Map(pages.map((page) => [page.rel, page])); const permalinkMap = new Map(); for (const page of pages) { if (page.frontmatter.permalink) { permalinkMap.set(normalizePermalink(page.frontmatter.permalink), 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) || "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, "favicon.svg"), faviconSvg(), "utf8"); copyStaticAsset("social-card.svg"); copyStaticAsset("social-card.png"); fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8"); if (cname) fs.writeFileSync(path.join(outDir, "CNAME"), cname, "utf8"); validateLinks(outDir); fs.writeFileSync(path.join(outDir, "llms.txt"), llmsTxt(), "utf8"); console.log(`built docs site: ${path.relative(root, outDir)}`); function llmsTxt() { const origin = docsOrigin(); const source = docsSourceUrl(); const name = typeof productName !== "undefined" ? productName : path.basename(root); const description = typeof productDescription !== "undefined" ? productDescription : `${name} documentation index.`; const install = docsInstallHint(); const docPages = docsLlmsPages().map((page) => `- ${page.title}: ${pageUrl(origin, page.outRel)}`); const lines = [ `# ${name}`, "", description, "", "Canonical documentation:", ...docPages, ]; if (install) { lines.push("", "Install:", `- ${install}`); } if (source) { lines.push("", `Source: ${source}`); } lines.push("", "Guidance for agents:", "- Prefer the canonical documentation URLs above over README excerpts or package metadata.", "- Fetch only the pages needed for the current task; this is an index, not a full-site corpus."); return `${lines.join("\n")}\n`; } function docsLlmsPages() { const seen = new Set(); const ordered = typeof orderedPages !== "undefined" ? orderedPages : []; return [...ordered, ...pages].filter((page) => page.outRel && !seen.has(page.outRel) && seen.add(page.outRel)); } function docsOrigin() { const value = (typeof siteBase !== "undefined" && siteBase) || (typeof siteUrl !== "undefined" && siteUrl) || (typeof customDomain !== "undefined" && customDomain ? `https://${customDomain}` : ""); return value.replace(/\/$/, ""); } function docsSourceUrl() { if (typeof repoBase !== "undefined") return repoBase; if (typeof repoUrl !== "undefined") return repoUrl; if (typeof repoEditBase !== "undefined") return repoEditBase.replace(/\/edit\/main\/docs\/?$/, ""); return ""; } function docsInstallHint() { if (typeof installCommand !== "undefined") return installCommand; if (typeof installLine !== "undefined") return installLine; if (typeof installCmd !== "undefined") return installCmd; if (typeof installSnippet !== "undefined") return installSnippet; if (typeof brewInstall !== "undefined") return brewInstall; return ""; } function pageUrl(origin, outRel) { const normalized = outRel === "index.html" ? "" : outRel.replace(/(?:^|\/)index\.html$/, (match) => match === "index.html" ? "" : "/"); if (!origin) return normalized || "index.html"; return normalized ? `${origin}/${normalized}` : `${origin}/`; } function readCname() { for (const candidate of [path.join(docsDir, "CNAME"), path.join(root, "CNAME")]) { if (fs.existsSync(candidate)) return fs.readFileSync(candidate, "utf8").trim(); } return ""; } function copyStaticAsset(name) { const source = path.join(docsDir, name); if (fs.existsSync(source)) fs.copyFileSync(source, path.join(outDir, name)); } function parseFrontmatter(raw) { const match = raw.match(/^---\n([\s\S]*?)\n---\n?/); if (!match) return { frontmatter: {}, body: raw }; const fm = {}; for (const line of match[1].split("\n")) { const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/); if (!m) continue; let value = m[2]; if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } fm[m[1]] = value; } return { frontmatter: fm, body: raw.slice(match[0].length) }; } function cleanKramdown(body) { const lines = body.replace(/\r\n/g, "\n").split("\n"); const out = []; let kramdownDivDepth = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (/^\s*\{:\s*[^}]*\}\s*$/.test(line)) continue; if (/^\s*1\.\s+TOC\s*$/.test(line) && /^\s*\{:toc\}\s*$/.test(lines[i + 1] || "")) { i += 1; continue; } if (/^\s*<div\b[^>]*\bmarkdown\s*=\s*"1"[^>]*>\s*$/.test(line)) { kramdownDivDepth++; continue; } if (kramdownDivDepth > 0 && /^\s*<\/div>\s*$/.test(line)) { kramdownDivDepth--; continue; } out.push(line.replace(/\s*\{:\s*[^}]*\}\s*$/, "")); } return out.join("\n"); } function normalizePermalink(value) { let v = value.trim(); if (!v) return "/"; if (!v.startsWith("/")) v = `/${v}`; if (v.length > 1 && v.endsWith("/")) v = v.slice(0, -1); return v; } 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, frontmatter = {}) { if (frontmatter.permalink) { const permalink = normalizePermalink(frontmatter.permalink); if (permalink === "/") return "index.html"; return `${permalink.slice(1)}/index.html`; } if (rel === "index.md") return "index.html"; 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; let blockquote = []; const flushParagraph = () => { if (!paragraph.length) return; const text = paragraph.join(" "); const className = currentRel === "index.md" && /^\[Quickstart\]\([^)]*\)\s+\[View on GitHub\]\(/.test(text) ? ' class="home-actions"' : ""; html.push(`<p${className}>${inline(text, currentRel)}</p>`); paragraph = []; }; const closeList = () => { if (!list) return; html.push(`</${list}>`); list = null; }; const flushBlockquote = () => { if (!blockquote.length) return; const inner = markdownToHtml(blockquote.join("\n"), currentRel); html.push(`<blockquote>${inner}</blockquote>`); blockquote = []; }; const splitRow = (line) => { let trimmed = line.trim(); if (trimmed.startsWith("|")) trimmed = trimmed.slice(1); if (trimmed.endsWith("|") && !trimmed.endsWith("\\|")) trimmed = trimmed.slice(0, -1); const cells = []; let current = ""; for (let idx = 0; idx < trimmed.length; idx++) { const char = trimmed[idx]; if (char === "\\" && trimmed[idx + 1] === "|") { current += "\\|"; idx += 1; continue; } if (char === "|") { cells.push(current.trim().replace(/\\\|/g, "|")); current = ""; continue; } current += char; } cells.push(current.trim().replace(/\\\|/g, "|")); return cells; }; 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(); flushBlockquote(); if (fence) { html.push(`<pre><code class="language-${escapeAttr(fence.lang)}">${highlightCode(fence.lines.join("\n"), fence.lang)}</code></pre>`); fence = null; } else { fence = { lang: fenceMatch[1] || "text", lines: [] }; } continue; } if (fence) { fence.lines.push(line); continue; } if (/^>\s?/.test(line)) { flushParagraph(); closeList(); blockquote.push(line.replace(/^>\s?/, "")); continue; } flushBlockquote(); if (!line.trim()) { flushParagraph(); closeList(); continue; } if (/^\s*---+\s*$/.test(line)) { flushParagraph(); closeList(); html.push("<hr>"); 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(`<h1 id="${id}">${inner}</h1>`); } else { html.push(`<h${level} id="${id}"><a class="anchor" href="#${id}" aria-label="Anchor link">#</a>${inner}</h${level}>`); } 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) => `<th${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ""}>${inline(c, currentRel)}</th>`).join(""); const tb = rows.map((r) => `<tr>${r.map((c, idx) => `<td${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ""}>${inline(c, currentRel)}</td>`).join("")}</tr>`).join(""); html.push(`<table><thead><tr>${th}</tr></thead><tbody>${tb}</tbody></table>`); 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(`<li>${inline((bullet || numbered)[1], currentRel)}</li>`); continue; } paragraph.push(line.trim()); } flushParagraph(); closeList(); flushBlockquote(); return html.join("\n"); } function highlightCode(code, lang) { const normalized = String(lang || "text").toLowerCase(); if (["bash", "sh", "shell", "zsh"].includes(normalized)) return highlightBash(code); if (normalized === "json") return highlightJSON(code); if (normalized === "toml") return highlightConfig(code, "toml"); if (["yaml", "yml"].includes(normalized)) return highlightConfig(code, "yaml"); if (normalized === "cron") return highlightCron(code); return escapeHtml(code); } function highlightBash(code) { return code.split("\n").map((line) => { if (/^\s*#/.test(line)) return span("comment", line); return highlightSegments(line, /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`[^`]*`|\$\{?[A-Za-z_][A-Za-z0-9_]*\}?|--?[A-Za-z0-9][A-Za-z0-9_-]*|\b(?:brew|case|cd|curl|do|done|else|esac|export|fi|for|gh|git|gitcrawl|go|if|in|jq|ln|local|mkdir|set|then|while)\b|#.*)/g, (token) => { if (token.startsWith("#")) return span("comment", token); if (/^["'`]/.test(token)) return span("string", token); if (token.startsWith("$")) return span("variable", token); if (token.startsWith("-")) return span("option", token); return span("keyword", token); }); }).join("\n"); } function highlightJSON(code) { return highlightSegments(code, /("(?:\\.|[^"\\])*"\s*:)|("(?:\\.|[^"\\])*")|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g, (token) => { if (token.endsWith(":")) return `${span("key", token.slice(0, -1))}:`; if (token.startsWith('"')) return span("string", token); if (/^(?:true|false|null)$/.test(token)) return span("literal", token); return span("number", token); }); } function highlightConfig(code, lang) { return code.split("\n").map((line) => { if (/^\s*#/.test(line)) return span("comment", line); const commentMatch = line.match(/(^|[^"'])#.*/); const commentStart = commentMatch ? commentMatch.index + commentMatch[1].length : -1; const body = commentStart >= 0 ? line.slice(0, commentStart) : line; const comment = commentStart >= 0 ? line.slice(commentStart) : ""; const highlighted = lang === "toml" ? highlightSegments(body, /(^\s*[A-Za-z0-9_.-]+(?=\s*=))|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\b(?:true|false)\b|-?\b\d+(?:\.\d+)?\b/g, configToken) : highlightSegments(body, /(^\s*[A-Za-z0-9_.-]+(?=\s*:))|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?\b/g, configToken); return highlighted + (comment ? span("comment", comment) : ""); }).join("\n"); } function configToken(token) { if (/^\s*[A-Za-z0-9_.-]+$/.test(token)) { const leading = token.match(/^\s*/)[0]; return `${escapeHtml(leading)}${span("key", token.slice(leading.length))}`; } if (/^["']/.test(token)) return span("string", token); if (/^(?:true|false|null)$/.test(token)) return span("literal", token); return span("number", token); } function highlightCron(code) { return code.split("\n").map((line) => { if (/^\s*#/.test(line)) return span("comment", line); return highlightSegments(line, /(\*|(?:\d+)(?:[-/,]\d+)*)|("[^"]*"|'[^']*')|#.*|\b[A-Z_][A-Z0-9_]*=/g, (token) => { if (token.startsWith("#")) return span("comment", token); if (/^["']/.test(token)) return span("string", token); if (token.endsWith("=")) return span("key", token.slice(0, -1)) + "="; return span("number", token); }); }).join("\n"); } function highlightSegments(text, pattern, classify) { let out = ""; let last = 0; for (const match of text.matchAll(pattern)) { out += escapeHtml(text.slice(last, match.index)); out += classify(match[0]); last = match.index + match[0].length; } return out + escapeHtml(text.slice(last)); } function span(kind, value) { return `<span class="hl-${kind}">${escapeHtml(value)}</span>`; } function inline(text, currentRel) { const stash = []; let out = text.replace(/`([^`]+)`/g, (_, code) => { stash.push(`<code>${escapeHtml(code)}</code>`); return `\u0000${stash.length - 1}\u0000`; }); out = escapeHtml(out) .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") .replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, "$1<em>$2</em>") .replace(/(^|[^_])_([^_\s][^_]*?)_(?!_)/g, "$1<em>$2</em>") .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${escapeAttr(rewriteHref(href, currentRel))}">${label}</a>`) .replace(/<(https?:\/\/[^\s<>]+)>/g, '<a href="$1">$1</a>'); 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 ? `#${hash}` : ""; if (raw.startsWith("/")) { const target = permalinkMap.get(normalizePermalink(raw)); if (target) { const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel); const out = hrefToOutRel(target.outRel, currentOut); return hash ? `${out}#${hash}` : out; } return href; } if (!raw.endsWith(".md")) return href; const from = path.posix.dirname(currentRel); const target = path.posix.normalize(path.posix.join(from, raw)); let rewritten = pageMap.get(target)?.outRel || outPath(target); const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel); rewritten = hrefToOutRel(rewritten, currentOut); return `${rewritten}${hash ? `#${hash}` : ""}`; } function tocFromHtml(html) { const items = []; const re = /<h([23]) id="([^"]+)">([\s\S]*?)<\/h[23]>/g; let m; while ((m = re.exec(html))) { const text = htmlTextContent(m[3]).replace(/^#/, "").trim(); items.push({ level: Number(m[1]), id: m[2], text }); } if (items.length < 2) return ""; return `<nav class="toc" aria-label="On this page"><h2>On this page</h2>${items