mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-10-31 02:44:17 +01:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
			fix-edge-d
			...
			bug/2492-f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 8127d01db7 | ||
|   | 3ac107508b | ||
|   | c3bf04b72e | ||
|   | 279c62af72 | ||
|   | 81d00bd4e4 | 
| @@ -1,5 +0,0 @@ | ||||
| --- | ||||
| 'mermaid': patch | ||||
| --- | ||||
|  | ||||
| fix: Allow IDs starting with L, R, T, or B in parser | ||||
| @@ -266,4 +266,156 @@ describe('[Arrows] when parsing', () => { | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('Issue #2492: Node names starting with o/x should not be consumed by arrow markers', () => { | ||||
|     it('should handle node names starting with "o" after plain arrows', function () { | ||||
|       const res = flow.parser.parse('graph TD;\ndev---ops;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('dev').id).toBe('dev'); | ||||
|       expect(vert.get('ops').id).toBe('ops'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('dev'); | ||||
|       expect(edges[0].end).toBe('ops'); | ||||
|       expect(edges[0].type).toBe('arrow_open'); | ||||
|       expect(edges[0].stroke).toBe('normal'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle node names starting with "x" after plain arrows', function () { | ||||
|       const res = flow.parser.parse('graph TD;\ndev---xerxes;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('dev').id).toBe('dev'); | ||||
|       expect(vert.get('xerxes').id).toBe('xerxes'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('dev'); | ||||
|       expect(edges[0].end).toBe('xerxes'); | ||||
|       expect(edges[0].type).toBe('arrow_open'); | ||||
|       expect(edges[0].stroke).toBe('normal'); | ||||
|     }); | ||||
|  | ||||
|     it('should still support circle arrows with spaces', function () { | ||||
|       const res = flow.parser.parse('graph TD;\nA --o B;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('A').id).toBe('A'); | ||||
|       expect(vert.get('B').id).toBe('B'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('A'); | ||||
|       expect(edges[0].end).toBe('B'); | ||||
|       expect(edges[0].type).toBe('arrow_circle'); | ||||
|       expect(edges[0].stroke).toBe('normal'); | ||||
|     }); | ||||
|  | ||||
|     it('should still support cross arrows with spaces', function () { | ||||
|       const res = flow.parser.parse('graph TD;\nC --x D;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('C').id).toBe('C'); | ||||
|       expect(vert.get('D').id).toBe('D'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('C'); | ||||
|       expect(edges[0].end).toBe('D'); | ||||
|       expect(edges[0].type).toBe('arrow_cross'); | ||||
|       expect(edges[0].stroke).toBe('normal'); | ||||
|     }); | ||||
|  | ||||
|     it('should support circle arrows to uppercase nodes without spaces', function () { | ||||
|       const res = flow.parser.parse('graph TD;\nA--oB;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('A').id).toBe('A'); | ||||
|       expect(vert.get('B').id).toBe('B'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('A'); | ||||
|       expect(edges[0].end).toBe('B'); | ||||
|       expect(edges[0].type).toBe('arrow_circle'); | ||||
|       expect(edges[0].stroke).toBe('normal'); | ||||
|     }); | ||||
|  | ||||
|     it('should support cross arrows to uppercase nodes without spaces', function () { | ||||
|       const res = flow.parser.parse('graph TD;\nA--xBar;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('A').id).toBe('A'); | ||||
|       expect(vert.get('Bar').id).toBe('Bar'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('A'); | ||||
|       expect(edges[0].end).toBe('Bar'); | ||||
|       expect(edges[0].type).toBe('arrow_cross'); | ||||
|       expect(edges[0].stroke).toBe('normal'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle thick arrows with lowercase node names starting with "o"', function () { | ||||
|       const res = flow.parser.parse('graph TD;\nalpha===omega;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('alpha').id).toBe('alpha'); | ||||
|       expect(vert.get('omega').id).toBe('omega'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('alpha'); | ||||
|       expect(edges[0].end).toBe('omega'); | ||||
|       expect(edges[0].type).toBe('arrow_open'); | ||||
|       expect(edges[0].stroke).toBe('thick'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle dotted arrows with lowercase node names starting with "o"', function () { | ||||
|       const res = flow.parser.parse('graph TD;\nfoo-.-opus;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('foo').id).toBe('foo'); | ||||
|       expect(vert.get('opus').id).toBe('opus'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('foo'); | ||||
|       expect(edges[0].end).toBe('opus'); | ||||
|       expect(edges[0].type).toBe('arrow_open'); | ||||
|       expect(edges[0].stroke).toBe('dotted'); | ||||
|     }); | ||||
|  | ||||
|     it('should still support dotted circle arrows with spaces', function () { | ||||
|       const res = flow.parser.parse('graph TD;\nB -.-o C;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('B').id).toBe('B'); | ||||
|       expect(vert.get('C').id).toBe('C'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('B'); | ||||
|       expect(edges[0].end).toBe('C'); | ||||
|       expect(edges[0].type).toBe('arrow_circle'); | ||||
|       expect(edges[0].stroke).toBe('dotted'); | ||||
|     }); | ||||
|  | ||||
|     it('should still support thick cross arrows with spaces', function () { | ||||
|       const res = flow.parser.parse('graph TD;\nC ==x D;'); | ||||
|  | ||||
|       const vert = flow.parser.yy.getVertices(); | ||||
|       const edges = flow.parser.yy.getEdges(); | ||||
|  | ||||
|       expect(vert.get('C').id).toBe('C'); | ||||
|       expect(vert.get('D').id).toBe('D'); | ||||
|       expect(edges.length).toBe(1); | ||||
|       expect(edges[0].start).toBe('C'); | ||||
|       expect(edges[0].end).toBe('D'); | ||||
|       expect(edges[0].type).toBe('arrow_cross'); | ||||
|       expect(edges[0].stroke).toBe('thick'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -0,0 +1,129 @@ | ||||
| import { describe, it, expect, beforeEach } from 'vitest'; | ||||
| import flow from './flowParser.js'; | ||||
| import { FlowDB } from '../flowDb.js'; | ||||
|  | ||||
| describe('Flowchart arrow parsing - Issue #2492', () => { | ||||
|   let flowDb: FlowDB; | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     flowDb = new FlowDB(); | ||||
|     flow.parser.yy = flowDb; | ||||
|     flowDb.clear(); | ||||
|   }); | ||||
|  | ||||
|   describe('Solid arrows with markers', () => { | ||||
|     it('should parse --> followed by uppercase node', () => { | ||||
|       const diagram = 'graph TD\nA-->B'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --> followed by lowercase node', () => { | ||||
|       const diagram = 'graph TD\nA-->b'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --> followed by space', () => { | ||||
|       const diagram = 'graph TD\nA--> B'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --- followed by uppercase node (issue #2492)', () => { | ||||
|       const diagram = 'graph TD\ndev---Ops'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --- followed by lowercase node (issue #2492)', () => { | ||||
|       const diagram = 'graph TD\ndev---ops'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --o followed by uppercase node', () => { | ||||
|       const diagram = 'graph TD\nA--oB'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --o followed by lowercase node', () => { | ||||
|       const diagram = 'graph TD\nA--ob'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --x followed by uppercase node', () => { | ||||
|       const diagram = 'graph TD\nA--xBar'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --x followed by lowercase node', () => { | ||||
|       const diagram = 'graph TD\nA--xbar'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('Thick arrows with markers', () => { | ||||
|     it('should parse ==> followed by uppercase node', () => { | ||||
|       const diagram = 'graph TD\nA==>B'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse ==> followed by lowercase node', () => { | ||||
|       const diagram = 'graph TD\nA==>b'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse === followed by lowercase node', () => { | ||||
|       const diagram = 'graph TD\nA===b'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('Dotted arrows with markers', () => { | ||||
|     it('should parse -.-> followed by uppercase node', () => { | ||||
|       const diagram = 'graph TD\nA-.->B'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse -.-> followed by lowercase node', () => { | ||||
|       const diagram = 'graph TD\nA-.->b'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse -.- followed by lowercase node', () => { | ||||
|       const diagram = 'graph TD\nA-.-b'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('Arrows with edge text', () => { | ||||
|     it('should parse arrow with edge text followed by uppercase node', () => { | ||||
|       const diagram = 'graph TD\nA-->|text|B'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse arrow with edge text followed by lowercase node', () => { | ||||
|       const diagram = 'graph TD\nA-->|text|b'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse multiple arrows with edge text (regression test)', () => { | ||||
|       const diagram = 'graph TD\nA-->|Get money|B\nB-->C\nC-->|One|D\nC-->|Two|E'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('Arrows followed by digits', () => { | ||||
|     it('should parse --> followed by digit', () => { | ||||
|       const diagram = 'graph LR\n47-->48'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse --> followed by node starting with digit', () => { | ||||
|       const diagram = 'graph LR\nA-->48(Node)'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|  | ||||
|     it('should parse complex diagram with digit node IDs (Sample 4)', () => { | ||||
|       const diagram = | ||||
|         'graph LR\n47(SAM.CommonFA.FMESummary)-->48(SAM.CommonFA.CommonFAFinanceBudget)\n37(SAM.CommonFA.BudgetSubserviceLineVolume)-->48(SAM.CommonFA.CommonFAFinanceBudget)'; | ||||
|       expect(() => flow.parser.parse(diagram)).not.toThrow(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -152,17 +152,29 @@ that id. | ||||
| ","                          return 'COMMA'; | ||||
| "*"                          return 'MULT'; | ||||
|  | ||||
| <INITIAL,edgeText>\s*[xo<]?\-\-+[-xo>]\s*          { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-\-\s*                          { this.pushState("edgeText"); return 'START_LINK'; } | ||||
| <edgeText>[^-]|\-(?!\-)+                           return 'EDGE_TEXT'; | ||||
| <INITIAL,edgeText>\s*[xo<]?\-\-+[-xo>]\s+                 { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-\-+[-xo>](?=[A-Z])                    { return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-\-+[-xo>](?=[a-z])                    { return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-\-+[-xo>](?=[0-9])                    { return 'LINK'; } | ||||
| <INITIAL,edgeText>\s*[xo<]?\-\-+[-xo>](?=\s*\|)           { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-\-\s*                              { this.pushState("edgeText"); return 'START_LINK'; } | ||||
| <edgeText>[^-]|\-(?!\-)+                              return 'EDGE_TEXT'; | ||||
|  | ||||
| <INITIAL,thickEdgeText>\s*[xo<]?\=\=+[=xo>]\s*      { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\=\=\s*                           { this.pushState("thickEdgeText"); return 'START_LINK'; } | ||||
| <thickEdgeText>[^=]|\=(?!=)                         return 'EDGE_TEXT'; | ||||
| <INITIAL,thickEdgeText>\s*[xo<]?\=\=+[=xo>]\s+            { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\=\=+[=xo>](?=[A-Z])                    { return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\=\=+[=xo>](?=[a-z])                    { return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\=\=+[=xo>](?=[0-9])                    { return 'LINK'; } | ||||
| <INITIAL,thickEdgeText>\s*[xo<]?\=\=+[=xo>](?=\s*\|)      { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\=\=\s*                              { this.pushState("thickEdgeText"); return 'START_LINK'; } | ||||
| <thickEdgeText>[^=]|\=(?!=)                            return 'EDGE_TEXT'; | ||||
|  | ||||
| <INITIAL,dottedEdgeText>\s*[xo<]?\-?\.+\-[xo>]?\s*   { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-\.\s*                            { this.pushState("dottedEdgeText"); return 'START_LINK'; } | ||||
| <dottedEdgeText>[^\.]|\.(?!-)                        return 'EDGE_TEXT'; | ||||
| <INITIAL,dottedEdgeText>\s*[xo<]?\-?\.+\-[xo>]?\s+         { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-?\.+\-[xo>]?(?=[A-Z])                  { return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-?\.+\-[xo>]?(?=[a-z])                  { return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-?\.+\-[xo>]?(?=[0-9])                  { return 'LINK'; } | ||||
| <INITIAL,dottedEdgeText>\s*[xo<]?\-?\.+\-[xo>]?(?=\s*\|)   { this.popState(); return 'LINK'; } | ||||
| <INITIAL>\s*[xo<]?\-\.\s*                               { this.pushState("dottedEdgeText"); return 'START_LINK'; } | ||||
| <dottedEdgeText>[^\.]|\.(?!-)                           return 'EDGE_TEXT'; | ||||
|  | ||||
|  | ||||
| <*>\s*\~\~[\~]+\s*              return 'LINK'; | ||||
|   | ||||
| @@ -20,11 +20,11 @@ fragment Statement: | ||||
| ; | ||||
|  | ||||
| fragment LeftPort: | ||||
|     ':' lhsDir=ID | ||||
|     ':'lhsDir=ARROW_DIRECTION | ||||
| ; | ||||
|  | ||||
| fragment RightPort: | ||||
|     rhsDir=ID ':' | ||||
|     rhsDir=ARROW_DIRECTION':' | ||||
| ; | ||||
|  | ||||
| fragment Arrow: | ||||
| @@ -47,5 +47,6 @@ Edge: | ||||
|     lhsId=ID lhsGroup?=ARROW_GROUP? Arrow rhsId=ID rhsGroup?=ARROW_GROUP? EOL | ||||
| ; | ||||
|  | ||||
| terminal ARROW_DIRECTION: 'L' | 'R' | 'T' | 'B'; | ||||
| terminal ARROW_GROUP: /\{group\}/; | ||||
| terminal ARROW_INTO: /<|>/; | ||||
|   | ||||
| @@ -19,64 +19,6 @@ describe('architecture', () => { | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('should handle services', () => { | ||||
|     it('should handle service with icon', () => { | ||||
|       const context = `architecture-beta | ||||
|         service TH(disk) | ||||
|       `; | ||||
|       const result = parse(context); | ||||
|       expectNoErrorsOrAlternatives(result); | ||||
|       expect(result.value.$type).toBe(Architecture); | ||||
|       expect(result.value.services).toHaveLength(1); | ||||
|       expect(result.value.services?.[0].id).toBe('TH'); | ||||
|       expect(result.value.services?.[0].icon).toBe('disk'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle service with icon starting with arrow direction letters', () => { | ||||
|       const context = `architecture-beta | ||||
|         service T(disk) | ||||
|         service TH(database) | ||||
|         service L(server) | ||||
|         service R(cloud) | ||||
|         service B(internet) | ||||
|         service TOP(disk) | ||||
|         service LEFT(disk) | ||||
|         service RIGHT(disk) | ||||
|         service BOTTOM(disk) | ||||
|       `; | ||||
|       const result = parse(context); | ||||
|       expectNoErrorsOrAlternatives(result); | ||||
|       expect(result.value.$type).toBe(Architecture); | ||||
|       expect(result.value.services).toHaveLength(9); | ||||
|     }); | ||||
|  | ||||
|     it('should handle service with icon and title', () => { | ||||
|       const context = `architecture-beta | ||||
|         service db(database)[Database] | ||||
|       `; | ||||
|       const result = parse(context); | ||||
|       expectNoErrorsOrAlternatives(result); | ||||
|       expect(result.value.$type).toBe(Architecture); | ||||
|       expect(result.value.services).toHaveLength(1); | ||||
|       expect(result.value.services?.[0].id).toBe('db'); | ||||
|       expect(result.value.services?.[0].icon).toBe('database'); | ||||
|       expect(result.value.services?.[0].title).toBe('Database'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle service in a group', () => { | ||||
|       const context = `architecture-beta | ||||
|         group api(cloud)[API] | ||||
|         service db(database)[Database] in api | ||||
|       `; | ||||
|       const result = parse(context); | ||||
|       expectNoErrorsOrAlternatives(result); | ||||
|       expect(result.value.$type).toBe(Architecture); | ||||
|       expect(result.value.services).toHaveLength(1); | ||||
|       expect(result.value.services?.[0].id).toBe('db'); | ||||
|       expect(result.value.services?.[0].in).toBe('api'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('should handle TitleAndAccessibilities', () => { | ||||
|     it.each([ | ||||
|       `architecture-beta title sample title`, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user