/** HTML shell + markdown→HTML rendering with server-side diff/code highlighting and client-side mermaid. */ import { Marked } from "marked"; import { parse as diffParse, html as diffHtml } from "diff2html"; import hljs from "highlight.js"; import { CONFIG, MERMAID_CDN } from "./config.js"; import { BASE_CSS, D2H_CSS, HLJS_CSS } from "./styles.js"; export interface RenderFlags { hasMermaid: boolean; hasDiff: boolean; hasCode: boolean; } /** Escape HTML special characters. */ function escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">"); } /** Escape for attribute context (quotes too). */ function escapeAttr(s: string): string { return escapeHtml(s).replace(/"/g, """); } /** Render markdown to an HTML body fragment, handling diff/code/mermaid fences. */ export function renderMarkdown(content: string, flags: RenderFlags): string { // Fresh instance per render: the code renderer closes over `flags`, and mutating // the global marked singleton would stack an override per call. const marked = new Marked({ gfm: true, breaks: false, renderer: { code({ text, lang }: { text: string; lang?: string }): string { const language = (lang ?? "").trim().toLowerCase(); if (language === "mermaid") { flags.hasMermaid = true; return `
${escapeHtml(text)}${out}`;
} catch {
// fall through
}
}
// Plain escaped code block (no unreliable auto-detect on short snippets)
if (language) flags.hasCode = true;
return `${escapeHtml(text)}`;
},
},
});
return marked.parse(content) as string;
}
/** Render a unified-diff string to diff2html HTML (server-side). Returns null if parse yields nothing. */
function renderDiff(diffText: string): string | null {
try {
const json = diffParse(diffText, {});
if (!json || json.length === 0) return null;
return diffHtml(json, {
outputFormat: "line-by-line",
drawFileList: false,
});
} catch {
return null;
}
}
// ─── Shell ────────────────────────────────────────────────────────────────────
/** SSE snippet: subscribe to /events, reload only on events for this slug. */
function sseSnippet(slug: string): string {
return `
`;
}
/** Mermaid init: render after the library loads. Uses the "base" theme fed with
* the document's own tokens (read from CSS variables at runtime), so diagrams
* follow the scheme and the configured accent instead of mermaid's built-ins —
* "neutral" in particular draws grayscale text that's unreadably pale. */
function mermaidSnippet(): string {
return `
`;
}
/** Build the full HTML document shell around a body fragment. */
export function buildShell(opts: {
title: string;
slug: string;
kind: "markdown" | "html";
bodyHtml: string;
flags: RenderFlags;
}): string {
const { title, slug, kind, bodyHtml, flags } = opts;
const projectPath = process.cwd();
const generated = Date.now();
const styles: string[] = [``];
if (flags.hasDiff) styles.push(``);
if (flags.hasCode) styles.push(``);
const scripts: string[] = [];
if (flags.hasMermaid) scripts.push(mermaidSnippet());
scripts.push(sseSnippet(slug));
const generatedIso = new Date(generated).toISOString();
return `
${escapeHtml(when)}No artifacts yet. Use the artifact tool to create one.
| Title | Kind | Generated |
|---|