Adding "Critical Region" and "Break" blocks

This commit is contained in:
Ronald Heggenberger
2022-05-21 09:31:06 +02:00
parent ef2fea157b
commit 7372ca5e8e
6 changed files with 311 additions and 51 deletions

View File

@@ -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(
`

View File

@@ -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
<script>
mermaid.initialize({ sequence: { showSequenceNumbers: true }, });
</script>
mermaid.initialize({ sequence: { showSequenceNumbers: true }, });
</script>
```
It can also be be turned on via the diagram code as in the diagram:

View File

@@ -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'; }
<LINE>(?:[:]?(?: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

View File

@@ -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;
}
}
};

View File

@@ -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

View File

@@ -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;