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..aac1817fe 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 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 f39c59914..7d68c9723 100644 --- a/src/diagrams/sequence/sequenceDb.js +++ b/src/diagrams/sequence/sequenceDb.js @@ -190,6 +190,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 = { @@ -422,6 +427,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 c083abf9a..df08fc550 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 e70ce2d09..caa285a8a 100644 --- a/src/diagrams/sequence/sequenceRenderer.js +++ b/src/diagrams/sequence/sequenceRenderer.js @@ -763,6 +763,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 @@ -1165,6 +1204,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, @@ -1175,6 +1216,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; @@ -1186,6 +1228,8 @@ 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;