diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js
index 6bedd233e..1122a3009 100644
--- a/cypress/integration/rendering/sequencediagram.spec.js
+++ b/cypress/integration/rendering/sequencediagram.spec.js
@@ -452,6 +452,42 @@ context('Sequence diagram', () => {
{}
);
});
+ it('should render rect around and inside criticals', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ A ->> B: 1
+ rect rgb(204, 0, 102)
+ critical yes
+ C ->> C: 1
+ option no
+ rect rgb(0, 204, 204)
+ C ->> C: 0
+ end
+ end
+ end
+ B ->> A: Return
+ `,
+ {}
+ );
+ });
+ it('should render rect around and inside breaks', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ A ->> B: 1
+ rect rgb(204, 0, 102)
+ break yes
+ rect rgb(0, 204, 204)
+ C ->> C: 0
+ end
+ end
+ end
+ B ->> A: Return
+ `,
+ {}
+ );
+ });
it('should render autonumber when configured with such', () => {
imgSnapshotTest(
`
diff --git a/docs/sequenceDiagram.md b/docs/sequenceDiagram.md
index bb925db28..286009d4d 100644
--- a/docs/sequenceDiagram.md
+++ b/docs/sequenceDiagram.md
@@ -230,6 +230,70 @@ sequenceDiagram
end
```
+## Critical Region
+
+It is possible to show actions that must happen automatically with conditional handling of circumstances.
+
+This is done by the notation
+
+```
+critical [Action that must be performed]
+... statements ...
+option [Circumstance A]
+... statements ...
+option [Circumstance B]
+... statements ...
+end
+```
+
+See the example below:
+
+```mermaid-example
+sequenceDiagram
+ critical Establish a connection to the DB
+ Service-->DB: connect
+ option Network timeout
+ Service-->Service: Log error
+ option Credentials rejected
+ Service-->Service: Log different error
+ end
+```
+
+It is also possible to have no options at all
+
+```mermaid-example
+sequenceDiagram
+ critical Establish a connection to the DB
+ Service-->DB: connect
+ end
+```
+
+This critical block can also be nested, equivalently to the `par` statement as seen above.
+
+## Break
+
+It is possible to indicate a stop of the sequence within the flow (usually used to model exceptions).
+
+This is done by the notation
+
+```
+break [something happened]
+... statements ...
+end
+```
+
+See the example below:
+
+```mermaid-example
+sequenceDiagram
+ Consumer-->API: Book something
+ API-->BookingService: Start booking process
+ break when the booking process fails
+ API-->Consumer: show failure
+ end
+ API-->BillingService: Start billing process
+```
+
## Background Highlighting
It is possible to highlight flows by providing colored background rects. This is done by the notation
@@ -300,8 +364,8 @@ It is possible to get a sequence number attached to each arrow in a sequence dia
```html
+ mermaid.initialize({ sequence: { showSequenceNumbers: true }, });
+
```
It can also be be turned on via the diagram code as in the diagram:
diff --git a/src/diagrams/sequence/parser/sequenceDiagram.jison b/src/diagrams/sequence/parser/sequenceDiagram.jison
index 2669e8606..65a4035f4 100644
--- a/src/diagrams/sequence/parser/sequenceDiagram.jison
+++ b/src/diagrams/sequence/parser/sequenceDiagram.jison
@@ -47,6 +47,9 @@
"else" { this.begin('LINE'); return 'else'; }
"par" { this.begin('LINE'); return 'par'; }
"and" { this.begin('LINE'); return 'and'; }
+"critical" { this.begin('LINE'); return 'critical'; }
+"option" { this.begin('LINE'); return 'option'; }
+"break" { this.begin('LINE'); return 'break'; }
(?:[:]?(?:no)?wrap:)?[^#\n;]* { this.popState(); return 'restOfLine'; }
"end" return 'end';
"left of" return 'left_of';
@@ -172,9 +175,28 @@ statement
// End
$3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END});
$$=$3;}
+ | critical restOfLine option_sections end
+ {
+ // critical start
+ $3.unshift({type: 'criticalStart', criticalText:yy.parseMessage($2), signalType: yy.LINETYPE.CRITICAL_START});
+ // Content in critical is already in $3
+ // critical end
+ $3.push({type: 'criticalEnd', signalType: yy.LINETYPE.CRITICAL_END});
+ $$=$3;}
+ | break restOfLine document end
+ {
+ $3.unshift({type: 'breakStart', breakText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_START});
+ $3.push({type: 'breakEnd', optText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_END});
+ $$=$3;}
| directive
;
+option_sections
+ : document
+ | document option restOfLine option_sections
+ { $$ = $1.concat([{type: 'option', optionText:yy.parseMessage($3), signalType: yy.LINETYPE.CRITICAL_OPTION}, $4]); }
+ ;
+
par_sections
: document
| document and restOfLine par_sections
diff --git a/src/diagrams/sequence/sequenceDb.js b/src/diagrams/sequence/sequenceDb.js
index e2898b5ba..df20c5751 100644
--- a/src/diagrams/sequence/sequenceDb.js
+++ b/src/diagrams/sequence/sequenceDb.js
@@ -156,8 +156,8 @@ export const parseMessage = function (str) {
_str.match(/^[:]?wrap:/) !== null
? true
: _str.match(/^[:]?nowrap:/) !== null
- ? false
- : undefined,
+ ? false
+ : undefined,
};
log.debug('parseMessage:', message);
return message;
@@ -188,6 +188,11 @@ export const LINETYPE = {
SOLID_POINT: 24,
DOTTED_POINT: 25,
AUTONUMBER: 26,
+ CRITICAL_START: 27,
+ CRITICAL_OPTION: 28,
+ CRITICAL_END: 29,
+ BREAK_START: 30,
+ BREAK_END: 31,
};
export const ARROWTYPE = {
@@ -429,6 +434,21 @@ export const apply = function (param) {
case 'parEnd':
addSignal(undefined, undefined, undefined, param.signalType);
break;
+ case 'criticalStart':
+ addSignal(undefined, undefined, param.criticalText, param.signalType);
+ break;
+ case 'option':
+ addSignal(undefined, undefined, param.optionText, param.signalType);
+ break;
+ case 'criticalEnd':
+ addSignal(undefined, undefined, undefined, param.signalType);
+ break;
+ case 'breakStart':
+ addSignal(undefined, undefined, param.breakText, param.signalType);
+ break;
+ case 'breakEnd':
+ addSignal(undefined, undefined, undefined, param.signalType);
+ break;
}
}
};
diff --git a/src/diagrams/sequence/sequenceDiagram.spec.js b/src/diagrams/sequence/sequenceDiagram.spec.js
index 9b9b9cfe6..b0e02ceb3 100644
--- a/src/diagrams/sequence/sequenceDiagram.spec.js
+++ b/src/diagrams/sequence/sequenceDiagram.spec.js
@@ -843,6 +843,80 @@ end`;
expect(messages[7].from).toBe('Bob');
expect(messages[8].type).toBe(parser.yy.LINETYPE.ALT_END);
});
+ it('it should handle critical statements without options', function () {
+ const str = `
+sequenceDiagram
+ critical Establish a connection to the DB
+ Service-->DB: connect
+ end`;
+
+ mermaidAPI.parse(str);
+ const actors = parser.yy.getActors();
+
+ expect(actors.Service.description).toBe('Service');
+ expect(actors.DB.description).toBe('DB');
+
+ const messages = parser.yy.getMessages();
+
+ expect(messages.length).toBe(3);
+ expect(messages[0].type).toBe(parser.yy.LINETYPE.CRITICAL_START);
+ expect(messages[1].from).toBe('Service');
+ expect(messages[2].type).toBe(parser.yy.LINETYPE.CRITICAL_END);
+ });
+ it('it should handle critical statements with options', function () {
+ const str = `
+sequenceDiagram
+ critical Establish a connection to the DB
+ Service-->DB: connect
+ option Network timeout
+ Service-->Service: Log error
+ option Credentials rejected
+ Service-->Service: Log different error
+ end`;
+
+ mermaidAPI.parse(str);
+ const actors = parser.yy.getActors();
+
+ expect(actors.Service.description).toBe('Service');
+ expect(actors.DB.description).toBe('DB');
+
+ const messages = parser.yy.getMessages();
+
+ expect(messages.length).toBe(7);
+ expect(messages[0].type).toBe(parser.yy.LINETYPE.CRITICAL_START);
+ expect(messages[1].from).toBe('Service');
+ expect(messages[2].type).toBe(parser.yy.LINETYPE.CRITICAL_OPTION);
+ expect(messages[3].from).toBe('Service');
+ expect(messages[4].type).toBe(parser.yy.LINETYPE.CRITICAL_OPTION);
+ expect(messages[5].from).toBe('Service');
+ expect(messages[6].type).toBe(parser.yy.LINETYPE.CRITICAL_END);
+ });
+ it('it should handle break statements', function () {
+ const str = `
+sequenceDiagram
+ Consumer-->API: Book something
+ API-->BookingService: Start booking process
+ break when the booking process fails
+ API-->Consumer: show failure
+ end
+ API-->BillingService: Start billing process`;
+
+ mermaidAPI.parse(str);
+ const actors = parser.yy.getActors();
+
+ expect(actors.Consumer.description).toBe('Consumer');
+ expect(actors.API.description).toBe('API');
+
+ const messages = parser.yy.getMessages();
+
+ expect(messages.length).toBe(6);
+ expect(messages[0].from).toBe('Consumer');
+ expect(messages[1].from).toBe('API');
+ expect(messages[2].type).toBe(parser.yy.LINETYPE.BREAK_START);
+ expect(messages[3].from).toBe('API');
+ expect(messages[4].type).toBe(parser.yy.LINETYPE.BREAK_END);
+ expect(messages[5].from).toBe('API');
+ });
it('it should handle par statements a sequenceDiagram', function () {
const str = `
sequenceDiagram
diff --git a/src/diagrams/sequence/sequenceRenderer.js b/src/diagrams/sequence/sequenceRenderer.js
index 6421e8d80..0040a18e4 100644
--- a/src/diagrams/sequence/sequenceRenderer.js
+++ b/src/diagrams/sequence/sequenceRenderer.js
@@ -367,21 +367,21 @@ const drawMessage = function (diagram, msgModel, lineStarty) {
.attr(
'd',
'M ' +
- startx +
- ',' +
- lineStarty +
- ' C ' +
- (startx + 60) +
- ',' +
- (lineStarty - 10) +
- ' ' +
- (startx + 60) +
- ',' +
- (lineStarty + 30) +
- ' ' +
- startx +
- ',' +
- (lineStarty + 20)
+ startx +
+ ',' +
+ lineStarty +
+ ' C ' +
+ (startx + 60) +
+ ',' +
+ (lineStarty - 10) +
+ ' ' +
+ (startx + 60) +
+ ',' +
+ (lineStarty + 30) +
+ ' ' +
+ startx +
+ ',' +
+ (lineStarty + 20)
);
}
} else {
@@ -764,6 +764,45 @@ export const draw = function (text, id) {
if (msg.message.visible) parser.yy.enableSequenceNumbers();
else parser.yy.disableSequenceNumbers();
break;
+ case parser.yy.LINETYPE.CRITICAL_START:
+ adjustLoopHeightForWrap(
+ loopWidths,
+ msg,
+ conf.boxMargin,
+ conf.boxMargin + conf.boxTextMargin,
+ (message) => bounds.newLoop(message)
+ );
+ break;
+ case parser.yy.LINETYPE.CRITICAL_OPTION:
+ adjustLoopHeightForWrap(
+ loopWidths,
+ msg,
+ conf.boxMargin + conf.boxTextMargin,
+ conf.boxMargin,
+ (message) => bounds.addSectionToLoop(message)
+ );
+ break;
+ case parser.yy.LINETYPE.CRITICAL_END:
+ loopModel = bounds.endLoop();
+ svgDraw.drawLoop(diagram, loopModel, 'critical', conf);
+ bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
+ bounds.models.addLoop(loopModel);
+ break;
+ case parser.yy.LINETYPE.BREAK_START:
+ adjustLoopHeightForWrap(
+ loopWidths,
+ msg,
+ conf.boxMargin,
+ conf.boxMargin + conf.boxTextMargin,
+ (message) => bounds.newLoop(message)
+ );
+ break;
+ case parser.yy.LINETYPE.BREAK_END:
+ loopModel = bounds.endLoop();
+ svgDraw.drawLoop(diagram, loopModel, 'break', conf);
+ bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
+ bounds.models.addLoop(loopModel);
+ break;
default:
try {
// lastMsg = msg
@@ -848,13 +887,13 @@ export const draw = function (text, id) {
diagram.attr(
'viewBox',
box.startx -
- conf.diagramMarginX +
- ' -' +
- (conf.diagramMarginY + extraVertForTitle) +
- ' ' +
- width +
- ' ' +
- (height + extraVertForTitle)
+ conf.diagramMarginX +
+ ' -' +
+ (conf.diagramMarginY + extraVertForTitle) +
+ ' ' +
+ width +
+ ' ' +
+ (height + extraVertForTitle)
);
addSVGAccessibilityFields(parser.yy, diagram, id);
@@ -1056,17 +1095,17 @@ const buildNoteModel = function (msg, actors) {
noteModel.width = shouldWrap
? Math.max(conf.width, textDimensions.width)
: Math.max(
- actors[msg.from].width / 2 + actors[msg.to].width / 2,
- textDimensions.width + 2 * conf.noteMargin
- );
+ actors[msg.from].width / 2 + actors[msg.to].width / 2,
+ textDimensions.width + 2 * conf.noteMargin
+ );
noteModel.startx = startx + (actors[msg.from].width + conf.actorMargin) / 2;
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
noteModel.width = shouldWrap
? Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin)
: Math.max(
- actors[msg.from].width / 2 + actors[msg.to].width / 2,
- textDimensions.width + 2 * conf.noteMargin
- );
+ actors[msg.from].width / 2 + actors[msg.to].width / 2,
+ textDimensions.width + 2 * conf.noteMargin
+ );
noteModel.startx = startx - noteModel.width + (actors[msg.from].width - conf.actorMargin) / 2;
} else if (msg.to === msg.from) {
textDimensions = utils.calculateTextDimensions(
@@ -1166,6 +1205,8 @@ const calculateLoopBounds = function (messages, actors) {
case parser.yy.LINETYPE.ALT_START:
case parser.yy.LINETYPE.OPT_START:
case parser.yy.LINETYPE.PAR_START:
+ case parser.yy.LINETYPE.CRITICAL_START:
+ case parser.yy.LINETYPE.BREAK_START:
stack.push({
id: msg.id,
msg: msg.message,
@@ -1176,6 +1217,7 @@ const calculateLoopBounds = function (messages, actors) {
break;
case parser.yy.LINETYPE.ALT_ELSE:
case parser.yy.LINETYPE.PAR_AND:
+ case parser.yy.LINETYPE.CRITICAL_OPTION:
if (msg.message) {
current = stack.pop();
loops[current.id] = current;
@@ -1187,31 +1229,33 @@ const calculateLoopBounds = function (messages, actors) {
case parser.yy.LINETYPE.ALT_END:
case parser.yy.LINETYPE.OPT_END:
case parser.yy.LINETYPE.PAR_END:
+ case parser.yy.LINETYPE.CRITICAL_END:
+ case parser.yy.LINETYPE.BREAK_END:
current = stack.pop();
loops[current.id] = current;
break;
case parser.yy.LINETYPE.ACTIVE_START:
- {
- const actorRect = actors[msg.from ? msg.from.actor : msg.to.actor];
- const stackedSize = actorActivations(msg.from ? msg.from.actor : msg.to.actor).length;
- const x =
- actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
- const toAdd = {
- startx: x,
- stopx: x + conf.activationWidth,
- actor: msg.from.actor,
- enabled: true,
- };
- bounds.activations.push(toAdd);
- }
+ {
+ const actorRect = actors[msg.from ? msg.from.actor : msg.to.actor];
+ const stackedSize = actorActivations(msg.from ? msg.from.actor : msg.to.actor).length;
+ const x =
+ actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
+ const toAdd = {
+ startx: x,
+ stopx: x + conf.activationWidth,
+ actor: msg.from.actor,
+ enabled: true,
+ };
+ bounds.activations.push(toAdd);
+ }
break;
case parser.yy.LINETYPE.ACTIVE_END:
- {
- const lastActorActivationIdx = bounds.activations
- .map((a) => a.actor)
- .lastIndexOf(msg.from.actor);
- delete bounds.activations.splice(lastActorActivationIdx, 1)[0];
- }
+ {
+ const lastActorActivationIdx = bounds.activations
+ .map((a) => a.actor)
+ .lastIndexOf(msg.from.actor);
+ delete bounds.activations.splice(lastActorActivationIdx, 1)[0];
+ }
break;
}
const isNote = msg.placement !== undefined;