mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-17 03:04:07 +01:00
feat: Adding support for the new Usecase diagram type
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
This commit is contained in:
@@ -28,6 +28,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
|||||||
'packet',
|
'packet',
|
||||||
'architecture',
|
'architecture',
|
||||||
'radar',
|
'radar',
|
||||||
|
'usecase',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ typeof
|
|||||||
typestr
|
typestr
|
||||||
unshift
|
unshift
|
||||||
urlsafe
|
urlsafe
|
||||||
|
usecase
|
||||||
|
Usecase
|
||||||
|
USECASE
|
||||||
verifymethod
|
verifymethod
|
||||||
VERIFYMTHD
|
VERIFYMTHD
|
||||||
WARN_DOCSDIR_DOESNT_MATCH
|
WARN_DOCSDIR_DOESNT_MATCH
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -50,5 +50,8 @@ demos/dev/**
|
|||||||
tsx-0/**
|
tsx-0/**
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
# autogenereated by langium-cli
|
# autogenereated by langium-cli and antlr-cli
|
||||||
generated/
|
generated/
|
||||||
|
|
||||||
|
# autogenereated by antlr-cli
|
||||||
|
.antlr/
|
||||||
|
|||||||
425
cypress/integration/rendering/usecase.spec.ts
Normal file
425
cypress/integration/rendering/usecase.spec.ts
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
|
||||||
|
|
||||||
|
describe('Usecase diagram', () => {
|
||||||
|
it('should render a simple usecase diagram with actors and use cases', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
usecase
|
||||||
|
actor User
|
||||||
|
actor Admin
|
||||||
|
User --> Login
|
||||||
|
Admin --> "Manage Users"
|
||||||
|
User --> "View Profile"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with quoted actor names', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor "Customer Service"
|
||||||
|
actor "System Administrator"
|
||||||
|
"Customer Service" --> "Handle Tickets"
|
||||||
|
"System Administrator" --> "Manage System"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with different arrow types', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
actor Admin
|
||||||
|
User --> Login
|
||||||
|
Admin <-- Logout
|
||||||
|
User -- "View Data"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with edge labels', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor Developer
|
||||||
|
actor Manager
|
||||||
|
Developer --important--> "Write Code"
|
||||||
|
Manager --review--> "Code Review"
|
||||||
|
Developer --urgent--> Manager
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with node ID syntax', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
User --> a(Login)
|
||||||
|
User --> b("View Profile")
|
||||||
|
User --> c("Update Settings")
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with comma-separated actors', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor "Customer Service", "Technical Support", "Sales Team"
|
||||||
|
actor SystemAdmin
|
||||||
|
"Customer Service" --> "Handle Tickets"
|
||||||
|
"Technical Support" --> "Resolve Issues"
|
||||||
|
"Sales Team" --> "Process Orders"
|
||||||
|
SystemAdmin --> "Manage System"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with actor metadata', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User@{ "type" : "primary", "icon" : "user" }
|
||||||
|
actor Admin@{ "type" : "secondary", "icon" : "admin" }
|
||||||
|
actor System@{ "type" : "hollow", "icon" : "system" }
|
||||||
|
User --> Login
|
||||||
|
Admin --> "Manage Users"
|
||||||
|
System --> "Process Data"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with system boundaries (rect type)', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor Admin, User
|
||||||
|
systemBoundary "Authentication"
|
||||||
|
Login
|
||||||
|
Logout
|
||||||
|
"Reset Password"
|
||||||
|
end
|
||||||
|
"Authentication"@{ type: rect }
|
||||||
|
Admin --> Login
|
||||||
|
User --> Login
|
||||||
|
User --> "Reset Password"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with system boundaries (package type)', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor Admin, User
|
||||||
|
systemBoundary "Authentication"
|
||||||
|
Login
|
||||||
|
Logout
|
||||||
|
"Reset Password"
|
||||||
|
end
|
||||||
|
"Authentication"@{ type: package }
|
||||||
|
Admin --> Login
|
||||||
|
User --> Login
|
||||||
|
User --> "Reset Password"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render complex usecase diagram with all features', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor "Customer Service"@{ "type" : "primary", "icon" : "user" }
|
||||||
|
actor "System Admin"@{ "type" : "secondary", "icon" : "admin" }
|
||||||
|
actor "Database"@{ "type" : "hollow", "icon" : "database" }
|
||||||
|
|
||||||
|
systemBoundary "Customer Support System"
|
||||||
|
"Handle Tickets"
|
||||||
|
"View Customer Info"
|
||||||
|
end
|
||||||
|
"Customer Support System"@{ type: package }
|
||||||
|
|
||||||
|
systemBoundary "Administration"
|
||||||
|
"User Management"
|
||||||
|
"System Config"
|
||||||
|
end
|
||||||
|
|
||||||
|
"Customer Service" --priority--> "Handle Tickets"
|
||||||
|
"Customer Service" --> "View Customer Info"
|
||||||
|
"System Admin" --manage--> "User Management"
|
||||||
|
"System Admin" --> "System Config"
|
||||||
|
"Database" <-- "Handle Tickets"
|
||||||
|
"Database" <-- "View Customer Info"
|
||||||
|
"Database" <-- "User Management"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with actor-to-actor relationships', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor Manager
|
||||||
|
actor Developer
|
||||||
|
actor Tester
|
||||||
|
|
||||||
|
Manager --supervises--> Developer
|
||||||
|
Manager --coordinates--> Tester
|
||||||
|
Developer --collaborates--> Tester
|
||||||
|
|
||||||
|
Developer --> "Write Code"
|
||||||
|
Tester --> "Test Code"
|
||||||
|
Manager --> "Review Progress"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with mixed relationship types', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
actor Admin
|
||||||
|
|
||||||
|
User --> "Basic Login"
|
||||||
|
Admin --> "Advanced Login"
|
||||||
|
User --includes--> "View Profile"
|
||||||
|
Admin --extends--> "Manage Profiles"
|
||||||
|
|
||||||
|
"Basic Login" <-- "Advanced Login"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with long labels and text wrapping', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor "Customer Service Representative"
|
||||||
|
actor "System Administrator with Extended Privileges"
|
||||||
|
|
||||||
|
"Customer Service Representative" --Process--> "Handle Complex Customer Support Tickets"
|
||||||
|
"System Administrator with Extended Privileges" --> "Manage System Configuration and User Permissions"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with special characters in names', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor "User@Company.com"
|
||||||
|
actor "Admin (Level-1)"
|
||||||
|
"User@Company.com" --> a("Login & Authenticate")
|
||||||
|
"Admin (Level-1)" --> b("Manage Users & Permissions")
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram when useMaxWidth is true (default)', () => {
|
||||||
|
renderGraph(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
actor Admin
|
||||||
|
User --> Login
|
||||||
|
Admin --> "Manage System"
|
||||||
|
User --> "View Profile"
|
||||||
|
`,
|
||||||
|
{ usecase: { 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;$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram when useMaxWidth is false', () => {
|
||||||
|
renderGraph(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
actor Admin
|
||||||
|
User --> Login
|
||||||
|
Admin --> "Manage System"
|
||||||
|
`,
|
||||||
|
{ usecase: { useMaxWidth: false } }
|
||||||
|
);
|
||||||
|
cy.get('svg').should((svg) => {
|
||||||
|
const width = parseFloat(svg.attr('width'));
|
||||||
|
expect(width).to.be.greaterThan(200);
|
||||||
|
expect(svg).to.not.have.attr('style');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render empty usecase diagram', () => {
|
||||||
|
imgSnapshotTest(`usecase`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with only actors', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
actor Admin
|
||||||
|
actor Guest
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with implicit use case creation', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
User --> Login
|
||||||
|
User --> Register
|
||||||
|
User --> "Forgot Password"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with nested system boundaries', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
actor Admin
|
||||||
|
|
||||||
|
systemBoundary "Main System"
|
||||||
|
Login
|
||||||
|
Logout
|
||||||
|
"Create User"
|
||||||
|
"Delete User"
|
||||||
|
end
|
||||||
|
|
||||||
|
User --> Login
|
||||||
|
User --> Logout
|
||||||
|
Admin --> "Create User"
|
||||||
|
Admin --> "Delete User"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with multiple edge labels on same relationship', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor Developer
|
||||||
|
actor Manager
|
||||||
|
|
||||||
|
Developer --"code review"--> Manager
|
||||||
|
Developer --"status update"--> Manager
|
||||||
|
Manager --"feedback"--> Developer
|
||||||
|
Manager --"approval"--> Developer
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with various actor icon types', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User@{ "icon": "user" }
|
||||||
|
actor Admin@{ "icon": "admin" }
|
||||||
|
actor Database@{ "icon": "database" }
|
||||||
|
actor API@{ "icon": "api" }
|
||||||
|
actor Mobile@{ "icon": "mobile" }
|
||||||
|
actor Web@{ "icon": "web" }
|
||||||
|
|
||||||
|
User --> "Access System"
|
||||||
|
Admin --> "Manage System"
|
||||||
|
Database --> "Store Data"
|
||||||
|
API --> "Provide Services"
|
||||||
|
Mobile --> "Mobile Access"
|
||||||
|
Web --> "Web Access"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with mixed arrow directions and labels', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
actor System
|
||||||
|
actor Admin
|
||||||
|
|
||||||
|
User --request--> System
|
||||||
|
System --response--> User
|
||||||
|
System <--monitor-- Admin
|
||||||
|
Admin --configure--> System
|
||||||
|
User -- "direct access" -- Admin
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with boundary-less use cases', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User
|
||||||
|
actor Admin
|
||||||
|
|
||||||
|
systemBoundary "Secure Area"
|
||||||
|
"Admin Panel"
|
||||||
|
"User Management"
|
||||||
|
end
|
||||||
|
|
||||||
|
User --> "Public Login"
|
||||||
|
User --> "Guest Access"
|
||||||
|
Admin --> "Public Login"
|
||||||
|
Admin --> "Admin Panel"
|
||||||
|
Admin --> "User Management"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with complex metadata combinations', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor "Primary User"@{ "type": "primary", "icon": "user", "fillColor": "lightblue" }
|
||||||
|
actor "Secondary User"@{ "type": "secondary", "icon": "client", "strokeColor": "red" }
|
||||||
|
actor "System Service"@{ "type": "hollow", "icon": "service", "strokeWidth": "3" }
|
||||||
|
|
||||||
|
"Primary User" --"high priority"--> a("Critical Process")
|
||||||
|
"Secondary User" --"low priority"--> b("Background Task")
|
||||||
|
"System Service" --"automated"--> c("System Maintenance")
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render usecase diagram with Unicode characters', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor "用户"@{ "icon": "user" }
|
||||||
|
actor "管理员"@{ "icon": "admin" }
|
||||||
|
|
||||||
|
"用户" --"登录"--> "系统访问"
|
||||||
|
"管理员" --"管理"--> "用户管理"
|
||||||
|
"用户" --> "数据查看"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render large usecase diagram with many elements', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`usecase
|
||||||
|
actor User1, User2, User3, User4
|
||||||
|
actor Admin1, Admin2
|
||||||
|
actor System1@{ "icon": "system" }
|
||||||
|
actor System2@{ "icon": "database" }
|
||||||
|
|
||||||
|
systemBoundary "Module A"
|
||||||
|
"Feature A1"
|
||||||
|
"Feature A2"
|
||||||
|
"Admin A1"
|
||||||
|
end
|
||||||
|
"Module A"@{ type: package }
|
||||||
|
|
||||||
|
systemBoundary "Module B"
|
||||||
|
"Feature B1"
|
||||||
|
"Feature B2"
|
||||||
|
"Admin B1"
|
||||||
|
end
|
||||||
|
|
||||||
|
User1 --> "Feature A1"
|
||||||
|
User2 --> "Feature A2"
|
||||||
|
Admin1 --> "Admin A1"
|
||||||
|
User3 --> "Feature B1"
|
||||||
|
User4 --> "Feature B2"
|
||||||
|
Admin2 --> "Admin B1"
|
||||||
|
|
||||||
|
System1 <-- "Feature A1"
|
||||||
|
System1 <-- "Feature B1"
|
||||||
|
System2 <-- "Admin A1"
|
||||||
|
System2 <-- "Admin B1"
|
||||||
|
|
||||||
|
User1 --"collaborates"--> User2
|
||||||
|
Admin1 --"supervises"--> Admin2
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
> `const` **configKeys**: `Set`<`string`>
|
> `const` **configKeys**: `Set`<`string`>
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/defaultConfig.ts:292](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L292)
|
Defined in: [packages/mermaid/src/defaultConfig.ts:295](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L295)
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ You can set this attribute to base the seed on a static string.
|
|||||||
|
|
||||||
> `optional` **dompurifyConfig**: `Config`
|
> `optional` **dompurifyConfig**: `Config`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
|
Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
|
|||||||
|
|
||||||
> `optional` **fontSize**: `number`
|
> `optional` **fontSize**: `number`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
|
Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -292,7 +292,7 @@ Defines which main look to use for the diagram.
|
|||||||
|
|
||||||
> `optional` **markdownAutoWrap**: `boolean`
|
> `optional` **markdownAutoWrap**: `boolean`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
|
Defined in: [packages/mermaid/src/config.type.ts:217](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L217)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -424,7 +424,7 @@ Defined in: [packages/mermaid/src/config.type.ts:198](https://github.com/mermaid
|
|||||||
|
|
||||||
> `optional` **suppressErrorRendering**: `boolean`
|
> `optional` **suppressErrorRendering**: `boolean`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L222)
|
Defined in: [packages/mermaid/src/config.type.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223)
|
||||||
|
|
||||||
Suppresses inserting 'Syntax error' diagram in the DOM.
|
Suppresses inserting 'Syntax error' diagram in the DOM.
|
||||||
This is useful when you want to control how to handle syntax errors in your application.
|
This is useful when you want to control how to handle syntax errors in your application.
|
||||||
@@ -466,11 +466,19 @@ Defined in: [packages/mermaid/src/config.type.ts:196](https://github.com/mermaid
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### usecase?
|
||||||
|
|
||||||
|
> `optional` **usecase**: `UsecaseDiagramConfig`
|
||||||
|
|
||||||
|
Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### wrap?
|
### wrap?
|
||||||
|
|
||||||
> `optional` **wrap**: `boolean`
|
> `optional` **wrap**: `boolean`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
|
Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ export interface MermaidConfig {
|
|||||||
packet?: PacketDiagramConfig;
|
packet?: PacketDiagramConfig;
|
||||||
block?: BlockDiagramConfig;
|
block?: BlockDiagramConfig;
|
||||||
radar?: RadarDiagramConfig;
|
radar?: RadarDiagramConfig;
|
||||||
|
usecase?: UsecaseDiagramConfig;
|
||||||
dompurifyConfig?: DOMPurifyConfiguration;
|
dompurifyConfig?: DOMPurifyConfiguration;
|
||||||
wrap?: boolean;
|
wrap?: boolean;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
@@ -1619,6 +1620,50 @@ export interface RadarDiagramConfig extends BaseDiagramConfig {
|
|||||||
*/
|
*/
|
||||||
curveTension?: number;
|
curveTension?: number;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* The object containing configurations specific for usecase diagrams.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||||
|
* via the `definition` "UsecaseDiagramConfig".
|
||||||
|
*/
|
||||||
|
export interface UsecaseDiagramConfig extends BaseDiagramConfig {
|
||||||
|
/**
|
||||||
|
* Font size for actor labels
|
||||||
|
*/
|
||||||
|
actorFontSize?: number;
|
||||||
|
/**
|
||||||
|
* Font family for actor labels
|
||||||
|
*/
|
||||||
|
actorFontFamily?: string;
|
||||||
|
/**
|
||||||
|
* Font weight for actor labels
|
||||||
|
*/
|
||||||
|
actorFontWeight?: string;
|
||||||
|
/**
|
||||||
|
* Font size for usecase labels
|
||||||
|
*/
|
||||||
|
usecaseFontSize?: number;
|
||||||
|
/**
|
||||||
|
* Font family for usecase labels
|
||||||
|
*/
|
||||||
|
usecaseFontFamily?: string;
|
||||||
|
/**
|
||||||
|
* Font weight for usecase labels
|
||||||
|
*/
|
||||||
|
usecaseFontWeight?: string;
|
||||||
|
/**
|
||||||
|
* Margin around actors
|
||||||
|
*/
|
||||||
|
actorMargin?: number;
|
||||||
|
/**
|
||||||
|
* Margin around use cases
|
||||||
|
*/
|
||||||
|
usecaseMargin?: number;
|
||||||
|
/**
|
||||||
|
* Padding around the entire diagram
|
||||||
|
*/
|
||||||
|
diagramPadding?: number;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||||
* via the `definition` "FontConfig".
|
* via the `definition` "FontConfig".
|
||||||
|
|||||||
@@ -264,6 +264,9 @@ const config: RequiredDeep<MermaidConfig> = {
|
|||||||
radar: {
|
radar: {
|
||||||
...defaultConfigJson.radar,
|
...defaultConfigJson.radar,
|
||||||
},
|
},
|
||||||
|
usecase: {
|
||||||
|
...defaultConfigJson.usecase,
|
||||||
|
},
|
||||||
treemap: {
|
treemap: {
|
||||||
useMaxWidth: true,
|
useMaxWidth: true,
|
||||||
padding: 10,
|
padding: 10,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
|||||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||||
import { registerDiagram } from './diagramAPI.js';
|
import { registerDiagram } from './diagramAPI.js';
|
||||||
import { treemap } from '../diagrams/treemap/detector.js';
|
import { treemap } from '../diagrams/treemap/detector.js';
|
||||||
|
import { usecase } from '../diagrams/usecase/usecaseDetector.js';
|
||||||
import '../type.d.ts';
|
import '../type.d.ts';
|
||||||
|
|
||||||
let hasLoadedDiagrams = false;
|
let hasLoadedDiagrams = false;
|
||||||
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
|
|||||||
xychart,
|
xychart,
|
||||||
block,
|
block,
|
||||||
radar,
|
radar,
|
||||||
treemap
|
treemap,
|
||||||
|
usecase
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
40
packages/mermaid/src/diagrams/usecase/styles.ts
Normal file
40
packages/mermaid/src/diagrams/usecase/styles.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const getStyles = (options: any) =>
|
||||||
|
`
|
||||||
|
.actor {
|
||||||
|
stroke: ${options.primaryColor};
|
||||||
|
fill: ${options.primaryColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor-label {
|
||||||
|
fill: ${options.primaryTextColor};
|
||||||
|
font-family: ${options.fontFamily};
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usecase {
|
||||||
|
stroke: ${options.primaryColor};
|
||||||
|
fill: ${options.primaryColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
.usecase-label {
|
||||||
|
fill: ${options.primaryTextColor};
|
||||||
|
font-family: ${options.fontFamily};
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship {
|
||||||
|
stroke: ${options.primaryColor};
|
||||||
|
fill: ${options.primaryColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-label {
|
||||||
|
fill: ${options.primaryTextColor};
|
||||||
|
font-family: ${options.fontFamily};
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default getStyles;
|
||||||
112
packages/mermaid/src/diagrams/usecase/usecase.spec.ts
Normal file
112
packages/mermaid/src/diagrams/usecase/usecase.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
|
import { setSiteConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
|
import mermaidAPI from '../../mermaidAPI.js';
|
||||||
|
import { Diagram } from '../../Diagram.js';
|
||||||
|
import { addDiagrams } from '../../diagram-api/diagram-orchestration.js';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Is required to load the useCase diagram
|
||||||
|
await Diagram.fromText('usecase\n actor TestActor');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UseCase diagrams require a basic d3 mock for rendering
|
||||||
|
*/
|
||||||
|
vi.mock('d3', () => {
|
||||||
|
const NewD3 = function (this: any) {
|
||||||
|
function returnThis(this: any) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
append: function () {
|
||||||
|
return NewD3();
|
||||||
|
},
|
||||||
|
lower: returnThis,
|
||||||
|
attr: returnThis,
|
||||||
|
style: returnThis,
|
||||||
|
text: returnThis,
|
||||||
|
getBBox: function () {
|
||||||
|
return {
|
||||||
|
height: 10,
|
||||||
|
width: 20,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
select: function () {
|
||||||
|
return new (NewD3 as any)();
|
||||||
|
},
|
||||||
|
|
||||||
|
selectAll: function () {
|
||||||
|
return new (NewD3 as any)();
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: In d3 these are CurveFactory types, not strings
|
||||||
|
curveBasis: 'basis',
|
||||||
|
curveBasisClosed: 'basisClosed',
|
||||||
|
curveBasisOpen: 'basisOpen',
|
||||||
|
curveBumpX: 'bumpX',
|
||||||
|
curveBumpY: 'bumpY',
|
||||||
|
curveBundle: 'bundle',
|
||||||
|
curveCardinalClosed: 'cardinalClosed',
|
||||||
|
curveCardinalOpen: 'cardinalOpen',
|
||||||
|
curveCardinal: 'cardinal',
|
||||||
|
curveCatmullRomClosed: 'catmullRomClosed',
|
||||||
|
curveCatmullRomOpen: 'catmullRomOpen',
|
||||||
|
curveCatmullRom: 'catmullRom',
|
||||||
|
curveLinear: 'linear',
|
||||||
|
curveLinearClosed: 'linearClosed',
|
||||||
|
curveMonotoneX: 'monotoneX',
|
||||||
|
curveMonotoneY: 'monotoneY',
|
||||||
|
curveNatural: 'natural',
|
||||||
|
curveStep: 'step',
|
||||||
|
curveStepAfter: 'stepAfter',
|
||||||
|
curveStepBefore: 'stepBefore',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// -------------------------------
|
||||||
|
|
||||||
|
addDiagrams();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param conf - Configuration object
|
||||||
|
* @param key - Configuration key
|
||||||
|
* @param value - Configuration value
|
||||||
|
*/
|
||||||
|
function addConf(conf: any, key: any, value: any) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
conf[key] = value;
|
||||||
|
}
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('UseCase diagram with ANTLR parser', () => {
|
||||||
|
it('should parse actors and use cases correctly', async () => {
|
||||||
|
const diagram = await Diagram.fromText(`
|
||||||
|
usecase
|
||||||
|
actor Developer1
|
||||||
|
actor Developer2
|
||||||
|
usecase "Login System"
|
||||||
|
usecase Authentication
|
||||||
|
Developer1 --> "Login System"
|
||||||
|
Developer2 --> Authentication
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(diagram).toBeDefined();
|
||||||
|
expect(diagram.type).toBe('usecase');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle simple usecase diagram', async () => {
|
||||||
|
const diagram = await Diagram.fromText(`
|
||||||
|
usecase
|
||||||
|
actor User
|
||||||
|
usecase "View Profile"
|
||||||
|
User --> "View Profile"
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(diagram).toBeDefined();
|
||||||
|
expect(diagram.type).toBe('usecase');
|
||||||
|
});
|
||||||
|
});
|
||||||
175
packages/mermaid/src/diagrams/usecase/usecaseDb.ts
Normal file
175
packages/mermaid/src/diagrams/usecase/usecaseDb.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { log } from '../../logger.js';
|
||||||
|
import {
|
||||||
|
setAccTitle,
|
||||||
|
getAccTitle,
|
||||||
|
setDiagramTitle,
|
||||||
|
getDiagramTitle,
|
||||||
|
getAccDescription,
|
||||||
|
setAccDescription,
|
||||||
|
clear as commonClear,
|
||||||
|
} from '../common/commonDb.js';
|
||||||
|
import type {
|
||||||
|
UsecaseFields,
|
||||||
|
UsecaseDB,
|
||||||
|
Actor,
|
||||||
|
UseCase,
|
||||||
|
SystemBoundary,
|
||||||
|
Relationship,
|
||||||
|
} from './usecaseTypes.js';
|
||||||
|
import type { RequiredDeep } from 'type-fest';
|
||||||
|
import type { UsecaseDiagramConfig } from '../../config.type.js';
|
||||||
|
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||||
|
|
||||||
|
export const DEFAULT_USECASE_CONFIG: Required<UsecaseDiagramConfig> = DEFAULT_CONFIG.usecase;
|
||||||
|
|
||||||
|
export const DEFAULT_USECASE_DB: RequiredDeep<UsecaseFields> = {
|
||||||
|
actors: new Map(),
|
||||||
|
useCases: new Map(),
|
||||||
|
systemBoundaries: new Map(),
|
||||||
|
relationships: [],
|
||||||
|
config: DEFAULT_USECASE_CONFIG,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
let actors = new Map<string, Actor>();
|
||||||
|
let useCases = new Map<string, UseCase>();
|
||||||
|
let systemBoundaries = new Map<string, SystemBoundary>();
|
||||||
|
let relationships: Relationship[] = [];
|
||||||
|
const config: Required<UsecaseDiagramConfig> = structuredClone(DEFAULT_USECASE_CONFIG);
|
||||||
|
|
||||||
|
const getConfig = (): Required<UsecaseDiagramConfig> => structuredClone(config);
|
||||||
|
|
||||||
|
const clear = (): void => {
|
||||||
|
actors = new Map();
|
||||||
|
useCases = new Map();
|
||||||
|
systemBoundaries = new Map();
|
||||||
|
relationships = [];
|
||||||
|
commonClear();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actor management
|
||||||
|
const addActor = (actor: Actor): void => {
|
||||||
|
if (!actor.id || !actor.name) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid actor: Actor must have both id and name. Received: ${JSON.stringify(actor)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actors.has(actor.id)) {
|
||||||
|
actors.set(actor.id, actor);
|
||||||
|
log.debug(`Added actor: ${actor.id} (${actor.name})`);
|
||||||
|
} else {
|
||||||
|
log.debug(`Actor ${actor.id} already exists`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActors = (): Map<string, Actor> => actors;
|
||||||
|
|
||||||
|
const getActor = (id: string): Actor | undefined => actors.get(id);
|
||||||
|
|
||||||
|
// UseCase management
|
||||||
|
const addUseCase = (useCase: UseCase): void => {
|
||||||
|
if (!useCase.id || !useCase.name) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid use case: Use case must have both id and name. Received: ${JSON.stringify(useCase)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useCases.has(useCase.id)) {
|
||||||
|
useCases.set(useCase.id, useCase);
|
||||||
|
log.debug(`Added use case: ${useCase.id} (${useCase.name})`);
|
||||||
|
} else {
|
||||||
|
log.debug(`Use case ${useCase.id} already exists`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUseCases = (): Map<string, UseCase> => useCases;
|
||||||
|
|
||||||
|
const getUseCase = (id: string): UseCase | undefined => useCases.get(id);
|
||||||
|
|
||||||
|
// SystemBoundary management
|
||||||
|
const addSystemBoundary = (systemBoundary: SystemBoundary): void => {
|
||||||
|
if (!systemBoundary.id || !systemBoundary.name) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid system boundary: System boundary must have both id and name. Received: ${JSON.stringify(systemBoundary)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!systemBoundaries.has(systemBoundary.id)) {
|
||||||
|
systemBoundaries.set(systemBoundary.id, systemBoundary);
|
||||||
|
log.debug(`Added system boundary: ${systemBoundary.name}`);
|
||||||
|
} else {
|
||||||
|
log.debug(`System boundary ${systemBoundary.id} already exists`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSystemBoundaries = (): Map<string, SystemBoundary> => systemBoundaries;
|
||||||
|
|
||||||
|
const getSystemBoundary = (id: string): SystemBoundary | undefined => systemBoundaries.get(id);
|
||||||
|
|
||||||
|
// Relationship management
|
||||||
|
const addRelationship = (relationship: Relationship): void => {
|
||||||
|
// Validate relationship structure
|
||||||
|
if (!relationship.id || !relationship.from || !relationship.to) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship: Relationship must have id, from, and to fields. Received: ${JSON.stringify(relationship)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relationship.type) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship: Relationship must have a type. Received: ${JSON.stringify(relationship)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate relationship type
|
||||||
|
const validTypes = ['association', 'include', 'extend'];
|
||||||
|
if (!validTypes.includes(relationship.type)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship type: ${relationship.type}. Valid types are: ${validTypes.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arrow type if present
|
||||||
|
if (relationship.arrowType !== undefined) {
|
||||||
|
const validArrowTypes = [0, 1, 2]; // SOLID_ARROW, BACK_ARROW, LINE_SOLID
|
||||||
|
if (!validArrowTypes.includes(relationship.arrowType)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid arrow type: ${relationship.arrowType}. Valid arrow types are: ${validArrowTypes.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relationships.push(relationship);
|
||||||
|
log.debug(
|
||||||
|
`Added relationship: ${relationship.from} -> ${relationship.to} (${relationship.type})`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelationships = (): Relationship[] => relationships;
|
||||||
|
|
||||||
|
export const db: UsecaseDB = {
|
||||||
|
getConfig,
|
||||||
|
|
||||||
|
clear,
|
||||||
|
setDiagramTitle,
|
||||||
|
getDiagramTitle,
|
||||||
|
setAccTitle,
|
||||||
|
getAccTitle,
|
||||||
|
setAccDescription,
|
||||||
|
getAccDescription,
|
||||||
|
|
||||||
|
addActor,
|
||||||
|
getActors,
|
||||||
|
getActor,
|
||||||
|
|
||||||
|
addUseCase,
|
||||||
|
getUseCases,
|
||||||
|
getUseCase,
|
||||||
|
|
||||||
|
addSystemBoundary,
|
||||||
|
getSystemBoundaries,
|
||||||
|
getSystemBoundary,
|
||||||
|
|
||||||
|
addRelationship,
|
||||||
|
getRelationships,
|
||||||
|
};
|
||||||
22
packages/mermaid/src/diagrams/usecase/usecaseDetector.ts
Normal file
22
packages/mermaid/src/diagrams/usecase/usecaseDetector.ts
Normal 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('./usecaseDiagram.js');
|
||||||
|
return { id, diagram };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usecase: ExternalDiagramDefinition = {
|
||||||
|
id,
|
||||||
|
detector,
|
||||||
|
loader,
|
||||||
|
};
|
||||||
12
packages/mermaid/src/diagrams/usecase/usecaseDiagram.ts
Normal file
12
packages/mermaid/src/diagrams/usecase/usecaseDiagram.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
|
import { parser } from './usecaseParser.js';
|
||||||
|
import { db } from './usecaseDb.js';
|
||||||
|
import { renderer } from './usecaseRenderer.js';
|
||||||
|
import styles from './styles.js';
|
||||||
|
|
||||||
|
export const diagram: DiagramDefinition = {
|
||||||
|
parser,
|
||||||
|
db,
|
||||||
|
renderer,
|
||||||
|
styles,
|
||||||
|
};
|
||||||
150
packages/mermaid/src/diagrams/usecase/usecaseParser.ts
Normal file
150
packages/mermaid/src/diagrams/usecase/usecaseParser.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// Import ANTLR parser from the parser package
|
||||||
|
import { parse } from '@mermaid-js/parser';
|
||||||
|
import { log } from '../../logger.js';
|
||||||
|
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||||
|
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||||
|
import type {
|
||||||
|
UsecaseDB,
|
||||||
|
Actor,
|
||||||
|
UseCase,
|
||||||
|
SystemBoundary,
|
||||||
|
Relationship,
|
||||||
|
ArrowType,
|
||||||
|
} from './usecaseTypes.js';
|
||||||
|
import { db } from './usecaseDb.js';
|
||||||
|
|
||||||
|
// ANTLR parser result interface
|
||||||
|
interface UsecaseParseResult {
|
||||||
|
actors: { id: string; name: string; metadata?: Record<string, string> }[];
|
||||||
|
useCases: { id: string; name: string; nodeId?: string; systemBoundary?: string }[];
|
||||||
|
systemBoundaries: { id: string; name: string; useCases: string[]; type?: 'package' | 'rect' }[];
|
||||||
|
relationships: {
|
||||||
|
id: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
type: 'association' | 'include' | 'extend';
|
||||||
|
arrowType: number;
|
||||||
|
label?: string;
|
||||||
|
}[];
|
||||||
|
accDescr?: string;
|
||||||
|
accTitle?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse usecase diagram using ANTLR parser
|
||||||
|
*/
|
||||||
|
const parseUsecaseWithAntlr = async (input: string): Promise<UsecaseParseResult> => {
|
||||||
|
// Use the ANTLR parser from @mermaid-js/parser
|
||||||
|
const result = (await parse('usecase', input)) as UsecaseParseResult;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate the database with parsed ANTLR results
|
||||||
|
*/
|
||||||
|
const populateDb = (ast: UsecaseParseResult, db: UsecaseDB) => {
|
||||||
|
// Clear existing data
|
||||||
|
db.clear();
|
||||||
|
|
||||||
|
// Add actors (ANTLR result already has id, name, and metadata)
|
||||||
|
ast.actors.forEach((actorData) => {
|
||||||
|
const actor: Actor = {
|
||||||
|
id: actorData.id,
|
||||||
|
name: actorData.name,
|
||||||
|
metadata: actorData.metadata,
|
||||||
|
};
|
||||||
|
db.addActor(actor);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add use cases (ANTLR result already has id, name, nodeId, and systemBoundary)
|
||||||
|
ast.useCases.forEach((useCaseData) => {
|
||||||
|
const useCase: UseCase = {
|
||||||
|
id: useCaseData.id,
|
||||||
|
name: useCaseData.name,
|
||||||
|
nodeId: useCaseData.nodeId,
|
||||||
|
systemBoundary: useCaseData.systemBoundary,
|
||||||
|
};
|
||||||
|
db.addUseCase(useCase);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add system boundaries
|
||||||
|
if (ast.systemBoundaries) {
|
||||||
|
ast.systemBoundaries.forEach((boundaryData) => {
|
||||||
|
const systemBoundary: SystemBoundary = {
|
||||||
|
id: boundaryData.id,
|
||||||
|
name: boundaryData.name,
|
||||||
|
useCases: boundaryData.useCases,
|
||||||
|
type: boundaryData.type || 'rect', // default to 'rect' if not specified
|
||||||
|
};
|
||||||
|
db.addSystemBoundary(systemBoundary);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add relationships (ANTLR result already has proper structure)
|
||||||
|
ast.relationships.forEach((relationshipData) => {
|
||||||
|
const relationship: Relationship = {
|
||||||
|
id: relationshipData.id,
|
||||||
|
from: relationshipData.from,
|
||||||
|
to: relationshipData.to,
|
||||||
|
type: relationshipData.type,
|
||||||
|
arrowType: relationshipData.arrowType as ArrowType,
|
||||||
|
label: relationshipData.label,
|
||||||
|
};
|
||||||
|
db.addRelationship(relationship);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.debug('Populated usecase database:', {
|
||||||
|
actors: ast.actors.length,
|
||||||
|
useCases: ast.useCases.length,
|
||||||
|
relationships: ast.relationships.length,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parser: ParserDefinition = {
|
||||||
|
parse: async (input: string): Promise<void> => {
|
||||||
|
log.debug('Parsing usecase diagram with ANTLR:', input);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use our ANTLR parser
|
||||||
|
const ast: UsecaseParseResult = await parseUsecaseWithAntlr(input);
|
||||||
|
log.debug('ANTLR parsing result:', ast);
|
||||||
|
|
||||||
|
// Populate common database fields
|
||||||
|
populateCommonDb(ast as any, db);
|
||||||
|
|
||||||
|
// Populate the database with validation
|
||||||
|
populateDb(ast, db);
|
||||||
|
|
||||||
|
log.debug('Usecase diagram parsing completed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error parsing usecase diagram:', error);
|
||||||
|
|
||||||
|
// Check if it's a UsecaseParseError from our ANTLR parser
|
||||||
|
if (
|
||||||
|
error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
'name' in error &&
|
||||||
|
error.name === 'UsecaseParseError'
|
||||||
|
) {
|
||||||
|
// Re-throw the detailed error for better error reporting
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors, wrap them in a generic error
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
const wrappedError = new Error(`Failed to parse usecase diagram: ${errorMessage}`);
|
||||||
|
|
||||||
|
// Add hash property for consistency with other diagram types
|
||||||
|
(wrappedError as any).hash = {
|
||||||
|
text: input.split('\n')[0] || '',
|
||||||
|
token: 'unknown',
|
||||||
|
line: '1',
|
||||||
|
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
||||||
|
expected: ['valid usecase syntax'],
|
||||||
|
};
|
||||||
|
|
||||||
|
throw wrappedError;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
545
packages/mermaid/src/diagrams/usecase/usecaseRenderer.ts
Normal file
545
packages/mermaid/src/diagrams/usecase/usecaseRenderer.ts
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
import type { DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js';
|
||||||
|
import { log } from '../../logger.js';
|
||||||
|
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||||
|
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||||
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
|
import {
|
||||||
|
insertEdgeLabel,
|
||||||
|
positionEdgeLabel,
|
||||||
|
} from '../../rendering-util/rendering-elements/edges.js';
|
||||||
|
import { db } from './usecaseDb.js';
|
||||||
|
import { ARROW_TYPE } from './usecaseTypes.js';
|
||||||
|
import type { Actor, UseCase, SystemBoundary, Relationship } from './usecaseTypes.js';
|
||||||
|
|
||||||
|
// Layout constants
|
||||||
|
const ACTOR_WIDTH = 80;
|
||||||
|
const ACTOR_HEIGHT = 100;
|
||||||
|
const USECASE_WIDTH = 120;
|
||||||
|
const USECASE_HEIGHT = 60;
|
||||||
|
const SYSTEM_BOUNDARY_PADDING = 30;
|
||||||
|
const MARGIN = 50;
|
||||||
|
const SPACING_X = 200;
|
||||||
|
const SPACING_Y = 150;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get actor styling based on metadata
|
||||||
|
*/
|
||||||
|
const getActorStyling = (metadata?: Record<string, string>) => {
|
||||||
|
const defaults = {
|
||||||
|
fillColor: 'none',
|
||||||
|
strokeColor: 'black',
|
||||||
|
strokeWidth: 2,
|
||||||
|
type: 'solid',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!metadata) {
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fillColor: metadata.type === 'hollow' ? 'none' : metadata.fillColor || defaults.fillColor,
|
||||||
|
strokeColor: metadata.strokeColor || defaults.strokeColor,
|
||||||
|
strokeWidth: parseInt(metadata.strokeWidth || '2', 10),
|
||||||
|
type: metadata.type || defaults.type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw an actor (stick figure) with metadata support
|
||||||
|
*/
|
||||||
|
const drawActor = (group: SVGGroup, actor: Actor, x: number, y: number): void => {
|
||||||
|
const actorGroup = group.append('g').attr('class', 'actor').attr('id', actor.id);
|
||||||
|
const styling = getActorStyling(actor.metadata);
|
||||||
|
|
||||||
|
// Add metadata as data attributes for CSS styling
|
||||||
|
if (actor.metadata) {
|
||||||
|
Object.entries(actor.metadata).forEach(([key, value]) => {
|
||||||
|
actorGroup.attr(`data-${key}`, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should render an icon instead of stick figure
|
||||||
|
if (actor.metadata?.icon) {
|
||||||
|
drawActorWithIcon(actorGroup, actor, x, y, styling);
|
||||||
|
} else {
|
||||||
|
drawStickFigure(actorGroup, actor, x, y, styling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actor name (always rendered)
|
||||||
|
const displayName = actor.metadata?.name ?? actor.name;
|
||||||
|
actorGroup
|
||||||
|
.append('text')
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', y + 50)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('class', 'actor-label')
|
||||||
|
.text(displayName);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw traditional stick figure
|
||||||
|
*/
|
||||||
|
const drawStickFigure = (
|
||||||
|
actorGroup: SVGGroup,
|
||||||
|
_actor: Actor,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
styling: ReturnType<typeof getActorStyling>
|
||||||
|
): void => {
|
||||||
|
// Head (circle)
|
||||||
|
actorGroup
|
||||||
|
.append('circle')
|
||||||
|
.attr('cx', x)
|
||||||
|
.attr('cy', y - 30)
|
||||||
|
.attr('r', 8)
|
||||||
|
.attr('fill', styling.fillColor)
|
||||||
|
.attr('stroke', styling.strokeColor)
|
||||||
|
.attr('stroke-width', styling.strokeWidth);
|
||||||
|
|
||||||
|
// Body (line)
|
||||||
|
actorGroup
|
||||||
|
.append('line')
|
||||||
|
.attr('x1', x)
|
||||||
|
.attr('y1', y - 22)
|
||||||
|
.attr('x2', x)
|
||||||
|
.attr('y2', y + 10)
|
||||||
|
.attr('stroke', styling.strokeColor)
|
||||||
|
.attr('stroke-width', styling.strokeWidth);
|
||||||
|
|
||||||
|
// Arms (line)
|
||||||
|
actorGroup
|
||||||
|
.append('line')
|
||||||
|
.attr('x1', x - 15)
|
||||||
|
.attr('y1', y - 10)
|
||||||
|
.attr('x2', x + 15)
|
||||||
|
.attr('y2', y - 10)
|
||||||
|
.attr('stroke', styling.strokeColor)
|
||||||
|
.attr('stroke-width', styling.strokeWidth);
|
||||||
|
|
||||||
|
// Left leg
|
||||||
|
actorGroup
|
||||||
|
.append('line')
|
||||||
|
.attr('x1', x)
|
||||||
|
.attr('y1', y + 10)
|
||||||
|
.attr('x2', x - 15)
|
||||||
|
.attr('y2', y + 30)
|
||||||
|
.attr('stroke', styling.strokeColor)
|
||||||
|
.attr('stroke-width', styling.strokeWidth);
|
||||||
|
|
||||||
|
// Right leg
|
||||||
|
actorGroup
|
||||||
|
.append('line')
|
||||||
|
.attr('x1', x)
|
||||||
|
.attr('y1', y + 10)
|
||||||
|
.attr('x2', x + 15)
|
||||||
|
.attr('y2', y + 30)
|
||||||
|
.attr('stroke', styling.strokeColor)
|
||||||
|
.attr('stroke-width', styling.strokeWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw actor with icon representation
|
||||||
|
*/
|
||||||
|
const drawActorWithIcon = (
|
||||||
|
actorGroup: SVGGroup,
|
||||||
|
actor: Actor,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
styling: ReturnType<typeof getActorStyling>
|
||||||
|
): void => {
|
||||||
|
const iconName = actor.metadata?.icon ?? 'user';
|
||||||
|
|
||||||
|
// Create a rectangle background for the icon
|
||||||
|
actorGroup
|
||||||
|
.append('rect')
|
||||||
|
.attr('x', x - 20)
|
||||||
|
.attr('y', y - 35)
|
||||||
|
.attr('width', 40)
|
||||||
|
.attr('height', 40)
|
||||||
|
.attr('rx', 5)
|
||||||
|
.attr('fill', styling.fillColor === 'none' ? 'white' : styling.fillColor)
|
||||||
|
.attr('stroke', styling.strokeColor)
|
||||||
|
.attr('stroke-width', styling.strokeWidth);
|
||||||
|
|
||||||
|
// Add icon text (could be enhanced to use actual icons/SVG symbols)
|
||||||
|
actorGroup
|
||||||
|
.append('text')
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', y - 10)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('class', 'actor-icon')
|
||||||
|
.attr('font-size', '16px')
|
||||||
|
.attr('font-weight', 'bold')
|
||||||
|
.text(getIconSymbol(iconName));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get symbol representation for common icons
|
||||||
|
*/
|
||||||
|
const getIconSymbol = (iconName: string): string => {
|
||||||
|
const iconMap: Record<string, string> = {
|
||||||
|
user: '👤',
|
||||||
|
admin: '👑',
|
||||||
|
system: '⚙️',
|
||||||
|
database: '🗄️',
|
||||||
|
api: '🔌',
|
||||||
|
service: '🔧',
|
||||||
|
client: '💻',
|
||||||
|
server: '🖥️',
|
||||||
|
mobile: '📱',
|
||||||
|
web: '🌐',
|
||||||
|
default: '👤',
|
||||||
|
};
|
||||||
|
|
||||||
|
return iconMap[iconName.toLowerCase()] || iconMap.default;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a use case (oval)
|
||||||
|
*/
|
||||||
|
const drawUseCase = (group: SVGGroup, useCase: UseCase, x: number, y: number): void => {
|
||||||
|
const useCaseGroup = group.append('g').attr('class', 'usecase').attr('id', useCase.id);
|
||||||
|
|
||||||
|
// Oval
|
||||||
|
useCaseGroup
|
||||||
|
.append('ellipse')
|
||||||
|
.attr('cx', x)
|
||||||
|
.attr('cy', y)
|
||||||
|
.attr('rx', USECASE_WIDTH / 2)
|
||||||
|
.attr('ry', USECASE_HEIGHT / 2)
|
||||||
|
.attr('fill', 'white')
|
||||||
|
.attr('stroke', 'black')
|
||||||
|
.attr('stroke-width', 2);
|
||||||
|
|
||||||
|
// Use case name
|
||||||
|
useCaseGroup
|
||||||
|
.append('text')
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', y + 5)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('class', 'usecase-label')
|
||||||
|
.text(useCase.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a system boundary (supports both 'rect' and 'package' types)
|
||||||
|
*/
|
||||||
|
const drawSystemBoundary = (
|
||||||
|
group: SVGGroup,
|
||||||
|
boundary: SystemBoundary,
|
||||||
|
useCasePositions: Map<string, { x: number; y: number }>
|
||||||
|
): void => {
|
||||||
|
// Calculate boundary dimensions based on contained use cases
|
||||||
|
const containedUseCases = boundary.useCases
|
||||||
|
.map((ucId) => useCasePositions.get(ucId))
|
||||||
|
.filter((pos) => pos !== undefined) as { x: number; y: number }[];
|
||||||
|
|
||||||
|
if (containedUseCases.length === 0) {
|
||||||
|
return; // No use cases to bound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find min/max coordinates of contained use cases
|
||||||
|
const minX =
|
||||||
|
Math.min(...containedUseCases.map((pos) => pos.x)) -
|
||||||
|
USECASE_WIDTH / 2 -
|
||||||
|
SYSTEM_BOUNDARY_PADDING;
|
||||||
|
const maxX =
|
||||||
|
Math.max(...containedUseCases.map((pos) => pos.x)) +
|
||||||
|
USECASE_WIDTH / 2 +
|
||||||
|
SYSTEM_BOUNDARY_PADDING;
|
||||||
|
const minY =
|
||||||
|
Math.min(...containedUseCases.map((pos) => pos.y)) -
|
||||||
|
USECASE_HEIGHT / 2 -
|
||||||
|
SYSTEM_BOUNDARY_PADDING;
|
||||||
|
const maxY =
|
||||||
|
Math.max(...containedUseCases.map((pos) => pos.y)) +
|
||||||
|
USECASE_HEIGHT / 2 +
|
||||||
|
SYSTEM_BOUNDARY_PADDING;
|
||||||
|
|
||||||
|
const boundaryType = boundary.type || 'rect'; // default to 'rect'
|
||||||
|
const boundaryGroup = group.append('g').attr('class', 'system-boundary').attr('id', boundary.id);
|
||||||
|
|
||||||
|
if (boundaryType === 'package') {
|
||||||
|
drawPackageBoundary(boundaryGroup, boundary, minX, minY, maxX, maxY);
|
||||||
|
} else {
|
||||||
|
drawRectBoundary(boundaryGroup, boundary, minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw rect-type system boundary (simple dashed rectangle)
|
||||||
|
*/
|
||||||
|
const drawRectBoundary = (
|
||||||
|
boundaryGroup: SVGGroup,
|
||||||
|
boundary: SystemBoundary,
|
||||||
|
minX: number,
|
||||||
|
minY: number,
|
||||||
|
maxX: number,
|
||||||
|
maxY: number
|
||||||
|
): void => {
|
||||||
|
// Draw dashed rectangle
|
||||||
|
boundaryGroup
|
||||||
|
.append('rect')
|
||||||
|
.attr('x', minX)
|
||||||
|
.attr('y', minY)
|
||||||
|
.attr('width', maxX - minX)
|
||||||
|
.attr('height', maxY - minY)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', 'black')
|
||||||
|
.attr('stroke-width', 2)
|
||||||
|
.attr('stroke-dasharray', '5,5');
|
||||||
|
// .attr('rx', 10)
|
||||||
|
// .attr('ry', 10);
|
||||||
|
|
||||||
|
// Draw boundary label at top-left
|
||||||
|
boundaryGroup
|
||||||
|
.append('text')
|
||||||
|
.attr('x', minX + 10)
|
||||||
|
.attr('y', minY + 20)
|
||||||
|
.attr('class', 'system-boundary-label')
|
||||||
|
.attr('font-weight', 'bold')
|
||||||
|
.attr('font-size', '14px')
|
||||||
|
.text(boundary.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw package-type system boundary (rectangle with separate name box)
|
||||||
|
*/
|
||||||
|
const drawPackageBoundary = (
|
||||||
|
boundaryGroup: SVGGroup,
|
||||||
|
boundary: SystemBoundary,
|
||||||
|
minX: number,
|
||||||
|
minY: number,
|
||||||
|
maxX: number,
|
||||||
|
maxY: number
|
||||||
|
): void => {
|
||||||
|
// Calculate name box dimensions
|
||||||
|
const nameBoxWidth = Math.max(80, boundary.name.length * 8 + 20);
|
||||||
|
const nameBoxHeight = 25;
|
||||||
|
|
||||||
|
// Draw main boundary rectangle
|
||||||
|
boundaryGroup
|
||||||
|
.append('rect')
|
||||||
|
.attr('x', minX)
|
||||||
|
.attr('y', minY)
|
||||||
|
.attr('width', maxX - minX)
|
||||||
|
.attr('height', maxY - minY)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', 'black')
|
||||||
|
.attr('stroke-width', 2);
|
||||||
|
|
||||||
|
// Draw name box (package tab)
|
||||||
|
boundaryGroup
|
||||||
|
.append('rect')
|
||||||
|
.attr('x', minX)
|
||||||
|
.attr('y', minY - nameBoxHeight)
|
||||||
|
.attr('width', nameBoxWidth)
|
||||||
|
.attr('height', nameBoxHeight)
|
||||||
|
.attr('fill', 'white')
|
||||||
|
.attr('stroke', 'black')
|
||||||
|
.attr('stroke-width', 2);
|
||||||
|
// .attr('rx', 5)
|
||||||
|
// .attr('ry', 5);
|
||||||
|
|
||||||
|
// Draw boundary label in the name box
|
||||||
|
boundaryGroup
|
||||||
|
.append('text')
|
||||||
|
.attr('x', minX + nameBoxWidth / 2)
|
||||||
|
.attr('y', minY - nameBoxHeight / 2 + 5)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('class', 'system-boundary-label')
|
||||||
|
.attr('font-weight', 'bold')
|
||||||
|
.attr('font-size', '14px')
|
||||||
|
.text(boundary.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a relationship (line with arrow)
|
||||||
|
*/
|
||||||
|
const drawRelationship = async (
|
||||||
|
group: SVGGroup,
|
||||||
|
relationship: Relationship,
|
||||||
|
fromPos: { x: number; y: number },
|
||||||
|
toPos: { x: number; y: number }
|
||||||
|
): Promise<void> => {
|
||||||
|
const relationshipGroup = group
|
||||||
|
.append('g')
|
||||||
|
.attr('class', 'relationship')
|
||||||
|
.attr('id', relationship.id);
|
||||||
|
|
||||||
|
// Calculate arrow direction
|
||||||
|
const dx = toPos.x - fromPos.x;
|
||||||
|
const dy = toPos.y - fromPos.y;
|
||||||
|
const length = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const unitX = dx / length;
|
||||||
|
const unitY = dy / length;
|
||||||
|
|
||||||
|
// Adjust start and end points to avoid overlapping with shapes
|
||||||
|
const startX = fromPos.x + unitX * 40;
|
||||||
|
const startY = fromPos.y + unitY * 40;
|
||||||
|
const endX = toPos.x - unitX * 60;
|
||||||
|
const endY = toPos.y - unitY * 30;
|
||||||
|
|
||||||
|
// Main line
|
||||||
|
relationshipGroup
|
||||||
|
.append('line')
|
||||||
|
.attr('x1', startX)
|
||||||
|
.attr('y1', startY)
|
||||||
|
.attr('x2', endX)
|
||||||
|
.attr('y2', endY)
|
||||||
|
.attr('stroke', 'black')
|
||||||
|
.attr('stroke-width', 2);
|
||||||
|
|
||||||
|
// Draw arrow based on arrow type
|
||||||
|
const arrowSize = 10;
|
||||||
|
|
||||||
|
if (relationship.arrowType === ARROW_TYPE.SOLID_ARROW) {
|
||||||
|
// Forward arrow (-->)
|
||||||
|
const arrowX1 = endX - arrowSize * unitX - arrowSize * unitY * 0.5;
|
||||||
|
const arrowY1 = endY - arrowSize * unitY + arrowSize * unitX * 0.5;
|
||||||
|
const arrowX2 = endX - arrowSize * unitX + arrowSize * unitY * 0.5;
|
||||||
|
const arrowY2 = endY - arrowSize * unitY - arrowSize * unitX * 0.5;
|
||||||
|
|
||||||
|
relationshipGroup
|
||||||
|
.append('polygon')
|
||||||
|
.attr('points', `${endX},${endY} ${arrowX1},${arrowY1} ${arrowX2},${arrowY2}`)
|
||||||
|
.attr('fill', 'black');
|
||||||
|
} else if (relationship.arrowType === ARROW_TYPE.BACK_ARROW) {
|
||||||
|
// Backward arrow (<--)
|
||||||
|
const arrowX1 = startX + arrowSize * unitX - arrowSize * unitY * 0.5;
|
||||||
|
const arrowY1 = startY + arrowSize * unitY + arrowSize * unitX * 0.5;
|
||||||
|
const arrowX2 = startX + arrowSize * unitX + arrowSize * unitY * 0.5;
|
||||||
|
const arrowY2 = startY + arrowSize * unitY - arrowSize * unitX * 0.5;
|
||||||
|
|
||||||
|
relationshipGroup
|
||||||
|
.append('polygon')
|
||||||
|
.attr('points', `${startX},${startY} ${arrowX1},${arrowY1} ${arrowX2},${arrowY2}`)
|
||||||
|
.attr('fill', 'black');
|
||||||
|
}
|
||||||
|
// For LINE_SOLID (--), no arrow head is drawn
|
||||||
|
|
||||||
|
// Enhanced edge label rendering (if present)
|
||||||
|
if (relationship.label) {
|
||||||
|
const midX = (startX + endX) / 2;
|
||||||
|
const midY = (startY + endY) / 2;
|
||||||
|
|
||||||
|
// Create edge data structure compatible with the edge label system
|
||||||
|
const edgeData = {
|
||||||
|
id: relationship.id,
|
||||||
|
label: relationship.label,
|
||||||
|
labelStyle: 'stroke: #333; stroke-width: 1.5px; fill: none;',
|
||||||
|
x: midX,
|
||||||
|
y: midY,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the proper edge label rendering system
|
||||||
|
await insertEdgeLabel(relationshipGroup, edgeData);
|
||||||
|
|
||||||
|
// Position the edge label at the midpoint
|
||||||
|
const points = [
|
||||||
|
{ x: startX, y: startY },
|
||||||
|
{ x: midX, y: midY },
|
||||||
|
{ x: endX, y: endY },
|
||||||
|
];
|
||||||
|
|
||||||
|
positionEdgeLabel(edgeData, {
|
||||||
|
originalPath: points,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to simple text if edge label system fails
|
||||||
|
log.warn(
|
||||||
|
'Failed to render edge label with advanced system, falling back to simple text:',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
relationshipGroup
|
||||||
|
.append('text')
|
||||||
|
.attr('x', midX)
|
||||||
|
.attr('y', midY - 5)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('class', 'relationship-label')
|
||||||
|
.text(relationship.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main draw function
|
||||||
|
*/
|
||||||
|
const draw: DrawDefinition = async (text, id, _version) => {
|
||||||
|
log.debug('Rendering usecase diagram\n' + text);
|
||||||
|
|
||||||
|
const svg: SVG = selectSvgElement(id);
|
||||||
|
const group: SVGGroup = svg.append('g');
|
||||||
|
|
||||||
|
// Get data from database
|
||||||
|
const actors = [...db.getActors().values()];
|
||||||
|
const useCases = [...db.getUseCases().values()];
|
||||||
|
const systemBoundaries = [...db.getSystemBoundaries().values()];
|
||||||
|
const relationships = db.getRelationships();
|
||||||
|
|
||||||
|
log.debug('Rendering data:', {
|
||||||
|
actors: actors.length,
|
||||||
|
useCases: useCases.length,
|
||||||
|
systemBoundaries: systemBoundaries.length,
|
||||||
|
relationships: relationships.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate layout
|
||||||
|
const actorPositions = new Map<string, { x: number; y: number }>();
|
||||||
|
const useCasePositions = new Map<string, { x: number; y: number }>();
|
||||||
|
|
||||||
|
// Position actors on the left
|
||||||
|
actors.forEach((actor, index) => {
|
||||||
|
const x = MARGIN + ACTOR_WIDTH / 2;
|
||||||
|
const y = MARGIN + ACTOR_HEIGHT / 2 + index * SPACING_Y;
|
||||||
|
actorPositions.set(actor.id, { x, y });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Position use cases on the right
|
||||||
|
useCases.forEach((useCase, index) => {
|
||||||
|
const x = MARGIN + SPACING_X + USECASE_WIDTH / 2;
|
||||||
|
const y = MARGIN + USECASE_HEIGHT / 2 + index * SPACING_Y;
|
||||||
|
useCasePositions.set(useCase.id, { x, y });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw actors
|
||||||
|
actors.forEach((actor) => {
|
||||||
|
const pos = actorPositions.get(actor.id)!;
|
||||||
|
drawActor(group, actor, pos.x, pos.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw use cases
|
||||||
|
useCases.forEach((useCase) => {
|
||||||
|
const pos = useCasePositions.get(useCase.id)!;
|
||||||
|
drawUseCase(group, useCase, pos.x, pos.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw system boundaries (after use cases, before relationships)
|
||||||
|
systemBoundaries.forEach((boundary) => {
|
||||||
|
drawSystemBoundary(group, boundary, useCasePositions);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw relationships (async to handle edge labels)
|
||||||
|
const relationshipPromises = relationships.map(async (relationship) => {
|
||||||
|
const fromPos =
|
||||||
|
actorPositions.get(relationship.from) ?? useCasePositions.get(relationship.from);
|
||||||
|
const toPos = actorPositions.get(relationship.to) ?? useCasePositions.get(relationship.to);
|
||||||
|
|
||||||
|
if (fromPos && toPos) {
|
||||||
|
await drawRelationship(group, relationship, fromPos, toPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for all relationships to be drawn
|
||||||
|
await Promise.all(relationshipPromises);
|
||||||
|
|
||||||
|
// Setup viewBox and SVG dimensions properly
|
||||||
|
const { usecase: conf } = getConfig();
|
||||||
|
const padding = 8; // Standard padding used by other diagrams
|
||||||
|
setupViewPortForSVG(svg, padding, 'usecaseDiagram', conf?.useMaxWidth ?? true);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderer = { draw };
|
||||||
40
packages/mermaid/src/diagrams/usecase/usecaseStyles.ts
Normal file
40
packages/mermaid/src/diagrams/usecase/usecaseStyles.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const getStyles = (options: any) =>
|
||||||
|
`
|
||||||
|
.actor {
|
||||||
|
stroke: ${options.primaryColor};
|
||||||
|
fill: ${options.primaryColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor-label {
|
||||||
|
fill: ${options.primaryTextColor};
|
||||||
|
font-family: ${options.fontFamily};
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usecase {
|
||||||
|
stroke: ${options.primaryColor};
|
||||||
|
fill: ${options.primaryColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
.usecase-label {
|
||||||
|
fill: ${options.primaryTextColor};
|
||||||
|
font-family: ${options.fontFamily};
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship {
|
||||||
|
stroke: ${options.primaryColor};
|
||||||
|
fill: ${options.primaryColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-label {
|
||||||
|
fill: ${options.primaryTextColor};
|
||||||
|
font-family: ${options.fontFamily};
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default getStyles;
|
||||||
80
packages/mermaid/src/diagrams/usecase/usecaseTypes.ts
Normal file
80
packages/mermaid/src/diagrams/usecase/usecaseTypes.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||||
|
import type { UsecaseDiagramConfig } from '../../config.type.js';
|
||||||
|
|
||||||
|
export type ActorMetadata = Record<string, string>;
|
||||||
|
|
||||||
|
export interface Actor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
metadata?: ActorMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCase {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
nodeId?: string; // Optional node ID (e.g., 'a' in 'a(Go through code)')
|
||||||
|
systemBoundary?: string; // Optional reference to system boundary
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SystemBoundaryType = 'package' | 'rect';
|
||||||
|
|
||||||
|
export interface SystemBoundary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
useCases: string[]; // Array of use case IDs within this boundary
|
||||||
|
type?: SystemBoundaryType; // Type of boundary rendering (default: 'rect')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow types for usecase diagrams (matching parser types)
|
||||||
|
export const ARROW_TYPE = {
|
||||||
|
SOLID_ARROW: 0, // -->
|
||||||
|
BACK_ARROW: 1, // <--
|
||||||
|
LINE_SOLID: 2, // --
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ArrowType = (typeof ARROW_TYPE)[keyof typeof ARROW_TYPE];
|
||||||
|
|
||||||
|
export interface Relationship {
|
||||||
|
id: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
type: 'association' | 'include' | 'extend';
|
||||||
|
arrowType: ArrowType;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsecaseFields {
|
||||||
|
actors: Map<string, Actor>;
|
||||||
|
useCases: Map<string, UseCase>;
|
||||||
|
systemBoundaries: Map<string, SystemBoundary>;
|
||||||
|
relationships: Relationship[];
|
||||||
|
config: Required<UsecaseDiagramConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsecaseDB extends DiagramDB {
|
||||||
|
getConfig: () => Required<UsecaseDiagramConfig>;
|
||||||
|
|
||||||
|
// Actor management
|
||||||
|
addActor: (actor: Actor) => void;
|
||||||
|
getActors: () => Map<string, Actor>;
|
||||||
|
getActor: (id: string) => Actor | undefined;
|
||||||
|
|
||||||
|
// UseCase management
|
||||||
|
addUseCase: (useCase: UseCase) => void;
|
||||||
|
getUseCases: () => Map<string, UseCase>;
|
||||||
|
getUseCase: (id: string) => UseCase | undefined;
|
||||||
|
|
||||||
|
// SystemBoundary management
|
||||||
|
addSystemBoundary: (systemBoundary: SystemBoundary) => void;
|
||||||
|
getSystemBoundaries: () => Map<string, SystemBoundary>;
|
||||||
|
getSystemBoundary: (id: string) => SystemBoundary | undefined;
|
||||||
|
|
||||||
|
// Relationship management
|
||||||
|
addRelationship: (relationship: Relationship) => void;
|
||||||
|
getRelationships: () => Relationship[];
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
clear: () => void;
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ required:
|
|||||||
- block
|
- block
|
||||||
- look
|
- look
|
||||||
- radar
|
- radar
|
||||||
|
- usecase
|
||||||
properties:
|
properties:
|
||||||
theme:
|
theme:
|
||||||
description: |
|
description: |
|
||||||
@@ -310,6 +311,8 @@ properties:
|
|||||||
$ref: '#/$defs/BlockDiagramConfig'
|
$ref: '#/$defs/BlockDiagramConfig'
|
||||||
radar:
|
radar:
|
||||||
$ref: '#/$defs/RadarDiagramConfig'
|
$ref: '#/$defs/RadarDiagramConfig'
|
||||||
|
usecase:
|
||||||
|
$ref: '#/$defs/UsecaseDiagramConfig'
|
||||||
dompurifyConfig:
|
dompurifyConfig:
|
||||||
title: DOM Purify Configuration
|
title: DOM Purify Configuration
|
||||||
description: Configuration options to pass to the `dompurify` library.
|
description: Configuration options to pass to the `dompurify` library.
|
||||||
@@ -2324,6 +2327,57 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
|||||||
maximum: 1
|
maximum: 1
|
||||||
default: 0.17
|
default: 0.17
|
||||||
|
|
||||||
|
UsecaseDiagramConfig:
|
||||||
|
title: Usecase Diagram Config
|
||||||
|
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
|
||||||
|
description: The object containing configurations specific for usecase diagrams.
|
||||||
|
type: object
|
||||||
|
unevaluatedProperties: false
|
||||||
|
required:
|
||||||
|
- useMaxWidth
|
||||||
|
properties:
|
||||||
|
actorFontSize:
|
||||||
|
description: Font size for actor labels
|
||||||
|
type: number
|
||||||
|
minimum: 1
|
||||||
|
default: 14
|
||||||
|
actorFontFamily:
|
||||||
|
description: Font family for actor labels
|
||||||
|
type: string
|
||||||
|
default: '"Open Sans", sans-serif'
|
||||||
|
actorFontWeight:
|
||||||
|
description: Font weight for actor labels
|
||||||
|
type: string
|
||||||
|
default: 'normal'
|
||||||
|
usecaseFontSize:
|
||||||
|
description: Font size for usecase labels
|
||||||
|
type: number
|
||||||
|
minimum: 1
|
||||||
|
default: 12
|
||||||
|
usecaseFontFamily:
|
||||||
|
description: Font family for usecase labels
|
||||||
|
type: string
|
||||||
|
default: '"Open Sans", sans-serif'
|
||||||
|
usecaseFontWeight:
|
||||||
|
description: Font weight for usecase labels
|
||||||
|
type: string
|
||||||
|
default: 'normal'
|
||||||
|
actorMargin:
|
||||||
|
description: Margin around actors
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 50
|
||||||
|
usecaseMargin:
|
||||||
|
description: Margin around use cases
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 50
|
||||||
|
diagramPadding:
|
||||||
|
description: Padding around the entire diagram
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 20
|
||||||
|
|
||||||
FontCalculator:
|
FontCalculator:
|
||||||
title: Font Calculator
|
title: Font Calculator
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist src/language/generated",
|
"clean": "rimraf dist src/language/generated",
|
||||||
"langium:generate": "langium generate",
|
"langium:generate": "langium generate",
|
||||||
"langium:watch": "langium generate --watch"
|
"langium:watch": "langium generate --watch",
|
||||||
|
"antlr:generate": "cd src/language/usecase && npx antlr-ng -Dlanguage=TypeScript --generate-visitor --generate-listener -o generated Usecase.g4"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -33,9 +34,11 @@
|
|||||||
"ast"
|
"ast"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"antlr4ng": "^3.0.7",
|
||||||
"langium": "3.3.1"
|
"langium": "3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"antlr-ng": "^1.0.10",
|
||||||
"chevrotain": "^11.0.3"
|
"chevrotain": "^11.0.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -45,3 +45,4 @@ export * from './pie/index.js';
|
|||||||
export * from './architecture/index.js';
|
export * from './architecture/index.js';
|
||||||
export * from './radar/index.js';
|
export * from './radar/index.js';
|
||||||
export * from './treemap/index.js';
|
export * from './treemap/index.js';
|
||||||
|
export * from './usecase/index.js';
|
||||||
|
|||||||
170
packages/parser/src/language/usecase/Usecase.g4
Normal file
170
packages/parser/src/language/usecase/Usecase.g4
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
grammar Usecase;
|
||||||
|
|
||||||
|
// Parser rules
|
||||||
|
usecaseDiagram
|
||||||
|
: 'usecase' NEWLINE* statement* EOF
|
||||||
|
;
|
||||||
|
|
||||||
|
statement
|
||||||
|
: actorStatement
|
||||||
|
| relationshipStatement
|
||||||
|
| systemBoundaryStatement
|
||||||
|
| systemBoundaryTypeStatement
|
||||||
|
| NEWLINE
|
||||||
|
;
|
||||||
|
|
||||||
|
actorStatement
|
||||||
|
: 'actor' actorList NEWLINE*
|
||||||
|
;
|
||||||
|
|
||||||
|
actorList
|
||||||
|
: actorName (',' actorName)*
|
||||||
|
;
|
||||||
|
|
||||||
|
actorName
|
||||||
|
: (IDENTIFIER | STRING) metadata?
|
||||||
|
;
|
||||||
|
|
||||||
|
metadata
|
||||||
|
: '@' '{' metadataContent '}'
|
||||||
|
;
|
||||||
|
|
||||||
|
metadataContent
|
||||||
|
: metadataProperty (',' metadataProperty)*
|
||||||
|
;
|
||||||
|
|
||||||
|
metadataProperty
|
||||||
|
: STRING ':' STRING
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
relationshipStatement
|
||||||
|
: entityName arrow entityName NEWLINE*
|
||||||
|
| actorDeclaration arrow entityName NEWLINE*
|
||||||
|
;
|
||||||
|
|
||||||
|
systemBoundaryStatement
|
||||||
|
: 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE*
|
||||||
|
;
|
||||||
|
|
||||||
|
systemBoundaryName
|
||||||
|
: IDENTIFIER
|
||||||
|
| STRING
|
||||||
|
;
|
||||||
|
|
||||||
|
systemBoundaryContent
|
||||||
|
: usecaseInBoundary NEWLINE*
|
||||||
|
| NEWLINE
|
||||||
|
;
|
||||||
|
|
||||||
|
usecaseInBoundary
|
||||||
|
: IDENTIFIER
|
||||||
|
| STRING
|
||||||
|
;
|
||||||
|
|
||||||
|
systemBoundaryTypeStatement
|
||||||
|
: systemBoundaryName '@' '{' systemBoundaryTypeContent '}' NEWLINE*
|
||||||
|
;
|
||||||
|
|
||||||
|
systemBoundaryTypeContent
|
||||||
|
: systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)*
|
||||||
|
;
|
||||||
|
|
||||||
|
systemBoundaryTypeProperty
|
||||||
|
: 'type' ':' systemBoundaryType
|
||||||
|
;
|
||||||
|
|
||||||
|
systemBoundaryType
|
||||||
|
: 'package'
|
||||||
|
| 'rect'
|
||||||
|
;
|
||||||
|
|
||||||
|
entityName
|
||||||
|
: IDENTIFIER
|
||||||
|
| STRING
|
||||||
|
| nodeIdWithLabel
|
||||||
|
;
|
||||||
|
|
||||||
|
actorDeclaration
|
||||||
|
: 'actor' actorName
|
||||||
|
;
|
||||||
|
|
||||||
|
nodeIdWithLabel
|
||||||
|
: IDENTIFIER '(' nodeLabel ')'
|
||||||
|
;
|
||||||
|
|
||||||
|
nodeLabel
|
||||||
|
: IDENTIFIER
|
||||||
|
| STRING
|
||||||
|
| nodeLabel IDENTIFIER
|
||||||
|
| nodeLabel STRING
|
||||||
|
;
|
||||||
|
|
||||||
|
arrow
|
||||||
|
: SOLID_ARROW
|
||||||
|
| BACK_ARROW
|
||||||
|
| LINE_SOLID
|
||||||
|
| labeledArrow
|
||||||
|
;
|
||||||
|
|
||||||
|
labeledArrow
|
||||||
|
: LINE_SOLID edgeLabel SOLID_ARROW
|
||||||
|
| BACK_ARROW edgeLabel LINE_SOLID
|
||||||
|
| LINE_SOLID edgeLabel LINE_SOLID
|
||||||
|
;
|
||||||
|
|
||||||
|
edgeLabel
|
||||||
|
: IDENTIFIER
|
||||||
|
| STRING
|
||||||
|
;
|
||||||
|
|
||||||
|
// Lexer rules
|
||||||
|
SOLID_ARROW
|
||||||
|
: '-->'
|
||||||
|
;
|
||||||
|
|
||||||
|
BACK_ARROW
|
||||||
|
: '<--'
|
||||||
|
;
|
||||||
|
|
||||||
|
LINE_SOLID
|
||||||
|
: '--'
|
||||||
|
;
|
||||||
|
|
||||||
|
COMMA
|
||||||
|
: ','
|
||||||
|
;
|
||||||
|
|
||||||
|
AT
|
||||||
|
: '@'
|
||||||
|
;
|
||||||
|
|
||||||
|
LBRACE
|
||||||
|
: '{'
|
||||||
|
;
|
||||||
|
|
||||||
|
RBRACE
|
||||||
|
: '}'
|
||||||
|
;
|
||||||
|
|
||||||
|
COLON
|
||||||
|
: ':'
|
||||||
|
;
|
||||||
|
|
||||||
|
IDENTIFIER
|
||||||
|
: [a-zA-Z_][a-zA-Z0-9_]*
|
||||||
|
;
|
||||||
|
|
||||||
|
STRING
|
||||||
|
: '"' (~["\r\n])* '"'
|
||||||
|
| '\'' (~['\r\n])* '\''
|
||||||
|
;
|
||||||
|
|
||||||
|
NEWLINE
|
||||||
|
: [\r\n]+
|
||||||
|
;
|
||||||
|
|
||||||
|
WS
|
||||||
|
: [ \t]+ -> skip
|
||||||
|
;
|
||||||
4
packages/parser/src/language/usecase/index.ts
Normal file
4
packages/parser/src/language/usecase/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './module.js';
|
||||||
|
export * from './types.js';
|
||||||
|
export * from './parser.js';
|
||||||
|
export * from './visitor.js';
|
||||||
50
packages/parser/src/language/usecase/module.ts
Normal file
50
packages/parser/src/language/usecase/module.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* ANTLR UseCase Module
|
||||||
|
*
|
||||||
|
* This module provides dependency injection and service creation
|
||||||
|
* for the ANTLR-based UseCase parser, following the Langium pattern.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AntlrUsecaseServices } from './types.js';
|
||||||
|
import { UsecaseAntlrParser } from './parser.js';
|
||||||
|
import { UsecaseAntlrVisitor } from './visitor.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ANTLR UseCase Module for dependency injection
|
||||||
|
*/
|
||||||
|
export const AntlrUsecaseModule = {
|
||||||
|
parser: () => new UsecaseAntlrParser(),
|
||||||
|
visitor: () => new UsecaseAntlrVisitor(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the full set of ANTLR UseCase services
|
||||||
|
*
|
||||||
|
* This follows the Langium pattern but for ANTLR services
|
||||||
|
*
|
||||||
|
* @returns An object with ANTLR UseCase services
|
||||||
|
*/
|
||||||
|
export function createAntlrUsecaseServices(): AntlrUsecaseServices {
|
||||||
|
const parser = new UsecaseAntlrParser();
|
||||||
|
const visitor = new UsecaseAntlrVisitor();
|
||||||
|
|
||||||
|
return {
|
||||||
|
parser,
|
||||||
|
visitor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton instance of ANTLR UseCase services
|
||||||
|
*/
|
||||||
|
let antlrUsecaseServices: AntlrUsecaseServices | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create the singleton ANTLR UseCase services
|
||||||
|
*/
|
||||||
|
export function getAntlrUsecaseServices(): AntlrUsecaseServices {
|
||||||
|
if (!antlrUsecaseServices) {
|
||||||
|
antlrUsecaseServices = createAntlrUsecaseServices();
|
||||||
|
}
|
||||||
|
return antlrUsecaseServices;
|
||||||
|
}
|
||||||
194
packages/parser/src/language/usecase/parser.ts
Normal file
194
packages/parser/src/language/usecase/parser.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* True ANTLR Parser Implementation for UseCase Diagrams
|
||||||
|
*
|
||||||
|
* This parser uses the actual ANTLR-generated files from Usecase.g4
|
||||||
|
* and implements the visitor pattern to build the AST.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CharStream, CommonTokenStream, BaseErrorListener } from 'antlr4ng';
|
||||||
|
import type { RecognitionException, Recognizer } from 'antlr4ng';
|
||||||
|
import { UsecaseLexer } from './generated/UsecaseLexer.js';
|
||||||
|
import { UsecaseParser } from './generated/UsecaseParser.js';
|
||||||
|
import { UsecaseAntlrVisitor } from './visitor.js';
|
||||||
|
import type { AntlrUsecaseParser, UsecaseParseResult } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom error listener for ANTLR parser to capture syntax errors
|
||||||
|
*/
|
||||||
|
class UsecaseErrorListener extends BaseErrorListener {
|
||||||
|
private errors: string[] = [];
|
||||||
|
|
||||||
|
syntaxError(
|
||||||
|
_recognizer: Recognizer<any>,
|
||||||
|
_offendingSymbol: any,
|
||||||
|
line: number,
|
||||||
|
charPositionInLine: number,
|
||||||
|
message: string,
|
||||||
|
_e: RecognitionException | null
|
||||||
|
): void {
|
||||||
|
const errorMsg = `Syntax error at line ${line}:${charPositionInLine} - ${message}`;
|
||||||
|
this.errors.push(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAmbiguity(): void {
|
||||||
|
// Optional: handle ambiguity reports
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAttemptingFullContext(): void {
|
||||||
|
// Optional: handle full context attempts
|
||||||
|
}
|
||||||
|
|
||||||
|
reportContextSensitivity(): void {
|
||||||
|
// Optional: handle context sensitivity reports
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrors(): string[] {
|
||||||
|
return this.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasErrors(): boolean {
|
||||||
|
return this.errors.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.errors = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom error class for usecase parsing errors
|
||||||
|
*/
|
||||||
|
export class UsecaseParseError extends Error {
|
||||||
|
public line?: number;
|
||||||
|
public column?: number;
|
||||||
|
public token?: string;
|
||||||
|
public expected?: string[];
|
||||||
|
public hash?: Record<string, any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
details?: {
|
||||||
|
line?: number;
|
||||||
|
column?: number;
|
||||||
|
token?: string;
|
||||||
|
expected?: string[];
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'UsecaseParseError';
|
||||||
|
this.line = details?.line;
|
||||||
|
this.column = details?.column;
|
||||||
|
this.token = details?.token;
|
||||||
|
this.expected = details?.expected;
|
||||||
|
|
||||||
|
// Create hash object similar to other diagram types
|
||||||
|
this.hash = {
|
||||||
|
text: details?.token ?? '',
|
||||||
|
token: details?.token ?? '',
|
||||||
|
line: details?.line?.toString() ?? '1',
|
||||||
|
loc: {
|
||||||
|
first_line: details?.line ?? 1,
|
||||||
|
last_line: details?.line ?? 1,
|
||||||
|
first_column: details?.column ?? 1,
|
||||||
|
last_column: (details?.column ?? 1) + (details?.token?.length ?? 0),
|
||||||
|
},
|
||||||
|
expected: details?.expected ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ANTLR-based UseCase parser implementation
|
||||||
|
*/
|
||||||
|
export class UsecaseAntlrParser implements AntlrUsecaseParser {
|
||||||
|
private visitor: UsecaseAntlrVisitor;
|
||||||
|
private errorListener: UsecaseErrorListener;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.visitor = new UsecaseAntlrVisitor();
|
||||||
|
this.errorListener = new UsecaseErrorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse UseCase diagram input using true ANTLR parsing
|
||||||
|
*
|
||||||
|
* @param input - The UseCase diagram text to parse
|
||||||
|
* @returns Parsed result with actors, use cases, and relationships
|
||||||
|
* @throws UsecaseParseError when syntax errors are encountered
|
||||||
|
*/
|
||||||
|
parse(input: string): UsecaseParseResult {
|
||||||
|
// Clear previous errors
|
||||||
|
this.errorListener.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Create ANTLR input stream
|
||||||
|
const chars = CharStream.fromString(input);
|
||||||
|
|
||||||
|
// Step 2: Create lexer from generated ANTLR lexer
|
||||||
|
const lexer = new UsecaseLexer(chars);
|
||||||
|
|
||||||
|
// Add error listener to lexer
|
||||||
|
lexer.removeErrorListeners();
|
||||||
|
lexer.addErrorListener(this.errorListener);
|
||||||
|
|
||||||
|
// Step 3: Create token stream
|
||||||
|
const tokens = new CommonTokenStream(lexer);
|
||||||
|
|
||||||
|
// Step 4: Create parser from generated ANTLR parser
|
||||||
|
const parser = new UsecaseParser(tokens);
|
||||||
|
|
||||||
|
// Add error listener to parser
|
||||||
|
parser.removeErrorListeners();
|
||||||
|
parser.addErrorListener(this.errorListener);
|
||||||
|
|
||||||
|
// Step 5: Parse using the grammar rule: usecaseDiagram
|
||||||
|
const tree = parser.usecaseDiagram();
|
||||||
|
|
||||||
|
// Check for syntax errors before proceeding
|
||||||
|
if (this.errorListener.hasErrors()) {
|
||||||
|
const errors = this.errorListener.getErrors();
|
||||||
|
throw new UsecaseParseError(`Syntax error in usecase diagram: ${errors.join('; ')}`, {
|
||||||
|
token: 'unknown',
|
||||||
|
expected: ['valid usecase syntax'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Visit the parse tree using our visitor
|
||||||
|
this.visitor.visitUsecaseDiagram!(tree);
|
||||||
|
|
||||||
|
// Step 7: Get the parse result
|
||||||
|
return this.visitor.getParseResult();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UsecaseParseError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other types of errors
|
||||||
|
throw new UsecaseParseError(
|
||||||
|
`Failed to parse usecase diagram: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
{
|
||||||
|
token: 'unknown',
|
||||||
|
expected: ['valid usecase syntax'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a new ANTLR UseCase parser
|
||||||
|
*/
|
||||||
|
export function createUsecaseAntlrParser(): AntlrUsecaseParser {
|
||||||
|
return new UsecaseAntlrParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function for parsing UseCase diagrams
|
||||||
|
*
|
||||||
|
* @param input - The UseCase diagram text to parse
|
||||||
|
* @returns Parsed result with actors, use cases, and relationships
|
||||||
|
*/
|
||||||
|
export function parseUsecaseWithAntlr(input: string): UsecaseParseResult {
|
||||||
|
const parser = createUsecaseAntlrParser();
|
||||||
|
return parser.parse(input);
|
||||||
|
}
|
||||||
70
packages/parser/src/language/usecase/types.ts
Normal file
70
packages/parser/src/language/usecase/types.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for ANTLR UseCase parser
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Arrow types for usecase diagrams (similar to sequence diagram LINETYPE)
|
||||||
|
export const ARROW_TYPE = {
|
||||||
|
SOLID_ARROW: 0, // -->
|
||||||
|
BACK_ARROW: 1, // <--
|
||||||
|
LINE_SOLID: 2, // --
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ArrowType = (typeof ARROW_TYPE)[keyof typeof ARROW_TYPE];
|
||||||
|
|
||||||
|
export type ActorMetadata = Record<string, string>;
|
||||||
|
|
||||||
|
export interface Actor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
metadata?: ActorMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCase {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
nodeId?: string; // Optional node ID (e.g., 'a' in 'a(Go through code)')
|
||||||
|
systemBoundary?: string; // Optional reference to system boundary
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SystemBoundaryType = 'package' | 'rect';
|
||||||
|
|
||||||
|
export interface SystemBoundary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
useCases: string[]; // Array of use case IDs within this boundary
|
||||||
|
type?: SystemBoundaryType; // Type of boundary rendering (default: 'rect')
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Relationship {
|
||||||
|
id: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
type: 'association' | 'include' | 'extend';
|
||||||
|
arrowType: ArrowType;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsecaseParseResult {
|
||||||
|
actors: Actor[];
|
||||||
|
useCases: UseCase[];
|
||||||
|
systemBoundaries: SystemBoundary[];
|
||||||
|
relationships: Relationship[];
|
||||||
|
accDescr?: string;
|
||||||
|
accTitle?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ANTLR Parser Services interface
|
||||||
|
*/
|
||||||
|
export interface AntlrUsecaseServices {
|
||||||
|
parser: AntlrUsecaseParser;
|
||||||
|
visitor: any; // UsecaseAntlrVisitor - using any to avoid circular dependency
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ANTLR Parser interface
|
||||||
|
*/
|
||||||
|
export interface AntlrUsecaseParser {
|
||||||
|
parse(input: string): UsecaseParseResult;
|
||||||
|
}
|
||||||
605
packages/parser/src/language/usecase/visitor.ts
Normal file
605
packages/parser/src/language/usecase/visitor.ts
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
/**
|
||||||
|
* ANTLR Visitor Implementation for UseCase Diagrams
|
||||||
|
*
|
||||||
|
* This visitor traverses the ANTLR parse tree and builds the AST
|
||||||
|
* according to the grammar rules defined in Usecase.g4
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { UsecaseVisitor } from './generated/UsecaseVisitor.js';
|
||||||
|
import type {
|
||||||
|
UsecaseDiagramContext,
|
||||||
|
StatementContext,
|
||||||
|
ActorStatementContext,
|
||||||
|
ActorListContext,
|
||||||
|
RelationshipStatementContext,
|
||||||
|
SystemBoundaryStatementContext,
|
||||||
|
SystemBoundaryTypeStatementContext,
|
||||||
|
SystemBoundaryNameContext,
|
||||||
|
SystemBoundaryTypeContentContext,
|
||||||
|
SystemBoundaryTypePropertyContext,
|
||||||
|
SystemBoundaryTypeContext,
|
||||||
|
UsecaseInBoundaryContext,
|
||||||
|
ActorNameContext,
|
||||||
|
ActorDeclarationContext,
|
||||||
|
NodeIdWithLabelContext,
|
||||||
|
NodeLabelContext,
|
||||||
|
MetadataContext,
|
||||||
|
MetadataContentContext,
|
||||||
|
MetadataPropertyContext,
|
||||||
|
EntityNameContext,
|
||||||
|
ArrowContext,
|
||||||
|
LabeledArrowContext,
|
||||||
|
EdgeLabelContext,
|
||||||
|
} from './generated/UsecaseParser.js';
|
||||||
|
import { ARROW_TYPE } from './types.js';
|
||||||
|
import type {
|
||||||
|
Actor,
|
||||||
|
UseCase,
|
||||||
|
SystemBoundary,
|
||||||
|
Relationship,
|
||||||
|
UsecaseParseResult,
|
||||||
|
ArrowType,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
export class UsecaseAntlrVisitor extends UsecaseVisitor<void> {
|
||||||
|
private actors: Actor[] = [];
|
||||||
|
private useCases: UseCase[] = [];
|
||||||
|
private systemBoundaries: SystemBoundary[] = [];
|
||||||
|
private relationships: Relationship[] = [];
|
||||||
|
private relationshipCounter = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Assign visitor functions as properties
|
||||||
|
this.visitUsecaseDiagram = this.visitUsecaseDiagramImpl.bind(this);
|
||||||
|
this.visitStatement = this.visitStatementImpl.bind(this);
|
||||||
|
this.visitActorStatement = this.visitActorStatementImpl.bind(this);
|
||||||
|
this.visitRelationshipStatement = this.visitRelationshipStatementImpl.bind(this);
|
||||||
|
this.visitSystemBoundaryStatement = this.visitSystemBoundaryStatementImpl.bind(this);
|
||||||
|
this.visitSystemBoundaryTypeStatement = this.visitSystemBoundaryTypeStatementImpl.bind(this);
|
||||||
|
this.visitActorName = this.visitActorNameImpl.bind(this);
|
||||||
|
this.visitArrow = this.visitArrowImpl.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit the root usecaseDiagram rule
|
||||||
|
* Grammar: usecaseDiagram : 'usecase' statement* EOF ;
|
||||||
|
*/
|
||||||
|
visitUsecaseDiagramImpl(ctx: UsecaseDiagramContext): void {
|
||||||
|
// Reset state
|
||||||
|
this.actors = [];
|
||||||
|
this.useCases = [];
|
||||||
|
this.relationships = [];
|
||||||
|
this.relationshipCounter = 0;
|
||||||
|
|
||||||
|
// Visit all statement children
|
||||||
|
if (ctx.statement) {
|
||||||
|
const statements = Array.isArray(ctx.statement()) ? ctx.statement() : [ctx.statement()];
|
||||||
|
for (const statementCtx of statements) {
|
||||||
|
if (Array.isArray(statementCtx)) {
|
||||||
|
for (const stmt of statementCtx) {
|
||||||
|
this.visitStatementImpl(stmt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.visitStatementImpl(statementCtx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit statement rule
|
||||||
|
* Grammar: statement : actorStatement | relationshipStatement | systemBoundaryStatement | systemBoundaryTypeStatement | NEWLINE ;
|
||||||
|
*/
|
||||||
|
private visitStatementImpl(ctx: StatementContext): void {
|
||||||
|
if (ctx.actorStatement?.()) {
|
||||||
|
this.visitActorStatementImpl(ctx.actorStatement()!);
|
||||||
|
} else if (ctx.relationshipStatement?.()) {
|
||||||
|
this.visitRelationshipStatementImpl(ctx.relationshipStatement()!);
|
||||||
|
} else if (ctx.systemBoundaryStatement?.()) {
|
||||||
|
this.visitSystemBoundaryStatementImpl(ctx.systemBoundaryStatement()!);
|
||||||
|
} else if (ctx.systemBoundaryTypeStatement?.()) {
|
||||||
|
this.visitSystemBoundaryTypeStatementImpl(ctx.systemBoundaryTypeStatement()!);
|
||||||
|
}
|
||||||
|
// NEWLINE is ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit actorStatement rule
|
||||||
|
* Grammar: actorStatement : 'actor' actorList NEWLINE* ;
|
||||||
|
*/
|
||||||
|
visitActorStatementImpl(ctx: ActorStatementContext): void {
|
||||||
|
if (ctx.actorList?.()) {
|
||||||
|
this.visitActorListImpl(ctx.actorList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit actorList rule
|
||||||
|
* Grammar: actorList : actorName (',' actorName)* ;
|
||||||
|
*/
|
||||||
|
visitActorListImpl(ctx: ActorListContext): void {
|
||||||
|
// Get all actorName contexts from the list
|
||||||
|
const actorNameContexts = ctx.actorName();
|
||||||
|
|
||||||
|
for (const actorNameCtx of actorNameContexts) {
|
||||||
|
const actorResult = this.visitActorNameImpl(actorNameCtx);
|
||||||
|
this.actors.push({
|
||||||
|
id: actorResult.name,
|
||||||
|
name: actorResult.name,
|
||||||
|
metadata: actorResult.metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit relationshipStatement rule
|
||||||
|
* Grammar: relationshipStatement : entityName arrow entityName NEWLINE* | actorDeclaration arrow entityName NEWLINE* ;
|
||||||
|
*/
|
||||||
|
visitRelationshipStatementImpl(ctx: RelationshipStatementContext): void {
|
||||||
|
let from = '';
|
||||||
|
let to = '';
|
||||||
|
|
||||||
|
// Handle different relationship patterns
|
||||||
|
if (ctx.actorDeclaration?.()) {
|
||||||
|
// Pattern: actor ActorName --> entityName
|
||||||
|
from = this.visitActorDeclarationImpl(ctx.actorDeclaration()!);
|
||||||
|
to = this.visitEntityNameImpl(ctx.entityName(0)!);
|
||||||
|
} else if (ctx.entityName && ctx.entityName().length >= 2) {
|
||||||
|
// Pattern: entityName --> entityName
|
||||||
|
from = this.visitEntityNameImpl(ctx.entityName(0)!);
|
||||||
|
to = this.visitEntityNameImpl(ctx.entityName(1)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get arrow information (type and optional label)
|
||||||
|
const arrowInfo = this.visitArrowImpl(ctx.arrow());
|
||||||
|
|
||||||
|
// Auto-create use cases for entities that are not actors
|
||||||
|
this.ensureUseCaseExists(from);
|
||||||
|
this.ensureUseCaseExists(to);
|
||||||
|
|
||||||
|
const relationship: Relationship = {
|
||||||
|
id: `rel_${this.relationshipCounter++}`,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
type: 'association',
|
||||||
|
arrowType: arrowInfo.arrowType,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add label if present
|
||||||
|
if (arrowInfo.label) {
|
||||||
|
relationship.label = arrowInfo.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.relationships.push(relationship);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a use case exists for the given entity name if it's not an actor
|
||||||
|
*/
|
||||||
|
private ensureUseCaseExists(entityName: string): void {
|
||||||
|
// Check if it's already an actor
|
||||||
|
const isActor = this.actors.some((actor) => actor.id === entityName);
|
||||||
|
|
||||||
|
// If it's not an actor, create it as a use case (if not already exists)
|
||||||
|
if (!isActor) {
|
||||||
|
const existingUseCase = this.useCases.some((useCase) => useCase.id === entityName);
|
||||||
|
if (!existingUseCase) {
|
||||||
|
this.useCases.push({
|
||||||
|
id: entityName,
|
||||||
|
name: entityName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit systemBoundaryStatement rule
|
||||||
|
* Grammar: systemBoundaryStatement : 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE* ;
|
||||||
|
*/
|
||||||
|
visitSystemBoundaryStatementImpl(ctx: SystemBoundaryStatementContext): void {
|
||||||
|
let boundaryName = '';
|
||||||
|
|
||||||
|
// Get the system boundary name
|
||||||
|
if (ctx.systemBoundaryName?.()) {
|
||||||
|
boundaryName = this.visitSystemBoundaryNameImpl(ctx.systemBoundaryName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect use cases within this boundary
|
||||||
|
const useCasesInBoundary: string[] = [];
|
||||||
|
|
||||||
|
if (ctx.systemBoundaryContent?.()?.length > 0) {
|
||||||
|
for (const contentCtx of ctx.systemBoundaryContent()) {
|
||||||
|
const usecaseInBoundary = contentCtx.usecaseInBoundary?.();
|
||||||
|
if (usecaseInBoundary) {
|
||||||
|
const useCaseName = this.visitUsecaseInBoundaryImpl(usecaseInBoundary);
|
||||||
|
useCasesInBoundary.push(useCaseName);
|
||||||
|
|
||||||
|
// Create the use case and mark it as being in this boundary
|
||||||
|
const existingUseCase = this.useCases.find((uc) => uc.id === useCaseName);
|
||||||
|
if (existingUseCase) {
|
||||||
|
existingUseCase.systemBoundary = boundaryName;
|
||||||
|
} else {
|
||||||
|
this.useCases.push({
|
||||||
|
id: useCaseName,
|
||||||
|
name: useCaseName,
|
||||||
|
systemBoundary: boundaryName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the system boundary with default type
|
||||||
|
this.systemBoundaries.push({
|
||||||
|
id: boundaryName,
|
||||||
|
name: boundaryName,
|
||||||
|
useCases: useCasesInBoundary,
|
||||||
|
type: 'rect', // default type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit systemBoundaryName rule
|
||||||
|
* Grammar: systemBoundaryName : IDENTIFIER | STRING ;
|
||||||
|
*/
|
||||||
|
private visitSystemBoundaryNameImpl(ctx: SystemBoundaryNameContext): string {
|
||||||
|
const identifier = ctx.IDENTIFIER?.();
|
||||||
|
if (identifier) {
|
||||||
|
return identifier.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
const string = ctx.STRING?.();
|
||||||
|
if (string) {
|
||||||
|
const text = string.getText();
|
||||||
|
// Remove quotes from string
|
||||||
|
return text.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit usecaseInBoundary rule
|
||||||
|
* Grammar: usecaseInBoundary : IDENTIFIER | STRING ;
|
||||||
|
*/
|
||||||
|
private visitUsecaseInBoundaryImpl(ctx: UsecaseInBoundaryContext): string {
|
||||||
|
const identifier = ctx.IDENTIFIER?.();
|
||||||
|
if (identifier) {
|
||||||
|
return identifier.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
const string = ctx.STRING?.();
|
||||||
|
if (string) {
|
||||||
|
const text = string.getText();
|
||||||
|
// Remove quotes from string
|
||||||
|
return text.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit systemBoundaryTypeStatement rule
|
||||||
|
* Grammar: systemBoundaryTypeStatement : systemBoundaryName '\@' '\{' systemBoundaryTypeContent '\}' NEWLINE* ;
|
||||||
|
*/
|
||||||
|
visitSystemBoundaryTypeStatementImpl(ctx: SystemBoundaryTypeStatementContext): void {
|
||||||
|
let boundaryName = '';
|
||||||
|
|
||||||
|
// Get the system boundary name
|
||||||
|
const systemBoundaryName = ctx.systemBoundaryName?.();
|
||||||
|
if (systemBoundaryName) {
|
||||||
|
boundaryName = this.visitSystemBoundaryNameImpl(systemBoundaryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the type configuration
|
||||||
|
let boundaryType: 'package' | 'rect' = 'rect'; // default
|
||||||
|
const systemBoundaryTypeContent = ctx.systemBoundaryTypeContent?.();
|
||||||
|
if (systemBoundaryTypeContent) {
|
||||||
|
boundaryType = this.visitSystemBoundaryTypeContentImpl(systemBoundaryTypeContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the existing system boundary and update its type
|
||||||
|
const existingBoundary = this.systemBoundaries.find((b) => b.id === boundaryName);
|
||||||
|
if (existingBoundary) {
|
||||||
|
existingBoundary.type = boundaryType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit systemBoundaryTypeContent rule
|
||||||
|
* Grammar: systemBoundaryTypeContent : systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)* ;
|
||||||
|
*/
|
||||||
|
private visitSystemBoundaryTypeContentImpl(
|
||||||
|
ctx: SystemBoundaryTypeContentContext
|
||||||
|
): 'package' | 'rect' {
|
||||||
|
// Get all type properties
|
||||||
|
const typeProperties = ctx.systemBoundaryTypeProperty();
|
||||||
|
|
||||||
|
for (const propCtx of typeProperties) {
|
||||||
|
const type = this.visitSystemBoundaryTypePropertyImpl(propCtx);
|
||||||
|
if (type) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'rect'; // default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit systemBoundaryTypeProperty rule
|
||||||
|
* Grammar: systemBoundaryTypeProperty : 'type' ':' systemBoundaryType ;
|
||||||
|
*/
|
||||||
|
private visitSystemBoundaryTypePropertyImpl(
|
||||||
|
ctx: SystemBoundaryTypePropertyContext
|
||||||
|
): 'package' | 'rect' | null {
|
||||||
|
const systemBoundaryType = ctx.systemBoundaryType?.();
|
||||||
|
if (systemBoundaryType) {
|
||||||
|
return this.visitSystemBoundaryTypeImpl(systemBoundaryType);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit systemBoundaryType rule
|
||||||
|
* Grammar: systemBoundaryType : 'package' | 'rect' ;
|
||||||
|
*/
|
||||||
|
private visitSystemBoundaryTypeImpl(ctx: SystemBoundaryTypeContext): 'package' | 'rect' {
|
||||||
|
const text = ctx.getText();
|
||||||
|
if (text === 'package') {
|
||||||
|
return 'package';
|
||||||
|
} else if (text === 'rect') {
|
||||||
|
return 'rect';
|
||||||
|
}
|
||||||
|
return 'rect'; // default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit actorName rule
|
||||||
|
* Grammar: actorName : (IDENTIFIER | STRING) metadata? ;
|
||||||
|
*/
|
||||||
|
private visitActorNameImpl(ctx: ActorNameContext): {
|
||||||
|
name: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
} {
|
||||||
|
let name = '';
|
||||||
|
|
||||||
|
if (ctx.IDENTIFIER?.()) {
|
||||||
|
name = ctx.IDENTIFIER()!.getText();
|
||||||
|
} else if (ctx.STRING?.()) {
|
||||||
|
const text = ctx.STRING()!.getText();
|
||||||
|
// Remove quotes from string
|
||||||
|
name = text.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = undefined;
|
||||||
|
if (ctx.metadata?.()) {
|
||||||
|
metadata = this.visitMetadataImpl(ctx.metadata()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name, metadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit metadata rule
|
||||||
|
* Grammar: metadata : '\@' '\{' metadataContent '\}' ;
|
||||||
|
*/
|
||||||
|
private visitMetadataImpl(ctx: MetadataContext): Record<string, string> {
|
||||||
|
const metadataContent = ctx.metadataContent?.();
|
||||||
|
if (metadataContent) {
|
||||||
|
return this.visitMetadataContentImpl(metadataContent);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit metadataContent rule
|
||||||
|
* Grammar: metadataContent : metadataProperty (',' metadataProperty)* ;
|
||||||
|
*/
|
||||||
|
private visitMetadataContentImpl(ctx: MetadataContentContext): Record<string, string> {
|
||||||
|
const metadata: Record<string, string> = {};
|
||||||
|
const properties = ctx.metadataProperty();
|
||||||
|
|
||||||
|
for (const property of properties) {
|
||||||
|
const { key, value } = this.visitMetadataPropertyImpl(property);
|
||||||
|
metadata[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit metadataProperty rule
|
||||||
|
* Grammar: metadataProperty : STRING ':' STRING ;
|
||||||
|
*/
|
||||||
|
private visitMetadataPropertyImpl(ctx: MetadataPropertyContext): { key: string; value: string } {
|
||||||
|
const strings = ctx.STRING();
|
||||||
|
if (strings.length >= 2) {
|
||||||
|
const key = strings[0].getText().slice(1, -1); // Remove quotes
|
||||||
|
const value = strings[1].getText().slice(1, -1); // Remove quotes
|
||||||
|
return { key, value };
|
||||||
|
}
|
||||||
|
return { key: '', value: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit entityName rule
|
||||||
|
* Grammar: entityName : IDENTIFIER | STRING | nodeIdWithLabel ;
|
||||||
|
*/
|
||||||
|
private visitEntityNameImpl(ctx: EntityNameContext): string {
|
||||||
|
const identifier = ctx.IDENTIFIER?.();
|
||||||
|
if (identifier) {
|
||||||
|
return identifier.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
const string = ctx.STRING?.();
|
||||||
|
if (string) {
|
||||||
|
const text = string.getText();
|
||||||
|
// Remove quotes from string
|
||||||
|
return text.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeIdWithLabel = ctx.nodeIdWithLabel?.();
|
||||||
|
if (nodeIdWithLabel) {
|
||||||
|
return this.visitNodeIdWithLabelImpl(nodeIdWithLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit actorDeclaration rule
|
||||||
|
* Grammar: actorDeclaration : 'actor' actorName ;
|
||||||
|
*/
|
||||||
|
private visitActorDeclarationImpl(ctx: ActorDeclarationContext): string {
|
||||||
|
const actorName = ctx.actorName?.();
|
||||||
|
if (actorName) {
|
||||||
|
const actorResult = this.visitActorNameImpl(actorName);
|
||||||
|
|
||||||
|
// Add the actor if it doesn't already exist
|
||||||
|
const existingActor = this.actors.find((actor) => actor.id === actorResult.name);
|
||||||
|
if (!existingActor) {
|
||||||
|
this.actors.push({
|
||||||
|
id: actorResult.name,
|
||||||
|
name: actorResult.name,
|
||||||
|
metadata: actorResult.metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actorResult.name;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit nodeIdWithLabel rule
|
||||||
|
* Grammar: nodeIdWithLabel : IDENTIFIER '(' nodeLabel ')' ;
|
||||||
|
*/
|
||||||
|
private visitNodeIdWithLabelImpl(ctx: NodeIdWithLabelContext): string {
|
||||||
|
let nodeId = '';
|
||||||
|
let nodeLabel = '';
|
||||||
|
|
||||||
|
const identifier = ctx.IDENTIFIER?.();
|
||||||
|
if (identifier) {
|
||||||
|
nodeId = identifier.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeLabelCtx = ctx.nodeLabel?.();
|
||||||
|
if (nodeLabelCtx) {
|
||||||
|
nodeLabel = this.visitNodeLabelImpl(nodeLabelCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the use case with nodeId and label
|
||||||
|
const existingUseCase = this.useCases.find((uc) => uc.id === nodeLabel || uc.nodeId === nodeId);
|
||||||
|
if (existingUseCase) {
|
||||||
|
// Update existing use case with nodeId if not already set
|
||||||
|
existingUseCase.nodeId ??= nodeId;
|
||||||
|
} else {
|
||||||
|
// Create new use case with nodeId and label
|
||||||
|
this.useCases.push({
|
||||||
|
id: nodeLabel,
|
||||||
|
name: nodeLabel,
|
||||||
|
nodeId: nodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeLabel; // Return the label as the entity name for relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit nodeLabel rule
|
||||||
|
* Grammar: nodeLabel : IDENTIFIER | STRING | nodeLabel IDENTIFIER | nodeLabel STRING ;
|
||||||
|
*/
|
||||||
|
private visitNodeLabelImpl(ctx: NodeLabelContext): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
// Handle recursive nodeLabel structure
|
||||||
|
const nodeLabel = ctx.nodeLabel?.();
|
||||||
|
if (nodeLabel) {
|
||||||
|
parts.push(this.visitNodeLabelImpl(nodeLabel));
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier = ctx.IDENTIFIER?.();
|
||||||
|
if (identifier) {
|
||||||
|
parts.push(identifier.getText());
|
||||||
|
} else {
|
||||||
|
const string = ctx.STRING?.();
|
||||||
|
if (string) {
|
||||||
|
const text = string.getText();
|
||||||
|
// Remove quotes from string
|
||||||
|
parts.push(text.slice(1, -1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit arrow rule
|
||||||
|
* Grammar: arrow : SOLID_ARROW | BACK_ARROW | LINE_SOLID | labeledArrow ;
|
||||||
|
*/
|
||||||
|
private visitArrowImpl(ctx: ArrowContext): { arrowType: ArrowType; label?: string } {
|
||||||
|
// Check if this is a labeled arrow
|
||||||
|
if (ctx.labeledArrow()) {
|
||||||
|
return this.visitLabeledArrowImpl(ctx.labeledArrow()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular arrow without label - determine type from token
|
||||||
|
if (ctx.SOLID_ARROW()) {
|
||||||
|
return { arrowType: ARROW_TYPE.SOLID_ARROW };
|
||||||
|
} else if (ctx.BACK_ARROW()) {
|
||||||
|
return { arrowType: ARROW_TYPE.BACK_ARROW };
|
||||||
|
} else if (ctx.LINE_SOLID()) {
|
||||||
|
return { arrowType: ARROW_TYPE.LINE_SOLID };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (should not happen with proper grammar)
|
||||||
|
return { arrowType: ARROW_TYPE.SOLID_ARROW };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit labeled arrow rule
|
||||||
|
* Grammar: labeledArrow : LINE_SOLID edgeLabel SOLID_ARROW | BACK_ARROW edgeLabel LINE_SOLID | LINE_SOLID edgeLabel LINE_SOLID ;
|
||||||
|
*/
|
||||||
|
private visitLabeledArrowImpl(ctx: LabeledArrowContext): { arrowType: ArrowType; label: string } {
|
||||||
|
const label = this.visitEdgeLabelImpl(ctx.edgeLabel());
|
||||||
|
|
||||||
|
// Determine arrow type based on the tokens present
|
||||||
|
if (ctx.SOLID_ARROW()) {
|
||||||
|
return { arrowType: ARROW_TYPE.SOLID_ARROW, label };
|
||||||
|
} else if (ctx.BACK_ARROW()) {
|
||||||
|
return { arrowType: ARROW_TYPE.BACK_ARROW, label };
|
||||||
|
} else {
|
||||||
|
return { arrowType: ARROW_TYPE.LINE_SOLID, label };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit edge label rule
|
||||||
|
* Grammar: edgeLabel : IDENTIFIER | STRING ;
|
||||||
|
*/
|
||||||
|
private visitEdgeLabelImpl(ctx: EdgeLabelContext): string {
|
||||||
|
const text = ctx.getText();
|
||||||
|
// Remove quotes if it's a string
|
||||||
|
if (
|
||||||
|
(text.startsWith('"') && text.endsWith('"')) ||
|
||||||
|
(text.startsWith("'") && text.endsWith("'"))
|
||||||
|
) {
|
||||||
|
return text.slice(1, -1);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parse result after visiting the diagram
|
||||||
|
*/
|
||||||
|
getParseResult(): UsecaseParseResult {
|
||||||
|
return {
|
||||||
|
actors: this.actors,
|
||||||
|
useCases: this.useCases,
|
||||||
|
systemBoundaries: this.systemBoundaries,
|
||||||
|
relationships: this.relationships,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { LangiumParser, ParseResult } from 'langium';
|
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 } from './index.js';
|
||||||
|
import type { UsecaseParseResult } from './language/usecase/types.js';
|
||||||
|
|
||||||
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
|
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar | UsecaseParseResult;
|
||||||
|
export type LangiumDiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
|
||||||
|
|
||||||
const parsers: Record<string, LangiumParser> = {};
|
const parsers: Record<string, LangiumParser> = {};
|
||||||
const initializers = {
|
const initializers = {
|
||||||
@@ -41,6 +43,9 @@ const initializers = {
|
|||||||
const parser = createTreemapServices().Treemap.parser.LangiumParser;
|
const parser = createTreemapServices().Treemap.parser.LangiumParser;
|
||||||
parsers.treemap = parser;
|
parsers.treemap = parser;
|
||||||
},
|
},
|
||||||
|
usecase: () => {
|
||||||
|
// ANTLR-based parser - no Langium parser needed
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export async function parse(diagramType: 'info', text: string): Promise<Info>;
|
export async function parse(diagramType: 'info', text: string): Promise<Info>;
|
||||||
@@ -50,7 +55,12 @@ export async function parse(diagramType: 'architecture', text: string): Promise<
|
|||||||
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
|
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: 'radar', text: string): Promise<Radar>;
|
||||||
export async function parse(diagramType: 'treemap', text: string): Promise<Treemap>;
|
export async function parse(diagramType: 'treemap', text: string): Promise<Treemap>;
|
||||||
|
export async function parse(diagramType: 'usecase', text: string): Promise<UsecaseParseResult>;
|
||||||
|
|
||||||
|
export async function parse<T extends LangiumDiagramAST>(
|
||||||
|
diagramType: Exclude<keyof typeof initializers, 'usecase'>,
|
||||||
|
text: string
|
||||||
|
): Promise<T>;
|
||||||
export async function parse<T extends DiagramAST>(
|
export async function parse<T extends DiagramAST>(
|
||||||
diagramType: keyof typeof initializers,
|
diagramType: keyof typeof initializers,
|
||||||
text: string
|
text: string
|
||||||
@@ -59,11 +69,19 @@ export async function parse<T extends DiagramAST>(
|
|||||||
if (!initializer) {
|
if (!initializer) {
|
||||||
throw new Error(`Unknown diagram type: ${diagramType}`);
|
throw new Error(`Unknown diagram type: ${diagramType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle ANTLR-based parsers separately
|
||||||
|
if (diagramType === 'usecase') {
|
||||||
|
const { parseUsecaseWithAntlr } = await import('./language/usecase/index.js');
|
||||||
|
return parseUsecaseWithAntlr(text) as T;
|
||||||
|
}
|
||||||
|
|
||||||
if (!parsers[diagramType]) {
|
if (!parsers[diagramType]) {
|
||||||
await initializer();
|
await initializer();
|
||||||
}
|
}
|
||||||
const parser: LangiumParser = parsers[diagramType];
|
const parser: LangiumParser = parsers[diagramType];
|
||||||
const result: ParseResult<T> = parser.parse<T>(text);
|
const result: ParseResult<T extends LangiumDiagramAST ? T : never> =
|
||||||
|
parser.parse<T extends LangiumDiagramAST ? T : never>(text);
|
||||||
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
|
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
|
||||||
throw new MermaidParseError(result);
|
throw new MermaidParseError(result);
|
||||||
}
|
}
|
||||||
|
|||||||
1612
packages/parser/tests/usecase.test.ts
Normal file
1612
packages/parser/tests/usecase.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user