diff --git a/src/components/Markdown.svelte b/src/components/Markdown.svelte index 13d48a50d..ca211ed1a 100644 --- a/src/components/Markdown.svelte +++ b/src/components/Markdown.svelte @@ -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; + } {#if frontMatter && frontMatter.length > 0} diff --git a/tests/fixtures/repos/markdown.tar.bz2 b/tests/fixtures/repos/markdown.tar.bz2 index 4153d642e..467a8f442 100644 Binary files a/tests/fixtures/repos/markdown.tar.bz2 and b/tests/fixtures/repos/markdown.tar.bz2 differ diff --git a/tests/visual/desktop/markdown.spec.ts b/tests/visual/desktop/markdown.spec.ts index 0b4a2c5ce..b17cf50bc 100644 --- a/tests/visual/desktop/markdown.spec.ts +++ b/tests/visual/desktop/markdown.spec.ts @@ -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 }); });