fix(mermaid): <br> breaking diagram rendering (closes #1345)

This commit is contained in:
Elian Doran 2025-03-06 21:18:36 +02:00
parent e795caa2f3
commit a162fbfe42
No known key found for this signature in database
4 changed files with 52 additions and 4 deletions

View File

@ -11,7 +11,7 @@ import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js"; import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js"; import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js"; import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
import { loadElkIfNeeded } from "./mermaid.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js"; import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js";
let idCounter = 1; let idCounter = 1;
@ -226,7 +226,7 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
await loadElkIfNeeded(content); await loadElkIfNeeded(content);
const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content); const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
$renderedContent.append($(svg)); $renderedContent.append($(postprocessMermaidSvg(svg)));
} catch (e) { } catch (e) {
const $error = $("<p>The diagram could not displayed.</p>"); const $error = $("<p>The diagram could not displayed.</p>");

View File

@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { postprocessMermaidSvg } from "./mermaid.js";
import { trimIndentation } from "../../../../spec/support/utils.js";
describe("Mermaid", () => {
it("converts <br> properly", () => {
const before = trimIndentation`\
<g transform="translate(-55.71875, -24)" style="color:black !important" class="label">
<rect></rect>
<foreignObject height="48" width="111.4375">
<div xmlns="http://www.w3.org/1999/xhtml"
style="color: black !important; display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;">
<span class="nodeLabel" style="color:black !important">
<p>Verify Output<br>Against<BR > Criteria</p>
</span>
</div>
</foreignObject>
</g>
`;
const after = trimIndentation`\
<g transform="translate(-55.71875, -24)" style="color:black !important" class="label">
<rect></rect>
<foreignObject height="48" width="111.4375">
<div xmlns="http://www.w3.org/1999/xhtml"
style="color: black !important; display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;">
<span class="nodeLabel" style="color:black !important">
<p>Verify Output<br/>Against<br/> Criteria</p>
</span>
</div>
</foreignObject>
</g>
`;
expect(postprocessMermaidSvg(before)).toBe(after);
});
});

View File

@ -23,3 +23,16 @@ export async function loadElkIfNeeded(mermaidContent: string) {
mermaid.registerLayoutLoaders((await import("@mermaid-js/layout-elk")).default); mermaid.registerLayoutLoaders((await import("@mermaid-js/layout-elk")).default);
} }
} }
/**
* Processes the output of a Mermaid SVG render before it should be delivered to the user.
*
* <p>
* Currently this fixes <br> to <br/> which would otherwise cause an invalid XML.
*
* @param svg the Mermaid SVG to process.
* @returns the processed SVG.
*/
export function postprocessMermaidSvg(svg: string) {
return svg.replaceAll(/<br\s*>/ig, "<br/>");
}

View File

@ -3,7 +3,7 @@ import libraryLoader from "../services/library_loader.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js"; import server from "../services/server.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import { loadElkIfNeeded } from "../services/mermaid.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "../services/mermaid.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js"; import type { EventData } from "../components/app_context.js";
@ -122,7 +122,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
await loadElkIfNeeded(content); await loadElkIfNeeded(content);
const { svg } = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content); const { svg } = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
return svg; return postprocessMermaidSvg(svg);
} }
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {