Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Box support in Sequence Diagrams #3965

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions cypress/integration/rendering/sequencediagram.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,42 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util';

context('Sequence diagram', () => {
it('should render a sequence diagram with boxes', () => {
oleveau marked this conversation as resolved.
Show resolved Hide resolved
renderGraph(
`
sequenceDiagram
box LightGrey Alice and Bob
participant Alice
participant Bob
end
participant John as John<br/>Second Line
Alice ->> Bob: Hello Bob, how are you?
Bob-->>John: How about you John?
Bob--x Alice: I am good thanks!
Bob-x John: I am good thanks!
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
Bob-->Alice: Checking with John...
alt either this
Alice->>John: Yes
else or this
Alice->>John: No
else or this will happen
Alice->John: Maybe
end
par this happens in parallel
Alice -->> Bob: Parallel message 1
and
Alice -->> John: Parallel message 2
end
`,
{ sequence: { useMaxWidth: false } }
);
cy.get('svg').should((svg) => {
const width = parseFloat(svg.attr('width'));
expect(width).to.be.within(830 * 0.95, 830 * 1.05);
expect(svg).to.not.have.attr('style');
});
});
it('should render a simple sequence diagram', () => {
imgSnapshotTest(
`
Expand Down
16 changes: 16 additions & 0 deletions demos/sequence.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,22 @@ <h1>Sequence diagram demos</h1>
</pre>
<hr />

<pre class="mermaid">
sequenceDiagram
box lightgreen Alice & John
participant A
participant J
end
box Another Group very very long description not wrapped
participant B
end
A->>J: Hello John, how are you?
J->>A: Great!
A->>B: Hello Bob, how are you ?
</pre
>
<hr />

<script src="./mermaid.js"></script>
<script>
mermaid.initialize({
Expand Down
53 changes: 53 additions & 0 deletions docs/syntax/sequenceDiagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,59 @@ sequenceDiagram
J->>A: Great!
```

### Grouping / Box

The actor(s) can be grouped in vertical boxes. You can define a color (if not, it will be transparent) and/or a descriptive label using the following notation:

box Aqua Group Description
... actors ...
end
box Group without description
... actors ...
end
box rgb(33,66,99)
... actors ...
end

> **Note**
> If your group name is a color you can force the color to be transparent:

box transparent Aqua
... actors ...
end

```mermaid-example
sequenceDiagram
box Purple Alice & John
participant A
participant J
end
box Another Group
participant B
participant C
end
A->>J: Hello John, how are you?
J->>A: Great!
A->>B: Hello Bob, how is Charly ?
B->>C: Hello Charly, how are you?
```

```mermaid
sequenceDiagram
box Purple Alice & John
participant A
participant J
end
box Another Group
participant B
participant C
end
A->>J: Hello John, how are you?
J->>A: Great!
A->>B: Hello Bob, how is Charly ?
B->>C: Hello Charly, how are you?
```

## Messages

Messages can be of two displayed either solid or with a dotted line.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@
\%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
[0-9]+(?=[ \n]+) return 'NUM';
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
<ID>[^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
Expand Down Expand Up @@ -117,16 +118,30 @@ line
| NEWLINE { $$=[]; }
;

box_section
: /* empty */ { $$ = [] }
| box_section box_line {$1.push($2);$$ = $1}
;

box_line
: SPACE participant_statement { $$ = $2 }
| participant_statement { $$ = $1 }
| NEWLINE { $$=[]; }
;


directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;

statement
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;}
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;}
: participant_statement
| 'box' restOfLine box_section end
{
$3.unshift({type: 'boxStart', boxData:yy.parseBoxData($2) });
$3.push({type: 'boxEnd', boxText:$2});
$$=$3;}
| signal 'NEWLINE'
| autonumber NUM NUM 'NEWLINE' { $$= {type:'sequenceIndex',sequenceIndex: Number($2), sequenceIndexStep:Number($3), sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER};}
| autonumber NUM 'NEWLINE' { $$ = {type:'sequenceIndex',sequenceIndex: Number($2), sequenceIndexStep:1, sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER};}
Expand Down Expand Up @@ -209,6 +224,13 @@ else_sections
{ $$ = $1.concat([{type: 'else', altText:yy.parseMessage($3), signalType: yy.LINETYPE.ALT_ELSE}, $4]); }
;

participant_statement
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;}
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;}
;

note_statement
: 'note' placement actor text2
{
Expand Down
112 changes: 109 additions & 3 deletions packages/mermaid/src/diagrams/sequence/sequenceDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,52 @@ import {

let prevActor = undefined;
let actors = {};
let boxes = [];
let messages = [];
const notes = [];
let sequenceNumbersEnabled = false;
let wrapEnabled;
let currentBox = undefined;

export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};

export const addBox = function (data) {
boxes.push({
name: data.text,
wrap: (data.wrap === undefined && autoWrap()) || !!data.wrap,
fill: data.color,
actorKeys: [],
});
currentBox = boxes.slice(-1)[0];
};

export const addActor = function (id, name, description, type) {
// Don't allow description nulling
let assignedBox = currentBox;
const old = actors[id];
if (old && name === old.name && description == null) {
return;
if (old) {
// If already set and trying to set to a new one throw error
if (currentBox && old.box && currentBox !== old.box) {
throw new Error(
'A same participant should only be defined in one Box: ' +
old.name +
" can't be in '" +
old.box.name +
"' and in '" +
currentBox.name +
"' at the same time."
);
}

// Don't change the box if already
assignedBox = old.box ? old.box : currentBox;
old.box = assignedBox;

// Don't allow description nulling
if (old && name === old.name && description == null) {
return;
}
}

// Don't allow null descriptions, either
Expand All @@ -39,6 +71,7 @@ export const addActor = function (id, name, description, type) {
}

actors[id] = {
box: assignedBox,
name: name,
description: description.text,
wrap: (description.wrap === undefined && autoWrap()) || !!description.wrap,
Expand All @@ -53,6 +86,9 @@ export const addActor = function (id, name, description, type) {
actors[prevActor].nextActor = id;
}

if (currentBox) {
currentBox.actorKeys.push(id);
}
prevActor = id;
};

Expand Down Expand Up @@ -111,10 +147,21 @@ export const addSignal = function (
return true;
};

export const hasAtLeastOneBox = function () {
return boxes.length > 0;
};

export const hasAtLeastOneBoxWithTitle = function () {
return boxes.some((b) => b.name);
};

export const getMessages = function () {
return messages;
};

export const getBoxes = function () {
return boxes;
};
export const getActors = function () {
return actors;
};
Expand Down Expand Up @@ -147,6 +194,7 @@ export const autoWrap = () => {

export const clear = function () {
actors = {};
boxes = [];
messages = [];
sequenceNumbersEnabled = false;
commonClear();
Expand All @@ -167,6 +215,47 @@ export const parseMessage = function (str) {
return message;
};

// We expect the box statement to be color first then description
// The color can be rgb,rgba,hsl,hsla, or css code names #hex codes are not supported for now because of the way the char # is handled
// We extract first segment as color, the rest of the line is considered as text
export const parseBoxData = function (str) {
const match = str.match(/^((?:rgba?|hsla?)\s*\(.*\)|\w*)(.*)$/);
let color = match != null && match[1] ? match[1].trim() : 'transparent';
let title = match != null && match[2] ? match[2].trim() : undefined;

// check that the string is a color
if (window && window.CSS) {
if (!window.CSS.supports('color', color)) {
color = 'transparent';
title = str.trim();
}
} else {
const style = new Option().style;
style.color = color;
if (style.color !== color) {
color = 'transparent';
title = str.trim();
}
}

const boxData = {
color: color,
text:
title !== undefined
? sanitizeText(title.replace(/^:?(?:no)?wrap:/, ''), configApi.getConfig())
: undefined,
wrap:
title !== undefined
? title.match(/^:?wrap:/) !== null
? true
: title.match(/^:?nowrap:/) !== null
? false
: undefined
: undefined,
};
return boxData;
};

export const LINETYPE = {
SOLID: 0,
DOTTED: 1,
Expand Down Expand Up @@ -311,6 +400,13 @@ function insertProperties(actor, properties) {
}
}

/**
*
*/
function boxEnd() {
currentBox = undefined;
}

export const addDetails = function (actorId, text) {
// find the actor
const actor = getActor(actorId);
Expand Down Expand Up @@ -391,6 +487,12 @@ export const apply = function (param) {
case 'addMessage':
addSignal(param.from, param.to, param.msg, param.signalType);
break;
case 'boxStart':
addBox(param.boxData);
break;
case 'boxEnd':
boxEnd();
break;
case 'loopStart':
addSignal(undefined, undefined, param.loopText, param.signalType);
break;
Expand Down Expand Up @@ -467,12 +569,14 @@ export default {
getActorKeys,
getActorProperty,
getAccTitle,
getBoxes,
getDiagramTitle,
setDiagramTitle,
parseDirective,
getConfig: () => configApi.getConfig().sequence,
clear,
parseMessage,
parseBoxData,
LINETYPE,
ARROWTYPE,
PLACEMENT,
Expand All @@ -481,4 +585,6 @@ export default {
apply,
setAccDescription,
getAccDescription,
hasAtLeastOneBox,
hasAtLeastOneBoxWithTitle,
};
Loading