diff --git a/.changeset/clean-wolves-turn.md b/.changeset/clean-wolves-turn.md
new file mode 100644
index 000000000..7a44c1c16
--- /dev/null
+++ b/.changeset/clean-wolves-turn.md
@@ -0,0 +1,5 @@
+---
+'mermaid': patch
+---
+
+fix: Render newlines as spaces in class diagrams
diff --git a/.changeset/crazy-loops-matter.md b/.changeset/crazy-loops-matter.md
new file mode 100644
index 000000000..e6377a9e5
--- /dev/null
+++ b/.changeset/crazy-loops-matter.md
@@ -0,0 +1,5 @@
+---
+'mermaid': patch
+---
+
+fix: Handle arrows correctly when auto number is enabled
diff --git a/.changeset/hungry-baths-glow.md b/.changeset/hungry-baths-glow.md
new file mode 100644
index 000000000..b3084bcab
--- /dev/null
+++ b/.changeset/hungry-baths-glow.md
@@ -0,0 +1,5 @@
+---
+'mermaid': minor
+---
+
+feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.
diff --git a/.github/workflows/e2e-applitools.yml b/.github/workflows/e2e-applitools.yml
index dd97b49e1..6aaa91eb8 100644
--- a/.github/workflows/e2e-applitools.yml
+++ b/.github/workflows/e2e-applitools.yml
@@ -23,9 +23,6 @@ env:
jobs:
e2e-applitools:
runs-on: ubuntu-latest
- container:
- image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1
- options: --user 1001
steps:
- if: ${{ ! env.USE_APPLI }}
name: Warn if not using Applitools
diff --git a/.github/workflows/e2e-timings.yml b/.github/workflows/e2e-timings.yml
index 2bbfa8412..21dbda293 100644
--- a/.github/workflows/e2e-timings.yml
+++ b/.github/workflows/e2e-timings.yml
@@ -58,7 +58,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Commit and create pull request
- uses: peter-evans/create-pull-request@cb4d3bfce175d44325c6b7697f81e0afe8a79bdf
+ uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a
with:
add-paths: |
cypress/timings.json
diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js
index bd2a96b34..6cea402f8 100644
--- a/cypress/integration/rendering/classDiagram.spec.js
+++ b/cypress/integration/rendering/classDiagram.spec.js
@@ -524,5 +524,18 @@ describe('Class diagram', () => {
`,
{}
);
+ it('should handle an empty class body with empty braces', () => {
+ imgSnapshotTest(
+ ` classDiagram
+ class FooBase~T~ {}
+ class Bar {
+ +Zip
+ +Zap()
+ }
+ FooBase <|-- Ba
+ `,
+ { flowchart: { defaultRenderer: 'elk' } }
+ );
+ });
});
});
diff --git a/cypress/integration/rendering/sequencediagram-v2.spec.js b/cypress/integration/rendering/sequencediagram-v2.spec.js
new file mode 100644
index 000000000..f1c2aafbd
--- /dev/null
+++ b/cypress/integration/rendering/sequencediagram-v2.spec.js
@@ -0,0 +1,659 @@
+import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
+
+const looks = ['classic'];
+const participantTypes = [
+ { type: 'participant', display: 'participant' },
+ { type: 'actor', display: 'actor' },
+ { type: 'boundary', display: 'boundary' },
+ { type: 'control', display: 'control' },
+ { type: 'entity', display: 'entity' },
+ { type: 'database', display: 'database' },
+ { type: 'collections', display: 'collections' },
+ { type: 'queue', display: 'queue' },
+];
+
+const restrictedTypes = ['boundary', 'control', 'entity', 'database', 'collections', 'queue'];
+
+const interactionTypes = ['->>', '-->>', '->', '-->', '-x', '--x', '->>+', '-->>+'];
+
+const notePositions = ['left of', 'right of', 'over'];
+
+function getParticipantLine(name, type, alias) {
+ if (restrictedTypes.includes(type)) {
+ return ` participant ${name}@{ "type" : "${type}" }\n`;
+ } else if (alias) {
+ return ` participant ${name}@{ "type" : "${type}" } \n`;
+ } else {
+ return ` participant ${name}@{ "type" : "${type}" }\n`;
+ }
+}
+
+looks.forEach((look) => {
+ describe(`Sequence Diagram Tests - ${look} look`, () => {
+ it('should render all participant types', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ participantTypes.forEach((pt, index) => {
+ const name = `${pt.display}${index}`;
+ diagramCode += getParticipantLine(name, pt.type);
+ });
+ for (let i = 0; i < participantTypes.length - 1; i++) {
+ diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
+ }
+ imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } });
+ });
+
+ it('should render all interaction types', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ diagramCode += getParticipantLine('A', 'actor');
+ diagramCode += getParticipantLine('B', 'boundary');
+ interactionTypes.forEach((interaction, index) => {
+ diagramCode += ` A ${interaction} B: ${interaction} message ${index}\n`;
+ });
+ imgSnapshotTest(diagramCode, { look });
+ });
+
+ it('should render participant creation and destruction', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ participantTypes.forEach((pt, index) => {
+ const name = `${pt.display}${index}`;
+ diagramCode += getParticipantLine('A', pt.type);
+ diagramCode += getParticipantLine('B', pt.type);
+ diagramCode += ` create participant ${name}@{ "type" : "${pt.type}" }\n`;
+ diagramCode += ` A ->> ${name}: Hello ${pt.display}\n`;
+ if (index % 2 === 0) {
+ diagramCode += ` destroy ${name}\n`;
+ }
+ });
+ imgSnapshotTest(diagramCode, { look });
+ });
+
+ it('should render notes in all positions', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ diagramCode += getParticipantLine('A', 'actor');
+ diagramCode += getParticipantLine('B', 'boundary');
+ notePositions.forEach((position, index) => {
+ diagramCode += ` Note ${position} A: Note ${position} ${index}\n`;
+ });
+ diagramCode += ` A ->> B: Message with notes\n`;
+ imgSnapshotTest(diagramCode, { look });
+ });
+
+ it('should render parallel interactions', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ participantTypes.slice(0, 4).forEach((pt, index) => {
+ diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
+ });
+ diagramCode += ` par Parallel actions\n`;
+ for (let i = 0; i < 3; i += 2) {
+ diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
+ if (i < participantTypes.length - 2) {
+ diagramCode += ` and\n`;
+ }
+ }
+ diagramCode += ` end\n`;
+ imgSnapshotTest(diagramCode, { look });
+ });
+
+ it('should render alternative flows', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ diagramCode += getParticipantLine('A', 'actor');
+ diagramCode += getParticipantLine('B', 'boundary');
+ diagramCode += ` alt Successful case\n`;
+ diagramCode += ` A ->> B: Request\n`;
+ diagramCode += ` B -->> A: Success\n`;
+ diagramCode += ` else Failure case\n`;
+ diagramCode += ` A ->> B: Request\n`;
+ diagramCode += ` B --x A: Failure\n`;
+ diagramCode += ` end\n`;
+ imgSnapshotTest(diagramCode, { look });
+ });
+
+ it('should render loops', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ participantTypes.slice(0, 3).forEach((pt, index) => {
+ diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
+ });
+ diagramCode += ` loop For each participant\n`;
+ for (let i = 0; i < 3; i++) {
+ diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Message ${i}\n`;
+ }
+ diagramCode += ` end\n`;
+ imgSnapshotTest(diagramCode, { look });
+ });
+
+ it('should render boxes around groups', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ diagramCode += ` box Group 1\n`;
+ participantTypes.slice(0, 3).forEach((pt, index) => {
+ diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
+ });
+ diagramCode += ` end\n`;
+ diagramCode += ` box rgb(200,220,255) Group 2\n`;
+ participantTypes.slice(3, 6).forEach((pt, index) => {
+ diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
+ });
+ diagramCode += ` end\n`;
+ diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[3].display}0: Cross-group message\n`;
+ imgSnapshotTest(diagramCode, { look });
+ });
+
+ it('should render with different font settings', () => {
+ let diagramCode = `sequenceDiagram\n`;
+ participantTypes.slice(0, 3).forEach((pt, index) => {
+ diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
+ });
+ diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Regular message\n`;
+ diagramCode += ` Note right of ${participantTypes[1].display}1: Regular note\n`;
+ imgSnapshotTest(diagramCode, {
+ look,
+ sequence: {
+ actorFontFamily: 'courier',
+ actorFontSize: 14,
+ messageFontFamily: 'Arial',
+ messageFontSize: 12,
+ noteFontFamily: 'times',
+ noteFontSize: 16,
+ noteAlign: 'left',
+ },
+ });
+ });
+ });
+});
+
+// Additional tests for specific combinations
+describe('Sequence Diagram Special Cases', () => {
+ it('should render complex sequence with all features', () => {
+ const diagramCode = `
+ sequenceDiagram
+ box rgb(200,220,255) Authentication
+ actor User
+ participant LoginUI@{ "type": "boundary" }
+ participant AuthService@{ "type": "control" }
+ participant UserDB@{ "type": "database" }
+ end
+
+ box rgb(200,255,220) Order Processing
+ participant Order@{ "type": "entity" }
+ participant OrderQueue@{ "type": "queue" }
+ participant AuditLogs@{ "type": "collections" }
+ end
+
+ User ->> LoginUI: Enter credentials
+ LoginUI ->> AuthService: Validate
+ AuthService ->> UserDB: Query user
+ UserDB -->> AuthService: User data
+ alt Valid credentials
+ AuthService -->> LoginUI: Success
+ LoginUI -->> User: Welcome
+
+ par Place order
+ User ->> Order: New order
+ Order ->> OrderQueue: Process
+ and
+ Order ->> AuditLogs: Record
+ end
+
+ loop Until confirmed
+ OrderQueue ->> Order: Update status
+ Order -->> User: Notification
+ end
+ else Invalid credentials
+ AuthService --x LoginUI: Failure
+ LoginUI --x User: Retry
+ end
+ `;
+ imgSnapshotTest(diagramCode, {});
+ });
+
+ it('should render with wrapped messages and notes', () => {
+ const diagramCode = `
+ sequenceDiagram
+ participant A
+ participant B
+
+ A ->> B: This is a very long message that should wrap properly in the diagram rendering
+ Note over A,B: This is a very long note that should also wrap properly when rendered in the diagram
+
+ par Wrapped parallel
+ A ->> B: Parallel message 1
with explicit line break
+ and
+ B ->> A: Parallel message 2
with explicit line break
+ end
+
+ loop Wrapped loop
+ Note right of B: This is a long note
in a loop
+ A ->> B: Message in loop
+ end
+ `;
+ imgSnapshotTest(diagramCode, { sequence: { wrap: true } });
+ });
+ describe('Sequence Diagram Rendering with Different Participant Types', () => {
+ it('should render a sequence diagram with various participant types', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant User@{ "type": "actor" }
+ participant AuthService@{ "type": "control" }
+ participant UI@{ "type": "boundary" }
+ participant OrderController@{ "type": "control" }
+ participant Product@{ "type": "entity" }
+ participant MongoDB@{ "type": "database" }
+ participant Products@{ "type": "collections" }
+ participant OrderQueue@{ "type": "queue" }
+ User ->> UI: Login request
+ UI ->> AuthService: Validate credentials
+ AuthService -->> UI: Authentication token
+ UI ->> OrderController: Place order
+ OrderController ->> Product: Check availability
+ Product -->> OrderController: Available
+ OrderController ->> MongoDB: Save order
+ MongoDB -->> OrderController: Order saved
+ OrderController ->> OrderQueue: Process payment
+ OrderQueue -->> User: Order confirmation
+ `
+ );
+ });
+
+ it('should render participant creation and destruction with different types', () => {
+ imgSnapshotTest(`
+ sequenceDiagram
+ participant Alice@{ "type" : "boundary" }
+ Alice->>Bob: Hello Bob, how are you ?
+ Bob->>Alice: Fine, thank you. And you?
+ create participant Carl@{ "type" : "control" }
+ Alice->>Carl: Hi Carl!
+ create actor D as Donald
+ Carl->>D: Hi!
+ destroy Carl
+ Alice-xCarl: We are too many
+ destroy Bob
+ Bob->>Alice: I agree
+ `);
+ });
+
+ it('should handle complex interactions between different participant types', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ box rgb(200,220,255) Authentication
+ participant User@{ "type": "actor" }
+ participant LoginUI@{ "type": "boundary" }
+ participant AuthService@{ "type": "control" }
+ participant UserDB@{ "type": "database" }
+ end
+
+ box rgb(200,255,220) Order Processing
+ participant Order@{ "type": "entity" }
+ participant OrderQueue@{ "type": "queue" }
+ participant AuditLogs@{ "type": "collections" }
+ end
+
+ User ->> LoginUI: Enter credentials
+ LoginUI ->> AuthService: Validate
+ AuthService ->> UserDB: Query user
+ UserDB -->> AuthService: User data
+
+ alt Valid credentials
+ AuthService -->> LoginUI: Success
+ LoginUI -->> User: Welcome
+
+ par Place order
+ User ->> Order: New order
+ Order ->> OrderQueue: Process
+ and
+ Order ->> AuditLogs: Record
+ end
+
+ loop Until confirmed
+ OrderQueue ->> Order: Update status
+ Order -->> User: Notification
+ end
+ else Invalid credentials
+ AuthService --x LoginUI: Failure
+ LoginUI --x User: Retry
+ end
+ `,
+ { sequence: { useMaxWidth: false } }
+ );
+ });
+
+ it('should render parallel processes with different participant types', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Customer@{ "type": "actor" }
+ participant Frontend@{ "type": "participant" }
+ participant PaymentService@{ "type": "boundary" }
+ participant InventoryManager@{ "type": "control" }
+ participant Order@{ "type": "entity" }
+ participant OrdersDB@{ "type": "database" }
+ participant NotificationQueue@{ "type": "queue" }
+
+ Customer ->> Frontend: Place order
+ Frontend ->> Order: Create order
+ par Parallel Processing
+ Order ->> PaymentService: Process payment
+ and
+ Order ->> InventoryManager: Reserve items
+ end
+ PaymentService -->> Order: Payment confirmed
+ InventoryManager -->> Order: Items reserved
+ Order ->> OrdersDB: Save finalized order
+ OrdersDB -->> Order: Order saved
+ Order ->> NotificationQueue: Send confirmation
+ NotificationQueue -->> Customer: Order confirmation
+ `
+ );
+ });
+ });
+ it('should render different participant types with notes and loops', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ actor Admin
+ participant Dashboard
+ participant AuthService@{ "type" : "boundary" }
+ participant UserManager@{ "type" : "control" }
+ participant UserProfile@{ "type" : "entity" }
+ participant UserDB@{ "type" : "database" }
+ participant Logs@{ "type" : "database" }
+
+ Admin ->> Dashboard: Open user management
+ loop Authentication check
+ Dashboard ->> AuthService: Verify admin rights
+ AuthService ->> Dashboard: Access granted
+ end
+ Dashboard ->> UserManager: List users
+ UserManager ->> UserDB: Query users
+ UserDB ->> UserManager: Return user data
+ Note right of UserDB: Encrypted data
requires decryption
+ UserManager ->> UserProfile: Format profiles
+ UserProfile ->> UserManager: Formatted data
+ UserManager ->> Dashboard: Display users
+ Dashboard ->> Logs: Record access
+ Logs ->> Admin: Audit trail
+ `
+ );
+ });
+
+ it('should render different participant types with alternative flows', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ actor Client
+ participant MobileApp
+ participant CloudService@{ "type" : "boundary" }
+ participant DataProcessor@{ "type" : "control" }
+ participant Transaction@{ "type" : "entity" }
+ participant TransactionsDB@{ "type" : "database" }
+ participant EventBus@{ "type" : "queue" }
+
+ Client ->> MobileApp: Initiate transaction
+ MobileApp ->> CloudService: Authenticate
+ alt Authentication successful
+ CloudService -->> MobileApp: Auth token
+ MobileApp ->> DataProcessor: Process data
+ DataProcessor ->> Transaction: Create transaction
+ Transaction ->> TransactionsDB: Save record
+ TransactionsDB -->> Transaction: Confirmation
+ Transaction ->> EventBus: Publish event
+ EventBus -->> Client: Notification
+ else Authentication failed
+ CloudService -->> MobileApp: Error
+ MobileApp -->> Client: Show error
+ end
+ `
+ );
+ });
+
+ it('should render different participant types with wrapping text', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant B@{ "type" : "boundary" }
+ participant C@{ "type" : "control" }
+ participant E@{ "type" : "entity" }
+ participant DB@{ "type" : "database" }
+ participant COL@{ "type" : "collections" }
+ participant Q@{ "type" : "queue" }
+
+ FE ->> B: Another long message
with explicit
line breaks
+ B -->> FE: Response message that is also quite long and needs to wrap
+ FE ->> C: Process data
+ C ->> E: Validate
+ E -->> C: Validation result
+ C ->> DB: Save
+ DB -->> C: Save result
+ C ->> COL: Log
+ COL -->> Q: Forward
+ Q -->> LongNameUser: Final response with confirmation of all actions taken
+ `,
+ { sequence: { wrap: true } }
+ );
+ });
+
+ describe('Sequence Diagram - New Participant Types with Long Notes and Messages', () => {
+ it('should render long notes left of boundary', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "boundary" }
+ actor Bob
+ Alice->>Bob: Hola
+ Note left of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ Bob->>Alice: I'm short though
+ `,
+ {}
+ );
+ });
+
+ it('should render wrapped long notes left of control', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "control" }
+ actor Bob
+ Alice->>Bob: Hola
+ Note left of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ Bob->>Alice: I'm short though
+ `,
+ {}
+ );
+ });
+
+ it('should render long notes right of entity', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "entity" }
+ actor Bob
+ Alice->>Bob: Hola
+ Note right of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ Bob->>Alice: I'm short though
+ `,
+ {}
+ );
+ });
+
+ it('should render wrapped long notes right of database', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "database" }
+ actor Bob
+ Alice->>Bob: Hola
+ Note right of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ Bob->>Alice: I'm short though
+ `,
+ {}
+ );
+ });
+
+ it('should render long notes over collections', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "collections" }
+ actor Bob
+ Alice->>Bob: Hola
+ Note over Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ Bob->>Alice: I'm short though
+ `,
+ {}
+ );
+ });
+
+ it('should render wrapped long notes over queue', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "queue" }
+ actor Bob
+ Alice->>Bob: Hola
+ Note over Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ Bob->>Alice: I'm short though
+ `,
+ {}
+ );
+ });
+
+ it('should render notes over actor and boundary', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ actor Alice
+ participant Charlie@{ "type" : "boundary" }
+ note over Alice: Some note
+ note over Charlie: Other note
+ `,
+ {}
+ );
+ });
+
+ it('should render long messages from database to collections', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "database" }
+ participant Bob@{ "type" : "collections" }
+ Alice->>Bob: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ Bob->>Alice: I'm short though
+ `,
+ {}
+ );
+ });
+
+ it('should render wrapped long messages from control to entity', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "control" }
+ participant Bob@{ "type" : "entity" }
+ Alice->>Bob:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ Bob->>Alice: I'm short though
+ `,
+ {}
+ );
+ });
+
+ it('should render long messages from queue to boundary', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ participant Alice@{ "type" : "queue" }
+ participant Bob@{ "type" : "boundary" }
+ Alice->>Bob: I'm short
+ Bob->>Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ `,
+ {}
+ );
+ });
+
+ it('should render wrapped long messages from actor to database', () => {
+ imgSnapshotTest(
+ `
+ sequenceDiagram
+ actor Alice
+ participant Bob@{ "type" : "database" }
+ Alice->>Bob: I'm short
+ Bob->>Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
+ `,
+ {}
+ );
+ });
+ });
+
+ describe('svg size', () => {
+ it('should render a sequence diagram when useMaxWidth is true (default)', () => {
+ renderGraph(
+ `
+ sequenceDiagram
+ actor Alice
+ participant Bob@{ "type" : "boundary" }
+ participant John@{ "type" : "control" }
+ Alice ->> Bob: Hello Bob, how are you?
+ Bob-->>John: How about you John?
+ Bob--x Alice: I am good thanks!
+ Bob-x John: I am good thanks!
+ Note right of John: Bob thinks a long
long time, so long
that the text does
not fit on a row.
+ Bob-->Alice: Checking with John...
+ alt either this
+ Alice->>John: Yes
+ else or this
+ Alice->>John: No
+ else or this will happen
+ Alice->John: Maybe
+ end
+ par this happens in parallel
+ Alice -->> Bob: Parallel message 1
+ and
+ Alice -->> John: Parallel message 2
+ end
+ `,
+ { sequence: { useMaxWidth: true } }
+ );
+ cy.get('svg').should((svg) => {
+ expect(svg).to.have.attr('width', '100%');
+ const style = svg.attr('style');
+ expect(style).to.match(/^max-width: [\d.]+px;$/);
+ const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
+ expect(maxWidthValue).to.be.within(820 * 0.95, 820 * 1.05);
+ });
+ });
+
+ it('should render a sequence diagram when useMaxWidth is false', () => {
+ renderGraph(
+ `
+ sequenceDiagram
+ actor Alice
+ participant Bob@{ "type" : "boundary" }
+ participant John@{ "type" : "control" }
+ Alice ->> Bob: Hello Bob, how are you?
+ Bob-->>John: How about you John?
+ Bob--x Alice: I am good thanks!
+ Bob-x John: I am good thanks!
+ Note right of John: Bob thinks a long
long time, so long
that the text does
not fit on a row.
+ Bob-->Alice: Checking with John...
+ alt either this
+ Alice->>John: Yes
+ else or this
+ Alice->>John: No
+ else or this will happen
+ Alice->John: Maybe
+ end
+ par this happens in parallel
+ Alice -->> Bob: Parallel message 1
+ and
+ Alice -->> John: Parallel message 2
+ end
+ `,
+ { sequence: { useMaxWidth: false } }
+ );
+ cy.get('svg').should((svg) => {
+ const width = parseFloat(svg.attr('width'));
+ expect(width).to.be.within(820 * 0.95, 820 * 1.05);
+ expect(svg).to.not.have.attr('style');
+ });
+ });
+ });
+});
diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js
index f18e99abf..6709b557c 100644
--- a/cypress/integration/rendering/sequencediagram.spec.js
+++ b/cypress/integration/rendering/sequencediagram.spec.js
@@ -893,6 +893,17 @@ describe('Sequence diagram', () => {
}
);
});
+
+ it('should handle bidirectional arrows with autonumber', () => {
+ imgSnapshotTest(`
+ sequenceDiagram
+ autonumber
+ participant A
+ participant B
+ A<<->>B: This is a bidirectional message
+ A->B: This is a normal message`);
+ });
+
it('should support actor links and properties when not mirrored EXPERIMENTAL: USE WITH CAUTION', () => {
//Be aware that the syntax for "properties" is likely to be changed.
imgSnapshotTest(
diff --git a/cypress/timings.json b/cypress/timings.json
index 86d5b5222..5708c3414 100644
--- a/cypress/timings.json
+++ b/cypress/timings.json
@@ -2,219 +2,223 @@
"durations": [
{
"spec": "cypress/integration/other/configuration.spec.js",
- "duration": 6297
+ "duration": 6162
},
{
"spec": "cypress/integration/other/external-diagrams.spec.js",
- "duration": 2187
+ "duration": 2148
},
{
"spec": "cypress/integration/other/ghsa.spec.js",
- "duration": 3509
+ "duration": 3585
},
{
"spec": "cypress/integration/other/iife.spec.js",
- "duration": 2218
+ "duration": 2099
},
{
"spec": "cypress/integration/other/interaction.spec.js",
- "duration": 12104
+ "duration": 12119
},
{
"spec": "cypress/integration/other/rerender.spec.js",
- "duration": 2151
+ "duration": 2063
},
{
"spec": "cypress/integration/other/xss.spec.js",
- "duration": 33064
+ "duration": 31921
},
{
"spec": "cypress/integration/rendering/appli.spec.js",
- "duration": 3488
+ "duration": 3385
},
{
"spec": "cypress/integration/rendering/architecture.spec.ts",
- "duration": 106
+ "duration": 108
},
{
"spec": "cypress/integration/rendering/block.spec.js",
- "duration": 18317
+ "duration": 18063
},
{
"spec": "cypress/integration/rendering/c4.spec.js",
- "duration": 5592
+ "duration": 5519
},
{
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
- "duration": 39358
+ "duration": 40040
},
{
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
- "duration": 37160
+ "duration": 38665
},
{
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
- "duration": 23660
+ "duration": 22836
},
{
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
- "duration": 36866
+ "duration": 37096
},
{
"spec": "cypress/integration/rendering/classDiagram.spec.js",
- "duration": 17334
+ "duration": 16452
},
{
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
- "duration": 9871
+ "duration": 10387
},
{
"spec": "cypress/integration/rendering/current.spec.js",
- "duration": 2833
+ "duration": 2803
},
{
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
- "duration": 85321
+ "duration": 86891
},
{
"spec": "cypress/integration/rendering/erDiagram.spec.js",
- "duration": 15673
+ "duration": 15206
},
{
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
- "duration": 3724
+ "duration": 3540
},
{
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
- "duration": 41178
+ "duration": 41975
},
{
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
- "duration": 29966
+ "duration": 30909
},
{
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
- "duration": 7689
+ "duration": 7881
},
{
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
- "duration": 24709
+ "duration": 24294
},
{
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
- "duration": 45565
+ "duration": 47652
},
{
"spec": "cypress/integration/rendering/flowchart.spec.js",
- "duration": 31144
+ "duration": 32049
},
{
"spec": "cypress/integration/rendering/gantt.spec.js",
- "duration": 20808
+ "duration": 20248
},
{
"spec": "cypress/integration/rendering/gitGraph.spec.js",
- "duration": 49985
+ "duration": 51202
},
{
"spec": "cypress/integration/rendering/iconShape.spec.ts",
- "duration": 273272
+ "duration": 283546
},
{
"spec": "cypress/integration/rendering/imageShape.spec.ts",
- "duration": 55880
+ "duration": 57257
},
{
"spec": "cypress/integration/rendering/info.spec.ts",
- "duration": 3271
+ "duration": 3352
},
{
"spec": "cypress/integration/rendering/journey.spec.js",
- "duration": 7293
+ "duration": 7423
},
{
"spec": "cypress/integration/rendering/kanban.spec.ts",
- "duration": 7861
+ "duration": 7804
},
{
"spec": "cypress/integration/rendering/katex.spec.js",
- "duration": 3922
+ "duration": 3847
},
{
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
- "duration": 2726
+ "duration": 2637
},
{
"spec": "cypress/integration/rendering/mindmap.spec.ts",
- "duration": 11670
+ "duration": 11658
},
{
"spec": "cypress/integration/rendering/newShapes.spec.ts",
- "duration": 146020
+ "duration": 149500
},
{
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
- "duration": 114244
+ "duration": 115427
},
{
"spec": "cypress/integration/rendering/packet.spec.ts",
- "duration": 5036
+ "duration": 4801
},
{
"spec": "cypress/integration/rendering/pie.spec.ts",
- "duration": 6545
+ "duration": 6786
},
{
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
- "duration": 9097
+ "duration": 9422
},
{
"spec": "cypress/integration/rendering/radar.spec.js",
- "duration": 5676
+ "duration": 5652
},
{
"spec": "cypress/integration/rendering/requirement.spec.js",
- "duration": 2795
+ "duration": 2787
},
{
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
- "duration": 51660
+ "duration": 53631
},
{
"spec": "cypress/integration/rendering/sankey.spec.ts",
- "duration": 6957
+ "duration": 7075
+ },
+ {
+ "spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
+ "duration": 20446
},
{
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
- "duration": 36026
+ "duration": 37326
},
{
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
- "duration": 29551
+ "duration": 29208
},
{
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
- "duration": 17364
+ "duration": 16328
},
{
"spec": "cypress/integration/rendering/theme.spec.js",
- "duration": 30209
+ "duration": 30541
},
{
"spec": "cypress/integration/rendering/timeline.spec.ts",
- "duration": 8699
+ "duration": 8611
},
{
"spec": "cypress/integration/rendering/treemap.spec.ts",
- "duration": 12168
+ "duration": 11878
},
{
"spec": "cypress/integration/rendering/xyChart.spec.js",
- "duration": 21453
+ "duration": 20400
},
{
"spec": "cypress/integration/rendering/zenuml.spec.js",
- "duration": 3577
+ "duration": 3528
}
]
}
diff --git a/docs/config/faq.md b/docs/config/faq.md
index db775e438..6d27b658e 100644
--- a/docs/config/faq.md
+++ b/docs/config/faq.md
@@ -6,7 +6,7 @@
# Frequently Asked Questions
-1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
+1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
diff --git a/docs/config/setup/config/README.md b/docs/config/setup/config/README.md
index 67fca78eb..c811c7b08 100644
--- a/docs/config/setup/config/README.md
+++ b/docs/config/setup/config/README.md
@@ -19,6 +19,7 @@
- [addDirective](functions/addDirective.md)
- [getConfig](functions/getConfig.md)
- [getSiteConfig](functions/getSiteConfig.md)
+- [getUserDefinedConfig](functions/getUserDefinedConfig.md)
- [reset](functions/reset.md)
- [sanitize](functions/sanitize.md)
- [saveConfigFromInitialize](functions/saveConfigFromInitialize.md)
diff --git a/docs/config/setup/config/functions/getUserDefinedConfig.md b/docs/config/setup/config/functions/getUserDefinedConfig.md
new file mode 100644
index 000000000..ed39f1337
--- /dev/null
+++ b/docs/config/setup/config/functions/getUserDefinedConfig.md
@@ -0,0 +1,19 @@
+> **Warning**
+>
+> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
+>
+> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md).
+
+[**mermaid**](../../README.md)
+
+---
+
+# Function: getUserDefinedConfig()
+
+> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
+
+Defined in: [packages/mermaid/src/config.ts:252](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L252)
+
+## Returns
+
+[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
diff --git a/docs/config/setup/mermaid/interfaces/ParseOptions.md b/docs/config/setup/mermaid/interfaces/ParseOptions.md
index ea96f2706..e068a91fb 100644
--- a/docs/config/setup/mermaid/interfaces/ParseOptions.md
+++ b/docs/config/setup/mermaid/interfaces/ParseOptions.md
@@ -10,7 +10,7 @@
# Interface: ParseOptions
-Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72)
+Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mer
> `optional` **suppressErrors**: `boolean`
-Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77)
+Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
The `parseError` function will not be called.
diff --git a/docs/config/setup/mermaid/interfaces/ParseResult.md b/docs/config/setup/mermaid/interfaces/ParseResult.md
index 7a5990610..1651a6fa9 100644
--- a/docs/config/setup/mermaid/interfaces/ParseResult.md
+++ b/docs/config/setup/mermaid/interfaces/ParseResult.md
@@ -10,7 +10,7 @@
# Interface: ParseResult
-Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
+Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mer
> **config**: [`MermaidConfig`](MermaidConfig.md)
-Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
+Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
The config passed as YAML frontmatter or directives
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
> **diagramType**: `string`
-Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
+Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
The diagram type, e.g. 'flowchart', 'sequence', etc.
diff --git a/docs/config/setup/mermaid/interfaces/RenderResult.md b/docs/config/setup/mermaid/interfaces/RenderResult.md
index fc5fac4f5..c0e5496b8 100644
--- a/docs/config/setup/mermaid/interfaces/RenderResult.md
+++ b/docs/config/setup/mermaid/interfaces/RenderResult.md
@@ -10,7 +10,7 @@
# Interface: RenderResult
-Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
+Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mer
> `optional` **bindFunctions**: (`element`) => `void`
-Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
+Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
Bind function to be called after the svg has been inserted into the DOM.
This is necessary for adding event listeners to the elements in the svg.
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
> **diagramType**: `string`
-Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
+Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
The diagram type, e.g. 'flowchart', 'sequence', etc.
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
> **svg**: `string`
-Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
+Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
The svg code for the rendered graph.
diff --git a/docs/syntax/flowchart.md b/docs/syntax/flowchart.md
index daaa29581..08c145f6f 100644
--- a/docs/syntax/flowchart.md
+++ b/docs/syntax/flowchart.md
@@ -983,11 +983,23 @@ flowchart TD
- `b`
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
-- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
+- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
- `on`
- `off`
-These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
+If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
+
+```mermaid-example
+flowchart TD
+ %% My image with a constrained aspect ratio
+ A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
+```
+
+```mermaid
+flowchart TD
+ %% My image with a constrained aspect ratio
+ A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
+```
## Links between nodes
diff --git a/docs/syntax/sequenceDiagram.md b/docs/syntax/sequenceDiagram.md
index 84240a0cd..a8f502dcd 100644
--- a/docs/syntax/sequenceDiagram.md
+++ b/docs/syntax/sequenceDiagram.md
@@ -74,6 +74,126 @@ sequenceDiagram
Bob->>Alice: Hi Alice
```
+### Boundary
+
+If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "boundary" }
+ participant Bob
+ Alice->>Bob: Request from boundary
+ Bob->>Alice: Response to boundary
+```
+
+```mermaid
+sequenceDiagram
+ participant Alice@{ "type" : "boundary" }
+ participant Bob
+ Alice->>Bob: Request from boundary
+ Bob->>Alice: Response to boundary
+```
+
+### Control
+
+If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "control" }
+ participant Bob
+ Alice->>Bob: Control request
+ Bob->>Alice: Control response
+```
+
+```mermaid
+sequenceDiagram
+ participant Alice@{ "type" : "control" }
+ participant Bob
+ Alice->>Bob: Control request
+ Bob->>Alice: Control response
+```
+
+### Entity
+
+If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "entity" }
+ participant Bob
+ Alice->>Bob: Entity request
+ Bob->>Alice: Entity response
+```
+
+```mermaid
+sequenceDiagram
+ participant Alice@{ "type" : "entity" }
+ participant Bob
+ Alice->>Bob: Entity request
+ Bob->>Alice: Entity response
+```
+
+### Database
+
+If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "database" }
+ participant Bob
+ Alice->>Bob: DB query
+ Bob->>Alice: DB result
+```
+
+```mermaid
+sequenceDiagram
+ participant Alice@{ "type" : "database" }
+ participant Bob
+ Alice->>Bob: DB query
+ Bob->>Alice: DB result
+```
+
+### Collections
+
+If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "collections" }
+ participant Bob
+ Alice->>Bob: Collections request
+ Bob->>Alice: Collections response
+```
+
+```mermaid
+sequenceDiagram
+ participant Alice@{ "type" : "collections" }
+ participant Bob
+ Alice->>Bob: Collections request
+ Bob->>Alice: Collections response
+```
+
+### Queue
+
+If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "queue" }
+ participant Bob
+ Alice->>Bob: Queue message
+ Bob->>Alice: Queue response
+```
+
+```mermaid
+sequenceDiagram
+ participant Alice@{ "type" : "queue" }
+ participant Bob
+ Alice->>Bob: Queue message
+ Bob->>Alice: Queue response
+```
+
### Aliases
The actor can have a convenient identifier and a descriptive label.
diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json
index 56446c34b..9e18739ed 100644
--- a/packages/mermaid/package.json
+++ b/packages/mermaid/package.json
@@ -68,7 +68,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "^7.0.4",
- "@iconify/utils": "^2.1.33",
+ "@iconify/utils": "^3.0.1",
"@mermaid-js/parser": "workspace:^",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.3",
diff --git a/packages/mermaid/src/config.spec.ts b/packages/mermaid/src/config.spec.ts
index 000be1282..7fbae03af 100644
--- a/packages/mermaid/src/config.spec.ts
+++ b/packages/mermaid/src/config.spec.ts
@@ -78,3 +78,187 @@ describe('when working with site config', () => {
expect(config_4.altFontFamily).toBeUndefined();
});
});
+
+describe('getUserDefinedConfig', () => {
+ beforeEach(() => {
+ configApi.reset();
+ });
+
+ it('should return empty object when no user config is defined', () => {
+ const userConfig = configApi.getUserDefinedConfig();
+ expect(userConfig).toEqual({});
+ });
+
+ it('should return config from initialize only', () => {
+ const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial' };
+ configApi.saveConfigFromInitialize(initConfig);
+
+ const userConfig = configApi.getUserDefinedConfig();
+ expect(userConfig).toEqual(initConfig);
+ });
+
+ it('should return config from directives only', () => {
+ const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
+ const directive2: MermaidConfig = { theme: 'forest' };
+
+ configApi.addDirective(directive1);
+ configApi.addDirective(directive2);
+
+ expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
+ {
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "layout": "elk",
+ "theme": "forest",
+ }
+ `);
+ });
+
+ it('should combine initialize config and directives', () => {
+ const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial', layout: 'dagre' };
+ const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
+ const directive2: MermaidConfig = { theme: 'forest' };
+
+ configApi.saveConfigFromInitialize(initConfig);
+ configApi.addDirective(directive1);
+ configApi.addDirective(directive2);
+
+ const userConfig = configApi.getUserDefinedConfig();
+ expect(userConfig).toMatchInlineSnapshot(`
+ {
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "layout": "elk",
+ "theme": "forest",
+ }
+ `);
+ });
+
+ it('should handle nested config objects properly', () => {
+ const initConfig: MermaidConfig = {
+ flowchart: { nodeSpacing: 50, rankSpacing: 100 },
+ theme: 'default',
+ };
+ const directive: MermaidConfig = {
+ flowchart: { nodeSpacing: 75, curve: 'basis' },
+ mindmap: { padding: 20 },
+ };
+
+ configApi.saveConfigFromInitialize(initConfig);
+ configApi.addDirective(directive);
+
+ const userConfig = configApi.getUserDefinedConfig();
+ expect(userConfig).toMatchInlineSnapshot(`
+ {
+ "flowchart": {
+ "curve": "basis",
+ "nodeSpacing": 75,
+ "rankSpacing": 100,
+ },
+ "mindmap": {
+ "padding": 20,
+ },
+ "theme": "default",
+ }
+ `);
+ });
+
+ it('should handle complex nested overrides', () => {
+ const initConfig: MermaidConfig = {
+ flowchart: {
+ nodeSpacing: 50,
+ rankSpacing: 100,
+ curve: 'linear',
+ },
+ theme: 'default',
+ };
+ const directive1: MermaidConfig = {
+ flowchart: {
+ nodeSpacing: 75,
+ },
+ fontSize: 12,
+ };
+ const directive2: MermaidConfig = {
+ flowchart: {
+ curve: 'basis',
+ nodeSpacing: 100,
+ },
+ mindmap: {
+ padding: 15,
+ },
+ };
+
+ configApi.saveConfigFromInitialize(initConfig);
+ configApi.addDirective(directive1);
+ configApi.addDirective(directive2);
+
+ const userConfig = configApi.getUserDefinedConfig();
+ expect(userConfig).toMatchInlineSnapshot(`
+ {
+ "flowchart": {
+ "curve": "basis",
+ "nodeSpacing": 100,
+ "rankSpacing": 100,
+ },
+ "fontSize": 12,
+ "mindmap": {
+ "padding": 15,
+ },
+ "theme": "default",
+ }
+ `);
+ });
+
+ it('should return independent copies (not references)', () => {
+ const initConfig: MermaidConfig = { theme: 'dark', flowchart: { nodeSpacing: 50 } };
+ configApi.saveConfigFromInitialize(initConfig);
+
+ const userConfig1 = configApi.getUserDefinedConfig();
+ const userConfig2 = configApi.getUserDefinedConfig();
+
+ userConfig1.theme = 'neutral';
+ userConfig1.flowchart!.nodeSpacing = 999;
+
+ expect(userConfig2).toMatchInlineSnapshot(`
+ {
+ "flowchart": {
+ "nodeSpacing": 50,
+ },
+ "theme": "dark",
+ }
+ `);
+ });
+
+ it('should handle edge cases with undefined values', () => {
+ const initConfig: MermaidConfig = { theme: 'dark', layout: undefined };
+ const directive: MermaidConfig = { fontSize: 14, fontFamily: undefined };
+
+ configApi.saveConfigFromInitialize(initConfig);
+ configApi.addDirective(directive);
+
+ expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
+ {
+ "fontSize": 14,
+ "layout": undefined,
+ "theme": "dark",
+ }
+ `);
+ });
+
+ it('should retain config from initialize after reset', () => {
+ const initConfig: MermaidConfig = { theme: 'dark' };
+ const directive: MermaidConfig = { layout: 'elk' };
+
+ configApi.saveConfigFromInitialize(initConfig);
+ configApi.addDirective(directive);
+
+ expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
+ {
+ "layout": "elk",
+ "theme": "dark",
+ }
+ `);
+
+ configApi.reset();
+ });
+});
diff --git a/packages/mermaid/src/config.ts b/packages/mermaid/src/config.ts
index 9468a3e46..4fcb3224d 100644
--- a/packages/mermaid/src/config.ts
+++ b/packages/mermaid/src/config.ts
@@ -248,3 +248,17 @@ const checkConfig = (config: MermaidConfig) => {
issueWarning('LAZY_LOAD_DEPRECATED');
}
};
+
+export const getUserDefinedConfig = (): MermaidConfig => {
+ let userConfig: MermaidConfig = {};
+
+ if (configFromInitialize) {
+ userConfig = assignWithDepth(userConfig, configFromInitialize);
+ }
+
+ for (const d of directives) {
+ userConfig = assignWithDepth(userConfig, d);
+ }
+
+ return userConfig;
+};
diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts
index 7c88f2e41..aa5e514e0 100644
--- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts
+++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts
@@ -1070,6 +1070,14 @@ describe('given a class diagram with members and methods ', function () {
parser.parse(str);
});
+ it('should handle an empty class body with {}', function () {
+ const str = 'classDiagram\nclass EmptyClass {}';
+ parser.parse(str);
+ const actual = parser.yy.getClass('EmptyClass');
+ expect(actual.label).toBe('EmptyClass');
+ expect(actual.members.length).toBe(0);
+ expect(actual.methods.length).toBe(0);
+ });
});
});
diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison
index 0f971c8b9..9a1f991a7 100644
--- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison
+++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison
@@ -293,6 +293,7 @@ classStatement
: classIdentifier
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
| classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
+ | classIdentifier STRUCT_START STRUCT_STOP {}
| classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);}
;
@@ -301,8 +302,15 @@ classIdentifier
| CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
;
+
+emptyBody
+ :
+ | SPACE emptyBody
+ | NEWLINE emptyBody
+ ;
+
annotationStatement
- :ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
+ : ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
;
members
diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
index 2dcadc0bb..13e63f3ae 100644
--- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
+++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
@@ -14,7 +14,7 @@
// Special states for recognizing aliases
// A special state for grabbing text up to the first comment/newline
-%x ID ALIAS LINE
+%x ID ALIAS LINE CONFIG CONFIG_DATA
%x acc_title
%x acc_descr
@@ -28,6 +28,11 @@
\%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
[0-9]+(?=[ \n]+) return 'NUM';
+\@\{ { this.begin('CONFIG'); return 'CONFIG_START'; }
+[^\}]+ { return 'CONFIG_CONTENT'; }
+\} { this.popState(); this.popState(); return 'CONFIG_END'; }
+[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
+[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
@@ -231,6 +236,8 @@ participant_statement
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
+ | 'participant' actor_with_config 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $$=$2;}
+
;
note_statement
@@ -301,6 +308,23 @@ signal
{ $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]}
;
+actor_with_config
+ : ACTOR config_object
+ {
+ $$ = {
+ type: 'addParticipant',
+ actor: $1,
+ config: $2
+ };
+ }
+ ;
+
+config_object
+ : CONFIG_START CONFIG_CONTENT CONFIG_END
+ {
+ $$ = $2.trim();
+ }
+ ;
// actor
// : actor_participant
// | actor_actor
@@ -313,7 +337,7 @@ 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; }
+ | 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 c6b44dac0..67ae19de5 100644
--- a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts
+++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts
@@ -1,4 +1,5 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
+import * as yaml from 'js-yaml';
import type { DiagramDB } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { ImperativeState } from '../../utils/imperativeState.js';
@@ -13,6 +14,7 @@ import {
setDiagramTitle,
} from '../common/commonDb.js';
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
+import type { ParticipantMetaData } from '../../types.js';
interface SequenceState {
prevActor?: string;
@@ -75,6 +77,17 @@ const PLACEMENT = {
OVER: 2,
} as const;
+export const PARTICIPANT_TYPE = {
+ ACTOR: 'actor',
+ BOUNDARY: 'boundary',
+ COLLECTIONS: 'collections',
+ CONTROL: 'control',
+ DATABASE: 'database',
+ ENTITY: 'entity',
+ PARTICIPANT: 'participant',
+ QUEUE: 'queue',
+} as const;
+
export class SequenceDB implements DiagramDB {
private readonly state = new ImperativeState(() => ({
prevActor: undefined,
@@ -119,9 +132,22 @@ export class SequenceDB implements DiagramDB {
id: string,
name: string,
description: { text: string; wrap?: boolean | null; type: string },
- type: string
+ type: string,
+ metadata?: any
) {
let assignedBox = this.state.records.currentBox;
+ let doc;
+ if (metadata !== undefined) {
+ let yamlData;
+ // detect if shapeData contains a newline character
+ if (!metadata.includes('\n')) {
+ yamlData = '{\n' + metadata + '\n}';
+ } else {
+ yamlData = metadata + '\n';
+ }
+ doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as ParticipantMetaData;
+ }
+ type = doc?.type ?? type;
const old = this.state.records.actors.get(id);
if (old) {
// If already set and trying to set to a new one throw error
@@ -518,7 +544,7 @@ export class SequenceDB implements DiagramDB {
});
break;
case 'addParticipant':
- this.addActor(param.actor, param.actor, param.description, param.draw);
+ this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
break;
case 'createParticipant':
if (this.state.records.actors.has(param.actor)) {
@@ -527,7 +553,7 @@ export class SequenceDB implements DiagramDB {
);
}
this.state.records.lastCreated = param.actor;
- this.addActor(param.actor, param.actor, param.description, param.draw);
+ this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
this.state.records.createdActors.set(param.actor, this.state.records.messages.length);
break;
case 'destroyParticipant':
diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js
index 97833be8f..f01ff9ec7 100644
--- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js
+++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js
@@ -2058,4 +2058,272 @@ Bob->>Alice:Got it!
expect(messages[0].from).toBe('Alice');
expect(messages[0].to).toBe('Bob');
});
+ describe('when parsing extended participant syntax', () => {
+ it('should parse participants with different quote styles and whitespace', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant Alice@{ "type" : "database" }
+ participant Bob@{ "type" : "database" }
+ participant Carl@{ type: "database" }
+ participant David@{ "type" : 'database' }
+ participant Eve@{ type: 'database' }
+ participant Favela@{ "type" : "database" }
+ Bob->>+Alice: Hi Alice
+ Alice->>+Bob: Hi Bob
+ `);
+
+ const actors = diagram.db.getActors();
+
+ expect(actors.get('Alice').type).toBe('database');
+ expect(actors.get('Alice').description).toBe('Alice');
+
+ expect(actors.get('Bob').type).toBe('database');
+ expect(actors.get('Bob').description).toBe('Bob');
+
+ expect(actors.get('Carl').type).toBe('database');
+ expect(actors.get('Carl').description).toBe('Carl');
+
+ expect(actors.get('David').type).toBe('database');
+ expect(actors.get('David').description).toBe('David');
+
+ expect(actors.get('Eve').type).toBe('database');
+ expect(actors.get('Eve').description).toBe('Eve');
+
+ expect(actors.get('Favela').type).toBe('database');
+ expect(actors.get('Favela').description).toBe('Favela');
+
+ // Verify messages were parsed correctly
+ const messages = diagram.db.getMessages();
+ expect(messages.length).toBe(4); // 2 messages + 2 activation messages
+ expect(messages[0].from).toBe('Bob');
+ expect(messages[0].to).toBe('Alice');
+ expect(messages[0].message).toBe('Hi Alice');
+ expect(messages[2].from).toBe('Alice'); // Second message (index 2 due to activation)
+ expect(messages[2].to).toBe('Bob');
+ expect(messages[2].message).toBe('Hi Bob');
+ });
+
+ it('should parse mixed participant types with extended syntax', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant lead
+ participant dsa@{ "type" : "queue" }
+ API->>+Database: getUserb
+ Database-->>-API: userb
+ dsa --> Database: hello
+`);
+
+ // Verify actors were created
+ const actors = diagram.db.getActors();
+
+ expect(actors.get('lead').type).toBe('participant');
+ expect(actors.get('lead').description).toBe('lead');
+
+ // Participant with extended syntax
+ expect(actors.get('dsa').type).toBe('queue');
+ expect(actors.get('dsa').description).toBe('dsa');
+
+ // Implicitly created actors (from messages)
+ expect(actors.get('API').type).toBe('participant');
+ expect(actors.get('API').description).toBe('API');
+
+ expect(actors.get('Database').type).toBe('participant');
+ expect(actors.get('Database').description).toBe('Database');
+
+ // Verify messages were parsed correctly
+ const messages = diagram.db.getMessages();
+ expect(messages.length).toBe(5); // 3 messages + 2 activation messages
+
+ // First message with activation
+ expect(messages[0].from).toBe('API');
+ expect(messages[0].to).toBe('Database');
+ expect(messages[0].message).toBe('getUserb');
+ expect(messages[0].activate).toBe(true);
+
+ // Second message with deactivation
+ expect(messages[2].from).toBe('Database');
+ expect(messages[2].to).toBe('API');
+ expect(messages[2].message).toBe('userb');
+
+ // Third message
+ expect(messages[4].from).toBe('dsa');
+ expect(messages[4].to).toBe('Database');
+ expect(messages[4].message).toBe('hello');
+ });
+
+ it('should fail for malformed JSON in participant definition', async () => {
+ const invalidDiagram = `
+ sequenceDiagram
+ participant D@{ "type: "entity" }
+ participant E@{ "type": "dat
+ abase }
+ `;
+
+ let error = false;
+ try {
+ await mermaidAPI.parse(invalidDiagram);
+ } catch (e) {
+ error = true;
+ }
+ expect(error).toBe(true);
+ });
+
+ it('should fail for missing colon separator', async () => {
+ const invalidDiagram = `
+ sequenceDiagram
+ participant C@{ "type" "control" }
+ C ->> C: action
+ `;
+
+ let error = false;
+ try {
+ await mermaidAPI.parse(invalidDiagram);
+ } catch (e) {
+ error = true;
+ }
+ expect(error).toBe(true);
+ });
+
+ it('should fail for missing closing brace', async () => {
+ const invalidDiagram = `
+ sequenceDiagram
+ participant E@{ "type": "entity"
+ E ->> E: process
+ `;
+
+ let error = false;
+ try {
+ await mermaidAPI.parse(invalidDiagram);
+ } catch (e) {
+ error = true;
+ }
+ expect(error).toBe(true);
+ });
+ });
+ describe('participant type parsing', () => {
+ it('should parse boundary participant', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant boundary@{ "type" : "boundary" }
+ boundary->boundary: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('boundary').type).toBe('boundary');
+ expect(actors.get('boundary').description).toBe('boundary');
+ });
+
+ it('should parse control participant', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant C@{ "type" : "control" }
+ C->C: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('C').type).toBe('control');
+ expect(actors.get('C').description).toBe('C');
+ });
+
+ it('should parse entity participant', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant E@{ "type" : "entity" }
+ E->E: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('E').type).toBe('entity');
+ expect(actors.get('E').description).toBe('E');
+ });
+
+ it('should parse database participant', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant D@{ "type" : "database" }
+ D->D: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('D').type).toBe('database');
+ expect(actors.get('D').description).toBe('D');
+ });
+
+ it('should parse collections participant', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant L@{ "type" : "collections" }
+ L->L: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('L').type).toBe('collections');
+ expect(actors.get('L').description).toBe('L');
+ });
+
+ it('should parse queue participant', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant Q@{ "type" : "queue" }
+ Q->Q: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('Q').type).toBe('queue');
+ expect(actors.get('Q').description).toBe('Q');
+ });
+ });
+
+ describe('participant type parsing', () => {
+ it('should parse actor participant', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant A@{ "type" : "queue" }
+ A->A: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('A').type).toBe('queue');
+ expect(actors.get('A').description).toBe('A');
+ });
+
+ it('should parse participant participant', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant P@{ "type" : "database" }
+ P->P: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('P').type).toBe('database');
+ expect(actors.get('P').description).toBe('P');
+ });
+
+ it('should parse boundary using actor keyword', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant Alice@{ "type" : "collections" }
+ participant Bob@{ "type" : "control" }
+ Alice->>Bob: Hello Bob, how are you?
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('Alice').type).toBe('collections');
+ expect(actors.get('Bob').type).toBe('control');
+ expect(actors.get('Bob').description).toBe('Bob');
+ });
+
+ it('should parse control using participant keyword', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant C@{ "type" : "control" }
+ C->C: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('C').type).toBe('control');
+ expect(actors.get('C').description).toBe('C');
+ });
+
+ it('should parse entity using actor keyword', async () => {
+ const diagram = await Diagram.fromText(`
+ sequenceDiagram
+ participant E@{ "type" : "entity" }
+ E->E: test
+ `);
+ const actors = diagram.db.getActors();
+ expect(actors.get('E').type).toBe('entity');
+ expect(actors.get('E').description).toBe('E');
+ });
+ });
});
diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
index cfba92b79..5fac3cf2d 100644
--- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
+++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
@@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth.js';
import utils from '../../utils.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { Diagram } from '../../Diagram.js';
+import { PARTICIPANT_TYPE } from './sequenceDb.js';
let conf = {};
@@ -476,7 +477,29 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
// add node number
if (sequenceVisible || conf.showSequenceNumbers) {
- line.attr('marker-start', 'url(' + url + '#sequencenumber)');
+ const isBidirectional =
+ type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID ||
+ type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED;
+
+ if (isBidirectional) {
+ const SEQUENCE_NUMBER_RADIUS = 6;
+
+ if (startx < stopx) {
+ line.attr('x1', startx + 2 * SEQUENCE_NUMBER_RADIUS);
+ } else {
+ line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS);
+ }
+ }
+
+ diagram
+ .append('line')
+ .attr('x1', startx)
+ .attr('y1', lineStartY)
+ .attr('x2', startx)
+ .attr('y2', lineStartY)
+ .attr('stroke-width', 0)
+ .attr('marker-start', 'url(' + url + '#sequencenumber)');
+
diagram
.append('text')
.attr('x', startx)
@@ -724,11 +747,19 @@ function adjustCreatedDestroyedData(
msgModel.startx = msgModel.startx - adjustment;
}
}
+ const actorArray = [
+ PARTICIPANT_TYPE.ACTOR,
+ PARTICIPANT_TYPE.CONTROL,
+ PARTICIPANT_TYPE.ENTITY,
+ PARTICIPANT_TYPE.DATABASE,
+ ];
// if it is a create message
if (createdActors.get(msg.to) == index) {
const actor = actors.get(msg.to);
- const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
+ const adjustment = actorArray.includes(actor.type)
+ ? ACTOR_TYPE_WIDTH / 2 + 3
+ : actor.width / 2 + 3;
receiverAdjustment(actor, adjustment);
actor.starty = lineStartY - actor.height / 2;
bounds.bumpVerticalPos(actor.height / 2);
@@ -737,7 +768,7 @@ function adjustCreatedDestroyedData(
else if (destroyedActors.get(msg.from) == index) {
const actor = actors.get(msg.from);
if (conf.mirrorActors) {
- const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
+ const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
senderAdjustment(actor, adjustment);
}
actor.stopy = lineStartY - actor.height / 2;
@@ -747,7 +778,9 @@ function adjustCreatedDestroyedData(
else if (destroyedActors.get(msg.to) == index) {
const actor = actors.get(msg.to);
if (conf.mirrorActors) {
- const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
+ const adjustment = actorArray.includes(actor.type)
+ ? ACTOR_TYPE_WIDTH / 2 + 3
+ : actor.width / 2 + 3;
receiverAdjustment(actor, adjustment);
}
actor.stopy = lineStartY - actor.height / 2;
@@ -1065,10 +1098,11 @@ export const draw = async function (_text: string, id: string, _version: string,
for (const box of bounds.models.boxes) {
box.height = bounds.getVerticalPos() - box.y;
bounds.insert(box.x, box.y, box.x + box.width, box.height);
- box.startx = box.x;
- box.starty = box.y;
- box.stopx = box.startx + box.width;
- box.stopy = box.starty + box.height;
+ const boxPadding = conf.boxMargin * 2;
+ box.startx = box.x - boxPadding;
+ box.starty = box.y - boxPadding * 0.25;
+ box.stopx = box.startx + box.width + 2 * boxPadding;
+ box.stopy = box.starty + box.height + boxPadding * 0.75;
box.stroke = 'rgb(0,0,0, 0.5)';
svgDraw.drawBox(diagram, box, conf);
}
@@ -1333,6 +1367,9 @@ async function calculateActorMargins(
return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0));
}, 0);
+ const standardBoxPadding = conf.boxMargin * 8;
+ totalWidth += standardBoxPadding;
+
totalWidth -= 2 * conf.boxTextMargin;
if (box.wrap) {
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
diff --git a/packages/mermaid/src/diagrams/sequence/styles.js b/packages/mermaid/src/diagrams/sequence/styles.js
index 5c36b4ed1..3cee9d3dc 100644
--- a/packages/mermaid/src/diagrams/sequence/styles.js
+++ b/packages/mermaid/src/diagrams/sequence/styles.js
@@ -12,6 +12,11 @@ const getStyles = (options) =>
.actor-line {
stroke: ${options.actorLineColor};
}
+
+ .innerArc {
+ stroke-width: 1.5;
+ stroke-dasharray: none;
+ }
.messageLine0 {
stroke-width: 1.5;
@@ -115,6 +120,7 @@ const getStyles = (options) =>
fill: ${options.actorBkg};
stroke-width: 2px;
}
+
`;
export default getStyles;
diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js
index 18fd2d034..1971082a8 100644
--- a/packages/mermaid/src/diagrams/sequence/svgDraw.js
+++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js
@@ -415,6 +415,600 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
return height;
};
+/**
+ * Draws an actor in the diagram with the attached line
+ *
+ * @param {any} elem - The diagram we'll draw to.
+ * @param {any} actor - The actor to draw.
+ * @param {any} conf - DrawText implementation discriminator object
+ * @param {boolean} isFooter - If the actor is the footer one
+ */
+const drawActorTypeCollections = function (elem, actor, conf, isFooter) {
+ const actorY = isFooter ? actor.stopy : actor.starty;
+ const center = actor.x + actor.width / 2;
+ const centerY = actorY + actor.height;
+
+ const boxplusLineGroup = elem.append('g').lower();
+ var g = boxplusLineGroup;
+
+ if (!isFooter) {
+ actorCnt++;
+ if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
+ g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
+ }
+ g.append('line')
+ .attr('id', 'actor' + actorCnt)
+ .attr('x1', center)
+ .attr('y1', centerY)
+ .attr('x2', center)
+ .attr('y2', 2000)
+ .attr('class', 'actor-line 200')
+ .attr('stroke-width', '0.5px')
+ .attr('stroke', '#999')
+ .attr('name', actor.name);
+
+ g = boxplusLineGroup.append('g');
+ actor.actorCnt = actorCnt;
+
+ if (actor.links != null) {
+ g.attr('id', 'root-' + actorCnt);
+ }
+ }
+
+ const rect = svgDrawCommon.getNoteRect();
+ var cssclass = 'actor';
+ if (actor.properties?.class) {
+ cssclass = actor.properties.class;
+ } else {
+ rect.fill = '#eaeaea';
+ }
+ if (isFooter) {
+ cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
+ } else {
+ cssclass += ` ${TOP_ACTOR_CLASS}`;
+ }
+ rect.x = actor.x;
+ rect.y = actorY;
+ rect.width = actor.width;
+ rect.height = actor.height;
+ rect.class = cssclass;
+ rect.name = actor.name;
+
+ // DRAW STACKED RECTANGLES
+ const offset = 6;
+ const shadowRect = {
+ ...rect,
+ x: rect.x + (isFooter ? -offset : -offset),
+ y: rect.y + (isFooter ? +offset : +offset),
+ class: 'actor',
+ };
+ const rectElem = drawRect(g, rect); // draw main rectangle on top
+ drawRect(g, shadowRect);
+ actor.rectData = rect;
+
+ if (actor.properties?.icon) {
+ const iconSrc = actor.properties.icon.trim();
+ if (iconSrc.charAt(0) === '@') {
+ svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
+ } else {
+ svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
+ }
+ }
+
+ _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+ actor.description,
+ g,
+ rect.x - offset,
+ rect.y + offset,
+ rect.width,
+ rect.height,
+ { class: `actor ${ACTOR_BOX_CLASS}` },
+ conf
+ );
+
+ let height = actor.height;
+ if (rectElem.node) {
+ const bounds = rectElem.node().getBBox();
+ actor.height = bounds.height;
+ height = bounds.height;
+ }
+
+ return height;
+};
+
+const drawActorTypeQueue = function (elem, actor, conf, isFooter) {
+ const actorY = isFooter ? actor.stopy : actor.starty;
+ const center = actor.x + actor.width / 2;
+ const centerY = actorY + actor.height;
+
+ const boxplusLineGroup = elem.append('g').lower();
+ let g = boxplusLineGroup;
+
+ if (!isFooter) {
+ actorCnt++;
+ if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
+ g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
+ }
+ g.append('line')
+ .attr('id', 'actor' + actorCnt)
+ .attr('x1', center)
+ .attr('y1', centerY)
+ .attr('x2', center)
+ .attr('y2', 2000)
+ .attr('class', 'actor-line 200')
+ .attr('stroke-width', '0.5px')
+ .attr('stroke', '#999')
+ .attr('name', actor.name);
+
+ g = boxplusLineGroup.append('g');
+ actor.actorCnt = actorCnt;
+
+ if (actor.links != null) {
+ g.attr('id', 'root-' + actorCnt);
+ }
+ }
+
+ const rect = svgDrawCommon.getNoteRect();
+ let cssclass = 'actor';
+ if (actor.properties?.class) {
+ cssclass = actor.properties.class;
+ } else {
+ rect.fill = '#eaeaea';
+ }
+
+ if (isFooter) {
+ cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
+ } else {
+ cssclass += ` ${TOP_ACTOR_CLASS}`;
+ }
+
+ rect.x = actor.x;
+ rect.y = actorY;
+ rect.width = actor.width;
+ rect.height = actor.height;
+ rect.class = cssclass;
+ rect.name = actor.name;
+
+ // Cylinder dimensions
+ const ry = rect.height / 2;
+ const rx = ry / (2.5 + rect.height / 50);
+
+ // Cylinder base group
+ const cylinderGroup = g.append('g');
+ const cylinderArc = g.append('g');
+
+ // Main cylinder body
+ cylinderGroup
+ .append('path')
+ .attr(
+ 'd',
+ `M ${rect.x},${rect.y + ry}
+ a ${rx},${ry} 0 0 0 0,${rect.height}
+ h ${rect.width - 2 * rx}
+ a ${rx},${ry} 0 0 0 0,-${rect.height}
+ Z
+ `
+ )
+ .attr('class', cssclass);
+ cylinderArc
+ .append('path')
+ .attr(
+ 'd',
+ `M ${rect.x},${rect.y + ry}
+ a ${rx},${ry} 0 0 0 0,${rect.height}`
+ )
+ .attr('stroke', '#666')
+ .attr('stroke-width', '1px')
+ .attr('class', cssclass);
+
+ cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`);
+ cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`);
+
+ actor.rectData = rect;
+
+ if (actor.properties?.icon) {
+ const iconSrc = actor.properties.icon.trim();
+ const iconX = rect.x + rect.width - 20;
+ const iconY = rect.y + 10;
+ if (iconSrc.charAt(0) === '@') {
+ svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1));
+ } else {
+ svgDrawCommon.drawImage(g, iconX, iconY, iconSrc);
+ }
+ }
+
+ _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+ actor.description,
+ g,
+ rect.x,
+ rect.y,
+ rect.width,
+ rect.height,
+ { class: `actor ${ACTOR_BOX_CLASS}` },
+ conf
+ );
+
+ let height = actor.height;
+ const lastPath = cylinderGroup.select('path:last-child');
+ if (lastPath.node()) {
+ const bounds = lastPath.node().getBBox();
+ actor.height = bounds.height;
+ height = bounds.height;
+ }
+
+ return height;
+};
+
+const drawActorTypeControl = function (elem, actor, conf, isFooter) {
+ const actorY = isFooter ? actor.stopy : actor.starty;
+ const center = actor.x + actor.width / 2;
+ const centerY = actorY + 75;
+
+ const line = elem.append('g').lower();
+
+ if (!isFooter) {
+ actorCnt++;
+ line
+ .append('line')
+ .attr('id', 'actor' + actorCnt)
+ .attr('x1', center)
+ .attr('y1', centerY)
+ .attr('x2', center)
+ .attr('y2', 2000)
+ .attr('class', 'actor-line 200')
+ .attr('stroke-width', '0.5px')
+ .attr('stroke', '#999')
+ .attr('name', actor.name);
+
+ actor.actorCnt = actorCnt;
+ }
+ const actElem = elem.append('g');
+ let cssClass = ACTOR_MAN_FIGURE_CLASS;
+ if (isFooter) {
+ cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
+ } else {
+ cssClass += ` ${TOP_ACTOR_CLASS}`;
+ }
+ actElem.attr('class', cssClass);
+ actElem.attr('name', actor.name);
+
+ const rect = svgDrawCommon.getNoteRect();
+ rect.x = actor.x;
+ rect.y = actorY;
+ rect.fill = '#eaeaea';
+ rect.width = actor.width;
+ rect.height = actor.height;
+ rect.class = 'actor';
+
+ const cx = actor.x + actor.width / 2;
+ const cy = actorY + 30;
+ const r = 18;
+
+ actElem
+ .append('defs')
+ .append('marker')
+ .attr('id', 'filled-head-control')
+ .attr('refX', 11)
+ .attr('refY', 5.8)
+ .attr('markerWidth', 20)
+ .attr('markerHeight', 28)
+ .attr('orient', '172.5')
+ .append('path')
+ .attr('d', 'M 14.4 5.6 L 7.2 10.4 L 8.8 5.6 L 7.2 0.8 Z');
+
+ // Draw the base circle
+ actElem
+ .append('circle')
+ .attr('cx', cx)
+ .attr('cy', cy)
+ .attr('r', r)
+ .attr('fill', '#eaeaf7')
+ .attr('stroke', '#666')
+ .attr('stroke-width', 1.2);
+
+ // Draw looping arrow as arc path
+ actElem
+ .append('line')
+ .attr('marker-end', 'url(#filled-head-control)')
+ .attr('transform', `translate(${cx}, ${cy - r})`);
+
+ const bounds = actElem.node().getBBox();
+ actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0);
+
+ _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+ actor.description,
+ actElem,
+ rect.x,
+ rect.y + r + (isFooter ? 5 : 10),
+ rect.width,
+ rect.height,
+ { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
+ conf
+ );
+
+ return actor.height;
+};
+
+const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
+ const actorY = isFooter ? actor.stopy : actor.starty;
+ const center = actor.x + actor.width / 2;
+ const centerY = actorY + 75;
+
+ const line = elem.append('g').lower();
+
+ const actElem = elem.append('g');
+ let cssClass = ACTOR_MAN_FIGURE_CLASS;
+ if (isFooter) {
+ cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
+ } else {
+ cssClass += ` ${TOP_ACTOR_CLASS}`;
+ }
+ actElem.attr('class', cssClass);
+ actElem.attr('name', actor.name);
+
+ const rect = svgDrawCommon.getNoteRect();
+ rect.x = actor.x;
+ rect.y = actorY;
+ rect.fill = '#eaeaea';
+ rect.width = actor.width;
+ rect.height = actor.height;
+ rect.class = 'actor';
+
+ const cx = actor.x + actor.width / 2;
+ const cy = actorY + (!isFooter ? 25 : 10);
+ const r = 18;
+
+ actElem
+ .append('circle')
+ .attr('cx', cx)
+ .attr('cy', cy)
+ .attr('r', r)
+ .attr('width', actor.width)
+ .attr('height', actor.height);
+
+ actElem
+ .append('line')
+ .attr('x1', cx - r)
+ .attr('x2', cx + r)
+ .attr('y1', cy + r)
+ .attr('y2', cy + r)
+ .attr('stroke', '#333')
+ .attr('stroke-width', 2);
+
+ const bounds = actElem.node().getBBox();
+ actor.height = bounds.height + (conf?.sequence?.labelBoxHeight ?? 0);
+
+ if (!isFooter) {
+ actorCnt++;
+ line
+ .append('line')
+ .attr('id', 'actor' + actorCnt)
+ .attr('x1', center)
+ .attr('y1', centerY)
+ .attr('x2', center)
+ .attr('y2', 2000)
+ .attr('class', 'actor-line 200')
+ .attr('stroke-width', '0.5px')
+ .attr('stroke', '#999')
+ .attr('name', actor.name);
+
+ actor.actorCnt = actorCnt;
+ }
+
+ _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+ actor.description,
+ actElem,
+ rect.x,
+ rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY + r - 5) / 2),
+ rect.width,
+ rect.height,
+ { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
+ conf
+ );
+
+ if (!isFooter) {
+ actElem.attr('transform', `translate(${0}, ${r / 2})`);
+ } else {
+ actElem.attr('transform', `translate(${0}, ${r / 2})`);
+ }
+
+ return actor.height;
+};
+
+const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
+ const actorY = isFooter ? actor.stopy : actor.starty;
+ const center = actor.x + actor.width / 2;
+ const centerY = actorY + actor.height + 2 * conf.boxTextMargin;
+
+ const boxplusLineGroup = elem.append('g').lower();
+ let g = boxplusLineGroup;
+
+ if (!isFooter) {
+ actorCnt++;
+ if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
+ g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
+ }
+ g.append('line')
+ .attr('id', 'actor' + actorCnt)
+ .attr('x1', center)
+ .attr('y1', centerY)
+ .attr('x2', center)
+ .attr('y2', 2000)
+ .attr('class', 'actor-line 200')
+ .attr('stroke-width', '0.5px')
+ .attr('stroke', '#999')
+ .attr('name', actor.name);
+
+ g = boxplusLineGroup.append('g');
+ actor.actorCnt = actorCnt;
+
+ if (actor.links != null) {
+ g.attr('id', 'root-' + actorCnt);
+ }
+ }
+
+ const rect = svgDrawCommon.getNoteRect();
+
+ let cssclass = 'actor';
+ if (actor.properties?.class) {
+ cssclass = actor.properties.class;
+ } else {
+ rect.fill = '#eaeaea';
+ }
+
+ if (isFooter) {
+ cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
+ } else {
+ cssclass += ` ${TOP_ACTOR_CLASS}`;
+ }
+
+ rect.x = actor.x;
+ rect.y = actorY;
+ rect.width = actor.width;
+ rect.height = actor.height;
+ rect.class = cssclass;
+ rect.name = actor.name;
+
+ // Cylinder dimensions
+ rect.x = actor.x;
+ rect.y = actorY;
+ const w = rect.width / 4;
+ const h = rect.width / 4;
+ const rx = w / 2;
+ const ry = rx / (2.5 + w / 50);
+
+ // Cylinder base group
+ const cylinderGroup = g.append('g');
+
+ const d = `
+ M ${rect.x},${rect.y + ry}
+ a ${rx},${ry} 0 0 0 ${w},0
+ a ${rx},${ry} 0 0 0 -${w},0
+ l 0,${h - 2 * ry}
+ a ${rx},${ry} 0 0 0 ${w},0
+ l 0,-${h - 2 * ry}
+`;
+ // Draw the main cylinder body
+ cylinderGroup
+ .append('path')
+ .attr('d', d)
+ .attr('fill', '#eaeaea')
+ .attr('stroke', '#000')
+ .attr('stroke-width', 1)
+ .attr('class', cssclass);
+
+ if (!isFooter) {
+ cylinderGroup.attr('transform', `translate(${w * 1.5}, ${(rect.height + ry) / 4})`);
+ } else {
+ cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
+ }
+ actor.rectData = rect;
+ _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+ actor.description,
+ g,
+ rect.x,
+ rect.y + (!isFooter ? (rect.height + ry) / 2 : (rect.height + h) / 4),
+ rect.width,
+ rect.height,
+ { class: `actor ${ACTOR_BOX_CLASS}` },
+ conf
+ );
+
+ const lastPath = cylinderGroup.select('path:last-child');
+ if (lastPath.node()) {
+ const bounds = lastPath.node().getBBox();
+ actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
+ }
+
+ return actor.height;
+};
+
+const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
+ const actorY = isFooter ? actor.stopy : actor.starty;
+ const center = actor.x + actor.width / 2;
+ const centerY = actorY + 80;
+ const radius = 30;
+ const line = elem.append('g').lower();
+
+ if (!isFooter) {
+ actorCnt++;
+ line
+ .append('line')
+ .attr('id', 'actor' + actorCnt)
+ .attr('x1', center)
+ .attr('y1', centerY)
+ .attr('x2', center)
+ .attr('y2', 2000)
+ .attr('class', 'actor-line 200')
+ .attr('stroke-width', '0.5px')
+ .attr('stroke', '#999')
+ .attr('name', actor.name);
+
+ actor.actorCnt = actorCnt;
+ }
+ const actElem = elem.append('g');
+ let cssClass = ACTOR_MAN_FIGURE_CLASS;
+ if (isFooter) {
+ cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
+ } else {
+ cssClass += ` ${TOP_ACTOR_CLASS}`;
+ }
+ actElem.attr('class', cssClass);
+ actElem.attr('name', actor.name);
+
+ const rect = svgDrawCommon.getNoteRect();
+ rect.x = actor.x;
+ rect.y = actorY;
+ rect.fill = '#eaeaea';
+ rect.width = actor.width;
+ rect.height = actor.height;
+ rect.class = 'actor';
+
+ actElem
+ .append('line')
+ .attr('id', 'actor-man-torso' + actorCnt)
+ .attr('x1', actor.x + actor.width / 2 - radius * 2.5)
+ .attr('y1', actorY + 10)
+ .attr('x2', actor.x + actor.width / 2 - 15)
+ .attr('y2', actorY + 10);
+
+ actElem
+ .append('line')
+ .attr('id', 'actor-man-arms' + actorCnt)
+ .attr('x1', actor.x + actor.width / 2 - radius * 2.5)
+ .attr('y1', actorY + 0) // starting Y
+ .attr('x2', actor.x + actor.width / 2 - radius * 2.5)
+ .attr('y2', actorY + 20); // ending Y (26px long, adjust as needed)
+
+ actElem
+ .append('circle')
+ .attr('cx', actor.x + actor.width / 2)
+ .attr('cy', actorY + 10)
+ .attr('r', radius);
+
+ const bounds = actElem.node().getBBox();
+ actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
+
+ _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+ actor.description,
+ actElem,
+ rect.x,
+ rect.y + (!isFooter ? radius / 2 + 3 : radius / 2 - 4),
+ rect.width,
+ rect.height,
+ { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
+ conf
+ );
+
+ if (!isFooter) {
+ actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
+ } else {
+ actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
+ }
+
+ return actor.height;
+};
+
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
@@ -516,6 +1110,18 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
return await drawActorTypeActor(elem, actor, conf, isFooter);
case 'participant':
return await drawActorTypeParticipant(elem, actor, conf, isFooter);
+ case 'boundary':
+ return await drawActorTypeBoundary(elem, actor, conf, isFooter);
+ case 'control':
+ return await drawActorTypeControl(elem, actor, conf, isFooter);
+ case 'entity':
+ return await drawActorTypeEntity(elem, actor, conf, isFooter);
+ case 'database':
+ return await drawActorTypeDatabase(elem, actor, conf, isFooter);
+ case 'collections':
+ return await drawActorTypeCollections(elem, actor, conf, isFooter);
+ case 'queue':
+ return await drawActorTypeQueue(elem, actor, conf, isFooter);
}
};
diff --git a/packages/mermaid/src/docs/config/faq.md b/packages/mermaid/src/docs/config/faq.md
index 6d1261fc1..4acf0c3d3 100644
--- a/packages/mermaid/src/docs/config/faq.md
+++ b/packages/mermaid/src/docs/config/faq.md
@@ -1,6 +1,6 @@
# Frequently Asked Questions
-1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
+1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
1. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
1. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
1. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
diff --git a/packages/mermaid/src/docs/package.json b/packages/mermaid/src/docs/package.json
index 6ec43eb93..b5b95e7c0 100644
--- a/packages/mermaid/src/docs/package.json
+++ b/packages/mermaid/src/docs/package.json
@@ -31,7 +31,7 @@
"fast-glob": "^3.3.3",
"https-localhost": "^4.7.1",
"pathe": "^2.0.3",
- "unocss": "^66.0.0",
+ "unocss": "^66.4.2",
"unplugin-vue-components": "^28.4.0",
"vite": "^6.1.1",
"vite-plugin-pwa": "^1.0.0",
diff --git a/packages/mermaid/src/docs/syntax/flowchart.md b/packages/mermaid/src/docs/syntax/flowchart.md
index a19dcff21..341143c47 100644
--- a/packages/mermaid/src/docs/syntax/flowchart.md
+++ b/packages/mermaid/src/docs/syntax/flowchart.md
@@ -590,11 +590,17 @@ flowchart TD
- `b`
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
-- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
+- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
- `on`
- `off`
-These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
+If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
+
+```mermaid
+flowchart TD
+ %% My image with a constrained aspect ratio
+ A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
+```
## Links between nodes
diff --git a/packages/mermaid/src/docs/syntax/sequenceDiagram.md b/packages/mermaid/src/docs/syntax/sequenceDiagram.md
index 3087eb743..6e0ac87bf 100644
--- a/packages/mermaid/src/docs/syntax/sequenceDiagram.md
+++ b/packages/mermaid/src/docs/syntax/sequenceDiagram.md
@@ -46,6 +46,78 @@ sequenceDiagram
Bob->>Alice: Hi Alice
```
+### Boundary
+
+If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "boundary" }
+ participant Bob
+ Alice->>Bob: Request from boundary
+ Bob->>Alice: Response to boundary
+```
+
+### Control
+
+If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "control" }
+ participant Bob
+ Alice->>Bob: Control request
+ Bob->>Alice: Control response
+```
+
+### Entity
+
+If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "entity" }
+ participant Bob
+ Alice->>Bob: Entity request
+ Bob->>Alice: Entity response
+```
+
+### Database
+
+If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "database" }
+ participant Bob
+ Alice->>Bob: DB query
+ Bob->>Alice: DB result
+```
+
+### Collections
+
+If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "collections" }
+ participant Bob
+ Alice->>Bob: Collections request
+ Bob->>Alice: Collections response
+```
+
+### Queue
+
+If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
+
+```mermaid-example
+sequenceDiagram
+ participant Alice@{ "type" : "queue" }
+ participant Bob
+ Alice->>Bob: Queue message
+ Bob->>Alice: Queue response
+```
+
### Aliases
The actor can have a convenient identifier and a descriptive label.
diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts
index b2d2d3cd3..ff794abb1 100644
--- a/packages/mermaid/src/mermaidAPI.spec.ts
+++ b/packages/mermaid/src/mermaidAPI.spec.ts
@@ -41,7 +41,6 @@ import { decodeEntities, encodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js';
import { StateDB } from './diagrams/state/stateDb.js';
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
-import { select } from 'd3';
import { JSDOM } from 'jsdom';
/**
@@ -50,7 +49,6 @@ import { JSDOM } from 'jsdom';
*/
// -------------------------------------------------------------------------------------
-
describe('mermaidAPI', () => {
describe('encodeEntities', () => {
it('removes the ending ; from style [text1]:[optional word]#[text2]; with ', () => {
@@ -913,4 +911,241 @@ graph TD;A--x|text including URL space|B;`)
expect(sequenceDiagram1.db.getActors()).not.toEqual(sequenceDiagram2.db.getActors());
});
});
+
+ describe('mermaidAPI config precedence', () => {
+ const id = 'mermaid-config-test';
+
+ beforeEach(() => {
+ mermaidAPI.globalReset();
+ });
+
+ jsdomIt('renders with YAML config taking precedence over initialize config', async () => {
+ mermaid.initialize({
+ theme: 'forest',
+ fontFamily: 'Arial',
+ themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
+ flowchart: { htmlLabels: false },
+ });
+
+ const diagramText = `---
+config:
+ theme: base
+ fontFamily: Courier
+ themeVariables:
+ fontFamily: "Courier New"
+ fontSize: "20px"
+ flowchart:
+ htmlLabels: true
+---
+flowchart TD
+ A --> B
+`;
+
+ const { svg } = await mermaidAPI.render('yaml-over-init', diagramText);
+
+ const config = mermaidAPI.getConfig();
+ expect(config.theme).toBe('base');
+ expect(config.fontFamily).toBe('Courier');
+ expect(config.themeVariables.fontFamily).toBe('Courier New');
+ expect(config.themeVariables.fontSize).toBe('20px');
+ expect(config.flowchart?.htmlLabels).toBe(true);
+
+ const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
+ expect(svgNode).not.toBeNull();
+ });
+
+ jsdomIt(
+ 'renders with YAML themeVariables fully overriding initialize themeVariables',
+ async () => {
+ mermaid.initialize({
+ themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
+ });
+
+ const diagramText = `---
+config:
+ themeVariables:
+ fontFamily: "Courier New"
+ fontSize: "20px"
+---
+flowchart TD
+ A --> B
+`;
+
+ const { svg } = await mermaidAPI.render(id, diagramText);
+ const config = mermaidAPI.getConfig();
+
+ expect(config.themeVariables.fontFamily).toBe('Courier New');
+ expect(config.themeVariables.fontSize).toBe('20px');
+ expect(config.themeVariables.fontFamily).not.toBe('Arial');
+ expect(config.themeVariables.fontSize).not.toBe('16px');
+
+ const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
+ expect(svgNode).not.toBeNull();
+ }
+ );
+
+ jsdomIt(
+ 'renders with YAML themeVariables overriding only provided keys and keeping others from initialize',
+ async () => {
+ mermaid.initialize({
+ theme: 'forest',
+ fontFamily: 'Arial',
+ themeVariables: { fontFamily: 'Arial', fontSize: '16px', colorPrimary: '#ff0000' },
+ });
+
+ const diagramText = `---
+config:
+ themeVariables:
+ fontFamily: "Courier New"
+---
+flowchart TD
+ A --> B
+`;
+
+ const { svg } = await mermaidAPI.render(id, diagramText);
+
+ const config = mermaidAPI.getConfig();
+ expect(config.themeVariables.fontFamily).toBe('Courier New');
+ expect(config.themeVariables.fontSize).toBe('16px');
+ expect(config.themeVariables.colorPrimary).toBe('#ff0000');
+
+ const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
+ expect(svgNode).not.toBeNull();
+ }
+ );
+
+ jsdomIt(
+ 'renders with YAML config (no themeVariables) and falls back to initialize themeVariables',
+ async () => {
+ mermaid.initialize({
+ themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
+ });
+
+ const diagramText = `---
+config:
+ theme: base
+---
+flowchart TD
+ A --> B
+`;
+
+ const { svg } = await mermaidAPI.render(id, diagramText);
+
+ const config = mermaidAPI.getConfig();
+ expect(config.themeVariables.fontFamily).toBe('Arial');
+ expect(config.themeVariables.fontSize).toBe('16px');
+ expect(config.theme).toBe('base');
+
+ const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
+ expect(svgNode).not.toBeNull();
+ }
+ );
+
+ jsdomIt(
+ 'renders with full YAML config block taking full precedence over initialize config',
+ async () => {
+ mermaid.initialize({
+ theme: 'forest',
+ fontFamily: 'Arial',
+ themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
+ flowchart: { htmlLabels: false },
+ });
+
+ const diagramText = `---
+config:
+ theme: base
+ fontFamily: Courier
+ themeVariables:
+ fontFamily: "Courier New"
+ fontSize: "20px"
+ flowchart:
+ htmlLabels: true
+---
+flowchart TD
+ A --> B
+`;
+
+ const { svg } = await mermaidAPI.render('yaml-over-init', diagramText);
+
+ const config = mermaidAPI.getConfig();
+ expect(config.theme).toBe('base');
+ expect(config.fontFamily).toBe('Courier');
+ expect(config.themeVariables.fontFamily).toBe('Courier New');
+ expect(config.themeVariables.fontSize).toBe('20px');
+ expect(config.flowchart?.htmlLabels).toBe(true);
+
+ const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
+ expect(svgNode).not.toBeNull();
+ }
+ );
+
+ jsdomIt(
+ 'renders with YAML config (no themeVariables) and falls back to initialize themeVariables (duplicate scenario)',
+ async () => {
+ mermaid.initialize({
+ themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
+ });
+
+ const diagramText = `---
+config:
+ theme: base
+---
+flowchart TD
+ A --> B
+`;
+
+ await mermaidAPI.render(id, diagramText);
+
+ const config = mermaidAPI.getConfig();
+ expect(config.themeVariables.fontFamily).toBe('Arial');
+ expect(config.themeVariables.fontSize).toBe('16px');
+ expect(config.theme).toBe('base');
+ }
+ );
+
+ jsdomIt('renders with no YAML config so initialize config is fully applied', async () => {
+ mermaid.initialize({
+ theme: 'forest',
+ fontFamily: 'Arial',
+ themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
+ });
+
+ const diagramText = `
+flowchart TD
+ A --> B
+`;
+
+ await mermaidAPI.render(id, diagramText);
+
+ const config = mermaidAPI.getConfig();
+ expect(config.theme).toBe('forest');
+ expect(config.fontFamily).toBe('Arial');
+ expect(config.themeVariables.fontFamily).toBe('Arial');
+ expect(config.themeVariables.fontSize).toBe('16px');
+ });
+
+ jsdomIt(
+ 'renders with empty YAML config block and falls back to initialize config',
+ async () => {
+ mermaid.initialize({
+ theme: 'dark',
+ themeVariables: { fontFamily: 'Times', fontSize: '14px' },
+ });
+
+ const diagramText = `---
+config: {}
+---
+flowchart TD
+ A --> B
+`;
+
+ await mermaidAPI.render(id, diagramText);
+
+ const config = mermaidAPI.getConfig();
+ expect(config.theme).toBe('dark');
+ expect(config.themeVariables.fontFamily).toBe('Times');
+ expect(config.themeVariables.fontSize).toBe('14px');
+ }
+ );
+ });
});
diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts
index d1394e71b..477fb17b1 100644
--- a/packages/mermaid/src/types.ts
+++ b/packages/mermaid/src/types.ts
@@ -13,6 +13,18 @@ export interface NodeMetaData {
ticket?: string;
}
+export interface ParticipantMetaData {
+ type?:
+ | 'actor'
+ | 'participant'
+ | 'boundary'
+ | 'control'
+ | 'entity'
+ | 'database'
+ | 'collections'
+ | 'queue';
+}
+
export interface EdgeMetaData {
animation?: 'fast' | 'slow';
animate?: boolean;