diff --git a/.changeset/loud-results-melt.md b/.changeset/loud-results-melt.md new file mode 100644 index 000000000..7005750c6 --- /dev/null +++ b/.changeset/loud-results-melt.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +feat: Add half-arrowheads (solid & stick) and central connection support diff --git a/cypress/integration/rendering/sequencediagram-v2.spec.js b/cypress/integration/rendering/sequencediagram-v2.spec.js index f1c2aafbd..42db4001d 100644 --- a/cypress/integration/rendering/sequencediagram-v2.spec.js +++ b/cypress/integration/rendering/sequencediagram-v2.spec.js @@ -655,5 +655,126 @@ describe('Sequence Diagram Special Cases', () => { expect(svg).to.not.have.attr('style'); }); }); + + describe('Central Connection Rendering Tests', () => { + it('should render central connection circles on actor vertical lines', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ()->>() Bob: Central connection + Bob ()-->> Charlie: Reverse central connection + Charlie ()<<-->>() Alice: Dual central connection`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with different arrow types', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + Alice ()->>() Bob: Solid open arrow + Alice ()-->>() Bob: Dotted open arrow + Alice ()-x() Bob: Solid cross + Alice ()--x() Bob: Dotted cross + Alice ()->() Bob: Solid arrow`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with bidirectional arrows', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + Alice ()<<->>() Bob: Bidirectional solid + Alice ()<<-->>() Bob: Bidirectional dotted`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with activations', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ()->>() Bob: Activate Bob + activate Bob + Bob ()-->> Charlie: Message to Charlie + Bob ()->>() Alice: Response to Alice + deactivate Bob`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections mixed with normal messages', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ->> Bob: Normal message + Bob ()->>() Charlie: Central connection + Charlie -->> Alice: Normal dotted message + Alice ()<<-->>() Bob: Dual central connection + Bob -x Charlie: Normal cross message`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with notes', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ()->>() Bob: Central connection + Note over Alice,Bob: Central connection note + Bob ()-->> Charlie: Reverse central connection + Note right of Charlie: Response note + Charlie ()<<-->>() Alice: Dual central connection`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with loops and alternatives', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + loop Every minute + Alice ()->>() Bob: Central heartbeat + Bob ()-->> Charlie: Forward heartbeat + end + alt Success + Charlie ()<<-->>() Alice: Success response + else Failure + Charlie ()-x() Alice: Failure response + end`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with different participant types', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + actor Bob + participant Charlie@{"type":"boundary"} + participant David@{"type":"control"} + participant Eve@{"type":"entity"} + Alice ()->>() Bob: To actor + Bob ()-->> Charlie: To boundary + Charlie ()->>() David: To control + David ()<<-->>() Eve: To entity + Eve ()-x() Alice: Back to participant`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + }); }); }); diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js index 6709b557c..0ec913a8c 100644 --- a/cypress/integration/rendering/sequencediagram.spec.js +++ b/cypress/integration/rendering/sequencediagram.spec.js @@ -1053,4 +1053,167 @@ describe('Sequence diagram', () => { ]); }); }); + describe('render new arrow type', () => { + it('should render Solid half arrow top', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice -|\\ John: Hello John, how are you? + Alice-|\\ John: Hi Alice, I can hear you! + Alice -|\\ John: Test + ` + ); + }); + it('should render Solid half arrow bottom', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice-|/John: Hello John, how are you? + Alice-|/John: Hi Alice, I can hear you! + Alice-|/John: Test + ` + ); + }); + + it('should render Stick half arrow top ', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice-\\\\John: Hello John, how are you? + Alice-\\\\John: Hi Alice, I can hear you! + Alice-\\\\John: Test + ` + ); + }); + it('should render Stick half arrow bottom ', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice-//John: Hello John, how are you? + Alice-//John: Hi Alice, I can hear you! + Alice-//John: Test + ` + ); + }); + it('should render Solid half arrow top reverse ', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice/|-John: Hello Alice, how are you? + Alice/|-John: Hi Alice, I can hear you! + Alice/|-John: Test + + ` + ); + }); + + it('should render Solid half arrow bottom reverse ', () => { + imgSnapshotTest( + `sequenceDiagram + Alice \\|- John: Hello Alice, how are you? + Alice \\|- John: Hi Alice, I can hear you! + Alice \\|- John: Test` + ); + }); + + it('should render Stick half arrow top reverse ', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice //-John: Hello Alice, how are you? + Alice //-John: Hi Alice, I can hear you! + Alice //-John: Test` + ); + }); + + it('should render Stick half arrow bottom reverse ', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice \\\\-John: Hello Alice, how are you? + Alice \\\\-John: Hi Alice, I can hear you! + Alice \\\\-John: Test` + ); + }); + + it('should render Solid half arrow top dotted', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice --|\\John: Hello John, how are you? + Alice --|\\John: Hi Alice, I can hear you! + Alice --|\\John: Test` + ); + }); + + it('should render Solid half arrow bottom dotted', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice --|/John: Hello John, how are you? + Alice --|/John: Hi Alice, I can hear you! + Alice --|/John: Test` + ); + }); + + it('should render Stick half arrow top dotted', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice--\\\\John: Hello John, how are you? + Alice--\\\\John: Hi Alice, I can hear you! + Alice--\\\\John: Test` + ); + }); + + it('should render Stick half arrow bottom dotted', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice--//John: Hello John, how are you? + Alice--//John: Hi Alice, I can hear you! + Alice--//John: Test` + ); + }); + + it('should render Solid half arrow top reverse dotted', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice/|--John: Hello Alice, how are you? + Alice/|--John: Hi Alice, I can hear you! + Alice/|--John: Test` + ); + }); + + it('should render Solid half arrow bottom reverse dotted', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice\\|--John: Hello Alice, how are you? + Alice\\|--John: Hi Alice, I can hear you! + Alice\\|--John: Test` + ); + }); + + it('should render Stick half arrow top reverse dotted ', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice//--John: Hello Alice, how are you? + Alice//--John: Hi Alice, I can hear you! + Alice//--John: Test` + ); + }); + + it('should render Stick half arrow bottom reverse dotted ', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice\\\\--John: Hello Alice, how are you? + Alice\\\\--John: Hi Alice, I can hear you! + Alice\\\\--John: Test` + ); + }); + }); }); diff --git a/docs/syntax/sequenceDiagram.md b/docs/syntax/sequenceDiagram.md index a8f502dcd..eb3cfc996 100644 --- a/docs/syntax/sequenceDiagram.md +++ b/docs/syntax/sequenceDiagram.md @@ -329,7 +329,11 @@ Messages can be of two displayed either solid or with a dotted line. [Actor][Arrow][Actor]:Message text ``` -There are ten types of arrows currently supported: +Lines can be solid or dotted, and can end with various types of arrowheads, crosses, or open arrows. + +#### Supported Arrow Types + +**Standard Arrow Types** | Type | Description | | -------- | ---------------------------------------------------- | @@ -344,6 +348,58 @@ There are ten types of arrows currently supported: | `-)` | Solid line with an open arrow at the end (async) | | `--)` | Dotted line with a open arrow at the end (async) | +**Half-Arrows (v\+)** + +The following half-arrow types are supported for more expressive sequence diagrams. Both solid and dotted variants are available by increasing the number of dashes (`-` → `--`). + +--- + +| Type | Description | +| ------- | ---------------------------------------------------- | +| `-\|\` | Solid line with top half arrowhead | +| `--\|\` | Dotted line with top half arrowhead | +| `-\|/` | Solid line with bottom half arrowhead | +| `--\|/` | Dotted line with bottom half arrowhead | +| `/\|-` | Solid line with reverse top half arrowhead | +| `/\|--` | Dotted line with reverse top half arrowhead | +| `\\-` | Solid line with reverse bottom half arrowhead | +| `\\--` | Dotted line with reverse bottom half arrowhead | +| `-\\` | Solid line with top stick half arrowhead | +| `--\\` | Dotted line with top stick half arrowhead | +| `-//` | Solid line with bottom stick half arrowhead | +| `--//` | Dotted line with bottom stick half arrowhead | +| `//-` | Solid line with reverse top stick half arrowhead | +| `//--` | Dotted line with reverse top stick half arrowhead | +| `\\-` | Solid line with reverse bottom stick half arrowhead | +| `\\--` | Dotted line with reverse bottom stick half arrowhead | + +## Central Connections (v\+) + +Mermaid sequence diagrams support **central lifeline connections** using a `()`. +This is useful to represent messages or signals that connect to a central point, rather than from one actor directly to another. + +To indicate a central connection, append `()` to the arrow syntax. + +#### Basic Syntax + +```mermaid-example +sequenceDiagram + participant Alice + participant John + Alice->>()John: Hello John + Alice()->>John: How are you? + John()->>()Alice: Great! +``` + +```mermaid +sequenceDiagram + participant Alice + participant John + Alice->>()John: Hello John + Alice()->>John: How are you? + John()->>()Alice: Great! +``` + ## Activations It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations: diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison index 13e63f3ae..f1364895b 100644 --- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison +++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison @@ -78,7 +78,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili "off" return 'off'; "," return ','; ";" return 'NEWLINE'; -[^+<\->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+<\->\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; } +[^\/\\\+\()\+<\->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)|\-\|\\|\-\\|\-\/|\-\/\/|\-\|\/|\/\|\-|\\\|\-|\/\/\-|\\\\\-|\/\|\-|\-\-\|\\|\-\-|\(\)))[\-]*[^\+<\->\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; } //final_4.11 "->>" return 'SOLID_ARROW'; "<<->>" return 'BIDIRECTIONAL_SOLID_ARROW'; "-->>" return 'DOTTED_ARROW'; @@ -89,10 +89,36 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili \-\-[x] return 'DOTTED_CROSS'; \-[\)] return 'SOLID_POINT'; \-\-[\)] return 'DOTTED_POINT'; + +//normal-dotted +\-\-\|\\ return 'SOLID_ARROW_TOP_DOTTED'; +\-\-\|\/ return 'SOLID_ARROW_BOTTOM_DOTTED'; +\-\-\\\\ return 'STICK_ARROW_TOP_DOTTED'; +\-\-\/\/ return 'STICK_ARROW_BOTTOM_DOTTED'; + +//reverse-dotted +\/\|\-\- return 'SOLID_ARROW_TOP_REVERSE_DOTTED'; +\\\|\-\- return 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED'; +\/\/\-\- return 'STICK_ARROW_TOP_REVERSE_DOTTED'; +\\\\\-\- return 'STICK_ARROW_BOTTOM_REVERSE_DOTTED'; + +//normal +\-\|\\ return 'SOLID_ARROW_TOP'; +\-\|\/ return 'SOLID_ARROW_BOTTOM'; +\-\\\\ return 'STICK_ARROW_TOP'; +\-\/\/ return 'STICK_ARROW_BOTTOM'; + +//reverse +\/\|\- return 'SOLID_ARROW_TOP_REVERSE'; +\\\|\- return 'SOLID_ARROW_BOTTOM_REVERSE'; +\/\/\- return 'STICK_ARROW_TOP_REVERSE'; +\\\\\- return 'STICK_ARROW_BOTTOM_REVERSE'; + ":"(?:(?:no)?wrap:)?[^#\n;]* return 'TXT'; ":" return 'TXT'; "+" return '+'; "-" return '-'; +"()" return '()'; <> return 'NEWLINE'; . return 'INVALID'; @@ -304,6 +330,20 @@ signal { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5}, {type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $1.actor} ]} + | actor signaltype '()' actor text2 + { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5, activate: true, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION}, + {type: 'centralConnection', signalType: yy.LINETYPE.CENTRAL_CONNECTION, actor: $4.actor, } + ]} + + | actor '()' signaltype actor text2 + { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$3, msg:$5, activate: false, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE}, + {type: 'centralConnectionReverse', signalType: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE, actor: $1.actor} + ]} + | actor '()' signaltype '()' actor text2 + { $$ = [$1,$5,{type: 'addMessage', from:$1.actor, to:$5.actor, signalType:$3, msg:$6, activate: true, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION_DUAL}, + {type: 'centralConnection', signalType: yy.LINETYPE.CENTRAL_CONNECTION, actor: $5.actor, }, + {type: 'centralConnectionReverse', signalType: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE, actor: $1.actor} + ]} | actor signaltype actor text2 { $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]} ; @@ -337,7 +377,28 @@ signaltype : SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; } | DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; } | SOLID_ARROW { $$ = yy.LINETYPE.SOLID; } - | BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; } + + | SOLID_ARROW_TOP { $$ = yy.LINETYPE.SOLID_TOP; } + | SOLID_ARROW_BOTTOM { $$ = yy.LINETYPE.SOLID_BOTTOM; } + | STICK_ARROW_TOP { $$ = yy.LINETYPE.STICK_TOP; } + | STICK_ARROW_BOTTOM { $$ = yy.LINETYPE.STICK_BOTTOM; } + + | SOLID_ARROW_TOP_DOTTED { $$ = yy.LINETYPE.SOLID_TOP_DOTTED; } + | SOLID_ARROW_BOTTOM_DOTTED { $$ = yy.LINETYPE.SOLID_BOTTOM_DOTTED; } + | STICK_ARROW_TOP_DOTTED { $$ = yy.LINETYPE.STICK_TOP_DOTTED; } + | STICK_ARROW_BOTTOM_DOTTED { $$ = yy.LINETYPE.STICK_BOTTOM_DOTTED; } + + | SOLID_ARROW_TOP_REVERSE { $$ = yy.LINETYPE.SOLID_ARROW_TOP_REVERSE; } + | SOLID_ARROW_BOTTOM_REVERSE { $$ = yy.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE; } + | STICK_ARROW_TOP_REVERSE { $$ = yy.LINETYPE.STICK_ARROW_TOP_REVERSE; } + | STICK_ARROW_BOTTOM_REVERSE { $$ = yy.LINETYPE.STICK_ARROW_BOTTOM_REVERSE; } + + | SOLID_ARROW_TOP_REVERSE_DOTTED { $$ = yy.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED; } + | SOLID_ARROW_BOTTOM_REVERSE_DOTTED { $$ = yy.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED; } + | STICK_ARROW_TOP_REVERSE_DOTTED { $$ = yy.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED; } + | STICK_ARROW_BOTTOM_REVERSE_DOTTED { $$ = yy.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED; } + + | BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; } | DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; } | BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; } | SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; } diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts index 67ae19de5..d4758f39e 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts @@ -64,6 +64,30 @@ const LINETYPE = { PAR_OVER_START: 32, BIDIRECTIONAL_SOLID: 33, BIDIRECTIONAL_DOTTED: 34, + + SOLID_TOP: 41, + SOLID_BOTTOM: 42, + STICK_TOP: 43, + STICK_BOTTOM: 44, + + SOLID_ARROW_TOP_REVERSE: 45, + SOLID_ARROW_BOTTOM_REVERSE: 46, + STICK_ARROW_TOP_REVERSE: 47, + STICK_ARROW_BOTTOM_REVERSE: 48, + + SOLID_TOP_DOTTED: 51, + SOLID_BOTTOM_DOTTED: 52, + STICK_TOP_DOTTED: 53, + STICK_BOTTOM_DOTTED: 54, + + SOLID_ARROW_TOP_REVERSE_DOTTED: 55, + SOLID_ARROW_BOTTOM_REVERSE_DOTTED: 56, + STICK_ARROW_TOP_REVERSE_DOTTED: 57, + STICK_ARROW_BOTTOM_REVERSE_DOTTED: 58, + + CENTRAL_CONNECTION: 59, + CENTRAL_CONNECTION_REVERSE: 60, + CENTRAL_CONNECTION_DUAL: 61, } as const; const ARROWTYPE = { @@ -244,7 +268,8 @@ export class SequenceDB implements DiagramDB { idTo?: Message['to'], message?: { text: string; wrap: boolean }, messageType?: number, - activate = false + activate = false, + centralConnection?: number ) { if (messageType === this.LINETYPE.ACTIVE_END) { const cnt = this.activationCount(idFrom ?? ''); @@ -271,6 +296,7 @@ export class SequenceDB implements DiagramDB { wrap: message?.wrap ?? this.autoWrap(), type: messageType, activate, + centralConnection: centralConnection ?? 0, }); return true; } @@ -563,6 +589,12 @@ export class SequenceDB implements DiagramDB { case 'activeStart': this.addSignal(param.actor, undefined, undefined, param.signalType); break; + case 'centralConnection': + this.addSignal(param.actor, undefined, undefined, param.signalType); + break; + case 'centralConnectionReverse': + this.addSignal(param.actor, undefined, undefined, param.signalType); + break; case 'activeEnd': this.addSignal(param.actor, undefined, undefined, param.signalType); break; @@ -606,7 +638,14 @@ export class SequenceDB implements DiagramDB { this.state.records.lastDestroyed = undefined; } } - this.addSignal(param.from, param.to, param.msg, param.signalType, param.activate); + this.addSignal( + param.from, + param.to, + param.msg, + param.signalType, + param.activate, + param.centralConnection + ); break; case 'boxStart': this.addBox(param.boxData); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 4e69fda7e..5f4e06dcd 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -104,6 +104,7 @@ describe('more than one sequence diagram', () => { [ { "activate": false, + "centralConnection": 0, "from": "Alice", "id": "0", "message": "Hello Bob, how are you?", @@ -113,6 +114,7 @@ describe('more than one sequence diagram', () => { }, { "activate": false, + "centralConnection": 0, "from": "Bob", "id": "1", "message": "I am good thanks!", @@ -131,6 +133,7 @@ describe('more than one sequence diagram', () => { [ { "activate": false, + "centralConnection": 0, "from": "Alice", "id": "0", "message": "Hello Bob, how are you?", @@ -140,6 +143,7 @@ describe('more than one sequence diagram', () => { }, { "activate": false, + "centralConnection": 0, "from": "Bob", "id": "1", "message": "I am good thanks!", @@ -160,6 +164,7 @@ describe('more than one sequence diagram', () => { [ { "activate": false, + "centralConnection": 0, "from": "Alice", "id": "0", "message": "Hello John, how are you?", @@ -169,6 +174,7 @@ describe('more than one sequence diagram', () => { }, { "activate": false, + "centralConnection": 0, "from": "John", "id": "1", "message": "I am good thanks!", @@ -181,6 +187,254 @@ describe('more than one sequence diagram', () => { }); }); +describe('Central Connection Parsing', () => { + describe('when parsing central connection syntax', () => { + it('should parse actor ()->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse + + // Find the actual message (type: 'addMessage') + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL + activate: true, + type: 0, // SOLID (based on test output) + }); + }); + + it('should parse actor ()-->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()-->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL + activate: true, + type: 1, // DOTTED (based on test output) + }); + }); + + it('should parse actor ->>() actor syntax as CENTRAL_CONNECTION', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(2); // addMessage + centralConnection (no activation for this pattern) + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 59, // CENTRAL_CONNECTION + activate: true, + type: 0, // SOLID (based on actual parsing) + }); + }); + + it('should parse actor ()-->> actor syntax as CENTRAL_CONNECTION_REVERSE', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()-->> Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(2); // addMessage + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 60, // CENTRAL_CONNECTION_REVERSE + activate: false, + type: 1, // DOTTED (based on test output) + }); + }); + + it('should parse actor ()->> actor syntax as CENTRAL_CONNECTION_REVERSE', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()->> Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(2); // addMessage + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 60, // CENTRAL_CONNECTION_REVERSE + activate: false, + type: 0, // SOLID (based on test output) + }); + }); + + it('should parse actor ()<<-->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()<<-->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL + activate: true, + type: 34, // BIDIRECTIONAL_DOTTED + }); + }); + + it('should parse actor ()<<->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()<<->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL + activate: true, + type: 33, // BIDIRECTIONAL_SOLID + }); + }); + + it('should handle multiple central connection types in one diagram', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ()->>() Bob: Message 1 + Bob ()-->> Charlie: Message 2 + Charlie ()<<-->>() Alice: Message 3 + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(8); // 3 addMessages + 5 central connection markers + + // Filter to get only the actual messages + const actualMessages = messages.filter((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessages).toHaveLength(3); + + expect(actualMessages[0]).toMatchObject({ + from: 'Alice', + to: 'Bob', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()->>()) + }); + + expect(actualMessages[1]).toMatchObject({ + from: 'Bob', + to: 'Charlie', + centralConnection: 60, // CENTRAL_CONNECTION_REVERSE (()-->>) + }); + + expect(actualMessages[2]).toMatchObject({ + from: 'Charlie', + to: 'Alice', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()<<-->>()) + }); + }); + + it('should handle central connections with different arrow types', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()-x() Bob: Cross message + Alice ()--x() Bob: Dotted cross message + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(6); // 2 addMessages + 4 central connection markers + + const actualMessages = messages.filter((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessages).toHaveLength(2); + + expect(actualMessages[0]).toMatchObject({ + from: 'Alice', + to: 'Bob', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()-x()) + type: 3, // SOLID_CROSS + }); + + expect(actualMessages[1]).toMatchObject({ + from: 'Alice', + to: 'Bob', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()--x()) + type: 4, // DOTTED_CROSS + }); + }); + + it('should not break existing parsing without central connections', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ->> Bob: Normal message + Bob -->> Alice: Normal dotted message + Alice -x Bob: Normal cross message + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); + + messages.forEach((msg) => { + expect(msg.centralConnection).toBe(0); // No central connection + }); + + expect(messages[0].type).toBe(0); // SOLID (based on actual parsing) + expect(messages[1].type).toBe(1); // DOTTED (based on actual parsing) + expect(messages[2].type).toBe(3); // SOLID_CROSS + }); + }); +}); + describe('when parsing a sequenceDiagram', function () { let diagram; beforeEach(async function () { @@ -2058,6 +2312,36 @@ Bob->>Alice:Got it! expect(messages[0].from).toBe('Alice'); expect(messages[0].to).toBe('Bob'); }); + + it('1 should parse ', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + actor Bob + actor Alice + Bob -|\\ Alice: Hello Alice, how are you? + Bob -|/ Alice: Hello Alice, how are you? + Bob -// Alice: Hello Alice, how are you? + Bob -\\\\ Alice: Hello Alice, how are you? + + Bob \\|- Alice: Hello Alice, how are you? + Bob /|- Alice: Hello Alice, how are you? + Bob //- Alice: Hello Alice, how are you? + Bob \\\\- Alice: Hello Alice, how are you? + `); + + const messages = diagram.db.getMessages(); + }); + + it('2 should parse ', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + actor Bob + actor Alice + Alice ()<<->>() Bob: hey? + `); + + const messages = diagram.db.getMessages(); + }); describe('when parsing extended participant syntax', () => { it('should parse participants with different quote styles and whitespace', async () => { const diagram = await Diagram.fromText(` diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 5fac3cf2d..04d5607ad 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -282,6 +282,49 @@ const drawNote = async function (elem: any, noteModel: NoteModel) { bounds.models.addNote(noteModel); }; +const drawCentralConnection = function ( + elem: any, + msg: any, + msgModel: any, + diagObj: Diagram, + startx: number, + stopx: number, + lineStartY: number +) { + const actors = diagObj.db.getActors(); + const fromActor = actors.get(msg.from); + const toActor = actors.get(msg.to); + const fromCenter = fromActor.x + fromActor.width / 2; + const toCenter = toActor.x + toActor.width / 2; + + const g = elem.append('g'); + + const drawCircle = (cx: number) => { + g.append('circle') + .attr('cx', cx) + .attr('cy', lineStartY) + .attr('r', 5) + .attr('width', 10) + .attr('height', 10); + }; + + const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } = + diagObj.db.LINETYPE; + + switch (msg.centralConnection) { + case CENTRAL_CONNECTION: + drawCircle(toCenter); + break; + case CENTRAL_CONNECTION_REVERSE: + drawCircle(fromCenter); + break; + case CENTRAL_CONNECTION_DUAL: + drawCircle(fromCenter); + drawCircle(toCenter); + break; + } +}; + const messageFont = (cnf) => { return { fontFamily: cnf.messageFontFamily, @@ -367,7 +410,7 @@ async function boundMessage(_diagram, msgModel): Promise { * @param lineStartY - The Y coordinate at which the message line starts * @param diagObj - The diagram object. */ -const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { +const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram, msg) { const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel; const textDims = utils.calculateTextDimensions(message, messageFont(conf)); const textObj = svgDrawCommon.getTextObj(); @@ -433,6 +476,9 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO line.attr('y1', lineStartY); line.attr('x2', stopx); line.attr('y2', lineStartY); + if (hasCentralConnection(msg, diagObj)) { + drawCentralConnection(diagram, msg, msgModel, diagObj, startx, stopx, lineStartY); + } } // Make an SVG Container // Draw the line @@ -441,7 +487,15 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO type === diagObj.db.LINETYPE.DOTTED_CROSS || type === diagObj.db.LINETYPE.DOTTED_POINT || type === diagObj.db.LINETYPE.DOTTED_OPEN || - type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED + type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED || + type === diagObj.db.LINETYPE.SOLID_TOP_DOTTED || + type === diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED || + type === diagObj.db.LINETYPE.STICK_TOP_DOTTED || + type === diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED || + type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED || + type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED || + type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED || + type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED ) { line.style('stroke-dasharray', '3, 3'); line.attr('class', 'messageLine1'); @@ -457,6 +511,51 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO line.attr('stroke-width', 2); line.attr('stroke', 'none'); // handled by theme/css anyway line.style('fill', 'none'); // remove any fill colour + + if (type === diagObj.db.LINETYPE.SOLID_TOP || type === diagObj.db.LINETYPE.SOLID_TOP_DOTTED) { + line.attr('marker-end', 'url(' + url + '#solidTopArrowHead)'); + } + if ( + type === diagObj.db.LINETYPE.SOLID_BOTTOM || + type === diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED + ) { + line.attr('marker-end', 'url(' + url + '#solidBottomArrowHead)'); + } + if (type === diagObj.db.LINETYPE.STICK_TOP || type === diagObj.db.LINETYPE.STICK_TOP_DOTTED) { + line.attr('marker-end', 'url(' + url + '#stickTopArrowHead)'); + } + if ( + type === diagObj.db.LINETYPE.STICK_BOTTOM || + type === diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED + ) { + line.attr('marker-end', 'url(' + url + '#stickBottomArrowHead)'); + } + + if ( + type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE || + type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED + ) { + line.attr('marker-start', 'url(' + url + '#solidBottomArrowHead)'); + } + if ( + type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE || + type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED + ) { + line.attr('marker-start', 'url(' + url + '#solidTopArrowHead)'); + } + if ( + type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE || + type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED + ) { + line.attr('marker-start', 'url(' + url + '#stickBottomArrowHead)'); + } + if ( + type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE || + type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED + ) { + line.attr('marker-start', 'url(' + url + '#stickTopArrowHead)'); + } + if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) { line.attr('marker-end', 'url(' + url + '#arrowhead)'); } @@ -481,7 +580,18 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID || type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED; - if (isBidirectional) { + const isReverseArrowType = + type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE || + type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED || + type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE || + type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED || + type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE || + type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED || + type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE || + type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED; + + let x = 0; + if (isBidirectional || isReverseArrowType) { const SEQUENCE_NUMBER_RADIUS = 6; if (startx < stopx) { @@ -489,6 +599,7 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO } else { line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS); } + x = 3.5; } diagram @@ -498,7 +609,8 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO .attr('x2', startx) .attr('y2', lineStartY) .attr('stroke-width', 0) - .attr('marker-start', 'url(' + url + '#sequencenumber)'); + .attr('marker-start', 'url(' + url + '#sequencenumber)') + .attr('transform', `translate(-${x}, 0)`); diagram .append('text') @@ -508,7 +620,8 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO .attr('font-size', '12px') .attr('text-anchor', 'middle') .attr('class', 'sequenceNumber') - .text(sequenceIndex); + .text(sequenceIndex) + .attr('transform', `translate(-${x}, 0)`); } }; @@ -857,6 +970,10 @@ export const draw = async function (_text: string, id: string, _version: string, svgDraw.insertArrowCrossHead(diagram); svgDraw.insertArrowFilledHead(diagram); svgDraw.insertSequenceNumber(diagram); + svgDraw.insertSolidTopArrowHead(diagram); + svgDraw.insertSolidBottomArrowHead(diagram); + svgDraw.insertStickTopArrowHead(diagram); + svgDraw.insertStickBottomArrowHead(diagram); /** * @param msg - The message to draw. @@ -897,6 +1014,12 @@ export const draw = async function (_text: string, id: string, _version: string, case diagObj.db.LINETYPE.ACTIVE_START: bounds.newActivation(msg, diagram, actors); break; + case diagObj.db.LINETYPE.CENTRAL_CONNECTION: + bounds.newActivation(msg, diagram, actors); + break; + case diagObj.db.LINETYPE.CENTRAL_CONNECTION_REVERSE: + bounds.newActivation(msg, diagram, actors); + break; case diagObj.db.LINETYPE.ACTIVE_END: activeEnd(msg, bounds.getVerticalPos()); break; @@ -1055,7 +1178,7 @@ export const draw = async function (_text: string, id: string, _version: string, createdActors, destroyedActors ); - messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY }); + messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY, msg }); bounds.models.addMessage(msgModel); } catch (e) { log.error('error while drawing message', e); @@ -1068,6 +1191,27 @@ export const draw = async function (_text: string, id: string, _version: string, diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN, diagObj.db.LINETYPE.SOLID, + + diagObj.db.LINETYPE.SOLID_TOP, + diagObj.db.LINETYPE.SOLID_BOTTOM, + diagObj.db.LINETYPE.STICK_TOP, + diagObj.db.LINETYPE.STICK_BOTTOM, + + diagObj.db.LINETYPE.SOLID_TOP_DOTTED, + diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED, + diagObj.db.LINETYPE.STICK_TOP_DOTTED, + diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED, + + diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE, + diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE, + diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE, + diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE, + + diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED, + diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED, + diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED, + diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED, + diagObj.db.LINETYPE.DOTTED, diagObj.db.LINETYPE.SOLID_CROSS, diagObj.db.LINETYPE.DOTTED_CROSS, @@ -1087,7 +1231,7 @@ export const draw = async function (_text: string, id: string, _version: string, await drawActors(diagram, actors, actorKeys, false); for (const e of messagesToDraw) { - await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj); + await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj, e.msg); } if (conf.mirrorActors) { await drawActors(diagram, actors, actorKeys, true); @@ -1461,12 +1605,85 @@ const buildNoteModel = async function (msg, actors, diagObj) { return noteModel; }; +// Central connection positioning constants +const CENTRAL_CONNECTION_BASE_OFFSET = 4; +const CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET = 6; + +/** + * Check if a message has central connection + * @param msg - The message object + * @param diagObj - The diagram object containing LINETYPE constants + * @returns True if the message has any type of central connection + */ +const hasCentralConnection = function (msg, diagObj) { + const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } = + diagObj.db.LINETYPE; + return [CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL].includes( + msg.centralConnection + ); +}; + +/** + * Calculate the positioning offset for central connection arrows + * @param msg - The message object + * @param diagObj - The diagram object containing LINETYPE constants + * @param isArrowToRight - Whether the arrow is pointing to the right + * @returns The offset to apply to startx position + */ +const calculateCentralConnectionOffset = function (msg, diagObj, isArrowToRight) { + const { + CENTRAL_CONNECTION_REVERSE, + CENTRAL_CONNECTION_DUAL, + BIDIRECTIONAL_SOLID, + BIDIRECTIONAL_DOTTED, + } = diagObj.db.LINETYPE; + + let offset = 0; + + if ( + msg.centralConnection === CENTRAL_CONNECTION_REVERSE || + msg.centralConnection === CENTRAL_CONNECTION_DUAL + ) { + offset += CENTRAL_CONNECTION_BASE_OFFSET; + } + + if ( + msg.centralConnection === CENTRAL_CONNECTION_DUAL && + (msg.type === BIDIRECTIONAL_SOLID || msg.type === BIDIRECTIONAL_DOTTED) + ) { + offset += isArrowToRight ? 0 : -CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET; + } + + return offset; +}; + const buildMessageModel = function (msg, actors, diagObj) { if ( ![ diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN, diagObj.db.LINETYPE.SOLID, + + diagObj.db.LINETYPE.SOLID_TOP, + diagObj.db.LINETYPE.SOLID_BOTTOM, + diagObj.db.LINETYPE.STICK_TOP, + diagObj.db.LINETYPE.STICK_BOTTOM, + + diagObj.db.LINETYPE.SOLID_TOP_DOTTED, + diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED, + diagObj.db.LINETYPE.STICK_TOP_DOTTED, + diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED, + + diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE, + diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE, + diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE, + diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE, + + diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED, + diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED, + diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED, + diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED, + diagObj.db.LINETYPE.DOTTED, diagObj.db.LINETYPE.SOLID_CROSS, diagObj.db.LINETYPE.DOTTED_CROSS, @@ -1484,6 +1701,8 @@ const buildMessageModel = function (msg, actors, diagObj) { let startx = isArrowToRight ? fromRight : fromLeft; let stopx = isArrowToRight ? toLeft : toRight; + // Apply central connection positioning adjustments + startx += calculateCentralConnectionOffset(msg, diagObj, isArrowToRight); // As the line width is considered, the left and right values will be off by 2. const isArrowToActivation = Math.abs(toLeft - toRight) > 2; @@ -1517,7 +1736,30 @@ const buildMessageModel = function (msg, actors, diagObj) { * Shorten the length of arrow at the end and move the marker forward (using refX) to have a clean arrowhead * This is not required for open arrows that don't have arrowheads */ - if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) { + if ( + ![ + diagObj.db.LINETYPE.SOLID_OPEN, + diagObj.db.LINETYPE.DOTTED_OPEN, + + diagObj.db.LINETYPE.STICK_TOP, + diagObj.db.LINETYPE.STICK_BOTTOM, + + diagObj.db.LINETYPE.STICK_TOP_DOTTED, + diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED, + + diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED, + diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED, + + diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE, + diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE, + + diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED, + diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED, + + diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE, + diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE, + ].includes(msg.type) + ) { stopx += adjustValue(3); } @@ -1525,9 +1767,14 @@ const buildMessageModel = function (msg, actors, diagObj) { * Shorten start position of bidirectional arrow to accommodate for second arrowhead */ if ( - [diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes( - msg.type - ) + [ + diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, + diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED, + diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED, + diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED, + diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE, + diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE, + ].includes(msg.type) ) { startx -= adjustValue(3); } diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index 1971082a8..7db661930 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -1709,6 +1709,77 @@ const _drawMenuItemTextCandidateFunc = (function () { }; })(); +/** + * Setup arrow head and define the marker. The result is appended to the svg. + * + * @param elem + */ +export const insertSolidTopArrowHead = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'solidTopArrowHead') + .attr('refX', 7.9) + .attr('refY', 7.25) + .attr('markerUnits', 'userSpaceOnUse') + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto-start-reverse') + .append('path') + .attr('d', 'M 0 0 L 10 8 L 0 8 z'); // this is actual shape for arrowhead +}; + +export const insertSolidBottomArrowHead = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'solidBottomArrowHead') + .attr('refX', 7.9) + .attr('refY', 0.75) + .attr('markerUnits', 'userSpaceOnUse') + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto-start-reverse') + .append('path') + .attr('d', 'M 0 0 L 10 0 L 0 8 z'); +}; + +export const insertStickTopArrowHead = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'stickTopArrowHead') + .attr('refX', 7.5) + .attr('refY', 7) + .attr('markerUnits', 'userSpaceOnUse') + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto-start-reverse') + .append('path') + .attr('d', 'M 0 0 L 7 7') + .attr('stroke', 'black') + .attr('stroke-width', 1.5) + .attr('fill', 'none'); +}; + +export const insertStickBottomArrowHead = function (elem) { + elem + .append('defs') + .append('marker') + .attr('id', 'stickBottomArrowHead') + .attr('refX', 7.5) + .attr('refY', 0) + .attr('markerUnits', 'userSpaceOnUse') + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto-start-reverse') + .append('path') + .attr('d', 'M 0 7 L 7 0') + .attr('stroke', 'black') + .attr('stroke-width', 1.5) + .attr('fill', 'none'); +}; + export default { drawRect, drawText, @@ -1731,4 +1802,8 @@ export default { getNoteRect, fixLifeLineHeights, sanitizeUrl, + insertSolidTopArrowHead, + insertSolidBottomArrowHead, + insertStickTopArrowHead, + insertStickBottomArrowHead, }; diff --git a/packages/mermaid/src/diagrams/sequence/types.ts b/packages/mermaid/src/diagrams/sequence/types.ts index 7cf2ead9c..c25463ac6 100644 --- a/packages/mermaid/src/diagrams/sequence/types.ts +++ b/packages/mermaid/src/diagrams/sequence/types.ts @@ -35,6 +35,7 @@ export interface Message { type?: number; activate?: boolean; placement?: string; + centralConnection?: number; } export interface AddMessageParams { @@ -50,6 +51,8 @@ export interface AddMessageParams { | 'destroyParticipant' | 'activeStart' | 'activeEnd' + | 'centralConnection' + | 'centralConnectionReverse' | 'addNote' | 'addLinks' | 'addALink' diff --git a/packages/mermaid/src/docs/syntax/sequenceDiagram.md b/packages/mermaid/src/docs/syntax/sequenceDiagram.md index 6e0ac87bf..25b770484 100644 --- a/packages/mermaid/src/docs/syntax/sequenceDiagram.md +++ b/packages/mermaid/src/docs/syntax/sequenceDiagram.md @@ -216,7 +216,11 @@ Messages can be of two displayed either solid or with a dotted line. [Actor][Arrow][Actor]:Message text ``` -There are ten types of arrows currently supported: +Lines can be solid or dotted, and can end with various types of arrowheads, crosses, or open arrows. + +#### Supported Arrow Types + +**Standard Arrow Types** | Type | Description | | -------- | ---------------------------------------------------- | @@ -231,6 +235,49 @@ There are ten types of arrows currently supported: | `-)` | Solid line with an open arrow at the end (async) | | `--)` | Dotted line with a open arrow at the end (async) | +**Half-Arrows (v+)** + +The following half-arrow types are supported for more expressive sequence diagrams. Both solid and dotted variants are available by increasing the number of dashes (`-` → `--`). + +--- + +| Type | Description | +| ------- | ---------------------------------------------------- | +| `-\|\` | Solid line with top half arrowhead | +| `--\|\` | Dotted line with top half arrowhead | +| `-\|/` | Solid line with bottom half arrowhead | +| `--\|/` | Dotted line with bottom half arrowhead | +| `/\|-` | Solid line with reverse top half arrowhead | +| `/\|--` | Dotted line with reverse top half arrowhead | +| `\\-` | Solid line with reverse bottom half arrowhead | +| `\\--` | Dotted line with reverse bottom half arrowhead | +| `-\\` | Solid line with top stick half arrowhead | +| `--\\` | Dotted line with top stick half arrowhead | +| `-//` | Solid line with bottom stick half arrowhead | +| `--//` | Dotted line with bottom stick half arrowhead | +| `//-` | Solid line with reverse top stick half arrowhead | +| `//--` | Dotted line with reverse top stick half arrowhead | +| `\\-` | Solid line with reverse bottom stick half arrowhead | +| `\\--` | Dotted line with reverse bottom stick half arrowhead | + +## Central Connections (v+) + +Mermaid sequence diagrams support **central lifeline connections** using a `()`. +This is useful to represent messages or signals that connect to a central point, rather than from one actor directly to another. + +To indicate a central connection, append `()` to the arrow syntax. + +#### Basic Syntax + +```mermaid-example +sequenceDiagram + participant Alice + participant John + Alice->>()John: Hello John + Alice()->>John: How are you? + John()->>()Alice: Great! +``` + ## Activations It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations: diff --git a/packages/mermaid/src/mermaid.spec.ts b/packages/mermaid/src/mermaid.spec.ts index 586d3605c..5a7551683 100644 --- a/packages/mermaid/src/mermaid.spec.ts +++ b/packages/mermaid/src/mermaid.spec.ts @@ -207,7 +207,7 @@ describe('when using mermaid and ', () => { [Error: Parse error on line 2: ...equenceDiagramAlice:->Bob: Hello Bob, h... ----------------------^ - Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT'] + Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT'] `); });