fix(frontend): fix Mermaid preview failure in historical messages (#3196)

* fix(frontend): render historical mermaid diagrams

* fix(frontend): address mermaid review feedback

* Stabilize cancel lifecycle test

* fix(frontend): handle mermaid fence variants

* fix(frontend): normalize mermaid arrow spacing

* fix(frontend): handle mermaid CRLF fences

* chore: keep mermaid fix frontend-scoped

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Admire
2026-05-28 18:20:02 +08:00
committed by GitHub
parent 737abc0e45
commit 2fdfff0db3
6 changed files with 341 additions and 2 deletions
+2
View File
@@ -1 +1,3 @@
export * from "./mermaid";
export * from "./preprocess";
export * from "./plugins";
+98
View File
@@ -0,0 +1,98 @@
const MERMAID_OPENING_FENCE_RE =
/^[ \t]{0,3}(`{3,}|~{3,})[ \t]*mermaid(?:[ \t].*)?$/i;
const WINDOWS_LINE_ENDING_RE = /\r\n?/g;
const LABELLED_DOTTED_ARROW_RE =
/^(\s*)(.+?)\s*--\s*("[^"\n]+"|'[^'\n]+')\s*-\.->\s*(.+?)\s*$/;
function normalizeMermaidCode(code: string): string {
return code
.split("\n")
.map((line) =>
line.replace(
LABELLED_DOTTED_ARROW_RE,
(
_match,
indent: string,
source: string,
label: string,
target: string,
) => `${indent}${source} -. ${label} .-> ${target}`,
),
)
.join("\n");
}
function isClosingFence(line: string, fence: string): boolean {
const trimmedLine = line.trimEnd();
const indentationLength = trimmedLine.length - trimmedLine.trimStart().length;
const fenceMarker = trimmedLine.slice(indentationLength);
const fenceChar = fence.charAt(0);
if (indentationLength > 3 || !fenceMarker.startsWith(fenceChar)) {
return false;
}
return (
fenceMarker.length >= fence.length &&
[...fenceMarker].every((char) => char === fenceChar)
);
}
export function normalizeMermaidMarkdown(markdown: string): string {
const lines = markdown.replace(WINDOWS_LINE_ENDING_RE, "\n").split("\n");
const normalizedLines: string[] = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index]!;
const openingFenceMatch = MERMAID_OPENING_FENCE_RE.exec(line);
if (!openingFenceMatch) {
normalizedLines.push(line);
continue;
}
const openingFence = openingFenceMatch[1];
if (openingFence === undefined) {
normalizedLines.push(line);
continue;
}
const codeLines: string[] = [];
let closingLine: string | undefined;
let cursor = index + 1;
for (; cursor < lines.length; cursor += 1) {
const candidateLine = lines[cursor]!;
if (isClosingFence(candidateLine, openingFence)) {
closingLine = candidateLine;
break;
}
codeLines.push(candidateLine);
}
if (closingLine === undefined) {
normalizedLines.push(line, ...codeLines);
index = cursor - 1;
continue;
}
normalizedLines.push(line);
if (codeLines.length > 0) {
normalizedLines.push(
...normalizeMermaidCode(codeLines.join("\n")).split("\n"),
);
}
normalizedLines.push(closingLine);
index = cursor;
}
return normalizedLines.join("\n");
}
@@ -0,0 +1,11 @@
import { normalizeMermaidMarkdown } from "./mermaid";
const MERMAID_BLOCK_HINT_RE = /mermaid/i;
export function preprocessStreamdownMarkdown(markdown: string): string {
if (!MERMAID_BLOCK_HINT_RE.test(markdown) || !markdown.includes("-.->")) {
return markdown;
}
return normalizeMermaidMarkdown(markdown);
}