fix(web): sanitize mermaid code to handle special characters (#444)
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
MIN_SCALE,
|
||||
MAX_SCALE,
|
||||
getSvgDimensions,
|
||||
sanitizeMermaidCode,
|
||||
} from "@/components/shared/mermaid-utils";
|
||||
|
||||
/**
|
||||
@@ -239,8 +240,9 @@ class MermaidNodeView implements NodeView {
|
||||
if (!mermaidInitialized) initMermaid();
|
||||
if (!code.trim()) return;
|
||||
const id = `mermaid-${++mermaidIdCounter}`;
|
||||
const sanitizedCode = sanitizeMermaidCode(code);
|
||||
mermaid
|
||||
.render(id, code)
|
||||
.render(id, sanitizedCode)
|
||||
.then(({ svg }) => {
|
||||
this.svgContainer.innerHTML = svg;
|
||||
this.updateSizeWrapper();
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { IconZoomIn, IconZoomOut, IconCode } from "@tabler/icons-react";
|
||||
import { DEFAULT_SCALE, SCALE_STEP, MIN_SCALE, MAX_SCALE, getSvgDimensions } from "./mermaid-utils";
|
||||
import {
|
||||
DEFAULT_SCALE,
|
||||
SCALE_STEP,
|
||||
MIN_SCALE,
|
||||
MAX_SCALE,
|
||||
getSvgDimensions,
|
||||
sanitizeMermaidCode,
|
||||
} from "./mermaid-utils";
|
||||
|
||||
type MermaidAPI = typeof import("mermaid").default;
|
||||
|
||||
@@ -36,10 +43,12 @@ export function MermaidBlock({ code }: MermaidBlockProps) {
|
||||
const theme = resolvedTheme === "dark" ? "dark" : "default";
|
||||
const id = `mermaid-md-${++mermaidIdCounter}`;
|
||||
|
||||
const sanitizedCode = sanitizeMermaidCode(code);
|
||||
|
||||
getMermaid()
|
||||
.then((mermaid) => {
|
||||
mermaid.initialize({ startOnLoad: false, theme, securityLevel: "loose" });
|
||||
return mermaid.render(id, code);
|
||||
return mermaid.render(id, sanitizedCode);
|
||||
})
|
||||
.then(({ svg }) => {
|
||||
if (!cancelled && containerRef.current) {
|
||||
|
||||
@@ -21,3 +21,51 @@ export function getSvgDimensions(container: HTMLElement): { w: number; h: number
|
||||
if (w > 0 && h > 0) return { w, h };
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Characters that require quoting in mermaid node/edge labels.
|
||||
* Includes: $, #, &, /, and other special chars that cause lexical errors.
|
||||
*/
|
||||
const SPECIAL_CHARS_RE = /[$#&/\\<>{}]/;
|
||||
|
||||
/**
|
||||
* Preprocesses mermaid code to quote text containing special characters.
|
||||
* Mermaid requires quotes around text with special chars like $, /, etc.
|
||||
*
|
||||
* Handles:
|
||||
* - Node labels: A[text] -> A["text"] if text contains special chars
|
||||
* - Edge labels: -->|text| -> -->|"text"| if text contains special chars
|
||||
* - Subgraph titles: subgraph text -> subgraph "text" if text contains special chars
|
||||
*/
|
||||
export function sanitizeMermaidCode(code: string): string {
|
||||
// Quote node labels: [text] or (text) or {text} or ([text]) or [[text]] etc.
|
||||
// Match brackets that aren't already quoted
|
||||
let result = code.replace(/(\[+)([^\]"]+?)(\]+)/g, (match, open, text, close) => {
|
||||
if (SPECIAL_CHARS_RE.test(text) && !text.startsWith('"')) {
|
||||
return `${open}"${text}"${close}`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
// Quote edge labels: |text|
|
||||
result = result.replace(/\|([^|"]+?)\|/g, (match, text) => {
|
||||
if (SPECIAL_CHARS_RE.test(text) && !text.startsWith('"')) {
|
||||
return `|"${text}"|`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
// Quote parentheses labels: (text) for stadium/circle nodes
|
||||
result = result.replace(/(\(+)([^)"]+?)(\)+)/g, (match, open, text, close) => {
|
||||
// Skip if it looks like a subgraph or keyword
|
||||
if (/^(subgraph|end|graph|flowchart|sequenceDiagram)\b/.test(text.trim())) {
|
||||
return match;
|
||||
}
|
||||
if (SPECIAL_CHARS_RE.test(text) && !text.startsWith('"')) {
|
||||
return `${open}"${text}"${close}`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user