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: '',
+ svg: [
+ '",
+ ].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: '',
+ svg: [
+ '",
+ ].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(`
+
+ `);
+
+ 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;
+}