From 2fef44b43f128dfbe958591b453666b391646d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Dourado?= Date: Wed, 25 Mar 2026 19:15:20 +0000 Subject: [PATCH 1/6] fix #7091: Ensure only one word is allowed between 'state' and '{' The parser allowed multiple words between the 'state' keyword and the '{' character, leading to incorrect parsing of state diagrams. Added a new rule to the parser to enforce a single-word constraint between 'state' and '{'. This ensures invalid syntax is rejected with an appropriate error message. --- .../diagrams/state/parser/state-parser.spec.js | 15 +++++++++++++-- .../src/diagrams/state/parser/stateDiagram.jison | 6 ++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js index c498b4b1a3d..988e0d6b568 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js @@ -14,6 +14,17 @@ describe('state parser can parse...', () => { stateDiagram.parser.yy.clear(); }); + describe('invalid name between state and curly bracket', () => { + it('should throw error when name has multiple words', () => { + const diagramText = `stateDiagram-v2 + state invalid syntax { X }`; + + expect(() => { + stateDiagram.parser.parse(diagramText); + }).toThrow('Error: State name must be a single word.'); + }); + }); + describe('states with id displayed as a (name)', () => { describe('syntax 1: stateID as "name in quotes"', () => { it('stateID as "some name"', () => { @@ -55,8 +66,8 @@ describe('state parser can parse...', () => { const diagramText = `stateDiagram-v2 assemble assemblies - state assemble - state assemblies + state assemble { innerState1 } + state assemblies {innerState2 } `; stateDiagram.parser.parse(diagramText); const states = stateDiagram.parser.yy.getStates(); diff --git a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison index 59cb3cd11b8..97b053c41e7 100644 --- a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison +++ b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison @@ -167,6 +167,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili /lex %left '^' +%right COMPOSIT_STATE /* fix the shift/reduce conflicts from new rule */ %start start @@ -229,6 +230,11 @@ statement // console.log('Adding document for state without id ', $1); $$={ stmt: 'state', id: $1, type: 'default', description: '', doc: $3 } } + | COMPOSIT_STATE COMPOSIT_STATE STRUCT_START document STRUCT_STOP + { + /* Rejects invalid syntax: multiple words between 'state' and '{' */ + throw new Error('Error: State name must be a single word.'); + } | STATE_DESCR AS ID { var id=$3; var description = $1.trim(); From ccc030d4f99c82fc8b4b3714aeb867472cac6fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Dourado?= Date: Fri, 3 Apr 2026 18:31:18 +0100 Subject: [PATCH 2/6] fix #7091: adjust rules to fit existing tests --- .../mermaid/src/diagrams/state/parser/state-parser.spec.js | 4 ++-- .../mermaid/src/diagrams/state/parser/stateDiagram.jison | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js index 988e0d6b568..d1fbbbaba7b 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js @@ -66,8 +66,8 @@ describe('state parser can parse...', () => { const diagramText = `stateDiagram-v2 assemble assemblies - state assemble { innerState1 } - state assemblies {innerState2 } + state assemble + state assemblies `; stateDiagram.parser.parse(diagramText); const states = stateDiagram.parser.yy.getStates(); diff --git a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison index 97b053c41e7..b9adfceb6e8 100644 --- a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison +++ b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison @@ -131,7 +131,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili ["] { this.popState(); } [^"]* { /* console.log('Long description:', yytext); */ return "STATE_DESCR"; } [^\n\s\{]+ { /* console.log('COMPOSIT_STATE', yytext); */ return 'COMPOSIT_STATE'; } -\n { this.popState(); } +\n { this.popState(); return 'NL'; } \{ { this.popState(); this.pushState('struct'); /* console.log('begin struct', yytext); */ return 'STRUCT_START'; } \} { /*console.log('Ending struct');*/ this.popState(); return 'STRUCT_STOP';} } [\n] /* nothing */ @@ -167,6 +167,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili /lex %left '^' +%left NL %right COMPOSIT_STATE /* fix the shift/reduce conflicts from new rule */ %start start @@ -225,6 +226,10 @@ statement | HIDE_EMPTY | scale WIDTH | COMPOSIT_STATE + | COMPOSIT_STATE NL + { + $$={ stmt: 'state', id: $1, type: 'default', description: '', doc: [] } + } | COMPOSIT_STATE STRUCT_START document STRUCT_STOP { // console.log('Adding document for state without id ', $1); From d323bb45cb1b0238d09ccc4fa1cde913ae84d84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Dourado?= Date: Fri, 3 Apr 2026 19:30:22 +0100 Subject: [PATCH 3/6] fix #7091: added/removed rules to pass failing tests --- packages/mermaid/src/diagrams/state/parser/stateDiagram.jison | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison index b9adfceb6e8..21d1bfb2058 100644 --- a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison +++ b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison @@ -130,6 +130,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili [^\n\{]* { if (!processId()) return; this.popState(); /* console.log('STATE_ID', yytext); */ return "ID"; } ["] { this.popState(); } [^"]* { /* console.log('Long description:', yytext); */ return "STATE_DESCR"; } +\s+"state"\s+ { this.popState(); this.pushState('STATE'); yytext='state '; return 'NL'; } [^\n\s\{]+ { /* console.log('COMPOSIT_STATE', yytext); */ return 'COMPOSIT_STATE'; } \n { this.popState(); return 'NL'; } \{ { this.popState(); this.pushState('struct'); /* console.log('begin struct', yytext); */ return 'STRUCT_START'; } @@ -167,8 +168,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili /lex %left '^' -%left NL -%right COMPOSIT_STATE /* fix the shift/reduce conflicts from new rule */ %start start @@ -225,7 +224,6 @@ statement } | HIDE_EMPTY | scale WIDTH - | COMPOSIT_STATE | COMPOSIT_STATE NL { $$={ stmt: 'state', id: $1, type: 'default', description: '', doc: [] } From ecc8c6e83bc5b350405457f4a4fa04c41d2eb28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Dourado?= Date: Thu, 23 Apr 2026 17:29:45 +0100 Subject: [PATCH 4/6] fix #7091: Moved fix from grammar to lexer Added test case for 3+ word case --- .../diagrams/state/parser/state-parser.spec.js | 3 ++- .../src/diagrams/state/parser/stateDiagram.jison | 16 ++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js index d1fbbbaba7b..da2a050c2d8 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js @@ -17,7 +17,8 @@ describe('state parser can parse...', () => { describe('invalid name between state and curly bracket', () => { it('should throw error when name has multiple words', () => { const diagramText = `stateDiagram-v2 - state invalid syntax { X }`; + state invalid syntax { X } + state invalid syntax with more than 2 words { Y }`; expect(() => { stateDiagram.parser.parse(diagramText); diff --git a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison index 21d1bfb2058..604111c2350 100644 --- a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison +++ b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison @@ -130,9 +130,9 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili [^\n\{]* { if (!processId()) return; this.popState(); /* console.log('STATE_ID', yytext); */ return "ID"; } ["] { this.popState(); } [^"]* { /* console.log('Long description:', yytext); */ return "STATE_DESCR"; } -\s+"state"\s+ { this.popState(); this.pushState('STATE'); yytext='state '; return 'NL'; } +\w+\s+\w+.*?\{ { throw new Error('Error: State name must be a single word. Found: "' + yytext.trim() + '"'); } [^\n\s\{]+ { /* console.log('COMPOSIT_STATE', yytext); */ return 'COMPOSIT_STATE'; } -\n { this.popState(); return 'NL'; } +\n { this.popState(); } \{ { this.popState(); this.pushState('struct'); /* console.log('begin struct', yytext); */ return 'STRUCT_START'; } \} { /*console.log('Ending struct');*/ this.popState(); return 'STRUCT_STOP';} } [\n] /* nothing */ @@ -224,20 +224,12 @@ statement } | HIDE_EMPTY | scale WIDTH - | COMPOSIT_STATE NL - { - $$={ stmt: 'state', id: $1, type: 'default', description: '', doc: [] } - } + | COMPOSIT_STATE | COMPOSIT_STATE STRUCT_START document STRUCT_STOP { // console.log('Adding document for state without id ', $1); $$={ stmt: 'state', id: $1, type: 'default', description: '', doc: $3 } } - | COMPOSIT_STATE COMPOSIT_STATE STRUCT_START document STRUCT_STOP - { - /* Rejects invalid syntax: multiple words between 'state' and '{' */ - throw new Error('Error: State name must be a single word.'); - } | STATE_DESCR AS ID { var id=$3; var description = $1.trim(); @@ -358,4 +350,4 @@ notePosition | right_of ; -%% +%% \ No newline at end of file From c2305df424963c0263d1c75804248db2969ee17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Dourado?= Date: Thu, 23 Apr 2026 17:42:43 +0100 Subject: [PATCH 5/6] Chore: Add changeset for parsing bug fix --- .changeset/tired-rockets-rule.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tired-rockets-rule.md diff --git a/.changeset/tired-rockets-rule.md b/.changeset/tired-rockets-rule.md new file mode 100644 index 00000000000..53c7d41308f --- /dev/null +++ b/.changeset/tired-rockets-rule.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +Fix invalid syntax between state and '}' From ee4aafdeb19a6258d45051305d93b26dd102f4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Dourado?= Date: Thu, 30 Apr 2026 15:52:03 +0100 Subject: [PATCH 6/6] fix #7091: Separated tests Added a still-works case Fixed a typo in changeset --- .changeset/tired-rockets-rule.md | 2 +- .../state/parser/state-parser.spec.js | 34 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.changeset/tired-rockets-rule.md b/.changeset/tired-rockets-rule.md index 53c7d41308f..5d850562222 100644 --- a/.changeset/tired-rockets-rule.md +++ b/.changeset/tired-rockets-rule.md @@ -2,4 +2,4 @@ 'mermaid': patch --- -Fix invalid syntax between state and '}' +Fix invalid syntax between state and '{' diff --git a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js index da2a050c2d8..a9964b84d52 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js @@ -15,14 +15,36 @@ describe('state parser can parse...', () => { }); describe('invalid name between state and curly bracket', () => { - it('should throw error when name has multiple words', () => { - const diagramText = `stateDiagram-v2 - state invalid syntax { X } - state invalid syntax with more than 2 words { Y }`; + describe('valid syntax', () => { + it('should only accept 1 word', () => { + const diagramText = `stateDiagram-v2 + state valid { X }`; - expect(() => { stateDiagram.parser.parse(diagramText); - }).toThrow('Error: State name must be a single word.'); + + const states = stateDiagram.parser.yy.getStates(); + expect(states.get('valid')).not.toBeUndefined(); + }); + }); + + describe('invalid syntax', () => { + it('should throw error with 2 words', () => { + const diagramText = `stateDiagram-v2 + state invalid syntax { Y }`; + + expect(() => { + stateDiagram.parser.parse(diagramText); + }).toThrow('Error: State name must be a single word.'); + }); + + it('should also throw with more than 2 words', () => { + const diagramText = `stateDiagram-v2 + state invalid syntax with more than 2 words { Z }`; + + expect(() => { + stateDiagram.parser.parse(diagramText); + }).toThrow('Error: State name must be a single word.'); + }); }); });