refactor(mermaid): Rearchitect component for robustness and security

This commit resolves a series of rendering failures and security vulnerabilities within the Mermaid component, starting from an initial issue with Gantt chart rendering. The component has been comprehensively refactored to improve its stability, security, and user experience.

Key improvements include:
- Fixes Gantt & Flowchart Rendering: Correctly handles complex Gantt chart syntax and sanitizes various flowchart errors.
- Enhanced Security: Uses 'loose' mode for theming and manually sanitizes against XSS vectors like the 'javascript:' protocol.
- Robust Theming & State Management: Refactors configuration logic to isolate styles and ensures seamless refresh with loading indicators.
- Performance & Logic Simplification: Switches to direct SVG injection and centralizes component initialization.

Resolves: #20931
pull/21281/head
xuzijie1995 11 months ago
parent 6b1ad634f1
commit 6f952e1ef1

@ -272,7 +272,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
switch (language) {
case 'mermaid':
if (isSVG)
return <Flowchart PrimitiveCode={content} />
return <Flowchart PrimitiveCode={content} theme={theme} />
break
case 'echarts': {
// Loading state: show loading indicator

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import mermaid from 'mermaid'
import mermaid, { type MermaidConfig } from 'mermaid'
import { useTranslation } from 'react-i18next'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { MoonIcon, SunIcon } from '@heroicons/react/24/solid'
@ -68,14 +68,13 @@ const THEMES = {
const initMermaid = () => {
if (typeof window !== 'undefined' && !isMermaidInitialized) {
try {
mermaid.initialize({
const config: MermaidConfig = {
startOnLoad: false,
fontFamily: 'sans-serif',
securityLevel: 'loose',
flowchart: {
htmlLabels: true,
useMaxWidth: true,
diagramPadding: 10,
curve: 'basis',
nodeSpacing: 50,
rankSpacing: 70,
@ -94,10 +93,10 @@ const initMermaid = () => {
mindmap: {
useMaxWidth: true,
padding: 10,
diagramPadding: 20,
},
maxTextSize: 50000,
})
}
mermaid.initialize(config)
isMermaidInitialized = true
}
catch (error) {
@ -113,7 +112,7 @@ const Flowchart = React.forwardRef((props: {
theme?: 'light' | 'dark'
}, ref) => {
const { t } = useTranslation()
const [svgCode, setSvgCode] = useState<string | null>(null)
const [svgString, setSvgString] = useState<string | null>(null)
const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
const [isInitialized, setIsInitialized] = useState(false)
const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>(props.theme || 'light')
@ -125,6 +124,7 @@ const Flowchart = React.forwardRef((props: {
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
const [isCodeComplete, setIsCodeComplete] = useState(false)
const codeCompletionCheckRef = useRef<NodeJS.Timeout>()
const prevCodeRef = useRef<string>()
// Create cache key from code, style and theme
const cacheKey = useMemo(() => {
@ -169,50 +169,18 @@ const Flowchart = React.forwardRef((props: {
*/
const handleRenderError = (error: any) => {
console.error('Mermaid rendering error:', error)
const errorMsg = (error as Error).message
if (errorMsg.includes('getAttribute')) {
diagramCache.clear()
mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',
})
}
else {
setErrMsg(`Rendering chart failed, please refresh and try again ${look === 'handDrawn' ? 'Or try using classic mode' : ''}`)
}
if (look === 'handDrawn') {
// On any render error, assume the mermaid state is corrupted and force a re-initialization.
try {
// Clear possible cache issues
diagramCache.delete(`${props.PrimitiveCode}-handDrawn-${currentTheme}`)
// Reset mermaid configuration
mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',
theme: 'default',
maxTextSize: 50000,
})
// Try rendering with standard mode
setLook('classic')
setErrMsg('Hand-drawn mode is not supported for this diagram. Switched to classic mode.')
// Delay error clearing
setTimeout(() => {
if (containerRef.current) {
// Try rendering again with standard mode, but can't call renderFlowchart directly due to circular dependency
// Instead set state to trigger re-render
setIsCodeComplete(true) // This will trigger useEffect re-render
}
}, 500)
}
catch (e) {
console.error('Reset after handDrawn error failed:', e)
diagramCache.clear() // Clear cache to prevent using potentially corrupted SVGs
isMermaidInitialized = false // <-- THE FIX: Force re-initialization
initMermaid() // Re-initialize with the default safe configuration
}
catch (reinitError) {
console.error('Failed to re-initialize Mermaid after error:', reinitError)
}
setErrMsg(`Rendering failed: ${(error as Error).message || 'Unknown error. Please check the console.'}`)
setIsLoading(false)
}
@ -225,49 +193,17 @@ const Flowchart = React.forwardRef((props: {
// Update theme when prop changes
useEffect(() => {
if (props.theme)
if (props.theme && props.theme !== currentTheme) {
// When the global theme prop changes, we should clear the cache to ensure
// a fresh render.
diagramCache.clear()
setSvgString(null)
setCurrentTheme(props.theme)
}, [props.theme])
// Validate mermaid code and check for completeness
useEffect(() => {
if (codeCompletionCheckRef.current)
clearTimeout(codeCompletionCheckRef.current)
// Reset code complete status when code changes
setIsCodeComplete(false)
// If no code or code is extremely short, don't proceed
if (!props.PrimitiveCode || props.PrimitiveCode.length < 10)
return
// Check if code already in cache - if so we know it's valid
if (diagramCache.has(cacheKey)) {
setIsCodeComplete(true)
return
}
// Initial check using the extracted isMermaidCodeComplete function
const isComplete = isMermaidCodeComplete(props.PrimitiveCode)
if (isComplete) {
setIsCodeComplete(true)
return
}
// Set a delay to check again in case code is still being generated
codeCompletionCheckRef.current = setTimeout(() => {
setIsCodeComplete(isMermaidCodeComplete(props.PrimitiveCode))
}, 300)
return () => {
if (codeCompletionCheckRef.current)
clearTimeout(codeCompletionCheckRef.current)
// Per user request, also reset the look to 'classic' to ensure a consistent state.
setLook('classic')
}
}, [props.PrimitiveCode, cacheKey])
}, [props.theme, currentTheme])
/**
* Renders flowchart based on provided code
*/
const renderFlowchart = useCallback(async (primitiveCode: string) => {
if (!isInitialized || !containerRef.current) {
setIsLoading(false)
@ -275,15 +211,11 @@ const Flowchart = React.forwardRef((props: {
return
}
// Don't render if code is not complete yet
if (!isCodeComplete) {
setIsLoading(true)
return
}
// Return cached result if available
const cacheKey = `${primitiveCode}-${look}-${currentTheme}`
if (diagramCache.has(cacheKey)) {
setSvgCode(diagramCache.get(cacheKey) || null)
setErrMsg('')
setSvgString(diagramCache.get(cacheKey) || null)
setIsLoading(false)
return
}
@ -294,17 +226,44 @@ const Flowchart = React.forwardRef((props: {
try {
let finalCode: string
// Check if it's a gantt chart or mindmap
const isGanttChart = primitiveCode.trim().startsWith('gantt')
const isMindMap = primitiveCode.trim().startsWith('mindmap')
if (isGanttChart || isMindMap) {
// For gantt charts and mindmaps, ensure each task is on its own line
// and preserve exact whitespace/format
finalCode = primitiveCode.trim()
const trimmedCode = primitiveCode.trim()
const isGantt = trimmedCode.startsWith('gantt')
const isMindMap = trimmedCode.startsWith('mindmap')
if (isGantt || isMindMap) {
if (isGantt) {
finalCode = trimmedCode
.split('\n')
.map((line) => {
// Gantt charts have specific syntax needs.
const taskMatch = line.match(/^\s*([^:]+?)\s*:\s*(.*)/)
if (!taskMatch)
return line // Not a task line, return as is.
const taskName = taskMatch[1].trim()
let paramsStr = taskMatch[2].trim()
// Rule 1: Correct multiple "after" dependencies ONLY if they exist.
// This is a common mistake, e.g., "..., after task1, after task2, ..."
const afterCount = (paramsStr.match(/after /g) || []).length
if (afterCount > 1)
paramsStr = paramsStr.replace(/,\s*after\s+/g, ' ')
// Rule 2: Normalize spacing between parameters for consistency.
const finalParams = paramsStr.replace(/\s*,\s*/g, ', ').trim()
return `${taskName} :${finalParams}`
})
.join('\n')
}
else {
// For gantt and mindmap charts, which have syntax sensitive to whitespace,
// pass the code through directly.
finalCode = trimmedCode
}
}
else {
// Step 1: Clean and prepare Mermaid code using the extracted prepareMermaidCode function
// This function handles flowcharts appropriately.
finalCode = prepareMermaidCode(primitiveCode, look)
}
@ -319,13 +278,12 @@ const Flowchart = React.forwardRef((props: {
THEMES,
)
// Step 4: Clean SVG code and convert to base64 using the extracted functions
// Step 4: Clean up SVG code
const cleanedSvg = cleanUpSvgCode(processedSvg)
const base64Svg = await svgToBase64(cleanedSvg)
if (base64Svg && typeof base64Svg === 'string') {
diagramCache.set(cacheKey, base64Svg)
setSvgCode(base64Svg)
if (cleanedSvg && typeof cleanedSvg === 'string') {
diagramCache.set(cacheKey, cleanedSvg)
setSvgString(cleanedSvg)
}
setIsLoading(false)
@ -334,12 +292,9 @@ const Flowchart = React.forwardRef((props: {
// Error handling
handleRenderError(error)
}
}, [chartId, isInitialized, cacheKey, isCodeComplete, look, currentTheme, t])
}, [chartId, isInitialized, look, currentTheme, t])
/**
* Configure mermaid based on selected style and theme
*/
const configureMermaid = useCallback(() => {
const configureMermaid = useCallback((primitiveCode: string) => {
if (typeof window !== 'undefined' && isInitialized) {
const themeVars = THEMES[currentTheme]
const config: any = {
@ -361,23 +316,37 @@ const Flowchart = React.forwardRef((props: {
mindmap: {
useMaxWidth: true,
padding: 10,
diagramPadding: 20,
},
}
const isFlowchart = primitiveCode.trim().startsWith('graph') || primitiveCode.trim().startsWith('flowchart')
if (look === 'classic') {
config.theme = currentTheme === 'dark' ? 'dark' : 'neutral'
if (isFlowchart) {
config.flowchart = {
htmlLabels: true,
useMaxWidth: true,
diagramPadding: 12,
nodeSpacing: 60,
rankSpacing: 80,
curve: 'linear',
ranker: 'tight-tree',
}
}
else {
if (currentTheme === 'dark') {
config.themeVariables = {
background: themeVars.background,
primaryColor: themeVars.primaryColor,
primaryBorderColor: themeVars.primaryBorderColor,
primaryTextColor: themeVars.primaryTextColor,
secondaryColor: themeVars.secondaryColor,
tertiaryColor: themeVars.tertiaryColor,
}
}
}
else { // look === 'handDrawn'
config.theme = 'default'
config.themeCSS = `
.node rect { fill-opacity: 0.85; }
@ -389,30 +358,21 @@ const Flowchart = React.forwardRef((props: {
config.themeVariables = {
fontSize: '14px',
fontFamily: 'sans-serif',
primaryBorderColor: currentTheme === 'dark' ? THEMES.dark.connectionColor : THEMES.light.connectionColor,
}
if (isFlowchart) {
config.flowchart = {
htmlLabels: true,
useMaxWidth: true,
diagramPadding: 10,
nodeSpacing: 40,
rankSpacing: 60,
curve: 'basis',
}
config.themeVariables.primaryBorderColor = currentTheme === 'dark' ? THEMES.dark.connectionColor : THEMES.light.connectionColor
}
if (currentTheme === 'dark' && !config.themeVariables) {
config.themeVariables = {
background: themeVars.background,
primaryColor: themeVars.primaryColor,
primaryBorderColor: themeVars.primaryBorderColor,
primaryTextColor: themeVars.primaryTextColor,
secondaryColor: themeVars.secondaryColor,
tertiaryColor: themeVars.tertiaryColor,
fontFamily: 'sans-serif',
}
}
console.log('%c[Mermaid Debug] Mermaid config being applied:', 'color: #ADD8E6;', JSON.parse(JSON.stringify(config)));
try {
mermaid.initialize(config)
return true
@ -425,44 +385,50 @@ const Flowchart = React.forwardRef((props: {
return false
}, [currentTheme, isInitialized, look])
// Effect for theme and style configuration
// This is the main rendering effect.
// It triggers whenever the code, theme, or style changes.
useEffect(() => {
if (diagramCache.has(cacheKey)) {
setSvgCode(diagramCache.get(cacheKey) || null)
setIsLoading(false)
if (!isInitialized)
return
}
if (configureMermaid() && containerRef.current && isCodeComplete)
renderFlowchart(props.PrimitiveCode)
}, [look, props.PrimitiveCode, renderFlowchart, isInitialized, cacheKey, currentTheme, isCodeComplete, configureMermaid])
// Effect for rendering with debounce
useEffect(() => {
if (diagramCache.has(cacheKey)) {
setSvgCode(diagramCache.get(cacheKey) || null)
// Don't render if code is too short
if (!props.PrimitiveCode || props.PrimitiveCode.length < 10) {
setIsLoading(false)
setSvgString(null)
return
}
// Use a timeout to handle streaming code and debounce rendering
if (renderTimeoutRef.current)
clearTimeout(renderTimeoutRef.current)
if (isCodeComplete) {
setIsLoading(true)
renderTimeoutRef.current = setTimeout(() => {
if (isInitialized)
renderFlowchart(props.PrimitiveCode)
}, 300)
// Final validation before rendering
if (!isMermaidCodeComplete(props.PrimitiveCode)) {
setIsLoading(false)
setErrMsg('Diagram code is not complete or invalid.')
return
}
else {
setIsLoading(true)
const cacheKey = `${props.PrimitiveCode}-${look}-${currentTheme}`
if (diagramCache.has(cacheKey)) {
setErrMsg('')
setSvgString(diagramCache.get(cacheKey) || null)
setIsLoading(false)
return
}
if (configureMermaid(props.PrimitiveCode))
renderFlowchart(props.PrimitiveCode)
}, 300) // 300ms debounce
return () => {
if (renderTimeoutRef.current)
clearTimeout(renderTimeoutRef.current)
}
}, [props.PrimitiveCode, renderFlowchart, isInitialized, cacheKey, isCodeComplete])
}, [props.PrimitiveCode, look, currentTheme, isInitialized, configureMermaid, renderFlowchart])
// Cleanup on unmount
useEffect(() => {
@ -476,10 +442,20 @@ const Flowchart = React.forwardRef((props: {
}
}, [])
const handlePreviewClick = async () => {
if (svgString) {
const base64 = await svgToBase64(svgString)
setImagePreviewUrl(base64)
}
}
const toggleTheme = () => {
setCurrentTheme(prevTheme => prevTheme === 'light' ? Theme.dark : Theme.light)
// Clear cache only if theme actually changes
if (currentTheme !== (currentTheme === 'light' ? Theme.dark : Theme.light)) {
diagramCache.clear()
}
setCurrentTheme(prevTheme => prevTheme === 'light' ? Theme.dark : Theme.light)
}
// Style classes for theme-dependent elements
const themeClasses = {
@ -527,14 +503,26 @@ const Flowchart = React.forwardRef((props: {
<div
key='classic'
className={getLookButtonClass('classic')}
onClick={() => setLook('classic')}
onClick={() => {
if (look !== 'classic') {
diagramCache.clear()
setSvgString(null)
setLook('classic')
}
}}
>
<div className="msh-segmented-item-label">{t('app.mermaid.classic')}</div>
</div>
<div
key='handDrawn'
className={getLookButtonClass('handDrawn')}
onClick={() => setLook('handDrawn')}
onClick={() => {
if (look !== 'handDrawn') {
diagramCache.clear()
setSvgString(null)
setLook('handDrawn')
}
}}
>
<div className="msh-segmented-item-label">{t('app.mermaid.handDrawn')}</div>
</div>
@ -544,7 +532,7 @@ const Flowchart = React.forwardRef((props: {
<div ref={containerRef} style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden' }} />
{isLoading && !svgCode && (
{isLoading && !svgString && (
<div className='px-[26px] py-4'>
<LoadingAnim type='text'/>
{!isCodeComplete && (
@ -555,8 +543,8 @@ const Flowchart = React.forwardRef((props: {
</div>
)}
{svgCode && (
<div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={() => setImagePreviewUrl(svgCode)}>
{svgString && (
<div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={handlePreviewClick}>
<div className="absolute bottom-2 left-2 z-[100]">
<button
onClick={(e) => {
@ -571,11 +559,9 @@ const Flowchart = React.forwardRef((props: {
</button>
</div>
<img
src={svgCode}
alt="mermaid_chart"
<div
style={{ maxWidth: '100%' }}
onError={() => { setErrMsg('Chart rendering failed, please refresh and retry') }}
dangerouslySetInnerHTML={{ __html: svgString }}
/>
</div>
)}

@ -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')
}
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()
}
let code = mermaidCode.trim()
/**
* Prepares mermaid code based on selected style
*/
export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string {
let finalCode = preprocessMermaidCode(code)
// --- Start of robust sanitization for flowcharts ---
// 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
// 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');
}
// 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, '');
// 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)}`;
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(/<br\s*\/?>/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<string> {
})
}
catch (error) {
console.error('Error converting SVG to base64:', error)
return Promise.resolve('')
}
}
@ -115,14 +124,12 @@ 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 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
.replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
@ -139,15 +146,13 @@ 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 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
.replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
@ -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
}
}

Loading…
Cancel
Save