fix: Merge conflicts

This commit is contained in:
Emerson Bottero 2022-10-26 16:51:22 -03:00
commit 455839c0b9
11 changed files with 1213 additions and 267 deletions

View File

@ -69,7 +69,8 @@
"flatmap",
"Kaufmann",
"viewports",
"edgechromium"
"edgechromium",
"statediagram"
],
"patterns": [
{

View File

@ -521,4 +521,42 @@ stateDiagram-v2
{ logLevel: 0, fontFamily: 'courier' }
);
});
describe('classDefs and applying classes', () => {
it('v2 states can have a class applied', () => {
imgSnapshotTest(
`
stateDiagram-v2
[*] --> A
A --> B: test({ foo#colon; 'far' })
B --> [*]
classDef badBadEvent fill:#f00,color:white,font-weight:bold
class B badBadEvent
`,
{ logLevel: 0, fontFamily: 'courier' }
);
});
it('v2 can have multiple classes applied to multiple states', () => {
imgSnapshotTest(
`
stateDiagram-v2
classDef notMoving fill:white
classDef movement font-style:italic;
classDef badBadEvent fill:#f00,color:white,font-weight:bold
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
class Still notMoving
class Moving, Crash movement
class Crash badBadEvent
`,
{ logLevel: 0, fontFamily: 'courier' }
);
});
});
});

View File

@ -15,6 +15,7 @@
<body>
<h1>State diagram demos</h1>
<h2>Very simple showing change from State1 to State2</h2>
<pre class="mermaid">
stateDiagram
accTitle: This is the accessible title
@ -24,59 +25,126 @@
<hr />
<h2>This has classDef statements to apply style classes to specific states</h2>
<h4>Here are the <code>classDef</code> statements:</h4>
<p>
<code>
classDef notMoving fill:white<br />
classDef movement font-style:italic;<br />
classDef badBadEvent fill:#f00,color:white,font-weight:bold<br />
</code>
</p>
<h4>And these are how they are applied:</h4>
<p>
<code>
class Still notMoving<br />
class Moving, Crash movement<br />
class Crash badBadEvent<br />
</code>
</p>
<pre class="mermaid">
stateDiagram-v2
accTitle: This is the accessible title
accDescr: This is an accessible description
classDef notMoving fill:white
classDef movement font-style:italic;
classDef badBadEvent fill:#f00,color:white,font-weight:bold
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
class Still notMoving
class Moving, Crash movement
class Crash badBadEvent
</pre>
<hr />
<pre class="mermaid">
stateDiagram
stateDiagram-v2
accTitle: very very simple state
accDescr: This is a state diagram showing one state
State1
</pre>
<hr />
<h2>You can label the relationships</h2>
<pre class="mermaid">
stateDiagram
stateDiagram-v2
[*] --> State1
State1 --> State2 : Transition 1
State1 --> State3 : Transition 2
State1 --> State4 : Transition 3
State1 --> [*]
</pre>
<hr />
<h2>This shows Composite states</h2>
<pre class="mermaid">
stateDiagram-v2
[*] --> First
First --> Second
First --> Third
state First {
[*] --> second
second --> [*]
[*] --> 1st
1st --> [*]
}
state Second {
[*] --> 2nd
2nd --> [*]
}
state Third {
[*] --> 3rd
3rd --> [*]
}
</pre>
<hr />
<h2>Compsite states can link to themselves</h2>
<pre class="mermaid">
stateDiagram
State1: The state with a note
note right of State1
Important information! You can write
notes.
end note
State1 --> State2
note left of State2 : This is the note to the left.
stateDiagram-v2
state Active {
Idle
}
Inactive --> Idle: ACT
Active --> Active: LOG
</pre>
<hr />
<h2>transition labels can span multiple lines using "br" tags or \n</h2>
<pre class="mermaid">
stateDiagram
State1
note right of State1
Line1<br>Line2<br />Line3<br />Line4<br />Line5
end note
stateDiagram-v2
[*] --> S1
S1 --> S2: This long line uses a br tag<br/>to create multiple<br/>lines.
S1 --> S3: This transition descripton uses \na newline character\nto create multiple\nlines.
</pre>
<hr />
<h2>You can add Notes</h2>
<pre class="mermaid">
stateDiagram-v2
direction LR
State1: A state with a note
note right of State1
Important information!<br />You can write notes.<br/>And\nthey\ncan\nbe\nmulti-\nline.
end note
State1 --> State2
note left of State2 : Notes can be to the left of a state\n(like this one).
note right of State2 : Notes can be to the right of a state\n(like this one).
</pre>
<hr />
<script src="./mermaid.js"></script>
<script>
mermaid.initialize({
theme: 'base',
theme: 'default',
// themeCSS: '.node rect { fill: red; }',
logLevel: 3,
securityLevel: 'loose',

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
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

View File

@ -0,0 +1,32 @@
/**
* Constants common to all State Diagram code
*/
// default diagram direction
export const DEFAULT_DIAGRAM_DIRECTION = 'LR';
// default direction for any nested documents (composites)
export const DEFAULT_NESTED_DOC_DIR = 'TB';
// parsed statement type for a state
export const STMT_STATE = 'state';
// parsed statement type for a relation
export const STMT_RELATION = 'relation';
// parsed statement type for a classDef
export const STMT_CLASSDEF = 'classDef';
// parsed statement type for applyClass
export const STMT_APPLYCLASS = 'applyClass';
export const DEFAULT_STATE_TYPE = 'default';
export const DIVIDER_TYPE = 'divider';
export default {
DEFAULT_DIAGRAM_DIRECTION,
DEFAULT_NESTED_DOC_DIR,
STMT_STATE,
STMT_RELATION,
STMT_CLASSDEF,
STMT_APPLYCLASS,
DEFAULT_STATE_TYPE,
DIVIDER_TYPE,
};

View File

@ -11,8 +11,58 @@ import {
clear as commonClear,
} from '../../commonDb';
const clone = (o) => JSON.parse(JSON.stringify(o));
import {
DEFAULT_DIAGRAM_DIRECTION,
STMT_STATE,
STMT_RELATION,
STMT_CLASSDEF,
STMT_APPLYCLASS,
DEFAULT_STATE_TYPE,
DIVIDER_TYPE,
} from './stateCommon';
const START_NODE = '[*]';
const START_TYPE = 'start';
const END_NODE = START_NODE;
const END_TYPE = 'end';
const COLOR_KEYWORD = 'color';
const FILL_KEYWORD = 'fill';
const BG_FILL = 'bgFill';
const STYLECLASS_SEP = ',';
let direction = DEFAULT_DIAGRAM_DIRECTION;
let rootDoc = [];
let classes = []; // style classes defined by a classDef
const newDoc = () => {
return {
relations: [],
states: {},
documents: {},
};
};
let documents = {
root: newDoc(),
};
let currentDocument = documents.root;
let startEndCount = 0;
let dividerCnt = 0;
export const lineType = {
LINE: 0,
DOTTED_LINE: 1,
};
export const relationType = {
AGGREGATION: 0,
EXTENSION: 1,
COMPOSITION: 2,
DEPENDENCY: 3,
};
const clone = (o) => JSON.parse(JSON.stringify(o));
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
@ -27,11 +77,11 @@ const setRootDoc = (o) => {
const getRootDoc = () => rootDoc;
const docTranslator = (parent, node, first) => {
if (node.stmt === 'relation') {
if (node.stmt === STMT_RELATION) {
docTranslator(parent, node.state1, true);
docTranslator(parent, node.state2, false);
} else {
if (node.stmt === 'state') {
if (node.stmt === STMT_STATE) {
if (node.id === '[*]') {
node.id = first ? parent.id + '_start' : parent.id + '_end';
node.start = first;
@ -41,10 +91,10 @@ const docTranslator = (parent, node, first) => {
if (node.doc) {
const doc = [];
// Check for concurrency
let i = 0;
let currentDoc = [];
let i;
for (i = 0; i < node.doc.length; i++) {
if (node.doc[i].type === 'divider') {
if (node.doc[i].type === DIVIDER_TYPE) {
// debugger;
const newNode = clone(node.doc[i]);
newNode.doc = clone(currentDoc);
@ -58,7 +108,7 @@ const docTranslator = (parent, node, first) => {
// If any divider was encountered
if (doc.length > 0 && currentDoc.length > 0) {
const newNode = {
stmt: 'state',
stmt: STMT_STATE,
id: generateId(),
type: 'divider',
doc: clone(currentDoc),
@ -77,6 +127,17 @@ const getRootDocV2 = () => {
// Here
};
/**
* Convert all of the statements (stmts) that were parsed into states and relationships.
* This is done because a state diagram may have nested sections,
* where each section is a 'document' and has its own set of statements.
* Ex: the section within a fork has its own statements, and incoming and outgoing statements
* refer to the fork as a whole (document).
* See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement.
* This will push the statement into the the list of statements for the current document.
*
* @param _doc
*/
const extract = (_doc) => {
// const res = { states: [], relations: [] };
let doc;
@ -95,48 +156,66 @@ const extract = (_doc) => {
log.info('Extract', doc);
doc.forEach((item) => {
if (item.stmt === 'state') {
addState(item.id, item.type, item.doc, item.description, item.note);
}
if (item.stmt === 'relation') {
addRelation(item.state1.id, item.state2.id, item.description);
switch (item.stmt) {
case STMT_STATE:
addState(
item.id,
item.type,
item.doc,
item.description,
item.note,
item.classes,
item.styles,
item.textStyles
);
break;
case STMT_RELATION:
addRelation(item.state1, item.state2, item.description);
break;
case STMT_CLASSDEF:
addStyleClass(item.id, item.classes);
break;
case STMT_APPLYCLASS:
setCssClass(item.id, item.styleClass);
break;
}
});
};
const newDoc = () => {
return {
relations: [],
states: {},
documents: {},
};
};
let documents = {
root: newDoc(),
};
let currentDocument = documents.root;
let startCnt = 0;
/**
* Function called by parser when a node definition has been found.
*
* @param {any} id
* @param {any} type
* @param {any} doc
* @param {any} descr
* @param {any} note
* @param {null | string} id
* @param {null | string} type
* @param {null | string} doc
* @param {null | string | string[]} descr - description for the state. Can be a string or a list or strings
* @param {null | string} note
* @param {null | string | string[]} classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class.
* @param {null | string | string[]} styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style.
* @param {null | string | string[]} textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style.
*/
export const addState = function (id, type, doc, descr, note) {
export const addState = function (
id,
type = DEFAULT_STATE_TYPE,
doc = null,
descr = null,
note = null,
classes = null,
styles = null,
textStyles = null
) {
// add the state if needed
if (typeof currentDocument.states[id] === 'undefined') {
log.info('Adding state ', id, descr);
currentDocument.states[id] = {
id: id,
descriptions: [],
type,
doc,
note,
classes: [],
styles: [],
textStyles: [],
};
} else {
if (!currentDocument.states[id].doc) {
@ -146,8 +225,9 @@ export const addState = function (id, type, doc, descr, note) {
currentDocument.states[id].type = type;
}
}
if (descr) {
log.info('Adding state ', id, descr);
log.info('Setting state description', id, descr);
if (typeof descr === 'string') {
addDescription(id, descr.trim());
}
@ -164,6 +244,24 @@ export const addState = function (id, type, doc, descr, note) {
configApi.getConfig()
);
}
if (classes) {
log.info('Setting state classes', id, classes);
const classesList = typeof classes === 'string' ? [classes] : classes;
classesList.forEach((klass) => setCssClass(id, klass.trim()));
}
if (styles) {
log.info('Setting state styles', id, styles);
const stylesList = typeof styles === 'string' ? [styles] : styles;
stylesList.forEach((style) => setStyle(id, style.trim()));
}
if (textStyles) {
log.info('Setting state styles', id, styles);
const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles;
textStylesList.forEach((textStyle) => setTextStyle(id, textStyle.trim()));
}
};
export const clear = function (saveCommon) {
@ -174,7 +272,8 @@ export const clear = function (saveCommon) {
currentDocument = documents.root;
startCnt = 0;
// number of start and end nodes; used to construct ids
startEndCount = 0;
classes = [];
if (!saveCommon) {
commonClear();
@ -195,36 +294,134 @@ export const getRelations = function () {
return currentDocument.relations;
};
export const addRelation = function (_id1, _id2, title) {
let id1 = _id1;
let id2 = _id2;
let type1 = 'default';
let type2 = 'default';
if (_id1 === '[*]') {
startCnt++;
id1 = 'start' + startCnt;
type1 = 'start';
/**
* If the id is a start node ( [*] ), then return a new id constructed from
* the start node name and the current start node count.
* else return the given id
*
* @param {string} id
* @returns {{id: string, type: string}} - the id and type that should be used
*/
function startIdIfNeeded(id = '') {
let fixedId = id;
if (id === START_NODE) {
startEndCount++;
fixedId = `${START_TYPE}${startEndCount}`;
}
if (_id2 === '[*]') {
id2 = 'end' + startCnt;
type2 = 'end';
return fixedId;
}
/**
* If the id is a start node ( [*] ), then return the start type ('start')
* else return the given type
*
* @param {string} id
* @param {string} type
* @returns {string} - the type that should be used
*/
function startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) {
return id === START_NODE ? START_TYPE : type;
}
/**
* If the id is an end node ( [*] ), then return a new id constructed from
* the end node name and the current start_end node count.
* else return the given id
*
* @param {string} id
* @returns {{id: string, type: string}} - the id and type that should be used
*/
function endIdIfNeeded(id = '') {
let fixedId = id;
if (id === END_NODE) {
startEndCount++;
fixedId = `${END_TYPE}${startEndCount}`;
}
addState(id1, type1);
addState(id2, type2);
return fixedId;
}
/**
* If the id is an end node ( [*] ), then return the end type
* else return the given type
*
* @param {string} id
* @param {string} type
* @returns {string} - the type that should be used
*/
function endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) {
return id === END_NODE ? END_TYPE : type;
}
/**
*
* @param item1
* @param item2
* @param relationTitle
*/
export function addRelationObjs(item1, item2, relationTitle) {
let id1 = startIdIfNeeded(item1.id);
let type1 = startTypeIfNeeded(item1.id, item1.type);
let id2 = startIdIfNeeded(item2.id);
let type2 = startTypeIfNeeded(item2.id, item2.type);
addState(
id1,
type1,
item1.doc,
item1.description,
item1.note,
item1.classes,
item1.styles,
item1.textStyles
);
addState(
id2,
type2,
item2.doc,
item2.description,
item2.note,
item2.classes,
item2.styles,
item2.textStyles
);
currentDocument.relations.push({
id1,
id2,
title: common.sanitizeText(title, configApi.getConfig()),
relationTitle: common.sanitizeText(relationTitle, configApi.getConfig()),
});
}
/**
* Add a relation between two items. The items may be full objects or just the string id of a state.
*
* @param {string | object} item1
* @param {string | object} item2
* @param {string} title
*/
export const addRelation = function (item1, item2, title) {
if (typeof item1 === 'object') {
addRelationObjs(item1, item2, title);
} else {
const id1 = startIdIfNeeded(item1);
const type1 = startTypeIfNeeded(item1);
const id2 = endIdIfNeeded(item2);
const type2 = endTypeIfNeeded(item2);
addState(id1, type1);
addState(id2, type2);
currentDocument.relations.push({
id1,
id2,
title: common.sanitizeText(title, configApi.getConfig()),
});
}
};
const addDescription = function (id, _descr) {
export const addDescription = function (id, descr) {
const theState = currentDocument.states[id];
let descr = _descr;
if (descr[0] === ':') {
descr = descr.substr(1).trim();
}
theState.descriptions.push(common.sanitizeText(descr, configApi.getConfig()));
const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr;
theState.descriptions.push(common.sanitizeText(_descr, configApi.getConfig()));
};
export const cleanupLabel = function (label) {
@ -235,34 +432,105 @@ export const cleanupLabel = function (label) {
}
};
export const lineType = {
LINE: 0,
DOTTED_LINE: 1,
};
let dividerCnt = 0;
const getDividerId = () => {
dividerCnt++;
return 'divider-id-' + dividerCnt;
};
let classes = [];
/**
* Called when the parser comes across a (style) class definition
* @example classDef my-style fill:#f96;
*
* @param {string} id - the id of this (style) class
* @param {string} styleAttributes - the string with 1 or more style attributes (each separated by a comma)
*/
export const addStyleClass = function (id, styleAttributes = '') {
// create a new style class object with this id
if (typeof classes[id] === 'undefined') {
classes[id] = { id: id, styles: [], textStyles: [] };
}
const foundClass = classes[id];
if (typeof styleAttributes !== 'undefined') {
if (styleAttributes !== null) {
styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => {
// remove any trailing ;
const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim();
const getClasses = () => classes;
// replace some style keywords
if (attrib.match(COLOR_KEYWORD)) {
const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL);
const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD);
foundClass.textStyles.push(newStyle2);
}
foundClass.styles.push(fixedAttrib);
});
}
}
};
/**
* Return all of the style classes
* @returns {{} | any | classes}
*/
export const getClasses = function () {
return classes;
};
/**
* Add a (style) class or css class to a state with the given id.
* If the state isn't already in the list of known states, add it.
* Might be called by parser when a style class or CSS class should be applied to a state
*
* @param {string | string[]} itemIds The id or a list of ids of the item(s) to apply the css class to
* @param {string} cssClassName CSS class name
*/
export const setCssClass = function (itemIds, cssClassName) {
itemIds.split(',').forEach(function (id) {
let foundState = getState(id);
if (typeof foundState === 'undefined') {
const trimmedId = id.trim();
addState(trimmedId);
foundState = getState(trimmedId);
}
foundState.classes.push(cssClassName);
});
};
/**
* Add a style to a state with the given id.
* @example style stateId fill:#f9f,stroke:#333,stroke-width:4px
* where 'style' is the keyword
* stateId is the id of a state
* the rest of the string is the styleText (all of the attributes to be applied to the state)
*
* @param itemId The id of item to apply the style to
* @param styleText - the text of the attributes for the style
*/
export const setStyle = function (itemId, styleText) {
const item = getState(itemId);
if (typeof item !== 'undefined') {
item.textStyles.push(styleText);
}
};
/**
* Add a text style to a state with the given id
*
* @param itemId The id of item to apply the css class to
* @param cssClassName CSS class name
*/
export const setTextStyle = function (itemId, cssClassName) {
const item = getState(itemId);
if (typeof item !== 'undefined') {
item.textStyles.push(cssClassName);
}
};
let direction = 'TB';
const getDirection = () => direction;
const setDirection = (dir) => {
direction = dir;
};
export const relationType = {
AGGREGATION: 0,
EXTENSION: 1,
COMPOSITION: 2,
DEPENDENCY: 3,
};
const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
export default {
@ -291,4 +559,7 @@ export default {
setAccTitle,
getAccDescription,
setAccDescription,
addStyleClass,
setCssClass,
addDescription,
};

View File

@ -0,0 +1,75 @@
import stateDb from './stateDb';
describe('State Diagram stateDb', () => {
beforeEach(() => {
stateDb.clear();
});
describe('addStyleClass', () => {
it('is added to the list of style classes', () => {
const newStyleClassId = 'newStyleClass';
const newStyleClassAttribs = 'font-weight:bold, border:blue;';
stateDb.addStyleClass(newStyleClassId, newStyleClassAttribs);
const styleClasses = stateDb.getClasses();
expect(styleClasses[newStyleClassId].id).toEqual(newStyleClassId);
expect(styleClasses[newStyleClassId].styles.length).toEqual(2);
expect(styleClasses[newStyleClassId].styles[0]).toEqual('font-weight:bold');
expect(styleClasses[newStyleClassId].styles[1]).toEqual('border:blue');
});
});
describe('addDescription to a state', () => {
beforeEach(() => {
stateDb.clear();
stateDb.addState('state1');
});
const testStateId = 'state1';
it('removes only the first leading :', () => {
const restOfTheDescription = 'rest of the description';
const oneLeadingColon = `:${restOfTheDescription}`;
const twoLeadingColons = `::${restOfTheDescription}`;
stateDb.addDescription(testStateId, restOfTheDescription);
let states = stateDb.getStates();
expect(states[testStateId].descriptions[0]).toEqual(restOfTheDescription);
stateDb.addDescription(testStateId, oneLeadingColon);
states = stateDb.getStates();
expect(states[testStateId].descriptions[1]).toEqual(restOfTheDescription);
stateDb.addDescription(testStateId, twoLeadingColons);
states = stateDb.getStates();
expect(states[testStateId].descriptions[2]).toEqual(`:${restOfTheDescription}`);
});
it('adds each description to the array of descriptions', () => {
stateDb.addDescription(testStateId, 'description 0');
stateDb.addDescription(testStateId, 'description 1');
stateDb.addDescription(testStateId, 'description 2');
let states = stateDb.getStates();
expect(states[testStateId].descriptions.length).toEqual(3);
expect(states[testStateId].descriptions[0]).toEqual('description 0');
expect(states[testStateId].descriptions[1]).toEqual('description 1');
expect(states[testStateId].descriptions[2]).toEqual('description 2');
});
it('sanitizes on the description', () => {
stateDb.addDescription(
testStateId,
'desc outside the script <script>the description</script>'
);
let states = stateDb.getStates();
expect(states[testStateId].descriptions[0]).toEqual('desc outside the script ');
});
it('adds the description to the state with the given id', () => {
stateDb.addDescription(testStateId, 'the description');
let states = stateDb.getStates();
expect(states[testStateId].descriptions[0]).toEqual('the description');
});
});
});

View File

@ -1,10 +1,13 @@
import { parser } from './parser/stateDiagram';
import stateDb from './stateDb';
import stateDiagram from './parser/stateDiagram.jison';
describe('state diagram, ', function () {
describe('state diagram V2, ', function () {
describe('when parsing an info graph it', function () {
beforeEach(function () {
parser.yy = stateDb;
stateDiagram.parser.yy = stateDb;
stateDiagram.parser.yy.clear();
});
it('super simple', function () {
@ -121,6 +124,30 @@ describe('state diagram, ', function () {
parser.parse(str);
});
describe('relationship labels', () => {
it('simple states with : labels', () => {
const diagram = `
stateDiagram-v2
[*] --> State1
State1 --> State2 : Transition 1
State1 --> State3 : Transition 2
State1 --> State4 : Transition 3
State1 --> [*]
`;
stateDiagram.parser.parse(diagram);
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
const rels = stateDb.getRelations();
const rel_1_2 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State2');
expect(rel_1_2.relationTitle).toEqual('Transition 1');
const rel_1_3 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State3');
expect(rel_1_3.relationTitle).toEqual('Transition 2');
const rel_1_4 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State4');
expect(rel_1_4.relationTitle).toEqual('Transition 3');
});
});
it('scale', function () {
const str = `stateDiagram-v2\n
scale 350 width
@ -355,7 +382,7 @@ describe('state diagram, ', function () {
parser.parse(str);
});
it('should handle notes for composit states', function () {
it('should handle notes for composite (nested) states', function () {
const str = `stateDiagram-v2\n
[*] --> NotShooting
@ -372,5 +399,28 @@ describe('state diagram, ', function () {
parser.parse(str);
});
it('A composite state should be able to link to itself', () => {
const diagram = `
stateDiagram-v2
state Active {
Idle
}
Inactive --> Idle: ACT
Active --> Active: LOG
`;
stateDiagram.parser.parse(diagram);
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
const states = stateDb.getStates();
expect(states['Active'].doc[0].id).toEqual('Idle');
const rels = stateDb.getRelations();
const rel_Inactive_Idle = rels.find((rel) => rel.id1 === 'Inactive' && rel.id2 === 'Idle');
expect(rel_Inactive_Idle.relationTitle).toEqual('ACT');
const rel_Active_Active = rels.find((rel) => rel.id1 === 'Active' && rel.id2 === 'Active');
expect(rel_Active_Active.relationTitle).toEqual('LOG');
});
});
});

View File

@ -6,8 +6,73 @@ import { log } from '../../logger';
import { configureSvgSize } from '../../setupGraphViewbox';
import common from '../common/common';
import addSVGAccessibilityFields from '../../accessibility';
import {
DEFAULT_DIAGRAM_DIRECTION,
DEFAULT_NESTED_DOC_DIR,
STMT_STATE,
STMT_RELATION,
DEFAULT_STATE_TYPE,
DIVIDER_TYPE,
} from './stateCommon';
// --------------------------------------
// Shapes
const SHAPE_STATE = 'rect';
const SHAPE_STATE_WITH_DESC = 'rectWithTitle';
const SHAPE_START = 'start';
const SHAPE_END = 'end';
const SHAPE_DIVIDER = 'divider';
const SHAPE_GROUP = 'roundedWithTitle';
const SHAPE_NOTE = 'note';
const SHAPE_NOTEGROUP = 'noteGroup';
// --------------------------------------
// CSS classes
const CSS_DIAGRAM = 'statediagram';
const CSS_STATE = 'state';
const CSS_DIAGRAM_STATE = `${CSS_DIAGRAM}-${CSS_STATE}`;
const CSS_EDGE = 'transition';
const CSS_NOTE = 'note';
const CSS_NOTE_EDGE = 'note-edge';
const CSS_EDGE_NOTE_EDGE = `${CSS_EDGE} ${CSS_NOTE_EDGE}`;
const CSS_DIAGRAM_NOTE = `${CSS_DIAGRAM}-${CSS_NOTE}`;
const CSS_CLUSTER = 'cluster';
const CSS_DIAGRAM_CLUSTER = `${CSS_DIAGRAM}-${CSS_CLUSTER}`;
const CSS_CLUSTER_ALT = 'cluster-alt';
const CSS_DIAGRAM_CLUSTER_ALT = `${CSS_DIAGRAM}-${CSS_CLUSTER_ALT}`;
// --------------------------------------
// DOM and element IDs
const PARENT = 'parent';
const NOTE = 'note';
const DOMID_STATE = 'state';
const DOMID_TYPE_SPACER = '----';
const NOTE_ID = `${DOMID_TYPE_SPACER}${NOTE}`;
const PARENT_ID = `${DOMID_TYPE_SPACER}${PARENT}`;
// --------------------------------------
// Graph edge settings
const G_EDGE_STYLE = 'fill:none';
const G_EDGE_ARROWHEADSTYLE = 'fill: #333';
const G_EDGE_LABELPOS = 'c';
const G_EDGE_LABELTYPE = 'text';
const G_EDGE_THICKNESS = 'normal';
// --------------------------------------
// When information is parsed and processed (extracted) by stateDb.extract()
// These are globals so the information can be accessed as needed (e.g. in setUpNode, etc.)
let diagramStates = [];
let diagramClasses = [];
// List of nodes created from the parsed diagram statement items
let nodeDb = {};
let graphItemCount = 0; // used to construct ids, etc.
// Configuration
const conf = {};
// -----------------------------------------------------------------------
export const setConf = function (cnf) {
const keys = Object.keys(cnf);
for (let i = 0; i < keys.length; i++) {
@ -15,223 +80,307 @@ export const setConf = function (cnf) {
}
};
let nodeDb = {};
/**
* Returns the all the styles from classDef statements in the graph definition.
*
* @param {any} text
* @param diag
* @param {string} text - the diagram text to be parsed
* @param {Diagram} diagramObj
* @returns {object} ClassDef styles
*/
export const getClasses = function (text, diag) {
export const getClasses = function (text, diagramObj) {
log.trace('Extracting classes');
diag.sb.clear();
if (diagramClasses.length > 0) {
return diagramClasses; // we have already extracted the classes
}
// Parse the graph definition
diag.parser.parse(text);
return diag.sb.getClasses();
diagramObj.db.clear();
try {
// Parse the graph definition
diagramObj.parser.parse(text);
// must run extract() to turn the parsed statements into states, relationships, classes, etc.
diagramObj.db.extract(diagramObj.db.getRootDocV2());
return diagramObj.db.getClasses();
} catch (e) {
return e;
}
};
const setupNode = (g, parent, node, altFlag) => {
// Add the node
if (node.id !== 'root') {
let shape = 'rect';
if (node.start === true) {
shape = 'start';
/**
* Get classes from the db info item.
* If there aren't any or if dbInfoItem isn't defined, return an empty string.
* Else create 1 string from the list of classes found
*
* @param {undefined | null | object} dbInfoItem
* @returns {string}
*/
function getClassesFromDbInfo(dbInfoItem) {
if (typeof dbInfoItem === 'undefined' || dbInfoItem === null) {
return '';
} else {
if (dbInfoItem.classes) {
return dbInfoItem.classes.join(' ');
} else {
return '';
}
if (node.start === false) {
shape = 'end';
}
}
/**
* Create a standard string for the dom ID of an item.
* If a type is given, insert that before the counter, preceded by the type spacer
*
* @param itemId
* @param counter
* @param type
* @param typeSpacer
* @returns {string}
*/
export function stateDomId(itemId = '', counter = 0, type = '', typeSpacer = DOMID_TYPE_SPACER) {
const typeStr = type !== null && type.length > 0 ? `${typeSpacer}${type}` : '';
return `${DOMID_STATE}-${itemId}${typeStr}-${counter}`;
}
/**
* Create a graph node based on the statement information
*
* @param g - graph
* @param {object} parent
* @param {object} parsedItem - parsed statement item
* @param {object} diagramDb
* @param {boolean} altFlag - for clusters, add the "statediagram-cluster-alt" CSS class
*/
const setupNode = (g, parent, parsedItem, diagramDb, altFlag) => {
const itemId = parsedItem.id;
const classStr = getClassesFromDbInfo(diagramStates[itemId]);
if (itemId !== 'root') {
let shape = SHAPE_STATE;
if (parsedItem.start === true) {
shape = SHAPE_START;
}
if (node.type !== 'default') {
shape = node.type;
if (parsedItem.start === false) {
shape = SHAPE_END;
}
if (parsedItem.type !== DEFAULT_STATE_TYPE) {
shape = parsedItem.type;
}
if (!nodeDb[node.id]) {
nodeDb[node.id] = {
id: node.id,
// Add the node to our list (nodeDb)
if (!nodeDb[itemId]) {
nodeDb[itemId] = {
id: itemId,
shape,
description: common.sanitizeText(node.id, getConfig()),
classes: 'statediagram-state',
description: common.sanitizeText(itemId, getConfig()),
classes: `${classStr} ${CSS_DIAGRAM_STATE}`,
};
}
// Build of the array of description strings according
if (node.description) {
if (Array.isArray(nodeDb[node.id].description)) {
// There already is an array of strings,add to it
nodeDb[node.id].shape = 'rectWithTitle';
nodeDb[node.id].description.push(node.description);
} else {
if (nodeDb[node.id].description.length > 0) {
// if there is a description already transform it to an array
nodeDb[node.id].shape = 'rectWithTitle';
if (nodeDb[node.id].description === node.id) {
// If the previous description was the is, remove it
nodeDb[node.id].description = [node.description];
} else {
nodeDb[node.id].description = [nodeDb[node.id].description, node.description];
}
} else {
nodeDb[node.id].shape = 'rect';
nodeDb[node.id].description = node.description;
}
}
nodeDb[node.id].description = common.sanitizeTextOrArray(
nodeDb[node.id].description,
getConfig()
);
}
//
if (nodeDb[node.id].description.length === 1 && nodeDb[node.id].shape === 'rectWithTitle') {
nodeDb[node.id].shape = 'rect';
}
const newNode = nodeDb[itemId];
// Save data for description and group so that for instance a statement without description overwrites
// one with description
// one with description @todo TODO What does this mean? If important, add a test for it
// group
if (!nodeDb[node.id].type && node.doc) {
log.info('Setting cluster for ', node.id, getDir(node));
nodeDb[node.id].type = 'group';
nodeDb[node.id].dir = getDir(node);
nodeDb[node.id].shape = node.type === 'divider' ? 'divider' : 'roundedWithTitle';
nodeDb[node.id].classes =
nodeDb[node.id].classes +
' ' +
(altFlag ? 'statediagram-cluster statediagram-cluster-alt' : 'statediagram-cluster');
// Build of the array of description strings
if (parsedItem.description) {
if (Array.isArray(newNode.description)) {
// There already is an array of strings,add to it
newNode.shape = SHAPE_STATE_WITH_DESC;
newNode.description.push(parsedItem.description);
} else {
if (newNode.description.length > 0) {
// if there is a description already transform it to an array
newNode.shape = SHAPE_STATE_WITH_DESC;
if (newNode.description === itemId) {
// If the previous description was this, remove it
newNode.description = [parsedItem.description];
} else {
newNode.description = [newNode.description, parsedItem.description];
}
} else {
newNode.shape = SHAPE_STATE;
newNode.description = parsedItem.description;
}
}
newNode.description = common.sanitizeTextOrArray(newNode.description, getConfig());
}
// If there's only 1 description entry, just use a regular state shape
if (newNode.description.length === 1 && newNode.shape === SHAPE_STATE_WITH_DESC) {
newNode.shape = SHAPE_STATE;
}
// group
if (!newNode.type && parsedItem.doc) {
log.info('Setting cluster for ', itemId, getDir(parsedItem));
newNode.type = 'group';
newNode.dir = getDir(parsedItem);
newNode.shape = parsedItem.type === DIVIDER_TYPE ? SHAPE_DIVIDER : SHAPE_GROUP;
newNode.classes =
newNode.classes +
' ' +
CSS_DIAGRAM_CLUSTER +
' ' +
(altFlag ? CSS_DIAGRAM_CLUSTER_ALT : '');
}
// This is what will be added to the graph
const nodeData = {
labelStyle: '',
shape: nodeDb[node.id].shape,
labelText: nodeDb[node.id].description,
// typeof nodeDb[node.id].description === 'object'
// ? nodeDb[node.id].description[0]
// : nodeDb[node.id].description,
classes: nodeDb[node.id].classes, //classStr,
shape: newNode.shape,
labelText: newNode.description,
// typeof newNode.description === 'object'
// ? newNode.description[0]
// : newNode.description,
classes: newNode.classes,
style: '', //styles.style,
id: node.id,
dir: nodeDb[node.id].dir,
domId: 'state-' + node.id + '-' + cnt,
type: nodeDb[node.id].type,
id: itemId,
dir: newNode.dir,
domId: stateDomId(itemId, graphItemCount),
type: newNode.type,
padding: 15, //getConfig().flowchart.padding
};
if (node.note) {
if (parsedItem.note) {
// Todo: set random id
const noteData = {
labelStyle: '',
shape: 'note',
labelText: node.note.text,
classes: 'statediagram-note', //classStr,
style: '', //styles.style,
id: node.id + '----note-' + cnt,
domId: 'state-' + node.id + '----note-' + cnt,
type: nodeDb[node.id].type,
shape: SHAPE_NOTE,
labelText: parsedItem.note.text,
classes: CSS_DIAGRAM_NOTE,
style: '', // styles.style,
id: itemId + NOTE_ID + '-' + graphItemCount,
domId: stateDomId(itemId, graphItemCount, NOTE),
type: newNode.type,
padding: 15, //getConfig().flowchart.padding
};
const groupData = {
labelStyle: '',
shape: 'noteGroup',
labelText: node.note.text,
classes: nodeDb[node.id].classes, //classStr,
style: '', //styles.style,
id: node.id + '----parent',
domId: 'state-' + node.id + '----parent-' + cnt,
shape: SHAPE_NOTEGROUP,
labelText: parsedItem.note.text,
classes: newNode.classes,
style: '', // styles.style,
id: itemId + PARENT_ID,
domId: stateDomId(itemId, graphItemCount, PARENT),
type: 'group',
padding: 0, //getConfig().flowchart.padding
};
cnt++;
graphItemCount++;
g.setNode(node.id + '----parent', groupData);
const parentNodeId = itemId + PARENT_ID;
g.setNode(parentNodeId, groupData);
g.setNode(noteData.id, noteData);
g.setNode(node.id, nodeData);
g.setNode(itemId, nodeData);
g.setParent(node.id, node.id + '----parent');
g.setParent(noteData.id, node.id + '----parent');
g.setParent(itemId, parentNodeId);
g.setParent(noteData.id, parentNodeId);
let from = node.id;
let from = itemId;
let to = noteData.id;
if (node.note.position === 'left of') {
if (parsedItem.note.position === 'left of') {
from = noteData.id;
to = node.id;
to = itemId;
}
g.setEdge(from, to, {
arrowhead: 'none',
arrowType: '',
style: 'fill:none',
style: G_EDGE_STYLE,
labelStyle: '',
classes: 'transition note-edge',
arrowheadStyle: 'fill: #333',
labelpos: 'c',
labelType: 'text',
thickness: 'normal',
classes: CSS_EDGE_NOTE_EDGE,
arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
labelpos: G_EDGE_LABELPOS,
labelType: G_EDGE_LABELTYPE,
thickness: G_EDGE_THICKNESS,
});
} else {
g.setNode(node.id, nodeData);
g.setNode(itemId, nodeData);
}
}
if (parent) {
if (parent.id !== 'root') {
log.trace('Setting node ', node.id, ' to be child of its parent ', parent.id);
g.setParent(node.id, parent.id);
log.trace('Setting node ', itemId, ' to be child of its parent ', parent.id);
g.setParent(itemId, parent.id);
}
}
if (node.doc) {
if (parsedItem.doc) {
log.trace('Adding nodes children ');
setupDoc(g, node, node.doc, !altFlag);
setupDoc(g, parsedItem, parsedItem.doc, diagramDb, !altFlag);
}
};
let cnt = 0;
const setupDoc = (g, parent, doc, altFlag) => {
// cnt = 0;
/**
* Turn parsed statements (item.stmt) into nodes, relationships, etc. for a document.
* (A document may be nested within others.)
*
* @param g
* @param parentParsedItem - parsed Item that is the parent of this document (doc)
* @param doc - the document to set up
* @param diagramDb
* @param altFlag
* @todo This duplicates some of what is done in stateDb.js extract method
*/
const setupDoc = (g, parentParsedItem, doc, diagramDb, altFlag) => {
// graphItemCount = 0;
log.trace('items', doc);
doc.forEach((item) => {
if (item.stmt === 'state' || item.stmt === 'default') {
setupNode(g, parent, item, altFlag);
} else if (item.stmt === 'relation') {
setupNode(g, parent, item.state1, altFlag);
setupNode(g, parent, item.state2, altFlag);
const edgeData = {
id: 'edge' + cnt,
arrowhead: 'normal',
arrowTypeEnd: 'arrow_barb',
style: 'fill:none',
labelStyle: '',
label: common.sanitizeText(item.description, getConfig()),
arrowheadStyle: 'fill: #333',
labelpos: 'c',
labelType: 'text',
thickness: 'normal',
classes: 'transition',
};
let startId = item.state1.id;
let endId = item.state2.id;
g.setEdge(startId, endId, edgeData, cnt);
cnt++;
switch (item.stmt) {
case STMT_STATE:
setupNode(g, parentParsedItem, item, diagramDb, altFlag);
break;
case DEFAULT_STATE_TYPE:
setupNode(g, parentParsedItem, item, diagramDb, altFlag);
break;
case STMT_RELATION:
{
setupNode(g, parentParsedItem, item.state1, diagramDb, altFlag);
setupNode(g, parentParsedItem, item.state2, diagramDb, altFlag);
const edgeData = {
id: 'edge' + graphItemCount,
arrowhead: 'normal',
arrowTypeEnd: 'arrow_barb',
style: G_EDGE_STYLE,
labelStyle: '',
label: common.sanitizeText(item.description, getConfig()),
arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
labelpos: G_EDGE_LABELPOS,
labelType: G_EDGE_LABELTYPE,
thickness: G_EDGE_THICKNESS,
classes: CSS_EDGE,
};
g.setEdge(item.state1.id, item.state2.id, edgeData, graphItemCount);
graphItemCount++;
}
break;
}
});
};
const getDir = (nodes, defaultDir) => {
let dir = defaultDir || 'TB';
if (nodes.doc) {
for (let i = 0; i < nodes.doc.length; i++) {
const node = nodes.doc[i];
if (node.stmt === 'dir') {
dir = node.value;
/**
* Get the direction from the statement items.
* Look through all of the documents (docs) in the parsedItems
* Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction.
* @param {object[]} parsedItem - the parsed statement item to look through
* @param [defaultDir=DEFAULT_NESTED_DOC_DIR] - the direction to use if none is found
* @returns {string}
*/
const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
let dir = defaultDir;
if (parsedItem.doc) {
for (let i = 0; i < parsedItem.doc.length; i++) {
const parsedItemDoc = parsedItem.doc[i];
if (parsedItemDoc.stmt === 'dir') {
dir = parsedItemDoc.value;
}
}
}
return dir;
};
/**
* Draws a flowchart in the tag with id: id based on the graph definition in text.
* Draws a state diagram in the tag with id: id based on the graph definition in text.
*
* @param {any} text
* @param {any} id
@ -245,7 +394,7 @@ export const draw = function (text, id, _version, diag) {
// Fetch the default direction, use TD if none was found
let dir = diag.db.getDirection();
if (typeof dir === 'undefined') {
dir = 'LR';
dir = DEFAULT_DIAGRAM_DIRECTION;
}
const { securityLevel, state: conf } = getConfig();
@ -253,9 +402,14 @@ export const draw = function (text, id, _version, diag) {
const rankSpacing = conf.rankSpacing || 50;
log.info(diag.db.getRootDocV2());
// This parses the diagram text and sets the classes, relations, styles, classDefs, etc.
diag.db.extract(diag.db.getRootDocV2());
log.info(diag.db.getRootDocV2());
diagramStates = diag.db.getStates();
diagramClasses = diag.db.getClasses();
// Create the input mermaid.graph
const g = new graphlib.Graph({
multigraph: true,
@ -272,7 +426,7 @@ export const draw = function (text, id, _version, diag) {
return {};
});
setupNode(g, undefined, diag.db.getRootDocV2(), true);
setupNode(g, undefined, diag.db.getRootDocV2(), diag.db, true);
// Set up an SVG group so that we can translate the final graph.
let sandboxElement;
@ -288,7 +442,7 @@ export const draw = function (text, id, _version, diag) {
// Run the renderer. This is what draws the final graph.
const element = root.select('#' + id + ' g');
render(element, g, ['barb'], 'statediagram', id);
render(element, g, ['barb'], CSS_DIAGRAM, id);
const padding = 8;
@ -298,7 +452,7 @@ export const draw = function (text, id, _version, diag) {
const height = bounds.height + padding * 2;
// Zoom in a bit
svg.attr('class', 'statediagram');
svg.attr('class', CSS_DIAGRAM);
const svgBounds = svg.node().getBBox();
@ -318,7 +472,7 @@ export const draw = function (text, id, _version, diag) {
// Get dimensions of label
const dim = label.getBBox();
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
const rect = document.createElementNS('http://www.w3.org/2000/svg', SHAPE_STATE);
rect.setAttribute('rx', 0);
rect.setAttribute('ry', 0);
rect.setAttribute('width', dim.width);

View File

@ -18,7 +18,6 @@ import * as configApi from './config';
import { addDiagrams } from './diagram-api/diagram-orchestration';
import classDb from './diagrams/class/classDb';
import flowDb from './diagrams/flowchart/flowDb';
import flowRenderer from './diagrams/flowchart/flowRenderer';
import ganttDb from './diagrams/gantt/ganttDb';
import Diagram, { getDiagramFromText, type ParseErrorFunction } from './Diagram';
import errorRenderer from './diagrams/error/errorRenderer';
@ -31,6 +30,9 @@ import DOMPurify from 'dompurify';
import { MermaidConfig } from './config.type';
import { evaluate } from './diagrams/common/common';
// diagram names that support classDef statements
const CLASSDEF_DIAGRAMS = ['graph', 'flowchart', 'flowchart-v2', 'stateDiagram'];
/**
* @param text - The mermaid diagram definition.
* @param parseError - If set, handles errors.
@ -251,8 +253,8 @@ const render = async function (
}
// classDef
if (graphType === 'flowchart' || graphType === 'flowchart-v2' || graphType === 'graph') {
const classes: any = flowRenderer.getClasses(text, diag);
if (CLASSDEF_DIAGRAMS.includes(graphType)) {
const classes: any = diag.renderer.getClasses(text, diag);
const htmlLabels = cnf.htmlLabels || cnf.flowchart?.htmlLabels;
for (const className in classes) {
if (htmlLabels) {