From 51bf4a4c5c91c4a396272829d8451834fc9d4333 Mon Sep 17 00:00:00 2001 From: knsv Date: Sat, 21 Sep 2019 08:19:55 -0700 Subject: [PATCH 01/17] #945 Parsing happy case and scale/hide statements --- src/diagrams/state/parser/stateDiagram.jison | 212 ++++++++++++++++++ src/diagrams/state/stateDb.js | 95 ++++++++ src/diagrams/state/stateDiagram.spec.js | 214 +++++++++++++++++++ src/diagrams/state/stateRenderer.js | 101 +++++++++ 4 files changed, 622 insertions(+) create mode 100644 src/diagrams/state/parser/stateDiagram.jison create mode 100644 src/diagrams/state/stateDb.js create mode 100644 src/diagrams/state/stateDiagram.spec.js create mode 100644 src/diagrams/state/stateRenderer.js diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison new file mode 100644 index 000000000..cfdb61158 --- /dev/null +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -0,0 +1,212 @@ +/** mermaid + * https://mermaidjs.github.io/ + * (c) 2014-2015 Knut Sveidqvist + * MIT license. + * + * Based on js sequence diagrams jison grammr + * http://bramp.github.io/js-sequence-diagrams/ + * (c) 2012-2013 Andrew Brampton (bramp.net) + * Simplified BSD license. + */ +%lex + +%options case-insensitive + +// Special states for recognizing aliases +%x ID +%x ALIAS +%x SCALE + +// A special state for grabbing text up to the first comment/newline +%x LINE + +%% + +[\n]+ return 'NL'; +\s+ /* skip all whitespace */ +((?!\n)\s)+ /* skip same-line whitespace */ +\#[^\n]* /* skip comments */ +\%%[^\n]* /* skip comments */ + +"scale"\s+ { this.pushState('SCALE'); console.log('Got scale', yytext);return 'scale'; } +\d+ return 'WIDTH'; +\s+"width" {this.popState();} + +"state"\s+ { this.begin('LINE'); return 'state'; } +"note"\s+ { this.begin('LINE'); return 'note'; } +"stateDiagram"\s+ { console.log('Got state diagram', yytext,'#');return 'SD'; } +"hide empty description" { console.log('HIDE_EMPTY', yytext,'#');return 'HIDE_EMPTY'; } +// "participant" { this.begin('ID'); return 'participant'; } +// [^\->:\n,;]+?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } +// "as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; } +// (?:) { this.popState(); this.popState(); return 'NL'; } +// "<>" { this.begin('LINE'); return 'else'; } +// "<>" { this.begin('LINE'); return 'par'; } +// "and" { this.begin('LINE'); return 'and'; } +// [^#\n;]* { this.popState(); return 'restOfLine'; } +// "end" return 'end'; +"[*]" { console.log('EDGE_STATE=',yytext); return 'EDGE_STATE';} +[^:\n\s\-]+ { console.log('ID=',yytext); return 'ID';} +\s*":"[^\+\->:\n,;]+ { yytext = yytext.trim(); console.log('Descr = ', yytext); return 'DESCR'; } +"left of" return 'left_of'; +"right of" return 'right_of'; +// "over" return 'over'; +// "note" return 'note'; +// "activate" { this.begin('ID'); return 'activate'; } +// "deactivate" { this.begin('ID'); return 'deactivate'; } +// "title" return 'title'; +// "stateDiagram" return 'SD'; +// "," return ','; +// ";" return 'NL'; +// [^\+\->:\n,;]+ { yytext = yytext.trim(); return 'ACTOR'; } +"-->" return '-->'; +// "--" return '--'; +// ":"[^#\n;]+ return 'TXT'; +<> return 'NL'; +. return 'INVALID'; + +/lex + +%left '^' + +%start start + +%% /* language grammar */ + +start + : SPACE start + | NL start + | SD document { return $2; } + ; + +document + : /* empty */ { $$ = [] } + | document line {$1.push($2);$$ = $1} + ; + +line + : SPACE statement { console.log('here');$$ = $2 } + | statement {console.log('there'); $$ = $1 } + | NL { $$=[];} + ; + +statement + : idStatement DESCR + | idStatement '-->' idStatement + | HIDE_EMPTY + | scale WIDTH + ; + +idStatement + : ID + | EDGE_STATE + ; +// statement +// : 'participant' actor 'AS' restOfLine 'NL' {$2.description=$4; $$=$2;} +// | 'participant' actor 'NL' {$$=$2;} +// | signal 'NL' +// | 'activate' actor 'NL' {$$={type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $2};} +// | 'deactivate' actor 'NL' {$$={type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $2};} +// | note_statement 'NL' +// | title text2 'NL' {$$=[{type:'setTitle', text:$2}]} +// | 'loop' restOfLine document end +// { +// $3.unshift({type: 'loopStart', loopText:$2, signalType: yy.LINETYPE.LOOP_START}); +// $3.push({type: 'loopEnd', loopText:$2, signalType: yy.LINETYPE.LOOP_END}); +// $$=$3;} +// | 'rect' restOfLine document end +// { +// $3.unshift({type: 'rectStart', color:$2, signalType: yy.LINETYPE.RECT_START }); +// $3.push({type: 'rectEnd', color:$2, signalType: yy.LINETYPE.RECT_END }); +// $$=$3;} +// | opt restOfLine document end +// { +// $3.unshift({type: 'optStart', optText:$2, signalType: yy.LINETYPE.OPT_START}); +// $3.push({type: 'optEnd', optText:$2, signalType: yy.LINETYPE.OPT_END}); +// $$=$3;} +// | alt restOfLine else_sections end +// { +// // Alt start +// $3.unshift({type: 'altStart', altText:$2, signalType: yy.LINETYPE.ALT_START}); +// // Content in alt is already in $3 +// // End +// $3.push({type: 'altEnd', signalType: yy.LINETYPE.ALT_END}); +// $$=$3;} +// | par restOfLine par_sections end +// { +// // Parallel start +// $3.unshift({type: 'parStart', parText:$2, signalType: yy.LINETYPE.PAR_START}); +// // Content in par is already in $3 +// // End +// $3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END}); +// $$=$3;} +// ; + +// par_sections +// : document +// | document and restOfLine par_sections +// { $$ = $1.concat([{type: 'and', parText:$3, signalType: yy.LINETYPE.PAR_AND}, $4]); } +// ; + +// else_sections +// : document +// | document else restOfLine else_sections +// { $$ = $1.concat([{type: 'else', altText:$3, signalType: yy.LINETYPE.ALT_ELSE}, $4]); } +// ; + +// note_statement +// : 'note' placement actor text2 +// { +// $$ = [$3, {type:'addNote', placement:$2, actor:$3.actor, text:$4}];} +// | 'note' 'over' actor_pair text2 +// { +// // Coerce actor_pair into a [to, from, ...] array +// $2 = [].concat($3, $3).slice(0, 2); +// $2[0] = $2[0].actor; +// $2[1] = $2[1].actor; +// $$ = [$3, {type:'addNote', placement:yy.PLACEMENT.OVER, actor:$2.slice(0, 2), text:$4}];} +// ; + +// spaceList +// : SPACE spaceList +// | SPACE +// ; +// actor_pair +// : actor ',' actor { $$ = [$1, $3]; } +// | actor { $$ = $1; } +// ; + +// placement +// : 'left_of' { $$ = yy.PLACEMENT.LEFTOF; } +// | 'right_of' { $$ = yy.PLACEMENT.RIGHTOF; } +// ; + +// signal +// : actor signaltype '+' actor text2 +// { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5}, +// {type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $4} +// ]} +// | actor signaltype '-' actor text2 +// { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5}, +// {type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $1} +// ]} +// | actor signaltype actor text2 +// { $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]} +// ; + +// actor +// : ACTOR {$$={type: 'addActor', actor:$1}} +// ; + +// signaltype +// : SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; } +// | DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; } +// | SOLID_ARROW { $$ = yy.LINETYPE.SOLID; } +// | DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; } +// | SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; } +// | DOTTED_CROSS { $$ = yy.LINETYPE.DOTTED_CROSS; } +// ; + +// text2: TXT {$$ = $1.substring(1).trim().replace(/\\n/gm, "\n");} ; + +%% diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js new file mode 100644 index 000000000..94ec0e21e --- /dev/null +++ b/src/diagrams/state/stateDb.js @@ -0,0 +1,95 @@ +import { logger } from '../../logger'; + +let relations = []; +let classes = {}; + +/** + * Function called by parser when a node definition has been found. + * @param id + * @param text + * @param type + * @param style + */ +export const addClass = function(id) { + if (typeof classes[id] === 'undefined') { + classes[id] = { + id: id, + methods: [], + members: [] + }; + } +}; + +export const clear = function() { + relations = []; + classes = {}; +}; + +export const getClass = function(id) { + return classes[id]; +}; +export const getClasses = function() { + return classes; +}; + +export const getRelations = function() { + return relations; +}; + +export const addRelation = function(relation) { + logger.debug('Adding relation: ' + JSON.stringify(relation)); + addClass(relation.id1); + addClass(relation.id2); + relations.push(relation); +}; + +export const addMember = function(className, member) { + const theClass = classes[className]; + if (typeof member === 'string') { + if (member.substr(-1) === ')') { + theClass.methods.push(member); + } else { + theClass.members.push(member); + } + } +}; + +export const addMembers = function(className, MembersArr) { + if (Array.isArray(MembersArr)) { + MembersArr.forEach(member => addMember(className, member)); + } +}; + +export const cleanupLabel = function(label) { + if (label.substring(0, 1) === ':') { + return label.substr(2).trim(); + } else { + return label.trim(); + } +}; + +export const lineType = { + LINE: 0, + DOTTED_LINE: 1 +}; + +export const relationType = { + AGGREGATION: 0, + EXTENSION: 1, + COMPOSITION: 2, + DEPENDENCY: 3 +}; + +export default { + addClass, + clear, + getClass, + getClasses, + getRelations, + addRelation, + addMember, + addMembers, + cleanupLabel, + lineType, + relationType +}; diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js new file mode 100644 index 000000000..e17cb3dee --- /dev/null +++ b/src/diagrams/state/stateDiagram.spec.js @@ -0,0 +1,214 @@ +/* eslint-env jasmine */ +import { parser } from './parser/stateDiagram'; +import stateDb from './stateDb'; + +describe('state diagram, ', function() { + describe('when parsing an info graph it', function() { + beforeEach(function() { + parser.yy = stateDb; + }); + + it('simple', function() { + const str = `stateDiagram\n + State1 : this is another string + [*] --> State1 + State1 --> [*] + `; + + parser.parse(str); + }); + it('should handle relation definitions', function() { + const str = `stateDiagram\n + [*] --> State1 + State1 --> [*] + State1 : this is a string + State1 : this is another string + + State1 --> State2 + State2 --> [*] + `; + + parser.parse(str); + }); + it('hide empty description', function() { + const str = `stateDiagram\n + hide empty description + [*] --> State1 + State1 --> [*] + State1 : this is a string + State1 : this is another string + + State1 --> State2 + State2 --> [*] + `; + + parser.parse(str); + }); + it('scale', function() { + const str = `stateDiagram\n + scale 350 width + [*] --> State1 + State1 --> [*] + State1 : this is a string + State1 : this is another string + + State1 --> State2 + State2 --> [*] + `; + + parser.parse(str); + }); + + xit('should handle relation definitions', function() { + const str = `stateDiagram\n + state Configuring { + [*] --> NewValueSelection + NewValueSelection --> NewValuePreview : EvNewValue + NewValuePreview --> NewValueSelection : EvNewValueRejected + NewValuePreview --> NewValueSelection : EvNewValueSaved + + state NewValuePreview { + State1 -> State2 + } + } + `; + + parser.parse(str); + }); + xit('should handle relation definitions', function() { + const str = `stateDiagram\n + scale 350 width + [*] --> NotShooting + + state NotShooting { + [*] --> Idle + Idle --> Configuring : EvConfig + Configuring --> Idle : EvConfig + } + + state Configuring { + [*] --> NewValueSelection + NewValueSelection --> NewValuePreview : EvNewValue + NewValuePreview --> NewValueSelection : EvNewValueRejected + NewValuePreview --> NewValueSelection : EvNewValueSaved + + state NewValuePreview { + State1 -> State2 + } + } + `; + + parser.parse(str); + }); + // it('should handle relation definitions', function() { + // const str = `stateDiagram\n + // scale 600 width + + // [*] --> State1 + // State1 --> State2 : Succeeded + // State1 --> [*] : Aborted + // State2 --> State3 : Succeeded + // State2 --> [*] : Aborted + // state State3 { + // state "Accumulate Enough Data\nLong State Name" as long1 + // long1 : Just a test + // [*] --> long1 + // long1 --> long1 : New Data + // long1 --> ProcessData : Enough Data + // } + // State3 --> State3 : Failed + // State3 --> [*] : Succeeded / Save Result + // State3 --> [*] : Aborted + // `; + + // parser.parse(str); + // }); + // it('should handle relation definitions', function() { + // const str = `stateDiagram\n + // state fork_state <> + // [*] --> fork_state + // fork_state --> State2 + // fork_state --> State3 + + // state join_state <> + // State2 --> join_state + // State3 --> join_state + // join_state --> State4 + // State4 --> [*] + // `; + + // parser.parse(str); + // }); + // it('should handle relation definitions', function() { + // const str = `stateDiagram\n + // [*] --> Active + + // state Active { + // [*] -> NumLockOff + // NumLockOff --> NumLockOn : EvNumLockPressed + // NumLockOn --> NumLockOff : EvNumLockPressed + // -- + // [*] -> CapsLockOff + // CapsLockOff --> CapsLockOn : EvCapsLockPressed + // CapsLockOn --> CapsLockOff : EvCapsLockPressed + // -- + // [*] -> ScrollLockOff + // ScrollLockOff --> ScrollLockOn : EvCapsLockPressed + // ScrollLockOn --> ScrollLockOff : EvCapsLockPressed + // `; + + // parser.parse(str); + // }); + // it('should handle relation definitions', function() { + // const str = `stateDiagram\n + // [*] -up-> First + // First -right-> Second + // Second --> Third + // Third -left-> Last + // `; + + // parser.parse(str); + // }); + // it('should handle relation definitions', function() { + // const str = `stateDiagram\n + // [*] --> Active + // Active --> Inactive + + // note left of Active : this is a short\nnote + + // note right of Inactive + // A note can also + // be defined on + // several lines + // end note + // `; + + // parser.parse(str); + // }); + // it('should handle relation definitions', function() { + // const str = `stateDiagram\n + // state foo + // note "This is a floating note" as N1 + // `; + + // parser.parse(str); + // }); + // it('should handle relation definitions', function() { + // const str = `stateDiagram\n + // [*] --> NotShooting + + // state "Not Shooting State" as NotShooting { + // state "Idle mode" as Idle + // state "Configuring mode" as Configuring + // [*] --> Idle + // Idle --> Configuring : EvConfig + // Configuring --> Idle : EvConfig + // } + + // note right of NotShooting : This is a note on a composite state + // `; + + // parser.parse(str); + // }); + }); +}); diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js new file mode 100644 index 000000000..6c6646ec5 --- /dev/null +++ b/src/diagrams/state/stateRenderer.js @@ -0,0 +1,101 @@ +import * as d3 from 'd3'; +import dagre from 'dagre-layout'; +import graphlib from 'graphlibrary'; +import { logger } from '../../logger'; +import stateDb from './stateDb'; +import { parser } from './parser/stateDiagram'; + +parser.yy = stateDb; + +const idCache = {}; + +let stateCnt = 0; +const conf = { + dividerMargin: 10, + padding: 5, + textHeight: 10 +}; + +export const setConf = function(cnf) {}; +/** + * Draws a flowchart in the tag with id: id based on the graph definition in text. + * @param text + * @param id + */ +export const draw = function(text, id) { + parser.yy.clear(); + parser.parse(text); + + logger.info('Rendering diagram ' + text); + + // /// / Fetch the default direction, use TD if none was found + // const diagram = d3.select(`[id='${id}']`); + // insertMarkers(diagram); + + // // Layout graph, Create a new directed graph + // const g = new graphlib.Graph({ + // multigraph: true + // }); + + // // Set an object for the graph label + // g.setGraph({ + // isMultiGraph: true + // }); + + // // Default to assigning a new object as a label for each new edge. + // g.setDefaultEdgeLabel(function() { + // return {}; + // }); + + // const classes = classDb.getClasses(); + // const keys = Object.keys(classes); + // total = keys.length; + // for (let i = 0; i < keys.length; i++) { + // const classDef = classes[keys[i]]; + // const node = drawClass(diagram, classDef); + // // Add nodes to the graph. The first argument is the node id. The second is + // // metadata about the node. In this case we're going to add labels to each of + // // our nodes. + // g.setNode(node.id, node); + // logger.info('Org height: ' + node.height); + // } + + // const relations = classDb.getRelations(); + // relations.forEach(function(relation) { + // logger.info( + // 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation) + // ); + // g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { + // relation: relation + // }); + // }); + // dagre.layout(g); + // g.nodes().forEach(function(v) { + // if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') { + // logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v))); + // d3.select('#' + v).attr( + // 'transform', + // 'translate(' + + // (g.node(v).x - g.node(v).width / 2) + + // ',' + + // (g.node(v).y - g.node(v).height / 2) + + // ' )' + // ); + // } + // }); + // g.edges().forEach(function(e) { + // if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') { + // logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e))); + // drawEdge(diagram, g.edge(e), g.edge(e).relation); + // } + // }); + + // diagram.attr('height', '100%'); + // diagram.attr('width', '100%'); + // diagram.attr('viewBox', '0 0 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20)); +}; + +export default { + setConf, + draw +}; From 6f054519e7c4eb81f9223df9e7bd4da4d425d6a4 Mon Sep 17 00:00:00 2001 From: knsv Date: Sat, 21 Sep 2019 08:50:32 -0700 Subject: [PATCH 02/17] #945 Handling simple state statements --- src/diagrams/state/parser/stateDiagram.jison | 33 +++++++++++++------- src/diagrams/state/stateDiagram.spec.js | 21 +++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index cfdb61158..880c274a1 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -14,8 +14,10 @@ // Special states for recognizing aliases %x ID +%x STATE %x ALIAS %x SCALE +%x struct // A special state for grabbing text up to the first comment/newline %x LINE @@ -24,16 +26,23 @@ [\n]+ return 'NL'; \s+ /* skip all whitespace */ -((?!\n)\s)+ /* skip same-line whitespace */ -\#[^\n]* /* skip comments */ +((?!\n)\s)+ /* skip same-line whitespace */ +\#[^\n]* /* skip comments */ \%%[^\n]* /* skip comments */ "scale"\s+ { this.pushState('SCALE'); console.log('Got scale', yytext);return 'scale'; } \d+ return 'WIDTH'; \s+"width" {this.popState();} -"state"\s+ { this.begin('LINE'); return 'state'; } -"note"\s+ { this.begin('LINE'); return 'note'; } +"state"\s+ { this.pushState('STATE'); } +[^\n\s\{]+ {console.log('COMPOSIT_STATE', yytext);return 'COMPOSIT_STATE';} +\{ {this.popState();this.pushState('struct'); console.log('begin struct', yytext);return 'STRUCT_START';} +\} { console.log('Ending struct'); this.popState(); return 'STRUCT_STOP';}} +[\n] /* nothing */ +// [^\{\}\n]* { /*console.log('lex-member: ' + yytext);*/ return "MEMBER";} + + +"note"\s+ { this.begin('LINE'); return 'note'; } "stateDiagram"\s+ { console.log('Got state diagram', yytext,'#');return 'SD'; } "hide empty description" { console.log('HIDE_EMPTY', yytext,'#');return 'HIDE_EMPTY'; } // "participant" { this.begin('ID'); return 'participant'; } @@ -45,11 +54,11 @@ // "and" { this.begin('LINE'); return 'and'; } // [^#\n;]* { this.popState(); return 'restOfLine'; } // "end" return 'end'; -"[*]" { console.log('EDGE_STATE=',yytext); return 'EDGE_STATE';} -[^:\n\s\-]+ { console.log('ID=',yytext); return 'ID';} -\s*":"[^\+\->:\n,;]+ { yytext = yytext.trim(); console.log('Descr = ', yytext); return 'DESCR'; } -"left of" return 'left_of'; -"right of" return 'right_of'; +"[*]" { console.log('EDGE_STATE=',yytext); return 'EDGE_STATE';} +[^:\n\s\-]+ { console.log('ID=',yytext); return 'ID';} +\s*":"[^\+\->:\n,;]+ { yytext = yytext.trim(); console.log('Descr = ', yytext); return 'DESCR'; } +"left of" return 'left_of'; +"right of" return 'right_of'; // "over" return 'over'; // "note" return 'note'; // "activate" { this.begin('ID'); return 'activate'; } @@ -59,7 +68,7 @@ // "," return ','; // ";" return 'NL'; // [^\+\->:\n,;]+ { yytext = yytext.trim(); return 'ACTOR'; } -"-->" return '-->'; +"-->" return '-->'; // "--" return '--'; // ":"[^#\n;]+ return 'TXT'; <> return 'NL'; @@ -86,15 +95,17 @@ document line : SPACE statement { console.log('here');$$ = $2 } - | statement {console.log('there'); $$ = $1 } + | statement {console.log('line', $1); $$ = $1 } | NL { $$=[];} ; statement : idStatement DESCR | idStatement '-->' idStatement + | idStatement '-->' idStatement DESCR | HIDE_EMPTY | scale WIDTH + | COMPOSIT_STATE STRUCT_START document STRUCT_STOP ; idStatement diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index e17cb3dee..1a0b92a31 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -59,6 +59,27 @@ describe('state diagram, ', function() { parser.parse(str); }); + it('description after second state', function() { + const str = `stateDiagram\n + scale 350 width + [*] --> State1 : This is the description + State1 --> [*] + `; + + parser.parse(str); + }); + it('should handle state statements', function() { + const str = `stateDiagram\n + state Configuring { + [*] --> NewValueSelection + NewValueSelection --> NewValuePreview : EvNewValue + NewValuePreview --> NewValueSelection : EvNewValueRejected + NewValuePreview --> NewValueSelection : EvNewValueSaved1 + } + `; + + parser.parse(str); + }); xit('should handle relation definitions', function() { const str = `stateDiagram\n state Configuring { From 9993b90a2062163d5fad130bdb040165fa4ddcb3 Mon Sep 17 00:00:00 2001 From: knsv Date: Sat, 21 Sep 2019 08:54:18 -0700 Subject: [PATCH 03/17] #945 Handling recursive state statements --- src/diagrams/state/parser/stateDiagram.jison | 2 +- src/diagrams/state/stateDiagram.spec.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index 880c274a1..b880355ed 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -34,7 +34,7 @@ \d+ return 'WIDTH'; \s+"width" {this.popState();} -"state"\s+ { this.pushState('STATE'); } +"state"\s+ { this.pushState('STATE'); } [^\n\s\{]+ {console.log('COMPOSIT_STATE', yytext);return 'COMPOSIT_STATE';} \{ {this.popState();this.pushState('struct'); console.log('begin struct', yytext);return 'STRUCT_START';} \} { console.log('Ending struct'); this.popState(); return 'STRUCT_STOP';}} diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index 1a0b92a31..61d97e44c 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -80,7 +80,7 @@ describe('state diagram, ', function() { parser.parse(str); }); - xit('should handle relation definitions', function() { + it('should handle recursive state definitions', function() { const str = `stateDiagram\n state Configuring { [*] --> NewValueSelection @@ -89,14 +89,14 @@ describe('state diagram, ', function() { NewValuePreview --> NewValueSelection : EvNewValueSaved state NewValuePreview { - State1 -> State2 + State1 --> State2 } } `; parser.parse(str); }); - xit('should handle relation definitions', function() { + it('should handle multiple recursive state definitions', function() { const str = `stateDiagram\n scale 350 width [*] --> NotShooting @@ -114,7 +114,7 @@ describe('state diagram, ', function() { NewValuePreview --> NewValueSelection : EvNewValueSaved state NewValuePreview { - State1 -> State2 + State1 --> State2 } } `; From 921d5464a1c12bbeb8854acdca894458f19dc372 Mon Sep 17 00:00:00 2001 From: knsv Date: Sat, 21 Sep 2019 09:12:02 -0700 Subject: [PATCH 04/17] #945 Handling recursive logn descriptions for states with quotes --- src/diagrams/state/parser/stateDiagram.jison | 8 ++++ src/diagrams/state/stateDiagram.spec.js | 49 +++++++++++--------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index b880355ed..23a2c8c12 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -15,6 +15,8 @@ // Special states for recognizing aliases %x ID %x STATE +%x STATE_STRING +%x STATE_ID %x ALIAS %x SCALE %x struct @@ -35,6 +37,11 @@ \s+"width" {this.popState();} "state"\s+ { this.pushState('STATE'); } +["] this.begin("STATE_STRING"); +"as"\s* {this.popState('STATE_ID');return "AS";} +[^\n] {this.popState('STATE_ID');return "ID";} +["] this.popState(); +[^"]* { console.log('Long description:', yytext);return "STATE_DESCR";} [^\n\s\{]+ {console.log('COMPOSIT_STATE', yytext);return 'COMPOSIT_STATE';} \{ {this.popState();this.pushState('struct'); console.log('begin struct', yytext);return 'STRUCT_START';} \} { console.log('Ending struct'); this.popState(); return 'STRUCT_STOP';}} @@ -106,6 +113,7 @@ statement | HIDE_EMPTY | scale WIDTH | COMPOSIT_STATE STRUCT_START document STRUCT_STOP + | STATE_DESCR AS ID ; idStatement diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index 61d97e44c..664d1051b 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -121,29 +121,36 @@ describe('state diagram, ', function() { parser.parse(str); }); - // it('should handle relation definitions', function() { - // const str = `stateDiagram\n - // scale 600 width + it('should handle state deifintions with separation of id', function() { + const str = `stateDiagram\n + state "Long state description" as state1 + `; - // [*] --> State1 - // State1 --> State2 : Succeeded - // State1 --> [*] : Aborted - // State2 --> State3 : Succeeded - // State2 --> [*] : Aborted - // state State3 { - // state "Accumulate Enough Data\nLong State Name" as long1 - // long1 : Just a test - // [*] --> long1 - // long1 --> long1 : New Data - // long1 --> ProcessData : Enough Data - // } - // State3 --> State3 : Failed - // State3 --> [*] : Succeeded / Save Result - // State3 --> [*] : Aborted - // `; + parser.parse(str); + }); + it('should State definition with quotes', function() { + const str = `stateDiagram\n + scale 600 width - // parser.parse(str); - // }); + [*] --> State1 + State1 --> State2 : Succeeded + State1 --> [*] : Aborted + State2 --> State3 : Succeeded + State2 --> [*] : Aborted + state State3 { + state "Accumulate Enough Data\nLong State Name" as long1 + long1 : Just a test + [*] --> long1 + long1 --> long1 : New Data + long1 --> ProcessData : Enough Data + } + State3 --> State3 : Failed + State3 --> [*] : Succeeded / Save Result + State3 --> [*] : Aborted + `; + + parser.parse(str); + }); // it('should handle relation definitions', function() { // const str = `stateDiagram\n // state fork_state <> From 1aa8b9b8042a514557c2b8a17f434461ba513fbf Mon Sep 17 00:00:00 2001 From: knsv Date: Sat, 21 Sep 2019 11:31:09 -0700 Subject: [PATCH 05/17] #945 Handling recursive logn descriptions for states with quotes --- src/diagrams/state/parser/stateDiagram.jison | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index 23a2c8c12..ad2897657 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -38,8 +38,8 @@ "state"\s+ { this.pushState('STATE'); } ["] this.begin("STATE_STRING"); -"as"\s* {this.popState('STATE_ID');return "AS";} -[^\n] {this.popState('STATE_ID');return "ID";} +"as"\s* {this.popState();this.pushState('STATE_ID');return "AS";} +[^\n]* {this.popState();console.log('ID');return "ID";} ["] this.popState(); [^"]* { console.log('Long description:', yytext);return "STATE_DESCR";} [^\n\s\{]+ {console.log('COMPOSIT_STATE', yytext);return 'COMPOSIT_STATE';} From 3a8564de92a01f95a069ac0ea6e16ea08721cf13 Mon Sep 17 00:00:00 2001 From: knsv Date: Sat, 21 Sep 2019 23:19:03 -0700 Subject: [PATCH 06/17] #945 Handling of fork statements --- src/diagrams/state/parser/stateDiagram.jison | 5 ++++ src/diagrams/state/stateDiagram.spec.js | 30 ++++++++++---------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index ad2897657..e845da13b 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -15,6 +15,7 @@ // Special states for recognizing aliases %x ID %x STATE +%x FORK_STATE %x STATE_STRING %x STATE_ID %x ALIAS @@ -37,6 +38,8 @@ \s+"width" {this.popState();} "state"\s+ { this.pushState('STATE'); } +.*"<>" {this.popState();console.log('Fork: ',yytext);return 'FORK';} +.*"<>" {this.popState();console.log('Join: ',yytext);return 'JOIN';} ["] this.begin("STATE_STRING"); "as"\s* {this.popState();this.pushState('STATE_ID');return "AS";} [^\n]* {this.popState();console.log('ID');return "ID";} @@ -114,6 +117,8 @@ statement | scale WIDTH | COMPOSIT_STATE STRUCT_START document STRUCT_STOP | STATE_DESCR AS ID + | FORK + | JOIN ; idStatement diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index 664d1051b..03e047eb4 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -151,23 +151,23 @@ describe('state diagram, ', function() { parser.parse(str); }); - // it('should handle relation definitions', function() { - // const str = `stateDiagram\n - // state fork_state <> - // [*] --> fork_state - // fork_state --> State2 - // fork_state --> State3 + it('should handle fork statements', function() { + const str = `stateDiagram\n + state fork_state <> + [*] --> fork_state + fork_state --> State2 + fork_state --> State3 - // state join_state <> - // State2 --> join_state - // State3 --> join_state - // join_state --> State4 - // State4 --> [*] - // `; + state join_state <> + State2 --> join_state + State3 --> join_state + join_state --> State4 + State4 --> [*] + `; - // parser.parse(str); - // }); - // it('should handle relation definitions', function() { + parser.parse(str); + }); + // it('should handle concurrent state', function() { // const str = `stateDiagram\n // [*] --> Active From 2f0248e6d5553f6faf703d79dbf3db87540f0ce6 Mon Sep 17 00:00:00 2001 From: knsv Date: Sun, 22 Sep 2019 02:38:04 -0700 Subject: [PATCH 07/17] #945 Handling of note statements --- src/diagrams/state/parser/stateDiagram.jison | 23 ++++-- src/diagrams/state/stateDiagram.spec.js | 80 ++++++++++++-------- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index e845da13b..e2cd40946 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -20,6 +20,9 @@ %x STATE_ID %x ALIAS %x SCALE +%x NOTE +%x NOTE_ID +%x NOTE_TEXT %x struct // A special state for grabbing text up to the first comment/newline @@ -49,11 +52,15 @@ \{ {this.popState();this.pushState('struct'); console.log('begin struct', yytext);return 'STRUCT_START';} \} { console.log('Ending struct'); this.popState(); return 'STRUCT_STOP';}} [\n] /* nothing */ -// [^\{\}\n]* { /*console.log('lex-member: ' + yytext);*/ return "MEMBER";} +"note"\s+ { this.begin('NOTE'); return 'note'; } +"left of" { this.popState();this.pushState('NOTE_ID');console.log('Got dir');return 'left_of';} +"right of" { this.popState();this.pushState('NOTE_ID');return 'right_of';} +\s*[^:\n\s\-]+ { this.popState();this.pushState('NOTE_TEXT');console.log('Got ID for note', yytext);return 'ID';} +\s*":"[^\+\-:\n,;]+ { this.popState();console.log('Got NOTE_TEXT for note',yytext);return 'NOTE_TEXT';} +\s*[^\+\-:,;]+"end note" { this.popState();console.log('Got NOTE_TEXT for note',yytext);return 'NOTE_TEXT';} -"note"\s+ { this.begin('LINE'); return 'note'; } -"stateDiagram"\s+ { console.log('Got state diagram', yytext,'#');return 'SD'; } +"stateDiagram"\s+ { console.log('Got state diagram', yytext,'#');return 'SD'; } "hide empty description" { console.log('HIDE_EMPTY', yytext,'#');return 'HIDE_EMPTY'; } // "participant" { this.begin('ID'); return 'participant'; } // [^\->:\n,;]+?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } @@ -67,8 +74,6 @@ "[*]" { console.log('EDGE_STATE=',yytext); return 'EDGE_STATE';} [^:\n\s\-]+ { console.log('ID=',yytext); return 'ID';} \s*":"[^\+\->:\n,;]+ { yytext = yytext.trim(); console.log('Descr = ', yytext); return 'DESCR'; } -"left of" return 'left_of'; -"right of" return 'right_of'; // "over" return 'over'; // "note" return 'note'; // "activate" { this.begin('ID'); return 'activate'; } @@ -79,6 +84,7 @@ // ";" return 'NL'; // [^\+\->:\n,;]+ { yytext = yytext.trim(); return 'ACTOR'; } "-->" return '-->'; +"--" return 'CONCURRENT'; // "--" return '--'; // ":"[^#\n;]+ return 'TXT'; <> return 'NL'; @@ -119,12 +125,19 @@ statement | STATE_DESCR AS ID | FORK | JOIN + | CONCURRENT + | note notePosition ID NOTE_TEXT ; idStatement : ID | EDGE_STATE ; + +notePosition + : left_of + | right_of + ; // statement // : 'participant' actor 'AS' restOfLine 'NL' {$2.description=$4; $$=$2;} // | 'participant' actor 'NL' {$$=$2;} diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index 03e047eb4..0b0ef4244 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -167,27 +167,43 @@ describe('state diagram, ', function() { parser.parse(str); }); - // it('should handle concurrent state', function() { - // const str = `stateDiagram\n - // [*] --> Active + it('should handle concurrent state', function() { + const str = `stateDiagram\n + [*] --> Active - // state Active { - // [*] -> NumLockOff - // NumLockOff --> NumLockOn : EvNumLockPressed - // NumLockOn --> NumLockOff : EvNumLockPressed - // -- - // [*] -> CapsLockOff - // CapsLockOff --> CapsLockOn : EvCapsLockPressed - // CapsLockOn --> CapsLockOff : EvCapsLockPressed - // -- - // [*] -> ScrollLockOff - // ScrollLockOff --> ScrollLockOn : EvCapsLockPressed - // ScrollLockOn --> ScrollLockOff : EvCapsLockPressed - // `; + state Active { + [*] --> NumLockOff + NumLockOff --> NumLockOn : EvNumLockPressed + NumLockOn --> NumLockOff : EvNumLockPressed + -- + [*] --> CapsLockOff + CapsLockOff --> CapsLockOn : EvCapsLockPressed + CapsLockOn --> CapsLockOff : EvCapsLockPressed + -- + [*] --> ScrollLockOff + ScrollLockOff --> ScrollLockOn : EvCapsLockPressed + ScrollLockOn --> ScrollLockOff : EvCapsLockPressed + } + `; - // parser.parse(str); - // }); - // it('should handle relation definitions', function() { + parser.parse(str); + }); + it('should handle concurrent state', function() { + const str = `stateDiagram\n + [*] --> Active + + state Active { + [*] --> NumLockOff + -- + [*] --> CapsLockOff + -- + [*] --> ScrollLockOff + } + `; + + parser.parse(str); + }); + // it('should handle arrow directions definitions', function() { // const str = `stateDiagram\n // [*] -up-> First // First -right-> Second @@ -197,22 +213,22 @@ describe('state diagram, ', function() { // parser.parse(str); // }); - // it('should handle relation definitions', function() { - // const str = `stateDiagram\n - // [*] --> Active - // Active --> Inactive + it('should handle note statements', function() { + const str = `stateDiagram\n + [*] --> Active + Active --> Inactive - // note left of Active : this is a short\nnote + note left of Active : this is a short
note - // note right of Inactive - // A note can also - // be defined on - // several lines - // end note - // `; + note right of Inactive + A note can also + be defined on + several lines + end note + `; - // parser.parse(str); - // }); + parser.parse(str); + }); // it('should handle relation definitions', function() { // const str = `stateDiagram\n // state foo From 2306534248e771487d8205a4633d8981fe616ac3 Mon Sep 17 00:00:00 2001 From: knsv Date: Sun, 22 Sep 2019 03:30:36 -0700 Subject: [PATCH 08/17] #945 Handling of dimples state definitions --- src/diagrams/state/parser/stateDiagram.jison | 17 +++++- src/diagrams/state/stateDiagram.spec.js | 64 +++++++++++++------- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index e2cd40946..0e73cee4a 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -23,6 +23,8 @@ %x NOTE %x NOTE_ID %x NOTE_TEXT +%x FLOATING_NOTE +%x FLOATING_NOTE_ID %x struct // A special state for grabbing text up to the first comment/newline @@ -45,17 +47,23 @@ .*"<>" {this.popState();console.log('Join: ',yytext);return 'JOIN';} ["] this.begin("STATE_STRING"); "as"\s* {this.popState();this.pushState('STATE_ID');return "AS";} -[^\n]* {this.popState();console.log('ID');return "ID";} +[^\n\{]* {this.popState();console.log('STATE_ID', yytext);return "ID";} ["] this.popState(); [^"]* { console.log('Long description:', yytext);return "STATE_DESCR";} [^\n\s\{]+ {console.log('COMPOSIT_STATE', yytext);return 'COMPOSIT_STATE';} -\{ {this.popState();this.pushState('struct'); console.log('begin struct', yytext);return 'STRUCT_START';} +\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 */ "note"\s+ { this.begin('NOTE'); return 'note'; } "left of" { this.popState();this.pushState('NOTE_ID');console.log('Got dir');return 'left_of';} "right of" { this.popState();this.pushState('NOTE_ID');return 'right_of';} +\" { this.popState();this.pushState('FLOATING_NOTE');} +\s*"as"\s* {this.popState();this.pushState('FLOATING_NOTE_ID');return "AS";} +["] /**/ +[^"]* { console.log('Floating note text: ', yytext);return "NOTE_TEXT";} +[^\n]* {this.popState();console.log('Floating note ID', yytext);return "ID";} \s*[^:\n\s\-]+ { this.popState();this.pushState('NOTE_TEXT');console.log('Got ID for note', yytext);return 'ID';} \s*":"[^\+\-:\n,;]+ { this.popState();console.log('Got NOTE_TEXT for note',yytext);return 'NOTE_TEXT';} \s*[^\+\-:,;]+"end note" { this.popState();console.log('Got NOTE_TEXT for note',yytext);return 'NOTE_TEXT';} @@ -72,7 +80,7 @@ // [^#\n;]* { this.popState(); return 'restOfLine'; } // "end" return 'end'; "[*]" { console.log('EDGE_STATE=',yytext); return 'EDGE_STATE';} -[^:\n\s\-]+ { console.log('ID=',yytext); return 'ID';} +[^:\n\s\-\{]+ { console.log('=>ID=',yytext); return 'ID';} \s*":"[^\+\->:\n,;]+ { yytext = yytext.trim(); console.log('Descr = ', yytext); return 'DESCR'; } // "over" return 'over'; // "note" return 'note'; @@ -121,12 +129,15 @@ statement | idStatement '-->' idStatement DESCR | HIDE_EMPTY | scale WIDTH + | COMPOSIT_STATE | COMPOSIT_STATE STRUCT_START document STRUCT_STOP | STATE_DESCR AS ID + | STATE_DESCR AS ID STRUCT_START document STRUCT_STOP | FORK | JOIN | CONCURRENT | note notePosition ID NOTE_TEXT + | note NOTE_TEXT AS ID ; idStatement diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index 0b0ef4244..58399bfdf 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -128,6 +128,20 @@ describe('state diagram, ', function() { parser.parse(str); }); + it('should handle state deifintions with separation of id', function() { + const str = `stateDiagram + state "Not Shooting State" as NotShooting { + state "Idle mode" as Idle + state "Configuring mode" as Configuring + [*] --> Idle + Idle --> Configuring : EvConfig + Configuring --> Idle : EvConfig + } + `; + + parser.parse(str); + }); + it('should State definition with quotes', function() { const str = `stateDiagram\n scale 600 width @@ -229,30 +243,38 @@ describe('state diagram, ', function() { parser.parse(str); }); - // it('should handle relation definitions', function() { - // const str = `stateDiagram\n - // state foo - // note "This is a floating note" as N1 - // `; + it('should handle floating notes', function() { + const str = `stateDiagram + foo: bar + note "This is a floating note" as N1 + `; - // parser.parse(str); - // }); - // it('should handle relation definitions', function() { - // const str = `stateDiagram\n - // [*] --> NotShooting + parser.parse(str); + }); + it('should handle floating notes', function() { + const str = `stateDiagram\n + state foo + note "This is a floating note" as N1 + `; - // state "Not Shooting State" as NotShooting { - // state "Idle mode" as Idle - // state "Configuring mode" as Configuring - // [*] --> Idle - // Idle --> Configuring : EvConfig - // Configuring --> Idle : EvConfig - // } + parser.parse(str); + }); + it('should handle notes for composit states', function() { + const str = `stateDiagram\n + [*] --> NotShooting - // note right of NotShooting : This is a note on a composite state - // `; + state "Not Shooting State" as NotShooting { + state "Idle mode" as Idle + state "Configuring mode" as Configuring + [*] --> Idle + Idle --> Configuring : EvConfig + Configuring --> Idle : EvConfig + } - // parser.parse(str); - // }); + note right of NotShooting : This is a note on a composite state + `; + + parser.parse(str); + }); }); }); From fad76ad534fafb9a518a6e2fd098ead5d6272b66 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 25 Sep 2019 21:01:21 +0200 Subject: [PATCH 09/17] #945 Rendering of start & end node --- .../rendering/stateDiagram.spec.js | 16 + src/diagrams/state/stateDb.js | 37 +- src/diagrams/state/stateRenderer.js | 486 ++++++++++++++++-- src/mermaidAPI.js | 12 + src/utils.js | 4 + 5 files changed, 480 insertions(+), 75 deletions(-) create mode 100644 cypress/integration/rendering/stateDiagram.spec.js diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js new file mode 100644 index 000000000..a696be5de --- /dev/null +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -0,0 +1,16 @@ +/* eslint-env jest */ +import { imgSnapshotTest } from '../../helpers/util'; + +describe('State diagram', () => { + it('should render a simple state diagrams', () => { + imgSnapshotTest( + ` + stateDiagram + [*] --> State1 + State1 --> [*] + `, + { logLevel: 0 } + ); + cy.get('svg'); + }); +}); diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index 94ec0e21e..8eb1a30e1 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -1,7 +1,7 @@ import { logger } from '../../logger'; let relations = []; -let classes = {}; +let states = {}; /** * Function called by parser when a node definition has been found. @@ -10,26 +10,25 @@ let classes = {}; * @param type * @param style */ -export const addClass = function(id) { - if (typeof classes[id] === 'undefined') { - classes[id] = { +export const addState = function(id) { + if (typeof states[id] === 'undefined') { + states[id] = { id: id, - methods: [], - members: [] + descriptions: [] }; } }; export const clear = function() { relations = []; - classes = {}; + states = {}; }; -export const getClass = function(id) { - return classes[id]; +export const getState = function(id) { + return states[id]; }; -export const getClasses = function() { - return classes; +export const getstates = function() { + return states; }; export const getRelations = function() { @@ -38,18 +37,18 @@ export const getRelations = function() { export const addRelation = function(relation) { logger.debug('Adding relation: ' + JSON.stringify(relation)); - addClass(relation.id1); - addClass(relation.id2); + addState(relation.id1); + addState(relation.id2); relations.push(relation); }; export const addMember = function(className, member) { - const theClass = classes[className]; + const theState = states[className]; if (typeof member === 'string') { if (member.substr(-1) === ')') { - theClass.methods.push(member); + theState.methods.push(member); } else { - theClass.members.push(member); + theState.members.push(member); } } }; @@ -81,10 +80,10 @@ export const relationType = { }; export default { - addClass, + addState, clear, - getClass, - getClasses, + getState, + getstates, getRelations, addRelation, addMember, diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 6c6646ec5..c40e902f8 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -10,6 +10,9 @@ parser.yy = stateDb; const idCache = {}; let stateCnt = 0; +let total = 0; +let edgeCount = 0; + const conf = { dividerMargin: 10, padding: 5, @@ -17,6 +20,363 @@ const conf = { }; export const setConf = function(cnf) {}; + +// Todo optimize +const getGraphId = function(label) { + const keys = Object.keys(idCache); + + for (let i = 0; i < keys.length; i++) { + if (idCache[keys[i]].label === label) { + return keys[i]; + } + } + + return undefined; +}; + +/** + * Setup arrow head and define the marker. The result is appended to the svg. + */ +const insertMarkers = function(elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'extensionStart') + .attr('class', 'extension') + .attr('refX', 0) + .attr('refY', 7) + .attr('markerWidth', 190) + .attr('markerHeight', 240) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 1,7 L18,13 V 1 Z'); + + elem + .append('defs') + .append('marker') + .attr('id', 'extensionEnd') + .attr('refX', 19) + .attr('refY', 7) + .attr('markerWidth', 20) + .attr('markerHeight', 28) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead + + elem + .append('defs') + .append('marker') + .attr('id', 'compositionStart') + .attr('class', 'extension') + .attr('refX', 0) + .attr('refY', 7) + .attr('markerWidth', 190) + .attr('markerHeight', 240) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); + + elem + .append('defs') + .append('marker') + .attr('id', 'compositionEnd') + .attr('refX', 19) + .attr('refY', 7) + .attr('markerWidth', 20) + .attr('markerHeight', 28) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); + + elem + .append('defs') + .append('marker') + .attr('id', 'aggregationStart') + .attr('class', 'extension') + .attr('refX', 0) + .attr('refY', 7) + .attr('markerWidth', 190) + .attr('markerHeight', 240) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); + + elem + .append('defs') + .append('marker') + .attr('id', 'aggregationEnd') + .attr('refX', 19) + .attr('refY', 7) + .attr('markerWidth', 20) + .attr('markerHeight', 28) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); + + elem + .append('defs') + .append('marker') + .attr('id', 'dependencyStart') + .attr('class', 'extension') + .attr('refX', 0) + .attr('refY', 7) + .attr('markerWidth', 190) + .attr('markerHeight', 240) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z'); + + elem + .append('defs') + .append('marker') + .attr('id', 'dependencyEnd') + .attr('refX', 19) + .attr('refY', 7) + .attr('markerWidth', 20) + .attr('markerHeight', 28) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); +}; +const drawStart = function(elem, stateDef) { + logger.info('Rendering class ' + stateDef); + + const addTspan = function(textEl, txt, isFirst) { + const tSpan = textEl + .append('tspan') + .attr('x', conf.padding) + .text(txt); + if (!isFirst) { + tSpan.attr('dy', conf.textHeight); + } + }; + + const id = 'classId' + (stateCnt % total); + const stateInfo = { + id: id, + label: stateDef.id, + width: 0, + height: 0 + }; + + const g = elem + .append('g') + .attr('id', id) + .attr('class', 'classGroup'); + const title = g + .append('text') + .attr('x', conf.padding) + .attr('y', conf.textHeight + conf.padding) + .text(stateDef.id); + + const titleHeight = title.node().getBBox().height; + + const stateBox = g.node().getBBox(); + g.insert('rect', ':first-child') + .attr('x', 0) + .attr('y', 0) + .attr('width', stateBox.width + 2 * conf.padding) + .attr('height', stateBox.height + conf.padding + 0.5 * conf.dividerMargin); + + membersLine.attr('x2', stateBox.width + 2 * conf.padding); + methodsLine.attr('x2', stateBox.width + 2 * conf.padding); + + stateInfo.width = stateBox.width + 2 * conf.padding; + stateInfo.height = stateBox.height + conf.padding + 0.5 * conf.dividerMargin; + + idCache[id] = stateInfo; + stateCnt++; + return stateInfo; +}; + +/** + * Draws a start state as a black circle + */ +const drawStartState = g => + g + .append('circle') + .style('stroke', 'black') + .style('fill', 'black') + .attr('r', 5) + .attr('cx', conf.padding + 5) + .attr('cy', conf.padding + 5); +/** + * Draws a an end state as a black circle + */ +const drawSimpleState = (g, stateDef) => { + const state = g + .append('text') + .attr('x', 2 * conf.padding) + .attr('y', conf.textHeight + 2 * conf.padding) + .attr('font-size', 24) + .text(stateDef.id); + + const classBox = state.node().getBBox(); + g.insert('rect', ':first-child') + .attr('x', conf.padding) + .attr('y', conf.padding) + .attr('width', classBox.width + 2 * conf.padding) + .attr('height', classBox.height + 2 * conf.padding) + .attr('rx', '5'); + + return state; +}; +const drawEndState = g => { + g.append('circle') + .style('stroke', 'black') + .style('fill', 'white') + .attr('r', 7) + .attr('cx', conf.padding + 7) + .attr('cy', conf.padding + 7); + + return g + .append('circle') + .style('stroke', 'black') + .style('fill', 'black') + .attr('r', 5) + .attr('cx', conf.padding + 7) + .attr('cy', conf.padding + 7); +}; + +const drawEdge = function(elem, path, relation) { + const getRelationType = function(type) { + switch (type) { + case stateDb.relationType.AGGREGATION: + return 'aggregation'; + case stateDb.relationType.EXTENSION: + return 'extension'; + case stateDb.relationType.COMPOSITION: + return 'composition'; + case stateDb.relationType.DEPENDENCY: + return 'dependency'; + } + }; + + path.points = path.points.filter(p => !Number.isNaN(p.y)); + + // The data for our line + const lineData = path.points; + + // This is the accessor function we talked about above + const lineFunction = d3 + .line() + .x(function(d) { + return d.x; + }) + .y(function(d) { + return d.y; + }) + .curve(d3.curveBasis); + + const svgPath = elem + .append('path') + .attr('d', lineFunction(lineData)) + .attr('id', 'edge' + edgeCount) + .attr('class', 'relation'); + let url = ''; + if (conf.arrowMarkerAbsolute) { + url = + window.location.protocol + + '//' + + window.location.host + + window.location.pathname + + window.location.search; + url = url.replace(/\(/g, '\\('); + url = url.replace(/\)/g, '\\)'); + } + + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'Start' + ')' + ); + svgPath.attr( + 'marker-end', + 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')' + ); + + let x, y; + const l = path.points.length; + if (l % 2 !== 0 && l > 1) { + const p1 = path.points[Math.floor(l / 2)]; + const p2 = path.points[Math.ceil(l / 2)]; + x = (p1.x + p2.x) / 2; + y = (p1.y + p2.y) / 2; + } else { + const p = path.points[Math.floor(l / 2)]; + x = p.x; + y = p.y; + } + + if (typeof relation.title !== 'undefined') { + const g = elem.append('g').attr('class', 'classLabel'); + const label = g + .append('text') + .attr('class', 'label') + .attr('x', x) + .attr('y', y) + .attr('fill', 'red') + .attr('text-anchor', 'middle') + .text(relation.title); + + window.label = label; + const bounds = label.node().getBBox(); + + g.insert('rect', ':first-child') + .attr('class', 'box') + .attr('x', bounds.x - conf.padding / 2) + .attr('y', bounds.y - conf.padding / 2) + .attr('width', bounds.width + conf.padding) + .attr('height', bounds.height + conf.padding); + } + + edgeCount++; +}; + +/** + * Draws a state + * @param {*} elem + * @param {*} stateDef + */ +const drawState = function(elem, stateDef) { + logger.info('Rendering class ' + stateDef); + + const addTspan = function(textEl, txt, isFirst) { + const tSpan = textEl + .append('tspan') + .attr('x', conf.padding) + .text(txt); + if (!isFirst) { + tSpan.attr('dy', conf.textHeight); + } + }; + + const id = stateDef.id; + const stateInfo = { + id: id, + label: stateDef.id, + width: 0, + height: 0 + }; + + const g = elem + .append('g') + .attr('id', id) + .attr('class', 'classGroup'); + + if (stateDef.type === 'start') drawStartState(g); + if (stateDef.type === 'end') drawEndState(g); + if (stateDef.type === 'default') drawSimpleState(g, stateDef); + + const stateBox = g.node().getBBox(); + + stateInfo.width = stateBox.width + 2 * conf.padding; + stateInfo.height = stateBox.height + 2 * conf.padding; + + idCache[id] = stateInfo; + stateCnt++; + return stateInfo; +}; + /** * Draws a flowchart in the tag with id: id based on the graph definition in text. * @param text @@ -25,74 +385,88 @@ export const setConf = function(cnf) {}; export const draw = function(text, id) { parser.yy.clear(); parser.parse(text); - logger.info('Rendering diagram ' + text); // /// / Fetch the default direction, use TD if none was found - // const diagram = d3.select(`[id='${id}']`); - // insertMarkers(diagram); + const diagram = d3.select(`[id='${id}']`); + insertMarkers(diagram); // // Layout graph, Create a new directed graph - // const g = new graphlib.Graph({ - // multigraph: true - // }); + const graph = new graphlib.Graph({ + multigraph: false + }); // // Set an object for the graph label - // g.setGraph({ - // isMultiGraph: true - // }); + graph.setGraph({ + isMultiGraph: false + }); // // Default to assigning a new object as a label for each new edge. - // g.setDefaultEdgeLabel(function() { - // return {}; - // }); + graph.setDefaultEdgeLabel(function() { + return {}; + }); - // const classes = classDb.getClasses(); - // const keys = Object.keys(classes); - // total = keys.length; - // for (let i = 0; i < keys.length; i++) { - // const classDef = classes[keys[i]]; - // const node = drawClass(diagram, classDef); - // // Add nodes to the graph. The first argument is the node id. The second is - // // metadata about the node. In this case we're going to add labels to each of - // // our nodes. - // g.setNode(node.id, node); - // logger.info('Org height: ' + node.height); - // } + // const states = stateDb.getStates(); + const states = { + start1: { + id: 'start1', + type: 'start' + }, + state1: { + id: 'state1', + type: 'default' + }, + exit: { + id: 'exit1', + type: 'end' + } + }; + const keys = Object.keys(states); + total = keys.length; + for (let i = 0; i < keys.length; i++) { + const stateDef = states[keys[i]]; + const node = drawState(diagram, stateDef); + // Add nodes to the graph. The first argument is the node id. The second is + // metadata about the node. In this case we're going to add labels to each of + // our nodes. + graph.setNode(node.id, node); + logger.info('Org height: ' + node.height); + } - // const relations = classDb.getRelations(); - // relations.forEach(function(relation) { - // logger.info( - // 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation) - // ); - // g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { - // relation: relation - // }); - // }); - // dagre.layout(g); - // g.nodes().forEach(function(v) { - // if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') { - // logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v))); - // d3.select('#' + v).attr( - // 'transform', - // 'translate(' + - // (g.node(v).x - g.node(v).width / 2) + - // ',' + - // (g.node(v).y - g.node(v).height / 2) + - // ' )' - // ); - // } - // }); - // g.edges().forEach(function(e) { - // if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') { - // logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e))); - // drawEdge(diagram, g.edge(e), g.edge(e).relation); - // } - // }); + // const relations = stateDb.getRelations(); + const relations = [{ id1: 'start1', id2: 'state1' }, { id1: 'state1', id2: 'exit1' }]; + relations.forEach(function(relation) { + logger.info( + 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation) + ); + graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { + relation: relation + }); + }); + dagre.layout(graph); + graph.nodes().forEach(function(v) { + if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') { + logger.debug('Node ' + v + ': ' + JSON.stringify(graph.node(v))); + d3.select('#' + v).attr( + 'transform', + 'translate(' + + (graph.node(v).x - graph.node(v).width / 2) + + ',' + + (graph.node(v).y - graph.node(v).height / 2) + + ' )' + ); + } + }); + graph.edges().forEach(function(e) { + if (typeof e !== 'undefined' && typeof graph.edge(e) !== 'undefined') { + logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e))); + drawEdge(diagram, graph.edge(e), graph.edge(e).relation); + } + }); - // diagram.attr('height', '100%'); - // diagram.attr('width', '100%'); - // diagram.attr('viewBox', '0 0 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20)); + diagram.attr('height', '100%'); + diagram.attr('width', '100%'); + diagram.attr('viewBox', '0 0 ' + (graph.graph().width + 20) + ' ' + (graph.graph().height + 20)); }; export default { diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index e6305d727..9fa0901a4 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -28,6 +28,9 @@ import ganttDb from './diagrams/gantt/ganttDb'; import classRenderer from './diagrams/class/classRenderer'; import classParser from './diagrams/class/parser/classDiagram'; import classDb from './diagrams/class/classDb'; +import stateRenderer from './diagrams/state/stateRenderer'; +import stateParser from './diagrams/state/parser/stateDiagram'; +import stateDb from './diagrams/state/stateDb'; import gitGraphRenderer from './diagrams/git/gitGraphRenderer'; import gitGraphParser from './diagrams/git/parser/gitGraph'; import gitGraphAst from './diagrams/git/gitGraphAst'; @@ -332,6 +335,10 @@ function parse(text) { parser = classParser; parser.parser.yy = classDb; break; + case 'state': + parser = stateParser; + parser.parser.yy = stateDb; + break; case 'info': logger.debug('info info info'); console.warn('In API', pkg.version); @@ -522,6 +529,11 @@ const render = function(id, txt, cb, container) { classRenderer.setConf(config.class); classRenderer.draw(txt, id); break; + case 'state': + // config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute; + stateRenderer.setConf(config.state); + stateRenderer.draw(txt, id); + break; case 'info': config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute; infoRenderer.setConf(config.class); diff --git a/src/utils.js b/src/utils.js index 357865fbf..080c54c96 100644 --- a/src/utils.js +++ b/src/utils.js @@ -33,6 +33,10 @@ export const detectType = function(text) { return 'class'; } + if (text.match(/^\s*stateDiagram/)) { + return 'state'; + } + if (text.match(/^\s*gitGraph/)) { return 'git'; } From 13baa43081d9c664dc57b1eae18e8eb34d95ec82 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 25 Sep 2019 21:29:32 +0200 Subject: [PATCH 10/17] #945 Rendering from diagram data --- src/diagrams/state/parser/stateDiagram.jison | 26 ++------------ src/diagrams/state/stateDb.js | 38 +++++++++++++++----- src/diagrams/state/stateDiagram.spec.js | 30 ++++++++++++++++ src/diagrams/state/stateRenderer.js | 19 ++-------- 4 files changed, 64 insertions(+), 49 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index 0e73cee4a..d48c2ed66 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -70,31 +70,11 @@ "stateDiagram"\s+ { console.log('Got state diagram', yytext,'#');return 'SD'; } "hide empty description" { console.log('HIDE_EMPTY', yytext,'#');return 'HIDE_EMPTY'; } -// "participant" { this.begin('ID'); return 'participant'; } -// [^\->:\n,;]+?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } -// "as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; } -// (?:) { this.popState(); this.popState(); return 'NL'; } -// "<>" { this.begin('LINE'); return 'else'; } -// "<>" { this.begin('LINE'); return 'par'; } -// "and" { this.begin('LINE'); return 'and'; } -// [^#\n;]* { this.popState(); return 'restOfLine'; } -// "end" return 'end'; "[*]" { console.log('EDGE_STATE=',yytext); return 'EDGE_STATE';} [^:\n\s\-\{]+ { console.log('=>ID=',yytext); return 'ID';} \s*":"[^\+\->:\n,;]+ { yytext = yytext.trim(); console.log('Descr = ', yytext); return 'DESCR'; } -// "over" return 'over'; -// "note" return 'note'; -// "activate" { this.begin('ID'); return 'activate'; } -// "deactivate" { this.begin('ID'); return 'deactivate'; } -// "title" return 'title'; -// "stateDiagram" return 'SD'; -// "," return ','; -// ";" return 'NL'; -// [^\+\->:\n,;]+ { yytext = yytext.trim(); return 'ACTOR'; } "-->" return '-->'; "--" return 'CONCURRENT'; -// "--" return '--'; -// ":"[^#\n;]+ return 'TXT'; <> return 'NL'; . return 'INVALID'; @@ -125,7 +105,7 @@ line statement : idStatement DESCR - | idStatement '-->' idStatement + | idStatement '-->' idStatement {yy.addRelation($1, $3);} | idStatement '-->' idStatement DESCR | HIDE_EMPTY | scale WIDTH @@ -141,8 +121,8 @@ statement ; idStatement - : ID - | EDGE_STATE + : ID {$$=$1;} + | EDGE_STATE {$$=$1;} ; notePosition diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index 8eb1a30e1..08928bfbb 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -3,6 +3,9 @@ import { logger } from '../../logger'; let relations = []; let states = {}; +let startCnt = 0; +let endCnt = 0; + /** * Function called by parser when a node definition has been found. * @param id @@ -10,11 +13,12 @@ let states = {}; * @param type * @param style */ -export const addState = function(id) { +export const addState = function(id, type) { if (typeof states[id] === 'undefined') { states[id] = { id: id, - descriptions: [] + descriptions: [], + type }; } }; @@ -27,19 +31,35 @@ export const clear = function() { export const getState = function(id) { return states[id]; }; -export const getstates = function() { +export const getStates = function() { return states; }; export const getRelations = function() { + // const relations1 = [{ id1: 'start1', id2: 'state1' }, { id1: 'state1', id2: 'exit1' }]; + // return relations; return relations; }; -export const addRelation = function(relation) { - logger.debug('Adding relation: ' + JSON.stringify(relation)); - addState(relation.id1); - addState(relation.id2); - relations.push(relation); +export const addRelation = function(_id1, _id2) { + let id1 = _id1; + let id2 = _id2; + let type1 = 'default'; + let type2 = 'default'; + if (_id1 === '[*]') { + startCnt++; + id1 = 'start' + startCnt; + type1 = 'start'; + } + if (_id2 === '[*]') { + endCnt++; + id2 = 'end' + startCnt; + type2 = 'end'; + } + console.log(id1, id2); + addState(id1, type1); + addState(id2, type2); + relations.push({ id1, id2 }); }; export const addMember = function(className, member) { @@ -83,7 +103,7 @@ export default { addState, clear, getState, - getstates, + getStates, getRelations, addRelation, addMember, diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index 58399bfdf..45555d4b4 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -8,6 +8,36 @@ describe('state diagram, ', function() { parser.yy = stateDb; }); + fit('super simple', function() { + const str = ` + stateDiagram + [*] --> State1 + State1 --> [*] + `; + + parser.parse(str); + expect(stateDb.getRelations()).toEqual([ + { id1: 'start1', id2: 'State1' }, + { id1: 'State1', id2: 'end1' } + ]); + expect(stateDb.getStates()).toEqual({ + State1: { + id: 'State1', + type: 'default', + descriptions: [] + }, + end1: { + id: 'end1', + type: 'end', + descriptions: [] + }, + start1: { + id: 'start1', + type: 'start', + descriptions: [] + } + }); + }); it('simple', function() { const str = `stateDiagram\n State1 : this is another string diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index c40e902f8..e6c1d2426 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -406,21 +406,7 @@ export const draw = function(text, id) { return {}; }); - // const states = stateDb.getStates(); - const states = { - start1: { - id: 'start1', - type: 'start' - }, - state1: { - id: 'state1', - type: 'default' - }, - exit: { - id: 'exit1', - type: 'end' - } - }; + const states = stateDb.getStates(); const keys = Object.keys(states); total = keys.length; for (let i = 0; i < keys.length; i++) { @@ -433,8 +419,7 @@ export const draw = function(text, id) { logger.info('Org height: ' + node.height); } - // const relations = stateDb.getRelations(); - const relations = [{ id1: 'start1', id2: 'state1' }, { id1: 'state1', id2: 'exit1' }]; + const relations = stateDb.getRelations(); relations.forEach(function(relation) { logger.info( 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation) From 3cffd1e3ed9dc9f7d737f0d388c6b8ca5bbc8d4e Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sat, 28 Sep 2019 13:31:10 +0200 Subject: [PATCH 11/17] #945 Rendering of labels and new label positioning algorithm --- .../rendering/stateDiagram.spec.js | 29 ++++++++ src/diagrams/state/parser/stateDiagram.jison | 2 +- src/diagrams/state/stateDb.js | 6 +- src/diagrams/state/stateDiagram.spec.js | 18 +++++ src/diagrams/state/stateRenderer.js | 69 ++++++++++++------- src/utils.js | 50 +++++++++++++- 6 files changed, 143 insertions(+), 31 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index a696be5de..6733c4ca4 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -13,4 +13,33 @@ describe('State diagram', () => { ); cy.get('svg'); }); + it('should render a simple state diagrams', () => { + imgSnapshotTest( + ` + stateDiagram + [*] --> State1 + State1 --> State2 + State1 --> State3 + State1 --> [*] + `, + { logLevel: 0 } + ); + cy.get('svg'); + }); + it('should render a simple state diagrams with labels', () => { + imgSnapshotTest( + ` + stateDiagram + [*] --> State1 + State1 --> State2 : Transition 1 + State1 --> State3 : Transition 2 + State1 --> State4 : Transition 3 + State1 --> State5 : Transition 4 + State2 --> State3 : Transition 5 + State1 --> [*] + `, + { logLevel: 0 } + ); + cy.get('svg'); + }); }); diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index d48c2ed66..0659258c6 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -106,7 +106,7 @@ line statement : idStatement DESCR | idStatement '-->' idStatement {yy.addRelation($1, $3);} - | idStatement '-->' idStatement DESCR + | idStatement '-->' idStatement DESCR {yy.addRelation($1, $3, $4.substr(1).trim());} | HIDE_EMPTY | scale WIDTH | COMPOSIT_STATE diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index 08928bfbb..d3db25e20 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -41,7 +41,7 @@ export const getRelations = function() { return relations; }; -export const addRelation = function(_id1, _id2) { +export const addRelation = function(_id1, _id2, title) { let id1 = _id1; let id2 = _id2; let type1 = 'default'; @@ -56,10 +56,10 @@ export const addRelation = function(_id1, _id2) { id2 = 'end' + startCnt; type2 = 'end'; } - console.log(id1, id2); + console.log(id1, id2, title); addState(id1, type1); addState(id2, type2); - relations.push({ id1, id2 }); + relations.push({ id1, id2, title }); }; export const addMember = function(className, member) { diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index 45555d4b4..d8918edd7 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -304,6 +304,24 @@ describe('state diagram, ', function() { note right of NotShooting : This is a note on a composite state `; + parser.parse(str); + }); + xit('should handle if statements', function() { + const str = `stateDiagram\n + [*] --> "Order Submitted" + if "Payment Accepted" then + -->[yes] "Pack products" + --> "Send parcel" + -right-> (*) + else + ->[no] "Send error message" + -->[Cancel Order] [*] + endif + } + + note right of NotShooting : This is a note on a composite state + `; + parser.parse(str); }); }); diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index e6c1d2426..be5dd1ea8 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -4,6 +4,7 @@ import graphlib from 'graphlibrary'; import { logger } from '../../logger'; import stateDb from './stateDb'; import { parser } from './parser/stateDiagram'; +import utils from '../../utils'; parser.yy = stateDb; @@ -136,7 +137,7 @@ const insertMarkers = function(elem) { .attr('markerHeight', 28) .attr('orient', 'auto') .append('path') - .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); + .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z'); }; const drawStart = function(elem, stateDef) { logger.info('Rendering class ' + stateDef); @@ -285,48 +286,67 @@ const drawEdge = function(elem, path, relation) { url = url.replace(/\)/g, '\\)'); } - svgPath.attr( - 'marker-start', - 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'Start' + ')' - ); + // svgPath.attr( + // 'marker-start', + // 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'Start' + ')' + // ); svgPath.attr( 'marker-end', 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')' ); - let x, y; - const l = path.points.length; - if (l % 2 !== 0 && l > 1) { - const p1 = path.points[Math.floor(l / 2)]; - const p2 = path.points[Math.ceil(l / 2)]; - x = (p1.x + p2.x) / 2; - y = (p1.y + p2.y) / 2; - } else { - const p = path.points[Math.floor(l / 2)]; - x = p.x; - y = p.y; - } + // Figure ou where to put the label given the points + // let x, y; + // const l = path.points.length; + // if (l % 2 !== 0 && l > 1) { + // const p1 = path.points[Math.floor(l / 2)]; + // const p2 = path.points[Math.ceil(l / 2)]; + // x = (p1.x + p2.x) / 2; + // y = (p1.y + p2.y) / 2; + // } else { + // const p = path.points[Math.floor(l / 2)]; + // x = p.x; + // y = p.y; + // } + + // console.log('calcLabelPosition', utils); if (typeof relation.title !== 'undefined') { const g = elem.append('g').attr('class', 'classLabel'); const label = g .append('text') .attr('class', 'label') - .attr('x', x) - .attr('y', y) .attr('fill', 'red') .attr('text-anchor', 'middle') .text(relation.title); - window.label = label; - const bounds = label.node().getBBox(); + const { x, y } = utils.calcLabelPosition(path.points); + label.attr('x', x).attr('y', y); + const bounds = label.node().getBBox(); g.insert('rect', ':first-child') .attr('class', 'box') .attr('x', bounds.x - conf.padding / 2) .attr('y', bounds.y - conf.padding / 2) .attr('width', bounds.width + conf.padding) .attr('height', bounds.height + conf.padding); + + // Debug points + // path.points.forEach(point => { + // g.append('circle') + // .style('stroke', 'red') + // .style('fill', 'red') + // .attr('r', 1) + // .attr('cx', point.x) + // .attr('cy', point.y); + // }); + + // g.append('circle') + // .style('stroke', 'blue') + // .style('fill', 'blue') + // .attr('r', 1) + // .attr('cx', x) + // .attr('cy', y); } edgeCount++; @@ -338,7 +358,7 @@ const drawEdge = function(elem, path, relation) { * @param {*} stateDef */ const drawState = function(elem, stateDef) { - logger.info('Rendering class ' + stateDef); + // logger.info('Rendering class ' + stateDef); const addTspan = function(textEl, txt, isFirst) { const tSpan = textEl @@ -416,14 +436,11 @@ export const draw = function(text, id) { // metadata about the node. In this case we're going to add labels to each of // our nodes. graph.setNode(node.id, node); - logger.info('Org height: ' + node.height); + // logger.info('Org height: ' + node.height); } const relations = stateDb.getRelations(); relations.forEach(function(relation) { - logger.info( - 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation) - ); graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { relation: relation }); diff --git a/src/utils.js b/src/utils.js index 080c54c96..2d11e7ef5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -73,8 +73,56 @@ export const interpolateToCurve = (interpolate, defaultCurve) => { return d3[curveName] || defaultCurve; }; +const distance = (p1, p2) => + p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0; + +const traverseEdge = points => { + let prevPoint; + let totalDistance = 0; + + points.forEach(point => { + totalDistance += distance(point, prevPoint); + prevPoint = point; + }); + + // Traverse half of total distance along points + const distanceToLabel = totalDistance / 2; + + let remainingDistance = distanceToLabel; + let center; + prevPoint = undefined; + points.forEach(point => { + if (prevPoint && !center) { + const vectorDistance = distance(point, prevPoint); + if (vectorDistance < remainingDistance) { + remainingDistance -= vectorDistance; + } else { + // The point is remainingDistance from prevPoint in the vector between prevPoint and point + // Calculate the coordinates + const distanceRatio = remainingDistance / vectorDistance; + if (distanceRatio <= 0) center = prevPoint; + if (distanceRatio >= 1) center = { x: point.x, y: point.y }; + if (distanceRatio > 0 && distanceRatio < 1) { + center = { + x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, + y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y + }; + } + } + } + prevPoint = point; + }); + return center; +}; + +const calcLabelPosition = points => { + const p = traverseEdge(points); + return p; +}; + export default { detectType, isSubstringInArray, - interpolateToCurve + interpolateToCurve, + calcLabelPosition }; From 94afcfb6f954cf77fa9cf0ac1a8834d3baed2d14 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 29 Sep 2019 15:50:43 +0200 Subject: [PATCH 12/17] #945 Rendering of state descriptions --- .../rendering/stateDiagram.spec.js | 12 +++ src/diagrams/state/parser/stateDiagram.jison | 4 +- src/diagrams/state/stateDb.js | 17 ++-- src/diagrams/state/stateRenderer.js | 91 ++++++++++++------- 4 files changed, 82 insertions(+), 42 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index 6733c4ca4..6ae7cbe4e 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -42,4 +42,16 @@ describe('State diagram', () => { ); cy.get('svg'); }); + it('should render state descriptions', () => { + imgSnapshotTest( + ` + stateDiagram + state "Long state description" as XState1 + state "Another Long state description" as XState2 + XState2 : New line + `, + { logLevel: 0 } + ); + cy.get('svg'); + }); }); diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index 0659258c6..c54ccd357 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -104,14 +104,14 @@ line ; statement - : idStatement DESCR + : idStatement DESCR {yy.addState($1, 'default');yy.addDescription($1, $2.trim());} | idStatement '-->' idStatement {yy.addRelation($1, $3);} | idStatement '-->' idStatement DESCR {yy.addRelation($1, $3, $4.substr(1).trim());} | HIDE_EMPTY | scale WIDTH | COMPOSIT_STATE | COMPOSIT_STATE STRUCT_START document STRUCT_STOP - | STATE_DESCR AS ID + | STATE_DESCR AS ID {yy.addState($3, 'default');yy.addDescription($3, $1);} | STATE_DESCR AS ID STRUCT_START document STRUCT_STOP | FORK | JOIN diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index d3db25e20..5a24f5680 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -62,15 +62,14 @@ export const addRelation = function(_id1, _id2, title) { relations.push({ id1, id2, title }); }; -export const addMember = function(className, member) { - const theState = states[className]; - if (typeof member === 'string') { - if (member.substr(-1) === ')') { - theState.methods.push(member); - } else { - theState.members.push(member); - } +export const addDescription = function(id, _descr) { + const theState = states[id]; + let descr = _descr; + if (descr[0] === ':') { + descr = descr.substr(1).trim(); } + + theState.descriptions.push(descr); }; export const addMembers = function(className, MembersArr) { @@ -106,7 +105,7 @@ export default { getStates, getRelations, addRelation, - addMember, + addDescription, addMembers, cleanupLabel, lineType, diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index be5dd1ea8..800465845 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -222,6 +222,63 @@ const drawSimpleState = (g, stateDef) => { return state; }; +/** + * Draws a state with descriptions + * @param {*} g + * @param {*} stateDef + */ +const drawDescrState = (g, stateDef) => { + const addTspan = function(textEl, txt, isFirst) { + const tSpan = textEl + .append('tspan') + .attr('x', 2 * conf.padding) + .text(txt); + if (!isFirst) { + tSpan.attr('dy', conf.textHeight); + } + }; + const title = g + .append('text') + .attr('x', 2 * conf.padding) + .attr('y', conf.textHeight + 1.5 * conf.padding) + .attr('font-size', 24) + .attr('class', 'state-title') + .text(stateDef.id); + + const titleHeight = title.node().getBBox().height; + + const description = g + .append('text') // text label for the x axis + .attr('x', conf.padding) + .attr('y', titleHeight + conf.padding * 0.2 + conf.dividerMargin + conf.textHeight) + .attr('fill', 'white') + .attr('class', 'state-description'); + + let isFirst = true; + stateDef.descriptions.forEach(function(descr) { + addTspan(description, descr, isFirst); + isFirst = false; + }); + + const descrLine = g + .append('line') // text label for the x axis + .attr('x1', conf.padding) + .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) + .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2) + .attr('class', 'descr-divider'); + const descrBox = description.node().getBBox(); + descrLine.attr('x2', descrBox.width + 3 * conf.padding); + // const classBox = title.node().getBBox(); + + g.insert('rect', ':first-child') + .attr('x', conf.padding) + .attr('y', conf.padding) + .attr('width', descrBox.width + 2 * conf.padding) + .attr('height', descrBox.height + titleHeight + 2 * conf.padding) + .attr('rx', '5'); + + return g; +}; const drawEndState = g => { g.append('circle') .style('stroke', 'black') @@ -286,31 +343,11 @@ const drawEdge = function(elem, path, relation) { url = url.replace(/\)/g, '\\)'); } - // svgPath.attr( - // 'marker-start', - // 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'Start' + ')' - // ); svgPath.attr( 'marker-end', 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')' ); - // Figure ou where to put the label given the points - // let x, y; - // const l = path.points.length; - // if (l % 2 !== 0 && l > 1) { - // const p1 = path.points[Math.floor(l / 2)]; - // const p2 = path.points[Math.ceil(l / 2)]; - // x = (p1.x + p2.x) / 2; - // y = (p1.y + p2.y) / 2; - // } else { - // const p = path.points[Math.floor(l / 2)]; - // x = p.x; - // y = p.y; - // } - - // console.log('calcLabelPosition', utils); - if (typeof relation.title !== 'undefined') { const g = elem.append('g').attr('class', 'classLabel'); const label = g @@ -360,16 +397,6 @@ const drawEdge = function(elem, path, relation) { const drawState = function(elem, stateDef) { // logger.info('Rendering class ' + stateDef); - const addTspan = function(textEl, txt, isFirst) { - const tSpan = textEl - .append('tspan') - .attr('x', conf.padding) - .text(txt); - if (!isFirst) { - tSpan.attr('dy', conf.textHeight); - } - }; - const id = stateDef.id; const stateInfo = { id: id, @@ -385,7 +412,9 @@ const drawState = function(elem, stateDef) { if (stateDef.type === 'start') drawStartState(g); if (stateDef.type === 'end') drawEndState(g); - if (stateDef.type === 'default') drawSimpleState(g, stateDef); + if (stateDef.type === 'default' && stateDef.descriptions.length === 0) + drawSimpleState(g, stateDef); + if (stateDef.type === 'default' && stateDef.descriptions.length > 0) drawDescrState(g, stateDef); const stateBox = g.node().getBBox(); From ea359c0037f17aa628df5c9fb78ae4de6369efcd Mon Sep 17 00:00:00 2001 From: Harrison Ulrich Date: Wed, 2 Oct 2019 15:03:03 -0500 Subject: [PATCH 13/17] Removed reference to mermaid.min.css in usage docs #797 --- docs/usage.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index 66ed75d17..18ecdbead 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -79,7 +79,6 @@ If your application is taking resposibility for the diagram source security you -
From e7428afb3e15a0e818a89f428cbd205070d56b89 Mon Sep 17 00:00:00 2001 From: Nacho Date: Thu, 3 Oct 2019 09:35:22 -0400 Subject: [PATCH 14/17] Revert "Removed reference to mermaid.min.css in usage docs #797" --- docs/usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/usage.md b/docs/usage.md index 18ecdbead..66ed75d17 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -79,6 +79,7 @@ If your application is taking resposibility for the diagram source security you +
From 54dbbd154ce62646cd4b37a455c57fed787a7ac1 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Tue, 8 Oct 2019 21:42:49 +0200 Subject: [PATCH 15/17] #949 Added utility function for calculating cardinality position --- src/utils.js | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 2d11e7ef5..c03621749 100644 --- a/src/utils.js +++ b/src/utils.js @@ -120,9 +120,59 @@ const calcLabelPosition = points => { return p; }; +const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition) => { + let prevPoint; + let totalDistance = 0; + if (points[0] !== initialPosition) { + points = points.reverse(); + } + points.forEach(point => { + totalDistance += distance(point, prevPoint); + prevPoint = point; + }); + + // Traverse only 25 total distance along points to find cardinality point + const distanceToCardinalityPoint = 25; + + let remainingDistance = distanceToCardinalityPoint; + let center; + prevPoint = undefined; + points.forEach(point => { + if (prevPoint && !center) { + const vectorDistance = distance(point, prevPoint); + if (vectorDistance < remainingDistance) { + remainingDistance -= vectorDistance; + } else { + // The point is remainingDistance from prevPoint in the vector between prevPoint and point + // Calculate the coordinates + const distanceRatio = remainingDistance / vectorDistance; + if (distanceRatio <= 0) center = prevPoint; + if (distanceRatio >= 1) center = { x: point.x, y: point.y }; + if (distanceRatio > 0 && distanceRatio < 1) { + center = { + x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, + y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y + }; + } + } + } + prevPoint = point; + }); + // if relation is present (Arrows will be added), change cardinality point off-set distance (d) + let d = isRelationTypePresent ? 10 : 5; + //Calculate Angle for x and y axis + let angle = Math.atan2(points[0].y - center.y, points[0].x - center.x); + let cardinalityPosition = { x: 0, y: 0 }; + //Calculation cardinality position using angle, center point on the line/curve but pendicular and with offset-distance + cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2; + cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2; + return cardinalityPosition; +}; + export default { detectType, isSubstringInArray, interpolateToCurve, - calcLabelPosition + calcLabelPosition, + calcCardinalityPosition }; From 35ddf9235f8bcfa18549cab703a3f046731b8e21 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Tue, 8 Oct 2019 21:43:58 +0200 Subject: [PATCH 16/17] #949 Added rendering of cardinality for class diagrams --- src/diagrams/class/classRenderer.js | 64 +++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/src/diagrams/class/classRenderer.js b/src/diagrams/class/classRenderer.js index 2d0825edc..60a88d2c8 100644 --- a/src/diagrams/class/classRenderer.js +++ b/src/diagrams/class/classRenderer.js @@ -3,6 +3,7 @@ import dagre from 'dagre-layout'; import graphlib from 'graphlibrary'; import { logger } from '../../logger'; import classDb from './classDb'; +import utils from '../../utils'; import { parser } from './parser/classDiagram'; parser.yy = classDb; @@ -198,15 +199,38 @@ const drawEdge = function(elem, path, relation) { let x, y; const l = path.points.length; + // Calculate Label position + let labalPosition = utils.calcLabelPosition(path.points); + x = labalPosition.x; + y = labalPosition.y; + + let p1_card_x, + p1_card_y, + p1_card_padd_x = conf.padding * 2, + p1_card_padd_y = conf.padding; + let p2_card_x, + p2_card_y, + p2_card_padd_x = conf.padding * 2, + p2_card_padd_y = -conf.padding / 2; if (l % 2 !== 0 && l > 1) { - const p1 = path.points[Math.floor(l / 2)]; - const p2 = path.points[Math.ceil(l / 2)]; - x = (p1.x + p2.x) / 2; - y = (p1.y + p2.y) / 2; - } else { - const p = path.points[Math.floor(l / 2)]; - x = p.x; - y = p.y; + let cardinality_1_point = utils.calcCardinalityPosition( + relation.relation.type1 !== 'none', + path.points, + path.points[0] + ); + let cardinality_2_point = utils.calcCardinalityPosition( + relation.relation.type2 !== 'none', + path.points, + path.points[l - 1] + ); + + logger.debug('cardinality_1_point ' + JSON.stringify(cardinality_1_point)); + logger.debug('cardinality_2_point ' + JSON.stringify(cardinality_2_point)); + + p1_card_x = cardinality_1_point.x; + p1_card_y = cardinality_1_point.y; + p2_card_x = cardinality_2_point.x; + p2_card_y = cardinality_2_point.y; } if (typeof relation.title !== 'undefined') { @@ -231,6 +255,30 @@ const drawEdge = function(elem, path, relation) { .attr('height', bounds.height + conf.padding); } + logger.info('Rendering relation ' + JSON.stringify(relation)); + if (typeof relation.relationTitle1 !== 'undefined' && relation.relationTitle1 !== 'none') { + const g = elem.append('g').attr('class', 'cardinality'); + const label = g + .append('text') + .attr('class', 'type1') + .attr('x', p1_card_x) + .attr('y', p1_card_y) + .attr('fill', 'black') + .attr('font-size', '6') + .text(relation.relationTitle1); + } + if (typeof relation.relationTitle2 !== 'undefined' && relation.relationTitle2 !== 'none') { + const g = elem.append('g').attr('class', 'cardinality'); + const label = g + .append('text') + .attr('class', 'type2') + .attr('x', p2_card_x) + .attr('y', p2_card_y) + .attr('fill', 'black') + .attr('font-size', '6') + .text(relation.relationTitle2); + } + edgeCount++; }; From aaac86fd9d68a67d94b5485b39ba9cec4609190b Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Tue, 8 Oct 2019 21:46:03 +0200 Subject: [PATCH 17/17] #949 Added cypress test case for class diagram cardinality support --- .../rendering/classDiagram.spec.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index 9c0b51581..d1f290436 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -30,4 +30,32 @@ describe('Class diagram', () => { ); cy.get('svg'); }); + it('should render a simple class diagrams with cardinality', () => { + imgSnapshotTest( + ` + classDiagram + Class01 "1" <|--|> "*" AveryLongClass : Cool + <<interface>> Class01 + Class03 "1" *-- "*" Class04 + Class05 "1" o-- "many" Class06 + Class07 "1" .. "*" Class08 + Class09 "1" --> "*" C2 : Where am i? + Class09 "*" --* "*" C3 + Class09 "1" --|> "1" Class07 + Class07 : equals() + Class07 : Object[] elementData + Class01 : size() + Class01 : int chimp + Class01 : int gorilla + Class08 "1" <--> "*" C2: Cool label + class Class10 { + <<service>> + int id + test() + } + `, + {} + ); + cy.get('svg'); + }); });