From 7372ca5e8eeb0d583b65de65dae048c583e7fa31 Mon Sep 17 00:00:00 2001 From: Ronald Heggenberger Date: Sat, 21 May 2022 09:31:06 +0200 Subject: [PATCH 1/2] Adding "Critical Region" and "Break" blocks --- .../rendering/sequencediagram.spec.js | 36 +++++ docs/sequenceDiagram.md | 68 ++++++++- .../sequence/parser/sequenceDiagram.jison | 22 +++ src/diagrams/sequence/sequenceDb.js | 24 ++- src/diagrams/sequence/sequenceDiagram.spec.js | 74 ++++++++++ src/diagrams/sequence/sequenceRenderer.js | 138 ++++++++++++------ 6 files changed, 311 insertions(+), 51 deletions(-) diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js index 6bedd233eb..1122a30092 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 bb925db28e..286009d4df 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 2669e86063..65a4035f44 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 e2898b5bad..df20c57515 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 9b9b9cfe6f..b0e02ceb31 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 6421e8d804..0040a18e42 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; From 077e6621b92b215aadb232d40b8ed7257550955f Mon Sep 17 00:00:00 2001 From: Ronald Heggenberger Date: Sat, 21 May 2022 09:40:21 +0200 Subject: [PATCH 2/2] Undo whitespace changes --- docs/sequenceDiagram.md | 4 +- src/diagrams/sequence/sequenceDb.js | 4 +- src/diagrams/sequence/sequenceRenderer.js | 94 +++++++++++------------ 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/docs/sequenceDiagram.md b/docs/sequenceDiagram.md index 286009d4df..aac1817fe6 100644 --- a/docs/sequenceDiagram.md +++ b/docs/sequenceDiagram.md @@ -364,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/sequenceDb.js b/src/diagrams/sequence/sequenceDb.js index df20c57515..6fe9c67f9a 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; diff --git a/src/diagrams/sequence/sequenceRenderer.js b/src/diagrams/sequence/sequenceRenderer.js index 0040a18e42..3d1000eb2c 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 { @@ -887,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); @@ -1095,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( @@ -1235,27 +1235,27 @@ const calculateLoopBounds = function (messages, actors) { 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;