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 +};