Merge pull request #10 from mermaid-js/develop

sync fork
This commit is contained in:
Justin Greywolf
2020-01-02 11:05:54 -08:00
committed by GitHub
29 changed files with 754 additions and 81 deletions

View File

@@ -70,7 +70,7 @@ describe('Interaction', () => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html'); expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
}); });
}); });
it('should handle a click on a task with a bound function', () => { it('should handle a click on a task with a bound function without args', () => {
const url = 'http://localhost:9000/click_security_loose.html'; const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024); cy.viewport(1440, 1024);
cy.visit(url); cy.visit(url);
@@ -78,9 +78,20 @@ describe('Interaction', () => {
.find('rect#cl2') .find('rect#cl2')
.click({ force: true }); .click({ force: true });
cy.get('.created-by-gant-click').should('have.text', 'Clicked By Gant'); cy.get('.created-by-gant-click').should('have.text', 'Clicked By Gant cl2');
}); });
it('should handle a click on a task with a bound function', () => { it('should handle a click on a task with a bound function with args', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('rect#cl3')
.click({ force: true });
cy.get('.created-by-gant-click').should('have.text', 'Clicked By Gant test1 test2 test3');
});
it('should handle a click on a task with a bound function without args', () => {
const url = 'http://localhost:9000/click_security_loose.html'; const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024); cy.viewport(1440, 1024);
cy.visit(url); cy.visit(url);
@@ -88,8 +99,19 @@ describe('Interaction', () => {
.find('text#cl2-text') .find('text#cl2-text')
.click({ force: true }); .click({ force: true });
cy.get('.created-by-gant-click').should('have.text', 'Clicked By Gant'); cy.get('.created-by-gant-click').should('have.text', 'Clicked By Gant cl2');
}); });
it('should handle a click on a task with a bound function with args ', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('text#cl3-text')
.click({ force: true });
cy.get('.created-by-gant-click').should('have.text', 'Clicked By Gant test1 test2 test3');
});
}); });
describe('Interaction - security level tight', () => { describe('Interaction - security level tight', () => {
@@ -170,7 +192,7 @@ describe('Interaction', () => {
.find('rect#cl2') .find('rect#cl2')
.click({ force: true }); .click({ force: true });
cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant'); cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant cl2');
}); });
it('should handle a click on a task with a bound function', () => { it('should handle a click on a task with a bound function', () => {
const url = 'http://localhost:9000/click_security_strict.html'; const url = 'http://localhost:9000/click_security_strict.html';
@@ -180,7 +202,7 @@ describe('Interaction', () => {
.find('text#cl2-text') .find('text#cl2-text')
.click({ force: true }); .click({ force: true });
cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant'); cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant cl2');
}); });
}); });
@@ -226,7 +248,7 @@ describe('Interaction', () => {
.find('rect#cl2') .find('rect#cl2')
.click({ force: true }); .click({ force: true });
cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant'); cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant cl2');
}); });
it('should handle a click on a task with a bound function', () => { it('should handle a click on a task with a bound function', () => {
const url = 'http://localhost:9000/click_security_strict.html'; const url = 'http://localhost:9000/click_security_strict.html';
@@ -236,7 +258,7 @@ describe('Interaction', () => {
.find('text#cl2-text') .find('text#cl2-text')
.click({ force: true }); .click({ force: true });
cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant'); cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant cl2');
}); });
}); });
}); });

View File

@@ -228,4 +228,48 @@ describe('Class diagram', () => {
); );
cy.get('svg'); cy.get('svg');
}); });
it('9: should render a simple class diagram with clickable link', () => {
imgSnapshotTest(
`
classDiagram
Class01~T~ <|-- AveryLongClass : Cool
Class03~T~ *-- Class04~T~
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class08 <--> C2: Cool label
class Class10~T~ {
&lt;&lt;service&gt;&gt;
int id
test()
}
link class01 "google.com" "A Tooltip"
`,
{}
);
cy.get('svg');
});
it('10: should render a simple class diagram with clickable callback', () => {
imgSnapshotTest(
`
classDiagram
Class01~T~ <|-- AveryLongClass : Cool
Class03~T~ *-- Class04~T~
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class08 <--> C2: Cool label
class Class10~T~ {
&lt;&lt;service&gt;&gt;
int id
test()
}
callback class01 "functionCall" "A Tooltip"
`,
{}
);
cy.get('svg');
});
}); });

View File

@@ -1,7 +1,7 @@
/* eslint-env jest */ /* eslint-env jest */
import { imgSnapshotTest } from '../../helpers/util'; import { imgSnapshotTest } from '../../helpers/util';
describe('Flowcart', () => { describe('Flowchart', () => {
it('1: should render a simple flowchart no htmlLabels', () => { it('1: should render a simple flowchart no htmlLabels', () => {
imgSnapshotTest( imgSnapshotTest(
`graph TD `graph TD
@@ -57,7 +57,7 @@ describe('Flowcart', () => {
); );
}); });
it('4: should style nodes via a class.', () => { it('5: should style nodes via a class.', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph TD graph TD
@@ -73,7 +73,7 @@ describe('Flowcart', () => {
); );
}); });
it('5: should render a flowchart full of circles', () => { it('6: should render a flowchart full of circles', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph LR graph LR
@@ -102,7 +102,7 @@ describe('Flowcart', () => {
); );
}); });
it('6: should render a flowchart full of icons', () => { it('7: should render a flowchart full of icons', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph TD graph TD
@@ -173,7 +173,7 @@ describe('Flowcart', () => {
); );
}); });
it('7: should render labels with numbers at the start', () => { it('8: should render labels with numbers at the start', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph TB;subgraph "number as labels";1;end; graph TB;subgraph "number as labels";1;end;
@@ -182,7 +182,7 @@ describe('Flowcart', () => {
); );
}); });
it('8: should render subgraphs', () => { it('9: should render subgraphs', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph TB graph TB
@@ -194,7 +194,7 @@ describe('Flowcart', () => {
); );
}); });
it('9: should render subgraphs with a title starting with a digit', () => { it('10: should render subgraphs with a title starting with a digit', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph TB graph TB
@@ -206,7 +206,7 @@ describe('Flowcart', () => {
); );
}); });
it('10: should render styled subgraphs', () => { it('11: should render styled subgraphs', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph TB graph TB
@@ -241,7 +241,7 @@ describe('Flowcart', () => {
); );
}); });
it('11: should render a flowchart with long names and class definitions', () => { it('12: should render a flowchart with long names and class definitions', () => {
imgSnapshotTest( imgSnapshotTest(
`graph LR `graph LR
sid-B3655226-6C29-4D00-B685-3D5C734DC7E1[" sid-B3655226-6C29-4D00-B685-3D5C734DC7E1["
@@ -343,7 +343,7 @@ describe('Flowcart', () => {
); );
}); });
it('12: should render color of styled nodes', () => { it('13: should render color of styled nodes', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph LR graph LR
@@ -361,7 +361,7 @@ describe('Flowcart', () => {
); );
}); });
it('13: should render hexagons', () => { it('14: should render hexagons', () => {
imgSnapshotTest( imgSnapshotTest(
` `
graph TD graph TD
@@ -383,7 +383,7 @@ describe('Flowcart', () => {
); );
}); });
it('14: should render a simple flowchart with comments', () => { it('15: should render a simple flowchart with comments', () => {
imgSnapshotTest( imgSnapshotTest(
`graph TD `graph TD
A[Christmas] -->|Get money| B(Go shopping) A[Christmas] -->|Get money| B(Go shopping)
@@ -396,7 +396,7 @@ describe('Flowcart', () => {
{ flowchart: { htmlLabels: false } } { flowchart: { htmlLabels: false } }
); );
}); });
it('15: Render Stadium shape', () => { it('16: Render Stadium shape', () => {
imgSnapshotTest( imgSnapshotTest(
` graph TD ` graph TD
A([stadium shape test]) A([stadium shape test])
@@ -412,7 +412,7 @@ describe('Flowcart', () => {
{ flowchart: { htmlLabels: false } } { flowchart: { htmlLabels: false } }
); );
}); });
it('16: Render Stadium shape', () => { it('17: Render multiline texts', () => {
imgSnapshotTest( imgSnapshotTest(
`graph LR `graph LR
A1[Multi<br>Line] -->|Multi<br>Line| B1(Multi<br>Line) A1[Multi<br>Line] -->|Multi<br>Line| B1(Multi<br>Line)
@@ -428,7 +428,7 @@ describe('Flowcart', () => {
{ flowchart: { htmlLabels: false } } { flowchart: { htmlLabels: false } }
); );
}); });
it('17: Chaining of nodes', () => { it('18: Chaining of nodes', () => {
imgSnapshotTest( imgSnapshotTest(
`graph LR `graph LR
a --> b --> c a --> b --> c
@@ -436,21 +436,42 @@ describe('Flowcart', () => {
{ flowchart: { htmlLabels: false } } { flowchart: { htmlLabels: false } }
); );
}); });
it('18: Multiple nodes and chaining in one statement', () => { it('19: Multiple nodes and chaining in one statement', () => {
imgSnapshotTest( imgSnapshotTest(
`graph LR `graph LR
a --> b c--> d a --> b & c--> d
`, `,
{ flowchart: { htmlLabels: false } } { flowchart: { htmlLabels: false } }
); );
}); });
it('19: Multiple nodes and chaining in one statement', () => { it('20: Multiple nodes and chaining in one statement', () => {
imgSnapshotTest( imgSnapshotTest(
`graph TD `graph TD
A[ h ] -- hello --> B[" test "]:::exClass C --> D; A[ h ] -- hello --> B[" test "]:::exClass & C --> D;
classDef exClass background:#bbb,border:1px solid red; classDef exClass background:#bbb,border:1px solid red;
`, `,
{ flowchart: { htmlLabels: false } } { flowchart: { htmlLabels: false } }
); );
}); });
it('21: Render cylindrical shape', () => {
imgSnapshotTest(
`graph LR
A[(cylindrical<br />shape<br />test)]
A -->|Get money| B1[(Go shopping 1)]
A -->|Get money| B2[(Go shopping 2)]
A -->|Get money| B3[(Go shopping 3)]
C[(Let me think...<br />Do I want something for work,<br />something to spend every free second with,<br />or something to get around?)]
B1 --> C
B2 --> C
B3 --> C
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;`,
{ flowchart: { htmlLabels: false } }
);
});
}); });

View File

@@ -54,4 +54,44 @@ describe('Sequencediagram', () => {
{} {}
); );
}); });
it('should render a gantt chart for issue #1060', () => {
imgSnapshotTest(
`
gantt
excludes weekdays 2017-01-10
title Projects Timeline
section asdf
specs :done, :ps, 2019-05-10, 50d
Plasma :pc, 2019-06-20, 60d
Rollup :or, 2019-08-20, 50d
section CEL
plasma-chamber :done, :pc, 2019-05-20, 60d
Plasma Implementation (Rust) :por, 2019-06-20, 120d
Predicates (Atomic Swap) :pred, 2019-07-20, 60d
section DEX 💰
History zkSNARK :hs, 2019-08-10, 40d
Exit :vs, after hs, 60d
PredicateSpec :ps, 2019-09-1, 20d
PlasmaIntegration :pi, after ps,40d
section Events 🏁
ETHBoston :done, :eb, 2019-09-08, 3d
DevCon :active, :dc, 2019-10-08, 3d
section Plasma Calls & updates ✨
OVM :ovm, 2019-07-12, 120d
Plasma call 26 :pc26, 2019-08-21, 1d
Plasma call 27 :pc27, 2019-09-03, 1d
Plasma call 28 :pc28, 2019-09-17, 1d
`,
{}
);
});
}); });

View File

@@ -39,12 +39,15 @@ context('Sequence diagram', () => {
participant 1 as multiline<br>using #lt;br#gt; participant 1 as multiline<br>using #lt;br#gt;
participant 2 as multiline<br/>using #lt;br/#gt; participant 2 as multiline<br/>using #lt;br/#gt;
participant 3 as multiline<br />using #lt;br /#gt; participant 3 as multiline<br />using #lt;br /#gt;
participant 4 as multiline<br \t/>using #lt;br \t/#gt;
1->>2: multiline<br>using #lt;br#gt; 1->>2: multiline<br>using #lt;br#gt;
note right of 2: multiline<br>using #lt;br#gt; note right of 2: multiline<br>using #lt;br#gt;
2->>3: multiline<br/>using #lt;br/#gt; 2->>3: multiline<br/>using #lt;br/#gt;
note right of 3: multiline<br/>using #lt;br/#gt; note right of 3: multiline<br/>using #lt;br/#gt;
3->>1: multiline<br />using #lt;br /#gt; 3->>4: multiline<br />using #lt;br /#gt;
note right of 1: multiline<br />using #lt;br /#gt; note right of 4: multiline<br />using #lt;br /#gt;
4->>1: multiline<br />using #lt;br /#gt;
note right of 1: multiline<br \t/>using #lt;br \t/#gt;
`, `,
{} {}
); );

View File

@@ -49,9 +49,11 @@
section Clickable section Clickable
Visit mermaidjs :active, cl1, 2014-01-07,2014-01-10 Visit mermaidjs :active, cl1, 2014-01-07,2014-01-10
Calling a Callback (look at the console log) :cl2, after cl1, 3d Calling a Callback (look at the console log) :cl2, after cl1, 3d
Calling a Callback with args :cl3, after cl1, 3d
click cl1 href "http://localhost:9000/webpackUsage.html" click cl1 href "http://localhost:9000/webpackUsage.html"
click cl2 call clickByGantt("test", test, test) click cl2 call clickByGantt()
click cl3 call clickByGantt("test1", test2, test3)
section Last section section Last section
Describe gantt syntax :after doc1, 3d Describe gantt syntax :after doc1, 3d
@@ -69,11 +71,14 @@
document.getElementsByTagName('body')[0].appendChild(div) document.getElementsByTagName('body')[0].appendChild(div)
} }
function clickByGantt(elemName) { function clickByGantt(arg1, arg2, arg3) {
const div = document.createElement('div') const div = document.createElement('div')
div.className = 'created-by-gant-click' div.className = 'created-by-gant-click'
div.style = 'padding: 20px; background: green; color: white;' div.style = 'padding: 20px; background: green; color: white;'
div.innerText = 'Clicked By Gant' div.innerText = 'Clicked By Gant'
if (arg1) div.innerText += ' ' + arg1;
if (arg2) div.innerText += ' ' + arg2;
if (arg3) div.innerText += ' ' + arg3;
document.getElementsByTagName('body')[0].appendChild(div) document.getElementsByTagName('body')[0].appendChild(div)
} }

View File

@@ -49,9 +49,11 @@
section Clickable section Clickable
Visit mermaidjs :active, cl1, 2014-01-07,2014-01-10 Visit mermaidjs :active, cl1, 2014-01-07,2014-01-10
Calling a Callback (look at the console log) :cl2, after cl1, 3d Calling a Callback (look at the console log) :cl2, after cl1, 3d
Calling a Callback with args :cl3, after cl1, 3d
click cl1 href "http://localhost:9000/webpackUsage.html" click cl1 href "http://localhost:9000/webpackUsage.html"
click cl2 call clickByGantt("test", test, test) click cl2 call clickByGantt()
click cl3 call clickByGantt("test1", test2, test3)
section Last section section Last section
Describe gantt syntax :after doc1, 3d Describe gantt syntax :after doc1, 3d
@@ -69,11 +71,14 @@
document.getElementsByTagName('body')[0].appendChild(div) document.getElementsByTagName('body')[0].appendChild(div)
} }
function clickByGantt(elemName) { function clickByGantt(arg1, arg2, arg3) {
const div = document.createElement('div') const div = document.createElement('div')
div.className = 'created-by-gant-click' div.className = 'created-by-gant-click'
div.style = 'padding: 20px; background: green; color: white;' div.style = 'padding: 20px; background: green; color: white;'
div.innerText = 'Clicked By Gant' div.innerText = 'Clicked By Gant'
if (arg1) div.innerText += ' ' + arg1;
if (arg2) div.innerText += ' ' + arg2;
if (arg3) div.innerText += ' ' + arg3;
document.getElementsByTagName('body')[0].appendChild(div) document.getElementsByTagName('body')[0].appendChild(div)
} }

23
dist/index.html vendored
View File

@@ -313,6 +313,24 @@ class A someclass;
classDef someclass fill:#f96; classDef someclass fill:#f96;
class A someclass; class A someclass;
</div> </div>
<div class="mermaid">
graph LR
A[(cylindrical<br />shape<br />test)]
A -->|Get money| B1[(Go shopping 1)]
A -->|Get money| B2[(Go shopping 2)]
A -->|Get money| B3[(Go shopping 3)]
C[(Let me think...<br />Do I want something for work,<br />something to spend every free second with,<br />or something to get around?)]
B1 --> C
B2 --> C
B3 --> C
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;
</div>
<div class="mermaid"> <div class="mermaid">
graph LR graph LR
A1[Multi<br>Line] -->|Multi<br>Line| B1(Multi<br>Line) A1[Multi<br>Line] -->|Multi<br>Line| B1(Multi<br>Line)
@@ -361,11 +379,14 @@ end
participant 1 as multiline<br>using #lt;br#gt; participant 1 as multiline<br>using #lt;br#gt;
participant 2 as multiline<br/>using #lt;br/#gt; participant 2 as multiline<br/>using #lt;br/#gt;
participant 3 as multiline<br />using #lt;br /#gt; participant 3 as multiline<br />using #lt;br /#gt;
participant 4 as multiline<br />using #lt;br /#gt;
1->>2: multiline<br>using #lt;br#gt; 1->>2: multiline<br>using #lt;br#gt;
note right of 2: multiline<br>using #lt;br#gt; note right of 2: multiline<br>using #lt;br#gt;
2->>3: multiline<br/>using #lt;br/#gt; 2->>3: multiline<br/>using #lt;br/#gt;
note right of 3: multiline<br/>using #lt;br/#gt; note right of 3: multiline<br/>using #lt;br/#gt;
3->>1: multiline<br />using #lt;br /#gt; 3->>4: multiline<br />using #lt;br /#gt;
note right of 4: multiline<br />using #lt;br /#gt;
4->>1: multiline<br />using #lt;br /#gt;
note right of 1: multiline<br />using #lt;br /#gt; note right of 1: multiline<br />using #lt;br /#gt;
</div> </div>

View File

@@ -89,6 +89,17 @@ graph LR
id1([This is the text in the box]) id1([This is the text in the box])
``` ```
### A node in a cylindrical shape
```
graph LR
id1[(Database)]
```
```mermaid
graph LR
id1[(Database)]
```
### A node in the form of a circle ### A node in the form of a circle
``` ```

View File

@@ -302,3 +302,48 @@ Param | Descriotion | Default value
--- | --- | --- --- | --- | ---
mirrorActor|Turns on/off the rendering of actors below the diagram as well as above it|false mirrorActor|Turns on/off the rendering of actors below the diagram as well as above it|false
bottomMarginAdj|Adjusts how far down the graph ended. Wide borders styles with css could generate unwantewd clipping which is why this config param exists.|1 bottomMarginAdj|Adjusts how far down the graph ended. Wide borders styles with css could generate unwantewd clipping which is why this config param exists.|1
## Interaction
It is possible to bind a click event to a task, the click can lead to either a javascript callback or to a link which will be opened in the current browser tab. **Note**: This functionality is disabled when using `securityLevel='strict'` and enabled when using `securityLevel='loose'`.
```
click taskId call callback(arguments)
click taskId href URL
```
* taskId is the id of the task
* callback is the name of a javascript function defined on the page displaying the graph, the function will be called with the taskId as the parameter if no other arguments are specified..
Beginners tip, a full example using interactive links in an html context:
```
<body>
<div class="mermaid">
gantt
dateFormat YYYY-MM-DD
section Clickable
Visit mermaidjs :active, cl1, 2014-01-07, 3d
Print arguments :cl2, after cl1, 3d
Print task :cl3, after cl2, 3d
click cl1 href "https://mermaidjs.github.io/"
click cl2 call printArguments("test1", "test2", test3)
click cl3 call printTask()
</div>
<script>
var printArguments = function(arg1, arg2, arg3) {
alert('printArguments called with arguments: ' + arg1 + ', ' + arg2 + ', ' + arg3);
}
var printTask = function(taskId) {
alert('taskId: ' + taskId);
}
var config = {
startOnLoad:true,
securityLevel:'loose',
};
mermaid.initialize(config);
</script>
</body>
```

View File

@@ -1,8 +1,17 @@
import * as d3 from 'd3';
import { sanitizeUrl } from '@braintree/sanitize-url';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { getConfig } from '../../config';
const MERMAID_DOM_ID_PREFIX = '';
const config = getConfig();
let relations = []; let relations = [];
let classes = {}; let classes = {};
let funs = [];
const splitClassNameAndType = function(id) { const splitClassNameAndType = function(id) {
let genericType = ''; let genericType = '';
let className = id; let className = id;
@@ -29,6 +38,7 @@ export const addClass = function(id) {
classes[classId.className] = { classes[classId.className] = {
id: classId.className, id: classId.className,
type: classId.type, type: classId.type,
cssClasses: [],
methods: [], methods: [],
members: [], members: [],
annotations: [] annotations: []
@@ -38,6 +48,8 @@ export const addClass = function(id) {
export const clear = function() { export const clear = function() {
relations = []; relations = [];
classes = {}; classes = {};
funs = [];
funs.push(setupToolTips);
}; };
export const getClass = function(id) { export const getClass = function(id) {
@@ -117,6 +129,91 @@ export const cleanupLabel = function(label) {
} }
}; };
/**
* Called by parser when a special node is found, e.g. a clickable element.
* @param ids Comma separated list of ids
* @param className Class to add
*/
export const setCssClass = function(ids, className) {
ids.split(',').forEach(function(_id) {
let id = _id;
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
if (typeof classes[id] !== 'undefined') {
classes[id].cssClasses.push(className);
}
});
};
/**
* Called by parser when a link is found. Adds the URL to the vertex data.
* @param ids Comma separated list of ids
* @param linkStr URL to create a link for
* @param tooltip Tooltip for the clickable element
*/
export const setLink = function(ids, linkStr, tooltip) {
ids.split(',').forEach(function(_id) {
let id = _id;
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
if (typeof classes[id] !== 'undefined') {
if (config.securityLevel !== 'loose') {
classes[id].link = sanitizeUrl(linkStr);
} else {
classes[id].link = linkStr;
}
if (tooltip) {
classes[id].tooltip = tooltip;
}
}
});
setCssClass(ids, 'clickable');
};
/**
* Called by parser when a click definition is found. Registers an event handler.
* @param ids Comma separated list of ids
* @param functionName Function to be called on click
* @param tooltip Tooltip for the clickable element
*/
export const setClickEvent = function(ids, functionName, tooltip) {
ids.split(',').forEach(function(id) {
setClickFunc(id, functionName, tooltip);
});
setCssClass(ids, 'clickable');
};
const setClickFunc = function(_id, functionName) {
let id = _id;
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
if (config.securityLevel !== 'loose') {
return;
}
if (typeof functionName === 'undefined') {
return;
}
if (typeof classes[id] !== 'undefined') {
funs.push(function() {
const elem = document.querySelector(`[id="${id}"]`);
if (elem !== null) {
elem.setAttribute('title', classes[id].tooltip);
elem.addEventListener(
'click',
function() {
window[functionName](id);
},
false
);
}
});
}
};
export const bindFunctions = function(element) {
funs.forEach(function(fun) {
fun(element);
});
};
export const lineType = { export const lineType = {
LINE: 0, LINE: 0,
DOTTED_LINE: 1 DOTTED_LINE: 1
@@ -129,8 +226,53 @@ export const relationType = {
DEPENDENCY: 3 DEPENDENCY: 3
}; };
const setupToolTips = function(element) {
let tooltipElem = d3.select('.mermaidTooltip');
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
tooltipElem = d3
.select('body')
.append('div')
.attr('class', 'mermaidTooltip')
.style('opacity', 0);
}
const svg = d3.select(element).select('svg');
const nodes = svg.selectAll('g.node');
nodes
.on('mouseover', function() {
const el = d3.select(this);
const title = el.attr('title');
// Dont try to draw a tooltip if no data is provided
if (title === null) {
return;
}
const rect = this.getBoundingClientRect();
tooltipElem
.transition()
.duration(200)
.style('opacity', '.9');
tooltipElem
.html(el.attr('title'))
.style('left', rect.left + (rect.right - rect.left) / 2 + 'px')
.style('top', rect.top - 14 + document.body.scrollTop + 'px');
el.classed('hover', true);
})
.on('mouseout', function() {
tooltipElem
.transition()
.duration(500)
.style('opacity', 0);
const el = d3.select(this);
el.classed('hover', false);
});
};
funs.push(setupToolTips);
export default { export default {
addClass, addClass,
bindFunctions,
clear, clear,
getClass, getClass,
getClasses, getClasses,
@@ -141,5 +283,8 @@ export default {
addMembers, addMembers,
cleanupLabel, cleanupLabel,
lineType, lineType,
relationType relationType,
setClickEvent,
setCssClass,
setLink
}; };

View File

@@ -250,6 +250,66 @@ describe('class diagram, ', function () {
parser.parse(str); parser.parse(str);
}); });
it('should handle click statement with link', function () {
const str =
'classDiagram\n' +
'class Class1 {\n' +
'%% Comment Class01 <|-- Class02\n' +
'int : test\n' +
'string : foo\n' +
'test()\n' +
'foo()\n' +
'}\n' +
'link Class01 "google.com" ';
parser.parse(str);
});
it('should handle click statement with link and tooltip', function () {
const str =
'classDiagram\n' +
'class Class1 {\n' +
'%% Comment Class01 <|-- Class02\n' +
'int : test\n' +
'string : foo\n' +
'test()\n' +
'foo()\n' +
'}\n' +
'link Class01 "google.com" "A Tooltip" ';
parser.parse(str);
});
it('should handle click statement with callback', function () {
const str =
'classDiagram\n' +
'class Class1 {\n' +
'%% Comment Class01 <|-- Class02\n' +
'int : test\n' +
'string : foo\n' +
'test()\n' +
'foo()\n' +
'}\n' +
'callback Class01 "functionCall" ';
parser.parse(str);
});
it('should handle click statement with callback and tooltip', function () {
const str =
'classDiagram\n' +
'class Class1 {\n' +
'%% Comment Class01 <|-- Class02\n' +
'int : test\n' +
'string : foo\n' +
'test()\n' +
'foo()\n' +
'}\n' +
'callback Class01 "functionCall" "A Tooltip" ';
parser.parse(str);
});
}); });
describe('when fetching data from a classDiagram graph it', function () { describe('when fetching data from a classDiagram graph it', function () {
@@ -464,5 +524,41 @@ describe('class diagram, ', function () {
expect(testClass.methods.length).toBe(1); expect(testClass.methods.length).toBe(1);
expect(testClass.methods[0]).toBe('someMethod()$'); expect(testClass.methods[0]).toBe('someMethod()$');
}); });
it('should associate link and css appropriately', function () {
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()\n' + 'link Class1 "google.com"';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.link).toBe('about:blank');//('google.com'); security needs to be set to 'loose' for this to work right
expect(testClass.cssClasses.length).toBe(1);
expect(testClass.cssClasses[0]).toBe('clickable');
});
it('should associate link with tooltip', function () {
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()\n' + 'link Class1 "google.com" "A tooltip"';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.link).toBe('about:blank');//('google.com'); security needs to be set to 'loose' for this to work right
expect(testClass.tooltip).toBe('A tooltip');
expect(testClass.cssClasses.length).toBe(1);
expect(testClass.cssClasses[0]).toBe('clickable');
});
it('should associate callback appropriately', function () {
spyOn(classDb, 'setClickEvent');
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()\n' + 'callback Class1 "functionCall"';
parser.parse(str);
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall', undefined);
});
it('should associate callback with tooltip', function () {
spyOn(classDb, 'setClickEvent');
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()\n' + 'callback Class1 "functionCall" "A tooltip"';
parser.parse(str);
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall', 'A tooltip');
});
}); });
}); });

View File

@@ -280,6 +280,11 @@ const drawEdge = function(elem, path, relation) {
const drawClass = function(elem, classDef) { const drawClass = function(elem, classDef) {
logger.info('Rendering class ' + classDef); logger.info('Rendering class ' + classDef);
let cssClassStr = 'classGroup ';
if (classDef.cssClasses.length > 0) {
cssClassStr = cssClassStr + classDef.cssClasses.join(' ');
}
const addTspan = function(textEl, txt, isFirst) { const addTspan = function(textEl, txt, isFirst) {
let displayText = txt; let displayText = txt;
let cssStyle = ''; let cssStyle = '';
@@ -326,13 +331,25 @@ const drawClass = function(elem, classDef) {
const g = elem const g = elem
.append('g') .append('g')
.attr('id', id) .attr('id', id)
.attr('class', 'classGroup'); .attr('class', cssClassStr);
// add title // add title
const title = g let title;
if (classDef.link) {
title = g
.append('svg:a')
.attr('xlink:href', classDef.link)
.attr('xlink:target', '_blank')
.attr('xlink:title', classDef.tooltip)
.append('text') .append('text')
.attr('y', conf.textHeight + conf.padding) .attr('y', conf.textHeight + conf.padding)
.attr('x', 0); .attr('x', 0);
} else {
title = g
.append('text')
.attr('y', conf.textHeight + conf.padding)
.attr('x', 0);
}
// add annotations // add annotations
let isFirst = true; let isFirst = true;
@@ -348,7 +365,6 @@ const drawClass = function(elem, classDef) {
classTitleString += '<' + classDef.type + '>'; classTitleString += '<' + classDef.type + '>';
} }
// add class title
const classTitle = title const classTitle = title
.append('tspan') .append('tspan')
.text(classTitleString) .text(classTitleString)
@@ -434,6 +450,7 @@ export const setConf = function(cnf) {
conf[key] = cnf[key]; conf[key] = cnf[key];
}); });
}; };
/** /**
* Draws a flowchart in the tag with id: id based on the graph definition in text. * Draws a flowchart in the tag with id: id based on the graph definition in text.
* @param text * @param text
@@ -470,10 +487,12 @@ export const draw = function(text, id) {
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const classDef = classes[keys[i]]; const classDef = classes[keys[i]];
const node = drawClass(diagram, classDef); const node = drawClass(diagram, classDef);
// Add nodes to the graph. The first argument is the node id. The second is // 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 // metadata about the node. In this case we're going to add labels to each of
// our nodes. // our nodes.
g.setNode(node.id, node); g.setNode(node.id, node);
logger.info('Org height: ' + node.height); logger.info('Org height: ' + node.height);
} }

View File

@@ -21,6 +21,9 @@
"class" return 'CLASS'; "class" return 'CLASS';
//"click" return 'CLICK';
"callback" return 'CALLBACK';
"link" return 'LINK';
"<<" return 'ANNOTATION_START'; "<<" return 'ANNOTATION_START';
">>" return 'ANNOTATION_END'; ">>" return 'ANNOTATION_END';
[~] this.begin("generic"); [~] this.begin("generic");
@@ -149,6 +152,7 @@ statement
| classStatement | classStatement
| methodStatement | methodStatement
| annotationStatement | annotationStatement
| clickStatement
; ;
classStatement classStatement
@@ -198,6 +202,13 @@ lineType
| DOTTED_LINE {$$=yy.lineType.DOTTED_LINE;} | DOTTED_LINE {$$=yy.lineType.DOTTED_LINE;}
; ;
clickStatement
: CALLBACK className STR {$$ = $1;yy.setClickEvent($2, $3, undefined);}
| CALLBACK className STR STR {$$ = $1;yy.setClickEvent($2, $3, $4);}
| LINK className STR {$$ = $1;yy.setLink($2, $3, undefined);}
| LINK className STR STR {$$ = $1;yy.setLink($2, $3, $4);}
;
commentToken : textToken | graphCodeTokens ; commentToken : textToken | graphCodeTokens ;
textToken : textNoTagsToken | TAGSTART | TAGEND | '==' | '--' | PCT | DEFAULT; textToken : textNoTagsToken | TAGSTART | TAGEND | '==' | '--' | PCT | DEFAULT;

View File

@@ -154,10 +154,74 @@ function stadium(parent, bbox, node) {
return shapeSvg; return shapeSvg;
} }
function cylinder(parent, bbox, node) {
const w = bbox.width;
const rx = w / 2;
const ry = rx / (2.5 + w / 50);
const h = bbox.height + ry;
const shape =
'M 0,' +
ry +
' a ' +
rx +
',' +
ry +
' 0,0,0 ' +
w +
' 0 a ' +
rx +
',' +
ry +
' 0,0,0 ' +
-w +
' 0 l 0,' +
h +
' a ' +
rx +
',' +
ry +
' 0,0,0 ' +
w +
' 0 l 0,' +
-h;
const shapeSvg = parent
.attr('label-offset-y', ry)
.insert('path', ':first-child')
.attr('d', shape)
.attr('transform', 'translate(' + -w / 2 + ',' + -(h / 2 + ry) + ')');
node.intersect = function(point) {
const pos = dagreD3.intersect.rect(node, point);
const x = pos.x - node.x;
if (
rx != 0 &&
(Math.abs(x) < node.width / 2 ||
(Math.abs(x) == node.width / 2 && Math.abs(pos.y - node.y) > node.height / 2 - ry))
) {
// ellipsis equation: x*x / a*a + y*y / b*b = 1
// solve for y to get adjustion value for pos.y
let y = ry * ry * (1 - (x * x) / (rx * rx));
if (y != 0) y = Math.sqrt(y);
y = ry - y;
if (point.y - node.y > 0) y = -y;
pos.y += y;
}
return pos;
};
return shapeSvg;
}
export function addToRender(render) { export function addToRender(render) {
render.shapes().question = question; render.shapes().question = question;
render.shapes().hexagon = hexagon; render.shapes().hexagon = hexagon;
render.shapes().stadium = stadium; render.shapes().stadium = stadium;
render.shapes().cylinder = cylinder;
// Add custom shape for box with inverted arrow on left side // Add custom shape for box with inverted arrow on left side
render.shapes().rect_left_inv_arrow = rect_left_inv_arrow; render.shapes().rect_left_inv_arrow = rect_left_inv_arrow;

View File

@@ -23,6 +23,23 @@ describe('flowchart shapes', function() {
}); });
}); });
// path-based shapes
[
['cylinder', useWidth, useHeight]
].forEach(function([shapeType, getW, getH]) {
it(`should add a ${shapeType} shape that renders a properly positioned path element`, function() {
const mockRender = MockRender();
const mockSvg = MockSvg();
addToRender(mockRender);
[[100, 100], [123, 45], [71, 300]].forEach(function([width, height]) {
const shape = mockRender.shapes()[shapeType](mockSvg, { width, height }, {});
expect(shape.__tag).toEqual('path');
expect(shape.__attrs).toHaveProperty('d');
});
});
});
// polygon-based shapes // polygon-based shapes
[ [
[ [

View File

@@ -608,7 +608,7 @@ const destructLink = (_str, _startStr) => {
let startInfo; let startInfo;
if (_startStr) { if (_startStr) {
startInfo = destructStartLink(_startStr); startInfo = destructStartLink(_startStr);
console.log(startInfo, info);
if (startInfo.stroke !== info.stroke) { if (startInfo.stroke !== info.stroke) {
return { type: 'INVALID', stroke: 'INVALID' }; return { type: 'INVALID', stroke: 'INVALID' };
} }

View File

@@ -104,15 +104,6 @@ export const addVertices = function(vert, g, svgId) {
vertexNode = svgLabel; vertexNode = svgLabel;
} }
// If the node has a link, we wrap it in a SVG link
if (vertex.link) {
const link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
link.appendChild(vertexNode);
vertexNode = link;
}
let radious = 0; let radious = 0;
let _shape = ''; let _shape = '';
// Set the shape based parameters // Set the shape based parameters
@@ -157,6 +148,9 @@ export const addVertices = function(vert, g, svgId) {
case 'stadium': case 'stadium':
_shape = 'stadium'; _shape = 'stadium';
break; break;
case 'cylinder':
_shape = 'cylinder';
break;
case 'group': case 'group':
_shape = 'rect'; _shape = 'rect';
break; break;
@@ -246,7 +240,7 @@ export const addEdges = function(edges, g) {
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>'; edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
} else { } else {
edgeData.labelType = 'text'; edgeData.labelType = 'text';
edgeData.label = edge.text.replace(/<br ?\/?>/g, '\n'); edgeData.label = edge.text.replace(/<br\s*\/?>/g, '\n');
if (typeof edge.style === 'undefined') { if (typeof edge.style === 'undefined') {
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none'; edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
@@ -494,6 +488,39 @@ export const draw = function(text, id) {
label.insertBefore(rect, label.firstChild); label.insertBefore(rect, label.firstChild);
} }
} }
// If node has a link, wrap it in an anchor SVG object.
const keys = Object.keys(vert);
keys.forEach(function(key) {
const vertex = vert[key];
if (vertex.link) {
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', 'href', vertex.link);
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
const linkNode = node.insert(function() {
return link;
}, ':first-child');
const shape = node.select('.label-container');
if (shape) {
linkNode.append(function() {
return shape.node();
});
}
const label = node.select('.label');
if (label) {
linkNode.append(function() {
return label.node();
});
}
}
}
});
}; };
export default { export default {

View File

@@ -23,6 +23,7 @@ describe('the flowchart renderer', function() {
['circle', 'circle'], ['circle', 'circle'],
['ellipse', 'ellipse'], ['ellipse', 'ellipse'],
['stadium', 'stadium'], ['stadium', 'stadium'],
['cylinder', 'cylinder'],
['group', 'rect'] ['group', 'rect']
].forEach(function([type, expectedShape, expectedRadios = 0]) { ].forEach(function([type, expectedShape, expectedRadios = 0]) {
it(`should add the correct shaped node to the graph for vertex type ${type}`, function() { it(`should add the correct shaped node to the graph for vertex type ${type}`, function() {

View File

@@ -37,7 +37,7 @@ describe('when parsing flowcharts', function() {
it('should handle chaining of vertices', function() { it('should handle chaining of vertices', function() {
const res = flow.parser.parse(` const res = flow.parser.parse(`
graph TD graph TD
A B --> C; A & B --> C;
`); `);
const vert = flow.parser.yy.getVertices(); const vert = flow.parser.yy.getVertices();
@@ -59,7 +59,7 @@ describe('when parsing flowcharts', function() {
it('should multiple vertices in link statement in the begining', function() { it('should multiple vertices in link statement in the begining', function() {
const res = flow.parser.parse(` const res = flow.parser.parse(`
graph TD graph TD
A-->B C; A-->B & C;
`); `);
const vert = flow.parser.yy.getVertices(); const vert = flow.parser.yy.getVertices();
@@ -81,7 +81,7 @@ describe('when parsing flowcharts', function() {
it('should multiple vertices in link statement at the end', function() { it('should multiple vertices in link statement at the end', function() {
const res = flow.parser.parse(` const res = flow.parser.parse(`
graph TD graph TD
A B--> C D; A & B--> C & D;
`); `);
const vert = flow.parser.yy.getVertices(); const vert = flow.parser.yy.getVertices();
@@ -112,7 +112,7 @@ describe('when parsing flowcharts', function() {
it('should handle chaining of vertices at both ends at once', function() { it('should handle chaining of vertices at both ends at once', function() {
const res = flow.parser.parse(` const res = flow.parser.parse(`
graph TD graph TD
A B--> C D; A & B--> C & D;
`); `);
const vert = flow.parser.yy.getVertices(); const vert = flow.parser.yy.getVertices();
@@ -140,10 +140,10 @@ describe('when parsing flowcharts', function() {
expect(edges[3].type).toBe('arrow'); expect(edges[3].type).toBe('arrow');
expect(edges[3].text).toBe(''); expect(edges[3].text).toBe('');
}); });
it('should handle chaining and multiple nodes in in link statement', function() { it('should handle chaining and multiple nodes in in link statement FVC ', function() {
const res = flow.parser.parse(` const res = flow.parser.parse(`
graph TD graph TD
A --> B C --> D; A --> B & B2 & C --> D2;
`); `);
const vert = flow.parser.yy.getVertices(); const vert = flow.parser.yy.getVertices();
@@ -151,30 +151,39 @@ describe('when parsing flowcharts', function() {
expect(vert['A'].id).toBe('A'); expect(vert['A'].id).toBe('A');
expect(vert['B'].id).toBe('B'); expect(vert['B'].id).toBe('B');
expect(vert['B2'].id).toBe('B2');
expect(vert['C'].id).toBe('C'); expect(vert['C'].id).toBe('C');
expect(vert['D'].id).toBe('D'); expect(vert['D2'].id).toBe('D2');
expect(edges.length).toBe(4); expect(edges.length).toBe(6);
expect(edges[0].start).toBe('A'); expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B'); expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe('arrow'); expect(edges[0].type).toBe('arrow');
expect(edges[0].text).toBe(''); expect(edges[0].text).toBe('');
expect(edges[1].start).toBe('A'); expect(edges[1].start).toBe('A');
expect(edges[1].end).toBe('C'); expect(edges[1].end).toBe('B2');
expect(edges[1].type).toBe('arrow'); expect(edges[1].type).toBe('arrow');
expect(edges[1].text).toBe(''); expect(edges[1].text).toBe('');
expect(edges[2].start).toBe('B'); expect(edges[2].start).toBe('A');
expect(edges[2].end).toBe('D'); expect(edges[2].end).toBe('C');
expect(edges[2].type).toBe('arrow'); expect(edges[2].type).toBe('arrow');
expect(edges[2].text).toBe(''); expect(edges[2].text).toBe('');
expect(edges[3].start).toBe('C'); expect(edges[3].start).toBe('B');
expect(edges[3].end).toBe('D'); expect(edges[3].end).toBe('D2');
expect(edges[3].type).toBe('arrow'); expect(edges[3].type).toBe('arrow');
expect(edges[3].text).toBe(''); expect(edges[3].text).toBe('');
expect(edges[4].start).toBe('B2');
expect(edges[4].end).toBe('D2');
expect(edges[4].type).toBe('arrow');
expect(edges[4].text).toBe('');
expect(edges[5].start).toBe('C');
expect(edges[5].end).toBe('D2');
expect(edges[5].type).toBe('arrow');
expect(edges[5].text).toBe('');
}); });
it('should handle chaining and multiple nodes in in link statement with extra info in statements', function() { it('should handle chaining and multiple nodes in in link statement with extra info in statements', function() {
const res = flow.parser.parse(` const res = flow.parser.parse(`
graph TD graph TD
A[ h ] -- hello --> B[" test "]:::exClass C --> D; A[ h ] -- hello --> B[" test "]:::exClass & C --> D;
classDef exClass background:#bbb,border:1px solid red; classDef exClass background:#bbb,border:1px solid red;
`); `);

View File

@@ -38,6 +38,7 @@
\# return 'BRKT'; \# return 'BRKT';
":::" return 'STYLE_SEPARATOR'; ":::" return 'STYLE_SEPARATOR';
":" return 'COLON'; ":" return 'COLON';
"&" return 'AMP';
";" return 'SEMI'; ";" return 'SEMI';
"," return 'COMMA'; "," return 'COMMA';
"*" return 'MULT'; "*" return 'MULT';
@@ -85,6 +86,8 @@
"-)" return '-)'; "-)" return '-)';
"([" return 'STADIUMSTART'; "([" return 'STADIUMSTART';
"])" return 'STADIUMEND'; "])" return 'STADIUMEND';
"[(" return 'CYLINDERSTART';
")]" return 'CYLINDEREND';
\- return 'MINUS'; \- return 'MINUS';
"." return 'DOT'; "." return 'DOT';
[\_] return 'UNDERSCORE'; [\_] return 'UNDERSCORE';
@@ -298,8 +301,8 @@ verticeStatement: verticeStatement link node
node: vertex node: vertex
{ /* console.warn('nod', $1); */ $$ = [$1];} { /* console.warn('nod', $1); */ $$ = [$1];}
| node spaceList vertex | node spaceList AMP spaceList vertex
{ $$ = [$1[0], $3]; /*console.warn('pip', $1, $3, $$);*/ } { $$ = $1.concat($5); /* console.warn('pip', $1[0], $5, $$); */ }
| vertex STYLE_SEPARATOR idString | vertex STYLE_SEPARATOR idString
{$$ = [$1];yy.setClass($1,$3)} {$$ = [$1];yy.setClass($1,$3)}
; ;
@@ -312,6 +315,8 @@ vertex: idString SQS text SQE
{$$ = $1;yy.addVertex($1,$3,'ellipse');} {$$ = $1;yy.addVertex($1,$3,'ellipse');}
| idString STADIUMSTART text STADIUMEND | idString STADIUMSTART text STADIUMEND
{$$ = $1;yy.addVertex($1,$3,'stadium');} {$$ = $1;yy.addVertex($1,$3,'stadium');}
| idString CYLINDERSTART text CYLINDEREND
{$$ = $1;yy.addVertex($1,$3,'cylinder');}
| idString PS text PE | idString PS text PE
{$$ = $1;yy.addVertex($1,$3,'round');} {$$ = $1;yy.addVertex($1,$3,'round');}
| idString DIAMOND_START text DIAMOND_STOP | idString DIAMOND_START text DIAMOND_STOP
@@ -464,9 +469,9 @@ alphaNumStatement
{$$='-';} {$$='-';}
; ;
alphaNumToken : PUNCTUATION | UNICODE_TEXT | NUM| ALPHA | COLON | COMMA | PLUS | EQUALS | MULT | DOT | BRKT| UNDERSCORE ; alphaNumToken : PUNCTUATION | AMP | UNICODE_TEXT | NUM| ALPHA | COLON | COMMA | PLUS | EQUALS | MULT | DOT | BRKT| UNDERSCORE ;
idStringToken : ALPHA|UNDERSCORE |UNICODE_TEXT | NUM| COLON | COMMA | PLUS | MINUS | DOWN |EQUALS | MULT | BRKT | DOT | PUNCTUATION; idStringToken : ALPHA|UNDERSCORE |UNICODE_TEXT | NUM| COLON | COMMA | PLUS | MINUS | DOWN |EQUALS | MULT | BRKT | DOT | PUNCTUATION | AMP;
graphCodeTokens: STADIUMSTART | STADIUMEND | TRAPSTART | TRAPEND | INVTRAPSTART | INVTRAPEND | PIPE | PS | PE | SQS | SQE | DIAMOND_START | DIAMOND_STOP | TAGSTART | TAGEND | ARROW_CROSS | ARROW_POINT | ARROW_CIRCLE | ARROW_OPEN | QUOTE | SEMI; graphCodeTokens: STADIUMSTART | STADIUMEND | CYLINDERSTART | CYLINDEREND | TRAPSTART | TRAPEND | INVTRAPSTART | INVTRAPEND | PIPE | PS | PE | SQS | SQE | DIAMOND_START | DIAMOND_STOP | TAGSTART | TAGEND | ARROW_CROSS | ARROW_POINT | ARROW_CIRCLE | ARROW_OPEN | QUOTE | SEMI;
%% %%

View File

@@ -231,7 +231,7 @@ describe('when parsing subgraphs', function() {
expect(edges[0].type).toBe('arrow'); expect(edges[0].type).toBe('arrow');
}); });
it('should handle subgraphs with multi node statements in it', function() { it('should handle subgraphs with multi node statements in it', function() {
const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\na b --> c e\n end;'); const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\na & b --> c & e\n end;');
const vert = flow.parser.yy.getVertices(); const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges(); const edges = flow.parser.yy.getEdges();

View File

@@ -117,7 +117,7 @@ const checkTaskDates = function(task, dateFormat, excludes) {
const fixTaskDates = function(startTime, endTime, dateFormat, excludes) { const fixTaskDates = function(startTime, endTime, dateFormat, excludes) {
let invalid = false; let invalid = false;
let renderEndTime = null; let renderEndTime = null;
while (startTime.date() <= endTime.date()) { while (startTime <= endTime) {
if (!invalid) { if (!invalid) {
renderEndTime = endTime.toDate(); renderEndTime = endTime.toDate();
} }
@@ -502,6 +502,11 @@ const setClickFun = function(id, functionName, functionArgs) {
} }
} }
/* if no arguments passed into callback, default to passing in id */
if (argList.length === 0) {
argList.push(id);
}
let rawTask = findTaskById(id); let rawTask = findTaskById(id);
if (typeof rawTask !== 'undefined') { if (typeof rawTask !== 'undefined') {
pushFun(id, () => { pushFun(id, () => {

View File

@@ -174,6 +174,26 @@ describe('when using the ganttDb', function() {
expect(tasks[6].task).toEqual('test7'); expect(tasks[6].task).toEqual('test7');
}); });
it('should work when end date is the 31st', function() {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.addSection('Task endTime is on the 31st day of the month');
ganttDb.addTask('test1', 'id1,2019-09-30,11d');
ganttDb.addTask('test2', 'id2,after id1,20d');
const tasks = ganttDb.getTasks();
expect(tasks[0].startTime).toEqual(moment('2019-09-30', 'YYYY-MM-DD').toDate());
expect(tasks[0].endTime).toEqual(moment('2019-10-11', 'YYYY-MM-DD').toDate());
expect(tasks[1].renderEndTime).toBeNull(); // Fixed end
expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('test1');
expect(tasks[1].startTime).toEqual(moment('2019-10-11', 'YYYY-MM-DD').toDate());
expect(tasks[1].endTime).toEqual(moment('2019-10-31', 'YYYY-MM-DD').toDate());
expect(tasks[1].renderEndTime).toBeNull(); // Fixed end
expect(tasks[1].id).toEqual('id2');
expect(tasks[1].task).toEqual('test2');
});
describe('when setting inclusive end dates', function() { describe('when setting inclusive end dates', function() {
beforeEach(function() { beforeEach(function() {
ganttDb.setDateFormat('YYYY-MM-DD'); ganttDb.setDateFormat('YYYY-MM-DD');

View File

@@ -106,4 +106,34 @@ describe('when parsing a gantt diagram it', function() {
} }
}); });
}); });
it('should parse callback specifier with no args', function() {
spyOn(ganttDb, 'setClickEvent');
const str =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' +
'section Clickable\n' +
'Visit mermaidjs :active, cl1, 2014-01-07, 3d\n' +
'Calling a callback :cl2, after cl1, 3d\n\n' +
'click cl1 href "https://mermaidjs.github.io/"\n' +
'click cl2 call ganttTestClick()\n';
expect(parserFnConstructor(str)).not.toThrow();
expect(ganttDb.setClickEvent).toHaveBeenCalledWith('cl2', 'ganttTestClick', null);
});
it('should parse callback specifier with arbitrary number of args', function() {
spyOn(ganttDb, 'setClickEvent');
const str =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' +
'section Clickable\n' +
'Visit mermaidjs :active, cl1, 2014-01-07, 3d\n' +
'Calling a callback :cl2, after cl1, 3d\n\n' +
'click cl1 href "https://mermaidjs.github.io/"\n' +
'click cl2 call ganttTestClick("test0", test1, test2)\n';
expect(parserFnConstructor(str)).not.toThrow();
const args = '"test1", "test2", "test3"';
expect(ganttDb.setClickEvent).toHaveBeenCalledWith('cl2', 'ganttTestClick', '"test0", test1, test2');
});
}); });

View File

@@ -339,12 +339,15 @@ describe('when parsing a sequenceDiagram', function() {
'participant 1 as multiline<br>text\n' + 'participant 1 as multiline<br>text\n' +
'participant 2 as multiline<br/>text\n' + 'participant 2 as multiline<br/>text\n' +
'participant 3 as multiline<br />text\n' + 'participant 3 as multiline<br />text\n' +
'participant 4 as multiline<br \t/>text\n' +
'1->>2: multiline<br>text\n' + '1->>2: multiline<br>text\n' +
'note right of 2: multiline<br>text\n' + 'note right of 2: multiline<br>text\n' +
'2->>3: multiline<br/>text\n' + '2->>3: multiline<br/>text\n' +
'note right of 3: multiline<br/>text\n' + 'note right of 3: multiline<br/>text\n' +
'3->>1: multiline<br />text\n' + '3->>4: multiline<br />text\n' +
'note right of 1: multiline<br />text\n'; 'note right of 4: multiline<br />text\n' +
'4->>1: multiline<br \t/>text\n' +
'note right of 1: multiline<br \t/>text\n';
parser.parse(str); parser.parse(str);
@@ -352,6 +355,7 @@ describe('when parsing a sequenceDiagram', function() {
expect(actors['1'].description).toBe('multiline<br>text'); expect(actors['1'].description).toBe('multiline<br>text');
expect(actors['2'].description).toBe('multiline<br/>text'); expect(actors['2'].description).toBe('multiline<br/>text');
expect(actors['3'].description).toBe('multiline<br />text'); expect(actors['3'].description).toBe('multiline<br />text');
expect(actors['4'].description).toBe('multiline<br \t/>text');
const messages = parser.yy.getMessages(); const messages = parser.yy.getMessages();
expect(messages[0].message).toBe('multiline<br>text'); expect(messages[0].message).toBe('multiline<br>text');
@@ -360,6 +364,8 @@ describe('when parsing a sequenceDiagram', function() {
expect(messages[3].message).toBe('multiline<br/>text'); expect(messages[3].message).toBe('multiline<br/>text');
expect(messages[4].message).toBe('multiline<br />text'); expect(messages[4].message).toBe('multiline<br />text');
expect(messages[5].message).toBe('multiline<br />text'); expect(messages[5].message).toBe('multiline<br />text');
expect(messages[6].message).toBe('multiline<br \t/>text');
expect(messages[7].message).toBe('multiline<br \t/>text');
}); });
it('it should handle notes over a single actor', function() { it('it should handle notes over a single actor', function() {
const str = const str =

View File

@@ -168,7 +168,7 @@ export const bounds = {
const _drawLongText = (text, x, y, g, width) => { const _drawLongText = (text, x, y, g, width) => {
let textHeight = 0; let textHeight = 0;
const lines = text.split(/<br ?\/?>/gi); const lines = text.split(/<br\s*\/?>/gi);
for (const line of lines) { for (const line of lines) {
const textObj = svgDraw.getTextObj(); const textObj = svgDraw.getTextObj();
textObj.x = x; textObj.x = x;
@@ -233,7 +233,7 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
let textElem; let textElem;
let counterBreaklines = 0; let counterBreaklines = 0;
let breaklineOffset = 17; let breaklineOffset = 17;
const breaklines = msg.message.split(/<br ?\/?>/gi); const breaklines = msg.message.split(/<br\s*\/?>/gi);
for (const breakline of breaklines) { for (const breakline of breaklines) {
textElem = g textElem = g
.append('text') // text label for the x axis .append('text') // text label for the x axis

View File

@@ -18,7 +18,7 @@ export const drawRect = function(elem, rectData) {
export const drawText = function(elem, textData) { export const drawText = function(elem, textData) {
// Remove and ignore br:s // Remove and ignore br:s
const nText = textData.text.replace(/<br ?\/?>/gi, ' '); const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
const textElem = elem.append('text'); const textElem = elem.append('text');
textElem.attr('x', textData.x); textElem.attr('x', textData.x);
@@ -321,7 +321,7 @@ const _drawTextCandidateFunc = (function() {
function byTspan(content, g, x, y, width, height, textAttrs, conf) { function byTspan(content, g, x, y, width, height, textAttrs, conf) {
const { actorFontSize, actorFontFamily } = conf; const { actorFontSize, actorFontFamily } = conf;
const lines = content.split(/<br ?\/?>/gi); const lines = content.split(/<br\s*\/?>/gi);
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const dy = i * actorFontSize - (actorFontSize * (lines.length - 1)) / 2; const dy = i * actorFontSize - (actorFontSize * (lines.length - 1)) / 2;
const text = g const text = g

View File

@@ -11,7 +11,8 @@
.node rect, .node rect,
.node circle, .node circle,
.node ellipse, .node ellipse,
.node polygon { .node polygon,
.node path {
fill: $mainBkg; fill: $mainBkg;
stroke: $nodeBorder; stroke: $nodeBorder;
stroke-width: 1px; stroke-width: 1px;