import MarkdownIt from "markdown-it"; import anchor from "markdown-it-anchor"; const markerPrefix = "OPENCLAW_DOCS_MARKER"; const knownBlocks = new Map([ ["AccordionGroup", ["accordion-group", ""]], ["Columns", ["card-grid", ""]], ["CardGroup", ["card-grid", ""]], ["Steps", ["steps", ""]], ["Tabs", ["tabs", ""]], ["CodeGroup", ["code-group", ""]], ["Frame", ["frame", ""]] ]); const callouts = new Map([ ["Note", "Note"], ["Warning", "Warning"], ["Tip", "Tip"], ["Info", "Info"], ["Check", "Check"], ["Say", "Say"] ]); export function createMarkdownRenderer() { return new MarkdownIt({ html: true, linkify: true, typographer: false, highlight: highlightCode }).use(anchor, { permalink: anchor.permalink.linkInsideHeader({ symbol: "#", class: "anchor", placement: "before", ariaHidden: true }) }); } function highlightCode(code, rawLang = "") { const lang = normalizeLang(rawLang); if (isShell(lang, code)) return highlightWith(code, shellToken, shellTokenClass); if (["json", "jsonc"].includes(lang)) return highlightWith(code, jsonToken, jsonTokenClass); if (["js", "javascript", "jsx", "mjs", "cjs", "ts", "typescript", "tsx", "json5"].includes(lang)) { return highlightWith(code, jsToken, jsTokenClass); } if (["yaml", "yml"].includes(lang)) return highlightWith(code, yamlToken, yamlTokenClass); if (["toml", "ini", "env"].includes(lang)) return highlightWith(code, configToken, configTokenClass); return escapeHtml(code); } function normalizeLang(rawLang) { return String(rawLang).trim().split(/\s+/)[0]?.toLowerCase().replace(/^language-/, "") ?? ""; } function isShell(lang, code) { return ["bash", "sh", "shell", "zsh", "console", "terminal", "text"].includes(lang) || (!lang && /(^|\n)\s*(?:[$#]\s*)?(?:npm|pnpm|bun|npx|openclaw|git|curl|brew|docker|node)\b/.test(code)); } function highlightWith(code, tokenPattern, tokenClass) { let out = ""; let last = 0; tokenPattern.lastIndex = 0; for (const match of code.matchAll(tokenPattern)) { out += escapeHtml(code.slice(last, match.index)); const className = tokenClass(match[0], match, code); out += className ? `${escapeHtml(match[0])}` : escapeHtml(match[0]); last = match.index + match[0].length; } return out + escapeHtml(code.slice(last)); } const shellToken = /(?/gi, "\n"); out = out.replace(/]*)\/>/g, (_, attrs) => `${marker("cardSelf", attrs)}\n`); out = out.replace(/]*)>/g, (_, attrs) => `\n${marker("cardOpen", attrs)}\n`); out = out.replace(/<\/Card>/g, `\n${marker("cardClose")}\n`); out = out.replace(/]*)>/g, (_, attrs) => `\n${marker("stepOpen", attrs)}\n`); out = out.replace(/<\/Step>/g, `\n${marker("stepClose")}\n`); out = out.replace(/]*)>/g, (_, attrs) => `\n${marker("tabOpen", attrs)}\n`); out = out.replace(/<\/Tab>/g, `\n${marker("tabClose")}\n`); out = out.replace(/]*)>/g, (_, attrs) => `\n${marker("accordionOpen", attrs)}\n`); out = out.replace(/<\/Accordion>/g, `\n${marker("accordionClose")}\n`); out = out.replace(/]*)>/g, (_, attrs) => `\n${marker("paramOpen", attrs)}\n`); out = out.replace(/<\/ParamField>/g, `\n${marker("paramClose")}\n`); for (const [name, [kind]] of knownBlocks) { out = out.replace(new RegExp(`<${name}\\b[^>]*>`, "g"), `\n${marker("blockOpen", kind)}\n`); out = out.replace(new RegExp(``, "g"), `\n${marker("blockClose", kind)}\n`); } for (const [name, label] of callouts) { out = out.replace(new RegExp(`<${name}\\b[^>]*>`, "g"), `\n${marker("calloutOpen", label)}\n`); out = out.replace(new RegExp(``, "g"), `\n${marker("calloutClose")}\n`); } out = out.replace(/<([A-Z][A-Za-z0-9_.-]*)([^>]*)>/g, (_, name, attrs) => escapeHtml(`<${name}${attrs}>`)); out = out.replace(/<\/([A-Z][A-Za-z0-9_.-]*)>/g, (_, name) => escapeHtml(``)); return dedentComponentChildren(out); } function postprocess(html) { return html.replace(new RegExp(`

${markerPrefix}:([^<]+)

`, "g"), (_, payload) => expandMarker(payload)); } function marker(kind, payload = "") { return `${markerPrefix}:${kind}:${Buffer.from(payload, "utf8").toString("base64url")}`; } function expandMarker(payload) { const [kind, encoded = ""] = payload.split(":"); const value = Buffer.from(encoded, "base64url").toString("utf8"); if (kind === "blockOpen") return `
`; if (kind === "blockClose") return "
"; if (kind === "calloutOpen") return `"; if (kind === "cardSelf") return cardHtml(value, true); if (kind === "cardOpen") return cardHtml(value, false); if (kind === "cardClose") return ""; if (kind === "stepOpen") return `
  • ${escapeHtml(parseAttrs(value).title ?? "Step")}

    `; if (kind === "stepClose") return "
  • "; if (kind === "tabOpen") return `

    ${escapeHtml(parseAttrs(value).title ?? "Tab")}

    `; if (kind === "tabClose") return "
    "; if (kind === "accordionOpen") return `
    ${escapeHtml(parseAttrs(value).title ?? "Details")}`; if (kind === "accordionClose") return "
    "; if (kind === "paramOpen") { const attrs = parseAttrs(value); const required = attrs.required !== undefined ? `required` : ""; const type = attrs.type ? `${escapeHtml(attrs.type)}` : ""; return `
    ${escapeHtml(attrs.path ?? attrs.name ?? "param")}${type}${required}
    `; } if (kind === "paramClose") return "
    "; return ""; } function cardHtml(rawAttrs, selfClosing) { const attrs = parseAttrs(rawAttrs); const href = attrs.href ?? "#"; const title = attrs.title ?? attrs.name ?? "Open"; const icon = attrs.icon ? iconSvg(attrs.icon) : ""; const end = selfClosing ? "" : ""; return `${icon}
    ${escapeHtml(title)}${end}`; } function iconSvg(name) { const paths = { rocket: ``, sparkles: ``, "layout-dashboard": ``, terminal: ``, settings: ``, book: ``, globe: ``, wrench: ``, gear: `` }; const path = paths[slug(name)] ?? ``; return ``; } function dedentComponentChildren(markdown) { let depth = 0; return markdown .split("\n") .map((line) => { const markerMatch = line.match(new RegExp(`^${markerPrefix}:([^:]+):`)); if (markerMatch) { if (markerMatch[1].endsWith("Close") || markerMatch[1] === "blockClose" || markerMatch[1] === "calloutClose") { depth = Math.max(0, depth - 1); } const markerLine = line; if (markerMatch[1].endsWith("Open") || markerMatch[1] === "blockOpen" || markerMatch[1] === "calloutOpen") { depth += 1; } return markerLine; } if (depth <= 0 || !line.startsWith(" ")) return line; return line.replace(new RegExp(`^ {1,${depth * 2}}`), ""); }) .join("\n"); } function parseAttrs(raw) { const attrs = {}; for (const match of raw.matchAll(/([A-Za-z0-9_-]+)(?:=(?:"([^"]*)"|'([^']*)'|\{([^}]*)\}|([^\s>]+)))?/g)) { attrs[match[1]] = match[2] ?? match[3] ?? match[4] ?? match[5] ?? ""; } return attrs; } function slug(value) { return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function escapeAttr(value) { return escapeHtml(value).replaceAll("'", "'"); }