diff --git a/cypress/integration/rendering/flowchart.spec.js b/cypress/integration/rendering/flowchart.spec.js index 8bb4b8dff..4ea4cbcee 100644 --- a/cypress/integration/rendering/flowchart.spec.js +++ b/cypress/integration/rendering/flowchart.spec.js @@ -374,6 +374,7 @@ describe('Flowchart', () => { click B testClick "click test" classDef someclass fill:#f96; class A someclass; + class C someclass; `, { listUrl: false, @@ -396,6 +397,7 @@ describe('Flowchart', () => { { flowchart: { htmlLabels: false } } ); }); + it('16: Render Stadium shape', () => { imgSnapshotTest( ` graph TD @@ -408,10 +410,13 @@ describe('Flowchart', () => { click A "index.html#link-clicked" "link test" click B testClick "click test" classDef someclass fill:#f96; - class A someclass;`, + class A someclass; + class C someclass; + `, { flowchart: { htmlLabels: false } } ); }); + it('17: Render multiline texts', () => { imgSnapshotTest( `graph LR @@ -428,6 +433,7 @@ describe('Flowchart', () => { { flowchart: { htmlLabels: false } } ); }); + it('18: Chaining of nodes', () => { imgSnapshotTest( `graph LR @@ -436,6 +442,7 @@ describe('Flowchart', () => { { flowchart: { htmlLabels: false } } ); }); + it('19: Multiple nodes and chaining in one statement', () => { imgSnapshotTest( `graph LR @@ -444,6 +451,7 @@ describe('Flowchart', () => { { flowchart: { htmlLabels: false } } ); }); + it('20: Multiple nodes and chaining in one statement', () => { imgSnapshotTest( `graph TD @@ -453,6 +461,7 @@ describe('Flowchart', () => { { flowchart: { htmlLabels: false } } ); }); + it('21: Render cylindrical shape', () => { imgSnapshotTest( `graph LR @@ -474,6 +483,7 @@ describe('Flowchart', () => { { flowchart: { htmlLabels: false } } ); }); + it('22: Render a simple flowchart with nodeSpacing set to 100', () => { imgSnapshotTest( `graph TD @@ -487,6 +497,7 @@ describe('Flowchart', () => { { flowchart: { nodeSpacing: 50 } } ); }); + it('23: Render a simple flowchart with rankSpacing set to 100', () => { imgSnapshotTest( `graph TD @@ -500,4 +511,17 @@ describe('Flowchart', () => { { flowchart: { rankSpacing: '100' } } ); }); + + it('24: Keep node label text (if already defined) when a style is applied', () => { + imgSnapshotTest( + `graph LR + A(( )) -->|step 1| B(( )) + B(( )) -->|step 2| C(( )) + C(( )) -->|step 3| D(( )) + linkStyle 1 stroke:greenyellow,stroke-width:2px + style C fill:greenyellow,stroke:green,stroke-width:4px + `, + { flowchart: { htmlLabels: false } } + ); + }); }); diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index 0ab68713e..3e0bf1e1c 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -121,6 +121,18 @@ describe('State diagram', () => { {} ); }); + it('should handle multiline notes with different line breaks', () => { + imgSnapshotTest( + ` + stateDiagram + State1 + note right of State1 + Line1
Line2
Line3
Line4
Line5 + end note + `, + {} + ); + }); it('should render a states with descriptions including multi-line descriptions', () => { imgSnapshotTest( diff --git a/dist/index.html b/dist/index.html index 4acc24c4f..2505d8010 100644 --- a/dist/index.html +++ b/dist/index.html @@ -289,16 +289,17 @@ graph TB style 456ac9b0d15a8b7f1e71073221059886 fill:#f9f,stroke:#333,stroke-width:4px
-graph TD -A[Christmas] -->|Get money| B(Go shopping) -B --> C{{Let me think...
Do I want something for work,
something to spend every free second with,
or something to get around?}} -C -->|One| D[Laptop] -C -->|Two| E[iPhone] -C -->|Three| F[Car] -click A "index.html#link-clicked" "link test" -click B testClick "click test" -classDef someclass fill:#f96; -class A someclass; + graph TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{{Let me think...
Do I want something for work,
something to spend every free second with,
or something to get around?}} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[Car] + click A "index.html#link-clicked" "link test" + click B testClick "click test" + classDef someclass fill:#f96; + class A someclass; + class C someclass;
graph TD @@ -312,6 +313,7 @@ class A someclass; click B testClick "click test" classDef someclass fill:#f96; class A someclass; + class C someclass;
graph LR @@ -343,6 +345,14 @@ class A someclass; linkStyle 1 stroke:DarkGray,stroke-width:2px linkStyle 2 stroke:DarkGray,stroke-width:2px
+
+ graph LR + A(( )) -->|step 1| B(( )) + B(( )) -->|step 2| C(( )) + C(( )) -->|step 3| D(( )) + linkStyle 1 stroke:greenyellow,stroke-width:2px + style C fill:greenyellow,stroke:green,stroke-width:4px +

@@ -519,45 +529,46 @@ class Class10 { int id size() } - + -
- classDiagram - Class01~T~ <|-- AveryLongClass : Cool - <<interface>> Class01 - Class03~T~ "0" *-- "0..n" Class04 - Class05 "1" o-- "many" Class06 - Class07~T~ .. Class08 - Class09 "many" --> "1" C2 : Where am i? - Class09 "0" --* "1..n" C3 - Class09 --|> Class07 - Class07 : equals() - Class07 : Object[] elementData - Class01 : #size() - Class01 : -int chimp - Class01 : +int gorilla - Class08 <--> C2: Cool label - class Class10 { - <<service>> - int id - size() - } -
+
+ classDiagram + Class01~T~ <|-- AveryLongClass : Cool + <<interface>> Class01 + Class03~T~ "0" *-- "0..n" Class04 + Class05 "1" o-- "many" Class06 + Class07~T~ .. Class08 + Class09 "many" --> "1" C2 : Where am i? + Class09 "0" --* "1..n" C3 + Class09 --|> Class07 + Class07 : equals() + Class07 : Object[] elementData + Class01 : #size() + Class01 : -int chimp + Class01 : +int gorilla + Class08 <--> C2: Cool label + class Class10 { + <<service>> + int id + size() + } +
stateDiagram State1 -
+ -
-
- stateDiagram - [*] --> First - state First { - [*] --> second - second --> [*] - } -
+
+ +
+ stateDiagram + [*] --> First + state First { + [*] --> second + second --> [*] + } +
stateDiagram State1: The state with a note @@ -567,8 +578,14 @@ class Class10 { end note State1 --> State2 note left of State2 : This is the note to the left. -
- + +
+ stateDiagram + State1 + note right of State1 + Line1
Line2
Line3
Line4
Line5 + end note +
- +</script> -A summary of all options and their defaults is found [here](https://github.com/knsv/mermaid/blob/master/docs/mermaidAPI.md#mermaidapi-configuration-defaults). A description of each option follows below. + +A summary of all options and their defaults is found [here][2]. A description of each option follows below. ## theme @@ -333,3 +333,5 @@ mermaidAPI.initialize({ [1]: https://github.com/knsv/mermaid/blob/master/docs/mermaidAPI.md#render + +[2]: https://github.com/knsv/mermaid/blob/master/docs/mermaidAPI.md#mermaidapi-configuration-defaults diff --git a/package.json b/package.json index cb3f2461b..66310f158 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "8.4.5", + "version": "8.4.6", "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "main": "dist/mermaid.core.js", "keywords": [ diff --git a/src/diagrams/class/classDb.js b/src/diagrams/class/classDb.js index a20bdd075..dcb62164c 100644 --- a/src/diagrams/class/classDb.js +++ b/src/diagrams/class/classDb.js @@ -140,7 +140,7 @@ export const addMembers = function(className, members) { export const cleanupLabel = function(label) { if (label.substring(0, 1) === ':') { - return label.substr(2).trim(); + return label.substr(1).trim(); } else { return label.trim(); } diff --git a/src/diagrams/class/classDiagram.spec.js b/src/diagrams/class/classDiagram.spec.js index 8510b086d..4ac53ae10 100644 --- a/src/diagrams/class/classDiagram.spec.js +++ b/src/diagrams/class/classDiagram.spec.js @@ -67,6 +67,48 @@ describe('class diagram, ', function () { parser.parse(str); }); + it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function() { + const str = + 'classDiagram\n' + + 'class Dummy_Class~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Dummy_Class {\n' + + 'class Flight {\n' + + ' flightNumber : Integer\n' + + ' departureTime : Date\n' + + '}'; + let testPased =false; + try{ + parser.parse(str); + }catch (error){ + console.log(error.name); + testPased = true; + } + expect(testPased).toBe(true); + }); + + it('should break when EOF is encountered before closing the first `{` while defining generic class with brackets', function() { + const str = + 'classDiagram\n' + + 'class Dummy_Class~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Dummy_Class {\n'; + let testPased =false; + try{ + parser.parse(str); + }catch (error){ + console.log(error.name); + testPased = true; + } + expect(testPased).toBe(true); + }); + it('should handle generic class with brackets', function() { const str = 'classDiagram\n' + @@ -79,8 +121,6 @@ describe('class diagram, ', function () { ' flightNumber : Integer\n' + ' departureTime : Date\n' + '}'; - - parser.parse(str); }); it('should handle class definitions', function() { diff --git a/src/diagrams/class/classRenderer.js b/src/diagrams/class/classRenderer.js index 39a15a5bd..5e6208ac5 100644 --- a/src/diagrams/class/classRenderer.js +++ b/src/diagrams/class/classRenderer.js @@ -540,9 +540,14 @@ export const draw = function(text, id) { logger.info( 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation) ); - g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { - relation: relation - }); + g.setEdge( + getGraphId(relation.id1), + getGraphId(relation.id2), + { + relation: relation + }, + relation.title || 'DEFAULT' + ); }); dagre.layout(g); g.nodes().forEach(function(v) { diff --git a/src/diagrams/class/parser/classDiagram.jison b/src/diagrams/class/parser/classDiagram.jison index 7bd768138..12e9a2564 100644 --- a/src/diagrams/class/parser/classDiagram.jison +++ b/src/diagrams/class/parser/classDiagram.jison @@ -14,6 +14,8 @@ \s+ /* skip whitespace */ "classDiagram" return 'CLASS_DIAGRAM'; [\{] { this.begin("struct"); /*console.log('Starting struct');*/return 'STRUCT_START';} +<> return "EOF_IN_STRUCT"; +[\{] return "OPEN_IN_STRUCT"; \} { /*console.log('Ending struct');*/this.popState(); return 'STRUCT_STOP';}} [\n] /* nothing */ [^\{\}\n]* { /*console.log('lex-member: ' + yytext);*/ return "MEMBER";} diff --git a/src/diagrams/flowchart/flowDb.js b/src/diagrams/flowchart/flowDb.js index 4b78231c2..33c85a15d 100644 --- a/src/diagrams/flowchart/flowDb.js +++ b/src/diagrams/flowchart/flowDb.js @@ -52,7 +52,7 @@ export const addVertex = function(_id, text, type, style, classes) { vertices[id].text = txt; } else { - if (!vertices[id].text) { + if (typeof vertices[id].text === 'undefined') { vertices[id].text = _id; } } diff --git a/src/diagrams/flowchart/flowRenderer.js b/src/diagrams/flowchart/flowRenderer.js index 52031e5db..931608b45 100644 --- a/src/diagrams/flowchart/flowRenderer.js +++ b/src/diagrams/flowchart/flowRenderer.js @@ -88,7 +88,7 @@ export const addVertices = function(vert, g, svgId) { } else { const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - const rows = vertexText.split(//); + const rows = vertexText.split(//gi); for (let j = 0; j < rows.length; j++) { const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); @@ -237,7 +237,7 @@ export const addEdges = function(edges, g) { edgeData.label = '' + edge.text + ''; } else { edgeData.labelType = 'text'; - edgeData.label = edge.text.replace(//g, '\n'); + edgeData.label = edge.text.replace(//gi, '\n'); if (typeof edge.style === 'undefined') { edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none'; @@ -461,6 +461,7 @@ export const draw = function(text, id) { const node = d3.select('#' + id + ' [id="' + key + '"]'); if (node) { const link = document.createElementNS('http://www.w3.org/2000/svg', 'a'); + link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' ')); link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link); link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener'); diff --git a/src/diagrams/flowchart/flowRenderer.spec.js b/src/diagrams/flowchart/flowRenderer.spec.js index de8a6a485..d691840ed 100644 --- a/src/diagrams/flowchart/flowRenderer.spec.js +++ b/src/diagrams/flowchart/flowRenderer.spec.js @@ -55,6 +55,43 @@ describe('the flowchart renderer', function() { expect(addedNodes[0][1]).toHaveProperty('ry', expectedRadios); }); }); + + [ + 'Multi
Line', + 'Multi
Line', + 'Multi
Line', + 'MultiLine' + ].forEach(function(labelText) { + it('should handle multiline texts with different line breaks', function() { + const addedNodes = []; + const mockG = { + setNode: function(id, object) { + addedNodes.push([id, object]); + } + }; + addVertices( + { + v1: { + type: 'rect', + id: 'my-node-id', + classes: [], + styles: [], + text: 'Multi
Line' + } + }, + mockG, + 'svg-id' + ); + expect(addedNodes).toHaveLength(1); + expect(addedNodes[0][0]).toEqual('my-node-id'); + expect(addedNodes[0][1]).toHaveProperty('id', 'my-node-id'); + expect(addedNodes[0][1]).toHaveProperty('labelType', 'svg'); + expect(addedNodes[0][1].label).toBeDefined(); + expect(addedNodes[0][1].label).toBeDefined(); // node + expect(addedNodes[0][1].label.firstChild.innerHTML).toEqual('Multi'); // node, line 1 + expect(addedNodes[0][1].label.lastChild.innerHTML).toEqual('Line'); // node, line 2 + }); + }); }); [ @@ -109,9 +146,11 @@ describe('the flowchart renderer', function() { { text: 'Multi
Line' }, { text: 'Multi
Line' }, { text: 'Multi
Line' }, + { text: 'MultiLine' }, { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi
Line' }, { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi
Line' }, - { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi
Line' } + { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi
Line' }, + { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'MultiLine' } ], mockG, 'svg-id' diff --git a/src/diagrams/flowchart/parser/flow-style.spec.js b/src/diagrams/flowchart/parser/flow-style.spec.js index b9d678b54..db92660ae 100644 --- a/src/diagrams/flowchart/parser/flow-style.spec.js +++ b/src/diagrams/flowchart/parser/flow-style.spec.js @@ -86,6 +86,17 @@ describe('[Style] when parsing', () => { expect(vert['T'].styles[1]).toBe('border:1px solid red'); }); + it('should keep node label text (if already defined) when a style is applied', function() { + const res = flow.parser.parse('graph TD;A(( ));B((Test));C;style A background:#fff;style D border:1px solid red;'); + + const vert = flow.parser.yy.getVertices(); + + expect(vert['A'].text).toBe(''); + expect(vert['B'].text).toBe('Test'); + expect(vert['C'].text).toBe('C'); + expect(vert['D'].text).toBe('D'); + }); + it('should be possible to declare a class', function() { const res = flow.parser.parse( 'graph TD;classDef exClass background:#bbb,border:1px solid red;' diff --git a/src/diagrams/sequence/sequenceDb.js b/src/diagrams/sequence/sequenceDb.js index 050691a6c..cb873147e 100644 --- a/src/diagrams/sequence/sequenceDb.js +++ b/src/diagrams/sequence/sequenceDb.js @@ -16,6 +16,25 @@ export const addActor = function(id, name, description) { actors[id] = { name: name, description: description }; }; +const activationCount = part => { + let i = 0; + let count = 0; + for (i = 0; i < messages.length; i++) { + // console.warn(i, messages[i]); + if (messages[i].type === LINETYPE.ACTIVE_START) { + if (messages[i].from.actor === part) { + count++; + } + } + if (messages[i].type === LINETYPE.ACTIVE_END) { + if (messages[i].from.actor === part) { + count--; + } + } + } + return count; +}; + export const addMessage = function(idFrom, idTo, message, answer) { messages.push({ from: idFrom, to: idTo, message: message, answer: answer }); }; @@ -24,7 +43,25 @@ export const addSignal = function(idFrom, idTo, message, messageType) { logger.debug( 'Adding message from=' + idFrom + ' to=' + idTo + ' message=' + message + ' type=' + messageType ); + + if (messageType === LINETYPE.ACTIVE_END) { + const cnt = activationCount(idFrom.actor); + logger.debug('Adding message from=', messages, cnt); + if (cnt < 1) { + // Bail out as there is an activation signal from an inactive participant + var error = new Error('Trying to inactivate an inactive participant (' + idFrom.actor + ')'); + error.hash = { + text: '->>-', + token: '->>-', + line: '1', + loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, + expected: ["'ACTIVE_PARTICIPANT'"] + }; + throw error; + } + } messages.push({ from: idFrom, to: idTo, message: message, type: messageType }); + return true; }; export const getMessages = function() { diff --git a/src/diagrams/sequence/sequenceDiagram.spec.js b/src/diagrams/sequence/sequenceDiagram.spec.js index 2f1832865..5b32c4627 100644 --- a/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/src/diagrams/sequence/sequenceDiagram.spec.js @@ -214,6 +214,33 @@ describe('when parsing a sequenceDiagram', function() { expect(messages[7].type).toBe(parser.yy.LINETYPE.ACTIVE_END); expect(messages[7].from.actor).toBe('Carol'); }); + it('it should handle fail parsing when activating an inactive participant', function() { + const str = + `sequenceDiagram + participant user as End User + participant Server as Server + participant System as System + participant System2 as System2 + + user->>+Server: Test + user->>+Server: Test2 + user->>System: Test + Server->>-user: Test + Server->>-user: Test2 + + %% The following deactivation of Server will fail + Server->>-user: Test3`; + + let error = false; + try { + parser.parse(str); + } catch(e) { + console.log(e.hash); + error = true; + } + expect(error).toBe(true); + }); + it('it should handle comments in a sequenceDiagram', function() { const str = 'sequenceDiagram\n' + diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index f4711d493..d42228e41 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -280,7 +280,7 @@ const drawForkJoinState = (g, stateDef) => { export const drawText = function(elem, textData) { // Remove and ignore br:s - const nText = textData.text.replace(//gi, ' '); + const nText = textData.text.replace(//gi, ' '); const textElem = elem.append('text'); textElem.attr('x', textData.x); @@ -308,7 +308,7 @@ const _drawLongText = (_text, x, y, g) => { let text = _text.replace(/\r\n/g, '
'); text = text.replace(/\n/g, '
'); - const lines = text.split(//gi); + const lines = text.split(//gi); let tHeight = 1.25 * getConfig().state.noteMargin; for (const line of lines) { @@ -392,7 +392,7 @@ export const drawState = function(elem, stateDef) { }; const getRows = s => { - let str = s.replace(//gi, '#br#'); + let str = s.replace(//gi, '#br#'); str = str.replace(/\\n/g, '#br#'); return str.split('#br#'); }; diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index ee3152682..d609978ad 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -261,6 +261,16 @@ describe('state diagram, ', function() { parser.parse(str); }); + it('should handle multiline notes with different line breaks', function() { + const str = `stateDiagram + State1 + note right of State1 + Line1
Line2
Line3
Line4
Line5 + end note + `; + + parser.parse(str); + }); it('should handle floating notes', function() { const str = `stateDiagram foo: bar diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 4b152fe84..cec09a898 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -102,7 +102,7 @@ const getLabelWidth = text => { /* TODO: REMOVE DUPLICATION, SEE SHAPES */ const getRows = s => { if (!s) return 1; - let str = s.replace(//gi, '#br#'); + let str = s.replace(//gi, '#br#'); str = str.replace(/\\n/g, '#br#'); return str.split('#br#'); }; diff --git a/src/mermaid.js b/src/mermaid.js index 08bf9af19..0cf24e81f 100644 --- a/src/mermaid.js +++ b/src/mermaid.js @@ -98,7 +98,7 @@ const init = function() { txt = he .decode(txt) .trim() - .replace(/
/gi, '
'); + .replace(//gi, '
'); mermaidAPI.render( id, diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 975b38299..08c090261 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -52,14 +52,14 @@ for (const themeName of ['default', 'forest', 'dark', 'neutral']) { *
  * mermaid.initialize({
  *   flowchart:{
- *      htmlLabels: false
+ *     htmlLabels: false
  *   }
  * });
  * 
* * **Example 2:** *
- *  
+ * </script>
  * 
* A summary of all options and their defaults is found [here](https://github.com/knsv/mermaid/blob/master/docs/mermaidAPI.md#mermaidapi-configuration-defaults). A description of each option follows below. * @@ -142,6 +142,20 @@ const config = { */ htmlLabels: true, + /** + * Defines the spacing between nodes on the same level (meaning horizontal spacing for + * TB or BT graphs, and the vertical spacing for LR as well as RL graphs). + * **Default value 50**. + */ + nodeSpacing: 50, + + /** + * Defines the spacing between nodes on different levels (meaning vertical spacing for + * TB or BT graphs, and the horizontal spacing for LR as well as RL graphs). + * **Default value 50**. + */ + rankSpacing: 50, + /** * How mermaid renders curves for flowcharts. Possible values are * * basis diff --git a/src/utils.js b/src/utils.js index 1e17366d1..de7718773 100644 --- a/src/utils.js +++ b/src/utils.js @@ -84,8 +84,7 @@ export const sanitize = (text, config) => { htmlLabels = false; if (config.securityLevel !== 'loose' && htmlLabels) { // eslint-disable-line - txt = txt.replace(/
/g, '#br#'); - txt = txt.replace(//g, '#br#'); + txt = txt.replace(//gi, '#br#'); txt = txt.replace(//g, '>'); txt = txt.replace(/=/g, '='); txt = txt.replace(/#br#/g, '
');