944 lines
49 KiB
JavaScript
944 lines
49 KiB
JavaScript
#!/usr/bin/env node
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { socialCardPng } from "./social-card.mjs";
|
||
|
||
const root = process.cwd();
|
||
const docsDir = path.join(root, "docs");
|
||
const outDir = path.join(root, "dist", "docs-site");
|
||
const repoUrl = "https://github.com/openclaw/clawsweeper";
|
||
const repoEditBase = `${repoUrl}/edit/main/docs`;
|
||
const customDomain = "clawsweeper.bot";
|
||
|
||
const sections = [
|
||
["Start", ["scheduler.md", "work-lane.md"]],
|
||
[
|
||
"Lanes",
|
||
[
|
||
"commit-sweeper.md",
|
||
"commit-dispatcher.md",
|
||
"target-dispatcher.md",
|
||
"pr-review-comments.md",
|
||
"openclaw-event-hooks.md",
|
||
],
|
||
],
|
||
[
|
||
"Repair",
|
||
[
|
||
"repair/README.md",
|
||
"repair/operations.md",
|
||
"repair/auto-update-prs.md",
|
||
"repair/automerge-flow.md",
|
||
"repair/internal-features.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 markdown = fs.readFileSync(file, "utf8");
|
||
const title = firstHeading(markdown) || titleize(path.basename(rel, ".md"));
|
||
return { file, rel, title, outRel: outPath(rel), markdown, synthetic: false };
|
||
});
|
||
|
||
const homePage = {
|
||
file: null,
|
||
rel: "__home",
|
||
title: "ClawSweeper",
|
||
outRel: "index.html",
|
||
markdown: "",
|
||
synthetic: true,
|
||
};
|
||
pages.unshift(homePage);
|
||
|
||
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 = [homePage, ...nav.flatMap((s) => s.pages)];
|
||
|
||
for (const page of pages) {
|
||
const html = page.synthetic ? "" : markdownToHtml(page.markdown, page.rel);
|
||
const toc = page.synthetic ? "" : 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, "clawsweeper.svg"), clawSvg(), "utf8");
|
||
fs.writeFileSync(path.join(outDir, "favicon.svg"), faviconSvg(), "utf8");
|
||
fs.writeFileSync(path.join(outDir, "social-card.png"), socialCardPng());
|
||
fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8");
|
||
fs.writeFileSync(path.join(outDir, "CNAME"), `${customDomain}\n`, "utf8");
|
||
fs.writeFileSync(
|
||
path.join(outDir, "robots.txt"),
|
||
`User-agent: *\nAllow: /\nSitemap: https://${customDomain}/sitemap.xml\n`,
|
||
"utf8",
|
||
);
|
||
fs.writeFileSync(path.join(outDir, "sitemap.xml"), sitemap(orderedPages), "utf8");
|
||
console.log(`built docs site: ${path.relative(root, outDir)} (${pages.length} pages)`);
|
||
|
||
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()
|
||
.replace(/^[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]\s+/u, "");
|
||
}
|
||
|
||
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(`<p>${inline(paragraph.join(" "), currentRel)}</p>`);
|
||
paragraph = [];
|
||
};
|
||
const closeList = () => {
|
||
if (!list) return;
|
||
html.push(`</${list}>`);
|
||
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];
|
||
if (line.trim().startsWith("<img ")) {
|
||
flushParagraph();
|
||
closeList();
|
||
continue;
|
||
}
|
||
const fenceMatch = line.match(/^```(\w+)?\s*$/);
|
||
if (fenceMatch) {
|
||
flushParagraph();
|
||
closeList();
|
||
if (fence) {
|
||
html.push(
|
||
`<pre><code class="language-${fence.lang}">${escapeHtml(fence.lines.join("\n"))}</code></pre>`,
|
||
);
|
||
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().replace(/^[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]\s+/u, "");
|
||
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 blockquote = line.match(/^>\s?(.*)$/);
|
||
if (blockquote) {
|
||
flushParagraph();
|
||
closeList();
|
||
const buf = [blockquote[1]];
|
||
while (i + 1 < lines.length && /^>\s?/.test(lines[i + 1])) {
|
||
i += 1;
|
||
buf.push(lines[i].replace(/^>\s?/, ""));
|
||
}
|
||
html.push(`<blockquote><p>${inline(buf.join(" "), currentRel)}</p></blockquote>`);
|
||
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();
|
||
return html.join("\n");
|
||
}
|
||
|
||
function inline(text, currentRel) {
|
||
const stash = [];
|
||
let out = text.replace(/`([^`]+)`/g, (_, code) => {
|
||
stash.push(`<code>${escapeHtml(code)}</code>`);
|
||
return ` |