/**
* Pure Markdown → sanitized-HTML renderer for assistant messages.
*
* Mirrors old Dispatch's stack (marked + marked-highlight + highlight.js +
* DOMPurify; GFM + line breaks; streaming delimiter-closing), but kept fully
* SYNCHRONOUS and pure: `input → output`, no effects, no `$effect`. Languages
* are a fixed "hot set" registered at module load (no lazy dynamic import), so a
* single `renderMarkdown(text)` call is deterministic and unit-testable.
*
* The only ambient dependency is DOMPurify, which sanitizes against the DOM —
* present in the browser and in the jsdom test env.
*/
import DOMPurify from "dompurify";
import type { LanguageFn } from "highlight.js";
import hljs from "highlight.js/lib/core";
import bash from "highlight.js/lib/languages/bash";
import c from "highlight.js/lib/languages/c";
import cpp from "highlight.js/lib/languages/cpp";
import csharp from "highlight.js/lib/languages/csharp";
import css from "highlight.js/lib/languages/css";
import go from "highlight.js/lib/languages/go";
import java from "highlight.js/lib/languages/java";
import javascript from "highlight.js/lib/languages/javascript";
import json from "highlight.js/lib/languages/json";
import markdownLang from "highlight.js/lib/languages/markdown";
import php from "highlight.js/lib/languages/php";
import plaintext from "highlight.js/lib/languages/plaintext";
import python from "highlight.js/lib/languages/python";
import ruby from "highlight.js/lib/languages/ruby";
import rust from "highlight.js/lib/languages/rust";
import shell from "highlight.js/lib/languages/shell";
import sql from "highlight.js/lib/languages/sql";
import typescript from "highlight.js/lib/languages/typescript";
import xml from "highlight.js/lib/languages/xml";
import yaml from "highlight.js/lib/languages/yaml";
import { Marked } from "marked";
import { markedHighlight } from "marked-highlight";
// Hot set: registered eagerly so common code blocks highlight on first paint.
const HOT_LANGUAGES: Record = {
bash,
c,
cpp,
csharp,
css,
go,
java,
javascript,
json,
markdown: markdownLang,
php,
plaintext,
python,
ruby,
rust,
shell,
sql,
typescript,
xml,
yaml,
};
for (const [name, lang] of Object.entries(HOT_LANGUAGES)) {
hljs.registerLanguage(name, lang);
}
// Normalize common fence aliases to canonical highlight.js names.
const ALIASES: Record = {
js: "javascript",
jsx: "javascript",
mjs: "javascript",
cjs: "javascript",
ts: "typescript",
tsx: "typescript",
py: "python",
py3: "python",
rb: "ruby",
sh: "bash",
zsh: "bash",
yml: "yaml",
"c++": "cpp",
cxx: "cpp",
"c#": "csharp",
cs: "csharp",
htm: "xml",
html: "xml",
svg: "xml",
md: "markdown",
mdx: "markdown",
golang: "go",
rs: "rust",
};
function normalizeLang(lang: string): string {
const lower = lang.toLowerCase().trim();
return ALIASES[lower] ?? lower;
}
function escapeHtml(s: string): string {
return s
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
const md = new Marked(
markedHighlight({
emptyLangClass: "hljs",
langPrefix: "hljs language-",
highlight(code: string, lang: string): string {
if (!lang) return escapeHtml(code);
const name = normalizeLang(lang);
if (!hljs.getLanguage(name)) return escapeHtml(code);
try {
return hljs.highlight(code, { language: name, ignoreIllegals: true }).value;
} catch {
return escapeHtml(code);
}
},
}),
{ gfm: true, breaks: true },
);
/**
* While a message is still streaming, balance dangling fences / emphasis so the
* partial text renders cleanly instead of flashing raw markers.
*/
function closeOpenDelimiters(src: string): string {
let out = src;
const fenceCount = (out.match(/^```/gm) ?? []).length;
if (fenceCount % 2 !== 0) out += "\n```";
const boldCount = (out.match(/\*\*/g) ?? []).length;
if (boldCount % 2 !== 0) out += "**";
const inlineCode = (out.match(/(?…`) in a positioned container with a
// copy button. marked emits exactly one `
`/`
` pair per block and
// escapes `<`/`>` inside code, so these literal tags only ever delimit blocks.
// `data-copy` is the delegation hook the component listens for; DOMPurify keeps
// `