diff --git a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js new file mode 100644 index 000000000..0d95cd117 --- /dev/null +++ b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js @@ -0,0 +1,189 @@ +import stateDb from '../stateDb'; +import stateDiagram from './stateDiagram'; +import { setConfig } from '../../../config'; + +setConfig({ + securityLevel: 'strict', +}); + +describe('ClassDefs and classes when parsing a State diagram', () => { + beforeEach(function () { + stateDiagram.parser.yy = stateDb; + stateDiagram.parser.yy.clear(); + }); + + describe('class for a state (classDef)', () => { + describe('defining (classDef)', () => { + it('has "classDef" as a keyword, an id, and can set a css style attribute', function () { + stateDiagram.parser.parse('stateDiagram-v2\n classDef exampleClass background:#bbb;'); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + + const styleClasses = stateDb.getClasses(); + expect(styleClasses['exampleClass'].styles.length).toEqual(1); + expect(styleClasses['exampleClass'].styles[0]).toEqual('background:#bbb'); + }); + + it('can define multiple attributes separated by commas', function () { + stateDiagram.parser.parse( + 'stateDiagram-v2\n classDef exampleClass background:#bbb, font-weight:bold, font-style:italic;' + ); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + + const styleClasses = stateDb.getClasses(); + expect(styleClasses['exampleClass'].styles.length).toEqual(3); + expect(styleClasses['exampleClass'].styles[0]).toEqual('background:#bbb'); + expect(styleClasses['exampleClass'].styles[1]).toEqual('font-weight:bold'); + expect(styleClasses['exampleClass'].styles[2]).toEqual('font-style:italic'); + }); + + // need to look at what the lexer is doing. see the work on the chevotrain parser + it('an attribute can have a dot in the style', function () { + stateDiagram.parser.parse( + 'stateDiagram-v2\n classDef exampleStyleClass background:#bbb,border:1.5px solid red;' + ); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + + const classes = stateDiagram.parser.yy.getClasses(); + expect(classes['exampleStyleClass'].styles.length).toBe(2); + expect(classes['exampleStyleClass'].styles[0]).toBe('background:#bbb'); + expect(classes['exampleStyleClass'].styles[1]).toBe('border:1.5px solid red'); + }); + + it('an attribute can have a space in the style', function () { + stateDiagram.parser.parse( + 'stateDiagram-v2\n classDef exampleStyleClass background: #bbb,border:1.5px solid red;' + ); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + + const classes = stateDiagram.parser.yy.getClasses(); + expect(classes['exampleStyleClass'].styles.length).toBe(2); + expect(classes['exampleStyleClass'].styles[0]).toBe('background: #bbb'); + expect(classes['exampleStyleClass'].styles[1]).toBe('border:1.5px solid red'); + }); + }); + + describe('applying to states in the diagram', () => { + it('can apply a class to a state', function () { + let diagram = ''; + diagram += 'stateDiagram-v2\n' + '\n'; + diagram += 'classDef exampleStyleClass background:#bbb,border:1px solid red;\n'; + diagram += 'a --> b '; + diagram += 'class a exampleStyleClass'; + + stateDiagram.parser.parse(diagram); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + + const classes = stateDb.getClasses(); + expect(classes['exampleStyleClass'].styles.length).toEqual(2); + expect(classes['exampleStyleClass'].styles[0]).toEqual('background:#bbb'); + expect(classes['exampleStyleClass'].styles[1]).toEqual('border:1px solid red'); + + const state_a = stateDb.getState('a'); + expect(state_a.classes.length).toEqual(1); + expect(state_a.classes[0]).toEqual('exampleStyleClass'); + }); + + it('can be applied to a state with an id containing _', function () { + let diagram = ''; + + diagram += 'stateDiagram-v2\n' + '\n'; + diagram += 'classDef exampleStyleClass background:#bbb,border:1px solid red;\n'; + diagram += 'a_a --> b_b' + '\n'; + diagram += 'class a_a exampleStyleClass'; + + stateDiagram.parser.parse(diagram); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + + const classes = stateDiagram.parser.yy.getClasses(); + expect(classes['exampleStyleClass'].styles.length).toBe(2); + expect(classes['exampleStyleClass'].styles[0]).toBe('background:#bbb'); + expect(classes['exampleStyleClass'].styles[1]).toBe('border:1px solid red'); + + const state_a_a = stateDiagram.parser.yy.getState('a_a'); + expect(state_a_a.classes.length).toEqual(1); + expect(state_a_a.classes[0]).toEqual('exampleStyleClass'); + }); + + describe('::: syntax', () => { + it('can be applied to a state using ::: syntax', () => { + let diagram = ''; + diagram += 'stateDiagram-v2\n' + '\n'; + diagram += 'classDef exampleStyleClass background:#bbb,border:1px solid red;' + '\n'; + diagram += 'a --> b:::exampleStyleClass' + '\n'; + + stateDiagram.parser.parse(diagram); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + + const states = stateDiagram.parser.yy.getStates(); + const classes = stateDiagram.parser.yy.getClasses(); + + expect(classes['exampleStyleClass'].styles.length).toEqual(2); + expect(classes['exampleStyleClass'].styles[0]).toEqual('background:#bbb'); + expect(classes['exampleStyleClass'].styles[1]).toEqual('border:1px solid red'); + + expect(states['b'].classes[0]).toEqual('exampleStyleClass'); + }); + + it('can be applied to a [*] state', () => { + let diagram = ''; + diagram += 'stateDiagram-v2\n\n'; + diagram += 'classDef exampleStyleClass background:#bbb,border:1px solid red;\n'; + diagram += '[*]:::exampleStyleClass --> b\n'; + + stateDiagram.parser.parse(diagram); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + + const states = stateDiagram.parser.yy.getStates(); + const classes = stateDiagram.parser.yy.getClasses(); + + expect(classes['exampleStyleClass'].styles.length).toEqual(2); + expect(classes['exampleStyleClass'].styles[0]).toEqual('background:#bbb'); + expect(classes['exampleStyleClass'].styles[1]).toEqual('border:1px solid red'); + + expect(states['root_start'].classes[0]).toEqual('exampleStyleClass'); + }); + + it('can be applied to a comma separated list of states', function () { + let diagram = ''; + diagram += 'stateDiagram-v2\n\n'; + diagram += 'classDef exampleStyleClass background:#bbb,border:1px solid red;\n'; + diagram += 'a-->b\n'; + diagram += 'class a,b exampleStyleClass'; + + stateDiagram.parser.parse(diagram); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + let classes = stateDiagram.parser.yy.getClasses(); + let states = stateDiagram.parser.yy.getStates(); + + expect(classes['exampleStyleClass'].styles.length).toEqual(2); + expect(classes['exampleStyleClass'].styles[0]).toEqual('background:#bbb'); + expect(classes['exampleStyleClass'].styles[1]).toEqual('border:1px solid red'); + expect(states['a'].classes[0]).toEqual('exampleStyleClass'); + expect(states['b'].classes[0]).toEqual('exampleStyleClass'); + }); + + it('a comma separated list of states may or may not have spaces after commas', function () { + let diagram = ''; + diagram += 'stateDiagram-v2\n\n'; + diagram += 'classDef exampleStyleClass background:#bbb,border:1px solid red;\n'; + diagram += 'a-->b\n'; + diagram += 'class a,b,c, d, e exampleStyleClass'; + + stateDiagram.parser.parse(diagram); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + const classes = stateDiagram.parser.yy.getClasses(); + const states = stateDiagram.parser.yy.getStates(); + + expect(classes['exampleStyleClass'].styles.length).toEqual(2); + expect(classes['exampleStyleClass'].styles[0]).toEqual('background:#bbb'); + expect(classes['exampleStyleClass'].styles[1]).toEqual('border:1px solid red'); + + const statesList = ['a', 'b', 'c', 'd', 'e']; + statesList.forEach((stateId) => { + expect(states[stateId].classes[0]).toEqual('exampleStyleClass'); + }); + }); + }); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison index e2a984ca1..1115edfe9 100644 --- a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison +++ b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison @@ -23,6 +23,10 @@ %x acc_title %x acc_descr %x acc_descr_multiline +%x CLASSDEF +%x CLASSDEFID +%x CLASS +%x CLASS_STYLE %x NOTE %x NOTE_ID %x NOTE_TEXT @@ -39,6 +43,8 @@ %% +"default" return 'DEFAULT'; + .*direction\s+TB[^\n]* return 'direction_tb'; .*direction\s+BT[^\n]* return 'direction_bt'; .*direction\s+RL[^\n]* return 'direction_rl'; @@ -69,6 +75,20 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili [\}] { this.popState(); } [^\}]* return "acc_descr_multiline_value"; +"classDef"\s+ { this.pushState('CLASSDEF'); return 'classDef'; } +DEFAULT\s+ { this.popState(); this.pushState('CLASSDEFID'); return 'DEFAULT_CLASSDEF_ID' } +\w+\s+ { this.popState(); this.pushState('CLASSDEFID'); return 'CLASSDEF_ID' } +[^\n]* { this.popState(); return 'CLASSDEF_STYLEOPTS' } + +"class"\s+ { this.pushState('CLASS'); return 'class'; } +(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' } +[^\n]* { this.popState(); return 'STYLECLASS' } + +"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; } +\d+ return 'WIDTH'; +\s+"width" {this.popState();} + + "state"\s+ { /*console.log('Starting STATE zxzx'+yy.getDirection());*/this.pushState('STATE'); } .*"<>" {this.popState();yytext=yytext.slice(0,-8).trim(); /*console.warn('Fork Fork: ',yytext);*/return 'FORK';} .*"<>" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';} @@ -111,10 +131,12 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili [^:\n\s\-\{]+ { /*console.log('=>ID=',yytext);*/ return 'ID';} // \s*":"[^\+\->:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; } \s*":"[^:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; } + "-->" return '-->'; -"--" return 'CONCURRENT'; -<> return 'NL'; -. return 'INVALID'; +"--" return 'CONCURRENT'; +":::" return 'STYLE_SEPARATOR'; +<> return 'NL'; +. return 'INVALID'; /lex @@ -124,20 +146,23 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili %% /* language grammar */ +/* $$ is the value of the symbol being evaluated (= what is to the left of the : in the rule */ + start : SPACE start | NL start | directive start - | SD document { /*console.warn('Root document', $2);*/ yy.setRootDoc($2);return $2; } + | SD document { /* console.log('--> Root document', $2); */ yy.setRootDoc($2); return $2; } ; document - : /* empty */ { $$ = [] } + : /* empty */ { /*console.log('empty document'); */ $$ = [] } | document line { - if($2!='nl'){ - $1.push($2);$$ = $1 + if($2 !='nl'){ + /* console.log(' document: 1: ', $1, ' pushing 2: ', $2); */ + $1.push($2); $$ = $1 } - // console.warn('Got document',$1, $2); + /* console.log('Got document',$1, $2); */ } ; @@ -148,24 +173,34 @@ line ; statement - : idStatement { /*console.warn('got id and descr', $1);*/$$={ stmt: 'state', id: $1, type: 'default', description: ''};} - | idStatement DESCR { /*console.warn('got id and descr', $1, $2.trim());*/$$={ stmt: 'state', id: $1, type: 'default', description: yy.trimColon($2)};} + : classDefStatement + | cssClassStatement + | idStatement { /* console.log('got id', $1); */ + $$=$1; + } + | idStatement DESCR { + const stateStmt = $1; + stateStmt.description = yy.trimColon($2); + $$ = stateStmt; + } | idStatement '-->' idStatement - { - /*console.warn('got id', $1);yy.addRelation($1, $3);*/ - $$={ stmt: 'relation', state1: { stmt: 'state', id: $1, type: 'default', description: '' }, state2:{ stmt: 'state', id: $3 ,type: 'default', description: ''}}; - } + { + /* console.info('got ids: 1: ', $1, ' 2:', $2,' 3: ', $3); */ + // console.log(' idStatement --> idStatement : state1 =', $1, ' state2 =', $3); + $$={ stmt: 'relation', state1: $1, state2: $3}; + } | idStatement '-->' idStatement DESCR - { - /*yy.addRelation($1, $3, $4.substr(1).trim());*/ - $$={ stmt: 'relation', state1: { stmt: 'state', id: $1, type: 'default', description: '' }, state2:{ stmt: 'state', id: $3 ,type: 'default', description: ''}, description: $4.substr(1).trim()}; - } + { + const relDescription = yy.trimColon($4); + /* console.log(' idStatement --> idStatement DESCR : state1 =', $1, ' state2stmt =', $3, ' description: ', relDescription); */ + $$={ stmt: 'relation', state1: $1, state2: $3, description: relDescription}; + } | HIDE_EMPTY | scale WIDTH | COMPOSIT_STATE | COMPOSIT_STATE STRUCT_START document STRUCT_STOP { - /* console.warn('Adding document for state without id ', $1);*/ + /* console.log('Adding document for state without id ', $1); */ $$={ stmt: 'state', id: $1, type: 'default', description: '', doc: $3 } } | STATE_DESCR AS ID { @@ -181,7 +216,7 @@ statement } | STATE_DESCR AS ID STRUCT_START document STRUCT_STOP { - // console.warn('Adding document for state with id zxzx', $3, $4, yy.getDirection()); yy.addDocument($3); + /* console.log('Adding document for state with id zxzx', $3, $4, yy.getDirection()); yy.addDocument($3);*/ $$={ stmt: 'state', id: $3, type: 'default', description: $1, doc: $5 } } | FORK { @@ -208,6 +243,23 @@ statement | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } ; + +classDefStatement + : classDef CLASSDEF_ID CLASSDEF_STYLEOPTS { + $$ = { stmt: 'classDef', id: $2.trim(), classes: $3.trim() }; + } + | classDef DEFAULT CLASSDEF_STYLEOPTS { + $$ = { stmt: 'classDef', id: $2.trim(), classes: $3.trim() }; + } + ; + +cssClassStatement + : class CLASSENTITY_IDS STYLECLASS { + //console.log('apply class: id(s): ',$2, ' style class: ', $3); + $$={ stmt: 'applyClass', id: $2.trim(), styleClass: $3.trim() }; + } + ; + directive : openDirective typeDirective closeDirective | openDirective typeDirective ':' argDirective closeDirective @@ -229,8 +281,22 @@ eol ; idStatement - : ID {$$=$1;} - | EDGE_STATE {$$=$1;} + : ID + { /* console.log('idStatement id: ', $1); */ + $$={ stmt: 'state', id: $1.trim(), type: 'default', description: '' }; + } + | EDGE_STATE + { /* console.log('idStatement id: ', $1); */ + $$={ stmt: 'state', id: $1.trim(), type: 'default', description: '' }; + } + | ID STYLE_SEPARATOR ID + { /*console.log('idStatement ID STYLE_SEPARATOR ID'); */ + $$={ stmt: 'state', id: $1.trim(), classes: [$3.trim()], type: 'default', description: '' }; + } + | EDGE_STATE STYLE_SEPARATOR ID + { /*console.log('idStatement EDGE_STATE STYLE_SEPARATOR ID'); */ + $$={ stmt: 'state', id: $1.trim(), classes: [$3.trim()], type: 'default', description: '' }; + } ; notePosition