\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(?:
)?\s*([\s\S]*?)<\/blockquote>/gi, + (match, type, body) => { + const key = type.toUpperCase() + const alert = GFM_ALERT_TYPES[key] + if (!alert) return match + const cleanBody = body.replace(/<\/p>\s*$/, "") + return `` + }, + ) +} + async function highlightCodeBlocks(html: string): Promise${alert.label}${cleanBody}{ const codeBlockRegex = / ([\s\S]*?)<\/code><\/pre>/g const matches = [...html.matchAll(codeBlockRegex)] @@ -444,6 +465,14 @@ async function highlightCodeBlocks(html: string): Promise{ .replace(/"/g, '"') .replace(/'/g, "'") + // Mermaid blocks: emit render container instead of syntax-highlighted code + if (lang === "mermaid") { + const encoded = btoa(unescape(encodeURIComponent(code))) + const mermaidHtml = ` ` + result = result.replace(fullMatch, () => mermaidHtml) + continue + } + let language = lang || "text" if (!(language in bundledLanguages)) { language = "text" @@ -483,6 +512,16 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext( }), markedShiki({ async highlight(code, lang) { + // Mermaid blocks: emit a container with render target + hidden source + if (lang === "mermaid") { + const encoded = btoa(unescape(encodeURIComponent(code))) + const escaped = code + .replace(/&/g, "&") + .replace(//g, ">") + return `Loading diagram\u2026${escapedCode}` + } + const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [], @@ -509,11 +548,17 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext( async parse(markdown: string): PromiseLoading diagram\u2026${escaped}{ const html = await nativeParser(markdown) const withMath = renderMathExpressions(html) - return highlightCodeBlocks(withMath) + const withAlerts = renderGfmAlerts(withMath) + return highlightCodeBlocks(withAlerts) }, } } - return jsParser + return { + async parse(markdown: string): Promise { + const html = await jsParser.parse(markdown) + return renderGfmAlerts(html) + }, + } }, })