mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-30 13:46:43 +02:00
Compare commits
3 Commits
6623-add-i
...
mermaid@11
Author | SHA1 | Date | |
---|---|---|---|
![]() |
af3bbdc591 | ||
![]() |
8813cf2c94 | ||
![]() |
d145c0e910 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': minor
|
|
||||||
---
|
|
||||||
|
|
||||||
feat: Add custom images and icons support for sequence diagram actors
|
|
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
fix: Handle arrows correctly when auto number is enabled
|
|
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': minor
|
|
||||||
---
|
|
||||||
|
|
||||||
feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.
|
|
@@ -1,118 +0,0 @@
|
|||||||
import { imgSnapshotTest } from '../../helpers/util';
|
|
||||||
|
|
||||||
const looks = ['classic'] as const;
|
|
||||||
|
|
||||||
looks.forEach((look) => {
|
|
||||||
describe(`SequenceDiagram icon participants in ${look} look`, () => {
|
|
||||||
it(`single participant with icon`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
Note over Bob: Icon participant`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`two participants, one icon and one normal`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Hello`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`two icon participants`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice@{ type: "icon", icon: "fa:user" }
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
Alice-->>Bob: Hi`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with markdown htmlLabels:true content`, () => {
|
|
||||||
// html/markdown in messages/notes (participants themselves don't support label/form/w/h)
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
|
|
||||||
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with markdown htmlLabels:false content`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
|
|
||||||
imgSnapshotTest(diagram, {
|
|
||||||
look,
|
|
||||||
htmlLabels: false,
|
|
||||||
flowchart: { htmlLabels: false },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with styles applied to participant`, () => {
|
|
||||||
// style by participant id
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Styled participant
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with classDef and class application`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Classed participant
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Colored emoji icon tests (analogous to the flowchart colored icon tests), no direction line.
|
|
||||||
describe('SequenceDiagram colored icon participant', () => {
|
|
||||||
it('colored emoji icon without styles', () => {
|
|
||||||
const icon = 'fluent-emoji:tropical-fish';
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
|
||||||
Note over Bob: colored emoji icon
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('colored emoji icon with styles', () => {
|
|
||||||
const icon = 'fluent-emoji:tropical-fish';
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mixed scenario: multiple interactions, still no direction line.
|
|
||||||
describe('SequenceDiagram icon participant with multiple interactions', () => {
|
|
||||||
const icon = 'fa:bell-slash';
|
|
||||||
it('icon participant interacts with two normal participants', () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
|
||||||
participant Alice
|
|
||||||
participant Carol
|
|
||||||
Bob->>Alice: Ping
|
|
||||||
Alice-->>Bob: Pong
|
|
||||||
Bob->>Carol: Notify
|
|
||||||
Note right of Bob: Icon side note`;
|
|
||||||
imgSnapshotTest(diagram);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,94 +0,0 @@
|
|||||||
import { imgSnapshotTest } from '../../helpers/util';
|
|
||||||
|
|
||||||
const looks = ['classic'] as const;
|
|
||||||
|
|
||||||
looks.forEach((look) => {
|
|
||||||
describe(`SequenceDiagram image participants in ${look} look`, () => {
|
|
||||||
it(`single participant with image`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
Note over Bob: Image participant`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`two participants, one image and one normal`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Hello`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`two image participants`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
Alice-->>Bob: Hi`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with markdown htmlLabels:true content`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
|
|
||||||
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with markdown htmlLabels:false content`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
|
|
||||||
imgSnapshotTest(diagram, {
|
|
||||||
look,
|
|
||||||
htmlLabels: false,
|
|
||||||
flowchart: { htmlLabels: false },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with styles applied to participant`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Styled participant
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with classDef and class application`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Classed participant
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mixed scenario: multiple interactions, still no direction line.
|
|
||||||
describe('SequenceDiagram image participant with multiple interactions', () => {
|
|
||||||
const imageUrl = 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg';
|
|
||||||
it('image participant interacts with two normal participants', () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "${imageUrl}" }
|
|
||||||
participant Alice
|
|
||||||
participant Carol
|
|
||||||
Bob->>Alice: Ping
|
|
||||||
Alice-->>Bob: Pong
|
|
||||||
Bob->>Carol: Notify
|
|
||||||
Note right of Bob: Image side note`;
|
|
||||||
imgSnapshotTest(diagram);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,659 +0,0 @@
|
|||||||
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<br>with explicit line break
|
|
||||||
and
|
|
||||||
B ->> A: Parallel message 2<br>with explicit line break
|
|
||||||
end
|
|
||||||
|
|
||||||
loop Wrapped loop
|
|
||||||
Note right of B: This is a long note<br>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<br/>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<br/>with explicit<br/>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<br/>long time, so long<br/>that the text does<br/>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<br/>long time, so long<br/>that the text does<br/>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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -74,166 +74,6 @@ sequenceDiagram
|
|||||||
Bob->>Alice: Hi Alice
|
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Icon
|
|
||||||
|
|
||||||
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
|
|
||||||
|
|
||||||
```mermaid-example
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Icon participant
|
|
||||||
Bob->>Alice: Response to icon
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Icon participant
|
|
||||||
Bob->>Alice: Response to icon
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image
|
|
||||||
|
|
||||||
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
|
|
||||||
|
|
||||||
```mermaid-example
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Image participant
|
|
||||||
Bob->>Alice: Response to image
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Image participant
|
|
||||||
Bob->>Alice: Response to image
|
|
||||||
```
|
|
||||||
|
|
||||||
### Aliases
|
### Aliases
|
||||||
|
|
||||||
The actor can have a convenient identifier and a descriptive label.
|
The actor can have a convenient identifier and a descriptive label.
|
||||||
|
@@ -1,5 +1,11 @@
|
|||||||
# mermaid
|
# mermaid
|
||||||
|
|
||||||
|
## 11.10.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#6886](https://github.com/mermaid-js/mermaid/pull/6886) [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Handle arrows correctly when auto number is enabled
|
||||||
|
|
||||||
## 11.10.0
|
## 11.10.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mermaid",
|
"name": "mermaid",
|
||||||
"version": "11.10.0",
|
"version": "11.10.1",
|
||||||
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
|
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "./dist/mermaid.core.mjs",
|
"module": "./dist/mermaid.core.mjs",
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
// Special states for recognizing aliases
|
// Special states for recognizing aliases
|
||||||
// A special state for grabbing text up to the first comment/newline
|
// A special state for grabbing text up to the first comment/newline
|
||||||
%x ID ALIAS LINE CONFIG CONFIG_DATA
|
%x ID ALIAS LINE
|
||||||
|
|
||||||
%x acc_title
|
%x acc_title
|
||||||
%x acc_descr
|
%x acc_descr
|
||||||
@@ -28,11 +28,6 @@
|
|||||||
\%%(?!\{)[^\n]* /* skip comments */
|
\%%(?!\{)[^\n]* /* skip comments */
|
||||||
[^\}]\%\%[^\n]* /* skip comments */
|
[^\}]\%\%[^\n]* /* skip comments */
|
||||||
[0-9]+(?=[ \n]+) return 'NUM';
|
[0-9]+(?=[ \n]+) return 'NUM';
|
||||||
<ID>\@\{ { this.begin('CONFIG'); return 'CONFIG_START'; }
|
|
||||||
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
|
|
||||||
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
|
|
||||||
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
|
|
||||||
<ID>[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
|
||||||
"box" { this.begin('LINE'); return 'box'; }
|
"box" { this.begin('LINE'); return 'box'; }
|
||||||
"participant" { this.begin('ID'); return 'participant'; }
|
"participant" { this.begin('ID'); return 'participant'; }
|
||||||
"actor" { this.begin('ID'); return 'participant_actor'; }
|
"actor" { this.begin('ID'); return 'participant_actor'; }
|
||||||
@@ -236,8 +231,6 @@ participant_statement
|
|||||||
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
| '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;}
|
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
|
||||||
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
|
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
|
||||||
| 'participant' actor_with_config 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $$=$2;}
|
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
note_statement
|
note_statement
|
||||||
@@ -308,23 +301,6 @@ signal
|
|||||||
{ $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]}
|
{ $$ = [$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
|
||||||
// : actor_participant
|
// : actor_participant
|
||||||
// | actor_actor
|
// | actor_actor
|
||||||
@@ -337,7 +313,7 @@ signaltype
|
|||||||
: SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
|
: SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
|
||||||
| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
|
| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
|
||||||
| SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
|
| SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
|
||||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||||
| DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
|
| DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
|
||||||
| BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; }
|
| BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; }
|
||||||
| SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }
|
| SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
import * as yaml from 'js-yaml';
|
|
||||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||||
import { log } from '../../logger.js';
|
import { log } from '../../logger.js';
|
||||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||||
@@ -13,7 +12,7 @@ import {
|
|||||||
setAccTitle,
|
setAccTitle,
|
||||||
setDiagramTitle,
|
setDiagramTitle,
|
||||||
} from '../common/commonDb.js';
|
} from '../common/commonDb.js';
|
||||||
import type { Actor, AddMessageParams, Box, Message, Note, ParticipantMetaData } from './types.js';
|
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
|
||||||
|
|
||||||
interface SequenceState {
|
interface SequenceState {
|
||||||
prevActor?: string;
|
prevActor?: string;
|
||||||
@@ -76,19 +75,6 @@ const PLACEMENT = {
|
|||||||
OVER: 2,
|
OVER: 2,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PARTICIPANT_TYPE = {
|
|
||||||
ACTOR: 'actor',
|
|
||||||
BOUNDARY: 'boundary',
|
|
||||||
COLLECTIONS: 'collections',
|
|
||||||
CONTROL: 'control',
|
|
||||||
DATABASE: 'database',
|
|
||||||
ENTITY: 'entity',
|
|
||||||
PARTICIPANT: 'participant',
|
|
||||||
QUEUE: 'queue',
|
|
||||||
ICON: 'icon',
|
|
||||||
IMAGE: 'image',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export class SequenceDB implements DiagramDB {
|
export class SequenceDB implements DiagramDB {
|
||||||
private readonly state = new ImperativeState<SequenceState>(() => ({
|
private readonly state = new ImperativeState<SequenceState>(() => ({
|
||||||
prevActor: undefined,
|
prevActor: undefined,
|
||||||
@@ -133,22 +119,9 @@ export class SequenceDB implements DiagramDB {
|
|||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
description: { text: string; wrap?: boolean | null; type: string },
|
description: { text: string; wrap?: boolean | null; type: string },
|
||||||
type: string,
|
type: string
|
||||||
metadata?: any
|
|
||||||
) {
|
) {
|
||||||
let assignedBox = this.state.records.currentBox;
|
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);
|
const old = this.state.records.actors.get(id);
|
||||||
if (old) {
|
if (old) {
|
||||||
// If already set and trying to set to a new one throw error
|
// If already set and trying to set to a new one throw error
|
||||||
@@ -187,7 +160,6 @@ export class SequenceDB implements DiagramDB {
|
|||||||
actorCnt: null,
|
actorCnt: null,
|
||||||
rectData: null,
|
rectData: null,
|
||||||
type: type ?? 'participant',
|
type: type ?? 'participant',
|
||||||
doc: doc,
|
|
||||||
});
|
});
|
||||||
if (this.state.records.prevActor) {
|
if (this.state.records.prevActor) {
|
||||||
const prevActorInRecords = this.state.records.actors.get(this.state.records.prevActor);
|
const prevActorInRecords = this.state.records.actors.get(this.state.records.prevActor);
|
||||||
@@ -546,7 +518,7 @@ export class SequenceDB implements DiagramDB {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'addParticipant':
|
case 'addParticipant':
|
||||||
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
|
this.addActor(param.actor, param.actor, param.description, param.draw);
|
||||||
break;
|
break;
|
||||||
case 'createParticipant':
|
case 'createParticipant':
|
||||||
if (this.state.records.actors.has(param.actor)) {
|
if (this.state.records.actors.has(param.actor)) {
|
||||||
@@ -555,7 +527,7 @@ export class SequenceDB implements DiagramDB {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.state.records.lastCreated = param.actor;
|
this.state.records.lastCreated = param.actor;
|
||||||
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
|
this.addActor(param.actor, param.actor, param.description, param.draw);
|
||||||
this.state.records.createdActors.set(param.actor, this.state.records.messages.length);
|
this.state.records.createdActors.set(param.actor, this.state.records.messages.length);
|
||||||
break;
|
break;
|
||||||
case 'destroyParticipant':
|
case 'destroyParticipant':
|
||||||
|
@@ -2058,341 +2058,4 @@ Bob->>Alice:Got it!
|
|||||||
expect(messages[0].from).toBe('Alice');
|
expect(messages[0].from).toBe('Alice');
|
||||||
expect(messages[0].to).toBe('Bob');
|
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('image and icon participant parsing', () => {
|
|
||||||
it('should parse image participant with image URL', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
Bob->>Bob: test
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Bob').type).toBe('image');
|
|
||||||
expect(actors.get('Bob').doc.image).toBe(
|
|
||||||
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse icon participant with icon name', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type": "icon", "icon": "fa:bell" }
|
|
||||||
Alice->>Alice: test
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Alice').type).toBe('icon');
|
|
||||||
expect(actors.get('Alice').doc.icon).toBe('fa:bell');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse two image participants', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg" }
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
Alice-->>Bob: Hi
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Bob').type).toBe('image');
|
|
||||||
expect(actors.get('Bob').doc.image).toBe(
|
|
||||||
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
|
|
||||||
);
|
|
||||||
expect(actors.get('Alice').type).toBe('image');
|
|
||||||
expect(actors.get('Alice').doc.image).toBe(
|
|
||||||
'https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse image participant with normal participant', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Bob').type).toBe('image');
|
|
||||||
expect(actors.get('Alice').type).toBe('participant');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse icon participant with normal participant', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ "type": "icon", "icon": "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Bob').type).toBe('icon');
|
|
||||||
expect(actors.get('Alice').type).toBe('participant');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -10,7 +10,6 @@ import assignWithDepth from '../../assignWithDepth.js';
|
|||||||
import utils from '../../utils.js';
|
import utils from '../../utils.js';
|
||||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||||
import type { Diagram } from '../../Diagram.js';
|
import type { Diagram } from '../../Diagram.js';
|
||||||
import { PARTICIPANT_TYPE } from './sequenceDb.js';
|
|
||||||
|
|
||||||
let conf = {};
|
let conf = {};
|
||||||
|
|
||||||
@@ -747,21 +746,11 @@ function adjustCreatedDestroyedData(
|
|||||||
msgModel.startx = msgModel.startx - adjustment;
|
msgModel.startx = msgModel.startx - adjustment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const actorArray = [
|
|
||||||
PARTICIPANT_TYPE.ACTOR,
|
|
||||||
PARTICIPANT_TYPE.CONTROL,
|
|
||||||
PARTICIPANT_TYPE.ENTITY,
|
|
||||||
PARTICIPANT_TYPE.DATABASE,
|
|
||||||
PARTICIPANT_TYPE.ICON,
|
|
||||||
PARTICIPANT_TYPE.IMAGE,
|
|
||||||
];
|
|
||||||
|
|
||||||
// if it is a create message
|
// if it is a create message
|
||||||
if (createdActors.get(msg.to) == index) {
|
if (createdActors.get(msg.to) == index) {
|
||||||
const actor = actors.get(msg.to);
|
const actor = actors.get(msg.to);
|
||||||
const adjustment = actorArray.includes(actor.type)
|
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
|
||||||
? ACTOR_TYPE_WIDTH / 2 + 3
|
|
||||||
: actor.width / 2 + 3;
|
|
||||||
receiverAdjustment(actor, adjustment);
|
receiverAdjustment(actor, adjustment);
|
||||||
actor.starty = lineStartY - actor.height / 2;
|
actor.starty = lineStartY - actor.height / 2;
|
||||||
bounds.bumpVerticalPos(actor.height / 2);
|
bounds.bumpVerticalPos(actor.height / 2);
|
||||||
@@ -770,7 +759,7 @@ function adjustCreatedDestroyedData(
|
|||||||
else if (destroyedActors.get(msg.from) == index) {
|
else if (destroyedActors.get(msg.from) == index) {
|
||||||
const actor = actors.get(msg.from);
|
const actor = actors.get(msg.from);
|
||||||
if (conf.mirrorActors) {
|
if (conf.mirrorActors) {
|
||||||
const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
|
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
|
||||||
senderAdjustment(actor, adjustment);
|
senderAdjustment(actor, adjustment);
|
||||||
}
|
}
|
||||||
actor.stopy = lineStartY - actor.height / 2;
|
actor.stopy = lineStartY - actor.height / 2;
|
||||||
@@ -780,9 +769,7 @@ function adjustCreatedDestroyedData(
|
|||||||
else if (destroyedActors.get(msg.to) == index) {
|
else if (destroyedActors.get(msg.to) == index) {
|
||||||
const actor = actors.get(msg.to);
|
const actor = actors.get(msg.to);
|
||||||
if (conf.mirrorActors) {
|
if (conf.mirrorActors) {
|
||||||
const adjustment = actorArray.includes(actor.type)
|
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
|
||||||
? ACTOR_TYPE_WIDTH / 2 + 3
|
|
||||||
: actor.width / 2 + 3;
|
|
||||||
receiverAdjustment(actor, adjustment);
|
receiverAdjustment(actor, adjustment);
|
||||||
}
|
}
|
||||||
actor.stopy = lineStartY - actor.height / 2;
|
actor.stopy = lineStartY - actor.height / 2;
|
||||||
@@ -1100,11 +1087,10 @@ export const draw = async function (_text: string, id: string, _version: string,
|
|||||||
for (const box of bounds.models.boxes) {
|
for (const box of bounds.models.boxes) {
|
||||||
box.height = bounds.getVerticalPos() - box.y;
|
box.height = bounds.getVerticalPos() - box.y;
|
||||||
bounds.insert(box.x, box.y, box.x + box.width, box.height);
|
bounds.insert(box.x, box.y, box.x + box.width, box.height);
|
||||||
const boxPadding = conf.boxMargin * 2;
|
box.startx = box.x;
|
||||||
box.startx = box.x - boxPadding;
|
box.starty = box.y;
|
||||||
box.starty = box.y - boxPadding * 0.25;
|
box.stopx = box.startx + box.width;
|
||||||
box.stopx = box.startx + box.width + 2 * boxPadding;
|
box.stopy = box.starty + box.height;
|
||||||
box.stopy = box.starty + box.height + boxPadding * 0.75;
|
|
||||||
box.stroke = 'rgb(0,0,0, 0.5)';
|
box.stroke = 'rgb(0,0,0, 0.5)';
|
||||||
svgDraw.drawBox(diagram, box, conf);
|
svgDraw.drawBox(diagram, box, conf);
|
||||||
}
|
}
|
||||||
@@ -1369,9 +1355,6 @@ async function calculateActorMargins(
|
|||||||
return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0));
|
return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0));
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const standardBoxPadding = conf.boxMargin * 8;
|
|
||||||
totalWidth += standardBoxPadding;
|
|
||||||
|
|
||||||
totalWidth -= 2 * conf.boxTextMargin;
|
totalWidth -= 2 * conf.boxTextMargin;
|
||||||
if (box.wrap) {
|
if (box.wrap) {
|
||||||
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
|
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
|
||||||
|
@@ -13,11 +13,6 @@ const getStyles = (options) =>
|
|||||||
stroke: ${options.actorLineColor};
|
stroke: ${options.actorLineColor};
|
||||||
}
|
}
|
||||||
|
|
||||||
.innerArc {
|
|
||||||
stroke-width: 1.5;
|
|
||||||
stroke-dasharray: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageLine0 {
|
.messageLine0 {
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
stroke-dasharray: none;
|
stroke-dasharray: none;
|
||||||
@@ -120,7 +115,6 @@ const getStyles = (options) =>
|
|||||||
fill: ${options.actorBkg};
|
fill: ${options.actorBkg};
|
||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default getStyles;
|
export default getStyles;
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||||
import * as configApi from '../../config.js';
|
import * as configApi from '../../config.js';
|
||||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
|
||||||
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
|
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
|
||||||
import common, {
|
import common, {
|
||||||
calculateMathMLDimensions,
|
calculateMathMLDimensions,
|
||||||
@@ -323,89 +322,6 @@ export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawActorTypeIcon = async 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;
|
|
||||||
|
|
||||||
const line = elem.append('g').lower();
|
|
||||||
if (!isFooter) {
|
|
||||||
actorCnt++;
|
|
||||||
line
|
|
||||||
.append('line')
|
|
||||||
.attr('id', 'actor' + actorCnt)
|
|
||||||
.attr('x1', center)
|
|
||||||
.attr('y1', centerY + 25)
|
|
||||||
.attr('x2', center)
|
|
||||||
.attr('y2', 2000)
|
|
||||||
.attr('class', 'actor-line')
|
|
||||||
.attr('stroke-width', '0.5px')
|
|
||||||
.attr('stroke', '#999')
|
|
||||||
.attr('name', actor.name);
|
|
||||||
actor.actorCnt = actorCnt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actElem = elem.append('g');
|
|
||||||
let cssClass = 'actor-icon';
|
|
||||||
if (isFooter) {
|
|
||||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
|
||||||
} else {
|
|
||||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
actElem.attr('class', cssClass);
|
|
||||||
actElem.attr('name', actor.name);
|
|
||||||
|
|
||||||
// Define the size of the square and icon
|
|
||||||
const iconSize = actor.width / 5;
|
|
||||||
const squareX = center - iconSize / 2;
|
|
||||||
const squareY = !isFooter ? actorY + 10 : actorY;
|
|
||||||
|
|
||||||
// Draw a square rectangle for the actor icon background
|
|
||||||
|
|
||||||
actElem
|
|
||||||
.append('rect')
|
|
||||||
.attr('x', squareX)
|
|
||||||
.attr('y', squareY)
|
|
||||||
.attr('width', iconSize)
|
|
||||||
.attr('height', iconSize)
|
|
||||||
.attr('rx', 3) // rounded corners, optional
|
|
||||||
.attr('ry', 3)
|
|
||||||
.attr('fill', 'none'); // light gray background or customize as needed
|
|
||||||
|
|
||||||
// Render icon SVG inside the rectangle
|
|
||||||
const iconGroup = actElem.append('g').attr('transform', `translate(${squareX}, ${squareY})`);
|
|
||||||
|
|
||||||
iconGroup
|
|
||||||
.append('svg')
|
|
||||||
.attr('width', iconSize)
|
|
||||||
.attr('height', iconSize)
|
|
||||||
.html(
|
|
||||||
`<g>${await getIconSVG(actor.doc.icon, {
|
|
||||||
height: iconSize,
|
|
||||||
width: iconSize,
|
|
||||||
fallbackPrefix: '',
|
|
||||||
})}</g>`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add text label below icon
|
|
||||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
|
||||||
actor.description,
|
|
||||||
actElem,
|
|
||||||
actor.x,
|
|
||||||
actorY + (!isFooter ? 40 : 30), // positioning below the square icon
|
|
||||||
actor.width,
|
|
||||||
20,
|
|
||||||
{ class: 'actor-icon-text' },
|
|
||||||
conf
|
|
||||||
);
|
|
||||||
|
|
||||||
const bounds = actElem.node().getBBox();
|
|
||||||
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
|
|
||||||
|
|
||||||
return actor.height;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws an actor in the diagram with the attached line
|
* Draws an actor in the diagram with the attached line
|
||||||
*
|
*
|
||||||
@@ -498,768 +414,6 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
|
|||||||
|
|
||||||
return height;
|
return height;
|
||||||
};
|
};
|
||||||
const drawActorTypeImage = async function (elem, actor, conf, isFooter) {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = actor.doc.image ?? '';
|
|
||||||
await img.decode();
|
|
||||||
|
|
||||||
const imageNaturalWidth = Number(img.naturalWidth.toString().replace('px', ''));
|
|
||||||
const imageNaturalHeight = Number(img.naturalHeight.toString().replace('px', ''));
|
|
||||||
|
|
||||||
actor.doc.imageAspectRatio = imageNaturalWidth / imageNaturalHeight;
|
|
||||||
|
|
||||||
// Calculate image dimensions with proper sizing logic
|
|
||||||
let imageWidth, imageHeight;
|
|
||||||
|
|
||||||
// Check if custom dimensions are provided and valid
|
|
||||||
const hasValidCustomDimensions =
|
|
||||||
actor.doc.height && actor.doc.height > 10 && actor.doc.width && actor.doc.width > 10;
|
|
||||||
|
|
||||||
if (hasValidCustomDimensions) {
|
|
||||||
if (actor.doc.constraint === 'on') {
|
|
||||||
// Maintain aspect ratio with constraint
|
|
||||||
const customAspectRatio = imageNaturalWidth / imageNaturalHeight;
|
|
||||||
|
|
||||||
if (customAspectRatio > actor.doc.imageAspectRatio) {
|
|
||||||
// Width is the limiting factor
|
|
||||||
imageHeight = actor.doc.height;
|
|
||||||
imageWidth = actor.doc.height * actor.doc.imageAspectRatio;
|
|
||||||
} else {
|
|
||||||
// Height is the limiting factor
|
|
||||||
imageWidth = actor.doc.width;
|
|
||||||
imageHeight = actor.doc.width / actor.doc.imageAspectRatio;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use custom dimensions without maintaining aspect ratio
|
|
||||||
imageWidth = actor.doc.width;
|
|
||||||
imageHeight = actor.doc.height;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use default sizing based on actor width
|
|
||||||
const defaultImageSize = actor.width / 3.5;
|
|
||||||
|
|
||||||
// Ensure minimum and maximum sizes
|
|
||||||
const minSize = 30;
|
|
||||||
const maxSize = actor.width * 0.8;
|
|
||||||
|
|
||||||
if (actor.doc.imageAspectRatio >= 1) {
|
|
||||||
// Landscape or square image
|
|
||||||
imageWidth = Math.min(Math.max(defaultImageSize, minSize), maxSize);
|
|
||||||
imageHeight = imageWidth / actor.doc.imageAspectRatio;
|
|
||||||
} else {
|
|
||||||
// Portrait image
|
|
||||||
imageHeight = Math.min(Math.max(defaultImageSize, minSize), maxSize);
|
|
||||||
imageWidth = imageHeight * actor.doc.imageAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the image doesn't exceed actor bounds
|
|
||||||
if (imageWidth > actor.width * 0.9) {
|
|
||||||
imageWidth = actor.width * 0.9;
|
|
||||||
imageHeight = imageWidth / actor.doc.imageAspectRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
|
||||||
const center = actor.x + actor.width / 2;
|
|
||||||
const centerY = actorY + imageHeight;
|
|
||||||
|
|
||||||
// Calculate positioning
|
|
||||||
const squareX = center - imageWidth / 2;
|
|
||||||
let squareY;
|
|
||||||
|
|
||||||
if (isFooter) {
|
|
||||||
squareY = actorY + (imageHeight - imageHeight * 1);
|
|
||||||
} else {
|
|
||||||
squareY = actorY + 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate text position based on image position and size
|
|
||||||
const textY = !isFooter ? squareY + imageHeight : actorY + imageHeight; // Place text below image for header
|
|
||||||
|
|
||||||
// Draw actor line for non-footer elements
|
|
||||||
const x = center;
|
|
||||||
const y = centerY + (isFooter ? 0 : imageHeight / 2); // Adjust line start based on image height
|
|
||||||
const line = elem.append('g').lower();
|
|
||||||
if (!isFooter) {
|
|
||||||
actorCnt++;
|
|
||||||
line
|
|
||||||
.append('line')
|
|
||||||
.attr('id', 'actor' + actorCnt)
|
|
||||||
.attr('x1', x)
|
|
||||||
.attr('y1', y) // Adjust line start based on image height
|
|
||||||
.attr('x2', center)
|
|
||||||
.attr('y2', 2000)
|
|
||||||
.attr('class', 'actor-line')
|
|
||||||
.attr('stroke-width', '0.5px')
|
|
||||||
.attr('stroke', '#999')
|
|
||||||
.attr('name', actor.name);
|
|
||||||
actor.actorCnt = actorCnt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actElem = elem.append('g');
|
|
||||||
let cssClass = 'actor-image';
|
|
||||||
if (isFooter) {
|
|
||||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
|
||||||
} else {
|
|
||||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
actElem.attr('class', cssClass);
|
|
||||||
actElem.attr('name', actor.name);
|
|
||||||
|
|
||||||
// Draw background rectangle for the actor image
|
|
||||||
actElem
|
|
||||||
.append('rect')
|
|
||||||
.attr('x', squareX)
|
|
||||||
.attr('y', squareY)
|
|
||||||
.attr('width', imageWidth)
|
|
||||||
.attr('height', imageHeight)
|
|
||||||
.attr('rx', 3)
|
|
||||||
.attr('ry', 3)
|
|
||||||
.attr('fill', 'white')
|
|
||||||
.attr('stroke', '#ddd')
|
|
||||||
.attr('stroke-width', '1px');
|
|
||||||
|
|
||||||
// Create clipping path for the image
|
|
||||||
const clipId = `clip-actor-${actorCnt || 'footer'}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
actElem
|
|
||||||
.append('defs')
|
|
||||||
.append('clipPath')
|
|
||||||
.attr('id', clipId)
|
|
||||||
.append('rect')
|
|
||||||
.attr('x', squareX)
|
|
||||||
.attr('y', squareY)
|
|
||||||
.attr('width', imageWidth)
|
|
||||||
.attr('height', imageHeight)
|
|
||||||
.attr('rx', 3)
|
|
||||||
.attr('ry', 3);
|
|
||||||
|
|
||||||
// Render image inside the rectangle
|
|
||||||
const imageGroup = actElem.append('g');
|
|
||||||
|
|
||||||
if (actor.doc.image) {
|
|
||||||
imageGroup
|
|
||||||
.append('image')
|
|
||||||
.attr('x', squareX)
|
|
||||||
.attr('y', squareY)
|
|
||||||
.attr('width', imageWidth)
|
|
||||||
.attr('height', imageHeight)
|
|
||||||
.attr('href', img.src)
|
|
||||||
.attr('preserveAspectRatio', actor.doc.constraint === 'on' ? 'xMidYMid meet' : 'none');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add text label
|
|
||||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
|
||||||
actor.description,
|
|
||||||
actElem,
|
|
||||||
actor.x,
|
|
||||||
textY,
|
|
||||||
actor.width,
|
|
||||||
20,
|
|
||||||
{ class: 'actor-image-text' },
|
|
||||||
conf
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate final bounds and update actor height
|
|
||||||
const bounds = actElem.node().getBBox();
|
|
||||||
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
|
|
||||||
|
|
||||||
return actor.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 drawActorTypeActor = function (elem, actor, conf, isFooter) {
|
||||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||||
@@ -1362,22 +516,6 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
|
|||||||
return await drawActorTypeActor(elem, actor, conf, isFooter);
|
return await drawActorTypeActor(elem, actor, conf, isFooter);
|
||||||
case 'participant':
|
case 'participant':
|
||||||
return await drawActorTypeParticipant(elem, actor, conf, isFooter);
|
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);
|
|
||||||
case 'icon':
|
|
||||||
return await drawActorTypeIcon(elem, actor, conf, isFooter);
|
|
||||||
case 'image':
|
|
||||||
return await drawActorTypeImage(elem, actor, conf, isFooter);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -17,9 +17,6 @@ export interface Actor {
|
|||||||
actorCnt: number | null;
|
actorCnt: number | null;
|
||||||
rectData: unknown;
|
rectData: unknown;
|
||||||
type: string;
|
type: string;
|
||||||
doc?: ParticipantMetaData; // For documentation
|
|
||||||
iconName?: string; // For icon type
|
|
||||||
imgSrc?: string; // For img type
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
@@ -93,20 +90,3 @@ export interface Note {
|
|||||||
message: string;
|
message: string;
|
||||||
wrap: boolean;
|
wrap: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParticipantMetaData {
|
|
||||||
type?:
|
|
||||||
| 'actor'
|
|
||||||
| 'participant'
|
|
||||||
| 'boundary'
|
|
||||||
| 'control'
|
|
||||||
| 'entity'
|
|
||||||
| 'database'
|
|
||||||
| 'collections'
|
|
||||||
| 'queue'
|
|
||||||
| 'icon'
|
|
||||||
| 'img';
|
|
||||||
icon?: string;
|
|
||||||
img?: string;
|
|
||||||
form?: string;
|
|
||||||
}
|
|
||||||
|
@@ -46,102 +46,6 @@ sequenceDiagram
|
|||||||
Bob->>Alice: Hi Alice
|
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Icon
|
|
||||||
|
|
||||||
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
|
|
||||||
|
|
||||||
```mermaid-example
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Icon participant
|
|
||||||
Bob->>Alice: Response to icon
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image
|
|
||||||
|
|
||||||
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
|
|
||||||
|
|
||||||
```mermaid-example
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Image participant
|
|
||||||
Bob->>Alice: Response to image
|
|
||||||
```
|
|
||||||
|
|
||||||
### Aliases
|
### Aliases
|
||||||
|
|
||||||
The actor can have a convenient identifier and a descriptive label.
|
The actor can have a convenient identifier and a descriptive label.
|
||||||
|
@@ -1,5 +1,11 @@
|
|||||||
# mermaid
|
# mermaid
|
||||||
|
|
||||||
|
## 11.10.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#6886](https://github.com/mermaid-js/mermaid/pull/6886) [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Handle arrows correctly when auto number is enabled
|
||||||
|
|
||||||
## 11.10.0
|
## 11.10.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mermaid-js/tiny",
|
"name": "@mermaid-js/tiny",
|
||||||
"version": "11.10.0",
|
"version": "11.10.1",
|
||||||
"description": "Tiny version of mermaid",
|
"description": "Tiny version of mermaid",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"main": "./dist/mermaid.tiny.js",
|
"main": "./dist/mermaid.tiny.js",
|
||||||
|
Reference in New Issue
Block a user