From 55b69d7df845b0a9e5cb3ca574a046494c36de6d Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Mon, 23 Jun 2025 11:03:28 +0200 Subject: [PATCH] 2 failing tests --- .../mermaid/src/diagrams/flowchart/flowDb.ts | 6 +- .../flowchart/parser/flow-chev-text.spec.js | 2 +- .../src/diagrams/flowchart/parser/flowAst.ts | 140 +++++++++++++----- .../diagrams/flowchart/parser/flowLexer.ts | 10 ++ .../diagrams/flowchart/parser/flowParser.ts | 15 +- .../flowchart/parser/lexer-comparison.spec.ts | 2 +- 6 files changed, 131 insertions(+), 44 deletions(-) diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts index d4491c51b..91728a695 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts @@ -160,7 +160,9 @@ export class FlowDB implements DiagramDB { if (textObj !== undefined) { this.config = getConfig(); - txt = this.sanitizeText(textObj.text.trim()); + // Don't trim text that contains newlines to preserve YAML multi-line formatting + const shouldTrim = !textObj.text.includes('\n'); + txt = this.sanitizeText(shouldTrim ? textObj.text.trim() : textObj.text); vertex.labelType = textObj.type; // strip quotes if string starts and ends with a quote if (txt.startsWith('"') && txt.endsWith('"')) { @@ -1037,7 +1039,7 @@ You have to call mermaid.initialize.` } else { const baseNode = { id: vertex.id, - label: vertex.text, + label: vertex.text?.replace(/
/g, '
'), labelStyle: '', parentId, padding: config.flowchart?.padding || 8, diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-chev-text.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-chev-text.spec.js index 1f61b0573..5899e295c 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-chev-text.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-chev-text.spec.js @@ -361,7 +361,7 @@ describe('[Text] when parsing with Chevrotain', () => { }); it('should handle edge case for odd vertex with node id ending with minus', function () { - const res = flow.parse('graph TD;A_node-->odd->Vertex Text];'); + flow.parse('graph TD;A_node-->odd->Vertex Text];'); const vert = flow.yy.getVertices(); expect(vert.get('odd-').type).toBe('odd'); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flowAst.ts b/packages/mermaid/src/diagrams/flowchart/parser/flowAst.ts index b38678e93..52e0f077d 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flowAst.ts +++ b/packages/mermaid/src/diagrams/flowchart/parser/flowAst.ts @@ -308,6 +308,8 @@ export class FlowchartAstVisitor extends BaseVisitor { this.visit(ctx.vertexWithCylinder); } else if (ctx.vertexWithOdd) { this.visit(ctx.vertexWithOdd); + } else if (ctx.vertexWithNodeIdOdd) { + this.visit(ctx.vertexWithNodeIdOdd); } else if (ctx.vertexWithRect) { this.visit(ctx.vertexWithRect); } else if (ctx.vertexWithNodeData) { @@ -447,6 +449,15 @@ export class FlowchartAstVisitor extends BaseVisitor { this.addVertex(nodeId, nodeTextData.text, 'odd', nodeTextData.labelType); } + // Special visitor for node IDs ending with minus followed by odd start (e.g., "odd->text]") + vertexWithNodeIdOdd(ctx: any): void { + // Extract node ID from NodeIdWithOddStart token (remove the trailing ">") + const nodeIdWithOddStart = ctx.NodeIdWithOddStart[0].image; + const nodeId = nodeIdWithOddStart.slice(0, -1); // Remove the ">" suffix, keep the "-" + const nodeTextData = this.visit(ctx.nodeText); + this.addVertex(nodeId, nodeTextData.text, 'odd', nodeTextData.labelType); + } + vertexWithRect(ctx: any): void { const nodeId = this.visit(ctx.nodeId); const nodeTextData = this.visit(ctx.nodeText); @@ -726,19 +737,31 @@ export class FlowchartAstVisitor extends BaseVisitor { private parseNodeDataContent(content: string, props: any): void { // Parse YAML-like content: "shape: rounded\nlabel: 'Hello'" or "shape: rounded, label: 'Hello'" // Handle both single-line and multi-line formats - const lines = content.split('\n'); // Don't trim yet - we need to preserve indentation for YAML-style parsing - let i = 0; - while (i < lines.length) { + // First, split by lines to handle multi-line YAML format + const lines = content.split('\n'); + + // Process each line + for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Skip empty lines if (!trimmedLine) { - i++; continue; } + // Check if this line contains comma-separated properties (single-line format) + if (trimmedLine.includes(',') && trimmedLine.includes(':')) { + // Split by comma and process each property + const properties = this.splitPropertiesByComma(trimmedLine); + for (const property of properties) { + this.parseProperty(property.trim(), props, lines, i); + } + continue; + } + + // Handle single property per line (multi-line YAML format) const colonIndex = trimmedLine.indexOf(':'); if (colonIndex > 0) { const key = trimmedLine.substring(0, colonIndex).trim(); @@ -746,7 +769,6 @@ export class FlowchartAstVisitor extends BaseVisitor { // Skip empty values if (!value) { - i++; continue; } @@ -788,7 +810,8 @@ export class FlowchartAstVisitor extends BaseVisitor { } // Join multiline content with newlines and add final newline - props[key] = multilineContent.join('\n') + '\n'; + const result = multilineContent.join('\n') + '\n'; + props[key] = result; continue; // Don't increment i again since we already did it in the loop } @@ -806,11 +829,11 @@ export class FlowchartAstVisitor extends BaseVisitor { const nextLine = lines[i].trim(); if (nextLine.endsWith(quote)) { // Found closing quote - multilineValue += '
' + nextLine.substring(0, nextLine.length - 1); + multilineValue += '
' + nextLine.substring(0, nextLine.length - 1); break; } else { // Continue collecting lines - multilineValue += '
' + nextLine; + multilineValue += '
' + nextLine; } i++; } @@ -820,31 +843,80 @@ export class FlowchartAstVisitor extends BaseVisitor { continue; } - // Remove quotes if present (for single-line quoted strings) - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - - // Convert boolean and numeric values - let parsedValue: any = value; - if (value === 'true') { - parsedValue = true; - } else if (value === 'false') { - parsedValue = false; - } else if (!isNaN(Number(value)) && value !== '') { - parsedValue = Number(value); - } - - props[key] = parsedValue; + // Process single property + this.parseProperty(trimmedLine, props, lines, i); } - - i++; } } + // Helper method to split properties by comma while respecting quoted strings + private splitPropertiesByComma(line: string): string[] { + const properties: string[] = []; + let current = ''; + let inQuotes = false; + let quoteChar = ''; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if ((char === '"' || char === "'") && !inQuotes) { + inQuotes = true; + quoteChar = char; + current += char; + } else if (char === quoteChar && inQuotes) { + inQuotes = false; + quoteChar = ''; + current += char; + } else if (char === ',' && !inQuotes) { + if (current.trim()) { + properties.push(current.trim()); + } + current = ''; + } else { + current += char; + } + } + + // Add the last property + if (current.trim()) { + properties.push(current.trim()); + } + + return properties; + } + + // Helper method to parse a single property + private parseProperty(property: string, props: any, lines: string[], lineIndex: number): void { + const colonIndex = property.indexOf(':'); + if (colonIndex <= 0) return; + + const key = property.substring(0, colonIndex).trim(); + let value = property.substring(colonIndex + 1).trim(); + + // Skip empty values + if (!value) return; + + // Remove quotes if present (for single-line quoted strings) + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + // Convert boolean and numeric values + let parsedValue: any = value; + if (value === 'true') { + parsedValue = true; + } else if (value === 'false') { + parsedValue = false; + } else if (!isNaN(Number(value)) && value !== '') { + parsedValue = Number(value); + } + + props[key] = parsedValue; + } + // Map shape names from node data to FlowDB types private mapShapeNameToType(shapeName: string): string { // Map common shape names to their FlowDB equivalents @@ -2198,17 +2270,15 @@ export class FlowchartAstVisitor extends BaseVisitor { const numbers: number[] = []; // Handle properly tokenized numbers (NumberToken, Comma, NumberToken, ...) - if (ctx.NumberToken && !ctx.NODE_STRING) { + if (ctx.NumberToken) { ctx.NumberToken.forEach((token: any) => { numbers.push(parseInt(token.image, 10)); }); } - // Handle mixed case: NumberToken followed by NODE_STRING (e.g., "0" + ",1") - if (ctx.NumberToken && ctx.NODE_STRING) { - // Add the first number - numbers.push(parseInt(ctx.NumberToken[0].image, 10)); - + // Handle comma-separated numbers that got tokenized as NODE_STRING (e.g., "0,1") + // Only if there are no NumberToken (to avoid conflicts with styles) + if (ctx.NODE_STRING && !ctx.NumberToken) { // Parse the comma-separated part const nodeString = ctx.NODE_STRING[0].image; if (nodeString.startsWith(',')) { diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flowLexer.ts b/packages/mermaid/src/diagrams/flowchart/parser/flowLexer.ts index faec142e0..63944f687 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flowLexer.ts +++ b/packages/mermaid/src/diagrams/flowchart/parser/flowLexer.ts @@ -1430,6 +1430,13 @@ const LeanRightStart = createToken({ // The distinction between lean_left and inv_trapezoid is made in the parser // Odd vertex tokens +// Special token for node IDs ending with minus followed by odd start (e.g., "odd->") +const NodeIdWithOddStart = createToken({ + name: 'NodeIdWithOddStart', + pattern: /([A-Za-z0-9!"#$%&'*+.`?\\_/,]|:(?!::)|-(?=[^>.-])|=(?!=))*->/, + push_mode: 'text_mode', +}); + const OddStart = createToken({ name: 'OddStart', pattern: />/, @@ -1820,6 +1827,7 @@ const multiModeLexerDefinition = { LINK_ID, // Odd shape start (must come before DirectionValue to avoid conflicts) + NodeIdWithOddStart, // Must come before OddStart to handle "nodeId->" pattern OddStart, // Direction values (must come after LINK tokens and OddStart) @@ -2025,6 +2033,7 @@ export const allTokens = [ HexagonEnd, DiamondStart, DiamondEnd, + NodeIdWithOddStart, OddStart, // Numbers must come before NODE_STRING to avoid being captured by it @@ -2208,6 +2217,7 @@ export { HexagonEnd, DiamondStart, DiamondEnd, + NodeIdWithOddStart, OddStart, // Text content diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts b/packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts index a695419f9..af3ac17ab 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts +++ b/packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts @@ -201,6 +201,7 @@ export class FlowchartParser extends CstParser { { ALT: () => this.SUBRULE2(this.vertexWithEllipse) }, { ALT: () => this.SUBRULE2(this.vertexWithCylinder) }, { ALT: () => this.SUBRULE(this.vertexWithOdd) }, + { ALT: () => this.SUBRULE(this.vertexWithNodeIdOdd) }, { ALT: () => this.SUBRULE(this.vertexWithRect) }, // Node with data syntax only { ALT: () => this.SUBRULE(this.vertexWithNodeData) }, @@ -325,6 +326,13 @@ export class FlowchartParser extends CstParser { this.CONSUME(tokens.SquareEnd); }); + // Special rule for node IDs ending with minus followed by odd start (e.g., "odd->text]") + private vertexWithNodeIdOdd = this.RULE('vertexWithNodeIdOdd', () => { + this.CONSUME(tokens.NodeIdWithOddStart); + this.SUBRULE(this.nodeText); + this.CONSUME(tokens.SquareEnd); + }); + private vertexWithRect = this.RULE('vertexWithRect', () => { this.SUBRULE(this.nodeId); this.CONSUME(tokens.RectStart); @@ -1094,16 +1102,13 @@ export class FlowchartParser extends CstParser { this.CONSUME(tokens.Comma); this.CONSUME2(tokens.NumberToken); }); - // Optionally handle mixed case: NumberToken followed by NODE_STRING - this.OPTION(() => { - this.CONSUME(tokens.NODE_STRING); - }); }, }, // Handle comma-separated numbers that got tokenized as NODE_STRING (e.g., "0,1") + // Only consume NODE_STRING if it looks like a number list (contains only digits and commas) { ALT: () => { - this.CONSUME2(tokens.NODE_STRING); + this.CONSUME(tokens.NODE_STRING); }, }, ]); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/lexer-comparison.spec.ts b/packages/mermaid/src/diagrams/flowchart/parser/lexer-comparison.spec.ts index 898ea6563..01f3085b5 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/lexer-comparison.spec.ts +++ b/packages/mermaid/src/diagrams/flowchart/parser/lexer-comparison.spec.ts @@ -19,7 +19,7 @@ describe('Lexer Comparison Tests', () => { const input = 'graph TD'; const expected: ExpectedToken[] = [ { type: 'GRAPH', value: 'graph' }, - { type: 'DIR', value: 'TD' }, + { type: 'DirectionValue', value: 'TD' }, ]; expect(() => runTest('GRA001', input, expected)).not.toThrow();