mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-18 03:34:12 +01:00
2 failing tests
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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(',')) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user