diff --git a/antlr-4.13.1-complete.jar b/antlr-4.13.1-complete.jar deleted file mode 100644 index f539ab040..000000000 Binary files a/antlr-4.13.1-complete.jar and /dev/null differ diff --git a/antlr-4.13.2-complete.jar b/antlr-4.13.2-complete.jar deleted file mode 100644 index 75bfcc397..000000000 Binary files a/antlr-4.13.2-complete.jar and /dev/null differ diff --git a/packages/mermaid/src/diagrams/flowchart/parser/antlr/FlowParserVisitor.ts b/packages/mermaid/src/diagrams/flowchart/parser/antlr/FlowParserVisitor.ts deleted file mode 100644 index a4ef90c58..000000000 --- a/packages/mermaid/src/diagrams/flowchart/parser/antlr/FlowParserVisitor.ts +++ /dev/null @@ -1,2261 +0,0 @@ -/** - * ANTLR Visitor for Mermaid Flowchart Parser - * - * This visitor walks the ANTLR parse tree and calls the appropriate FlowDB methods - * to build the flowchart data structure, providing equivalent functionality to the - * original Jison parser. - */ - -import type { FlowDB } from '../../flowDb.js'; -import type { FlowVertexTypeParam } from '../../types.js'; - -interface Connection { - fromNode: string; - toNode: string; - edgeType: string; - edgeText?: string; - edgeTextType?: string; // 'text', 'string', or 'markdown' - edgeId?: string; - originalStartEdge?: string; - originalEndEdge?: string; - length?: number; // Pre-calculated length for specific patterns -} - -export class FlowParserVisitor { - private db: FlowDB; - - constructor(db: FlowDB) { - this.db = db; - } - - // Main visit method that processes the parse tree - visit(ctx: any): void { - if (!ctx) return; - - const text = ctx.getText(); - const contextName = ctx.constructor.name; - - console.log('visit called with context:', contextName, 'text:', text); - - // Only process specific contexts to avoid duplicates - if (contextName === 'StartContext') { - console.log('Processing StartContext'); - // Parse direction from graph declaration - // Let FlowDB handle direction symbol mapping just like Jison does - const directionPattern = /graph\s+(td|tb|bt|rl|lr|>|<|\^|v)/i; - const dirMatch = text.match(directionPattern); - if (dirMatch) { - // Pass the raw direction value to FlowDB - it will handle symbol mapping - this.db.setDirection(dirMatch[1]); - } else { - // Set default direction - this.db.setDirection('TB'); - } - - // Process the entire flowchart at the top level with subgraph support - this.parseFlowchartContentWithSubgraphs(text); - } - - // Visit children recursively - this.visitChildren(ctx); - } - - private visitChildren(ctx: any): void { - if (!ctx?.children) { - return; - } - - for (const child of ctx.children) { - this.visit(child); - } - } - - // Fallback method to handle parsing failures gracefully - visitErrorNode(node: any): void { - // When ANTLR parser fails, we can still try to extract useful information - // from the error node text using our regex patterns - if (node && node.getText) { - const text = node.getText(); - if (text && text.trim()) { - this.parseFlowchartContent(text); - } - } - } - - private parseFlowchartContent(text: string): void { - // Parse different types of connections and nodes - this.parseConnections(text); - this.parseStandaloneNodes(text); - this.parseShapeData(text); - this.parseClickStatements(text); - this.parseLinkStyleStatements(text); - this.parseEdgeCurveProperties(text); - } - - private preprocessChainedConnections(text: string): string { - // Handle chained connections like "A e1@-->B-->D" -> "A e1@-->B\nB-->D" - // Process line by line to avoid complex regex issues - - const lines = text.split('\n'); - const processedLines: string[] = []; - - for (const line of lines) { - const trimmedLine = line.trim(); - - // Skip lines that don't contain connections or contain edge curve properties like @{curve: ...} - // But allow node shape data like A@{label: "text"} - const isEdgeCurveProperty = trimmedLine.includes('@{') && trimmedLine.includes('curve:'); - if (!trimmedLine || !trimmedLine.includes('-->') || isEdgeCurveProperty) { - processedLines.push(line); - continue; - } - - // Handle specific case: "A-->C e4@-->D-->E" should become "A-->C\nC e4@-->D\nD-->E" - if (trimmedLine.includes(' e4@-->')) { - // Special handling for the test case - const parts = trimmedLine.split(' e4@-->'); - if (parts.length === 2) { - const firstConnection = parts[0]; // "A-->C" - const secondPart = 'C e4@-->' + parts[1]; // "C e4@-->D-->E" - - // Process the second part for chaining - const chainPattern = - /([A-Za-z0-9_]+(?:\s+[A-Za-z0-9_]+@)?)(-->)([A-Za-z0-9_]+)((?:-->)[A-Za-z0-9_]+)+/; - const match = chainPattern.exec(secondPart); - - if (match) { - const [, firstPart, arrow, secondNode, remaining] = match; - let result = firstConnection + '\n' + `${firstPart}${arrow}${secondNode}`; - - // Process remaining chain - const remainingConnections = remaining.match(/(-->)([A-Za-z0-9_]+)/g); - if (remainingConnections) { - let currentNode = secondNode; - for (const conn of remainingConnections) { - const connMatch = conn.match(/(-->)([A-Za-z0-9_]+)/); - if (connMatch) { - const [, edge, nextNode] = connMatch; - result += `\n${currentNode}${edge}${nextNode}`; - currentNode = nextNode; - } - } - } - - processedLines.push(result); - } else { - processedLines.push(firstConnection + '\n' + secondPart); - } - } else { - processedLines.push(line); - } - } else { - // Handle chained connections with edge text - // Pattern: A[...]-- "text" -->B[...] -- "text2" -->C - const arrowCount = (trimmedLine.match(/-->/g) || []).length; - - if (arrowCount > 1) { - // Multiple arrows detected - attempt to split the connection - const connections = []; - let remaining = trimmedLine; - - // Use a pattern that handles complex nodes and edge text - const connectionPattern = - /^([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*(-+)\s*(.+?)\s*(--?>)\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*(.*)$/; - - while (remaining?.includes('-->')) { - const match = remaining.match(connectionPattern); - if (match) { - const [, fromNode, startEdge, edgeText, endEdge, toNode, rest] = match; - - // Create the connection - const connection = `${fromNode} ${startEdge} ${edgeText} ${endEdge} ${toNode}`; - connections.push(connection); - - // Prepare for next iteration - remaining = rest.trim(); - if (remaining && !remaining.startsWith(toNode)) { - // Prepend the target node if it's not already there - remaining = `${toNode} ${remaining}`; - } - } else { - // Couldn't parse further, add remaining as-is - if (remaining.trim()) { - connections.push(remaining.trim()); - } - break; - } - } - - if (connections.length > 1) { - processedLines.push(...connections); - continue; - } - } - - // Regular chained connection handling (simple case without edge text) - const chainPattern = - /([A-Za-z0-9_]+(?:\s+[A-Za-z0-9_]+@)?)(-->)([A-Za-z0-9_]+)((?:-->)[A-Za-z0-9_]+)+/; - const match = chainPattern.exec(trimmedLine); - - if (match) { - const [, firstPart, arrow, secondNode, remaining] = match; - let result = `${firstPart}${arrow}${secondNode}`; - - // Process remaining chain - const remainingConnections = remaining.match(/(-->)([A-Za-z0-9_]+)/g); - if (remainingConnections) { - let currentNode = secondNode; - for (const conn of remainingConnections) { - const connMatch = conn.match(/(-->)([A-Za-z0-9_]+)/); - if (connMatch) { - const [, edge, nextNode] = connMatch; - result += `\n${currentNode}${edge}${nextNode}`; - currentNode = nextNode; - } - } - } - - processedLines.push(result); - } else { - processedLines.push(line); - } - } - } - - const result = processedLines.join('\n'); - return result; - } - - private parseConnections(text: string): void { - // First, preprocess the text to handle chained connections - // Convert "A e1@-->B-->D" to "A e1@-->B\nB-->D" - const preprocessedText = this.preprocessChainedConnections(text); - - // Comprehensive pattern for connections with various edge types and optional text - // Supports all edge types from flow-edges.spec.js including: - // - Basic: A-->B, A==>B, A-.->B, A---B, A--xB, A--oB - // - With text: A-->|text|B, A-- text --B, A== text ==B, A-. text .-B - // - Bidirectional: A<-->B, A<==>B, A<-.->B, Ax--x B, Ao--oB - // - Variable length: A----B, A=====B, A-...-B - // - With IDs: A e1@-->B, A id@--xB - // - Node shapes: A[Text]-->B[Text], A((Text))-->B((Text)), etc. - // Order matters: more specific patterns first to avoid overlapping matches - const connectionPatterns = [ - // Dotted double-ended edges with variable length: A <-. Label ..-> B, A <-... Label ...-> B - // This pattern handles the specific failing cases with post-processing for length calculation - // Pattern: A <-. Label ..-> B where the right side has variable dots (length = right dots count) - // IMPORTANT: Requires non-empty text to avoid matching A <-..-> B (which should be handled by Pattern 1) - // Simplified node pattern to avoid regex complexity issues - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*<-\.\s*([^-=.<>]+?)\s*(\.+)->\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // Dotted double-ended edges without text: A <-..-> B, A <-...-> B (length = dots count) - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*<-(\.{2,})->\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // Edge IDs with double-ended edges with text: A e1@x-- text --x B, A id@o== label ==o B, A e1@<-- text -->B, A e1@x-. label .-x B - // This must come FIRST to avoid being matched by the general Edge ID pattern - // Simplified node pattern to avoid regex complexity issues - // Updated to handle single characters for dotted patterns: x-. label .-x - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s+([A-Za-z0-9_]+)@(x|o|<)([-=.]+)\s+(.+?)\s+([-=.]+)(x|o|>)\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // Edge IDs with various patterns: A e1@-->B, A id@--xB, A e1@== text ==B, A e1@.->B, A e1@.-xB, A e1@-.->B - // Simplified node pattern to avoid regex complexity issues - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s+([A-Za-z0-9_]+)@([-.=x>o]+)\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // Edge IDs with text: A e1@-- text --B, A id@== label ==B, A e1@-. text .-x B - // Simplified node pattern to avoid regex complexity issues - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s+([A-Za-z0-9_]+)@((?:x|o)?(?:-+|=+|\.+)|-\.)\s+([^-=.]+?)\s+((?:-+|=+|\.+)(?:x|o|>)?|\.+(?:->|x|o|-)+)\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // Double-ended edges with text: A x-- text --x B, A o== label ==o B, A<-- text -->B, A x-. text .-x B - // Simplified node pattern to avoid regex complexity issues - // Updated to handle single characters for dotted patterns: x-. text .-x - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*(x|o|<)([-=.]+)\s+(.+?)\s+([-=.]+)(x|o|>)\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // Text between dashes/equals/dots: A-- text --B, A== label ==B, A-. text .-B, A -- Label --> B, A-- text --x B, A-- text --o B - // Simplified node pattern to avoid regex complexity issues - // Updated to handle arrow endings: --> ==> .-> etc. - // FIXED: Added mixed dash-dot patterns for dotted labelled edges - // FIXED: Handle both spaced (A-- text -->B) and non-spaced (A--text-->B) patterns - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*(-+|=+|\.+|-\.|-\.\.|-.\.\.)\s*(.+?)\s*([-=.]+>|[-=.]+x|[-=.]+o|[-=.]+|\.-|\.\.|-|\.\.\.)\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // Double-ended edges: A<-->B, A<==>B, A<-.->B, Ax--xB, Ao--oB, Ax-.-xB - // Simplified node pattern to avoid regex complexity issues - // Updated to handle single characters for dotted patterns: x-.-x - // MOVED UP: Must be checked before single-ended edges to avoid pattern conflicts - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*(x|o|<)([-=.]+)(x|o|>)\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // Edge with text in pipes: A-->|text|B, A==>|text|B, A-.->|text|B, A---|text|B, A--x|text|B, A--o|text|B - // Simplified node pattern to avoid regex complexity issues - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*(-+>|=+>|\.+->|-+x|=+x|\.+x|-+o|=+o|\.+o|-+|=+|\.+)\s*\|([^|]+)\|\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - // All edge types: A-->B, A==>B, A-.->B, A---B, A==B, A..B, A-.-B, A--xB, A--oB (including variable length) - // Simplified node pattern to avoid regex complexity issues - // FIXED: Added missing =+ and \.+ patterns for thick and dotted edges without endings - // FIXED: Added mixed dash-dot patterns like -.- for dotted edges - // FIXED: Added mixed dash-dot-arrow patterns like -.-> for dotted edges with arrows - // FIXED: Require at least 2 dashes/equals/dots to avoid matching node IDs with single hyphens - /([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)\s*(-{2,}>|={2,}>|\.{2,}->|-\.{2,}->|-{2,}x|={2,}x|\.{2,}x|-{2,}o|={2,}o|\.{2,}o|-{2,}|={2,}|\.{2,}|-\.-|-\.\.-|-\.\.\.-)\s*([A-Za-z0-9_]+(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})*)/g, - ]; - - // Collect all connections first - const connections: Connection[] = []; - - // Collect all matches first, then sort by position to ensure text order processing - const allMatches: Array<{ - match: RegExpExecArray; - patternIndex: number; - position: number; - }> = []; - - for (let patternIndex = 0; patternIndex < connectionPatterns.length; patternIndex++) { - const pattern = connectionPatterns[patternIndex]; - let match; - while ((match = pattern.exec(preprocessedText)) !== null) { - allMatches.push({ - match, - patternIndex, - position: match.index!, - }); - } - } - - // Sort matches by position to process in text order - allMatches.sort((a, b) => a.position - b.position); - - // Track processed text ranges to avoid overlapping matches - const processedRanges: Array<{ start: number; end: number }> = []; - - for (const { match, patternIndex, position } of allMatches) { - const matchStart = position; - const matchEnd = position + match[0].length; - - // Check if this match overlaps with any previously processed range - const overlaps = processedRanges.some( - (range) => - (matchStart >= range.start && matchStart < range.end) || - (matchEnd > range.start && matchEnd <= range.end) || - (matchStart <= range.start && matchEnd >= range.end) - ); - - if (overlaps) { - continue; // Skip overlapping matches - } - - // Mark this range as processed - processedRanges.push({ start: matchStart, end: matchEnd }); - - // Process the match based on its structure and pattern type - this.processEdgeMatch(match, connections, patternIndex); - } - - // Collect all unique nodes and prioritize detailed ones - const nodeMap = new Map< - string, - { id: string; text: any; shape: string | undefined; priority: number } - >(); - - for (const conn of connections) { - const fromData = this.parseNode(conn.fromNode); - const toData = this.parseNode(conn.toNode); - - // Priority: shaped nodes (3) > custom text (2) > plain nodes (1) - const fromPriority = fromData.shape ? 3 : fromData.text.text !== fromData.id ? 2 : 1; - const toPriority = toData.shape ? 3 : toData.text.text !== toData.id ? 2 : 1; - - // Only update if this has higher priority - if (!nodeMap.has(fromData.id) || nodeMap.get(fromData.id)!.priority < fromPriority) { - nodeMap.set(fromData.id, { ...fromData, priority: fromPriority }); - } - if (!nodeMap.has(toData.id) || nodeMap.get(toData.id)!.priority < toPriority) { - nodeMap.set(toData.id, { ...toData, priority: toPriority }); - } - } - - // Add all nodes with their best definitions - for (const nodeData of nodeMap.values()) { - this.db.addVertex(nodeData.id, nodeData.text, nodeData.shape, [], [], '', {}, undefined); - } - - // Now process all connections - for (const conn of connections) { - const fromData = this.parseNode(conn.fromNode); - const toData = this.parseNode(conn.toNode); - const linkData = this.getEdgeProperties(conn.edgeType, conn); - - this.db.addLink([fromData.id], [toData.id], linkData); - } - } - - private parseStandaloneNodes(text: string): void { - // Parse nodes that might not be in connections (for completeness) - // Include markdown string support and shape data - - // First, handle shape data nodes separately - this.parseShapeDataNodes(text); - - const nodePatterns = [ - // IMPORTANT: Specific bracket patterns MUST come before general square bracket pattern - // Trapezoid nodes: A[/Text\] - /([A-Za-z0-9_]+)\[\/([^\\]+)\\\]/g, - // Inverted trapezoid nodes: A[\Text/] - /([A-Za-z0-9_]+)\[\\([^\/]+)\/\]/g, - // Lean right nodes: A[/Text/] - Allow forward slashes in text - /([A-Za-z0-9_]+)\[\/(.+?)\/\]/g, - // Lean left nodes: A[\Text\] - Allow backslashes in text - /([A-Za-z0-9_]+)\[\\(.+?)\\\]/g, - // Rect nodes with properties: A[|field:value|Text] - /([A-Za-z0-9_]+)\[\|([^:]+):([^|]+)\|([^\]]+)\]/g, - // Markdown strings in square brackets: A["`Text`"] - /([A-Za-z0-9_]+)\["`([^`]*)`"\]/g, - // Regular quoted strings in square brackets: A["Text"] - /([A-Za-z0-9_]+)\["([^"]*)"\]/g, - // Unquoted text in square brackets: A[Text] - MUST come AFTER specific bracket patterns - /([A-Za-z0-9_]+)\[([^\]]+)\]/g, - // Double circle nodes with markdown: A((("`Text`"))) - /([A-Za-z0-9_]+)\(\(\("`([^`]*)`"\)\)\)/g, - // Double circle nodes with quotes: A((("Text"))) - /([A-Za-z0-9_]+)\(\(\("([^"]*)"\)\)\)/g, - // Double circle nodes unquoted: A(((Text))) - /([A-Za-z0-9_]+)\(\(\(([^)]+)\)\)\)/g, - // Circle nodes with markdown: A(("`Text`")) - /([A-Za-z0-9_]+)\(\("`([^`]*)`"\)\)/g, - // Circle nodes with quotes: A(("Text")) - /([A-Za-z0-9_]+)\(\("([^"]*)"\)\)/g, - // Circle nodes unquoted: A((Text)) - MUST come before round nodes - /([A-Za-z0-9_]+)\(\(([^)]+)\)\)/g, - // Round nodes with markdown: A("`Text`") - MUST come before unquoted round nodes - /([A-Za-z0-9_]+)\("`([^`]*)`"\)/g, - // Round nodes with quotes: A("Text") - MUST come before unquoted round nodes - /([A-Za-z0-9_]+)\("([^"]*)"\)/g, - // Round nodes unquoted: A(Text) - MUST come LAST to avoid conflicts - /([A-Za-z0-9_]+)\(([^)]+)\)/g, - // Diamond nodes with markdown: A{"`Text`"} - /([A-Za-z0-9_]+)\{"`([^`]*)`"\}/g, - // Diamond nodes with quotes: A{"Text"} - /([A-Za-z0-9_]+)\{"([^"]*)"\}/g, - // Diamond nodes unquoted: A{Text} - /([A-Za-z0-9_]+)\{([^}]+)\}/g, - // Stadium nodes: A([Text]) - Handle before ellipse and round nodes - /([A-Za-z0-9_]+)\(\[([^\]]+)\]\)/g, - // Ellipse nodes: A(-Text-) - Allow hyphens within text - /([A-Za-z0-9_]+)\(-(.+?)-\)/g, - // Subroutine nodes: A[[Text]] - /([A-Za-z0-9_]+)\[\[([^\]]+)\]\]/g, - // Cylinder nodes: A[(Text)] - /([A-Za-z0-9_]+)\[\(([^)]+)\)\]/g, - // Hexagon nodes with markdown: A{{"`Text`"}} - /([A-Za-z0-9_]+)\{\{"`([^`]*)`"\}\}/g, - // Hexagon nodes with quotes: A{{"Text"}} - /([A-Za-z0-9_]+)\{\{"([^"]*)"\}\}/g, - // Hexagon nodes unquoted: A{{Text}} - /([A-Za-z0-9_]+)\{\{([^}]+)\}\}/g, - // Odd nodes with markdown: A>"`Text`"] - /([A-Za-z0-9_]+)>"`([^`]*)`"\]/g, - // Odd nodes with quotes: A>"Text"] - /([A-Za-z0-9_]+)>"([^"]*)"\]/g, - // Odd nodes unquoted: A>Text] - /([A-Za-z0-9_]+)>([^\]]+)\]/g, - ]; - - for (const pattern of nodePatterns) { - let match; - while ((match = pattern.exec(text)) !== null) { - // Parse the node and add it to the database - const nodeData = this.parseNode(match[0]); - this.db.addVertex(nodeData.id, nodeData.text, nodeData.shape, [], [], '', {}, undefined); - } - } - - // Also handle simple standalone nodes without shapes (like "A;", "i-d;") - const simpleNodePattern = /([A-Za-z0-9_-]+)\s*;/g; - let match; - while ((match = simpleNodePattern.exec(text)) !== null) { - const [, nodeId] = match; - // Only add if not already processed by shape patterns - if (!this.db.getVertices().has(nodeId)) { - this.db.addVertex( - nodeId, - { text: nodeId, type: 'text' }, - undefined, - [], - [], - '', - {}, - undefined - ); - } - } - } - - private processEdgeMatch( - match: RegExpExecArray, - connections: Connection[], - patternIndex: number - ): void { - const matchStr = match[0]; - - // Pattern 0: Dotted double-ended edges with variable length: A <-. Label ..-> B - if (patternIndex === 0 && match.length === 5) { - const [, fromNode, edgeText, endDots, toNode] = match; - // Construct the edge type: <-. + endDots + -> - const edgeType = '<-.' + endDots + '->'; - - const parsedEdgeText = this.parseEdgeText(edgeText); - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: parsedEdgeText.text, - edgeTextType: parsedEdgeText.type, - // Store original parts for length calculation - originalStartEdge: '<-.', - originalEndEdge: endDots + '->', - // Store the calculated length directly for dotted edges (based on right side dots) - length: endDots.length, - }); - return; - } - - // Pattern 1: Dotted double-ended edges without text: A <-..-> B, A <-...-> B - if (patternIndex === 1 && match.length === 4) { - const [, fromNode, dots, toNode] = match; - // Construct the edge type: <- + dots + -> - const edgeType = '<-' + dots + '->'; - - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: '', // No text for this pattern - // Store original parts for length calculation - originalStartEdge: '<-', - originalEndEdge: dots + '->', - // Store the calculated length directly for dotted edges (based on dots count) - length: dots.length, - }); - return; - } - - // Pattern 1a: Edge IDs with double-ended edges with text: A e1@x-- text --x B, A id@o== label ==o B, A e1@<-- text -->B - if (match.length === 9 && matchStr.includes('@')) { - const [, fromNode, edgeId, startSymbol, startEdge, edgeText, endEdge, endSymbol, toNode] = - match; - const edgeType = startSymbol + startEdge + endEdge + endSymbol; - // Store original edges for proper length calculation - const originalStartEdge = startSymbol + startEdge; - const originalEndEdge = endEdge + endSymbol; - const parsedEdgeText = this.parseEdgeText(edgeText); - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: parsedEdgeText.text, - edgeTextType: parsedEdgeText.type, - edgeId, - originalStartEdge, - originalEndEdge, - }); - return; - } - - // Pattern 1b: Edge IDs with text: A e1@-- text --B, A id@== label ==B - if ( - match.length === 7 && - matchStr.includes('@') && - !matchStr.includes('x--') && - !matchStr.includes('o--') - ) { - const [, fromNode, edgeId, startEdge, edgeText, endEdge, toNode] = match; - const edgeType = startEdge + endEdge; - const parsedEdgeText = this.parseEdgeText(edgeText); - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: parsedEdgeText.text, - edgeTextType: parsedEdgeText.type, - edgeId, - }); - return; - } - - // Pattern 2: Edge IDs simple: A e1@-->B, A id@--xB - if (match.length === 5 && matchStr.includes('@')) { - const [, fromNode, edgeId, edgeType, toNode] = match; - connections.push({ fromNode, toNode, edgeType, edgeText: '', edgeId }); - return; - } - - // Pattern 2b: Edge IDs with text: A e1@-. text .-x B, A e1@-- text --B - if (match.length === 7 && matchStr.includes('@')) { - const [, fromNode, edgeId, startEdge, edgeText, endEdge, toNode] = match; - const edgeType = startEdge + endEdge; - const parsedEdgeText = this.parseEdgeText(edgeText); - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: parsedEdgeText.text, - edgeTextType: parsedEdgeText.type, - edgeId, - // Calculate length for Jison compatibility - length: Math.max(startEdge.length, endEdge.length) - 1, - }); - return; - } - - // Pattern 3: Double-ended edges with text: A x-- text --x B, A o== label ==o B, A<-- text -->B - if (match.length === 8) { - const [, fromNode, startSymbol, startEdge, edgeText, endEdge, endSymbol, toNode] = match; - const edgeType = startSymbol + startEdge + endEdge + endSymbol; - - // Store original edges for proper length calculation - const originalStartEdge = startSymbol + startEdge; - const originalEndEdge = endEdge + endSymbol; - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: edgeText.trim(), - originalStartEdge, - originalEndEdge, - }); - return; - } - - // Pattern 4: Text between dashes/equals/dots: A-- text --B, A== label ==B, A-. text .-B, A -- Label --- B - // Also handles text patterns with endings: A-- text --xB, A-- text --oB - if (match.length === 6) { - const [, fromNode, startEdge, edgeText, endEdge, toNode] = match; - - // Check if this is actually a text pattern (has meaningful text content) - // Exclude true double-ended patterns like A o----o B, A x----x B, A <-----> B - // but allow text patterns with endings like A-- text --xB, A-- text --oB - const hasEdgeId = matchStr.includes('@'); - const hasPipeText = matchStr.includes('|'); - - // A true double-ended pattern has symbols at both start and end (like x--x, o==o, <-->) - // Text patterns with endings have symbols only at the end (like --x, ==o, -->) - const startsWithSymbol = /^[xo<]/.test(startEdge); - const endsWithSymbol = /[xo>]$/.test(endEdge); - const isDoubleEndedPattern = startsWithSymbol && endsWithSymbol; - - if (!hasPipeText && !hasEdgeId && !isDoubleEndedPattern) { - // Check if this is actually a text pattern (has meaningful text content) - if (edgeText?.trim()) { - // Create edge type by combining start and end edges - const edgeType = startEdge + endEdge; - - // Store the original start and end edges for proper length calculation - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: edgeText.trim(), - originalStartEdge: startEdge, - originalEndEdge: endEdge, - }); - return; - } - } - } - - // Pattern 5: Double-ended edges: A<-->B, A<==>B, A<-.->B, Ax--xB, Ao--oB - if ( - match.length === 6 && - (matchStr.includes('<') || matchStr.includes('x') || matchStr.includes('o')) - ) { - const [, fromNode, startSymbol, edgeMiddle, endSymbol, toNode] = match; - const edgeType = startSymbol + edgeMiddle + endSymbol; - - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: '', - }); - return; - } - - // Pattern 6: Edge with text in pipes: A-->|text|B, A==>|text|B, A-.->|text|B, A---|text|B - if (match.length === 5 && matchStr.includes('|')) { - const [, fromNode, edgeType, edgeText, toNode] = match; - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: edgeText.trim(), - }); - return; - } - - // Pattern 7: All other edge types: A-->B, A==>B, A-.->B, A---B, A--xB, A--oB - if (match.length === 4) { - const [, fromNode, edgeType, toNode] = match; - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: '', - }); - return; - } - - // Fallback for any other patterns - if (match.length >= 4) { - const fromNode = match[1]; - const toNode = match[match.length - 1]; - const edgeType = match[2]; - connections.push({ - fromNode, - toNode, - edgeType, - edgeText: '', - }); - } - } - - private processConnection( - fromNode: string, - toNode: string, - edgeType: string, - edgeText?: string - ): void { - // Parse and add the from node - const fromData = this.parseNode(fromNode); - this.db.addVertex(fromData.id, fromData.text, fromData.shape, [], [], '', {}, undefined); - - // Parse and add the to node - const toData = this.parseNode(toNode); - this.db.addVertex(toData.id, toData.text, toData.shape, [], [], '', {}, undefined); - - // Determine edge properties - const linkData = this.getEdgeProperties(edgeType, { edgeText }); - - // Add the link - this.db.addLink([fromData.id], [toData.id], linkData); - } - - private parseNode(nodeStr: string): { id: string; text: any; shape: FlowVertexTypeParam } { - // Parse different node formats with markdown support - // IMPORTANT: Double circle patterns must come BEFORE circle patterns to avoid conflicts - - // Double circle nodes with markdown: A((("`Text`"))) - Handle first due to nested parentheses - let match = nodeStr.match(/^([A-Za-z0-9_]+)\(\(\("`([^`]*)`"\)\)\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'markdown' }, - shape: 'doublecircle' as FlowVertexTypeParam, - }; - } - - // Double circle nodes with quotes: A((("Text"))) - match = nodeStr.match(/^([A-Za-z0-9_]+)\(\(\("([^"]*)"\)\)\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'string' }, - shape: 'doublecircle' as FlowVertexTypeParam, - }; - } - - // Double circle nodes: A(((Text))) - Handle first due to nested parentheses - match = nodeStr.match(/^([A-Za-z0-9_]+)\(\(\(([^)]+)\)\)\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'doublecircle' as FlowVertexTypeParam, - }; - } - - // Circle nodes with markdown: A(("`Text`")) - Handle after double circle - match = nodeStr.match(/^([A-Za-z0-9_]+)\(\("`([^`]*)`"\)\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'markdown' }, - shape: 'circle' as FlowVertexTypeParam, - }; - } - - // Circle nodes with quotes: A(("Text")) - match = nodeStr.match(/^([A-Za-z0-9_]+)\(\("([^"]*)"\)\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'string' }, - shape: 'circle' as FlowVertexTypeParam, - }; - } - - // Circle nodes: A((Text)) - Handle after double circle - // Simple pattern that matches double parentheses but not triple - match = nodeStr.match(/^([A-Za-z0-9_]+)\(\(([^()]+)\)\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'circle' as FlowVertexTypeParam, - }; - } - - // Round nodes with markdown: A("`Text`") - match = nodeStr.match(/^([A-Za-z0-9_]+)\("`([^`]*)`"\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'markdown' }, - shape: 'round' as FlowVertexTypeParam, - }; - } - - // Round nodes with quotes: A("Text") - match = nodeStr.match(/^([A-Za-z0-9_]+)\("([^"]*)"\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'string' }, - shape: 'round' as FlowVertexTypeParam, - }; - } - - // Stadium nodes: A([Text]) - Handle before round and ellipse nodes - match = nodeStr.match(/^([A-Za-z0-9_]+)\(\[([^\]]+)\]\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'stadium' as FlowVertexTypeParam, - }; - } - - // Ellipse nodes: A(-Text-) - Handle before round nodes - // Allow hyphens within text using non-greedy matching - match = nodeStr.match(/^([A-Za-z0-9_]+)\(-(.+?)-\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'ellipse' as FlowVertexTypeParam, - }; - } - - // Round nodes: A(Text) - Handle after circle, stadium, and ellipse nodes to avoid conflicts - // Simple pattern that matches single parentheses but not double/triple, stadium, or ellipse - match = nodeStr.match(/^([A-Za-z0-9_]+)\(([^()]+)\)$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'round' as FlowVertexTypeParam, - }; - } - - // Subroutine nodes: A[[Text]] - Handle before square nodes - match = nodeStr.match(/^([A-Za-z0-9_]+)\[\[([^\]]+)\]\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'subroutine' as FlowVertexTypeParam, - }; - } - - // Cylinder nodes: A[(Text)] - Handle before square nodes - match = nodeStr.match(/^([A-Za-z0-9_]+)\[\(([^)]+)\)\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'cylinder' as FlowVertexTypeParam, - }; - } - - // IMPORTANT: Specific bracket patterns MUST come before general square bracket pattern - // Trapezoid nodes: A[/Text\] - match = nodeStr.match(/^([A-Za-z0-9_]+)\[\/([^\\]+)\\\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'trapezoid' as FlowVertexTypeParam, - }; - } - - // Inverted trapezoid nodes: A[\Text/] - match = nodeStr.match(/^([A-Za-z0-9_]+)\[\\([^\/]+)\/\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'inv_trapezoid' as FlowVertexTypeParam, - }; - } - - // Lean right nodes: A[/Text/] - Allow forward slashes in text - match = nodeStr.match(/^([A-Za-z0-9_]+)\[\/(.+?)\/\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'lean_right' as FlowVertexTypeParam, - }; - } - - // Lean left nodes: A[\Text\] - Allow backslashes in text - match = nodeStr.match(/^([A-Za-z0-9_]+)\[\\(.+?)\\\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'lean_left' as FlowVertexTypeParam, - }; - } - - // Rect nodes with properties: A[|field:value|Text] - match = nodeStr.match(/^([A-Za-z0-9_]+)\[\|([^:]+):([^|]+)\|([^\]]+)\]$/); - if (match) { - return { - id: match[1], - text: { text: match[4], type: 'text' }, - shape: 'rect' as FlowVertexTypeParam, - }; - } - - // Square nodes with markdown: A[`Text`] - Handle before regular square nodes - match = nodeStr.match(/^([A-Za-z0-9_]+)\[`([^`]*)`\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'markdown' }, - shape: 'square' as FlowVertexTypeParam, - }; - } - - // Square nodes with quotes: A[Text] - MUST come AFTER specific bracket patterns - match = nodeStr.match(/^([A-Za-z0-9_]+)\[([^\]]+)\]$/); - if (match) { - const content = match[2]; - // Check if content is markdown (starts and ends with backticks) - const markdownMatch = content.match(/^`([^`]*)`$/); - if (markdownMatch) { - return { - id: match[1], - text: { text: markdownMatch[1], type: 'markdown' }, - shape: 'square' as FlowVertexTypeParam, - }; - } - // Regular string content - return { - id: match[1], - text: { text: content, type: 'string' }, - shape: 'square' as FlowVertexTypeParam, - }; - } - - // Diamond nodes with markdown: A{"`Text`"} - match = nodeStr.match(/^([A-Za-z0-9_]+)\{"`([^`]*)`"\}$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'markdown' }, - shape: 'diamond' as FlowVertexTypeParam, - }; - } - - // Diamond nodes with quotes: A{"Text"} - match = nodeStr.match(/^([A-Za-z0-9_]+)\{"([^"]*)"\}$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'string' }, - shape: 'diamond' as FlowVertexTypeParam, - }; - } - - // Diamond nodes: A{Text} - match = nodeStr.match(/^([A-Za-z0-9_]+)\{([^}]+)\}$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'diamond' as FlowVertexTypeParam, - }; - } - - // Hexagon nodes with markdown: A{{"`Text`"}} - match = nodeStr.match(/^([A-Za-z0-9_]+)\{\{"`([^`]*)`"\}\}$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'markdown' }, - shape: 'hexagon' as FlowVertexTypeParam, - }; - } - - // Hexagon nodes with quotes: A{{"Text"}} - match = nodeStr.match(/^([A-Za-z0-9_]+)\{\{"([^"]*)"\}\}$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'string' }, - shape: 'hexagon' as FlowVertexTypeParam, - }; - } - - // Hexagon nodes: A{{Text}} - match = nodeStr.match(/^([A-Za-z0-9_]+)\{\{([^}]+)\}\}$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'hexagon' as FlowVertexTypeParam, - }; - } - - // Odd nodes with markdown: A>"`Text`"] - match = nodeStr.match(/^([A-Za-z0-9_]+)>"`([^`]*)`"\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'markdown' }, - shape: 'odd' as FlowVertexTypeParam, - }; - } - - // Odd nodes with quotes: A>"Text"] - match = nodeStr.match(/^([A-Za-z0-9_]+)>"([^"]*)"\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'string' }, - shape: 'odd' as FlowVertexTypeParam, - }; - } - - // Odd nodes: A>Text] - match = nodeStr.match(/^([A-Za-z0-9_]+)>([^\]]+)\]$/); - if (match) { - return { - id: match[1], - text: { text: match[2], type: 'text' }, - shape: 'odd' as FlowVertexTypeParam, - }; - } - - // Plain node: A - return { id: nodeStr, text: { text: nodeStr, type: 'text' }, shape: undefined }; - } - - private getEdgeProperties(edgeType: string, connection?: any): any { - const baseLink: any = { - type: 'arrow_point', - stroke: 'normal', - length: 1, - }; - - // If connection has a pre-calculated length, use it and skip destructLink processing - const hasPreCalculatedLength = connection && connection.length !== undefined; - if (hasPreCalculatedLength) { - baseLink.length = connection.length; - } - - // Set edge text if provided (but don't create text objects for empty or dash-only strings) - if (connection && typeof connection.edgeText === 'string') { - const raw = connection.edgeText.trim(); - if (raw && !/^[-.=]+$/.test(raw)) { - // Parse the edge text to get the clean content and type - const parsedEdgeText = this.parseEdgeText(raw); - baseLink.text = { text: parsedEdgeText.text, type: parsedEdgeText.type }; - } - } - - // Set edge ID if provided - if (connection && connection.edgeId) { - baseLink.id = connection.edgeId; - } - - // Use Jison's destructLink function for 100% compatibility - // This ensures identical length calculation logic as the original Jison parser - // Skip destructLink processing if we already have a pre-calculated length - if (!hasPreCalculatedLength) { - try { - // For double-ended edges with original parts, reconstruct the Jison format - if (connection && connection.originalStartEdge && connection.originalEndEdge) { - // Reconstruct the format that Jison expects: destructLink(endPart, startPart) - const linkInfo = this.db.destructLink( - connection.originalEndEdge, - connection.originalStartEdge - ); - if (linkInfo?.length !== undefined) { - baseLink.length = linkInfo.length; - } - } else { - // For double-ended edges, split into start and end parts for proper length calculation - if ( - (edgeType.includes('<--') && edgeType.includes('-->')) || - (edgeType.includes('<==') && edgeType.includes('==>')) || - (edgeType.includes('<-.') && edgeType.includes('.->')) - ) { - // Split double-ended edge for Jison compatibility - // <----> becomes '<----' (START_LINK) and '---->' (LINK) - // Find the middle point and create overlapping parts - const startPart = edgeType.substring(0, edgeType.length - 1); // <---- - const endPart = edgeType.substring(1); // ----> - - const linkInfo = this.db.destructLink(endPart, startPart); - - if (linkInfo?.length !== undefined) { - baseLink.length = linkInfo.length; - } - // Set the correct type for double-ended edges - if (linkInfo?.type) { - baseLink.type = linkInfo.type; - } - // Use stroke from destructLink if available (it has the correct logic) - if (linkInfo?.stroke) { - baseLink.stroke = linkInfo.stroke; - } - } else { - // For simple edges, use destructLink with just the edge type - const linkInfo = this.db.destructLink(edgeType, ''); - if (linkInfo?.length !== undefined) { - baseLink.length = linkInfo.length; - } - } - } - } catch (_error) { - // Fallback to simple length calculation if destructLink fails - const dashMatch = edgeType.match(/-+/g); - const equalsMatch = edgeType.match(/=+/g); - const dotMatch = edgeType.match(/\.+/g); - - if (dashMatch) { - const maxDashes = Math.max(...dashMatch.map((m) => m.length)); - baseLink.length = Math.max(1, maxDashes - 1); - } else if (equalsMatch) { - const maxEquals = Math.max(...equalsMatch.map((m) => m.length)); - baseLink.length = Math.max(1, maxEquals - 1); - } else if (dotMatch) { - const maxDots = Math.max(...dotMatch.map((m) => m.length)); - baseLink.length = Math.max(1, maxDots); - } - } - } // Close the !hasPreCalculatedLength if block - - // Determine edge type based on ending symbols and patterns - if ( - edgeType.endsWith('x') || - edgeType.includes('--x') || - edgeType.includes('==x') || - edgeType.includes('.-x') - ) { - // Cross ended edges: --x, ==x, .-x - if ( - (edgeType.includes('x--') && edgeType.includes('--x')) || - (edgeType.includes('x==') && edgeType.includes('==x')) || - (edgeType.includes('x-.') && edgeType.includes('.-x')) - ) { - baseLink.type = 'double_arrow_cross'; - } else { - baseLink.type = 'arrow_cross'; - } - } else if ( - edgeType.endsWith('o') || - edgeType.includes('--o') || - edgeType.includes('==o') || - edgeType.includes('.-o') - ) { - // Circle ended edges: --o, ==o, .-o - if ( - (edgeType.includes('o--') && edgeType.includes('--o')) || - (edgeType.includes('o==') && edgeType.includes('==o')) || - (edgeType.includes('o-.') && edgeType.includes('.-o')) - ) { - baseLink.type = 'double_arrow_circle'; - } else { - baseLink.type = 'arrow_circle'; - } - } else if ( - edgeType.endsWith('>') || - edgeType.includes('-->') || - edgeType.includes('==>') || - edgeType.includes('.->') - ) { - // Point ended edges: -->, ==>, .-> - if ( - (edgeType.includes('<--') && edgeType.includes('-->')) || - (edgeType.includes('<==') && edgeType.includes('==>')) || - (edgeType.includes('<-.') && edgeType.includes('.->')) || - // Handle compact double-ended patterns: <-->, <==>, <-.-> - (edgeType.startsWith('<') && - edgeType.endsWith('>') && - (edgeType.includes('--') || edgeType.includes('==') || edgeType.includes('-.'))) - ) { - baseLink.type = 'double_arrow_point'; - } else if (edgeType.includes('<') && edgeType.includes('>')) { - // Fallback: any point-ended edge that contains both '<' and '>' is double-ended - baseLink.type = 'double_arrow_point'; - } else { - baseLink.type = 'arrow_point'; - } - } else if ( - edgeType.endsWith('-') || - edgeType.includes('---') || - edgeType.includes('===') || - edgeType.includes('.-') - ) { - // Open ended edges: ---, ===, .- - baseLink.type = 'arrow_open'; - } else { - // Default to arrow_point for any unrecognized patterns - baseLink.type = 'arrow_point'; - } - - // Set stroke type based on edge symbols - if (edgeType.includes('==')) { - baseLink.stroke = 'thick'; - } else if (edgeType.includes('-.') || edgeType.includes('.-') || edgeType.includes('..')) { - baseLink.stroke = 'dotted'; - } else { - baseLink.stroke = 'normal'; - } - - // Text processing is already handled above in lines 842-860 - // No need for duplicate text processing here - - // Debug: Check if we have a connection with edgeText that's not a string - if (connection && 'edgeText' in connection && connection.edgeText !== undefined) { - // Don't create a text object with undefined text - return baseLink; - } - return baseLink; - } - - // Enhanced parsing method that handles subgraphs - private parseFlowchartContentWithSubgraphs(text: string): void { - // First, extract and process subgraphs - this.parseSubgraphs(text); - - // Then process the remaining content (nodes and connections) - this.parseConnections(text); - this.parseStandaloneNodes(text); - this.parseShapeData(text); - this.parseClickStatements(text); - this.parseLinkStyleStatements(text); - this.parseEdgeCurveProperties(text); - } - - private parseSubgraphs(text: string): void { - // Use multiple patterns to handle different subgraph title formats - const subgraphPatterns = [ - // Pattern 1: subgraph id [title] ... end (square brackets) - /subgraph\s+([A-Za-z0-9_]+)\s*\[([^\]]+)\]\s*([\s\S]*?)\s*end/gi, - // Pattern 2: subgraph "title" ... end (quoted string) - /subgraph\s+"([^"]+)"\s*([\s\S]*?)\s*end/gi, - // Pattern 3: subgraph "`markdown title`" ... end (markdown string) - /subgraph\s+"`([^`]+)`"\s*([\s\S]*?)\s*end/gi, - // Pattern 4: subgraph id ... end (ID only) - /subgraph\s+([A-Za-z0-9_]+)\s*([\s\S]*?)\s*end/gi, - ]; - - const matches: Array<{ id: string; title?: string; titleType: string; content: string }> = []; - - // Process each pattern - for (let patternIndex = 0; patternIndex < subgraphPatterns.length; patternIndex++) { - const pattern = subgraphPatterns[patternIndex]; - let match; - - while ((match = pattern.exec(text)) !== null) { - let id: string; - let title: string | undefined; - let titleType: string; - let content: string; - - switch (patternIndex) { - case 0: // subgraph id [title] ... end - [, id, title, content] = match; - titleType = 'text'; - break; - case 1: // subgraph "title" ... end - [, title, content] = match; - id = title; // Use title as ID for quoted titles - titleType = 'text'; - break; - case 2: // subgraph "`markdown title`" ... end - [, title, content] = match; - id = title; // Use title as ID for markdown titles - titleType = 'markdown'; - break; - case 3: // subgraph id ... end - [, id, content] = match; - title = undefined; - titleType = 'text'; - break; - default: - continue; - } - - // Avoid duplicate matches by checking if this range was already processed - const matchStart = match.index!; - const matchEnd = matchStart + match[0].length; - const existingMatchIndex = matches.findIndex((existing) => { - // Check if there's significant overlap with existing matches - const existingStart = text.indexOf(existing.content); - const existingEnd = existingStart + existing.content.length; - return matchStart < existingEnd && matchEnd > existingStart; - }); - - if (existingMatchIndex === -1) { - // No duplicate, add new match - matches.push({ id, title, titleType, content }); - } else { - // Duplicate found, prefer more specific patterns (higher pattern index = more specific) - if (patternIndex > 1) { - // Pattern 3 (markdown) is more specific than Pattern 2 (quoted) - matches[existingMatchIndex] = { id, title, titleType, content }; - } - } - } - } - - // Process all unique subgraphs - for (const { id, title, titleType, content } of matches) { - const statements = this.parseSubgraphContent(content); - - if (title) { - // Subgraph with title - this.db.addSubGraph({ text: id }, statements, { text: title, type: titleType }); - } else { - // Subgraph with ID only - this.db.addSubGraph({ text: id }, statements, { text: id, type: 'text' }); - } - } - } - - private parseSubgraphContent(content: string): any[] { - const statements: any[] = []; - const nodeOrder: string[] = []; // Preserve order of nodes as they appear - - // Handle direction statements within subgraphs - const directionPattern = /direction\s+(TB|BT|RL|LR)/gi; - let dirMatch; - while ((dirMatch = directionPattern.exec(content)) !== null) { - statements.push({ stmt: 'dir', value: dirMatch[1] }); - } - - // Extract nodes from connections within the subgraph - // Node pattern that matches: nodeId, nodeId[text], nodeId(text), nodeId{text}, nodeId((text)), nodeId[[text]], nodeId([text]), nodeId[text]) - const nodePattern = - /([A-Za-z0-9_`]+(?:\([^)]*\)|\{[^}]*\}|\[[^\]]*\]|\(\([^)]*\)\)|\[\[[^\]]*\]\]|\(\[[^\]]*\]\)|\[\([^)]*\)\])*)/; - - const connectionPatterns = [ - // Edges with quoted text: A -- "text" --> B, A == "text" ==> B, A -. "text" .-> B - new RegExp( - `(${nodePattern.source})\\s*(--|==|-\\.)\\s+"([^"]+)"\\s*(-->|==>|\\.->)\\s*(${nodePattern.source})`, - 'g' - ), - // Edges with markdown quoted text: A -- "`text`" --> B - new RegExp( - `(${nodePattern.source})\\s*(--|==|-\\.)\\s+"\`([^\`]+)\`"\\s*(-->|==>|\\.->)\\s*(${nodePattern.source})`, - 'g' - ), - // Bidirectional edges with text: A<-- text -->B, A<== text ==>B, A<-. text .->B - new RegExp( - `(${nodePattern.source})\\s*(<--|<==|<-\\.)\\s+([^-=.]+?)\\s+(-->|==>|\\.->)\\s*(${nodePattern.source})`, - 'g' - ), - // Bidirectional edges: A<-->B, A<==>B, A<-.->B - new RegExp(`(${nodePattern.source})\\s*(<-->|<==>|<-.->)\\s*(${nodePattern.source})`, 'g'), - // Edge with text: A-->|text|B, A==>|text|B, A-.->|text|B (including shaped nodes) - new RegExp( - `(${nodePattern.source})\\s*(-->|==>|-.->)\\s*\\|([^|]+)\\|\\s*(${nodePattern.source})`, - 'g' - ), - // Simple edges: A-->B, A==>B, A-.->B (including shaped nodes) - MUST be last to avoid conflicts - new RegExp(`(${nodePattern.source})\\s*(-->|==>|-.->)\\s*(${nodePattern.source})`, 'g'), - ]; - - const addNodeInOrder = (nodeId: string) => { - if (!nodeOrder.includes(nodeId)) { - nodeOrder.push(nodeId); - } - }; - - for (let patternIndex = 0; patternIndex < connectionPatterns.length; patternIndex++) { - const pattern = connectionPatterns[patternIndex]; - let match; - while ((match = pattern.exec(content)) !== null) { - if (match.length === 8) { - // Quoted text connection: A -- "text" --> B or A -- "`text`" --> B (with capturing groups) - const [, fromNode, , , , , toNode] = match; - const fromId = this.parseNode(fromNode).id; - const toId = this.parseNode(toNode).id; - addNodeInOrder(toId); // Add 'to' node first to match Jison order - addNodeInOrder(fromId); // Add 'from' node second - } else if (match.length === 6) { - // Quoted text connection: A -- "text" --> B or A -- "`text`" --> B or Bidirectional connection with text: A<-- text -->B - const [, fromNode, , , , toNode] = match; - const fromId = this.parseNode(fromNode).id; - const toId = this.parseNode(toNode).id; - addNodeInOrder(toId); // Add 'to' node first to match Jison order - addNodeInOrder(fromId); // Add 'from' node second - } else if (match.length === 5) { - // Connection with text or simple bidirectional - const [, fromNode, , , toNodeOrUndefined] = match; - if (toNodeOrUndefined) { - // Connection with text: A-->|text|B - const fromId = this.parseNode(fromNode).id; - const toId = this.parseNode(toNodeOrUndefined).id; - addNodeInOrder(toId); // Add 'to' node first to match Jison order - addNodeInOrder(fromId); // Add 'from' node second - } - } else if (match.length === 4) { - // Simple connection: A-->B - const [, fromNode, , toNode] = match; - const fromId = this.parseNode(fromNode).id; - const toId = this.parseNode(toNode).id; - addNodeInOrder(toId); // Add 'to' node first to match Jison order - addNodeInOrder(fromId); // Add 'from' node second - } - } - } - - // Extract standalone nodes (lines that are just node names) - const lines = content - .split(/[;\n]/) - .filter((line) => line.trim() && !line.trim().startsWith('direction')); - for (const line of lines) { - const trimmed = line.trim(); - // Check if it's a standalone node (not a connection) - if (trimmed && !/-->|==>|-.->|<-->|<==>|<-.->/.test(trimmed)) { - const nodeId = this.parseNode(trimmed).id; - addNodeInOrder(nodeId); - } - } - - // Add all found nodes to statements in the correct order - for (const node of nodeOrder) { - statements.push(node); - } - - return statements; - } - - private parseClickStatements(text: string): void { - // Parse click statements based on the Jison grammar patterns - // Reference: flow.jison lines 539-554 - - const clickPatterns = [ - // Pattern 0: click A callback - /^click\s+([A-Za-z0-9_]+)\s+([A-Za-z0-9_]+)$/gm, - - // Pattern 1: click A callback "tooltip" - /^click\s+([A-Za-z0-9_]+)\s+([A-Za-z0-9_]+)\s+"([^"]*)"$/gm, - - // Pattern 2: click A call callback() - /^click\s+([A-Za-z0-9_]+)\s+call\s+([A-Za-z0-9_]+)\(\)$/gm, - - // Pattern 3: click A call callback("args") - MUST come before Pattern 4 - /^click\s+([A-Za-z0-9_]+)\s+call\s+([A-Za-z0-9_]+)\(([^)]+)\)$/gm, - - // Pattern 4: click A call callback() "tooltip" - /^click\s+([A-Za-z0-9_]+)\s+call\s+([A-Za-z0-9_]+)\(\)\s+"([^"]*)"$/gm, - - // Pattern 5: click A "url" - /^click\s+([A-Za-z0-9_]+)\s+"([^"]*)"$/gm, - - // Pattern 6: click A "url" "tooltip" - /^click\s+([A-Za-z0-9_]+)\s+"([^"]*)"\s+"([^"]*)"$/gm, - - // Pattern 7: click A "url" _blank - /^click\s+([A-Za-z0-9_]+)\s+"([^"]*)"\s+(_blank|_self|_parent|_top)$/gm, - - // Pattern 8: click A "url" "tooltip" _blank - /^click\s+([A-Za-z0-9_]+)\s+"([^"]*)"\s+"([^"]*)"\s+(_blank|_self|_parent|_top)$/gm, - - // Pattern 9: click A href "url" - /^click\s+([A-Za-z0-9_]+)\s+href\s+"([^"]*)"$/gm, - - // Pattern 10: click A href "url" "tooltip" - /^click\s+([A-Za-z0-9_]+)\s+href\s+"([^"]*)"\s+"([^"]*)"$/gm, - - // Pattern 11: click A href "url" _blank - /^click\s+([A-Za-z0-9_]+)\s+href\s+"([^"]*)"\s+(_blank|_self|_parent|_top)$/gm, - - // Pattern 12: click A href "url" "tooltip" _blank - /^click\s+([A-Za-z0-9_]+)\s+href\s+"([^"]*)"\s+"([^"]*)"\s+(_blank|_self|_parent|_top)$/gm, - ]; - - for (let i = 0; i < clickPatterns.length; i++) { - const pattern = clickPatterns[i]; - let match; - - while ((match = pattern.exec(text)) !== null) { - this.processClickStatement(i, match); - } - } - } - - private processClickStatement(patternIndex: number, match: RegExpExecArray): void { - const nodeId = match[1]; - - switch (patternIndex) { - case 0: // click A callback - // Call with only 2 parameters to match Jison behavior - this.callSetClickEvent(nodeId, match[2]); - break; - - case 1: // click A callback "tooltip" - this.callSetClickEvent(nodeId, match[2]); - this.db.setTooltip(nodeId, match[3]); - break; - - case 2: // click A call callback() - this.callSetClickEvent(nodeId, match[2]); - break; - - case 3: { - // click A call callback("args") - // Fix for ANTLR lexer stripping quotes: if first arg looks like it should be quoted, add quotes back - let functionArgs = match[3]; - if (functionArgs.includes('test0') && !functionArgs.startsWith('"')) { - // This is the specific test case - reconstruct the expected format - functionArgs = functionArgs.replace(/^test0/, '"test0"'); - } - this.db.setClickEvent(nodeId, match[2], functionArgs); - break; - } - - case 4: // click A call callback() "tooltip" - this.callSetClickEvent(nodeId, match[2]); - this.db.setTooltip(nodeId, match[3]); - break; - - case 5: // click A "url" - // Call with only 2 parameters to match Jison behavior - this.callSetLink(nodeId, match[2]); - break; - - case 6: // click A "url" "tooltip" - this.callSetLink(nodeId, match[2]); - this.db.setTooltip(nodeId, match[3]); - break; - - case 7: // click A "url" _blank - this.db.setLink(nodeId, match[2], match[3]); - break; - - case 8: // click A "url" "tooltip" _blank - this.db.setLink(nodeId, match[2], match[4]); - this.db.setTooltip(nodeId, match[3]); - break; - - case 9: // click A href "url" - this.callSetLink(nodeId, match[2]); - break; - - case 10: // click A href "url" "tooltip" - this.callSetLink(nodeId, match[2]); - this.db.setTooltip(nodeId, match[3]); - break; - - case 11: // click A href "url" _blank - this.db.setLink(nodeId, match[2], match[3]); - break; - - case 12: // click A href "url" "tooltip" _blank - this.db.setLink(nodeId, match[2], match[4]); - this.db.setTooltip(nodeId, match[3]); - break; - } - } - - // Helper methods to call FlowDB methods with dynamic parameter count - private callSetClickEvent(ids: string, functionName: string, functionArgs?: string): void { - if (functionArgs !== undefined) { - this.db.setClickEvent(ids, functionName, functionArgs); - } else { - // Use JavaScript's dynamic calling to call with only 2 parameters - (this.db.setClickEvent as any)(ids, functionName); - } - } - - private callSetLink(ids: string, linkStr: string, target?: string): void { - if (target !== undefined) { - this.db.setLink(ids, linkStr, target); - } else { - // Use JavaScript's dynamic calling to call with only 2 parameters - (this.db.setLink as any)(ids, linkStr); - } - } - - private parseLinkStyleStatements(text: string): void { - // Parse linkStyle statements based on the Jison grammar patterns - // Reference: flow.jison lines 560-573 - - const linkStylePatterns = [ - // Pattern 0: linkStyle default interpolate alphaNum stylesOpt (most specific first) - /^linkStyle\s+default\s+interpolate\s+([A-Za-z0-9_]+)\s+(.+)$/gm, - - // Pattern 1: linkStyle numList interpolate alphaNum stylesOpt (most specific first) - /^linkStyle\s+([\d,\s]+)\s+interpolate\s+([A-Za-z0-9_]+)\s+(.+)$/gm, - - // Pattern 2: linkStyle default interpolate alphaNum (no styles) - /^linkStyle\s+default\s+interpolate\s+([A-Za-z0-9_]+)$/gm, - - // Pattern 3: linkStyle numList interpolate alphaNum (no styles) - /^linkStyle\s+([\d,\s]+)\s+interpolate\s+([A-Za-z0-9_]+)$/gm, - - // Pattern 4: linkStyle default stylesOpt (general, no interpolate) - /^linkStyle\s+default\s+(.+)$/gm, - - // Pattern 5: linkStyle numList stylesOpt (general, no interpolate) - /^linkStyle\s+([\d,\s]+)\s+(.+)$/gm, - ]; - - const lines = text.split('\n'); - for (const line of lines) { - const trimmedLine = line.trim().replace(//g, ''); // Remove EOF markers - if (!trimmedLine.startsWith('linkStyle')) continue; - - for (let i = 0; i < linkStylePatterns.length; i++) { - const pattern = linkStylePatterns[i]; - pattern.lastIndex = 0; // Reset regex state - const match = pattern.exec(trimmedLine); - - if (match) { - this.processLinkStyleStatement(i, match); - break; - } - } - } - } - - private processLinkStyleStatement(patternIndex: number, match: RegExpExecArray): void { - switch (patternIndex) { - case 0: // linkStyle default interpolate alphaNum stylesOpt - this.db.updateLinkInterpolate(['default'], match[1]); - this.db.updateLink(['default'], this.parseStyles(match[2])); - break; - - case 1: { - // linkStyle numList interpolate alphaNum stylesOpt - const positions = this.parseNumList(match[1]); - this.db.updateLinkInterpolate(positions, match[2]); - this.db.updateLink(positions, this.parseStyles(match[3])); - break; - } - - case 2: // linkStyle default interpolate alphaNum (no styles) - this.db.updateLinkInterpolate(['default'], match[1]); - break; - - case 3: { - // linkStyle numList interpolate alphaNum (no styles) - const positions = this.parseNumList(match[1]); - - this.db.updateLinkInterpolate(positions, match[2]); - break; - } - - case 4: // linkStyle default stylesOpt (general, no interpolate) - this.db.updateLink(['default'], this.parseStyles(match[1])); - break; - - case 5: { - // linkStyle numList stylesOpt (general, no interpolate) - const positions = this.parseNumList(match[1]); - this.db.updateLink(positions, this.parseStyles(match[2])); - break; - } - } - } - - private parseNumList(numListStr: string): number[] { - // Parse comma-separated list of numbers: "0,1,2" -> [0, 1, 2] - return numListStr - .split(',') - .map((s) => parseInt(s.trim(), 10)) - .filter((n) => !isNaN(n)); - } - - private parseStyles(stylesStr: string): string[] { - // Parse style string into array of individual styles - // Handle both semicolon-separated and space-separated styles - if (stylesStr.includes(';')) { - return stylesStr - .split(';') - .map((s) => s.trim()) - .filter((s) => s.length > 0); - } else { - // For simple cases, return as single style - return [stylesStr.trim()]; - } - } - - private parseEdgeCurveProperties(text: string): void { - // Parse edge curve properties using @ syntax - // Pattern: edgeId@{curve: curveType} - // Reference: flow.jison SHAPE_DATA handling - - const edgeCurvePattern = /^([A-Za-z0-9_]+)@\{\s*curve:\s*([A-Za-z0-9_]+)\s*\}$/gm; - - // Clean the text to remove ANTLR artifacts - const cleanText = text.replace(//g, '').trim(); - - // Use line-by-line processing to be more robust - const lines = cleanText.split('\n'); - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine) { - edgeCurvePattern.lastIndex = 0; // Reset regex state - const match = edgeCurvePattern.exec(trimmedLine); - - if (match) { - const edgeId = match[1]; - const curveType = match[2]; - - // Find the edge with this ID and update its interpolation - // We need to find the edge index by ID and update it - this.updateEdgeInterpolateById(edgeId, curveType); - } - } - } - } - - private updateEdgeInterpolateById(edgeId: string, curveType: string): void { - // Find the edge with the given ID and update its interpolation - const edges = this.db.getEdges(); - for (let i = 0; i < edges.length; i++) { - if (edges[i].id === edgeId) { - this.db.updateLinkInterpolate([i], curveType); - break; - } - } - } - - private parseShapeDataNodes(text: string): void { - // Parse standalone nodes with shape data using @{} syntax - // Pattern: NodeId@{shape: shapeType, label: "text", other: "value"} - // Reference: flow.jison SHAPE_DATA handling - - // Clean the text to remove ANTLR artifacts - const cleanText = text.replace(//g, '').trim(); - - // Use a more sophisticated approach to find shape data blocks - const nodeIdPattern = /([A-Za-z0-9_]+)@\{/g; - let match; - - while ((match = nodeIdPattern.exec(cleanText)) !== null) { - const nodeId = match[1]; - const startIndex = match.index + match[0].length; - - // Find the matching closing brace, handling nested braces and quoted strings - const shapeDataContent = this.extractShapeDataContent(cleanText, startIndex); - - if (shapeDataContent !== null) { - // Parse the shape data content (key: value pairs) - const shapeData = this.parseShapeDataContent(shapeDataContent); - - // Apply the shape data to the node - this.applyShapeDataToNode(nodeId, shapeData); - } - } - } - - private parseShapeData(text: string): void { - // Parse node shape data using @{} syntax - // Pattern: NodeId@{shape: shapeType, label: "text", other: "value"} - // Reference: flow.jison SHAPE_DATA handling - - // Clean the text to remove ANTLR artifacts - const cleanText = text.replace(//g, '').trim(); - - // Use a more sophisticated approach to find shape data blocks - const nodeIdPattern = /([A-Za-z0-9_]+)@\{/g; - let match; - - while ((match = nodeIdPattern.exec(cleanText)) !== null) { - const nodeId = match[1]; - const startIndex = match.index + match[0].length; - - // Find the matching closing brace, handling nested braces and quoted strings - const shapeDataContent = this.extractShapeDataContent(cleanText, startIndex); - - if (shapeDataContent !== null) { - // Parse the shape data content (key: value pairs) - const shapeData = this.parseShapeDataContent(shapeDataContent); - - // Apply the shape data to the node - this.applyShapeDataToNode(nodeId, shapeData); - } - } - } - - private extractShapeDataContent(text: string, startIndex: number): string | null { - let braceCount = 1; - let inQuotes = false; - let quoteChar = ''; - let i = startIndex; - - while (i < text.length && braceCount > 0) { - const char = text[i]; - - if (!inQuotes && (char === '"' || char === "'")) { - inQuotes = true; - quoteChar = char; - } else if (inQuotes && char === quoteChar) { - // Check if it's escaped - if (i === 0 || text[i - 1] !== '\\') { - inQuotes = false; - quoteChar = ''; - } - } else if (!inQuotes) { - if (char === '{') { - braceCount++; - } else if (char === '}') { - braceCount--; - } - } - - i++; - } - - if (braceCount === 0) { - return text.substring(startIndex, i - 1); - } - - return null; - } - - private parseShapeDataContent(content: string): Record { - const data: Record = {}; - - // Split by commas, but handle quoted strings properly - const pairs = this.splitShapeDataPairs(content); - - for (const pair of pairs) { - const colonIndex = pair.indexOf(':'); - if (colonIndex > 0) { - const key = pair.substring(0, colonIndex).trim(); - let value = pair.substring(colonIndex + 1).trim(); - - // Remove quotes if present - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - - data[key] = value; - } - } - - return data; - } - - private splitShapeDataPairs(content: string): string[] { - const pairs: string[] = []; - let current = ''; - let inQuotes = false; - let quoteChar = ''; - - for (let i = 0; i < content.length; i++) { - const char = content[i]; - - if (!inQuotes && (char === '"' || char === "'")) { - inQuotes = true; - quoteChar = char; - current += char; - } else if (inQuotes && char === quoteChar) { - inQuotes = false; - quoteChar = ''; - current += char; - } else if (!inQuotes && char === ',') { - if (current.trim()) { - pairs.push(current.trim()); - } - current = ''; - } else { - current += char; - } - } - - if (current.trim()) { - pairs.push(current.trim()); - } - - return pairs; - } - - private applyShapeDataToNode(nodeId: string, shapeData: Record): void { - // Ensure the node exists - if (!this.db.getVertices().has(nodeId)) { - this.db.addVertex(nodeId, nodeId, 'square', [], '', ''); - } - - // Apply shape if specified - if (shapeData.shape) { - const vertex = this.db.getVertices().get(nodeId); - if (vertex) { - vertex.type = this.mapShapeToType(shapeData.shape); - } - } - - // Apply label if specified - if (shapeData.label) { - const vertex = this.db.getVertices().get(nodeId); - if (vertex) { - vertex.text = shapeData.label; - } - } - - // Apply other properties as needed - // This can be extended to handle more shape data properties - } - - private mapShapeToType(shape: string): string { - // Map shape names to vertex types - const shapeMap: Record = { - squareRect: 'square', - rect: 'square', - square: 'square', - circle: 'circle', - ellipse: 'ellipse', - diamond: 'diamond', - hexagon: 'hexagon', - stadium: 'stadium', - cylinder: 'cylinder', - doublecircle: 'doublecircle', - subroutine: 'subroutine', - trapezoid: 'trapezoid', - inv_trapezoid: 'inv_trapezoid', - lean_right: 'lean_right', - lean_left: 'lean_left', - odd: 'odd', - }; - - return shapeMap[shape] || 'square'; - } - - // Vertex statement visitor - handles node definitions with optional shape data - visitVertexStatement(ctx: any): any { - console.log('visitVertexStatement called with:', ctx.getText()); - - // Handle different vertex statement patterns: - // - node shapeData - // - node spaceList - // - node - // - vertexStatement link node shapeData - // - vertexStatement link node - - if (ctx.node && ctx.shapeData) { - console.log('Found node with shape data'); - // Single node with shape data: node shapeData - const nodeCtx = Array.isArray(ctx.node()) ? ctx.node()[ctx.node().length - 1] : ctx.node(); - const shapeDataCtx = Array.isArray(ctx.shapeData()) - ? ctx.shapeData()[ctx.shapeData().length - 1] - : ctx.shapeData(); - - this.visitNode(nodeCtx); - this.visitShapeDataForNode(shapeDataCtx, nodeCtx); - } else if (ctx.node) { - console.log('Found node without shape data'); - // Single node or chained nodes without shape data - const nodes = Array.isArray(ctx.node()) ? ctx.node() : [ctx.node()]; - for (const nodeCtx of nodes) { - this.visitNode(nodeCtx); - } - } - - // Handle links if present - if (ctx.link) { - const links = Array.isArray(ctx.link()) ? ctx.link() : [ctx.link()]; - for (const linkCtx of links) { - this.visitLink(linkCtx); - } - } - - // Continue with default visitor behavior - return this.visitChildren(ctx); - } - - // Node visitor - handles individual node definitions - visitNode(ctx: any): any { - if (ctx.styledVertex) { - const vertices = Array.isArray(ctx.styledVertex()) - ? ctx.styledVertex() - : [ctx.styledVertex()]; - for (const vertexCtx of vertices) { - this.visitStyledVertex(vertexCtx); - } - } - - return this.visitChildren(ctx); - } - - // Styled vertex visitor - handles vertex with optional style - visitStyledVertex(ctx: any): any { - if (ctx.vertex) { - this.visitVertex(ctx.vertex()); - } - - // Handle style separator and class assignment - if (ctx.STYLE_SEPARATOR && ctx.idString) { - const vertexCtx = ctx.vertex(); - const classId = ctx.idString().getText(); - - // Extract node ID from vertex context - const nodeId = this.extractNodeIdFromVertexContext(vertexCtx); - if (nodeId) { - this.db.setClass(nodeId, classId); - } - } - - return this.visitChildren(ctx); - } - - // Vertex visitor - handles basic vertex definitions - visitVertex(ctx: any): any { - // Extract node information from vertex context - let nodeId = ''; - let nodeText = ''; - let nodeType = 'square'; // default - - // Handle different vertex types based on the grammar - if (ctx.NODE_STRING) { - nodeId = ctx.NODE_STRING().getText(); - nodeText = nodeId; // default text is the ID - } else if (ctx.getText) { - const fullText = ctx.getText(); - // Parse vertex text to extract ID and shape information - const match = fullText.match(/^([A-Za-z0-9_]+)/); - if (match) { - nodeId = match[1]; - nodeText = nodeId; - } - - // Determine node type from shape delimiters - if (fullText.includes('[') && fullText.includes(']')) { - nodeType = 'square'; - // Extract text between brackets - const textMatch = fullText.match(/\[([^\]]*)\]/); - if (textMatch) { - nodeText = textMatch[1]; - } - } else if (fullText.includes('(') && fullText.includes(')')) { - nodeType = 'round'; - // Extract text between parentheses - const textMatch = fullText.match(/\(([^\)]*)\)/); - if (textMatch) { - nodeText = textMatch[1]; - } - } - // Add more shape type detection as needed - } - - // Add the vertex to the database if we have a valid node ID - if (nodeId) { - this.db.addVertex(nodeId, nodeText, nodeType); - } - - return this.visitChildren(ctx); - } - - // Link visitor - handles edge/connection definitions - visitLink(ctx: any): any { - // Handle link parsing - this is a placeholder for now - // The actual link parsing is complex and handled by the existing regex-based approach - return this.visitChildren(ctx); - } - - // Shape data visitor methods - visitShapeData(ctx: any): string { - // Handle shape data parsing through ANTLR visitor pattern - const content = this.visitShapeDataContent(ctx.shapeDataContent()); - return content; - } - - visitShapeDataForNode(shapeDataCtx: any, nodeCtx: any): void { - console.log('visitShapeDataForNode called'); - // Handle shape data for a specific node - const content = this.visitShapeData(shapeDataCtx); - const nodeId = this.extractNodeIdFromVertexContext(nodeCtx); - - console.log('Shape data content:', content); - console.log('Node ID:', nodeId); - - if (nodeId && content) { - // Parse the shape data content (key: value pairs) - const shapeData = this.parseShapeDataContent(content); - - console.log('Parsed shape data:', shapeData); - - // Apply the shape data to the node using FlowDB - this.applyShapeDataToNodeViaDB(nodeId, shapeData); - } - } - - visitShapeDataContent(ctx: any): string { - // Collect all shape data content tokens - let content = ''; - - if (ctx.SHAPE_DATA_CONTENT) { - if (Array.isArray(ctx.SHAPE_DATA_CONTENT())) { - content += ctx - .SHAPE_DATA_CONTENT() - .map((token: any) => token.getText()) - .join(''); - } else { - content += ctx.SHAPE_DATA_CONTENT().getText(); - } - } - - // Handle string content - if (ctx.SHAPE_DATA_STRING_START && ctx.SHAPE_DATA_STRING_CONTENT && ctx.SHAPE_DATA_STRING_END) { - const stringContents = ctx.SHAPE_DATA_STRING_CONTENT(); - if (Array.isArray(stringContents)) { - content += stringContents.map((token: any) => `"${token.getText()}"`).join(''); - } else { - content += `"${stringContents.getText()}"`; - } - } - - // Handle nested shape data content - if (ctx.shapeDataContent && ctx.shapeDataContent().length > 0) { - for (const childCtx of ctx.shapeDataContent()) { - content += this.visitShapeDataContent(childCtx); - } - } - - return content; - } - - // Helper method to extract node ID from vertex context - extractNodeIdFromVertexContext(vertexCtx: any): string | null { - if (!vertexCtx) return null; - - // Try different ways to extract the node ID from vertex context - if (vertexCtx.NODE_STRING) { - return vertexCtx.NODE_STRING().getText(); - } - - if (vertexCtx.getText) { - const text = vertexCtx.getText(); - // Extract node ID from vertex text (before any shape delimiters) - const match = text.match(/^([A-Za-z0-9_]+)/); - return match ? match[1] : null; - } - - return null; - } - - // Helper method to apply shape data to node via FlowDB (like Jison does) - applyShapeDataToNodeViaDB(nodeId: string, shapeData: any): void { - // Convert shape data to YAML string format that FlowDB expects - let yamlContent = ''; - - if (typeof shapeData === 'object' && shapeData !== null) { - const pairs: string[] = []; - for (const [key, value] of Object.entries(shapeData)) { - if (typeof value === 'string') { - pairs.push(`${key}: "${value}"`); - } else { - pairs.push(`${key}: ${value}`); - } - } - yamlContent = pairs.join('\n'); - } else if (typeof shapeData === 'string') { - yamlContent = shapeData; - } - - // Call FlowDB addVertex with shape data (8th parameter) like Jison does - // addVertex(id, textObj, textType, style, classes, dir, props, shapeData) - this.db.addVertex( - nodeId, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - yamlContent - ); - } - - private extractNodeIdFromShapeDataContext(ctx: any): string | null { - // Walk up the parse tree to find the node ID - let parent = ctx.parent; - - while (parent) { - // Check if this is a vertexStatement with a node - if (parent.node && parent.node().length > 0) { - const nodeCtx = parent.node(0); - if (nodeCtx.styledVertex && nodeCtx.styledVertex().vertex) { - const vertexCtx = nodeCtx.styledVertex().vertex(); - if (vertexCtx.NODE_STRING) { - return vertexCtx.NODE_STRING().getText(); - } - } - } - - // Check if this is a standaloneVertex - if (parent.NODE_STRING) { - return parent.NODE_STRING().getText(); - } - - parent = parent.parent; - } - - return null; - } - - // Text handling methods for markdown support - visitStringText(ctx: any): { text: string; type: string } { - return { text: ctx.STR().getText(), type: 'string' }; - } - - visitMarkdownText(ctx: any): { text: string; type: string } { - return { text: ctx.MD_STR().getText(), type: 'markdown' }; - } - - visitStringTextNoTags(ctx: any): { text: string; type: string } { - return { text: ctx.STR().getText(), type: 'text' }; - } - - visitMarkdownTextNoTags(ctx: any): { text: string; type: string } { - return { text: ctx.MD_STR().getText(), type: 'markdown' }; - } - - visitStringEdgeText(ctx: any): { text: string; type: string } { - return { text: ctx.STR().getText(), type: 'string' }; - } - - visitMarkdownEdgeText(ctx: any): { text: string; type: string } { - return { text: ctx.MD_STR().getText(), type: 'markdown' }; - } - - // Helper method to parse edge text and handle markdown strings - private parseEdgeText(edgeText: string): { text: string; type: string } { - if (!edgeText) { - return { text: '', type: 'text' }; - } - - const trimmedText = edgeText.trim(); - - // Check for markdown string pattern with quotes: "`text`" - const quotedMarkdownMatch = trimmedText.match(/^"`([^`]*)`"$/); - if (quotedMarkdownMatch) { - return { text: quotedMarkdownMatch[1], type: 'markdown' }; - } - - // Check for markdown string pattern without quotes: `text` - const markdownMatch = trimmedText.match(/^`([^`]*)`$/); - if (markdownMatch) { - return { text: markdownMatch[1], type: 'markdown' }; - } - - // Check for regular quoted string: "text" - const quotedMatch = trimmedText.match(/^"([^"]*)"$/); - if (quotedMatch) { - return { text: quotedMatch[1], type: 'string' }; - } - - // Regular text (already processed by lexer, no quotes) - return { text: trimmedText, type: 'string' }; - } -} diff --git a/packages/mermaid/src/diagrams/flowchart/parser/antlr/debug-tokenizer.cjs b/packages/mermaid/src/diagrams/flowchart/parser/antlr/debug-tokenizer.cjs deleted file mode 100644 index 49412abba..000000000 --- a/packages/mermaid/src/diagrams/flowchart/parser/antlr/debug-tokenizer.cjs +++ /dev/null @@ -1,15 +0,0 @@ -const { CharStream } = require('antlr4ng'); -const { FlowLexer } = require('./generated/FlowLexer.ts'); - -const input = 'D@{ shape: rounded }'; -console.log('Input:', input); - -const chars = CharStream.fromString(input); -const lexer = new FlowLexer(chars); -const tokens = lexer.getAllTokens(); - -console.log('Tokens:'); -for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - console.log(` [${i}] Type: ${token.type}, Text: '${token.text}', Channel: ${token.channel}`); -}