Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ For official OpenClaw support, visit [community.openclaw.ai](https://community.o
- `npm run docs:smoke` checks representative English and locale pages plus the Pagefind search bundle.
- `npm run docs:check` runs both steps.
- Source indexing writes completion metadata and reports success only after the output stream finishes and its file closes successfully; write or close errors fail the build before those completion signals.
- Mermaid fences and `<Mermaid>` blocks render in the browser with Pele 1.6.0, using the docs theme tokens without re-rendering on theme changes. Source Markdown and the expanded-diagram controls are unchanged; diagram links and images stay disabled. Layout follows Pele rather than Mermaid.
- The generated site includes the language picker and static full-text search via Pagefind.
- Navigation groups can nest recursively; sidebar links, section metadata, and desktop/mobile tab targets resolve to descendant pages at every depth.
- Cloudflare deploys `workers/docs-router.ts`, which serves slashless page URLs, English markdown responses for `.md` paths or `Accept: text/markdown`, and `/api/search` through the `DOCS_BUCKET` R2 binding.
Expand Down
1,437 changes: 87 additions & 1,350 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"lucide": "1.56.0",
"markdown-it": "15.0.2",
"markdown-it-anchor": "10.0.0",
"mermaid": "12.1.0",
"pagefind": "1.5.2",
"pele": "1.6.0",
"playwright": "^1.64.0",
"tsx": "4.23.15",
"web-vitals": "6.2.3",
Expand Down
12 changes: 5 additions & 7 deletions scripts/docs-site/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -873,13 +873,11 @@ function writeStaticAssets() {
const glimmDir = path.dirname(glimmEntry);
fs.writeFileSync(path.join(assetsDir, glimmAssetName), glimmRuntime, "utf8");
fs.copyFileSync(path.join(glimmDir, "..", "LICENSE"), path.join(assetsDir, "glimm-LICENSE.txt"));
const mermaidDist = path.join(root, "node_modules", "mermaid", "dist");
const mermaidEntry = path.join(mermaidDist, "mermaid.esm.min.mjs");
if (fs.existsSync(mermaidEntry)) {
fs.copyFileSync(mermaidEntry, path.join(assetsDir, "mermaid.esm.min.mjs"));
copyDir(path.join(mermaidDist, "chunks", "mermaid.esm.min"), path.join(assetsDir, "chunks", "mermaid.esm.min"), {
filter: (source) => !source.endsWith(".map"),
});
const peleDir = path.join(root, "node_modules", "pele");
const peleEntry = path.join(peleDir, "dist", "pele.js");
if (fs.existsSync(peleEntry)) {
fs.copyFileSync(peleEntry, path.join(assetsDir, "pele-1.6.0.js"));
fs.copyFileSync(path.join(peleDir, "LICENSE"), path.join(assetsDir, "pele-LICENSE.txt"));
}
fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8");
fs.writeFileSync(path.join(outDir, "og-card.png"), defaultOgPng);
Expand Down
74 changes: 74 additions & 0 deletions scripts/docs-site/diagram-rendering.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { test } from "node:test";
import { chromium, webkit } from "playwright";
import { fixture } from "./test-helpers/redirect-fixture.mjs";

const source = 'flowchart LR\n accTitle: Public diagram\n accDescr: Ready becomes rendered\n A["Ready<br>state"] --> B[Rendered]';
const hostile = 'flowchart LR\n A["<img src=https://diagram.example/pixel onerror=alert(1)>"] --> B[Safe]\n click B "javascript:alert(1)"';
const markdown = '# Diagrams\n\n' + [source, source, hostile, 'flowchart LR\n Broken -->'].map(text => '\x60\x60\x60mermaid\n' + text + '\n\x60\x60\x60').join('\n\n');

for (const [name, engine] of [["Chromium", chromium], ["WebKit", webkit]]) {
test(name + " uses loaded fonts, unique accessible SVG and strict diagram isolation", async t => {
const f = fixture(t, [], { "guide.md": markdown });
fs.symlinkSync(path.resolve("node_modules"), path.join(f.root, "node_modules"), process.platform === "win32" ? "junction" : "dir");
const built = f.build();
assert.equal(built.status, 0, built.stderr);
const site = path.join(f.root, "dist/docs-site");
const browser = await engine.launch({ headless: true });
t.after(() => browser.close());
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
page.setDefaultTimeout(10000);
const diagramRequests = [];
let releaseFonts;
const fontsHeld = new Promise(resolve => { releaseFonts = resolve; });
t.after(() => releaseFonts());
let fontRequested;
const fontStarted = new Promise(resolve => { fontRequested = resolve; });
await page.route("**/*", async route => {
const url = new URL(route.request().url());
if (url.hostname === "diagram.example") diagramRequests.push(url.href);
if (url.origin !== "https://docs.example") return route.abort();
let file = path.join(site, url.pathname);
if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, "index.html");
if (!fs.existsSync(file)) return route.fulfill({ status: 404 });
if (path.extname(file) === ".woff2") { fontRequested(); await fontsHeld; }
const contentType = ({ ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".woff2": "font/woff2", ".svg": "image/svg+xml" })[path.extname(file)] || "application/octet-stream";
return route.fulfill({ contentType, body: fs.readFileSync(file) });
});
await page.goto("https://docs.example/guide", { waitUntil: "domcontentloaded" });
await fontStarted;
// Settle the renderer import without releasing the intentionally cold webfonts.
await page.evaluate(async () => { await import("/assets/pele-1.6.0.js"); await new Promise(requestAnimationFrame); });
assert.equal(await page.locator(".oc-mermaid.is-rendered").count(), 0, "diagram layout must wait for the label font");
releaseFonts();
await page.waitForFunction(() => document.querySelectorAll(".oc-mermaid.is-rendered").length === 3);
assert.match(await page.locator(".oc-mermaid > svg").first().getAttribute("class"), /pele/);
assert.equal(await page.locator(".oc-mermaid > svg text").first().evaluate(text => getComputedStyle(text).fontSize), "16px");
assert.ok(await page.locator(".oc-mermaid > svg").first().evaluate(async svg => {
const pele = await import("/assets/pele-1.6.0.js");
const fontFamily = getComputedStyle(svg).fontFamily;
const loaded = pele.render(svg.parentElement.dataset.mermaid, { fontFamily, fontSize: 16, links: false, images: false });
return svg.viewBox.baseVal.width === loaded.width && svg.viewBox.baseVal.height === loaded.height;
}), "cold-load geometry must match the loaded font");
const titleIds = await page.locator(".oc-mermaid > svg[aria-labelledby]").evaluateAll(svgs => svgs.map(svg => {
const title = document.getElementById(svg.getAttribute("aria-labelledby"));
const desc = document.getElementById(svg.getAttribute("aria-describedby"));
return { id: title.id, title: title.textContent, desc: desc.textContent, owned: svg.contains(title) && svg.contains(desc) };
}));
assert.equal(titleIds.length, 2);
assert.equal(new Set(titleIds.map(entry => entry.id)).size, 2);
for (const entry of titleIds) assert.deepEqual({ title: entry.title, desc: entry.desc, owned: entry.owned }, { title: "Public diagram", desc: "Ready becomes rendered", owned: true });
assert.equal(await page.locator(".oc-mermaid svg a,.oc-mermaid svg image,.oc-mermaid svg foreignObject,.oc-mermaid svg script,.oc-mermaid svg [onerror]").count(), 0);
assert.deepEqual(diagramRequests, []);
await page.locator("[data-mermaid-expand]").first().click();
const expanded = page.locator("[data-mermaid-overlay-canvas] > svg");
await expanded.waitFor({ state: "visible" });
const expandedTitle = await expanded.getAttribute("aria-labelledby");
assert.ok(!titleIds.some(entry => entry.id === expandedTitle));
assert.equal(await expanded.locator("title").textContent(), "Public diagram");
await page.keyboard.press("Escape");
assert.deepEqual(diagramRequests, []);
});
}
17 changes: 13 additions & 4 deletions scripts/docs-site/docs-layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,19 @@
.docs-layout .oc-mermaid-expand .icon { width: 16px; min-width: 16px; max-width: 16px; height: 16px; stroke-width: 1.5; }
.docs-layout :is(.oc-mermaid-expand,.oc-mermaid-overlay-close):hover { border-color: var(--docs-border); background: color-mix(in srgb,var(--ink) 6%,var(--paper)); color: var(--ink); }
.docs-layout :is(.oc-mermaid-expand,.oc-mermaid-overlay-close):focus-visible { outline: 2px solid var(--oc-focus-ring); outline-offset: 2px; }
.docs-layout :is(.oc-mermaid,.oc-mermaid-overlay-canvas) svg rect.actor { rx: 10px; ry: 10px; }
.docs-layout :is(.oc-mermaid,.oc-mermaid-overlay-canvas) svg .node rect { rx: 8px; ry: 8px; }
.docs-layout :is(.oc-mermaid,.oc-mermaid-overlay-canvas) svg .actor-line { stroke-width: 1px; stroke-dasharray: 4 5; }
.docs-layout :is(.oc-mermaid,.oc-mermaid-overlay-canvas) svg :is(.messageLine0,.messageLine1) { stroke-width: 1.25px; }
.docs-layout :is(.oc-mermaid,.oc-mermaid-overlay-canvas) {
--pele-bg: var(--paper);
--pele-fg: var(--text);
--pele-muted: var(--muted);
--pele-line: var(--docs-diagram-line);
--pele-surface: var(--docs-diagram-node);
--pele-surface-alt: var(--soft);
--pele-border: var(--docs-diagram-border);
--pele-accent: var(--brand);
--pele-font: var(--oc-font-body);
--pele-font-mono: var(--oc-font-mono);
--pele-radius: 8px;
}
.docs-layout .oc-mermaid-overlay-canvas { border-color: var(--docs-border); background: var(--paper); box-shadow: none; padding: 32px; }
.docs-layout .doc .oc-mermaid.is-error { padding: 18px 20px; border-color: var(--docs-border); }
.docs-layout .oc-mermaid-error { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; color: var(--muted); font: 400 12px/1.5 var(--oc-font-body); }
Expand Down
Loading
Loading