2 failing tests

This commit is contained in:
Knut Sveidqvist
2025-06-23 11:03:28 +02:00
parent cb6f8e51a2
commit 55b69d7df8
6 changed files with 131 additions and 44 deletions

View File

@@ -160,7 +160,9 @@ export class FlowDB implements DiagramDB {
if (textObj !== undefined) { if (textObj !== undefined) {
this.config = getConfig(); 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; vertex.labelType = textObj.type;
// strip quotes if string starts and ends with a quote // strip quotes if string starts and ends with a quote
if (txt.startsWith('"') && txt.endsWith('"')) { if (txt.startsWith('"') && txt.endsWith('"')) {
@@ -1037,7 +1039,7 @@ You have to call mermaid.initialize.`
} else { } else {
const baseNode = { const baseNode = {
id: vertex.id, id: vertex.id,
label: vertex.text, label: vertex.text?.replace(/<br>/g, '<br/>'),
labelStyle: '', labelStyle: '',
parentId, parentId,
padding: config.flowchart?.padding || 8, padding: config.flowchart?.padding || 8,

View File

@@ -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 () { 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(); const vert = flow.yy.getVertices();
expect(vert.get('odd-').type).toBe('odd'); expect(vert.get('odd-').type).toBe('odd');

View File

@@ -308,6 +308,8 @@ export class FlowchartAstVisitor extends BaseVisitor {
this.visit(ctx.vertexWithCylinder); this.visit(ctx.vertexWithCylinder);
} else if (ctx.vertexWithOdd) { } else if (ctx.vertexWithOdd) {
this.visit(ctx.vertexWithOdd); this.visit(ctx.vertexWithOdd);
} else if (ctx.vertexWithNodeIdOdd) {
this.visit(ctx.vertexWithNodeIdOdd);
} else if (ctx.vertexWithRect) { } else if (ctx.vertexWithRect) {
this.visit(ctx.vertexWithRect); this.visit(ctx.vertexWithRect);
} else if (ctx.vertexWithNodeData) { } else if (ctx.vertexWithNodeData) {
@@ -447,6 +449,15 @@ export class FlowchartAstVisitor extends BaseVisitor {
this.addVertex(nodeId, nodeTextData.text, 'odd', nodeTextData.labelType); 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 { vertexWithRect(ctx: any): void {
const nodeId = this.visit(ctx.nodeId); const nodeId = this.visit(ctx.nodeId);
const nodeTextData = this.visit(ctx.nodeText); const nodeTextData = this.visit(ctx.nodeText);
@@ -726,19 +737,31 @@ export class FlowchartAstVisitor extends BaseVisitor {
private parseNodeDataContent(content: string, props: any): void { private parseNodeDataContent(content: string, props: any): void {
// Parse YAML-like content: "shape: rounded\nlabel: 'Hello'" or "shape: rounded, label: 'Hello'" // Parse YAML-like content: "shape: rounded\nlabel: 'Hello'" or "shape: rounded, label: 'Hello'"
// Handle both single-line and multi-line formats // 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; // First, split by lines to handle multi-line YAML format
while (i < lines.length) { const lines = content.split('\n');
// Process each line
for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
const trimmedLine = line.trim(); const trimmedLine = line.trim();
// Skip empty lines // Skip empty lines
if (!trimmedLine) { if (!trimmedLine) {
i++;
continue; 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(':'); const colonIndex = trimmedLine.indexOf(':');
if (colonIndex > 0) { if (colonIndex > 0) {
const key = trimmedLine.substring(0, colonIndex).trim(); const key = trimmedLine.substring(0, colonIndex).trim();
@@ -746,7 +769,6 @@ export class FlowchartAstVisitor extends BaseVisitor {
// Skip empty values // Skip empty values
if (!value) { if (!value) {
i++;
continue; continue;
} }
@@ -788,7 +810,8 @@ export class FlowchartAstVisitor extends BaseVisitor {
} }
// Join multiline content with newlines and add final newline // 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 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(); const nextLine = lines[i].trim();
if (nextLine.endsWith(quote)) { if (nextLine.endsWith(quote)) {
// Found closing quote // Found closing quote
multilineValue += '<br/>' + nextLine.substring(0, nextLine.length - 1); multilineValue += '<br>' + nextLine.substring(0, nextLine.length - 1);
break; break;
} else { } else {
// Continue collecting lines // Continue collecting lines
multilineValue += '<br/>' + nextLine; multilineValue += '<br>' + nextLine;
} }
i++; i++;
} }
@@ -820,31 +843,80 @@ export class FlowchartAstVisitor extends BaseVisitor {
continue; continue;
} }
// Remove quotes if present (for single-line quoted strings) // Process single property
if ( this.parseProperty(trimmedLine, props, lines, i);
(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;
} }
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 // Map shape names from node data to FlowDB types
private mapShapeNameToType(shapeName: string): string { private mapShapeNameToType(shapeName: string): string {
// Map common shape names to their FlowDB equivalents // Map common shape names to their FlowDB equivalents
@@ -2198,17 +2270,15 @@ export class FlowchartAstVisitor extends BaseVisitor {
const numbers: number[] = []; const numbers: number[] = [];
// Handle properly tokenized numbers (NumberToken, Comma, NumberToken, ...) // Handle properly tokenized numbers (NumberToken, Comma, NumberToken, ...)
if (ctx.NumberToken && !ctx.NODE_STRING) { if (ctx.NumberToken) {
ctx.NumberToken.forEach((token: any) => { ctx.NumberToken.forEach((token: any) => {
numbers.push(parseInt(token.image, 10)); numbers.push(parseInt(token.image, 10));
}); });
} }
// Handle mixed case: NumberToken followed by NODE_STRING (e.g., "0" + ",1") // Handle comma-separated numbers that got tokenized as NODE_STRING (e.g., "0,1")
if (ctx.NumberToken && ctx.NODE_STRING) { // Only if there are no NumberToken (to avoid conflicts with styles)
// Add the first number if (ctx.NODE_STRING && !ctx.NumberToken) {
numbers.push(parseInt(ctx.NumberToken[0].image, 10));
// Parse the comma-separated part // Parse the comma-separated part
const nodeString = ctx.NODE_STRING[0].image; const nodeString = ctx.NODE_STRING[0].image;
if (nodeString.startsWith(',')) { if (nodeString.startsWith(',')) {

View File

@@ -1430,6 +1430,13 @@ const LeanRightStart = createToken({
// The distinction between lean_left and inv_trapezoid is made in the parser // The distinction between lean_left and inv_trapezoid is made in the parser
// Odd vertex tokens // 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({ const OddStart = createToken({
name: 'OddStart', name: 'OddStart',
pattern: />/, pattern: />/,
@@ -1820,6 +1827,7 @@ const multiModeLexerDefinition = {
LINK_ID, LINK_ID,
// Odd shape start (must come before DirectionValue to avoid conflicts) // Odd shape start (must come before DirectionValue to avoid conflicts)
NodeIdWithOddStart, // Must come before OddStart to handle "nodeId->" pattern
OddStart, OddStart,
// Direction values (must come after LINK tokens and OddStart) // Direction values (must come after LINK tokens and OddStart)
@@ -2025,6 +2033,7 @@ export const allTokens = [
HexagonEnd, HexagonEnd,
DiamondStart, DiamondStart,
DiamondEnd, DiamondEnd,
NodeIdWithOddStart,
OddStart, OddStart,
// Numbers must come before NODE_STRING to avoid being captured by it // Numbers must come before NODE_STRING to avoid being captured by it
@@ -2208,6 +2217,7 @@ export {
HexagonEnd, HexagonEnd,
DiamondStart, DiamondStart,
DiamondEnd, DiamondEnd,
NodeIdWithOddStart,
OddStart, OddStart,
// Text content // Text content

View File

@@ -201,6 +201,7 @@ export class FlowchartParser extends CstParser {
{ ALT: () => this.SUBRULE2(this.vertexWithEllipse) }, { ALT: () => this.SUBRULE2(this.vertexWithEllipse) },
{ ALT: () => this.SUBRULE2(this.vertexWithCylinder) }, { ALT: () => this.SUBRULE2(this.vertexWithCylinder) },
{ ALT: () => this.SUBRULE(this.vertexWithOdd) }, { ALT: () => this.SUBRULE(this.vertexWithOdd) },
{ ALT: () => this.SUBRULE(this.vertexWithNodeIdOdd) },
{ ALT: () => this.SUBRULE(this.vertexWithRect) }, { ALT: () => this.SUBRULE(this.vertexWithRect) },
// Node with data syntax only // Node with data syntax only
{ ALT: () => this.SUBRULE(this.vertexWithNodeData) }, { ALT: () => this.SUBRULE(this.vertexWithNodeData) },
@@ -325,6 +326,13 @@ export class FlowchartParser extends CstParser {
this.CONSUME(tokens.SquareEnd); 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', () => { private vertexWithRect = this.RULE('vertexWithRect', () => {
this.SUBRULE(this.nodeId); this.SUBRULE(this.nodeId);
this.CONSUME(tokens.RectStart); this.CONSUME(tokens.RectStart);
@@ -1094,16 +1102,13 @@ export class FlowchartParser extends CstParser {
this.CONSUME(tokens.Comma); this.CONSUME(tokens.Comma);
this.CONSUME2(tokens.NumberToken); 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") // 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: () => { ALT: () => {
this.CONSUME2(tokens.NODE_STRING); this.CONSUME(tokens.NODE_STRING);
}, },
}, },
]); ]);

View File

@@ -19,7 +19,7 @@ describe('Lexer Comparison Tests', () => {
const input = 'graph TD'; const input = 'graph TD';
const expected: ExpectedToken[] = [ const expected: ExpectedToken[] = [
{ type: 'GRAPH', value: 'graph' }, { type: 'GRAPH', value: 'graph' },
{ type: 'DIR', value: 'TD' }, { type: 'DirectionValue', value: 'TD' },
]; ];
expect(() => runTest('GRA001', input, expected)).not.toThrow(); expect(() => runTest('GRA001', input, expected)).not.toThrow();