setImagePreviewUrl(svgCode)}>
+ {svgString && (
+
-

{ setErrMsg('Chart rendering failed, please refresh and retry') }}
+ dangerouslySetInnerHTML={{ __html: svgString }}
/>
)}
diff --git a/web/app/components/base/mermaid/utils.ts b/web/app/components/base/mermaid/utils.ts
index 9936a9fc59..6dfd1dd9cd 100644
--- a/web/app/components/base/mermaid/utils.ts
+++ b/web/app/components/base/mermaid/utils.ts
@@ -3,59 +3,69 @@ export function cleanUpSvgCode(svgCode: string): string {
}
/**
- * Preprocesses mermaid code to fix common syntax issues
+ * Prepares mermaid code for rendering by sanitizing common syntax issues.
+ * @param {string} mermaidCode - The mermaid code to prepare
+ * @param {'classic' | 'handDrawn'} style - The rendering style
+ * @returns {string} - The prepared mermaid code
*/
-export function preprocessMermaidCode(code: string): string {
- if (!code || typeof code !== 'string')
+export const prepareMermaidCode = (mermaidCode: string, style: 'classic' | 'handDrawn'): string => {
+ if (!mermaidCode || typeof mermaidCode !== 'string')
return ''
- // First check if this is a gantt chart
- if (code.trim().startsWith('gantt')) {
- // For gantt charts, we need to ensure each task is on its own line
- // Split the code into lines and process each line separately
- const lines = code.split('\n').map(line => line.trim())
- return lines.join('\n')
+ let code = mermaidCode.trim()
+
+ // --- Start of robust sanitization for flowcharts ---
+
+ // 1. Ensure a direction is present for `graph` or `flowchart`.
+ if (code.startsWith('graph') || code.startsWith('flowchart')) {
+ const firstLine = code.split('\n')[0].trim();
+ if (!/^(graph|flowchart)\s+(TD|TB|LR|RL)/.test(firstLine))
+ code = code.replace(/^(graph|flowchart)/, '$1 TD');
}
- return code
- // Replace English colons with Chinese colons in section nodes to avoid parsing issues
- .replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}:`)
- // Fix common syntax issues
- .replace(/fifopacket/g, 'rect')
- // Ensure graph has direction
- .replace(/^graph\s+((?:TB|BT|RL|LR)*)/, (match, direction) => {
- return direction ? match : 'graph TD'
- })
- // Clean up empty lines and extra spaces
- .trim()
-}
+ // 2. Fix for subgraph titles with quotes, e.g., subgraph "title"
+ // Converts to the more robust `subgraph id[title]` syntax.
+ const subgraphReplacer = (match: string, title: string): string => {
+ // Create a valid ID from the title by removing any character that is not a
+ // letter, number, or underscore. This supports unicode characters.
+ const id = title.replace(/[^\p{L}\p{N}_]/gu, '');
-/**
- * Prepares mermaid code based on selected style
- */
-export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string {
- let finalCode = preprocessMermaidCode(code)
+ // If the ID is empty after sanitization (e.g., title was "---"),
+ // create a random fallback ID.
+ const finalId = id || `gen-id-${Math.random().toString(36).substring(2, 7)}`;
- // Special handling for gantt charts and mindmaps
- if (finalCode.trim().startsWith('gantt') || finalCode.trim().startsWith('mindmap')) {
- // For gantt charts and mindmaps, preserve the structure exactly as is
- return finalCode
- }
+ return `subgraph ${finalId} [${title}]`;
+ };
+
+ code = code.replace(/subgraph\s+"([^"]+)"/g, subgraphReplacer);
+ code = code.replace(/subgraph\s+'([^']+)'/g, subgraphReplacer);
+ // 3. Sanitize against javascript: protocol in click events (XSS vector)
+ code = code.replace(/(\bclick\s+\w+\s+")javascript:[^"]*(")/g, '$1#$2');
+
+ // 4. Fix for edge labels with quotes, e.g., -- "text" -->
+ code = code.replace(/(--\s*)"([^"]+)"(\s*--[->]?)/g, '$1$2$3');
+ code = code.replace(/(--\s*)'([^']+)'(\s*--[->]?)/g, '$1$2$3');
+
+ // 5. Basic BR replacement. This should be safe.
+ code = code.replace(/
/g, '\n');
+
+ // --- End of sanitization ---
+
+ let finalCode = code;
+
+ // Hand-drawn style requires some specific clean-up.
if (style === 'handDrawn') {
finalCode = finalCode
- // Remove style definitions that interfere with hand-drawn style
.replace(/style\s+[^\n]+/g, '')
.replace(/linkStyle\s+[^\n]+/g, '')
.replace(/^flowchart/, 'graph')
- // Remove any styles that might interfere with hand-drawn style
.replace(/class="[^"]*"/g, '')
.replace(/fill="[^"]*"/g, '')
- .replace(/stroke="[^"]*"/g, '')
+ .replace(/stroke="[^"]*"/g, '');
- // Ensure hand-drawn style charts always start with graph
if (!finalCode.startsWith('graph') && !finalCode.startsWith('flowchart'))
- finalCode = `graph TD\n${finalCode}`
+ finalCode = `graph TD\n${finalCode}`;
}
return finalCode
@@ -82,7 +92,6 @@ export function svgToBase64(svgGraph: string): Promise
{
})
}
catch (error) {
- console.error('Error converting SVG to base64:', error)
return Promise.resolve('')
}
}
@@ -115,13 +124,11 @@ export function processSvgForTheme(
}
else {
let i = 0
- themes.dark.nodeColors.forEach(() => {
- const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
- processedSvg = processedSvg.replace(regex, (match: string) => {
- const colorIndex = i % themes.dark.nodeColors.length
- i++
- return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.dark.nodeColors[colorIndex].bg}"`)
- })
+ const nodeColorRegex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
+ processedSvg = processedSvg.replace(nodeColorRegex, (match: string) => {
+ const colorIndex = i % themes.dark.nodeColors.length
+ i++
+ return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.dark.nodeColors[colorIndex].bg}"`)
})
processedSvg = processedSvg
@@ -139,14 +146,12 @@ export function processSvgForTheme(
.replace(/stroke-width="1"/g, 'stroke-width="1.5"')
}
else {
- themes.light.nodeColors.forEach(() => {
- const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
- let i = 0
- processedSvg = processedSvg.replace(regex, (match: string) => {
- const colorIndex = i % themes.light.nodeColors.length
- i++
- return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.light.nodeColors[colorIndex].bg}"`)
- })
+ let i = 0
+ const nodeColorRegex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
+ processedSvg = processedSvg.replace(nodeColorRegex, (match: string) => {
+ const colorIndex = i % themes.light.nodeColors.length
+ i++
+ return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.light.nodeColors[colorIndex].bg}"`)
})
processedSvg = processedSvg
@@ -187,24 +192,10 @@ export function isMermaidCodeComplete(code: string): boolean {
// Check for basic syntax structure
const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram|mindmap)/.test(trimmedCode)
- // Check for balanced brackets and parentheses
- const isBalanced = (() => {
- const stack = []
- const pairs = { '{': '}', '[': ']', '(': ')' }
-
- for (const char of trimmedCode) {
- if (char in pairs) {
- stack.push(char)
- }
- else if (Object.values(pairs).includes(char)) {
- const last = stack.pop()
- if (pairs[last as keyof typeof pairs] !== char)
- return false
- }
- }
-
- return stack.length === 0
- })()
+ // The balanced bracket check was too strict and produced false negatives for valid
+ // mermaid syntax like the asymmetric shape `A>B]`. Relying on Mermaid's own
+ // parser is more robust.
+ const isBalanced = true;
// Check for common syntax errors
const hasNoSyntaxErrors = !trimmedCode.includes('undefined')
@@ -215,7 +206,6 @@ export function isMermaidCodeComplete(code: string): boolean {
return hasValidStart && isBalanced && hasNoSyntaxErrors
}
catch (error) {
- console.debug('Mermaid code validation error:', error)
return false
}
}