parser: idStatement returns stmt: 'state'...; add classDef, class statements

This commit is contained in:
Ashley Engelund (weedySeaDragon @ github) 2022-10-11 08:36:04 -07:00
parent 15dd60ab85
commit 370806365f
2 changed files with 277 additions and 22 deletions

View File

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

View File

@ -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
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
<INITIAL,struct>"classDef"\s+ { this.pushState('CLASSDEF'); return 'classDef'; }
<CLASSDEF>DEFAULT\s+ { this.popState(); this.pushState('CLASSDEFID'); return 'DEFAULT_CLASSDEF_ID' }
<CLASSDEF>\w+\s+ { this.popState(); this.pushState('CLASSDEFID'); return 'CLASSDEF_ID' }
<CLASSDEFID>[^\n]* { this.popState(); return 'CLASSDEF_STYLEOPTS' }
<INITIAL,struct>"class"\s+ { this.pushState('CLASS'); return 'class'; }
<CLASS>(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' }
<CLASS_STYLE>[^\n]* { this.popState(); return 'STYLECLASS' }
"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
<SCALE>\d+ return 'WIDTH';
<SCALE>\s+"width" {this.popState();}
<INITIAL,struct>"state"\s+ { /*console.log('Starting STATE zxzx'+yy.getDirection());*/this.pushState('STATE'); }
<STATE>.*"<<fork>>" {this.popState();yytext=yytext.slice(0,-8).trim(); /*console.warn('Fork Fork: ',yytext);*/return 'FORK';}
<STATE>.*"<<join>>" {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
<INITIAL,struct>[^:\n\s\-\{]+ { /*console.log('=>ID=',yytext);*/ return 'ID';}
// <INITIAL,struct>\s*":"[^\+\->:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; }
<INITIAL,struct>\s*":"[^:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; }
<INITIAL,struct>"-->" return '-->';
<struct>"--" return 'CONCURRENT';
<<EOF>> return 'NL';
. return 'INVALID';
<struct>"--" return 'CONCURRENT';
":::" return 'STYLE_SEPARATOR';
<<EOF>> 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