Recover gracefully from mermaid render errors

When mermaid.render fails, suppress mermaid's "Syntax error"
overlay and show our own small warning above the original code
block so the source stays visible. Subsequent diagrams in the
same document still render.
This commit is contained in:
Rūdolfs Ošiņš
2026-05-07 18:31:28 +02:00
parent 9ebc5f6b3b
commit 6fb702f886
3 changed files with 43 additions and 20 deletions
+33 -16
View File
@@ -10,6 +10,9 @@
mermaid.initialize({
startOnLoad: false,
securityLevel: "strict",
// Don't let mermaid inject its own "Syntax error" bomb into the
// document; we render a custom warning instead.
suppressErrorRendering: true,
});
return mermaid;
});
@@ -254,31 +257,36 @@
const language = className.slice(prefix.length);
// Handle mermaid diagrams
if (language === "mermaid") {
const mermaid = await loadMermaid();
const mermaidCode = node.textContent || "";
const diagramId = `mermaid-${crypto.randomUUID()}`;
try {
// First, validate the syntax without rendering
const isValid = await mermaid.parse(mermaidCode, {
suppressErrors: true,
});
if (isValid) {
// Generate a unique id for this diagram
const diagramId = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
// Try to render the mermaid diagram
const { svg } = await mermaid.render(diagramId, mermaidCode);
// Replace the code block with the rendered diagram
const { svg } = await mermaid.render(
diagramId,
node.textContent ?? "",
);
// The component may have re-rendered or unmounted while we were
// awaiting; in that case `preWrapper` is detached and there's
// nothing to replace.
if (preWrapper.isConnected) {
const mermaidDiv = document.createElement("div");
mermaidDiv.innerHTML = svg;
mermaidDiv.classList.add("mermaid-diagram");
preWrapper.replaceWith(mermaidDiv);
}
} catch (error) {
// On any error, keep the raw text visible (do nothing, let it render as code)
console.warn("Not able to render mermaid diagram", error);
if (preWrapper.isConnected) {
const warning = document.createElement("div");
warning.classList.add("mermaid-error");
const icon = document.createElement("radicle-icon-small");
icon.setAttribute("name", "warning");
warning.appendChild(icon);
const text = document.createElement("span");
text.textContent = "Couldn't render diagram";
warning.appendChild(text);
preWrapper.before(warning);
}
}
continue;
}
@@ -562,6 +570,15 @@
max-width: 100%;
height: auto;
}
.markdown :global(.mermaid-error) {
display: flex;
align-items: center;
gap: 0.375rem;
color: var(--color-feedback-warning-text);
font: var(--txt-body-s-regular);
margin: 1rem 0 0.25rem;
}
</style>
{#if frontMatter && frontMatter.length > 0}
Binary file not shown.
+10 -4
View File
@@ -125,20 +125,26 @@ test("math", async ({ page }) => {
await expect(page).toHaveScreenshot({ fullPage: true });
});
test("mermaid diagram", async ({ page }) => {
test("mermaid diagrams", async ({ page }) => {
await page.goto(`${markdownUrl}/tree/main/mermaid.md`, {
waitUntil: "networkidle",
});
await expect(page.locator(".mermaid-diagram svg")).toBeVisible();
await expect(page.locator(".mermaid-diagram svg")).toHaveCount(2);
await expect(page).toHaveScreenshot({ fullPage: true });
});
test("broken mermaid falls back to code block", async ({ page }) => {
test("broken mermaid shows warning and does not block subsequent diagrams", async ({
page,
}) => {
await page.goto(`${markdownUrl}/tree/main/mermaid-broken.md`, {
waitUntil: "networkidle",
});
// Our custom warning replaces mermaid's built-in error overlay.
await expect(page.locator(".mermaid-error")).toBeVisible();
// The original source for the broken diagram stays visible.
await expect(page.locator("pre code.language-mermaid")).toBeVisible();
await expect(page.locator(".mermaid-diagram")).toHaveCount(0);
// The valid diagram following the broken one still renders.
await expect(page.locator(".mermaid-diagram svg")).toHaveCount(1);
await expect(page).toHaveScreenshot({ fullPage: true });
});