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) {
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(/<br>/g, '<br/>'),
labelStyle: '',
parentId,
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 () {
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');

View File

@@ -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 += '<br/>' + nextLine.substring(0, nextLine.length - 1);
multilineValue += '<br>' + nextLine.substring(0, nextLine.length - 1);
break;
} else {
// Continue collecting lines
multilineValue += '<br/>' + nextLine;
multilineValue += '<br>' + 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(',')) {

View File

@@ -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

View File

@@ -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);
},
},
]);

View File

@@ -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();