diff --git a/web/app/components/base/mermaid/index.tsx b/web/app/components/base/mermaid/index.tsx index 0df3543325..a953ef15a8 100644 --- a/web/app/components/base/mermaid/index.tsx +++ b/web/app/components/base/mermaid/index.tsx @@ -191,18 +191,22 @@ const Flowchart = React.forwardRef((props: { setIsInitialized(true) }, []) - // Update theme when prop changes + // Update theme when prop changes, but allow internal override. + const prevThemeRef = useRef() useEffect(() => { - if (props.theme && props.theme !== currentTheme) { - // When the global theme prop changes, we should clear the cache to ensure - // a fresh render. + // Only react if the theme prop from the outside has actually changed. + if (props.theme && props.theme !== prevThemeRef.current) { + // When the global theme prop changes, it should act as the source of truth, + // overriding any local theme selection. diagramCache.clear() setSvgString(null) setCurrentTheme(props.theme) - // Per user request, also reset the look to 'classic' to ensure a consistent state. + // Reset look to classic for a consistent state after a global change. setLook('classic') } - }, [props.theme, currentTheme]) + // Update the ref to the current prop value for the next render. + prevThemeRef.current = props.theme + }, [props.theme]) const renderFlowchart = useCallback(async (primitiveCode: string) => { if (!isInitialized || !containerRef.current) { @@ -229,8 +233,9 @@ const Flowchart = React.forwardRef((props: { const trimmedCode = primitiveCode.trim() const isGantt = trimmedCode.startsWith('gantt') const isMindMap = trimmedCode.startsWith('mindmap') + const isSequence = trimmedCode.startsWith('sequenceDiagram') - if (isGantt || isMindMap) { + if (isGantt || isMindMap || isSequence) { if (isGantt) { finalCode = trimmedCode .split('\n') @@ -256,7 +261,7 @@ const Flowchart = React.forwardRef((props: { .join('\n') } else { - // For gantt and mindmap charts, which have syntax sensitive to whitespace, + // For mindmap and sequence charts, which are sensitive to syntax, // pass the code through directly. finalCode = trimmedCode } @@ -372,7 +377,6 @@ const Flowchart = React.forwardRef((props: { } } - console.log('%c[Mermaid Debug] Mermaid config being applied:', 'color: #ADD8E6;', JSON.parse(JSON.stringify(config))); try { mermaid.initialize(config) return true @@ -437,8 +441,6 @@ const Flowchart = React.forwardRef((props: { containerRef.current.innerHTML = '' if (renderTimeoutRef.current) clearTimeout(renderTimeoutRef.current) - if (codeCompletionCheckRef.current) - clearTimeout(codeCompletionCheckRef.current) } }, []) @@ -450,11 +452,11 @@ const Flowchart = React.forwardRef((props: { } const toggleTheme = () => { - // 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) + const newTheme = currentTheme === 'light' ? 'dark' : 'light' + // Ensure a full, clean re-render cycle, consistent with global theme change. + diagramCache.clear() + setSvgString(null) + setCurrentTheme(newTheme) } // Style classes for theme-dependent elements diff --git a/web/app/components/base/mermaid/utils.ts b/web/app/components/base/mermaid/utils.ts index 6dfd1dd9cd..cc71572339 100644 --- a/web/app/components/base/mermaid/utils.ts +++ b/web/app/components/base/mermaid/utils.ts @@ -14,45 +14,13 @@ export const prepareMermaidCode = (mermaidCode: string, style: 'classic' | 'hand let code = mermaidCode.trim() - // --- Start of robust sanitization for flowcharts --- + // Security: Sanitize against javascript: protocol in click events (XSS vector) + code = code.replace(/(\bclick\s+\w+\s+")javascript:[^"]*(")/g, '$1#$2') - // 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(//g, '\n'); - - // --- End of sanitization --- + // Convenience: Basic BR replacement. This is a common and safe operation. + code = code.replace(//g, '\n') - let finalCode = code; + let finalCode = code // Hand-drawn style requires some specific clean-up. if (style === 'handDrawn') {