diff --git a/webviewUi/src/components/MermaidDiagram.tsx b/webviewUi/src/components/MermaidDiagram.tsx
index 88d8628..92ecf06 100644
--- a/webviewUi/src/components/MermaidDiagram.tsx
+++ b/webviewUi/src/components/MermaidDiagram.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useMemo, useRef, useState } from "react";
import mermaid from "mermaid";
import styled, { keyframes } from "styled-components";
@@ -6,7 +6,7 @@ import styled, { keyframes } from "styled-components";
mermaid.initialize({
startOnLoad: false,
theme: "dark",
- securityLevel: "loose",
+ securityLevel: "strict",
themeVariables: {
primaryColor: "#7c3aed",
primaryTextColor: "#e2e8f0",
@@ -39,7 +39,7 @@ mermaid.initialize({
sequenceNumberColor: "#e2e8f0",
},
flowchart: {
- htmlLabels: true,
+ htmlLabels: false,
curve: "basis",
},
sequence: {
@@ -202,17 +202,13 @@ const CodeToggle = styled.button`
}
`;
-const RawCode = styled.pre`
+const RawCode = styled.div`
margin-top: 12px;
- padding: 16px;
- background: rgba(20, 20, 35, 0.8);
- border-radius: 8px;
- border: 1px solid rgba(139, 92, 246, 0.2);
- overflow-x: auto;
- font-size: 12px;
- line-height: 1.6;
- color: #e2e8f0;
- font-family: 'Fira Code', 'Consolas', monospace;
+ background: rgba(13, 16, 34, 0.92);
+ border-radius: 10px;
+ border: 1px solid rgba(139, 92, 246, 0.25);
+ overflow: hidden;
+ box-shadow: inset 0 1px 0 rgba(124, 58, 237, 0.12);
.keyword {
color: #c792ea;
@@ -226,8 +222,124 @@ const RawCode = styled.pre`
.text {
color: #c3e88d;
}
+ .comment {
+ color: #6ee7b7;
+ }
`;
+const RawCodeMeta = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ background: linear-gradient(135deg, rgba(76, 29, 149, 0.55) 0%, rgba(30, 64, 175, 0.35) 100%);
+ border-bottom: 1px solid rgba(139, 92, 246, 0.25);
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: #c4b5fd;
+`;
+
+const RawCodeBadge = styled.span`
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ background: rgba(55, 48, 163, 0.6);
+ border: 1px solid rgba(139, 92, 246, 0.4);
+ border-radius: 999px;
+ padding: 4px 10px;
+ font-size: 10px;
+ letter-spacing: 0.08em;
+ color: #e0e7ff;
+`;
+
+const RawCodeContent = styled.div`
+ max-height: 360px;
+ overflow: auto;
+ padding: 14px 0;
+ font-family: 'Fira Code', 'Consolas', monospace;
+ font-size: 12px;
+ color: #e2e8f0;
+
+ .code-lines {
+ display: grid;
+ row-gap: 4px;
+ padding: 0 16px 8px 16px;
+ }
+
+ .code-line {
+ display: grid;
+ grid-template-columns: 44px 1fr;
+ gap: 12px;
+ align-items: start;
+ }
+
+ .code-line-number {
+ position: relative;
+ display: inline-flex;
+ justify-content: flex-end;
+ color: rgba(129, 140, 248, 0.9);
+ font-variant-numeric: tabular-nums;
+ padding-right: 12px;
+ border-right: 1px solid rgba(99, 102, 241, 0.2);
+ }
+
+ .code-line-text {
+ white-space: pre;
+ line-height: 1.5;
+ }
+`;
+
+const formatMermaidCode = (code: string): { html: string; lineCount: number } => {
+ const escapeHtml = (value: string): string =>
+ value
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+
+ const diagramKeywordPattern = /(flowchart|sequenceDiagram|classDiagram|stateDiagram(?:-v\d+)?|erDiagram|gantt|pie|journey|gitGraph|mindmap|timeline|sankey|block|architecture)/g;
+ const directivePattern = /(direction|TD|TB|BT|RL|LR|participant|actor|Note|loop|alt|else|end|opt|par|critical|break|state)/g;
+ const escapeRegex = (token: string) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const arrowTokens = ['-->', '==>', '<--', '-o->', '-x->', '-.->', '--o', '--x', '--|', '|--', '|>', '<|', '===', '---', '=='];
+ const arrowPattern = new RegExp(`(${arrowTokens.map(escapeRegex).join('|')})`, 'g');
+ const startStopPattern = /\[\*\]/g;
+
+ const highlightLine = (line: string): string => {
+ const escaped = escapeHtml(line);
+
+ if (/^\s*%%/.test(line)) {
+ return ``;
+ }
+
+ let highlighted = escaped;
+ highlighted = highlighted.replace(diagramKeywordPattern, '$1');
+ highlighted = highlighted.replace(directivePattern, '$1');
+ highlighted = highlighted.replace(arrowPattern, '$1');
+ highlighted = highlighted.replace(startStopPattern, '$&');
+ highlighted = highlighted.replace(/\b([A-Za-z][\w]*)\s*(?=\[|\{|\()/g, '$1');
+ highlighted = highlighted.replace(/(\[[^\]]+\])/g, '$1');
+
+ return highlighted;
+ };
+
+ const normalized = code.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+ const lines = normalized.split('\n');
+ const htmlLines = lines.map((line, index) => {
+ const highlighted = highlightLine(line);
+ const display = highlighted.length ? highlighted : ' ';
+ const lineNumber = (index + 1).toString().padStart(3, ' ');
+ const safeNumber = lineNumber.replace(/ /g, ' ');
+ return `
${safeNumber}${display}
`;
+ });
+
+ return {
+ html: `${htmlLines.join('')}
`,
+ lineCount: lines.length,
+ };
+};
+
export const MermaidDiagram: React.FC = ({ chart }) => {
const containerRef = useRef(null);
const [svg, setSvg] = useState("");
@@ -235,6 +347,20 @@ export const MermaidDiagram: React.FC = ({ chart }) => {
const [isLoading, setIsLoading] = useState(true);
const [copied, setCopied] = useState(false);
const [showRawCode, setShowRawCode] = useState(false);
+ const formattedCode = useMemo(() => formatMermaidCode(chart), [chart]);
+ const lineSummary = `${formattedCode.lineCount} ${formattedCode.lineCount === 1 ? 'line' : 'lines'}`;
+
+ const renderRawCodePanel = () => (
+
+
+ Mermaid Source
+ {lineSummary}
+
+
+
+
+
+ );
// Minimal sanitization - the code should now come through cleanly via base64
const sanitizeMermaidCode = (code: string): string => {
@@ -302,16 +428,30 @@ export const MermaidDiagram: React.FC = ({ chart }) => {
seqFixed = seqFixed.replace(/Note\s+over\s+/gi, 'Note over ');
seqFixed = seqFixed.replace(/Note\s+left\s+of\s+/gi, 'Note left of ');
seqFixed = seqFixed.replace(/Note\s+right\s+of\s+/gi, 'Note right of ');
+ // Remove numbering accidentally appended to end statements
+ seqFixed = seqFixed.replace(/end\s*\d+/gi, 'end');
if (seqFixed !== code) fixes.push(seqFixed);
}
// Fix 5: Fix flowchart specific issues
if (code.includes('flowchart') || code.includes('graph')) {
- // Fix arrow syntax issues
let flowFixed = code
- .replace(/-->\|([^|]+)\|>/g, '-->|$1|') // Remove extra > after label
- .replace(/-+>/g, '-->') // Normalize arrows
- .replace(/=+>/g, '==>'); // Normalize thick arrows
+ // Remove extra > after label arrows
+ .replace(/--\|([^|]+)\|>/g, '-->|$1|')
+ // Normalize arrows like -> or ---> to -->
+ .replace(/-+>/g, '-->')
+ // Normalize thick arrows like => or ===>
+ .replace(/=+>/g, '==>')
+ // Normalize reverse arrows like <- or <---
+ .replace(/<-+/g, '<--')
+ // Fix circle arrow shorthand -o> to -o->
+ .replace(/-o>/g, '-o->')
+ // Convert single hyphen edge definitions to proper arrows
+ .replace(/^(\s*[A-Za-z0-9_]+)\s*-\s+(\w+)/gm, '$1 --> $2')
+ .replace(/^(\s*[A-Za-z0-9_]+)\s*-\s+\|([^|]+)\|\s*(\w+)/gm, '$1 -->|$2| $3')
+ // Replace colon direction declarations like "graph: TD" with valid syntax
+ .replace(/^(graph|flowchart)\s*:\s*/gm, '$1 ');
+
if (flowFixed !== code) fixes.push(flowFixed);
}
@@ -328,7 +468,14 @@ export const MermaidDiagram: React.FC = ({ chart }) => {
const noHtml = code.replace(/<[^>]+>/g, '');
if (noHtml !== code) fixes.push(noHtml);
- // Fix 8: Combine multiple fixes
+ // Fix 8: Remove duplicated node identifiers appended after definitions (e.g., N[Label]N -->)
+ const removeDuplicateNodeIds = code.replace(/([A-Za-z0-9_]+)(\[[^\]]+\])\1/g, '$1$2');
+ if (removeDuplicateNodeIds !== code) fixes.push(removeDuplicateNodeIds);
+
+ const fixTrailingNodeAfterBracket = code.replace(/(\[[^\]]+\])[A-Za-z0-9_]+(\s*-->|\s*$)/g, '$1$2');
+ if (fixTrailingNodeAfterBracket !== code) fixes.push(fixTrailingNodeAfterBracket);
+
+ // Fix 9: Combine multiple fixes
let combinedFix = code
.replace(/(\w)\s*&\s*(\w)/g, '$1 and $2')
.replace(/[""'']/g, '"')
@@ -346,7 +493,57 @@ export const MermaidDiagram: React.FC = ({ chart }) => {
combinedFix = combinedLines.join('\n');
if (combinedFix !== code) fixes.push(combinedFix);
-
+
+ // Fix 10: Replace semicolon separators with newlines for flowcharts/sequence diagrams
+ const semicolonSplit = code.replace(/;\s*/g, '\n');
+ if (semicolonSplit !== code) fixes.push(semicolonSplit);
+
+ // Fix 11: Remove numbered list prefixes (e.g., "1.", "2)") that break syntax
+ const numberStripped = code
+ .split('\n')
+ .map(line => line.replace(/^\s*\d+[\.)]\s*/, ''))
+ .join('\n');
+ if (numberStripped !== code) fixes.push(numberStripped);
+
+ // Fix 12: Ensure standalone end statements don't have trailing text
+ const cleanEndDigits = code.replace(/\bend\s*\d+\b/gi, 'end');
+ if (cleanEndDigits !== code) fixes.push(cleanEndDigits);
+
+ // Fix 13: Merge multiline node labels where bracketed text spilled onto the next line
+ // const mergedLabelLines = (() => {
+ // const labelLines = code.split('\n');
+ // const rebuilt: string[] = [];
+
+ // for (const current of labelLines) {
+ // if (rebuilt.length) {
+ // const previous = rebuilt[rebuilt.length - 1];
+ // const openSquare = (previous.match(/\[/g) || []).length;
+ // const closeSquare = (previous.match(/\]/g) || []).length;
+ // const openCurly = (previous.match(/\{/g) || []).length;
+ // const closeCurly = (previous.match(/\}/g) || []).length;
+ // const openParen = (previous.match(/\(/g) || []).length;
+ // const closeParen = (previous.match(/\)/g) || []).length;
+
+ // const hasUnclosedBracket = openSquare > closeSquare || openCurly > closeCurly || openParen > closeParen;
+ // const trimmedCurrent = current.trim();
+ // const looksLikeContinuation =
+ // trimmedCurrent.length > 0 &&
+ // !/^\s*[A-Za-z0-9_]+\s*[\[\{\(]/.test(current) &&
+ // !/(-->|==>|<--|\|>|::|:::)/.test(current);
+
+ // if (hasUnclosedBracket && looksLikeContinuation) {
+ // rebuilt[rebuilt.length - 1] = `${previous.trimEnd()} ${trimmedCurrent}`;
+ // continue;
+ // }
+ // }
+
+ // rebuilt.push(current);
+ // }
+
+ // return rebuilt.join('\n');
+ // })();
+ // if (mergedLabelLines !== code) fixes.push(mergedLabelLines);
+
// Remove duplicates and the original code
return [...new Set(fixes)].filter(f => f !== code);
};
@@ -414,24 +611,6 @@ export const MermaidDiagram: React.FC = ({ chart }) => {
console.error("Failed to copy:", err);
}
};
-
- // Simple syntax highlighting for Mermaid code
- const highlightMermaidCode = (code: string): string => {
- return code
- // Highlight diagram type keywords
- .replace(/^(flowchart|sequenceDiagram|classDiagram|stateDiagram|erDiagram|gantt|pie|journey|gitGraph|mindmap|timeline|sankey|block|architecture)/gm,
- '$1')
- // Highlight direction keywords
- .replace(/\b(TD|TB|BT|RL|LR|participant|actor|Note|loop|alt|else|end|opt|par|critical|break)\b/g,
- '$1')
- // Highlight arrows
- .replace(/(-->|--o|--x|-.->|-.-|==>|==|---|\|>|<\||--\||>|<)/g,
- '$1')
- // Highlight node IDs (before brackets)
- .replace(/\b([A-Z][a-zA-Z0-9_]*)\s*[\[\{\(]/g,
- '$1[');
- };
-
return (
@@ -462,9 +641,7 @@ export const MermaidDiagram: React.FC = ({ chart }) => {
{showRawCode ? 'Hide' : 'Show'} Raw Diagram Code
- {showRawCode && (
-
- )}
+ {showRawCode && renderRawCodePanel()}
)}
@@ -478,9 +655,7 @@ export const MermaidDiagram: React.FC = ({ chart }) => {
{showRawCode ? 'Hide' : 'Show'} Source Code
- {showRawCode && (
-
- )}
+ {showRawCode && renderRawCodePanel()}
>
)}