diff --git a/CHANGELOG.md b/CHANGELOG.md index abd99ac0..e5f4e29b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,12 @@ refactors, dependency updates, CI changes, and code cleanup do not belong here. --- +## [1.2.5] - 2026-08-14 + +### Fixed + +- Fixed Mermaid diagrams with HTML labels or named entities failing to display in Markdown live preview and standalone Mermaid files. + ## [1.2.4] - 2026-08-14 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 154ace00..73737c04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1845,7 +1845,7 @@ dependencies = [ [[package]] name = "neverwrite-native-backend" -version = "1.2.4" +version = "1.2.5" dependencies = [ "agent-client-protocol", "agent-client-protocol-legacy", diff --git a/apps/desktop/native-backend/Cargo.toml b/apps/desktop/native-backend/Cargo.toml index e736a88e..35bb4d49 100644 --- a/apps/desktop/native-backend/Cargo.toml +++ b/apps/desktop/native-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neverwrite-native-backend" -version = "1.2.4" +version = "1.2.5" edition = "2021" [dependencies] diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json index 96fcabb2..09af4f21 100644 --- a/apps/desktop/package-lock.json +++ b/apps/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "bifrostwrite-desktop", - "version": "1.2.4", + "version": "1.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bifrostwrite-desktop", - "version": "1.2.4", + "version": "1.2.5", "hasInstallScript": true, "dependencies": { "@codemirror/commands": "^6.10.3", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 31cc4580..5c9e1789 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "bifrostwrite-desktop", "private": true, - "version": "1.2.4", + "version": "1.2.5", "homepage": "https://github.com/bifrost-proxy/BifrostWrite", "type": "module", "engines": { diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 610d6c69..5f32e9b7 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -250,7 +250,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bifrostwrite-desktop" -version = "1.2.4" +version = "1.2.5" dependencies = [ "arboard", "base64 0.22.1", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 6356b5fb..7ca7f479 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bifrostwrite-desktop" -version = "1.2.4" +version = "1.2.5" edition = "2021" description = "Tauri desktop shell for BifrostWrite" license = "Apache-2.0" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 8c0f7280..4190fd1a 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "BifrostWrite", - "version": "1.2.4", + "version": "1.2.5", "identifier": "com.bifrostwrite.desktop", "build": { "beforeDevCommand": "npm run renderer:dev", diff --git a/apps/desktop/src/features/editor/MermaidFilePreview.test.tsx b/apps/desktop/src/features/editor/MermaidFilePreview.test.tsx index 3b1438d3..7a3fed75 100644 --- a/apps/desktop/src/features/editor/MermaidFilePreview.test.tsx +++ b/apps/desktop/src/features/editor/MermaidFilePreview.test.tsx @@ -30,14 +30,18 @@ describe("MermaidFilePreview", () => { A --> B`; mockedRenderMermaidDiagram.mockResolvedValueOnce({ status: "ok", - svg: 'Flow', + svg: [ + '', + '
README Flow
', + "
", + ].join(""), }); renderComponent(); expect(screen.getByText("Rendering Mermaid diagram...")).toBeInTheDocument(); - expect(await screen.findByText("Flow")).not.toBeNull(); + expect(await screen.findByText("README Flow")).not.toBeNull(); expect(mockedRenderMermaidDiagram).toHaveBeenCalledWith( source, expect.stringMatching(/^mermaid-file-tab-1-/), diff --git a/apps/desktop/src/features/editor/MermaidFilePreview.tsx b/apps/desktop/src/features/editor/MermaidFilePreview.tsx index 073e986f..dda61431 100644 --- a/apps/desktop/src/features/editor/MermaidFilePreview.tsx +++ b/apps/desktop/src/features/editor/MermaidFilePreview.tsx @@ -4,6 +4,7 @@ import { renderMermaidDiagram, type MermaidRenderResult, } from "./mermaid/mermaidRenderer"; +import { parseMermaidSvg } from "./mermaid/mermaidSvg"; type MermaidFilePreviewState = | { status: "idle" } @@ -130,13 +131,6 @@ function toPreviewState(result: MermaidRenderResult): MermaidFilePreviewState { return { status: "rendered", svg }; } -function parseMermaidSvg(svg: string): SVGElement | null { - const parsed = new DOMParser().parseFromString(svg, "image/svg+xml"); - const root = parsed.documentElement; - if (root.nodeName.toLowerCase() !== "svg") return null; - return document.importNode(root, true) as unknown as SVGElement; -} - function sanitizeIdPart(value: string) { return value.replace(/[^a-zA-Z0-9_-]/g, "-"); } diff --git a/apps/desktop/src/features/editor/extensions/livePreviewBlocks.test.ts b/apps/desktop/src/features/editor/extensions/livePreviewBlocks.test.ts index 00e65240..8a10450e 100644 --- a/apps/desktop/src/features/editor/extensions/livePreviewBlocks.test.ts +++ b/apps/desktop/src/features/editor/extensions/livePreviewBlocks.test.ts @@ -192,7 +192,11 @@ describe("code block live preview", () => { it("renders Mermaid SVG blocks asynchronously", async () => { mockedRenderMermaidDiagram.mockResolvedValueOnce({ status: "ok", - svg: 'Diagram', + svg: [ + '', + '
README Diagram
', + "
", + ].join(""), }); const parent = document.createElement("div"); document.body.appendChild(parent); @@ -215,8 +219,9 @@ describe("code block live preview", () => { await flushPromises(); expect( - view.dom.querySelector(".cm-mermaid-preview svg text")?.textContent, - ).toBe("Diagram"); + view.dom.querySelector(".cm-mermaid-preview svg foreignObject") + ?.textContent, + ).toBe("README\u00a0Diagram"); expect(view.dom.querySelector(".cm-mermaid-preview-error")).toBeNull(); view.destroy(); diff --git a/apps/desktop/src/features/editor/extensions/livePreviewBlocks.ts b/apps/desktop/src/features/editor/extensions/livePreviewBlocks.ts index b05b3bd2..bbd18b58 100644 --- a/apps/desktop/src/features/editor/extensions/livePreviewBlocks.ts +++ b/apps/desktop/src/features/editor/extensions/livePreviewBlocks.ts @@ -54,6 +54,7 @@ import { renderEmbedPreview, } from "./notePreviewSource"; import { renderMermaidDiagram } from "../mermaid/mermaidRenderer"; +import { parseMermaidSvg } from "../mermaid/mermaidSvg"; import { formatCodeFenceLanguageLabel } from "../codeFencePresentation"; const IMAGE_EXTENSIONS = /\.(png|jpe?g|gif|svg|webp|bmp|ico|avif)([?#].*)?$/i; @@ -1072,13 +1073,6 @@ const mermaidBlockSourceHidden = Decoration.line({ class: "cm-code-block-fence-hidden cm-mermaid-source-hidden", }); -function parseMermaidSvg(svg: string): SVGElement | null { - const parsed = new DOMParser().parseFromString(svg, "image/svg+xml"); - const root = parsed.documentElement; - if (root.nodeName.toLowerCase() !== "svg") return null; - return document.importNode(root, true) as unknown as SVGElement; -} - function renderMermaidError(container: HTMLElement, message: string) { container.className = "cm-mermaid-preview-body cm-mermaid-preview-error"; diff --git a/apps/desktop/src/features/editor/mermaid/mermaidSvg.test.ts b/apps/desktop/src/features/editor/mermaid/mermaidSvg.test.ts new file mode 100644 index 00000000..3d9200da --- /dev/null +++ b/apps/desktop/src/features/editor/mermaid/mermaidSvg.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { parseMermaidSvg } from "./mermaidSvg"; + +describe("parseMermaidSvg", () => { + it("accepts Mermaid HTML labels that are not strict XML", () => { + const svg = parseMermaidSvg(` + + +
README flow
ready
+
+
+ `); + + expect(svg).not.toBeNull(); + expect(svg?.namespaceURI).toBe("http://www.w3.org/2000/svg"); + expect(svg?.textContent).toContain("README\u00a0flow"); + expect(svg?.textContent).toContain("ready"); + }); + + it("rejects output without an SVG root", () => { + expect(parseMermaidSvg("
not an SVG
")).toBeNull(); + expect(parseMermaidSvg("bad SVG")).toBeNull(); + }); +}); diff --git a/apps/desktop/src/features/editor/mermaid/mermaidSvg.ts b/apps/desktop/src/features/editor/mermaid/mermaidSvg.ts new file mode 100644 index 00000000..c14e5950 --- /dev/null +++ b/apps/desktop/src/features/editor/mermaid/mermaidSvg.ts @@ -0,0 +1,17 @@ +const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; + +/** + * Mermaid SVG output may contain HTML labels inside foreignObject elements. + * Parse it as HTML so named entities and HTML-style void elements remain + * compatible with the browser/WebView, then require an SVG document root. + */ +export function parseMermaidSvg(svg: string): SVGSVGElement | null { + const parsed = new DOMParser().parseFromString(svg, "text/html"); + const root = parsed.body.firstElementChild; + + if (root?.localName !== "svg" || root.namespaceURI !== SVG_NAMESPACE) { + return null; + } + + return document.importNode(root, true) as SVGSVGElement; +}