Compare commits

..

1 Commits

Author SHA1 Message Date
shubham-mermaid
5c04a8d09d Update: Added folder structure for usecase diagram
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-06 14:54:29 +05:30
28 changed files with 358 additions and 1868 deletions

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.

View File

@@ -143,6 +143,7 @@ typeof
typestr
unshift
urlsafe
usecase
verifymethod
VERIFYMTHD
WARN_DOCSDIR_DOESNT_MATCH

View File

@@ -1,644 +0,0 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
const looks = ['classic'];
const participantTypes = [
'participant',
'actor',
'boundary',
'control',
'entity',
'database',
'collections',
'queue',
];
const interactionTypes = [
'->>', // Solid arrow with arrowhead
'-->>', // Dotted arrow with arrowhead
'->', // Solid arrow without arrowhead
'-->', // Dotted arrow without arrowhead
'-x', // Solid arrow with cross
'--x', // Dotted arrow with cross
'->>+', // Solid arrow with arrowhead (activate)
'-->>+', // Dotted arrow with arrowhead (activate)
];
const notePositions = ['left of', 'right of', 'over'];
looks.forEach((look) => {
describe(`Sequence Diagram Tests - ${look} look`, () => {
it('should render all participant types', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.forEach((type, index) => {
diagramCode += ` ${type} ${type}${index} as ${type} ${index}\n`;
});
// Add some basic interactions
for (let i = 0; i < participantTypes.length - 1; i++) {
diagramCode += ` ${participantTypes[i]}${i} ->> ${participantTypes[i + 1]}${i + 1}: Message ${i}\n`;
}
imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } });
});
it('should render all interaction types', () => {
let diagramCode = `sequenceDiagram\n`;
// Create two participants
// Add all interaction types
diagramCode += ` participant A\n`;
diagramCode += ` participant B\n`;
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((type, index) => {
diagramCode += ` ${type} A\n`;
diagramCode += ` ${type} B\n`;
diagramCode += ` create ${type} ${type}${index}\n`;
diagramCode += ` A ->> ${type}${index}: Hello ${type}\n`;
if (index % 2 === 0) {
diagramCode += ` destroy ${type}${index}\n`;
}
});
imgSnapshotTest(diagramCode, { look });
});
it('should render notes in all positions', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += ` participant A\n`;
diagramCode += ` participant B\n`;
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((type, index) => {
diagramCode += ` ${type} ${type}${index}\n`;
});
diagramCode += ` par Parallel actions\n`;
for (let i = 0; i < participantTypes.length - 1; i += 2) {
diagramCode += ` ${participantTypes[i]}${i} ->> ${participantTypes[i + 1]}${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 += ` participant A\n`;
diagramCode += ` participant B\n`;
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((type, index) => {
diagramCode += ` ${type} ${type}${index}\n`;
});
diagramCode += ` loop For each participant\n`;
for (let i = 0; i < 3; i++) {
diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[1]}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((type, index) => {
diagramCode += ` ${type} ${type}${index}\n`;
});
diagramCode += ` end\n`;
diagramCode += ` box rgb(200,220,255) Group 2\n`;
participantTypes.slice(3, 6).forEach((type, index) => {
diagramCode += ` ${type} ${type}${index}\n`;
});
diagramCode += ` end\n`;
// Add some interactions
diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[3]}0: Cross-group message\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render with different font settings', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.slice(0, 3).forEach((type, index) => {
diagramCode += ` ${type} ${type}${index}\n`;
});
diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[1]}1: Regular message\n`;
diagramCode += ` Note right of ${participantTypes[1]}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
boundary LoginUI
control AuthService
database UserDB
end
box rgb(200,255,220) Order Processing
entity Order
queue OrderQueue
collections AuditLogs
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
actor User
participant AuthService as Authentication Service
boundary UI
control OrderController
entity Product
database MongoDB
collections Products
queue OrderQueue
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
actor Customer
participant Frontend
boundary PaymentGateway
Customer ->> Frontend: Place order
Frontend ->> OrderProcessor: Process order
create database OrderDB
OrderProcessor ->> OrderDB: Save order
`
);
});
it('should handle complex interactions between different participant types', () => {
imgSnapshotTest(
`
sequenceDiagram
box rgba(200,220,255,0.5) System Components
actor User
boundary WebUI
control API
entity BusinessLogic
database MainDB
end
box rgba(200,255,220,0.5) External Services
queue MessageQueue
database AuditLogs
end
User ->> WebUI: Submit request
WebUI ->> API: Process request
API ->> BusinessLogic: Execute business rules
BusinessLogic ->> MainDB: Query data
MainDB -->> BusinessLogic: Return results
BusinessLogic ->> MessageQueue: Publish event
MessageQueue -->> AuditLogs: Store audit trail
AuditLogs -->> API: Audit complete
API -->> WebUI: Return response
WebUI -->> User: Show results
`,
{ sequence: { useMaxWidth: false } }
);
});
it('should render parallel processes with different participant types', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Customer
participant Frontend
boundary PaymentService
control InventoryManager
entity Order
database OrdersDB
queue NotificationQueue
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
boundary AuthService
control UserManager
entity UserProfile
database UserDB
database Logs
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
boundary CloudService
control DataProcessor
entity Transaction
database TransactionsDB
queue EventBus
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
actor LongNameUser as User With A Very<br/>Long Name
participant FE as Frontend Service<br/>With Long Name
boundary B as Boundary With<br/>Multiline Name
control C as Control With<br/>Multiline Name
entity E as Entity With<br/>Multiline Name
database DB as Database With<br/>Multiline Name
collections COL as Collections With<br/>Multiline Name
queue Q as Queue With<br/>Multiline Name
LongNameUser ->> FE: This is a very long message that should wrap properly in the diagram
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
boundary Alice
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
control Alice
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
entity Alice
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
database Alice
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
collections Alice
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
queue Alice
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
boundary Charlie
note over Alice: Some note
note over Charlie: Other note
`,
{}
);
});
it('should render long messages from database to collections', () => {
imgSnapshotTest(
`
sequenceDiagram
database Alice
collections Bob
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
control Alice
entity Bob
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
queue Alice
boundary Bob
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
database Bob
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
boundary Bob
control John as John<br/>Second Line
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
boundary Bob
control John as John<br/>Second Line
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');
});
});
});
});

View File

@@ -74,222 +74,6 @@ sequenceDiagram
Bob->>Alice: Hi Alice
```
### Boundary
If you want to use the boundary symbol for a participant, use the `boundary` statement as shown below.
```mermaid-example
sequenceDiagram
boundary theBoundary
participant Bob
theBoundary->>Bob: Request from boundary
Bob->>theBoundary: Response to boundary
```
```mermaid
sequenceDiagram
boundary theBoundary
participant Bob
theBoundary->>Bob: Request from boundary
Bob->>theBoundary: Response to boundary
```
```mermaid-example
sequenceDiagram
boundary theBoundary
participant Bob
theBoundary->>Bob: Request from boundary
Bob->>theBoundary: Response to boundary
```
```mermaid
sequenceDiagram
boundary theBoundary
participant Bob
theBoundary->>Bob: Request from boundary
Bob->>theBoundary: Response to boundary
```
### Control
If you want to use the control symbol for a participant, use the `control` statement as shown below.
```mermaid-example
sequenceDiagram
control theControl
participant Alice
theControl->>Alice: Control request
Alice->>theControl: Control response
```
```mermaid
sequenceDiagram
control theControl
participant Alice
theControl->>Alice: Control request
Alice->>theControl: Control response
```
```mermaid-example
sequenceDiagram
control theControl
participant Alice
theControl->>Alice: Control request
Alice->>theControl: Control response
```
```mermaid
sequenceDiagram
control theControl
participant Alice
theControl->>Alice: Control request
Alice->>theControl: Control response
```
### Entity
If you want to use the entity symbol for a participant, use the `entity` statement as shown below.
```mermaid-example
sequenceDiagram
entity theEntity
participant Bob
theEntity->>Bob: Entity request
Bob->>theEntity: Entity response
```
```mermaid
sequenceDiagram
entity theEntity
participant Bob
theEntity->>Bob: Entity request
Bob->>theEntity: Entity response
```
```mermaid-example
sequenceDiagram
entity theEntity
participant Bob
theEntity->>Bob: Entity request
Bob->>theEntity: Entity response
```
```mermaid
sequenceDiagram
entity theEntity
participant Bob
theEntity->>Bob: Entity request
Bob->>theEntity: Entity response
```
### Database
If you want to use the database symbol for a participant, use the `database` statement as shown below.
```mermaid-example
sequenceDiagram
database theDb
participant Alice
theDb->>Alice: DB query
Alice->>theDb: DB result
```
```mermaid
sequenceDiagram
database theDb
participant Alice
theDb->>Alice: DB query
Alice->>theDb: DB result
```
```mermaid-example
sequenceDiagram
database theDb
participant Alice
theDb->>Alice: DB query
Alice->>theDb: DB result
```
```mermaid
sequenceDiagram
database theDb
participant Alice
theDb->>Alice: DB query
Alice->>theDb: DB result
```
### Collections
If you want to use the collections symbol for a participant, use the `collections` statement as shown below.
```mermaid-example
sequenceDiagram
collections theCollection
participant Bob
theCollection->>Bob: Collections request
Bob->>theCollection: Collections response
```
```mermaid
sequenceDiagram
collections theCollection
participant Bob
theCollection->>Bob: Collections request
Bob->>theCollection: Collections response
```
```mermaid-example
sequenceDiagram
collections theCollection
participant Bob
theCollection->>Bob: Collections request
Bob->>theCollection: Collections response
```
```mermaid
sequenceDiagram
collections theCollection
participant Bob
theCollection->>Bob: Collections request
Bob->>theCollection: Collections response
```
### Queue
If you want to use the queue symbol for a participant, use the `queue` statement as shown below.
```mermaid-example
sequenceDiagram
queue theQueue
participant Alice
theQueue->>Alice: Queue message
Alice->>theQueue: Queue
```
```mermaid
sequenceDiagram
queue theQueue
participant Alice
theQueue->>Alice: Queue message
Alice->>theQueue: Queue
```
```mermaid-example
sequenceDiagram
queue theQueue
participant Alice
theQueue->>Alice: Queue message
Alice->>theQueue: Queue
```
```mermaid
sequenceDiagram
queue theQueue
participant Alice
theQueue->>Alice: Queue message
Alice->>theQueue: Queue
```
### Aliases
The actor can have a convenient identifier and a descriptive label.

View File

@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
import { treemap } from '../diagrams/treemap/detector.js';
import { usecase } from '../diagrams/usecase/detector.js';
import '../type.d.ts';
let hasLoadedDiagrams = false;
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
xychart,
block,
radar,
treemap
treemap,
usecase
);
};

View File

@@ -12,7 +12,6 @@
%options case-insensitive
// Special states for recognizing aliases
// A special state for grabbing text up to the first comment/newline
%x ID ALIAS LINE
@@ -32,12 +31,6 @@
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
"boundary" { return yy.matchAsActorOrParticipant('boundary', 'participant_boundary', this._input, this); }
"control" { return yy.matchAsActorOrParticipant('control', 'participant_control', this._input, this); }
"entity" { return yy.matchAsActorOrParticipant('entity', 'participant_entity', this._input, this); }
"database" { return yy.matchAsActorOrParticipant('database', 'participant_database', this._input, this); }
"collections" { return yy.matchAsActorOrParticipant('collections', 'participant_collections', this._input, this); }
"queue" { return yy.matchAsActorOrParticipant('queue', 'participant_queue', this._input, this); }
"create" return 'create';
"destroy" { this.begin('ID'); return 'destroy'; }
<ID>[^\<->\->:\n,;]+?([\-]*[^\<->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
@@ -238,25 +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 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
| 'participant_boundary' actor 'AS' restOfLine 'NEWLINE' {$2.draw='boundary'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_boundary' actor 'NEWLINE' {$2.draw='boundary'; $2.type='addParticipant'; $$=$2;}
| 'participant_control' actor 'AS' restOfLine 'NEWLINE' {$2.draw='control'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_control' actor 'NEWLINE' {$2.draw='control'; $2.type='addParticipant'; $$=$2;}
| 'participant_entity' actor 'AS' restOfLine 'NEWLINE' {$2.draw='entity'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_entity' actor 'NEWLINE' {$2.draw='entity'; $2.type='addParticipant'; $$=$2;}
| 'participant_database' actor 'AS' restOfLine 'NEWLINE' {$2.draw='database'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_database' actor 'NEWLINE' {$2.draw='database'; $2.type='addParticipant'; $$=$2;}
| 'participant_collections' actor 'AS' restOfLine 'NEWLINE' {$2.draw='collections'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_collections' actor 'NEWLINE' {$2.draw='collections'; $2.type='addParticipant'; $$=$2;}
| 'participant_queue' actor 'AS' restOfLine 'NEWLINE' {$2.draw='queue'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_queue' actor 'NEWLINE' {$2.draw='queue'; $2.type='addParticipant'; $$=$2;}
;
note_statement

View File

@@ -75,17 +75,6 @@ const PLACEMENT = {
OVER: 2,
} as const;
export const PARTICIPANT_TYPE = {
ACTOR: 'actor',
BOUNDARY: 'boundary',
COLLECTIONS: 'collections',
CONTROL: 'control',
DATABASE: 'database',
ENTITY: 'entity',
PARTICIPANT: 'participant',
QUEUE: 'queue',
} as const;
export class SequenceDB implements DiagramDB {
private readonly state = new ImperativeState<SequenceState>(() => ({
prevActor: undefined,
@@ -107,7 +96,6 @@ export class SequenceDB implements DiagramDB {
this.apply = this.apply.bind(this);
this.parseBoxData = this.parseBoxData.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.matchAsActorOrParticipant = this.matchAsActorOrParticipant.bind(this);
this.clear();
@@ -342,22 +330,6 @@ export class SequenceDB implements DiagramDB {
return message;
}
public matchAsActorOrParticipant(
tokenName: string,
tokenType: string,
inputRemainder: string,
lexer: any
): string {
const arrowLike = /^\s*(->>|-->>|->|-->|<<->>|<<-->>|-x|--x|-\))/;
const colonLike = /^\s*:/;
if (arrowLike.test(inputRemainder) || colonLike.test(inputRemainder)) {
return 'ACTOR';
}
lexer.begin('ID'); // used the passed lexer
return tokenType;
}
// We expect the box statement to be color first then description
// The color can be rgb,rgba,hsl,hsla, or css code names #hex codes are not supported for now because of the way the char # is handled
// We extract first segment as color, the rest of the line is considered as text

View File

@@ -2038,189 +2038,4 @@ Bob->>Alice:Got it!
expect(messages[0].from).toBe('Alice');
expect(messages[0].to).toBe('Bob');
});
describe('when newly parsing messages ', () => {
it('should parse a message', async () => {
const actor1 = 'database';
const diagram = await Diagram.fromText(`
sequenceDiagram
database Alice
database Bob
Bob->>+Alice: Hi Alice
Alice->>+Bob: Hi Bob
`);
const messages = diagram.db.getMessages();
});
it('should parse a message', async () => {
const actor1 = 'database';
const diagram = await Diagram.fromText(`
sequenceDiagram
participant lead
queue dsa
API->>+Database: getUserb
Database-->>-API: userb
queue --> Database: hello
`);
const messages = diagram.db.getMessages();
});
});
describe('participant type parsing', () => {
it('should parse boundary participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
boundary B as Boundary Box
B->B: test
`);
const actors = diagram.db.getActors();
expect(actors.get('B').type).toBe('boundary');
expect(actors.get('B').description).toBe('Boundary Box');
});
it('should parse control participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
control C as Controller
C->C: test
`);
const actors = diagram.db.getActors();
expect(actors.get('C').type).toBe('control');
expect(actors.get('C').description).toBe('Controller');
});
it('should parse entity participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
entity E as Entity
E->E: test
`);
const actors = diagram.db.getActors();
expect(actors.get('E').type).toBe('entity');
expect(actors.get('E').description).toBe('Entity');
});
it('should parse database participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
database D as Database
D->D: test
`);
const actors = diagram.db.getActors();
expect(actors.get('D').type).toBe('database');
expect(actors.get('D').description).toBe('Database');
});
it('should parse collections participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
collections L as List
L->L: test
`);
const actors = diagram.db.getActors();
expect(actors.get('L').type).toBe('collections');
expect(actors.get('L').description).toBe('List');
});
it('should parse queue participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
queue Q as Jobs
Q->Q: test
`);
const actors = diagram.db.getActors();
expect(actors.get('Q').type).toBe('queue');
expect(actors.get('Q').description).toBe('Jobs');
});
});
describe('participant type parsing', () => {
it('should parse actor participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
queue A as ActorName
A->A: test
`);
const actors = diagram.db.getActors();
expect(actors.get('A').type).toBe('queue');
expect(actors.get('A').description).toBe('ActorName');
});
it('should parse participant participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
database P as PartName
P->P: test
`);
const actors = diagram.db.getActors();
expect(actors.get('P').type).toBe('database');
expect(actors.get('P').description).toBe('PartName');
});
it('should parse boundary using actor keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
collections B as Boundary
B->B: test
`);
const actors = diagram.db.getActors();
expect(actors.get('B').type).toBe('collections');
expect(actors.get('B').description).toBe('Boundary');
});
it('should parse control using participant keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
control C as Controller
C->C: test
`);
const actors = diagram.db.getActors();
expect(actors.get('C').type).toBe('control');
expect(actors.get('C').description).toBe('Controller');
});
it('should parse entity using actor keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
entity E as Entity
E->E: test
`);
const actors = diagram.db.getActors();
expect(actors.get('E').type).toBe('entity');
expect(actors.get('E').description).toBe('Entity');
});
it('should parse database using participant keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant D as Database
D->D: test
`);
const actors = diagram.db.getActors();
expect(actors.get('D').type).toBe('participant');
expect(actors.get('D').description).toBe('Database');
});
it('should parse collections using actor keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
actor L as List
L->L: test
`);
const actors = diagram.db.getActors();
expect(actors.get('L').type).toBe('actor');
expect(actors.get('L').description).toBe('List');
});
it('should parse queue using participant keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Q as Jobs
Q->Q: test
`);
const actors = diagram.db.getActors();
expect(actors.get('Q').type).toBe('participant');
expect(actors.get('Q').description).toBe('Jobs');
});
});
});

View File

@@ -10,7 +10,6 @@ import assignWithDepth from '../../assignWithDepth.js';
import utils from '../../utils.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { Diagram } from '../../Diagram.js';
import { PARTICIPANT_TYPE } from './sequenceDb.js';
let conf = {};
@@ -725,19 +724,11 @@ function adjustCreatedDestroyedData(
msgModel.startx = msgModel.startx - adjustment;
}
}
const actorArray = [
PARTICIPANT_TYPE.ACTOR,
PARTICIPANT_TYPE.CONTROL,
PARTICIPANT_TYPE.ENTITY,
PARTICIPANT_TYPE.DATABASE,
];
// if it is a create message
if (createdActors.get(msg.to) == index) {
const actor = actors.get(msg.to);
const adjustment = actorArray.includes(actor.type)
? ACTOR_TYPE_WIDTH / 2 + 3
: actor.width / 2 + 3;
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
receiverAdjustment(actor, adjustment);
actor.starty = lineStartY - actor.height / 2;
bounds.bumpVerticalPos(actor.height / 2);
@@ -746,7 +737,7 @@ function adjustCreatedDestroyedData(
else if (destroyedActors.get(msg.from) == index) {
const actor = actors.get(msg.from);
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);
}
actor.stopy = lineStartY - actor.height / 2;
@@ -756,9 +747,7 @@ function adjustCreatedDestroyedData(
else if (destroyedActors.get(msg.to) == index) {
const actor = actors.get(msg.to);
if (conf.mirrorActors) {
const adjustment = actorArray.includes(actor.type)
? ACTOR_TYPE_WIDTH / 2 + 3
: actor.width / 2 + 3;
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
receiverAdjustment(actor, adjustment);
}
actor.stopy = lineStartY - actor.height / 2;
@@ -1076,11 +1065,10 @@ export const draw = async function (_text: string, id: string, _version: string,
for (const box of bounds.models.boxes) {
box.height = bounds.getVerticalPos() - box.y;
bounds.insert(box.x, box.y, box.x + box.width, box.height);
const boxPadding = conf.boxMargin * 2;
box.startx = box.x - boxPadding;
box.starty = box.y - boxPadding * 0.25;
box.stopx = box.startx + box.width + 2 * boxPadding;
box.stopy = box.starty + box.height + boxPadding * 0.75;
box.startx = box.x;
box.starty = box.y;
box.stopx = box.startx + box.width;
box.stopy = box.starty + box.height;
box.stroke = 'rgb(0,0,0, 0.5)';
svgDraw.drawBox(diagram, box, conf);
}
@@ -1345,9 +1333,6 @@ async function calculateActorMargins(
return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0));
}, 0);
const standardBoxPadding = conf.boxMargin * 8;
totalWidth += standardBoxPadding;
totalWidth -= 2 * conf.boxTextMargin;
if (box.wrap) {
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);

View File

@@ -12,11 +12,6 @@ const getStyles = (options) =>
.actor-line {
stroke: ${options.actorLineColor};
}
.innerArc {
stroke-width: 1.5;
stroke-dasharray: none;
}
.messageLine0 {
stroke-width: 1.5;
@@ -120,7 +115,6 @@ const getStyles = (options) =>
fill: ${options.actorBkg};
stroke-width: 2px;
}
`;
export default getStyles;

View File

@@ -411,608 +411,6 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
return height;
};
/**
* Draws an actor in the diagram with the attached line
*
* @param {any} elem - The diagram we'll draw to.
* @param {any} actor - The actor to draw.
* @param {any} conf - DrawText implementation discriminator object
* @param {boolean} isFooter - If the actor is the footer one
*/
const drawActorTypeCollections = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + actor.height;
const boxplusLineGroup = elem.append('g').lower();
var g = boxplusLineGroup;
if (!isFooter) {
actorCnt++;
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
}
g.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line 200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
g = boxplusLineGroup.append('g');
actor.actorCnt = actorCnt;
if (actor.links != null) {
g.attr('id', 'root-' + actorCnt);
}
}
const rect = svgDrawCommon.getNoteRect();
var cssclass = 'actor';
if (actor.properties?.class) {
cssclass = actor.properties.class;
} else {
rect.fill = '#eaeaea';
}
if (isFooter) {
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssclass += ` ${TOP_ACTOR_CLASS}`;
}
rect.x = actor.x;
rect.y = actorY;
rect.width = actor.width;
rect.height = actor.height;
rect.class = cssclass;
// rect.rx = 3;
// rect.ry = 3;
rect.name = actor.name;
// 🔹 DRAW STACKED RECTANGLES
const offset = 6;
const shadowRect = {
...rect,
x: rect.x + offset,
y: rect.y + (isFooter ? +offset : -offset),
class: 'actor',
};
drawRect(g, shadowRect);
const rectElem = drawRect(g, rect); // draw main rectangle on top
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,
rect.y,
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', 15.5)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
.attr('orient', '180')
.append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 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('path')
.attr('d', `M ${cx},${cy - r}`)
.attr('stroke-width', 1.5)
.attr('marker-end', 'url(#filled-head-control)');
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 + 30,
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 + 25;
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) / 2 + r + 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.height * 0.8;
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 / 4 - 2 * ry})`);
} 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 / 2 : h / 2 + ry),
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';
rect.rx = 3;
rect.ry = 3;
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)
.attr('width', actor.width);
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 : radius / 2),
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})`);
}
// actElem.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`);
return actor.height;
};
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
@@ -1114,18 +512,6 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
return await drawActorTypeActor(elem, actor, conf, isFooter);
case 'participant':
return await drawActorTypeParticipant(elem, actor, conf, isFooter);
case 'boundary':
return await drawActorTypeBoundary(elem, actor, conf, isFooter);
case 'control':
return await drawActorTypeControl(elem, actor, conf, isFooter);
case 'entity':
return await drawActorTypeEntity(elem, actor, conf, isFooter);
case 'database':
return await drawActorTypeDatabase(elem, actor, conf, isFooter);
case 'collections':
return await drawActorTypeCollections(elem, actor, conf, isFooter);
case 'queue':
return await drawActorTypeQueue(elem, actor, conf, isFooter);
}
};

View File

@@ -0,0 +1,38 @@
import type { DiagramDB } from '../../diagram-api/types.js';
import type { UsecaseDiagramConfig, UsecaseNode } from './types.js';
import { cleanAndMerge } from '../../utils.js';
export class UsecaseDiagramDB implements DiagramDB {
public getNodes() {
return [];
}
public getConfig() {
return cleanAndMerge({}) as Required<UsecaseDiagramConfig>;
}
public addNode(node: UsecaseNode, level: number) {
if (level === 0) {
// TODO
}
}
public getRoot() {
return { name: '', children: [] };
}
public addClass(_id: string, _style: string) {
// TODO
}
public getClasses() {
// TODO
}
public getStylesForClass(_classSelector: string) {
// TODO
}
public clear() {
// commonClear();
}
}

View File

@@ -0,0 +1,22 @@
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';
const id = 'usecase';
const detector: DiagramDetector = (txt) => {
return /^\s*usecase/.test(txt);
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./diagram.js');
return { id, diagram };
};
export const usecase: ExternalDiagramDefinition = {
id,
detector,
loader,
};

View File

@@ -0,0 +1,14 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { UsecaseDiagramDB } from './db.js';
import { parser } from './parser.js';
import { renderer } from './renderer.js';
import styles from './styles.js';
export const diagram: DiagramDefinition = {
parser,
get db() {
return new UsecaseDiagramDB();
},
renderer,
styles,
};

View File

@@ -0,0 +1,40 @@
import { parse } from '@mermaid-js/parser';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import type { UsecaseAst } from './types.js';
import { UsecaseDiagramDB } from './db.js';
/**
* Populates the database with data from the Usecase AST
* @param ast - The Usecase AST
*/
const populate = (ast: UsecaseAst, db: UsecaseDiagramDB) => {
// We need to bypass the type checking for populateCommonDb
// eslint-disable-next-line @typescript-eslint/no-explicit-any
populateCommonDb(ast as any, db);
};
export const parser: ParserDefinition = {
// @ts-expect-error - UsecaseDB is not assignable to DiagramDB
parser: { yy: undefined },
parse: async (text: string): Promise<void> => {
try {
// Use a generic parse that accepts any diagram type
const parseFunc = parse as (diagramType: string, text: string) => Promise<UsecaseAst>;
const ast = await parseFunc('usecase', text);
log.debug('Usecase AST:', ast);
const db = parser.parser?.yy;
if (!(db instanceof UsecaseDiagramDB)) {
throw new Error(
'parser.parser?.yy was not a UsecaseDiagramDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
);
}
populate(ast, db);
} catch (error) {
log.error('Error parsing usecase:', error);
throw error;
}
},
};

View File

@@ -0,0 +1,21 @@
import type { Diagram } from '../../Diagram.js';
import type {
DiagramRenderer,
DiagramStyleClassDef,
DrawDefinition,
} from '../../diagram-api/types.js';
/**
* Draws the Usecase diagram
*/
const draw: DrawDefinition = (_text, _id, _version, _diagram: Diagram) => {
// TODO: Implement the draw function for the usecase diagram
};
const getClasses = function (
_text: string,
_diagramObj: Pick<Diagram, 'db'>
): Map<string, DiagramStyleClassDef> {
return new Map<string, DiagramStyleClassDef>();
};
export const renderer: DiagramRenderer = { draw, getClasses };

View File

@@ -0,0 +1,51 @@
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
import { cleanAndMerge } from '../../utils.js';
import type { UsecaseStyleOptions } from './types.js';
const defaultUsecaseStyleOptions: UsecaseStyleOptions = {
sectionStrokeColor: 'black',
sectionStrokeWidth: '1',
sectionFillColor: '#efefef',
leafStrokeColor: 'black',
leafStrokeWidth: '1',
leafFillColor: '#efefef',
labelColor: 'black',
labelFontSize: '12px',
valueFontSize: '10px',
valueColor: 'black',
titleColor: 'black',
titleFontSize: '14px',
};
export const getStyles: DiagramStylesProvider = ({
usecase,
}: { usecase?: UsecaseStyleOptions } = {}) => {
const options = cleanAndMerge(defaultUsecaseStyleOptions, usecase);
return `
.usecaseNode.section {
stroke: ${options.sectionStrokeColor};
stroke-width: ${options.sectionStrokeWidth};
fill: ${options.sectionFillColor};
}
.usecaseNode.leaf {
stroke: ${options.leafStrokeColor};
stroke-width: ${options.leafStrokeWidth};
fill: ${options.leafFillColor};
}
.usecaseLabel {
fill: ${options.labelColor};
font-size: ${options.labelFontSize};
}
.usecaseValue {
fill: ${options.valueColor};
font-size: ${options.valueFontSize};
}
.usecaseTitle {
fill: ${options.titleColor};
font-size: ${options.titleFontSize};
}
`;
};
export default getStyles;

View File

@@ -0,0 +1,68 @@
import type { DiagramDBBase, DiagramStyleClassDef } from '../../diagram-api/types.js';
import type { BaseDiagramConfig } from '../../config.type.js';
export interface UsecaseNode {
name: string;
children?: UsecaseNode[];
value?: number;
parent?: UsecaseNode;
classSelector?: string;
cssCompiledStyles?: string[];
}
export interface UsecaseDiagramDB extends DiagramDBBase<UsecaseDiagramConfig> {
getNodes: () => UsecaseNode[];
addNode: (node: UsecaseNode, level: number) => void;
getRoot: () => UsecaseNode | undefined;
getClasses: () => Map<string, DiagramStyleClassDef>;
addClass: (className: string, style: string) => void;
getStylesForClass: (classSelector: string) => string[];
// Update
}
export interface UsecaseStyleOptions {
sectionStrokeColor?: string;
sectionStrokeWidth?: string;
sectionFillColor?: string;
leafStrokeColor?: string;
leafStrokeWidth?: string;
leafFillColor?: string;
labelColor?: string;
labelFontSize?: string;
valueFontSize?: string;
valueColor?: string;
titleColor?: string;
titleFontSize?: string;
}
export interface UsecaseData {
nodes: UsecaseNode[];
levels: Map<UsecaseNode, number>;
root?: UsecaseNode;
outerNodes: UsecaseNode[];
}
export interface UsecaseItem {
$type: string;
name: string;
value?: number;
classSelector?: string;
}
export interface UsecaseAst {
title?: string;
description?: string;
}
// Define the UsecaseDiagramConfig interface
export interface UsecaseDiagramConfig extends BaseDiagramConfig {
padding?: number;
diagramPadding?: number;
showValues?: boolean;
nodeWidth?: number;
nodeHeight?: number;
borderWidth?: number;
valueFontSize?: number;
labelFontSize?: number;
valueFormat?: string;
}

View File

@@ -46,126 +46,6 @@ sequenceDiagram
Bob->>Alice: Hi Alice
```
### Boundary
If you want to use the boundary symbol for a participant, use the `boundary` statement as shown below.
```mermaid-example
sequenceDiagram
boundary theBoundary
participant Bob
theBoundary->>Bob: Request from boundary
Bob->>theBoundary: Response to boundary
```
```mermaid
sequenceDiagram
boundary theBoundary
participant Bob
theBoundary->>Bob: Request from boundary
Bob->>theBoundary: Response to boundary
```
### Control
If you want to use the control symbol for a participant, use the `control` statement as shown below.
```mermaid-example
sequenceDiagram
control theControl
participant Alice
theControl->>Alice: Control request
Alice->>theControl: Control response
```
```mermaid
sequenceDiagram
control theControl
participant Alice
theControl->>Alice: Control request
Alice->>theControl: Control response
```
### Entity
If you want to use the entity symbol for a participant, use the `entity` statement as shown below.
```mermaid-example
sequenceDiagram
entity theEntity
participant Bob
theEntity->>Bob: Entity request
Bob->>theEntity: Entity response
```
```mermaid
sequenceDiagram
entity theEntity
participant Bob
theEntity->>Bob: Entity request
Bob->>theEntity: Entity response
```
### Database
If you want to use the database symbol for a participant, use the `database` statement as shown below.
```mermaid-example
sequenceDiagram
database theDb
participant Alice
theDb->>Alice: DB query
Alice->>theDb: DB result
```
```mermaid
sequenceDiagram
database theDb
participant Alice
theDb->>Alice: DB query
Alice->>theDb: DB result
```
### Collections
If you want to use the collections symbol for a participant, use the `collections` statement as shown below.
```mermaid-example
sequenceDiagram
collections theCollection
participant Bob
theCollection->>Bob: Collections request
Bob->>theCollection: Collections response
```
```mermaid
sequenceDiagram
collections theCollection
participant Bob
theCollection->>Bob: Collections request
Bob->>theCollection: Collections response
```
### Queue
If you want to use the queue symbol for a participant, use the `queue` statement as shown below.
```mermaid-example
sequenceDiagram
queue theQueue
participant Alice
theQueue->>Alice: Queue message
Alice->>theQueue: Queue
```
```mermaid
sequenceDiagram
queue theQueue
participant Alice
theQueue->>Alice: Queue message
Alice->>theQueue: Queue
```
### Aliases
The actor can have a convenient identifier and a descriptive label.

View File

@@ -35,6 +35,11 @@
"id": "treemap",
"grammar": "src/language/treemap/treemap.langium",
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "usecase",
"grammar": "src/language/usecase/usecase.langium",
"fileExtensions": [".mmd", ".mermaid"]
}
],
"mode": "production",

View File

@@ -9,6 +9,7 @@ export {
GitGraph,
Radar,
Treemap,
Usecase,
Branch,
Commit,
Merge,
@@ -24,6 +25,7 @@ export {
isBranch,
isCommit,
isMerge,
isUsecase,
} from './generated/ast.js';
export {
@@ -35,6 +37,7 @@ export {
GitGraphGeneratedModule,
RadarGeneratedModule,
TreemapGeneratedModule,
UsecaseGeneratedModule,
} from './generated/module.js';
export * from './gitGraph/index.js';
@@ -45,3 +48,4 @@ export * from './pie/index.js';
export * from './architecture/index.js';
export * from './radar/index.js';
export * from './treemap/index.js';
export * from './usecase/index.js';

View File

@@ -0,0 +1 @@
export * from './module.js';

View File

@@ -0,0 +1,35 @@
import { type DefaultSharedCoreModuleContext, type LangiumCoreServices } from 'langium';
import type { Module, PartialLangiumCoreServices } from 'langium';
import { EmptyFileSystem } from 'langium';
import { UsecaseTokenBuilder } from './tokenBuilder.js';
import { UsecaseValueConverter } from './valueConverter.js';
import { UsecaseValidator } from './usecase-validator.js';
interface UsecaseAddedServices {
parser: {
TokenBuilder: UsecaseTokenBuilder;
ValueConverter: UsecaseValueConverter;
};
validation: {
UsecaseValidator: UsecaseValidator;
};
}
export type UsecaseServices = LangiumCoreServices & UsecaseAddedServices;
export const UsecaseModule: Module<
UsecaseServices,
PartialLangiumCoreServices & UsecaseAddedServices
> = {
parser: {
TokenBuilder: () => new UsecaseTokenBuilder(),
ValueConverter: () => new UsecaseValueConverter(),
},
validation: {
UsecaseValidator: () => new UsecaseValidator(),
},
};
export function createUsecaseServices(_context: DefaultSharedCoreModuleContext = EmptyFileSystem) {
// TODO
}

View File

@@ -0,0 +1,7 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class UsecaseTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['usecase']);
}
}

View File

@@ -0,0 +1,12 @@
import type { UsecaseServices } from './module.js';
/**
* Register custom validation checks.
*/
export function registerValidationChecks(_services: UsecaseServices) {
// TODO
}
/**
* Implementation of custom validations.
*/
export class UsecaseValidator {}

View File

@@ -0,0 +1 @@
// TODO

View File

@@ -0,0 +1,12 @@
import type { CstNode, GrammarAST, ValueType } from 'langium';
import { AbstractMermaidValueConverter } from '../common/index.js';
export class UsecaseValueConverter extends AbstractMermaidValueConverter {
protected runCustomConverter(
_rule: GrammarAST.AbstractRule,
_input: string,
_cstNode: CstNode
): ValueType | undefined {
return undefined;
}
}

View File

@@ -1,6 +1,15 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { Info, Packet, Pie, Architecture, GitGraph, Radar, Treemap } from './index.js';
import type {
Info,
Packet,
Pie,
Architecture,
GitGraph,
Radar,
Treemap,
// Usecase,
} from './index.js';
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
@@ -41,6 +50,11 @@ const initializers = {
const parser = createTreemapServices().Treemap.parser.LangiumParser;
parsers.treemap = parser;
},
// usecase: async () => {
// const { createUsecaseServices } = await import('./language/usecase/index.js');
// const parser = createUsecaseServices().Usecase.parser.LangiumParser;
// parsers.usecase = parser;
// },
} as const;
export async function parse(diagramType: 'info', text: string): Promise<Info>;
@@ -50,6 +64,7 @@ export async function parse(diagramType: 'architecture', text: string): Promise<
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
export async function parse(diagramType: 'radar', text: string): Promise<Radar>;
export async function parse(diagramType: 'treemap', text: string): Promise<Treemap>;
// export async function parse(diagramType: 'usecase', text: string): Promise<Usecase>;
export async function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,