From 2af85c331a43bc5337b5b091d4da045377d0fe1c Mon Sep 17 00:00:00 2001
From: GrgaPalcic <26028466+GrgaPalcic@users.noreply.github.com>
Date: Tue, 6 Oct 2026 19:53:26 +0200
Subject: [PATCH] fix(desktop): make mermaid SVG well-formed via the HTML
parser, not a void-tag regex
The void-tag pass from the previous commit fixes ` `, but mermaid's
label HTML carries more than void tags: a non-breaking space (` `,
mermaid's `#nbsp;`, or a literal U+00A0 in the source) is serialised as
the ` ` entity, which XML does not define. Such a label still
decodes to a broken image.
mermaid.render() returns the HTML serialisation of the diagram, so parse
it with the HTML parser it was written for and re-serialise it with
XMLSerializer. That fixes the whole class at once. Against mermaid
11.16 in Chromium, across flowchart, class, state, ER, mindmap, block,
kanban, sequence, journey, gantt, pie, timeline, gitGraph, quadrant and
xychart output, the round-trip yields the same element tree and image
size as the regex wherever the regex output is valid XML, and also loads
the non-breaking-space cases the regex leaves broken.
Drop a label's plain `xmlns` attribute before serialising: the
serializer declares the namespace itself, and a spec-conformant one
(jsdom) writes both, a duplicate attribute.
The tests' fixtures put ` ` directly in SVG , which mermaid
never emits; replace them with the real 11.16 label shape (XHTML in a
) and assert the label keeps its line break and its
non-breaking space.
Refs #133089, #134031
---
.../embeds/mermaid-embed.test.tsx | 18 +++----
.../assistant-ui/embeds/mermaid-embed.tsx | 7 +--
apps/desktop/src/lib/svg-image.test.ts | 47 +++++++------------
apps/desktop/src/lib/svg-image.ts | 39 +++++++++------
4 files changed, 55 insertions(+), 56 deletions(-)
diff --git a/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.test.tsx b/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.test.tsx
index d4ce73065c..ade733e3df 100644
--- a/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.test.tsx
+++ b/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.test.tsx
@@ -47,21 +47,23 @@ describe('MermaidRenderer', () => {
expect(decodeURIComponent(images[0]?.src.split(',')[1] ?? '')).toContain('marker-end="url(#arrow)"')
})
- it('repairs a label mermaid returned with an open , so the image stays loadable', async () => {
- // `A[x y]` comes back from mermaid as ` ` — malformed XML, so the
- // data: URI fails to decode and the message shows a broken image with the
- // alt text instead of the diagram (#133089).
+ it('repairs a label mermaid returned as HTML, so the image stays loadable', async () => {
+ // `A["a b c"]` comes back from mermaid with an open ` ` and an
+ // ` ` entity — malformed XML, so the data: URI fails to decode and the
+ // message shows a broken image with the alt text instead of the diagram
+ // (#133089).
renderMermaid.mockResolvedValueOnce({
- svg: ''
+ svg: ''
})
- const { container } = render()
+ const { container } = render()
await waitFor(() => expect(container.querySelectorAll('img')).toHaveLength(1))
const src = decodeURIComponent([...container.querySelectorAll('img')][0]?.src.split(',')[1] ?? '')
+ const doc = new DOMParser().parseFromString(src, 'image/svg+xml')
- expect(src).toContain(' ')
- expect(new DOMParser().parseFromString(src, 'image/svg+xml').documentElement.tagName).toBe('svg')
+ expect(doc.documentElement.tagName).toBe('svg')
+ expect(doc.querySelector('p')?.textContent).toBe('ab c')
})
})
diff --git a/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.tsx b/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.tsx
index 2dc7a93491..8b5857638a 100644
--- a/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.tsx
+++ b/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.tsx
@@ -36,9 +36,10 @@ const renderCache = createMermaidRenderCache({
const id = `mmd-${Math.random().toString(36).slice(2)}`
const result = await mermaid.render(id, code)
- // Mermaid's markup targets inline HTML, so a ` ` in a label reaches us
- // as an open ` `; close void tags before the size pass and before the
- // data: URI below, or the diagram decodes to a broken image (#133089).
+ // Mermaid's markup targets inline HTML, so a label's ` ` reaches us as
+ // an open ` ` and a non-breaking space as ` `; make it XML before
+ // the size pass and the data: URI below, or the diagram decodes to a
+ // broken image (#133089).
return normalizeSvgSize(xmlWellFormedSvg(result.svg))
},
// Defer until the source fallback has had a frame to paint, so the mermaid
diff --git a/apps/desktop/src/lib/svg-image.test.ts b/apps/desktop/src/lib/svg-image.test.ts
index 4197f39290..87be49ad1d 100644
--- a/apps/desktop/src/lib/svg-image.test.ts
+++ b/apps/desktop/src/lib/svg-image.test.ts
@@ -65,40 +65,27 @@ describe('normalizeSvgSize', () => {
})
})
-// A mermaid label keeps HTML void tags open: ` ` in the source comes back
-// from `mermaid.render()` as ` `. That makes the string malformed XML, so
-// every strict `image/svg+xml` parse below degrades — and Blink fails the same
-// way when the string is handed to an as a data: URI, which is how a
-// diagram silently stops rendering (broken image, no error anywhere).
+// Real mermaid 11.16 label island for `A["a b c"]` (verified against
+// the installed package): the HTML serialisation leaves ` ` open and writes
+// the non-breaking space as ` `, and neither is well-formed XML. Every
+// strict `image/svg+xml` parse degrades on it — and Blink fails the same way
+// when the string is handed to an as a data: URI, which is how a diagram
+// silently stops rendering (broken image, no error anywhere).
+const MERMAID_LABEL_SVG = ``
+
describe('xmlWellFormedSvg', () => {
- const rootOf = (svg: string) => new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement.tagName
+ const parseXml = (svg: string) => new DOMParser().parseFromString(svg, 'image/svg+xml')
- it('closes a void tag mermaid left open inside a label', () => {
- expect(xmlWellFormedSvg(' x')).toBe(' x')
+ it('makes a mermaid label with and parse as an