Merge branch 'develop' of github.com:mermaid-js/mermaid into develop

This commit is contained in:
Knut Sveidqvist 2020-06-14 18:41:31 +02:00
commit 3589bb9597
27 changed files with 1527 additions and 704 deletions

View File

@ -20,223 +20,40 @@
</head> </head>
<body> <body>
<h1>info below</h1> <h1>info below</h1>
<div class="mermaid" style="width: 100%; height: 20%;"> <div class="mermaid" style="width: 50%; height: 20%;">
flowchart LR flowchart TB
user1[fa:fa-user User 1] -- edit -> folder subgraph 1
</div> A --> B;
<div class="mermaid2" style="width: 50%; height: 20%;"> A -.-> C;
flowchart LR A ==> D;
A{{A}}-- apa -->B{{B}}; A ==> E;
click A callback "Tooltip" B <--> F
click B "http://www.github.com" "This is a link" C <--> F
D <--> F
</div> E <--> F
<div class="mermaid2" style="width: 50%; height: 20%;"> end
graph LR subgraph 2
A{{A}}--apa-->B{{B}}; A2 --x B2;
A2 -.-x C2;
</div> A2 ==x D2;
<div class="mermaid2" style="width: 50%; height: 20%;"> A2 ==x E2;
stateDiagram-v2 B2 x--x F2
[*]-->TV C2 x--x F2
D2 x--x F2
state TV { E2 x--x F2
state fork_state &lt;&lt;fork&gt;&gt; end
[*] --> fork_state subgraph 3
fork_state --> State2 A3 --o B3;
fork_state --> State3 A3 -.-o C3;
A3 ==o D3;
state join_state &lt;&lt;join&gt;&gt; A3 ==o E3;
State2 --> join_state B3 o--o F3
State3 --> join_state C3 o--o F3
join_state --> State4 D3 o--o F3
State4 --> [*] E3 o--o F3
} end
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2
[*] --> First
First --> Third
First --> sec
state First {
[*] --> fir
fir --> [*]
}
state Second {
[*] --> sec
sec --> [*]
}
state Third {
[*] --> thi
thi --> [*]
}
thi --> sec
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
flowchart TD
subgraph A
a
end
subgraph B
b
end
subgraph C
subgraph D
d
end
end
A -- oAo --o B
A --> C
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
flowchart TD
subgraph A
a
end
subgraph B
b
end
c-->A
c-->B
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2
[*] --> First
First --> Second
First --> Third
state First {
[*] --> fir
fir --> [*]
}
state Second {
[*] --> sec
sec --> [*]
}
state Third {
[*] --> thi
thi --> [*]
}
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2
[*]-->TV
state TV {
[*] --> Off: Off to start with
On --> Off : Turn off
Off --> On : Turn on
}
TV--> Console
state Console {
[*] --> Off2: Off to start with
On2--> Off2 : Turn off
Off2 --> On2 : Turn on
On2-->Playing
state Playing {
Alive --> Dead
Dead-->Alive
}
}
</div> </div>
<div style="display: flex;flex-direction:column;width: 100%; height: 100%">
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2
state apa {
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
}
</div>
<div class="mermaid2" style="width: 100%; height: 100%">
flowchart TB
a --> b
subgraph id1 [Test]
a --apa--> c
b
c-->b
b-->H
end
G-->H
G-->id1
id1 --> I
I --> G
</div>
<div class="mermaid2" style="width: 100%; height: 100%">
flowchart RL
a --> b
subgraph id1 [Test]
a --apa--> c
b
c-->b
b-->H
end
G-->H
G-->id1
id1 --> I
I --> G
</div>
<div class="mermaid2" style="width: 100%; height: 100%">
flowchart RL
subgraph id1 [Test]
a
end
b-->id1
</div>
<div class="mermaid2" style="width: 100%; height: 100%">
flowchart RL
subgraph id1 [Test1]
a
end
subgraph id2 [Test2]
b
end
a --> id2
a --> b
b-->id1
id1 --> id2
</div>
new:
<div class="mermaid2" style="width: 100%; height: 100%">
flowchart LR
a <--> b
b o--o c
c x--x d
a21([In the box]) --> b2
b2((b2)) --o c2
c2(c2) --x d2 --> id1{{This is the text in the box}} --> A[(cylindrical<br />shape<br />test)]
</div>
old:
<div class="mermaid2" style="width: 100%; height: 100%">
graph LR
a((a)) --> b --> id1{{This is the text in the box}}
A[(cylindrical<br />shape<br />test)]
</div>
</div>
<script src="./mermaid.js"></script> <script src="./mermaid.js"></script>
<script> <script>
mermaid.parseError = function (err, hash) { mermaid.parseError = function (err, hash) {

12
dist/mermaid.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,15 +3,15 @@
> A Gantt chart is a type of bar chart, first developed by Karol Adamiecki in 1896, and independently by Henry Gantt in the 1910s, that illustrates a project schedule and the amount of time it would take for any one project to finish. Gantt charts illustrate number of days between the start and finish dates of the terminal elements and summary elements of a project. > A Gantt chart is a type of bar chart, first developed by Karol Adamiecki in 1896, and independently by Henry Gantt in the 1910s, that illustrates a project schedule and the amount of time it would take for any one project to finish. Gantt charts illustrate number of days between the start and finish dates of the terminal elements and summary elements of a project.
## A note to users ## A note to users
Gannt Charts will record each scheduled task as one continuous bar that extends from the left to the right. The x axis represents time and the y records the different tasks and the order in which they are to be completed. Gantt Charts will record each scheduled task as one continuous bar that extends from the left to the right. The x axis represents time and the y records the different tasks and the order in which they are to be completed.
It is important to remember that when a date, day or collection of dates specific to a task are "excluded", the Gannt Chart will accomodate those changes by extending an equal number of day, towards the right, not by creating a gap inside the task. It is important to remember that when a date, day, or collection of dates specific to a task are "excluded", the Gantt Chart will accomodate those changes by extending an equal number of day, towards the right, not by creating a gap inside the task.
As shown here ![](https://github.com/NeilCuzon/mermaid/blob/develop/docs/img/Gantt-excluded-days-within.png) As shown here ![](https://raw.githubusercontent.com/NeilCuzon/mermaid/develop/docs/img/Gantt-excluded-days-within.png)
However, if the excluded date/s is between two tasks that are set to start consecutively, the excluded dates will be skipped graphically and left blank, and the following task will begin after the end of the excluded date/s. However, if the excluded dates are between two tasks that are set to start consecutively, the excluded dates will be skipped graphically and left blank, and the following task will begin after the end of the excluded dates.
As shown here ![](https://github.com/NeilCuzon/mermaid/blob/develop/docs/img/Gantt-long-weekend-look.png) As shown here ![](https://raw.githubusercontent.com/NeilCuzon/mermaid/develop/docs/img/Gantt-long-weekend-look.png)
A Gantt chart is useful for tracking the amount of time it would take before a project is finished, but it can also be used to graphically represent "non-working days, with a few tweaks. A Gantt chart is useful for tracking the amount of time it would take before a project is finished, but it can also be used to graphically represent "non-working days", with a few tweaks.
Mermaid can render Gantt diagrams as SVG, PNG or a MarkDown link that can be pasted into docs. Mermaid can render Gantt diagrams as SVG, PNG or a MarkDown link that can be pasted into docs.
@ -117,12 +117,14 @@ It is possible to set multiple depenendenies separated by space:
### Title ### Title
Tbd The `title` is an *optional* string to be displayed at the top of the Gantt chart to describe the chart as a whole.
## Sections statements ### Section statements
Tbd You can divide the chart into various sections, for example to separate different parts of a project like development and documentation.
To do so, start a line with the `section` keyword and give it a name. (Note that unlike with the [title for the entire chart](#title), this name is *required*.
## Setting dates ## Setting dates
@ -293,6 +295,19 @@ noteText | Styles for the text on in the note boxes.
} }
``` ```
## Today marker
You can style or hide the marker for the current date. To style it, add a value for the `todayMarker` key.
```
todayMarker stroke-width:5px,stroke:#0f0,opacity:0.5
```
To hide the marker, set `todayMarker` to `off`.
```
todayMarker off
```
## Configuration ## Configuration

View File

@ -219,6 +219,11 @@ This sets the font size of the actor's description
This sets the font family of the actor's description This sets the font family of the actor's description
**Default value "Open-Sans", "sans-serif"**. **Default value "Open-Sans", "sans-serif"**.
### actorFontWeight
This sets the font weight of the actor's description
\*\*Default value 400.
### noteFontSize ### noteFontSize
This sets the font size of actor-attached notes. This sets the font size of actor-attached notes.
@ -229,6 +234,11 @@ This sets the font size of actor-attached notes.
This sets the font family of actor-attached notes. This sets the font family of actor-attached notes.
**Default value "trebuchet ms", verdana, arial**. **Default value "trebuchet ms", verdana, arial**.
### noteFontWeight
This sets the font weight of the note's description
\*\*Default value 400.
### noteAlign ### noteAlign
This sets the text alignment of actor-attached notes. This sets the text alignment of actor-attached notes.
@ -244,6 +254,21 @@ This sets the font size of actor messages.
This sets the font family of actor messages. This sets the font family of actor messages.
**Default value "trebuchet ms", verdana, arial**. **Default value "trebuchet ms", verdana, arial**.
### messageFontWeight
This sets the font weight of the message's description
\*\*Default value 400.
### wrapEnabled
This sets the auto-wrap state for the diagram
\*\*Default value false.
### wrapPadding
This sets the auto-wrap padding for the diagram (sides only)
\*\*Default value 15.
## gantt ## gantt
The object containing configurations specific for gantt diagrams\* The object containing configurations specific for gantt diagrams\*

View File

@ -506,8 +506,11 @@ mermaid.sequenceConfig = {
| bottomMarginAdj | Adjusts how far down the graph ended. Wide borders styles with css could generate unwanted clipping which is why this config param exists. | 1 | | bottomMarginAdj | Adjusts how far down the graph ended. Wide borders styles with css could generate unwanted clipping which is why this config param exists. | 1 |
| actorFontSize | Sets the font size for the actor's description | 14 | | actorFontSize | Sets the font size for the actor's description | 14 |
| actorFontFamily | Sets the font family for the actor's description | "Open-Sans", "sans-serif" | | actorFontFamily | Sets the font family for the actor's description | "Open-Sans", "sans-serif" |
| actorFontWeight | Sets the font weight for the actor's description | "Open-Sans", "sans-serif" |
| noteFontSize | Sets the font size for actor-attached notes | 14 | | noteFontSize | Sets the font size for actor-attached notes | 14 |
| noteFontFamily | Sets the font family for actor-attached notes | "trebuchet ms", verdana, arial | | noteFontFamily | Sets the font family for actor-attached notes | "trebuchet ms", verdana, arial |
| noteFontWeight | Sets the font weight for actor-attached notes | "trebuchet ms", verdana, arial |
| noteAlign | Sets the text alignment for text in actor-attached notes | center | | noteAlign | Sets the text alignment for text in actor-attached notes | center |
| messageFontSize | Sets the font size for actor<->actor messages | 16 | | messageFontSize | Sets the font size for actor<->actor messages | 16 |
| messageFontFamily | Sets the font family for actor<->actor messages | "trebuchet ms", verdana, arial | | messageFontFamily | Sets the font family for actor<->actor messages | "trebuchet ms", verdana, arial |
| messageFontWeight | Sets the font weight for actor<->actor messages | "trebuchet ms", verdana, arial |

View File

@ -112,6 +112,8 @@ Required edgeData for proper rendering:
| label | overlap between label and labelText? | | label | overlap between label and labelText? |
| labelPos | | | labelPos | |
| labelType | overlap between label and labelText? | | labelType | overlap between label and labelText? |
| thickness | Sets the thinkess of the edge. Can be ['normal', 'thick'] |
| pattern | Sets the pattern of the edge. Can be ['solid', 'dotted', 'dashed'] |
# Markers # Markers

View File

@ -191,11 +191,35 @@ export const insertEdge = function(elem, e, edge, clusterDb, diagramType, graph)
}) })
.curve(curveBasis); .curve(curveBasis);
// Contruct stroke classes based on properties
let strokeClasses;
switch (edge.thickness) {
case 'normal':
strokeClasses = 'edge-thickness-normal';
break;
case 'thick':
strokeClasses = 'edge-thickness-thick';
break;
default:
strokeClasses = '';
}
switch (edge.pattern) {
case 'solid':
strokeClasses += ' edge-pattern-solid';
break;
case 'dotted':
strokeClasses += ' edge-pattern-dotted';
break;
case 'dashed':
strokeClasses += ' edge-pattern-dashed';
break;
}
const svgPath = elem const svgPath = elem
.append('path') .append('path')
.attr('d', lineFunction(lineData)) .attr('d', lineFunction(lineData))
.attr('id', edge.id) .attr('id', edge.id)
.attr('class', 'transition' + (edge.classes ? ' ' + edge.classes : '')); .attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''));
// DEBUG code, adds a red circle at each edge coordinate // DEBUG code, adds a red circle at each edge coordinate
// edge.points.forEach(point => { // edge.points.forEach(point => {

View File

@ -17,7 +17,7 @@ const extension = (elem, type, id) => {
.append('defs') .append('defs')
.append('marker') .append('marker')
.attr('id', type + '-extensionStart') .attr('id', type + '-extensionStart')
.attr('class', 'extension ' + type) .attr('class', 'marker extension ' + type)
.attr('refX', 0) .attr('refX', 0)
.attr('refY', 7) .attr('refY', 7)
.attr('markerWidth', 190) .attr('markerWidth', 190)
@ -30,7 +30,7 @@ const extension = (elem, type, id) => {
.append('defs') .append('defs')
.append('marker') .append('marker')
.attr('id', type + '-extensionEnd ' + type) .attr('id', type + '-extensionEnd ' + type)
.attr('class', 'extension ' + type) .attr('class', 'marker extension ' + type)
.attr('refX', 19) .attr('refX', 19)
.attr('refY', 7) .attr('refY', 7)
.attr('markerWidth', 20) .attr('markerWidth', 20)
@ -45,7 +45,7 @@ const composition = (elem, type) => {
.append('defs') .append('defs')
.append('marker') .append('marker')
.attr('id', type + '-compositionStart') .attr('id', type + '-compositionStart')
.attr('class', 'extension ' + type) .attr('class', 'marker extension ' + type)
.attr('refX', 0) .attr('refX', 0)
.attr('refY', 7) .attr('refY', 7)
.attr('markerWidth', 190) .attr('markerWidth', 190)
@ -58,7 +58,7 @@ const composition = (elem, type) => {
.append('defs') .append('defs')
.append('marker') .append('marker')
.attr('id', type + '-compositionEnd') .attr('id', type + '-compositionEnd')
.attr('class', 'extension ' + type) .attr('class', 'marker extension ' + type)
.attr('refX', 19) .attr('refX', 19)
.attr('refY', 7) .attr('refY', 7)
.attr('markerWidth', 20) .attr('markerWidth', 20)
@ -72,7 +72,7 @@ const aggregation = (elem, type) => {
.append('defs') .append('defs')
.append('marker') .append('marker')
.attr('id', type + '-aggregationStart') .attr('id', type + '-aggregationStart')
.attr('class', 'extension ' + type) .attr('class', 'marker extension ' + type)
.attr('refX', 0) .attr('refX', 0)
.attr('refY', 7) .attr('refY', 7)
.attr('markerWidth', 190) .attr('markerWidth', 190)
@ -85,7 +85,7 @@ const aggregation = (elem, type) => {
.append('defs') .append('defs')
.append('marker') .append('marker')
.attr('id', type + '-aggregationEnd') .attr('id', type + '-aggregationEnd')
.attr('class', type) .attr('class', 'marker ' + type)
.attr('refX', 19) .attr('refX', 19)
.attr('refY', 7) .attr('refY', 7)
.attr('markerWidth', 20) .attr('markerWidth', 20)
@ -99,7 +99,7 @@ const dependency = (elem, type) => {
.append('defs') .append('defs')
.append('marker') .append('marker')
.attr('id', type + '-dependencyStart') .attr('id', type + '-dependencyStart')
.attr('class', 'extension ' + type) .attr('class', 'marker extension ' + type)
.attr('refX', 0) .attr('refX', 0)
.attr('refY', 7) .attr('refY', 7)
.attr('markerWidth', 190) .attr('markerWidth', 190)
@ -112,7 +112,7 @@ const dependency = (elem, type) => {
.append('defs') .append('defs')
.append('marker') .append('marker')
.attr('id', type + '-dependencyEnd') .attr('id', type + '-dependencyEnd')
.attr('class', type) .attr('class', 'marker ' + type)
.attr('refX', 19) .attr('refX', 19)
.attr('refY', 7) .attr('refY', 7)
.attr('markerWidth', 20) .attr('markerWidth', 20)
@ -125,13 +125,13 @@ const point = (elem, type) => {
elem elem
.append('marker') .append('marker')
.attr('id', type + '-pointEnd') .attr('id', type + '-pointEnd')
.attr('class', type) .attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10') .attr('viewBox', '0 0 10 10')
.attr('refX', 10) .attr('refX', 9)
.attr('refY', 5) .attr('refY', 5)
.attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 8) .attr('markerWidth', 12)
.attr('markerHeight', 8) .attr('markerHeight', 12)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z') .attr('d', 'M 0 0 L 10 5 L 0 10 z')
@ -141,13 +141,13 @@ const point = (elem, type) => {
elem elem
.append('marker') .append('marker')
.attr('id', type + '-pointStart') .attr('id', type + '-pointStart')
.attr('class', type) .attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10') .attr('viewBox', '0 0 10 10')
.attr('refX', 0) .attr('refX', 0)
.attr('refY', 5) .attr('refY', 5)
.attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 8) .attr('markerWidth', 12)
.attr('markerHeight', 8) .attr('markerHeight', 12)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 0 5 L 10 10 L 10 0 z') .attr('d', 'M 0 5 L 10 10 L 10 0 z')
@ -159,13 +159,13 @@ const circle = (elem, type) => {
elem elem
.append('marker') .append('marker')
.attr('id', type + '-circleEnd') .attr('id', type + '-circleEnd')
.attr('class', type) .attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10') .attr('viewBox', '0 0 10 10')
.attr('refX', 11) .attr('refX', 11)
.attr('refY', 5) .attr('refY', 5)
.attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 7) .attr('markerWidth', 11)
.attr('markerHeight', 7) .attr('markerHeight', 11)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('circle') .append('circle')
.attr('cx', '5') .attr('cx', '5')
@ -178,13 +178,13 @@ const circle = (elem, type) => {
elem elem
.append('marker') .append('marker')
.attr('id', type + '-circleStart') .attr('id', type + '-circleStart')
.attr('class', type) .attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10') .attr('viewBox', '0 0 10 10')
.attr('refX', -1) .attr('refX', -1)
.attr('refY', 5) .attr('refY', 5)
.attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 7) .attr('markerWidth', 11)
.attr('markerHeight', 7) .attr('markerHeight', 11)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('circle') .append('circle')
.attr('cx', '5') .attr('cx', '5')
@ -198,16 +198,16 @@ const cross = (elem, type) => {
elem elem
.append('marker') .append('marker')
.attr('id', type + '-crossEnd') .attr('id', type + '-crossEnd')
.attr('class', type) .attr('class', 'marker cross ' + type)
.attr('viewBox', '0 0 11 11') .attr('viewBox', '0 0 11 11')
.attr('refX', 12) .attr('refX', 12)
.attr('refY', 5.2) .attr('refY', 5.2)
.attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 7) .attr('markerWidth', 11)
.attr('markerHeight', 7) .attr('markerHeight', 11)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('stroke', 'black') // .attr('stroke', 'black')
.attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9') .attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9')
.attr('class', 'arrowMarkerPath') .attr('class', 'arrowMarkerPath')
.style('stroke-width', 2) .style('stroke-width', 2)
@ -216,16 +216,16 @@ const cross = (elem, type) => {
elem elem
.append('marker') .append('marker')
.attr('id', type + '-crossStart') .attr('id', type + '-crossStart')
.attr('class', type) .attr('class', 'marker cross ' + type)
.attr('viewBox', '0 0 11 11') .attr('viewBox', '0 0 11 11')
.attr('refX', -1) .attr('refX', -1)
.attr('refY', 5.2) .attr('refY', 5.2)
.attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 7) .attr('markerWidth', 11)
.attr('markerHeight', 7) .attr('markerHeight', 11)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('stroke', 'black') // .attr('stroke', 'black')
.attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9') .attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9')
.attr('class', 'arrowMarkerPath') .attr('class', 'arrowMarkerPath')
.style('stroke-width', 2) .style('stroke-width', 2)

View File

@ -217,7 +217,7 @@ const setClickFunc = function(domId, functionName, tooltip) {
elem.addEventListener( elem.addEventListener(
'click', 'click',
function() { function() {
window[functionName](elemId); utils.runFunc(functionName, elemId);
}, },
false false
); );

View File

@ -232,7 +232,7 @@ const setClickFun = function(_id, functionName) {
elem.addEventListener( elem.addEventListener(
'click', 'click',
function() { function() {
window[functionName](id); utils.runFunc(functionName, id);
}, },
false false
); );

View File

@ -217,12 +217,16 @@ export const addEdges = function(edges, g) {
if (typeof defaultLabelStyle !== 'undefined') { if (typeof defaultLabelStyle !== 'undefined') {
labelStyle = defaultLabelStyle; labelStyle = defaultLabelStyle;
} }
edgeData.thickness = 'normal';
edgeData.pattern = 'solid';
break; break;
case 'dotted': case 'dotted':
style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; edgeData.thickness = 'normal';
edgeData.pattern = 'dotted';
break; break;
case 'thick': case 'thick':
style = ' stroke-width: 3.5px;fill:none'; edgeData.thickness = 'thick';
edgeData.pattern = 'solid';
break; break;
} }
} }
@ -262,7 +266,7 @@ export const addEdges = function(edges, g) {
} }
edgeData.id = linkId; edgeData.id = linkId;
edgeData.class = linkNameStart + ' ' + linkNameEnd; edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
// Add the edge to the graph // Add the edge to the graph
g.setEdge(edge.start, edge.end, edgeData, cnt); g.setEdge(edge.start, edge.end, edgeData, cnt);

View File

@ -2,6 +2,7 @@ import moment from 'moment-mini';
import { sanitizeUrl } from '@braintree/sanitize-url'; import { sanitizeUrl } from '@braintree/sanitize-url';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { getConfig } from '../../config'; import { getConfig } from '../../config';
import utils from '../../utils';
const config = getConfig(); const config = getConfig();
let dateFormat = ''; let dateFormat = '';
@ -520,7 +521,7 @@ const setClickFun = function(id, functionName, functionArgs) {
let rawTask = findTaskById(id); let rawTask = findTaskById(id);
if (typeof rawTask !== 'undefined') { if (typeof rawTask !== 'undefined') {
pushFun(id, () => { pushFun(id, () => {
window[functionName](...argList); utils.runFunc(functionName, ...argList);
}); });
} }
}; };

View File

@ -16,52 +16,63 @@
%x ID %x ID
%x ALIAS %x ALIAS
// Directive states
%x OPEN_DIRECTIVE
%x TYPE_DIRECTIVE
%x ARG_DIRECTIVE
// A special state for grabbing text up to the first comment/newline // A special state for grabbing text up to the first comment/newline
%x LINE %x LINE
%% %%
[\n]+ return 'NL'; \%\%\{ { this.begin('OPEN_DIRECTIVE'); return 'open_directive'; }
\s+ /* skip all whitespace */ <OPEN_DIRECTIVE>((?:(?!\}\%\%)[^:.])*) { this.begin('TYPE_DIRECTIVE'); return 'type_directive'; }
<ID,ALIAS,LINE>((?!\n)\s)+ /* skip same-line whitespace */ <TYPE_DIRECTIVE>":" { this.popState(); this.begin('ARG_DIRECTIVE'); return ':'; }
<INITIAL,ID,ALIAS,LINE>\#[^\n]* /* skip comments */ <TYPE_DIRECTIVE,ARG_DIRECTIVE>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
\%%[^\n]* /* skip comments */ <ARG_DIRECTIVE>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
"participant" { this.begin('ID'); return 'participant'; } [\n]+ return 'NL';
<ID>[^\->:\n,;]+?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } \s+ /* skip all whitespace */
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; } <ID,ALIAS,LINE>((?!\n)\s)+ /* skip same-line whitespace */
<ALIAS>(?:) { this.popState(); this.popState(); return 'NL'; } <INITIAL,ID,ALIAS,LINE,ARG_DIRECTIVE,TYPE_DIRECTIVE,OPEN_DIRECTIVE>\#[^\n]* /* skip comments */
"loop" { this.begin('LINE'); return 'loop'; } \%%(?!\{)[^\n]* /* skip comments */
"rect" { this.begin('LINE'); return 'rect'; } [^\}]\%\%[^\n]* /* skip comments */
"opt" { this.begin('LINE'); return 'opt'; } "participant" { this.begin('ID'); return 'participant'; }
"alt" { this.begin('LINE'); return 'alt'; } <ID>[^\->:\n,;]+?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
"else" { this.begin('LINE'); return 'else'; } <ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
"par" { this.begin('LINE'); return 'par'; } <ALIAS>(?:) { this.popState(); this.popState(); return 'NL'; }
"and" { this.begin('LINE'); return 'and'; } "loop" { this.begin('LINE'); return 'loop'; }
<LINE>[^#\n;]* { this.popState(); return 'restOfLine'; } "rect" { this.begin('LINE'); return 'rect'; }
"end" return 'end'; "opt" { this.begin('LINE'); return 'opt'; }
"left of" return 'left_of'; "alt" { this.begin('LINE'); return 'alt'; }
"right of" return 'right_of'; "else" { this.begin('LINE'); return 'else'; }
"over" return 'over'; "par" { this.begin('LINE'); return 'par'; }
"note" return 'note'; "and" { this.begin('LINE'); return 'and'; }
"activate" { this.begin('ID'); return 'activate'; } <LINE>(?:[:]?(?:no)?wrap:)?[^#\n;]* { this.popState(); return 'restOfLine'; }
"deactivate" { this.begin('ID'); return 'deactivate'; } "end" return 'end';
"title" return 'title'; "left of" return 'left_of';
"sequenceDiagram" return 'SD'; "right of" return 'right_of';
"autonumber" return 'autonumber'; "over" return 'over';
"," return ','; "note" return 'note';
";" return 'NL'; "activate" { this.begin('ID'); return 'activate'; }
[^\+\->:\n,;]+ { yytext = yytext.trim(); return 'ACTOR'; } "deactivate" { this.begin('ID'); return 'deactivate'; }
"->>" return 'SOLID_ARROW'; "title" return 'title';
"-->>" return 'DOTTED_ARROW'; "sequenceDiagram" return 'SD';
"->" return 'SOLID_OPEN_ARROW'; "autonumber" return 'autonumber';
"-->" return 'DOTTED_OPEN_ARROW'; "," return ',';
\-[x] return 'SOLID_CROSS'; ";" return 'NL';
\-\-[x] return 'DOTTED_CROSS'; [^\+\->:\n,;]+ { yytext = yytext.trim(); return 'ACTOR'; }
":"[^#\n;]+ return 'TXT'; "->>" return 'SOLID_ARROW';
"+" return '+'; "-->>" return 'DOTTED_ARROW';
"-" return '-'; "->" return 'SOLID_OPEN_ARROW';
<<EOF>> return 'NL'; "-->" return 'DOTTED_OPEN_ARROW';
. return 'INVALID'; \-[x] return 'SOLID_CROSS';
\-\-[x] return 'DOTTED_CROSS';
":"(?:(?:no)?wrap:)?[^#\n;]+ return 'TXT';
"+" return '+';
"-" return '-';
<<EOF>> return 'NL';
. return 'INVALID';
/lex /lex
@ -74,6 +85,7 @@
start start
: SPACE start : SPACE start
| NL start | NL start
| directive start
| SD document { yy.apply($2);return $2; } | SD document { yy.apply($2);return $2; }
; ;
@ -85,11 +97,16 @@ document
line line
: SPACE statement { $$ = $2 } : SPACE statement { $$ = $2 }
| statement { $$ = $1 } | statement { $$ = $1 }
| NL { $$=[];} | NL { $$=[]; }
; ;
directive
: openDirective typeDirective closeDirective 'NL'
| openDirective typeDirective ':' argDirective closeDirective 'NL'
;
statement statement
: 'participant' actor 'AS' restOfLine 'NL' {$2.description=$4; $$=$2;} : 'participant' actor 'AS' restOfLine 'NL' {$2.description=yy.parseMessage($4); $$=$2;}
| 'participant' actor 'NL' {$$=$2;} | 'participant' actor 'NL' {$$=$2;}
| signal 'NL' | signal 'NL'
| autonumber {yy.enableSequenceNumbers()} | autonumber {yy.enableSequenceNumbers()}
@ -99,23 +116,23 @@ statement
| title text2 'NL' {$$=[{type:'setTitle', text:$2}]} | title text2 'NL' {$$=[{type:'setTitle', text:$2}]}
| 'loop' restOfLine document end | 'loop' restOfLine document end
{ {
$3.unshift({type: 'loopStart', loopText:$2, signalType: yy.LINETYPE.LOOP_START}); $3.unshift({type: 'loopStart', loopText:yy.parseMessage($2), signalType: yy.LINETYPE.LOOP_START});
$3.push({type: 'loopEnd', loopText:$2, signalType: yy.LINETYPE.LOOP_END}); $3.push({type: 'loopEnd', loopText:$2, signalType: yy.LINETYPE.LOOP_END});
$$=$3;} $$=$3;}
| 'rect' restOfLine document end | 'rect' restOfLine document end
{ {
$3.unshift({type: 'rectStart', color:$2, signalType: yy.LINETYPE.RECT_START }); $3.unshift({type: 'rectStart', color:yy.parseMessage($2), signalType: yy.LINETYPE.RECT_START });
$3.push({type: 'rectEnd', color:$2, signalType: yy.LINETYPE.RECT_END }); $3.push({type: 'rectEnd', color:yy.parseMessage($2), signalType: yy.LINETYPE.RECT_END });
$$=$3;} $$=$3;}
| opt restOfLine document end | opt restOfLine document end
{ {
$3.unshift({type: 'optStart', optText:$2, signalType: yy.LINETYPE.OPT_START}); $3.unshift({type: 'optStart', optText:yy.parseMessage($2), signalType: yy.LINETYPE.OPT_START});
$3.push({type: 'optEnd', optText:$2, signalType: yy.LINETYPE.OPT_END}); $3.push({type: 'optEnd', optText:yy.parseMessage($2), signalType: yy.LINETYPE.OPT_END});
$$=$3;} $$=$3;}
| alt restOfLine else_sections end | alt restOfLine else_sections end
{ {
// Alt start // Alt start
$3.unshift({type: 'altStart', altText:$2, signalType: yy.LINETYPE.ALT_START}); $3.unshift({type: 'altStart', altText:yy.parseMessage($2), signalType: yy.LINETYPE.ALT_START});
// Content in alt is already in $3 // Content in alt is already in $3
// End // End
$3.push({type: 'altEnd', signalType: yy.LINETYPE.ALT_END}); $3.push({type: 'altEnd', signalType: yy.LINETYPE.ALT_END});
@ -123,23 +140,24 @@ statement
| par restOfLine par_sections end | par restOfLine par_sections end
{ {
// Parallel start // Parallel start
$3.unshift({type: 'parStart', parText:$2, signalType: yy.LINETYPE.PAR_START}); $3.unshift({type: 'parStart', parText:yy.parseMessage($2), signalType: yy.LINETYPE.PAR_START});
// Content in par is already in $3 // Content in par is already in $3
// End // End
$3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END}); $3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END});
$$=$3;} $$=$3;}
| directive
; ;
par_sections par_sections
: document : document
| document and restOfLine par_sections | document and restOfLine par_sections
{ $$ = $1.concat([{type: 'and', parText:$3, signalType: yy.LINETYPE.PAR_AND}, $4]); } { $$ = $1.concat([{type: 'and', parText:yy.parseMessage($3), signalType: yy.LINETYPE.PAR_AND}, $4]); }
; ;
else_sections else_sections
: document : document
| document else restOfLine else_sections | document else restOfLine else_sections
{ $$ = $1.concat([{type: 'else', altText:$3, signalType: yy.LINETYPE.ALT_ELSE}, $4]); } { $$ = $1.concat([{type: 'else', altText:yy.parseMessage($3), signalType: yy.LINETYPE.ALT_ELSE}, $4]); }
; ;
note_statement note_statement
@ -195,6 +213,24 @@ signaltype
| DOTTED_CROSS { $$ = yy.LINETYPE.DOTTED_CROSS; } | DOTTED_CROSS { $$ = yy.LINETYPE.DOTTED_CROSS; }
; ;
text2: TXT {$$ = $1.substring(1).trim().replace(/\\n/gm, "\n");} ; text2
: TXT {$$ = yy.parseMessage($1.trim().substring(1)) }
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive'); }
;
%% %%

View File

@ -1,11 +1,68 @@
import { logger } from '../../logger'; import { logger } from '../../logger';
import { getConfig, setConfig } from '../../config';
import mermaidAPI from '../../mermaidAPI';
let prevActor = undefined; let prevActor = undefined;
let actors = {}; let actors = {};
let messages = []; let messages = [];
const notes = []; const notes = [];
let title = ''; let title = '';
let titleWrapped = false;
let sequenceNumbersEnabled = false; let sequenceNumbersEnabled = false;
let wrapEnabled = false;
let configUpdated = false;
let currentDirective = {};
export const parseDirective = function(statement, context) {
try {
if (statement !== undefined) {
statement = statement.trim();
switch (context) {
case 'open_directive':
currentDirective = {};
break;
case 'type_directive':
currentDirective.type = statement.toLowerCase();
break;
case 'arg_directive':
currentDirective.args = JSON.parse(statement);
break;
case 'close_directive':
handleDirective(currentDirective);
currentDirective = null;
break;
}
}
} catch (error) {
logger.error(
`Error while rendering sequenceDiagram directive: ${statement} jison context: ${context}`
);
logger.error(error.message);
}
};
const handleDirective = function(directive) {
logger.debug(`Directive type=${directive.type} with args:`, directive.args);
switch (directive.type) {
case 'init':
case 'initialize':
mermaidAPI.initialize(directive.args);
break;
case 'config':
updateConfig(directive.args);
break;
case 'wrap':
case 'nowrap':
wrapEnabled = directive.type === 'wrap';
break;
default:
logger.warn(
`Unrecognized directive: source: '%%{${directive.type}: ${directive.args}}%%`,
directive
);
break;
}
};
export const addActor = function(id, name, description) { export const addActor = function(id, name, description) {
// Don't allow description nulling // Don't allow description nulling
@ -13,9 +70,16 @@ export const addActor = function(id, name, description) {
if (old && name === old.name && description == null) return; if (old && name === old.name && description == null) return;
// Don't allow null descriptions, either // Don't allow null descriptions, either
if (description == null) description = name; if (description == null || description.text == null) {
description = { text: name, wrap: null };
}
actors[id] = { name: name, description: description, prevActor: prevActor }; actors[id] = {
name: name,
description: description.text,
wrap: (description.wrap === null && autoWrap()) || !!description.wrap,
prevActor: prevActor
};
if (prevActor && actors[prevActor]) { if (prevActor && actors[prevActor]) {
actors[prevActor].nextActor = id; actors[prevActor].nextActor = id;
} }
@ -24,7 +88,7 @@ export const addActor = function(id, name, description) {
}; };
const activationCount = part => { const activationCount = part => {
let i = 0; let i;
let count = 0; let count = 0;
for (i = 0; i < messages.length; i++) { for (i = 0; i < messages.length; i++) {
// console.warn(i, messages[i]); // console.warn(i, messages[i]);
@ -43,12 +107,27 @@ const activationCount = part => {
}; };
export const addMessage = function(idFrom, idTo, message, answer) { export const addMessage = function(idFrom, idTo, message, answer) {
messages.push({ from: idFrom, to: idTo, message: message, answer: answer }); messages.push({
from: idFrom,
to: idTo,
message: message.text,
wrap: (message.wrap === null && autoWrap()) || !!message.wrap,
answer: answer
});
}; };
export const addSignal = function(idFrom, idTo, message, messageType) { export const addSignal = function(idFrom, idTo, message = { text: null, wrap: null }, messageType) {
logger.debug( logger.debug(
'Adding message from=' + idFrom + ' to=' + idTo + ' message=' + message + ' type=' + messageType 'Adding message from=' +
idFrom +
' to=' +
idTo +
' message=' +
message.text +
' wrap=' +
message.wrap +
' type=' +
messageType
); );
if (messageType === LINETYPE.ACTIVE_END) { if (messageType === LINETYPE.ACTIVE_END) {
@ -56,7 +135,7 @@ export const addSignal = function(idFrom, idTo, message, messageType) {
logger.debug('Adding message from=', messages, cnt); logger.debug('Adding message from=', messages, cnt);
if (cnt < 1) { if (cnt < 1) {
// Bail out as there is an activation signal from an inactive participant // Bail out as there is an activation signal from an inactive participant
var error = new Error('Trying to inactivate an inactive participant (' + idFrom.actor + ')'); let error = new Error('Trying to inactivate an inactive participant (' + idFrom.actor + ')');
error.hash = { error.hash = {
text: '->>-', text: '->>-',
token: '->>-', token: '->>-',
@ -67,7 +146,13 @@ export const addSignal = function(idFrom, idTo, message, messageType) {
throw error; throw error;
} }
} }
messages.push({ from: idFrom, to: idTo, message: message, type: messageType }); messages.push({
from: idFrom,
to: idTo,
message: message.text,
wrap: (message.wrap === null && autoWrap()) || !!message.wrap,
type: messageType
});
return true; return true;
}; };
@ -87,14 +172,43 @@ export const getActorKeys = function() {
export const getTitle = function() { export const getTitle = function() {
return title; return title;
}; };
export const getTitleWrapped = function() {
return titleWrapped;
};
export const enableSequenceNumbers = function() { export const enableSequenceNumbers = function() {
sequenceNumbersEnabled = true; sequenceNumbersEnabled = true;
}; };
export const showSequenceNumbers = () => sequenceNumbersEnabled; export const showSequenceNumbers = () => sequenceNumbersEnabled;
export const enableWrap = function() {
wrapEnabled = true;
};
export const disableWrap = function() {
wrapEnabled = false;
};
export const autoWrap = () => wrapEnabled;
export const clear = function() { export const clear = function() {
actors = {}; actors = {};
messages = []; messages = [];
configUpdated = false;
};
export const parseMessage = function(str) {
const _str = str.trim();
return {
text: _str.replace(/^[:]?(?:no)?wrap:/, '').trim(),
wrap:
_str.match(/^[:]?(?:no)?wrap:/) === null
? autoWrap()
: _str.match(/^[:]?wrap:/) !== null
? true
: _str.match(/^[:]?nowrap:/) !== null
? false
: autoWrap()
};
}; };
export const LINETYPE = { export const LINETYPE = {
@ -133,7 +247,12 @@ export const PLACEMENT = {
}; };
export const addNote = function(actor, placement, message) { export const addNote = function(actor, placement, message) {
const note = { actor: actor, placement: placement, message: message }; const note = {
actor: actor,
placement: placement,
message: message.text,
wrap: (message.wrap === null && autoWrap()) || !!message.wrap
};
// Coerce actor into a [to, from, ...] array // Coerce actor into a [to, from, ...] array
const actors = [].concat(actor, actor); const actors = [].concat(actor, actor);
@ -142,14 +261,29 @@ export const addNote = function(actor, placement, message) {
messages.push({ messages.push({
from: actors[0], from: actors[0],
to: actors[1], to: actors[1],
message: message, message: message.text,
wrap: (message.wrap === null && autoWrap()) || !!message.wrap,
type: LINETYPE.NOTE, type: LINETYPE.NOTE,
placement: placement placement: placement
}); });
}; };
export const setTitle = function(titleText) { export const setTitle = function(titleWrap) {
title = titleText; title = titleWrap.text;
titleWrapped = (titleWrap.wrap === null && autoWrap()) || !!titleWrap.wrap;
};
export const updateConfig = function(config = getConfig()) {
try {
setConfig(config);
configUpdated = true;
} catch (error) {
logger.error('Error: unable to parse config');
}
};
export const hasConfigChange = function() {
return configUpdated;
}; };
export const apply = function(param) { export const apply = function(param) {
@ -221,14 +355,23 @@ export default {
addActor, addActor,
addMessage, addMessage,
addSignal, addSignal,
enableWrap,
disableWrap,
enableSequenceNumbers, enableSequenceNumbers,
showSequenceNumbers, showSequenceNumbers,
autoWrap,
getMessages, getMessages,
getActors, getActors,
getActor, getActor,
getActorKeys, getActorKeys,
getTitle, getTitle,
parseDirective,
hasConfigChange,
getConfig,
updateConfig,
getTitleWrapped,
clear, clear,
parseMessage,
LINETYPE, LINETYPE,
ARROWTYPE, ARROWTYPE,
PLACEMENT, PLACEMENT,

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import { logger } from '../../logger';
import { parser } from './parser/sequenceDiagram'; import { parser } from './parser/sequenceDiagram';
import common from '../common/common'; import common from '../common/common';
import sequenceDb from './sequenceDb'; import sequenceDb from './sequenceDb';
import { getConfig } from '../../config';
parser.yy = sequenceDb; parser.yy = sequenceDb;
@ -18,13 +19,17 @@ const conf = {
height: 65, height: 65,
actorFontSize: 14, actorFontSize: 14,
actorFontFamily: '"Open-Sans", "sans-serif"', actorFontFamily: '"Open-Sans", "sans-serif"',
// 400 = normal
actorFontWeight: 400,
// Note font settings // Note font settings
noteFontSize: 14, noteFontSize: 14,
noteFontFamily: '"trebuchet ms", verdana, arial', noteFontFamily: '"trebuchet ms", verdana, arial',
noteFontWeight: 400,
noteAlign: 'center', noteAlign: 'center',
// Message font settings // Message font settings
messageFontSize: 16, messageFontSize: 16,
messageFontFamily: '"trebuchet ms", verdana, arial', messageFontFamily: '"trebuchet ms", verdana, arial',
messageFontWeight: 400,
// Margin around loop boxes // Margin around loop boxes
boxMargin: 10, boxMargin: 10,
boxTextMargin: 5, boxTextMargin: 5,
@ -45,7 +50,12 @@ const conf = {
// text placement as: tspan | fo | old only text as before // text placement as: tspan | fo | old only text as before
textPlacement: 'tspan', textPlacement: 'tspan',
showSequenceNumbers: false showSequenceNumbers: false,
// wrap text
wrapEnabled: false,
// padding for wrapped text
wrapPadding: 15
}; };
export const bounds = { export const bounds = {
@ -69,6 +79,9 @@ export const bounds = {
stopy: undefined stopy: undefined
}; };
this.verticalPos = 0; this.verticalPos = 0;
if (parser.yy.hasConfigChange()) {
setConf(getConfig());
}
}, },
updateVal: function(obj, key, val, fun) { updateVal: function(obj, key, val, fun) {
if (typeof obj[key] === 'undefined') { if (typeof obj[key] === 'undefined') {
@ -138,8 +151,7 @@ export const bounds = {
return activation.actor; return activation.actor;
}) })
.lastIndexOf(message.from.actor); .lastIndexOf(message.from.actor);
const activation = this.activations.splice(lastActorActivationIdx, 1)[0]; return this.activations.splice(lastActorActivationIdx, 1)[0];
return activation;
}, },
newLoop: function(title, fill) { newLoop: function(title, fill) {
this.sequenceItems.push({ this.sequenceItems.push({
@ -152,8 +164,7 @@ export const bounds = {
}); });
}, },
endLoop: function() { endLoop: function() {
const loop = this.sequenceItems.pop(); return this.sequenceItems.pop();
return loop;
}, },
addSectionToLoop: function(message) { addSectionToLoop: function(message) {
const loop = this.sequenceItems.pop(); const loop = this.sequenceItems.pop();
@ -175,6 +186,55 @@ export const bounds = {
} }
}; };
const wrapLabel = (label, maxWidth, joinWith = '<br/>') => {
if (common.lineBreakRegex.test(label)) {
return label;
}
const words = label.split(' ');
const completedLines = [];
let nextLine = '';
words.forEach((word, index) => {
const wordLength = calculateTextWidth(`${word} `);
const nextLineLength = calculateTextWidth(nextLine);
if (wordLength > maxWidth) {
const { hyphenatedStrings, remainingWord } = breakString(word, maxWidth);
completedLines.push(nextLine, ...hyphenatedStrings);
nextLine = remainingWord;
} else if (nextLineLength + wordLength >= maxWidth) {
completedLines.push(nextLine);
nextLine = word;
} else {
nextLine = [nextLine, word].filter(Boolean).join(' ');
}
const currentWord = index + 1;
const isLastWord = currentWord === words.length;
if (isLastWord) {
completedLines.push(nextLine);
}
});
return completedLines.filter(line => line !== '').join(joinWith);
};
const breakString = (word, maxWidth, hyphenCharacter = '-') => {
const characters = word.split('');
const lines = [];
let currentLine = '';
characters.forEach((character, index) => {
const nextLine = `${currentLine}${character}`;
const lineWidth = calculateTextWidth(nextLine);
if (lineWidth >= maxWidth) {
const currentCharacter = index + 1;
const isLastLine = characters.length === currentCharacter;
const hyphenatedNextLine = `${nextLine}${hyphenCharacter}`;
lines.push(isLastLine ? nextLine : hyphenatedNextLine);
currentLine = '';
} else {
currentLine = nextLine;
}
});
return { hyphenatedStrings: lines, remainingWord: currentLine };
};
const _drawLongText = (text, x, y, g, width) => { const _drawLongText = (text, x, y, g, width) => {
let textHeight = 0; let textHeight = 0;
let prevTextHeight = 0; let prevTextHeight = 0;
@ -187,7 +247,6 @@ const _drawLongText = (text, x, y, g, width) => {
right: 'end', right: 'end',
end: 'end' end: 'end'
}; };
const lines = text.split(common.lineBreakRegex); const lines = text.split(common.lineBreakRegex);
for (const line of lines) { for (const line of lines) {
const textObj = svgDraw.getTextObj(); const textObj = svgDraw.getTextObj();
@ -215,6 +274,7 @@ const _drawLongText = (text, x, y, g, width) => {
.style('text-anchor', alignment) .style('text-anchor', alignment)
.style('font-size', conf.noteFontSize) .style('font-size', conf.noteFontSize)
.style('font-family', conf.noteFontFamily) .style('font-family', conf.noteFontFamily)
.style('font-weight', conf.noteFontWeight)
.attr('dominant-baseline', 'central') .attr('dominant-baseline', 'central')
.attr('alignment-baseline', 'central'); .attr('alignment-baseline', 'central');
@ -263,25 +323,48 @@ const drawNote = function(elem, startx, verticalPos, msg, forceWidth) {
* @param startx * @param startx
* @param stopx * @param stopx
* @param verticalPos * @param verticalPos
* @param txtCenter
* @param msg * @param msg
* @param sequenceIndex
*/ */
const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceIndex) { const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceIndex) {
const g = elem.append('g'); const g = elem.append('g');
const txtCenter = startx + (stopx - startx) / 2; const txtCenter = startx + (stopx - startx) / 2;
let textElems = []; let textElems = [];
/*
let textHeight = 0;
const breaklines = msg.message.split(common.lineBreakRegex);
for (const breakline of breaklines) {
let textElem = g
.append('text') // text label for the x axis
.attr('x', txtCenter)
.attr('y', verticalPos + textHeight)
.style('font-size', conf.messageFontSize)
.style('font-family', conf.messageFontFamily)
.style('font-weight', conf.messageFontWeight)
.style('text-anchor', 'middle')
.attr('class', 'messageText')
.text(breakline.trim());
textElems.push(textElem);
textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
}
let totalOffset = textHeight;
*/
let counterBreaklines = 0; let counterBreaklines = 0;
let breaklineOffset = 17; let breaklineOffset = conf.messageFontSize + 4;
const breaklines = msg.message.split(common.lineBreakRegex); const breaklines = msg.message.split(common.lineBreakRegex);
for (const breakline of breaklines) { for (const breakline of breaklines) {
textElems.push( textElems.push(
g g
.append('text') // text label for the x axis .append('text') // text label for the x axis
.attr('x', txtCenter) .attr('x', txtCenter)
// .attr('y', verticalPos - breaklineVerticalOffset + counterBreaklines * breaklineOffset)
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset) .attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
.style('font-size', conf.messageFontSize) .style('font-size', conf.messageFontSize)
.style('font-family', conf.messageFontFamily) .style('font-family', conf.messageFontFamily)
.style('font-weight', conf.messageFontWeight)
.style('text-anchor', 'middle') .style('text-anchor', 'middle')
.attr('class', 'messageText') .attr('class', 'messageText')
.text(breakline.trim()) .text(breakline.trim())
@ -290,7 +373,6 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
} }
const offsetLineCounter = counterBreaklines - 1; const offsetLineCounter = counterBreaklines - 1;
let totalOffset = offsetLineCounter * breaklineOffset; let totalOffset = offsetLineCounter * breaklineOffset;
let textWidths = textElems.map(function(textElem) { let textWidths = textElems.map(function(textElem) {
return (textElem._groups || textElem)[0][0].getBBox().width; return (textElem._groups || textElem)[0][0].getBBox().width;
}); });
@ -418,28 +500,42 @@ export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
// Draw the actors // Draw the actors
let prevWidth = 0; let prevWidth = 0;
let prevMargin = 0; let prevMargin = 0;
let maxActorHeight = conf.height;
for (let i = 0; i < actorKeys.length; i++) { for (let i = 0; i < actorKeys.length; i++) {
const actor = actors[actorKeys[i]]; const actor = actors[actorKeys[i]];
// Add some rendering data to the object // Add some rendering data to the object
actor.width = actor.width || calculateActorWidth(actor); actor.width = actor.width || calculateActorWidth(actor);
actor.height = conf.height; actor.height = actor.wrap
? calculateTextHeight(
actor.message,
conf.height,
actor.width,
conf.wrapPadding,
actor.wrap,
conf.actorFontSize
)
: conf.height;
maxActorHeight = Math.max(maxActorHeight, actor.height);
actor.margin = actor.margin || conf.actorMargin; actor.margin = actor.margin || conf.actorMargin;
actor.x = prevWidth + prevMargin; actor.x = prevWidth + prevMargin;
actor.y = verticalPos; actor.y = verticalPos;
if (actor.wrap) {
actor.description = wrapLabel(actor.description, actor.width);
}
// Draw the box with the attached line // Draw the box with the attached line
svgDraw.drawActor(diagram, actor, conf); svgDraw.drawActor(diagram, actor, conf);
bounds.insert(actor.x, verticalPos, actor.x + actor.width, conf.height); bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
prevWidth += actor.width; prevWidth += actor.width;
prevMargin += actor.margin; prevMargin += actor.margin;
} }
// Add a margin between the actor boxes and the first arrow // Add a margin between the actor boxes and the first arrow
bounds.bumpVerticalPos(conf.height); bounds.bumpVerticalPos(maxActorHeight);
}; };
export const setConf = function(cnf) { export const setConf = function(cnf) {
@ -450,7 +546,14 @@ export const setConf = function(cnf) {
}); });
if (cnf.fontFamily) { if (cnf.fontFamily) {
conf.actorFontFamily = conf.noteFontFamily = cnf.fontFamily; conf.actorFontFamily = conf.noteFontFamily = conf.messageFontFamily = cnf.fontFamily;
}
if (cnf.fontSize) {
conf.actorFontSize = conf.noteFontSize = conf.messageFontSize = cnf.fontSize;
// conf.height = cnf.fontSize * (65 / 14);
}
if (cnf.fontWeight) {
conf.actorFontWeight = conf.noteFontWeight = conf.messageFontWeight = cnf.fontWeight;
} }
}; };
@ -489,10 +592,50 @@ const calculateActorWidth = function(actor) {
return conf.width; return conf.width;
} }
return Math.max( return actor.wrap
conf.width, ? conf.width
calculateTextWidth(actor.description, conf.actorFontSize, conf.actorFontFamily) : Math.max(
); conf.width,
calculateTextWidth(
actor.description,
conf.actorFontSize,
conf.actorFontFamily,
conf.actorFontWeight
)
);
};
/**
* This calculates the text's height, taking into account the wrap value and
* both the statically configured height, width, and the length of the text (in pixels).
*
* If the wrapped text text has greater height, we extend the height, so it's
* value won't overflow.
*
* @return - The height for the given actor
* @param message the text to measure
* @param elementHeight the height of the default bounding box containing the text
* @param elementWidth the width of the default bounding box containing the text
* @param margin space above and below
* @param wrap wrap the text based on: elementWidth - 2 * margin
* @param fontSize
*/
export const calculateTextHeight = function(
message,
elementHeight,
elementWidth,
margin,
wrap,
fontSize
) {
if (!message) {
return elementHeight;
}
let lineHeightFactor = wrap
? wrapLabel(message, elementWidth - 2 * margin).split(common.lineBreakRegex).length
: 1;
return wrap ? Math.max(elementHeight, lineHeightFactor * fontSize) : elementHeight;
}; };
/** /**
@ -501,14 +644,16 @@ const calculateActorWidth = function(actor) {
* @param text - The text to calculate the width of * @param text - The text to calculate the width of
* @param fontSize - The font size of the given text * @param fontSize - The font size of the given text
* @param fontFamily - The font family (one, or more fonts) to render * @param fontFamily - The font family (one, or more fonts) to render
* @param fontWeight - The font weight (normal, bold, italics)
*/ */
export const calculateTextWidth = function(text, fontSize, fontFamily) { export const calculateTextWidth = function(text, fontSize, fontFamily, fontWeight) {
if (!text) { if (!text) {
return 0; return 0;
} }
fontSize = fontSize ? fontSize : conf.actorFontSize; fontSize = fontSize ? fontSize : conf.actorFontSize;
fontFamily = fontFamily ? fontFamily : conf.actorFontFamily; fontFamily = fontFamily ? fontFamily : conf.actorFontFamily;
fontWeight = fontWeight ? fontWeight : conf.actorFontWeight;
// We can't really know if the user supplied font family will render on the user agent; // We can't really know if the user supplied font family will render on the user agent;
// thus, we'll take the max width between the user supplied font family, and a default // thus, we'll take the max width between the user supplied font family, and a default
@ -518,7 +663,7 @@ export const calculateTextWidth = function(text, fontSize, fontFamily) {
let maxWidth = 0; let maxWidth = 0;
const body = select('body'); const body = select('body');
// We don'y want to leak DOM elements - if a removal operation isn't available // We don't want to leak DOM elements - if a removal operation isn't available
// for any reason, do not continue. // for any reason, do not continue.
if (!body.remove) { if (!body.remove) {
return 0; return 0;
@ -533,6 +678,7 @@ export const calculateTextWidth = function(text, fontSize, fontFamily) {
const textElem = svgDraw const textElem = svgDraw
.drawText(g, textObj) .drawText(g, textObj)
.style('font-size', fontSize) .style('font-size', fontSize)
.style('font-weight', fontWeight)
.style('font-family', fontFamily); .style('font-family', fontFamily);
maxWidth = Math.max(maxWidth, (textElem._groups || textElem)[0][0].getBBox().width); maxWidth = Math.max(maxWidth, (textElem._groups || textElem)[0][0].getBBox().width);
@ -542,7 +688,7 @@ export const calculateTextWidth = function(text, fontSize, fontFamily) {
g.remove(); g.remove();
// Adds some padding, so the text won't sit exactly within the actor's borders // Adds some padding, so the text won't sit exactly within the actor's borders
return maxWidth + 35; return maxWidth + conf.wrapPadding * 2;
}; };
/** /**
@ -568,10 +714,16 @@ export const draw = function(text, id) {
const title = parser.yy.getTitle(); const title = parser.yy.getTitle();
const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages); const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages);
calculateActorMargins(actors, maxMessageWidthPerActor); const maxActorHeight = calculateActorMargins(actors, maxMessageWidthPerActor);
drawActors(diagram, actors, actorKeys, 0); drawActors(diagram, actors, actorKeys, 0);
bounds.bumpVerticalPos(
maxActorHeight > conf.height
? Math.min(conf.boxMargin, Math.abs(maxActorHeight - conf.height))
: 0
);
// The arrow head definition is attached to the svg once // The arrow head definition is attached to the svg once
svgDraw.insertArrowHead(diagram); svgDraw.insertArrowHead(diagram);
svgDraw.insertArrowCrossHead(diagram); svgDraw.insertArrowCrossHead(diagram);
@ -597,11 +749,10 @@ export const draw = function(text, id) {
// Draw the messages/signals // Draw the messages/signals
let sequenceIndex = 1; let sequenceIndex = 1;
messages.forEach(function(msg) { messages.forEach(function(msg) {
let loopData; let loopData,
const noteWidth = Math.max( noteWidth,
conf.width, textWidth,
calculateTextWidth(msg.message, conf.noteFontSize, conf.noteFontFamily) shouldWrap = msg.wrap && msg.message && !common.lineBreakRegex.test(msg.message);
);
switch (msg.type) { switch (msg.type) {
case parser.yy.LINETYPE.NOTE: case parser.yy.LINETYPE.NOTE:
@ -609,8 +760,18 @@ export const draw = function(text, id) {
startx = actors[msg.from].x; startx = actors[msg.from].x;
stopx = actors[msg.to].x; stopx = actors[msg.to].x;
textWidth = calculateTextWidth(
msg.message,
conf.noteFontSize,
conf.noteFontFamily,
conf.noteFontWeight
);
noteWidth = shouldWrap ? conf.width : Math.max(conf.width, textWidth);
if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) { if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) {
if (shouldWrap) {
msg.message = wrapLabel(msg.message, noteWidth);
}
drawNote( drawNote(
diagram, diagram,
startx + (actors[msg.from].width + conf.actorMargin) / 2, startx + (actors[msg.from].width + conf.actorMargin) / 2,
@ -619,6 +780,9 @@ export const draw = function(text, id) {
noteWidth noteWidth
); );
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) { } else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
if (shouldWrap) {
msg.message = wrapLabel(msg.message, noteWidth);
}
drawNote( drawNote(
diagram, diagram,
startx - noteWidth + (actors[msg.from].width - conf.actorMargin) / 2, startx - noteWidth + (actors[msg.from].width - conf.actorMargin) / 2,
@ -628,6 +792,9 @@ export const draw = function(text, id) {
); );
} else if (msg.to === msg.from) { } else if (msg.to === msg.from) {
// Single-actor over // Single-actor over
if (shouldWrap) {
msg.message = wrapLabel(msg.message, noteWidth);
}
drawNote( drawNote(
diagram, diagram,
startx + (actors[msg.to].width - noteWidth) / 2, startx + (actors[msg.to].width - noteWidth) / 2,
@ -637,11 +804,17 @@ export const draw = function(text, id) {
); );
} else { } else {
// Multi-actor over // Multi-actor over
forceWidth = Math.abs(startx - stopx) + conf.actorMargin; forceWidth = Math.abs(startx - stopx) + conf.actorMargin / 2;
if (shouldWrap) {
noteWidth = forceWidth;
msg.message = wrapLabel(msg.message, noteWidth);
} else {
noteWidth = Math.max(forceWidth, textWidth - 2 * conf.noteMargin);
}
let x = let x =
startx < stopx startx < stopx
? startx + (actors[msg.from].width - conf.actorMargin) / 2 ? startx + (actors[msg.from].width - conf.actorMargin / 2) / 2
: stopx + (actors[msg.to].width - conf.actorMargin) / 2; : stopx + (actors[msg.to].width - conf.actorMargin / 2) / 2;
drawNote(diagram, x, bounds.getVerticalPos(), msg, forceWidth); drawNote(diagram, x, bounds.getVerticalPos(), msg, forceWidth);
} }
@ -703,6 +876,9 @@ export const draw = function(text, id) {
break; break;
case parser.yy.LINETYPE.PAR_START: case parser.yy.LINETYPE.PAR_START:
bounds.bumpVerticalPos(conf.boxMargin); bounds.bumpVerticalPos(conf.boxMargin);
if (shouldWrap) {
msg.message = wrapLabel(msg.message, conf.boxMargin);
}
bounds.newLoop(msg.message); bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin); bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break; break;
@ -726,6 +902,15 @@ export const draw = function(text, id) {
const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1; const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
startx = fromBounds[fromIdx]; startx = fromBounds[fromIdx];
stopx = toBounds[toIdx]; stopx = toBounds[toIdx];
if (shouldWrap) {
msg.message = wrapLabel(
msg.message,
Math.max(
Math.abs(stopx - startx) + conf.messageMargin * 2,
conf.width + conf.messageMargin * 2
)
);
}
const verticalPos = bounds.getVerticalPos(); const verticalPos = bounds.getVerticalPos();
drawMessage(diagram, startx, stopx, verticalPos, msg, sequenceIndex); drawMessage(diagram, startx, stopx, verticalPos, msg, sequenceIndex);
@ -823,12 +1008,12 @@ const getMaxMessageWidthPerActor = function(actors, messages) {
const actor = actors[msg.to]; const actor = actors[msg.to];
// If this is the first actor, and the message is left of it, no need to calculate the margin // If this is the first actor, and the message is left of it, no need to calculate the margin
if (msg.placement == parser.yy.PLACEMENT.LEFTOF && !actor.prevActor) { if (msg.placement === parser.yy.PLACEMENT.LEFTOF && !actor.prevActor) {
return; return;
} }
// If this is the last actor, and the message is right of it, no need to calculate the margin // If this is the last actor, and the message is right of it, no need to calculate the margin
if (msg.placement == parser.yy.PLACEMENT.RIGHTOF && !actor.nextActor) { if (msg.placement === parser.yy.PLACEMENT.RIGHTOF && !actor.nextActor) {
return; return;
} }
@ -837,7 +1022,13 @@ const getMaxMessageWidthPerActor = function(actors, messages) {
const fontSize = isNote ? conf.noteFontSize : conf.messageFontSize; const fontSize = isNote ? conf.noteFontSize : conf.messageFontSize;
const fontFamily = isNote ? conf.noteFontFamily : conf.messageFontFamily; const fontFamily = isNote ? conf.noteFontFamily : conf.messageFontFamily;
const messageWidth = calculateTextWidth(msg.message, fontSize, fontFamily); const fontWeight = isNote ? conf.noteFontWeight : conf.messageFontWeight;
const messageWidth = calculateTextWidth(
msg.wrap ? wrapLabel(msg.message, conf.width - conf.noteMargin) : msg.message,
fontSize,
fontFamily,
fontWeight
);
/* /*
* The following scenarios should be supported: * The following scenarios should be supported:
@ -855,25 +1046,25 @@ const getMaxMessageWidthPerActor = function(actors, messages) {
* - If the note is on the right of the actor, we should define the current actor * - If the note is on the right of the actor, we should define the current actor
* margin * margin
*/ */
if (isMessage && msg.from == actor.nextActor) { if (isMessage && msg.from === actor.nextActor) {
maxMessageWidthPerActor[msg.to] = Math.max( maxMessageWidthPerActor[msg.to] = Math.max(
maxMessageWidthPerActor[msg.to] || 0, maxMessageWidthPerActor[msg.to] || 0,
messageWidth messageWidth
); );
} else if ( } else if (
(isMessage && msg.from == actor.prevActor) || (isMessage && msg.from === actor.prevActor) ||
msg.placement == parser.yy.PLACEMENT.RIGHTOF msg.placement === parser.yy.PLACEMENT.RIGHTOF
) { ) {
maxMessageWidthPerActor[msg.from] = Math.max( maxMessageWidthPerActor[msg.from] = Math.max(
maxMessageWidthPerActor[msg.from] || 0, maxMessageWidthPerActor[msg.from] || 0,
messageWidth messageWidth
); );
} else if (msg.placement == parser.yy.PLACEMENT.LEFTOF) { } else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
maxMessageWidthPerActor[actor.prevActor] = Math.max( maxMessageWidthPerActor[actor.prevActor] = Math.max(
maxMessageWidthPerActor[actor.prevActor] || 0, maxMessageWidthPerActor[actor.prevActor] || 0,
messageWidth messageWidth
); );
} else if (msg.placement == parser.yy.PLACEMENT.OVER) { } else if (msg.placement === parser.yy.PLACEMENT.OVER) {
if (actor.prevActor) { if (actor.prevActor) {
maxMessageWidthPerActor[actor.prevActor] = Math.max( maxMessageWidthPerActor[actor.prevActor] = Math.max(
maxMessageWidthPerActor[actor.prevActor] || 0, maxMessageWidthPerActor[actor.prevActor] || 0,
@ -905,6 +1096,7 @@ const getMaxMessageWidthPerActor = function(actors, messages) {
* @param actorToMessageWidth - A map of actor key -> max message width it holds * @param actorToMessageWidth - A map of actor key -> max message width it holds
*/ */
const calculateActorMargins = function(actors, actorToMessageWidth) { const calculateActorMargins = function(actors, actorToMessageWidth) {
let maxHeight = 0;
for (let actorKey in actorToMessageWidth) { for (let actorKey in actorToMessageWidth) {
const actor = actors[actorKey]; const actor = actors[actorKey];
@ -919,21 +1111,42 @@ const calculateActorMargins = function(actors, actorToMessageWidth) {
continue; continue;
} }
actor.width = Math.max( [actor, nextActor].forEach(function(act) {
conf.width, act.width = act.wrap
calculateTextWidth(actor.description, conf.actorFontSize, conf.actorFontFamily) ? conf.width
); : Math.max(
conf.width,
calculateTextWidth(
act.description,
conf.actorFontSize,
conf.actorFontFamily,
conf.actorFontWeight
)
);
nextActor.width = Math.max( act.height = act.wrap
conf.width, ? calculateTextHeight(
calculateTextWidth(nextActor.description, conf.actorFontSize, conf.actorFontFamily) act.description,
); conf.height,
actor.width,
conf.actorMargin,
act.wrap,
conf.actorFontSize
)
: conf.height;
maxHeight = Math.max(maxHeight, act.height);
});
const messageWidth = actorToMessageWidth[actorKey]; const messageWidth = actorToMessageWidth[actorKey];
const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2; const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2;
actor.margin = Math.max(actorWidth, conf.actorMargin); actor.margin = Math.max(actorWidth, conf.actorMargin);
} }
Object.keys(actors).forEach(function(key) {
actors[key].height = maxHeight;
});
return maxHeight;
}; };
export default { export default {

View File

@ -26,6 +26,9 @@ export const drawText = function(elem, textData) {
textElem.attr('x', textData.x); textElem.attr('x', textData.x);
textElem.attr('y', textData.y); textElem.attr('y', textData.y);
textElem.style('text-anchor', textData.anchor); textElem.style('text-anchor', textData.anchor);
textElem.style('font-family', textData.fontFamily);
textElem.style('font-size', textData.fontSize);
textElem.style('font-weight', textData.fontWeight);
textElem.attr('fill', textData.fill); textElem.attr('fill', textData.fill);
if (typeof textData.class !== 'undefined') { if (typeof textData.class !== 'undefined') {
textElem.attr('class', textData.class); textElem.attr('class', textData.class);
@ -77,7 +80,7 @@ let actorCnt = -1;
* Draws an actor in the diagram with the attaced line * Draws an actor in the diagram with the attaced line
* @param elem - The diagram we'll draw to. * @param elem - The diagram we'll draw to.
* @param actor - The actor to draw. * @param actor - The actor to draw.
* @param config - The sequence diagram config object. * @param conf - drawText implementation discriminator object
*/ */
export const drawActor = function(elem, actor, conf) { export const drawActor = function(elem, actor, conf) {
const center = actor.x + actor.width / 2; const center = actor.x + actor.width / 2;
@ -146,7 +149,7 @@ export const drawActivation = function(elem, bounds, verticalPos, conf, actorAct
* @param elem - elemenet to append the loop to. * @param elem - elemenet to append the loop to.
* @param bounds - bounds of the given loop. * @param bounds - bounds of the given loop.
* @param labelText - Text within the loop. * @param labelText - Text within the loop.
* @param config - sequence diagram config object. * @param conf
*/ */
export const drawLoop = function(elem, bounds, labelText, conf) { export const drawLoop = function(elem, bounds, labelText, conf) {
const g = elem.append('g'); const g = elem.append('g');
@ -169,11 +172,17 @@ export const drawLoop = function(elem, bounds, labelText, conf) {
}); });
} }
let minSize =
Math.round((3 * conf.fontSize) / 4) < 10 ? conf.fontSize : Math.round((3 * conf.fontSize) / 4);
let txt = getTextObj(); let txt = getTextObj();
txt.text = labelText; txt.text = labelText;
txt.x = bounds.startx; txt.x = bounds.startx;
txt.y = bounds.starty; txt.y = bounds.starty;
txt.labelMargin = 1.5 * 10; // This is the small box that says "loop" txt.labelMargin = 1.5 * 10; // This is the small box that says "loop"
txt.fontFamily = conf.fontFamily;
txt.fontSize = minSize;
txt.fontWeight = conf.fontWeight;
txt.class = 'labelText'; // Its size & position are fixed. txt.class = 'labelText'; // Its size & position are fixed.
drawLabel(g, txt); drawLabel(g, txt);
@ -184,23 +193,31 @@ export const drawLoop = function(elem, bounds, labelText, conf) {
txt.y = bounds.starty + 1.5 * conf.boxMargin; txt.y = bounds.starty + 1.5 * conf.boxMargin;
txt.anchor = 'middle'; txt.anchor = 'middle';
txt.class = 'loopText'; txt.class = 'loopText';
txt.fontFamily = conf.fontFamily;
txt.fontSize = minSize;
txt.fontWeight = conf.fontWeight;
drawText(g, txt); let textElem = drawText(g, txt);
let textHeight = (textElem._groups || textElem)[0][0].getBBox().height;
if (typeof bounds.sectionTitles !== 'undefined') { if (typeof bounds.sectionTitles !== 'undefined') {
bounds.sectionTitles.forEach(function(item, idx) { bounds.sectionTitles.forEach(function(item, idx) {
if (item !== '') { if (item !== '') {
txt.text = '[ ' + item + ' ]'; txt.text = '[ ' + item + ' ]';
txt.y = bounds.sections[idx] + 1.5 * conf.boxMargin; txt.y = bounds.sections[idx] + 1.5 * conf.boxMargin;
drawText(g, txt); textElem = drawText(g, txt);
textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
} }
}); });
} }
return textHeight + 4;
}; };
/** /**
* Draws a background rectangle * Draws a background rectangle
* @param color - The fill color for the background * @param elem diagram (reference for bounds)
* @param bounds shape of the rectangle
*/ */
export const drawBackgroundRect = function(elem, bounds) { export const drawBackgroundRect = function(elem, bounds) {
const rectElem = drawRect(elem, { const rectElem = drawRect(elem, {
@ -325,7 +342,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, actorFontWeight } = conf;
const lines = content.split(common.lineBreakRegex); const lines = content.split(common.lineBreakRegex);
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
@ -336,6 +353,7 @@ const _drawTextCandidateFunc = (function() {
.attr('y', y) .attr('y', y)
.style('text-anchor', 'middle') .style('text-anchor', 'middle')
.style('font-size', actorFontSize) .style('font-size', actorFontSize)
.style('font-weight', actorFontWeight)
.style('font-family', actorFontFamily); .style('font-family', actorFontFamily);
text text
.append('tspan') .append('tspan')

View File

@ -145,10 +145,11 @@ const setupNode = (g, parent, node, altFlag) => {
arrowType: '', arrowType: '',
style: 'fill:none', style: 'fill:none',
labelStyle: '', labelStyle: '',
classes: 'note-edge', classes: 'transition note-edge',
arrowheadStyle: 'fill: #333', arrowheadStyle: 'fill: #333',
labelpos: 'c', labelpos: 'c',
labelType: 'text' labelType: 'text',
thickness: 'normal'
}); });
} else { } else {
g.setNode(node.id, nodeData); g.setNode(node.id, nodeData);
@ -184,7 +185,9 @@ const setupDoc = (g, parent, doc, altFlag) => {
label: item.description, label: item.description,
arrowheadStyle: 'fill: #333', arrowheadStyle: 'fill: #333',
labelpos: 'c', labelpos: 'c',
labelType: 'text' labelType: 'text',
thickness: 'normal',
classes: 'transition'
}; };
let startId = item.state1.id; let startId = item.state1.id;
let endId = item.state2.id; let endId = item.state2.id;

View File

@ -16,7 +16,13 @@ export const logger = {
fatal: () => {} fatal: () => {}
}; };
export const setLogLevel = function(level) { export const setLogLevel = function(level = 'fatal') {
if (isNaN(level)) {
level = level.toLowerCase();
if (LEVELS[level] !== undefined) {
level = LEVELS[level];
}
}
logger.trace = () => {}; logger.trace = () => {};
logger.debug = () => {}; logger.debug = () => {};
logger.info = () => {}; logger.info = () => {};

View File

@ -285,6 +285,11 @@ const config = {
* **Default value "Open-Sans", "sans-serif"**. * **Default value "Open-Sans", "sans-serif"**.
*/ */
actorFontFamily: '"Open-Sans", "sans-serif"', actorFontFamily: '"Open-Sans", "sans-serif"',
/**
* This sets the font weight of the actor's description
* **Default value 400.
*/
actorFontWeight: 400,
/** /**
* This sets the font size of actor-attached notes. * This sets the font size of actor-attached notes.
* **Default value 14**. * **Default value 14**.
@ -295,6 +300,11 @@ const config = {
* **Default value "trebuchet ms", verdana, arial**. * **Default value "trebuchet ms", verdana, arial**.
*/ */
noteFontFamily: '"trebuchet ms", verdana, arial', noteFontFamily: '"trebuchet ms", verdana, arial',
/**
* This sets the font weight of the note's description
* **Default value 400.
*/
noteFontWeight: 400,
/** /**
* This sets the text alignment of actor-attached notes. * This sets the text alignment of actor-attached notes.
* **Default value center**. * **Default value center**.
@ -309,7 +319,22 @@ const config = {
* This sets the font family of actor messages. * This sets the font family of actor messages.
* **Default value "trebuchet ms", verdana, arial**. * **Default value "trebuchet ms", verdana, arial**.
*/ */
messageFontFamily: '"trebuchet ms", verdana, arial' messageFontFamily: '"trebuchet ms", verdana, arial',
/**
* This sets the font weight of the message's description
* **Default value 400.
*/
messageFontWeight: 400,
/**
* This sets the auto-wrap state for the diagram
* **Default value false.
*/
wrapEnabled: false,
/**
* This sets the auto-wrap padding for the diagram (sides only)
* **Default value 15.
*/
wrapPadding: 15
}, },
/** /**
@ -536,6 +561,11 @@ setLogLevel(config.logLevel);
setConfig(config); setConfig(config);
function parse(text) { function parse(text) {
const graphInit = utils.detectInit(text);
if (graphInit) {
reinitialize(graphInit);
logger.debug('Init ', graphInit);
}
const graphType = utils.detectType(text); const graphType = utils.detectType(text);
let parser; let parser;
@ -711,6 +741,10 @@ const render = function(id, _txt, cb, container) {
txt = encodeEntities(txt); txt = encodeEntities(txt);
const element = select('#d' + id).node(); const element = select('#d' + id).node();
const graphInit = utils.detectInit(txt);
if (graphInit) {
reinitialize(graphInit);
}
const graphType = utils.detectType(txt); const graphType = utils.detectType(txt);
// insert inline style into svg // insert inline style into svg
@ -919,15 +953,25 @@ const setConf = function(cnf) {
} }
}; };
function initialize(options) { function reinitialize(options) {
logger.debug('Initializing mermaidAPI ', pkg.version);
// Update default config with options supplied at initialization
if (typeof options === 'object') { if (typeof options === 'object') {
setConf(options); setConf(options);
} }
setConfig(config); setConfig(config);
setLogLevel(config.logLevel); setLogLevel(config.logLevel);
logger.debug('RE-Initializing mermaidAPI ', { version: pkg.version, options, config });
}
function initialize(options) {
let _config = config;
logger.debug('Initializing mermaidAPI ', { version: pkg.version, options, _config });
// Update default config with options supplied at initialization
if (typeof options === 'object') {
_config = Object.assign(_config, options);
setConf(_config);
}
setConfig(_config);
setLogLevel(_config.logLevel);
} }
// function getConfig () { // function getConfig () {
@ -939,6 +983,7 @@ const mermaidAPI = {
render, render,
parse, parse,
initialize, initialize,
reinitialize,
getConfig getConfig
}; };

View File

@ -1,5 +1,5 @@
$mainBkg: #BDD5EA; $mainBkg: #1f2020;
$secondBkg: #6D6D65; $secondBkg: lighten(#1f2020, 16);
$mainContrastColor: lightgrey; $mainContrastColor: lightgrey;
$darkTextColor: #323D47; $darkTextColor: #323D47;
$lineColor: $mainContrastColor; $lineColor: $mainContrastColor;
@ -21,18 +21,18 @@ $edgeLabelBackground: #e8e8e8;
$actorBorder: $border1; $actorBorder: $border1;
$actorBkg: $mainBkg; $actorBkg: $mainBkg;
$actorTextColor: black; $actorTextColor: $mainContrastColor;
$actorLineColor: $mainContrastColor; $actorLineColor: $mainContrastColor;
$signalColor: $mainContrastColor; $signalColor: $mainContrastColor;
$signalTextColor: $mainContrastColor; $signalTextColor: $mainContrastColor;
$labelBoxBkgColor: $actorBkg; $labelBoxBkgColor: $actorBkg;
$labelBoxBorderColor: $actorBorder; $labelBoxBorderColor: $actorBorder;
$labelTextColor: $darkTextColor; $labelTextColor: $mainContrastColor;
$loopTextColor: $mainContrastColor; $loopTextColor: $mainContrastColor;
$noteBorderColor: $border2; $noteBorderColor: $border2;
$noteBkgColor: #fff5ad; $noteBkgColor: #fff5ad;
$activationBorderColor: #666; $activationBorderColor: $border1;
$activationBkgColor: #f4f4f4; $activationBkgColor: $secondBkg;
$sequenceNumberColor: white; $sequenceNumberColor: white;
/* Gantt chart variables */ /* Gantt chart variables */

View File

@ -34,6 +34,11 @@
stroke-width: 1.5px; stroke-width: 1.5px;
} }
.flowchart-link {
stroke: $lineColor;
fill: none;
}
.edgeLabel { .edgeLabel {
background-color: $edgeLabelBackground; background-color: $edgeLabelBackground;
rect { rect {

View File

@ -17,10 +17,38 @@
// --mermaid-alt-font-family: '"Lucida Console", Monaco, monospace'; // --mermaid-alt-font-family: '"Lucida Console", Monaco, monospace';
} }
/* Classes common for multiple diagrams */
.error-icon { .error-icon {
fill: $errorBkgColor; fill: $errorBkgColor;
} }
.error-text { .error-text {
fill: $errorTextColor; fill: $errorTextColor;
stroke: $errorTextColor; stroke: $errorTextColor;
} }
.edge-thickness-normal {
// stroke: $lineColor;
stroke-width: 2px;
}
.edge-thickness-thick {
// stroke: $lineColor;
stroke-width: 3.5px
}
.edge-pattern-solid {
stroke-dasharray: 0;
}
.edge-pattern-dashed{
stroke-dasharray: 3;
}
.edge-pattern-dotted {
stroke-dasharray: 2;
}
.marker {
fill: $lineColor;
}
.marker.cross {
stroke: $lineColor;
}

View File

@ -3,7 +3,7 @@
fill: $actorBkg; fill: $actorBkg;
} }
text.actor { text.actor > tspan {
fill: $actorTextColor; fill: $actorTextColor;
stroke: none; stroke: none;
} }
@ -14,18 +14,19 @@ text.actor {
.messageLine0 { .messageLine0 {
stroke-width: 1.5; stroke-width: 1.5;
stroke-dasharray: '2 2'; stroke-dasharray: none;
stroke: $signalColor; stroke: $signalColor;
} }
.messageLine1 { .messageLine1 {
stroke-width: 1.5; stroke-width: 1.5;
stroke-dasharray: '2 2'; stroke-dasharray: 2, 2;
stroke: $signalColor; stroke: $signalColor;
} }
#arrowhead { #arrowhead path {
fill: $signalColor; fill: $signalColor;
stroke: $signalColor;
} }
.sequenceNumber { .sequenceNumber {
@ -37,13 +38,13 @@ text.actor {
} }
#crosshead path { #crosshead path {
fill: $signalColor !important; fill: $signalColor;
stroke: $signalColor !important; stroke: $signalColor;
} }
.messageText { .messageText {
fill: $signalTextColor; fill: $signalTextColor;
stroke: none; stroke: $signalTextColor;
} }
.labelBox { .labelBox {
@ -51,20 +52,21 @@ text.actor {
fill: $labelBoxBkgColor; fill: $labelBoxBkgColor;
} }
.labelText { .labelText, .labelText > tspan {
fill: $labelTextColor; fill: $labelTextColor;
stroke: none; stroke: none;
} }
.loopText { .loopText, .loopText > tspan {
fill: $loopTextColor; fill: $loopTextColor;
stroke: none; stroke: none;
} }
.loopLine { .loopLine {
stroke-width: 2; stroke-width: 2px;
stroke-dasharray: '2 2'; stroke-dasharray: 2, 2;
stroke: $labelBoxBorderColor; stroke: $labelBoxBorderColor;
fill: $labelBoxBorderColor;
} }
.note { .note {
@ -74,11 +76,8 @@ text.actor {
} }
.noteText { .noteText {
fill: black; fill: $actorBkg;
stroke: none; stroke: none;
font-family: 'trebuchet ms', verdana, arial;
font-family: var(--mermaid-font-family);
font-size: 14px;
} }
.activation0 { .activation0 {
@ -95,3 +94,14 @@ text.actor {
fill: $activationBkgColor; fill: $activationBkgColor;
stroke: $activationBorderColor; stroke: $activationBorderColor;
} }
g > rect[class="note"] {
stroke: $noteBorderColor;
fill: $noteBkgColor;
}
g > text[class="noteText"] > tspan {
stroke: $actorBkg;
fill: $actorBkg;
stroke-width: 1px;
}

View File

@ -28,11 +28,124 @@ const d3CurveTypes = {
curveStepAfter: curveStepAfter, curveStepAfter: curveStepAfter,
curveStepBefore: curveStepBefore curveStepBefore: curveStepBefore
}; };
const directive = /[%]{2}[{]\s*(?:(?:(\w+)\s*:|(\w+))\s*(?:(?:(\w+))|((?:(?![}][%]{2}).|\r?\n)*))?\s*)(?:[}][%]{2})?/gi;
const directiveWithoutOpen = /\s*(?:(?:(\w+)(?=:):|(\w+))\s*(?:(?:(\w+))|((?:(?![}][%]{2}).|\r?\n)*))?\s*)(?:[}][%]{2})?/gi;
const anyComment = /\s*%%.*\n/gm;
/**
* @function detectInit
* Detects the init config object from the text
* ```mermaid
* %%{init: {"theme": "debug", "logLevel": 1 }}%%
* graph LR
* a-->b
* b-->c
* c-->d
* d-->e
* e-->f
* f-->g
* g-->h
* ```
* or
* ```mermaid
* %%{initialize: {"theme": "dark", logLevel: "debug" }}%%
* graph LR
* a-->b
* b-->c
* c-->d
* d-->e
* e-->f
* f-->g
* g-->h
* ```
*
* @param {string} text The text defining the graph
* @returns {object} the json object representing the init to pass to mermaid.initialize()
*/
export const detectInit = function(text) {
let inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
let results = {};
if (Array.isArray(inits)) {
let args = inits.map(init => init.args);
results = Object.assign(results, ...args);
} else {
results = inits.args;
}
return results;
};
/**
* @function detectDirective
* Detects the directive from the text. Text can be single line or multiline. If type is null or omitted
* the first directive encountered in text will be returned
* ```mermaid
* graph LR
* %%{somedirective}%%
* a-->b
* b-->c
* c-->d
* d-->e
* e-->f
* f-->g
* g-->h
* ```
*
* @param {string} text The text defining the graph
* @param {string|RegExp} type The directive to return (default: null
* @returns {object | Array} An object or Array representing the directive(s): { type: string, args: object|null } matchd by the input type
* if a single directive was found, that directive object will be returned.
*/
export const detectDirective = function(text, type = null) {
try {
const commentWithoutDirectives = new RegExp(
`[%]{2}(?![{]${directiveWithoutOpen.source})(?=[}][%]{2}).*\n`,
'ig'
);
text = text
.trim()
.replace(commentWithoutDirectives, '')
.replace(/'/gm, '"');
logger.debug(
`Detecting diagram directive${type !== null ? ' type:' + type : ''} based on the text:${text}`
);
let match,
result = [];
while ((match = directive.exec(text)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (match.index === directive.lastIndex) {
directive.lastIndex++;
}
if (
(match && !type) ||
(type && match[1] && match[1].match(type)) ||
(type && match[2] && match[2].match(type))
) {
let type = match[1] ? match[1] : match[2];
let args = match[3] ? match[3].trim() : match[4] ? JSON.parse(match[4].trim()) : null;
result.push({ type, args });
}
}
if (result.length === 0) {
result.push({ type: text, args: null });
}
return result.length === 1 ? result[0] : result;
} catch (error) {
logger.error(
`ERROR: ${error.message} - Unable to parse directive${
type !== null ? ' type:' + type : ''
} based on the text:${text}`
);
return { type: null, args: null };
}
};
/** /**
* @function detectType * @function detectType
* Detects the type of the graph text. * Detects the type of the graph text. Takes into consideration the possible existence of an %%init
* directive
* ```mermaid * ```mermaid
* %%{initialize: {"startOnLoad": true, logLevel: "fatal" }}%%
* graph LR * graph LR
* a-->b * a-->b
* b-->c * b-->c
@ -47,7 +160,7 @@ const d3CurveTypes = {
* @returns {string} A graph definition key * @returns {string} A graph definition key
*/ */
export const detectType = function(text) { export const detectType = function(text) {
text = text.replace(/^\s*%%.*\n/g, '\n'); text = text.replace(directive, '').replace(anyComment, '\n');
logger.debug('Detecting diagram type based on the text ' + text); logger.debug('Detecting diagram type based on the text ' + text);
if (text.match(/^\s*sequenceDiagram/)) { if (text.match(/^\s*sequenceDiagram/)) {
return 'sequence'; return 'sequence';
@ -127,6 +240,21 @@ export const formatUrl = (linkStr, config) => {
} }
}; };
export const runFunc = (functionName, ...params) => {
var arrPaths = functionName.split('.');
var len = arrPaths.length - 1;
var fnName = arrPaths[len];
var obj = window;
for (var i = 0; i < len; i++) {
obj = obj[arrPaths[i]];
if (!obj) return;
}
obj[fnName](...params);
};
const distance = (p1, p2) => const distance = (p1, p2) =>
p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0; p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0;
@ -255,6 +383,8 @@ export const generateId = () => {
}; };
export default { export default {
detectInit,
detectDirective,
detectType, detectType,
isSubstringInArray, isSubstringInArray,
interpolateToCurve, interpolateToCurve,
@ -262,5 +392,6 @@ export default {
calcCardinalityPosition, calcCardinalityPosition,
formatUrl, formatUrl,
getStylesFromArray, getStylesFromArray,
generateId generateId,
runFunc
}; };

View File

@ -2,23 +2,73 @@
import utils from './utils'; import utils from './utils';
describe('when detecting chart type ', function() { describe('when detecting chart type ', function() {
it('should handle a graph defintion', function() { it('should handle a graph definition', function() {
const str = 'graph TB\nbfs1:queue'; const str = 'graph TB\nbfs1:queue';
const type = utils.detectType(str); const type = utils.detectType(str);
expect(type).toBe('flowchart'); expect(type).toBe('flowchart');
}); });
it('should handle a graph defintion with leading spaces', function() { it('should handle an initialize definition', function() {
const str = `
%%{initialize: { 'logLevel': 0, 'theme': 'dark' }}%%
sequenceDiagram
Alice->Bob: hi`;
const type = utils.detectType(str);
const init = utils.detectInit(str);
expect(type).toBe('sequence');
expect(init).toEqual({logLevel:0,theme:"dark"});
});
it('should handle an init definition', function() {
const str = `
%%{init: { 'logLevel': 0, 'theme': 'dark' }}%%
sequenceDiagram
Alice->Bob: hi`;
const type = utils.detectType(str);
const init = utils.detectInit(str);
expect(type).toBe('sequence');
expect(init).toEqual({logLevel:0,theme:"dark"});
});
it('should handle a multiline init definition', function() {
const str = `
%%{
init: {
'logLevel': 0,
'theme': 'dark'
}
}%%
sequenceDiagram
Alice->Bob: hi`;
const type = utils.detectType(str);
const init = utils.detectInit(str);
expect(type).toBe('sequence');
expect(init).toEqual({logLevel:0,theme:"dark"});
});
it('should handle multiple init directives', function() {
const str = `
%%{ init: { 'logLevel': 0, 'theme': 'forest' } }%%
%%{
init: {
'theme': 'dark'
}
}%%
sequenceDiagram
Alice->Bob: hi`;
const type = utils.detectType(str);
const init = utils.detectInit(str);
expect(type).toBe('sequence');
expect(init).toEqual({logLevel:0,theme:"dark"});
});
it('should handle a graph definition with leading spaces', function() {
const str = ' graph TB\nbfs1:queue'; const str = ' graph TB\nbfs1:queue';
const type = utils.detectType(str); const type = utils.detectType(str);
expect(type).toBe('flowchart'); expect(type).toBe('flowchart');
}); });
it('should handle a graph defintion with leading spaces and newline', function() { it('should handle a graph definition with leading spaces and newline', function() {
const str = ' \n graph TB\nbfs1:queue'; const str = ' \n graph TB\nbfs1:queue';
const type = utils.detectType(str); const type = utils.detectType(str);
expect(type).toBe('flowchart'); expect(type).toBe('flowchart');
}); });
it('should handle a graph defintion for gitGraph', function() { it('should handle a graph definition for gitGraph', function() {
const str = ' \n gitGraph TB:\nbfs1:queue'; const str = ' \n gitGraph TB:\nbfs1:queue';
const type = utils.detectType(str); const type = utils.detectType(str);
expect(type).toBe('git'); expect(type).toBe('git');