mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-20 06:29:43 +02:00
Compare commits
7 Commits
aggregatio
...
demo/useca
Author | SHA1 | Date | |
---|---|---|---|
![]() |
89b29898d2 | ||
![]() |
2972bf25bf | ||
![]() |
6b1a7a9e1a | ||
![]() |
33bc4a0b4e | ||
![]() |
c6f25167a2 | ||
![]() |
0ef3130510 | ||
![]() |
862d40cc3a |
5
.changeset/clean-wolves-turn.md
Normal file
5
.changeset/clean-wolves-turn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Render newlines as spaces in class diagrams
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add support for aggregation relationships in ER diagram
|
@@ -524,5 +524,18 @@ describe('Class diagram', () => {
|
||||
`,
|
||||
{}
|
||||
);
|
||||
it('should handle an empty class body with empty braces', () => {
|
||||
imgSnapshotTest(
|
||||
` classDiagram
|
||||
class FooBase~T~ {}
|
||||
class Bar {
|
||||
+Zip
|
||||
+Zap()
|
||||
}
|
||||
FooBase <|-- Ba
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -369,94 +369,4 @@ ORDER ||--|{ LINE-ITEM : contains
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Aggregation Relationships', () => {
|
||||
it('should render basic aggregation relationships', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
PROJECT <>.. TASK : manages
|
||||
TEAM <> MEMBER : consists_of
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render aggregation with entity attributes', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
DEPARTMENT {
|
||||
int id PK
|
||||
string name
|
||||
string location
|
||||
}
|
||||
EMPLOYEE {
|
||||
int id PK
|
||||
string name
|
||||
int department_id FK
|
||||
}
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render aggregation with quoted labels', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
UNIVERSITY <> COLLEGE : "has multiple"
|
||||
COLLEGE <> DEPARTMENT : "contains"
|
||||
DEPARTMENT <> FACULTY : "employs"
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render mixed relationship types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ ORDER_ITEM : contains
|
||||
PRODUCT <> ORDER_ITEM : "aggregated in"
|
||||
WAREHOUSE <>.. PRODUCT : "stores"
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render aggregation with entity aliases', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
d[DEPARTMENT]
|
||||
e[EMPLOYEE]
|
||||
p[PROJECT]
|
||||
t[TASK]
|
||||
|
||||
d <> e : contains
|
||||
p <>.. t : manages
|
||||
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render complex aggregation scenarios', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
COMPANY <> DEPARTMENT : owns
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
EMPLOYEE <> PROJECT : works_on
|
||||
PROJECT <> TASK : consists_of
|
||||
TASK <> SUBTASK : includes
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
158
demos/er.html
158
demos/er.html
@@ -169,164 +169,6 @@
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<!-- Aggregation Examples -->
|
||||
<h2>Aggregation Examples</h2>
|
||||
|
||||
<h3>Basic Aggregation (Solid Line)</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
DEPARTMENT {
|
||||
int id PK
|
||||
string name
|
||||
string location
|
||||
}
|
||||
EMPLOYEE {
|
||||
int id PK
|
||||
string name
|
||||
int department_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Dashed Aggregation</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
PROJECT <>.. TASK : manages
|
||||
PROJECT {
|
||||
int id PK
|
||||
string name
|
||||
string description
|
||||
}
|
||||
TASK {
|
||||
int id PK
|
||||
string title
|
||||
int project_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Aggregation with Different Cardinalities</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
COMPANY <> DEPARTMENT : owns
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
EMPLOYEE <> PROJECT : works_on
|
||||
PROJECT <> TASK : consists_of
|
||||
|
||||
COMPANY {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
DEPARTMENT {
|
||||
int id PK
|
||||
string name
|
||||
int company_id FK
|
||||
}
|
||||
EMPLOYEE {
|
||||
int id PK
|
||||
string name
|
||||
int department_id FK
|
||||
}
|
||||
PROJECT {
|
||||
int id PK
|
||||
string name
|
||||
int employee_id FK
|
||||
}
|
||||
TASK {
|
||||
int id PK
|
||||
string title
|
||||
int project_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Two-way Aggregation</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
TEAM <> MEMBER : consists_of
|
||||
TEAM {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
MEMBER {
|
||||
int id PK
|
||||
string name
|
||||
int team_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Complex Aggregation with Labels</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
UNIVERSITY <> COLLEGE : "has multiple"
|
||||
COLLEGE <> DEPARTMENT : "contains"
|
||||
DEPARTMENT <> FACULTY : "employs"
|
||||
FACULTY <> STUDENT : "teaches"
|
||||
|
||||
UNIVERSITY {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
COLLEGE {
|
||||
int id PK
|
||||
string name
|
||||
int university_id FK
|
||||
}
|
||||
DEPARTMENT {
|
||||
int id PK
|
||||
string name
|
||||
int college_id FK
|
||||
}
|
||||
FACULTY {
|
||||
int id PK
|
||||
string name
|
||||
int department_id FK
|
||||
}
|
||||
STUDENT {
|
||||
int id PK
|
||||
string name
|
||||
int faculty_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Mixed Relationship Types</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ ORDER_ITEM : contains
|
||||
PRODUCT <> ORDER_ITEM : "aggregated in"
|
||||
WAREHOUSE <>.. PRODUCT : "stores"
|
||||
|
||||
CUSTOMER {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
ORDER {
|
||||
int id PK
|
||||
int customer_id FK
|
||||
date order_date
|
||||
}
|
||||
ORDER_ITEM {
|
||||
int id PK
|
||||
int order_id FK
|
||||
int product_id FK
|
||||
int quantity
|
||||
}
|
||||
PRODUCT {
|
||||
int id PK
|
||||
string name
|
||||
int warehouse_id FK
|
||||
}
|
||||
WAREHOUSE {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
mermaid.initialize({
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
|
||||
2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
|
||||
3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
|
||||
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
||||
|
@@ -209,42 +209,6 @@ erDiagram
|
||||
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
||||
```
|
||||
|
||||
### Aggregation
|
||||
|
||||
Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints.
|
||||
|
||||
| Value | Alias for | Description |
|
||||
| :---: | :------------------: | ------------------------------ |
|
||||
| <> | _aggregation_ | Basic aggregation (solid line) |
|
||||
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
PROJECT <>.. TASK : manages
|
||||
TEAM <> MEMBER : consists_of
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
PROJECT <>.. TASK : manages
|
||||
TEAM <> MEMBER : consists_of
|
||||
```
|
||||
|
||||
In these examples:
|
||||
|
||||
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation)
|
||||
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation)
|
||||
- `TEAM <> MEMBER` shows that a team consists of members (aggregation)
|
||||
|
||||
**Aggregation vs Association**
|
||||
|
||||
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently
|
||||
- **Association** (`||--`, `}o--`): General relationship between entities
|
||||
|
||||
### Attributes
|
||||
|
||||
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:
|
||||
|
@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
import { treemap } from '../diagrams/treemap/detector.js';
|
||||
import usecase from '../diagrams/useCase/useCaseDetector.js';
|
||||
import '../type.d.ts';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
|
||||
xychart,
|
||||
block,
|
||||
radar,
|
||||
treemap
|
||||
treemap,
|
||||
usecase
|
||||
);
|
||||
};
|
||||
|
@@ -2,7 +2,6 @@ import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { Edge, Node } from '../../rendering-util/types.js';
|
||||
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
|
||||
import { AggregationType } from './erTypes.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
@@ -34,11 +33,6 @@ export class ErDB implements DiagramDB {
|
||||
IDENTIFYING: 'IDENTIFYING',
|
||||
};
|
||||
|
||||
private Aggregation = {
|
||||
AGGREGATION: AggregationType.AGGREGATION,
|
||||
AGGREGATION_DASHED: AggregationType.AGGREGATION_DASHED,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
this.addEntity = this.addEntity.bind(this);
|
||||
@@ -137,31 +131,6 @@ export class ErDB implements DiagramDB {
|
||||
return this.relationships;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate aggregation relationship
|
||||
* @param rSpec - The relationship specification to validate
|
||||
* @returns boolean indicating if the aggregation relationship is valid
|
||||
*/
|
||||
public validateAggregationRelationship(rSpec: RelSpec): boolean {
|
||||
const isAggregation =
|
||||
rSpec.relType === this.Aggregation.AGGREGATION ||
|
||||
rSpec.relType === this.Aggregation.AGGREGATION_DASHED;
|
||||
|
||||
if (!isAggregation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const validCardinalities = [
|
||||
this.Cardinality.ZERO_OR_ONE,
|
||||
this.Cardinality.ZERO_OR_MORE,
|
||||
this.Cardinality.ONE_OR_MORE,
|
||||
this.Cardinality.ONLY_ONE,
|
||||
this.Cardinality.MD_PARENT,
|
||||
];
|
||||
|
||||
return validCardinalities.includes(rSpec.cardA) && validCardinalities.includes(rSpec.cardB);
|
||||
}
|
||||
|
||||
public getDirection() {
|
||||
return this.direction;
|
||||
}
|
||||
@@ -279,17 +248,4 @@ export class ErDB implements DiagramDB {
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getConfig = () => getConfig().er;
|
||||
|
||||
// Getter methods for aggregation constants
|
||||
public get AggregationConstants() {
|
||||
return this.Aggregation;
|
||||
}
|
||||
|
||||
public get CardinalityConstants() {
|
||||
return this.Cardinality;
|
||||
}
|
||||
|
||||
public get IdentificationConstants() {
|
||||
return this.Identification;
|
||||
}
|
||||
}
|
||||
|
@@ -9,10 +9,6 @@ const ERMarkers = {
|
||||
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END',
|
||||
MD_PARENT_END: 'MD_PARENT_END',
|
||||
MD_PARENT_START: 'MD_PARENT_START',
|
||||
AGGREGATION_START: 'AGGREGATION_START',
|
||||
AGGREGATION_END: 'AGGREGATION_END',
|
||||
AGGREGATION_DASHED_START: 'AGGREGATION_DASHED_START',
|
||||
AGGREGATION_DASHED_END: 'AGGREGATION_DASHED_END',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -184,66 +180,6 @@ const insertMarkers = function (elem, conf) {
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
|
||||
|
||||
// Aggregation markers (hollow diamond)
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.AGGREGATION_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('d', 'M18,9 L9,0 L0,9 L9,18 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.AGGREGATION_END)
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('d', 'M2,9 L11,0 L20,9 L11,18 Z');
|
||||
|
||||
// Dashed aggregation markers
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.AGGREGATION_DASHED_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke-dasharray', '3,3')
|
||||
.attr('d', 'M18,9 L9,0 L0,9 L9,18 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.AGGREGATION_DASHED_END)
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke-dasharray', '3,3')
|
||||
.attr('d', 'M2,9 L11,0 L20,9 L11,18 Z');
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
|
@@ -448,11 +448,6 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
svgPath.attr('stroke-dasharray', '8,8');
|
||||
}
|
||||
|
||||
// Handle aggregation relationship styling
|
||||
if (rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION_DASHED) {
|
||||
svgPath.attr('stroke-dasharray', '8,8');
|
||||
}
|
||||
|
||||
// TODO: Understand this better
|
||||
let url = '';
|
||||
if (conf.arrowMarkerAbsolute) {
|
||||
@@ -508,15 +503,6 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle aggregation markers
|
||||
if (
|
||||
rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION ||
|
||||
rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION_DASHED
|
||||
) {
|
||||
// Add aggregation marker at the start (entity B side)
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.AGGREGATION_START + ')');
|
||||
}
|
||||
|
||||
// Now label the relationship
|
||||
|
||||
// Find the half-way point
|
||||
|
@@ -35,15 +35,3 @@ export interface EntityClass {
|
||||
styles: string[];
|
||||
textStyles: string[];
|
||||
}
|
||||
|
||||
// Aggregation relationship types
|
||||
export const AggregationType = {
|
||||
AGGREGATION: 'AGGREGATION',
|
||||
AGGREGATION_DASHED: 'AGGREGATION_DASHED',
|
||||
} as const;
|
||||
|
||||
// Line types for aggregation
|
||||
export const AggregationLineType = {
|
||||
SOLID: 'SOLID',
|
||||
DASHED: 'DASHED',
|
||||
} as const;
|
||||
|
@@ -72,8 +72,6 @@ o\| return 'ZERO_OR_ONE';
|
||||
o\{ return 'ZERO_OR_MORE';
|
||||
\|\{ return 'ONE_OR_MORE';
|
||||
\s*u return 'MD_PARENT';
|
||||
"<>.." return 'AGGREGATION_DASHED';
|
||||
"<>" return 'AGGREGATION';
|
||||
\.\. return 'NON_IDENTIFYING';
|
||||
\-\- return 'IDENTIFYING';
|
||||
"to" return 'IDENTIFYING';
|
||||
@@ -169,47 +167,6 @@ statement
|
||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
||||
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
|
||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
||||
| entityName aggregationRelSpec entityName COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, $2);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList aggregationRelSpec entityName STYLE_SEPARATOR idList COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($5);
|
||||
yy.addRelationship($1, $9, $5, $4);
|
||||
yy.setClass([$1], $3);
|
||||
yy.setClass([$5], $7);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList aggregationRelSpec entityName COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($5);
|
||||
yy.addRelationship($1, $7, $5, $4);
|
||||
yy.setClass([$1], $3);
|
||||
}
|
||||
| entityName aggregationRelSpec entityName STYLE_SEPARATOR idList COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $7, $3, $2);
|
||||
yy.setClass([$3], $5);
|
||||
}
|
||||
| entityName 'AGGREGATION' entityName COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION', cardB: 'ZERO_OR_MORE' });
|
||||
}
|
||||
| entityName 'AGGREGATION_DASHED' entityName COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION_DASHED', cardB: 'ZERO_OR_MORE' });
|
||||
}
|
||||
|
||||
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
@@ -315,17 +272,6 @@ relSpec
|
||||
}
|
||||
;
|
||||
|
||||
aggregationRelSpec
|
||||
: 'AGGREGATION' cardinality cardinality
|
||||
{
|
||||
$$ = { cardA: $2, relType: $1, cardB: $3 };
|
||||
}
|
||||
| 'AGGREGATION_DASHED' cardinality cardinality
|
||||
{
|
||||
$$ = { cardA: $2, relType: $1, cardB: $3 };
|
||||
}
|
||||
;
|
||||
|
||||
cardinality
|
||||
: 'ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE; }
|
||||
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
|
||||
@@ -337,8 +283,6 @@ cardinality
|
||||
relType
|
||||
: 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; }
|
||||
| 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; }
|
||||
| 'AGGREGATION' { $$ = yy.Aggregation.AGGREGATION; }
|
||||
| 'AGGREGATION_DASHED' { $$ = yy.Aggregation.AGGREGATION_DASHED; }
|
||||
;
|
||||
|
||||
role
|
||||
|
@@ -1001,75 +1001,4 @@ describe('when parsing ER diagram it...', function () {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('aggregation relationships', function () {
|
||||
it('should parse basic aggregation syntax', function () {
|
||||
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].roleA).toBe('contains');
|
||||
});
|
||||
|
||||
it('should parse dashed aggregation syntax', function () {
|
||||
erDiagram.parser.parse('erDiagram\nPROJECT <>.. TASK : manages');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].roleA).toBe('manages');
|
||||
});
|
||||
|
||||
it('should parse aggregation with quoted labels', function () {
|
||||
erDiagram.parser.parse('erDiagram\nUNIVERSITY <> COLLEGE : "has multiple"');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(rels[0].roleA).toBe('has multiple');
|
||||
});
|
||||
|
||||
it('should parse multiple aggregation relationships', function () {
|
||||
erDiagram.parser.parse(
|
||||
'erDiagram\nDEPARTMENT <> EMPLOYEE : contains\nPROJECT <>.. TASK : manages'
|
||||
);
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(4);
|
||||
expect(rels.length).toBe(2);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
|
||||
});
|
||||
|
||||
it('should parse aggregation with entity aliases', function () {
|
||||
erDiagram.parser.parse('erDiagram\nd[DEPARTMENT]\ne[EMPLOYEE]\nd <> e : contains');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(erDb.getEntity('d').alias).toBe('DEPARTMENT');
|
||||
expect(erDb.getEntity('e').alias).toBe('EMPLOYEE');
|
||||
});
|
||||
|
||||
it('should validate aggregation relationships', function () {
|
||||
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.validateAggregationRelationship(rels[0].relSpec)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle mixed relationship types', function () {
|
||||
erDiagram.parser.parse(
|
||||
'erDiagram\nCUSTOMER ||--o{ ORDER : places\nPRODUCT <> ORDER_ITEM : "aggregated in"'
|
||||
);
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(4);
|
||||
expect(rels.length).toBe(2);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
|
||||
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -68,32 +68,6 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.aggregation {
|
||||
stroke: ${options.lineColor};
|
||||
stroke-width: 1;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.aggregation-dashed {
|
||||
stroke: ${options.lineColor};
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 8,8;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.aggregation-marker {
|
||||
fill: white !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.aggregation-marker-dashed {
|
||||
fill: white !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 3,3;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
124
packages/mermaid/src/diagrams/useCase/styles.js
Normal file
124
packages/mermaid/src/diagrams/useCase/styles.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const getStyles = (options) =>
|
||||
`
|
||||
.usecase-diagram {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: ${options.fontSize};
|
||||
}
|
||||
|
||||
/* Actor styles */
|
||||
.usecase-actor-man {
|
||||
stroke: ${options.actorBorder};
|
||||
fill: ${options.actorBkg};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.usecase-actor-man circle {
|
||||
fill: ${options.useCaseActorBkg};
|
||||
stroke: ${options.useCaseActorBorder};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.usecase-actor-man line {
|
||||
stroke: ${options.useCaseActorBorder};
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.usecase-actor-man text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
fill: ${options.useCaseActorTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Use case styles */
|
||||
.usecase-usecase {
|
||||
fill: ${options.useCaseUseCaseBkg};
|
||||
stroke: ${options.useCaseUseCaseBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.usecase-usecase text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseUseCaseTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* System boundary styles */
|
||||
.usecase-system-boundary {
|
||||
fill: ${options.useCaseSystemBoundaryBkg};
|
||||
stroke: ${options.useCaseSystemBoundaryBorder};
|
||||
stroke-width: 2px;
|
||||
stroke-dasharray: 5,5;
|
||||
}
|
||||
|
||||
.usecase-system-boundary text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
fill: ${options.useCaseSystemBoundaryTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Arrow and relationship styles */
|
||||
.usecase-arrow {
|
||||
stroke: ${'red'};
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.usecase-arrow-label {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseArrowTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Node styles for standalone nodes */
|
||||
.usecase-node {
|
||||
fill: ${options.useCaseUseCaseBkg};
|
||||
stroke: ${options.useCaseUseCaseBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.usecase-node text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseUseCaseTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.usecase-actor-man:hover circle {
|
||||
fill: ${options.useCaseActorBkg};
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-actor-man:hover line {
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-actor-man:hover text {
|
||||
fill: ${options.useCaseArrowColor};
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.usecase-usecase:hover {
|
||||
fill: ${options.useCaseSystemBoundaryBkg};
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-usecase:hover text {
|
||||
fill: ${options.useCaseArrowColor};
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
586
packages/mermaid/src/diagrams/useCase/useCaseDb.ts
Normal file
586
packages/mermaid/src/diagrams/useCase/useCaseDb.ts
Normal file
@@ -0,0 +1,586 @@
|
||||
// Simple actor type for useCase diagrams
|
||||
interface Actor {
|
||||
type: 'actor';
|
||||
name: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Simple use case type
|
||||
interface UseCase {
|
||||
type: 'useCase';
|
||||
name: string;
|
||||
}
|
||||
|
||||
// System boundary type
|
||||
interface SystemBoundary {
|
||||
type: 'systemBoundary';
|
||||
name: string;
|
||||
useCases: UseCase[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// System boundary metadata type
|
||||
interface SystemBoundaryMetadata {
|
||||
type: 'systemBoundaryMetadata';
|
||||
name: string; // boundary name
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
// Actor-UseCase relationship type
|
||||
interface ActorUseCaseRelationship {
|
||||
type: 'actorUseCaseRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // use case name
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
// Node type
|
||||
interface Node {
|
||||
type: 'node';
|
||||
id: string; // node ID (e.g., 'a', 'b', 'c')
|
||||
label: string; // node label (e.g., 'Go through code')
|
||||
}
|
||||
|
||||
// Actor-Node relationship type
|
||||
interface ActorNodeRelationship {
|
||||
type: 'actorNodeRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // node ID
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
// Inline Actor-Node relationship type
|
||||
interface InlineActorNodeRelationship {
|
||||
type: 'inlineActorNodeRelationship';
|
||||
actor: string; // actor name
|
||||
node: Node; // node definition
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export class UseCaseDB {
|
||||
private actors: Actor[] = [];
|
||||
private systemBoundaries: SystemBoundary[] = [];
|
||||
private systemBoundaryMetadata: SystemBoundaryMetadata[] = [];
|
||||
private useCases: UseCase[] = [];
|
||||
private relationships: ActorUseCaseRelationship[] = [];
|
||||
private nodes: Node[] = [];
|
||||
private nodeRelationships: ActorNodeRelationship[] = [];
|
||||
private inlineRelationships: InlineActorNodeRelationship[] = [];
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.actors = [];
|
||||
this.systemBoundaries = [];
|
||||
this.systemBoundaryMetadata = [];
|
||||
this.useCases = [];
|
||||
this.relationships = [];
|
||||
this.nodes = [];
|
||||
this.nodeRelationships = [];
|
||||
this.inlineRelationships = [];
|
||||
}
|
||||
|
||||
addActor(actor: Actor): void {
|
||||
this.actors.push(actor);
|
||||
}
|
||||
|
||||
addSystemBoundary(boundary: SystemBoundary): void {
|
||||
this.systemBoundaries.push(boundary);
|
||||
}
|
||||
|
||||
addSystemBoundaryMetadata(metadata: SystemBoundaryMetadata): void {
|
||||
this.systemBoundaryMetadata.push(metadata);
|
||||
// Apply metadata to existing system boundary
|
||||
const boundary = this.systemBoundaries.find(b => b.name === metadata.name);
|
||||
if (boundary) {
|
||||
boundary.metadata = metadata.metadata;
|
||||
}
|
||||
}
|
||||
|
||||
addUseCase(useCase: UseCase): void {
|
||||
this.useCases.push(useCase);
|
||||
}
|
||||
|
||||
addRelationship(relationship: ActorUseCaseRelationship): void {
|
||||
this.relationships.push(relationship);
|
||||
}
|
||||
|
||||
addNode(node: Node): void {
|
||||
this.nodes.push(node);
|
||||
}
|
||||
|
||||
addNodeRelationship(relationship: ActorNodeRelationship): void {
|
||||
this.nodeRelationships.push(relationship);
|
||||
}
|
||||
|
||||
addInlineRelationship(relationship: InlineActorNodeRelationship): void {
|
||||
this.inlineRelationships.push(relationship);
|
||||
// Also add the node and actor separately
|
||||
this.addNode(relationship.node);
|
||||
// Add actor if not already exists
|
||||
const actorExists = this.actors.some(actor => actor.name === relationship.actor);
|
||||
if (!actorExists) {
|
||||
this.addActor({
|
||||
type: 'actor',
|
||||
name: relationship.actor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getActors(): Actor[] {
|
||||
return this.actors;
|
||||
}
|
||||
|
||||
getSystemBoundaries(): SystemBoundary[] {
|
||||
return this.systemBoundaries;
|
||||
}
|
||||
|
||||
getSystemBoundaryMetadata(): SystemBoundaryMetadata[] {
|
||||
return this.systemBoundaryMetadata;
|
||||
}
|
||||
|
||||
getUseCases(): UseCase[] {
|
||||
return this.useCases;
|
||||
}
|
||||
|
||||
getRelationships(): ActorUseCaseRelationship[] {
|
||||
return this.relationships;
|
||||
}
|
||||
|
||||
getNodes(): Node[] {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
getNodeRelationships(): ActorNodeRelationship[] {
|
||||
return this.nodeRelationships;
|
||||
}
|
||||
|
||||
getInlineRelationships(): InlineActorNodeRelationship[] {
|
||||
return this.inlineRelationships;
|
||||
}
|
||||
|
||||
parse(text: string): void {
|
||||
this.clear();
|
||||
|
||||
// For now, use the simple parser with enhanced metadata support
|
||||
// TODO: Integrate ANTLR parser in the future
|
||||
|
||||
// Simple parser for usecase diagrams (fallback)
|
||||
const lines = text.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('%'));
|
||||
|
||||
let foundUsecase = false;
|
||||
let inSystemBoundary = false;
|
||||
let currentBoundary: SystemBoundary | null = null;
|
||||
let inMetadataBlock = false;
|
||||
let currentMetadataName = '';
|
||||
let currentMetadataContent = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === 'usecase') {
|
||||
foundUsecase = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundUsecase) {
|
||||
continue
|
||||
};
|
||||
|
||||
if (line.startsWith('actor ')) {
|
||||
const actorPart = line.substring(6).trim();
|
||||
if (actorPart) {
|
||||
// Check if this is an inline actor-node relationship
|
||||
if (this.isInlineActorNodeRelationshipLine(actorPart)) {
|
||||
const relationship = this.parseInlineActorNodeRelationshipLine(actorPart);
|
||||
if (relationship) {
|
||||
this.addInlineRelationship(relationship);
|
||||
}
|
||||
} else {
|
||||
const actors = this.parseActorList(actorPart);
|
||||
actors.forEach((actor: Actor) => this.addActor(actor));
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('systemBoundary ')) {
|
||||
const boundaryPart = line.substring(15).trim();
|
||||
if (boundaryPart.endsWith(' {')) {
|
||||
// New curly brace syntax: systemBoundary Name {
|
||||
const boundaryName = boundaryPart.substring(0, boundaryPart.length - 2).trim();
|
||||
currentBoundary = {
|
||||
type: 'systemBoundary',
|
||||
name: boundaryName,
|
||||
useCases: []
|
||||
};
|
||||
inSystemBoundary = true;
|
||||
} else if (boundaryPart) {
|
||||
// Old syntax: systemBoundary Name (followed by 'end')
|
||||
currentBoundary = {
|
||||
type: 'systemBoundary',
|
||||
name: boundaryPart,
|
||||
useCases: []
|
||||
};
|
||||
inSystemBoundary = true;
|
||||
}
|
||||
} else if (line === 'end' || (line === '}' && !inMetadataBlock)) {
|
||||
if (inSystemBoundary && currentBoundary) {
|
||||
this.addSystemBoundary(currentBoundary);
|
||||
currentBoundary = null;
|
||||
inSystemBoundary = false;
|
||||
}
|
||||
} else if (inSystemBoundary && currentBoundary && line) {
|
||||
// This is a use case inside the system boundary
|
||||
const useCase: UseCase = {
|
||||
type: 'useCase',
|
||||
name: line
|
||||
};
|
||||
currentBoundary.useCases.push(useCase);
|
||||
} else if (line && !inSystemBoundary) {
|
||||
// Handle multi-line metadata blocks
|
||||
if (inMetadataBlock) {
|
||||
if (line.includes('}')) {
|
||||
// End of metadata block
|
||||
currentMetadataContent += line.replace('}', '').trim();
|
||||
const metadata = this.parseMetadataContent(currentMetadataName, currentMetadataContent);
|
||||
if (metadata) {
|
||||
this.addSystemBoundaryMetadata(metadata);
|
||||
}
|
||||
inMetadataBlock = false;
|
||||
currentMetadataName = '';
|
||||
currentMetadataContent = '';
|
||||
} else {
|
||||
// Continue collecting metadata content
|
||||
currentMetadataContent += line.trim() + ' ';
|
||||
}
|
||||
} else if (line.includes('@{')) {
|
||||
// Start of metadata block
|
||||
const match = line.match(/^(\w+)@\{(.*)$/);
|
||||
if (match) {
|
||||
currentMetadataName = match[1];
|
||||
const content = match[2].trim();
|
||||
if (content.includes('}')) {
|
||||
// Single line metadata
|
||||
const metadata = this.parseMetadataContent(currentMetadataName, content.replace('}', ''));
|
||||
if (metadata) {
|
||||
this.addSystemBoundaryMetadata(metadata);
|
||||
}
|
||||
} else {
|
||||
// Multi-line metadata
|
||||
inMetadataBlock = true;
|
||||
currentMetadataContent = content + ' ';
|
||||
}
|
||||
}
|
||||
} else if (this.isRelationshipLine(line)) {
|
||||
// Check if this is a relationship (actor --> usecase or actor --> node)
|
||||
const relationship = this.parseRelationshipLine(line);
|
||||
if (relationship) {
|
||||
if (relationship.type === 'actorUseCaseRelationship') {
|
||||
this.addRelationship(relationship);
|
||||
} else if (relationship.type === 'actorNodeRelationship') {
|
||||
this.addNodeRelationship(relationship);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a standalone use case
|
||||
const useCase: UseCase = {
|
||||
type: 'useCase',
|
||||
name: line
|
||||
};
|
||||
this.addUseCase(useCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private parseActorList(actorPart: string): Actor[] {
|
||||
// Smart split by comma that respects metadata braces
|
||||
const actorNames = this.smartSplitActors(actorPart);
|
||||
|
||||
return actorNames.map(actorName => this.parseActorWithMetadata(actorName));
|
||||
}
|
||||
|
||||
private smartSplitActors(input: string): string[] {
|
||||
const actors: string[] = [];
|
||||
let current = '';
|
||||
let braceDepth = 0;
|
||||
let inQuotes = false;
|
||||
let quoteChar = '';
|
||||
|
||||
for (const char of input) {
|
||||
|
||||
if (!inQuotes && (char === '"' || char === "'")) {
|
||||
inQuotes = true;
|
||||
quoteChar = char;
|
||||
current += char;
|
||||
} else if (inQuotes && char === quoteChar) {
|
||||
inQuotes = false;
|
||||
quoteChar = '';
|
||||
current += char;
|
||||
} else if (!inQuotes && char === '{') {
|
||||
braceDepth++;
|
||||
current += char;
|
||||
} else if (!inQuotes && char === '}') {
|
||||
braceDepth--;
|
||||
current += char;
|
||||
} else if (!inQuotes && char === ',' && braceDepth === 0) {
|
||||
// This is a real separator, not inside metadata
|
||||
if (current.trim()) {
|
||||
actors.push(current.trim());
|
||||
}
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last actor
|
||||
if (current.trim()) {
|
||||
actors.push(current.trim());
|
||||
}
|
||||
|
||||
return actors;
|
||||
}
|
||||
|
||||
private parseActorWithMetadata(actorPart: string): Actor {
|
||||
// Check if there's metadata (contains @{...})
|
||||
const metadataRegex = /^([^@]+)@{([^}]*)}$/;
|
||||
const metadataMatch = metadataRegex.exec(actorPart);
|
||||
|
||||
if (metadataMatch) {
|
||||
const name = metadataMatch[1].trim();
|
||||
const metadataStr = metadataMatch[2].trim();
|
||||
const metadata = this.parseMetadataString(metadataStr);
|
||||
|
||||
return {
|
||||
type: 'actor',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
} else {
|
||||
// No metadata, just return the name
|
||||
return {
|
||||
type: 'actor',
|
||||
name: actorPart
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private parseMetadataString(metadataStr: string): Record<string, string> {
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
if (!metadataStr.trim()) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// Split by comma and parse key-value pairs
|
||||
const pairs = metadataStr.split(',');
|
||||
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private isRelationshipLine(line: string): boolean {
|
||||
return line.includes('-->') || line.includes('->');
|
||||
}
|
||||
|
||||
private parseRelationshipLine(line: string): ActorUseCaseRelationship | ActorNodeRelationship | null {
|
||||
let arrow = '';
|
||||
let label: string | undefined;
|
||||
let parts: string[] = [];
|
||||
|
||||
// Check for labeled arrows first (--label--> or --label->)
|
||||
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
|
||||
if (labeledArrowMatch) {
|
||||
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
|
||||
arrow = labeledArrowMatch[2];
|
||||
// Extract label from arrow
|
||||
const labelMatch = arrow.match(/^--(\w+)--?>$/);
|
||||
if (labelMatch) {
|
||||
label = labelMatch[1];
|
||||
}
|
||||
} else if (line.includes('-->')) {
|
||||
arrow = '-->';
|
||||
parts = line.split('-->').map(part => part.trim());
|
||||
} else if (line.includes('->')) {
|
||||
arrow = '->';
|
||||
parts = line.split('->').map(part => part.trim());
|
||||
}
|
||||
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
// Check if target is a node definition (contains parentheses)
|
||||
if (this.isNodeDefinitionString(parts[1])) {
|
||||
const node = this.parseNodeDefinitionString(parts[1]);
|
||||
if (node) {
|
||||
this.addNode(node);
|
||||
return {
|
||||
type: 'actorNodeRelationship',
|
||||
from: parts[0],
|
||||
to: node.id,
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'actorUseCaseRelationship',
|
||||
from: parts[0],
|
||||
to: parts[1],
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isInlineActorNodeRelationshipLine(line: string): boolean {
|
||||
// Check for pattern: ActorName --> nodeId(label) or ActorName --label--> nodeId(label)
|
||||
const hasArrow = line.includes('-->') || line.includes('->') || !!line.match(/--\w+-->/);
|
||||
const hasNodeDefinition = line.includes('(') && line.includes(')');
|
||||
return hasArrow && hasNodeDefinition;
|
||||
}
|
||||
|
||||
private parseInlineActorNodeRelationshipLine(line: string): InlineActorNodeRelationship | null {
|
||||
let arrow = '';
|
||||
let label: string | undefined;
|
||||
let parts: string[] = [];
|
||||
|
||||
// Check for labeled arrows first (--label--> or --label->)
|
||||
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
|
||||
if (labeledArrowMatch) {
|
||||
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
|
||||
arrow = labeledArrowMatch[2];
|
||||
// Extract label from arrow
|
||||
const labelMatch = arrow.match(/^--(\w+)--?>$/);
|
||||
if (labelMatch) {
|
||||
label = labelMatch[1];
|
||||
}
|
||||
} else if (line.includes('-->')) {
|
||||
arrow = '-->';
|
||||
parts = line.split('-->').map(part => part.trim());
|
||||
} else if (line.includes('->')) {
|
||||
arrow = '->';
|
||||
parts = line.split('->').map(part => part.trim());
|
||||
}
|
||||
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
const node = this.parseNodeDefinitionString(parts[1]);
|
||||
if (node) {
|
||||
return {
|
||||
type: 'inlineActorNodeRelationship',
|
||||
actor: parts[0],
|
||||
node,
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isNodeDefinitionString(str: string): boolean {
|
||||
return str.includes('(') && str.includes(')');
|
||||
}
|
||||
|
||||
private parseNodeDefinitionString(str: string): Node | null {
|
||||
const match = str.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\((.+)\)$/);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'node',
|
||||
id: match[1],
|
||||
label: match[2]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isSystemBoundaryMetadataLine(line: string): boolean {
|
||||
// Check for pattern: boundaryName@{...}
|
||||
return line.includes('@{') && line.includes('}');
|
||||
}
|
||||
|
||||
private parseSystemBoundaryMetadataLine(line: string): SystemBoundaryMetadata | null {
|
||||
// Parse pattern: boundaryName@{key: value, key2: value2}
|
||||
const match = line.match(/^(\w+)@\{(.+)\}$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = match[1];
|
||||
const metadataContent = match[2];
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse key-value pairs
|
||||
const pairs = metadataContent.split(',').map(pair => pair.trim());
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
private parseMetadataContent(name: string, content: string): SystemBoundaryMetadata | null {
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse key-value pairs from content
|
||||
const pairs = content.split(',').map(pair => pair.trim()).filter(pair => pair);
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
}
|
24
packages/mermaid/src/diagrams/useCase/useCaseDetector.ts
Normal file
24
packages/mermaid/src/diagrams/useCase/useCaseDetector.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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 };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
1421
packages/mermaid/src/diagrams/useCase/useCaseDiagram.spec.js
Normal file
1421
packages/mermaid/src/diagrams/useCase/useCaseDiagram.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
33
packages/mermaid/src/diagrams/useCase/useCaseDiagram.ts
Normal file
33
packages/mermaid/src/diagrams/useCase/useCaseDiagram.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { UseCaseDB } from './useCaseDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './useCaseRenderer.js';
|
||||
|
||||
// Shared database instance
|
||||
let db: UseCaseDB;
|
||||
|
||||
// Create a simple parser that integrates with our custom parser
|
||||
const parser = {
|
||||
parse: (text: string) => {
|
||||
// Use the shared database instance
|
||||
db.parse(text);
|
||||
},
|
||||
};
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
get db() {
|
||||
if (!db) {
|
||||
db = new UseCaseDB();
|
||||
}
|
||||
return db;
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
// Initialize configuration if needed
|
||||
if (!db) {
|
||||
db = new UseCaseDB();
|
||||
}
|
||||
},
|
||||
};
|
619
packages/mermaid/src/diagrams/useCase/useCaseRenderer.ts
Normal file
619
packages/mermaid/src/diagrams/useCase/useCaseRenderer.ts
Normal file
@@ -0,0 +1,619 @@
|
||||
import { select } from 'd3';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import type { UseCaseDB } from './useCaseDb.js';
|
||||
import { log } from '../../logger.js';
|
||||
|
||||
// Position interfaces
|
||||
interface NodePosition {
|
||||
name: string; // node ID (for relationship matching)
|
||||
label: string; // node label (for display)
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// Constants for actor rendering
|
||||
const ACTOR_TYPE_WIDTH = 36; // 18 * 2 from sequence diagram
|
||||
const ACTOR_MAN_FIGURE_CLASS = 'usecase-actor-man';
|
||||
const ACTOR_SPACING = 120; // Horizontal spacing between actors
|
||||
const ACTOR_HEIGHT = 80; // Height of actor figure
|
||||
const MARGIN = 50; // Margin around the diagram
|
||||
|
||||
// Simple actor interface for positioning
|
||||
interface ActorPosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// System boundary interface for positioning
|
||||
interface SystemBoundaryPosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
useCases: UseCasePosition[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Use case interface for positioning
|
||||
interface UseCasePosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a stick figure actor similar to sequence diagrams but optimized for useCase
|
||||
*/
|
||||
const drawActorTypeActor = (elem: any, actor: ActorPosition, conf: any): number => {
|
||||
const center = actor.x + actor.width / 2;
|
||||
const actorY = actor.y;
|
||||
|
||||
// Create actor group
|
||||
const actElem = elem.append('g');
|
||||
actElem.attr('class', ACTOR_MAN_FIGURE_CLASS);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
// Draw stick figure
|
||||
// Head (circle)
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', center)
|
||||
.attr('cy', actorY + 15)
|
||||
.attr('r', 10);
|
||||
|
||||
// Body (torso line)
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 25)
|
||||
.attr('x2', center)
|
||||
.attr('y2', actorY + 50)
|
||||
.style('stroke', 'black');
|
||||
|
||||
// Arms (horizontal line)
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center - ACTOR_TYPE_WIDTH / 2)
|
||||
.attr('y1', actorY + 35)
|
||||
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
|
||||
.style('stroke', 'black')
|
||||
.attr('y2', actorY + 35);
|
||||
|
||||
// Left leg
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 50)
|
||||
.attr('x2', center - ACTOR_TYPE_WIDTH / 2)
|
||||
.style('stroke', 'black')
|
||||
.attr('y2', actorY + 70);
|
||||
|
||||
// Right leg
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 50)
|
||||
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
|
||||
.attr('y2', actorY + 70)
|
||||
.style('stroke', 'black');
|
||||
|
||||
// Actor name text
|
||||
const textY = actorY + ACTOR_HEIGHT + 15;
|
||||
drawActorText(actor.name, actElem, actor.x, textY, actor.width, 20);
|
||||
|
||||
return ACTOR_HEIGHT; // Total height including text and metadata
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws text for actor name - simplified version of sequence diagram text drawing
|
||||
*/
|
||||
const drawActorText = (content: string, g: any, x: number, y: number, width: number, height: number): void => {
|
||||
g.append('text')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('y', y + height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.text(content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a system boundary box with use cases inside
|
||||
*/
|
||||
const drawSystemBoundary = (g: any, boundary: SystemBoundaryPosition, conf: any): void => {
|
||||
// Determine boundary type from metadata (default to 'rect')
|
||||
const boundaryType = boundary.metadata?.type || 'rect';
|
||||
|
||||
if (boundaryType === 'package') {
|
||||
// Draw package-style boundary with title box
|
||||
const titleHeight = 25;
|
||||
const titleWidth = Math.max(100, boundary.name.length * 8 + 20);
|
||||
|
||||
// Draw main boundary rectangle
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y + titleHeight)
|
||||
.attr('width', boundary.width)
|
||||
.attr('height', boundary.height - titleHeight)
|
||||
.attr('class', 'usecase-system-boundary')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw title box
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y)
|
||||
.attr('width', titleWidth)
|
||||
.attr('height', titleHeight)
|
||||
.attr('class', 'usecase-system-boundary')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw title text
|
||||
g.append('text')
|
||||
.attr('x', boundary.x + titleWidth / 2)
|
||||
.attr('y', boundary.y + titleHeight / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.style('font-size', '14px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('font-family', 'Arial, sans-serif')
|
||||
.style('fill', '#333')
|
||||
.text(boundary.name);
|
||||
} else {
|
||||
// Draw rect-style boundary (default)
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y)
|
||||
.attr('width', boundary.width)
|
||||
.attr('height', boundary.height)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,5');
|
||||
|
||||
// Draw boundary title
|
||||
g.append('text')
|
||||
.attr('x', boundary.x + 10)
|
||||
.attr('y', boundary.y + 20)
|
||||
.style('font-size', '16px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('font-family', 'Arial, sans-serif')
|
||||
.style('fill', '#333')
|
||||
.text(boundary.name);
|
||||
}
|
||||
|
||||
// Draw use cases inside the boundary
|
||||
boundary.useCases.forEach((useCase) => {
|
||||
// Draw use case oval
|
||||
g.append('ellipse')
|
||||
.attr('cx', useCase.x + useCase.width / 2)
|
||||
.attr('cy', useCase.y + useCase.height / 2)
|
||||
.attr('rx', useCase.width / 2)
|
||||
.attr('ry', useCase.height / 2)
|
||||
.attr('class', 'usecase-usecase')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333');
|
||||
|
||||
// Draw use case text
|
||||
g.append('text')
|
||||
.attr('x', useCase.x + useCase.width / 2)
|
||||
.attr('y', useCase.y + useCase.height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.text(useCase.name);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a standalone node as an oval
|
||||
*/
|
||||
const drawNode = (g: any, nodePos: NodePosition): void => {
|
||||
const nodeGroup = g.append('g').attr('class', `node-${nodePos.name}`);
|
||||
|
||||
// Draw oval background
|
||||
nodeGroup.append('ellipse')
|
||||
.attr('cx', nodePos.x + nodePos.width / 2)
|
||||
.attr('cy', nodePos.y + nodePos.height / 2)
|
||||
.attr('rx', nodePos.width / 2)
|
||||
.attr('ry', nodePos.height / 2)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('class', 'usecase-node');
|
||||
|
||||
// Add node label
|
||||
nodeGroup.append('text')
|
||||
.attr('x', nodePos.x + nodePos.width / 2)
|
||||
.attr('y', nodePos.y + nodePos.height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.text(nodePos.label);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship between entities (actor-to-usecase or actor-to-actor)
|
||||
*/
|
||||
const drawRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], boundaryPositions: SystemBoundaryPosition[], conf: any): void => {
|
||||
// Find the source entity (always an actor)
|
||||
const fromEntity = actorPositions.find(a => a.name === relationship.from);
|
||||
if (!fromEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the target entity (could be a use case or another actor)
|
||||
let toEntity: UseCasePosition | ActorPosition | undefined;
|
||||
let isTargetUseCase = false;
|
||||
|
||||
// First check if target is a use case in system boundaries
|
||||
for (const boundary of boundaryPositions) {
|
||||
toEntity = boundary.useCases.find(uc => uc.name === relationship.to);
|
||||
if (toEntity) {
|
||||
isTargetUseCase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in boundaries, check if target is another actor
|
||||
toEntity ??= actorPositions.find(a => a.name === relationship.to);
|
||||
|
||||
if (!toEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate connection points
|
||||
const fromCenterX = fromEntity.x + fromEntity.width / 2;
|
||||
const fromCenterY = fromEntity.y + fromEntity.height / 2;
|
||||
|
||||
// For use cases, connect to the edge (left side), for actors connect to center
|
||||
const toCenterX = isTargetUseCase ? toEntity.x : toEntity.x + toEntity.width / 2;
|
||||
const toCenterY = isTargetUseCase ? toEntity.y + toEntity.height / 2 : toEntity.y + toEntity.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', fromCenterX)
|
||||
.attr('y1', fromCenterY)
|
||||
.attr('x2', toCenterX)
|
||||
.attr('y2', toCenterY)
|
||||
.attr('class', 'usecase-arrow')
|
||||
.attr('stroke', '#333')
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (fromCenterX + toCenterX) / 2;
|
||||
const midY = (fromCenterY + toCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('class', 'usecase-arrow-label')
|
||||
.attr('stroke', '#333')
|
||||
.attr('font-weight', 200)
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship between an actor and a standalone node
|
||||
*/
|
||||
const drawNodeRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
|
||||
// Find the actor position
|
||||
const actor = actorPositions.find(a => a.name === relationship.from);
|
||||
if (!actor) {return};
|
||||
|
||||
// Find the node position
|
||||
const node = nodePositions.find(n => n.name === relationship.to);
|
||||
if (!node) {return};
|
||||
|
||||
// Calculate connection points
|
||||
const actorCenterX = actor.x + actor.width / 2;
|
||||
const actorCenterY = actor.y + actor.height / 2;
|
||||
|
||||
// For nodes (which are like use cases), connect to the edge (left side)
|
||||
const nodeCenterX = node.x;
|
||||
const nodeCenterY = node.y + node.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', actorCenterX)
|
||||
.attr('y1', actorCenterY)
|
||||
.attr('x2', nodeCenterX)
|
||||
.attr('y2', nodeCenterY)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (actorCenterX + nodeCenterX) / 2;
|
||||
const midY = (actorCenterY + nodeCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-family', 'Arial, sans-serif')
|
||||
.attr('fill', '#333')
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship from an inline actor-node definition
|
||||
*/
|
||||
const drawInlineRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
|
||||
// Find the actor position
|
||||
const actor = actorPositions.find(a => a.name === relationship.actor);
|
||||
if (!actor) {return};
|
||||
|
||||
// Find the node position by node ID
|
||||
const node = nodePositions.find(n => n.name === relationship.node.id);
|
||||
if (!node) {return};
|
||||
|
||||
// Calculate connection points
|
||||
const actorCenterX = actor.x + actor.width / 2;
|
||||
const actorCenterY = actor.y + actor.height / 2;
|
||||
|
||||
// For nodes (which are like use cases), connect to the edge (left side)
|
||||
const nodeCenterX = node.x;
|
||||
const nodeCenterY = node.y + node.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', actorCenterX)
|
||||
.attr('y1', actorCenterY)
|
||||
.attr('x2', nodeCenterX)
|
||||
.attr('y2', nodeCenterY)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (actorCenterX + nodeCenterX) / 2;
|
||||
const midY = (actorCenterY + nodeCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-family', 'Arial, sans-serif')
|
||||
.attr('fill', '#333')
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main draw function for useCase diagrams
|
||||
*/
|
||||
const draw = (text: string, id: string, version: string, diagram: Diagram): void => {
|
||||
const db = diagram.db as UseCaseDB;
|
||||
|
||||
log.debug('Drawing useCase diagram', id);
|
||||
|
||||
const actors = db.getActors();
|
||||
const systemBoundaries = db.getSystemBoundaries();
|
||||
const useCases = db.getUseCases();
|
||||
const relationships = db.getRelationships();
|
||||
const nodes = db.getNodes();
|
||||
const nodeRelationships = db.getNodeRelationships();
|
||||
const inlineRelationships = db.getInlineRelationships();
|
||||
|
||||
// Create SVG container - use the same approach as other diagrams
|
||||
const svg = select(`[id="${id}"]`);
|
||||
svg.selectAll('*').remove();
|
||||
|
||||
if (actors.length === 0 && systemBoundaries.length === 0 && useCases.length === 0 && relationships.length === 0 && nodes.length === 0 && nodeRelationships.length === 0 && inlineRelationships.length === 0) {
|
||||
// Empty diagram
|
||||
svg.attr('width', 200);
|
||||
svg.attr('height', 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate layout
|
||||
let currentX = MARGIN;
|
||||
let currentY = MARGIN;
|
||||
let maxHeight = 0;
|
||||
|
||||
// Position actors
|
||||
const actorPositions: ActorPosition[] = actors.map((actor, index) => ({
|
||||
name: actor.name,
|
||||
x: currentX + index * ACTOR_SPACING,
|
||||
y: currentY,
|
||||
width: ACTOR_TYPE_WIDTH + 20, // Extra width for text
|
||||
height: ACTOR_HEIGHT,
|
||||
metadata: actor.metadata
|
||||
}));
|
||||
|
||||
if (actors.length > 0) {
|
||||
currentX += actors.length * ACTOR_SPACING;
|
||||
maxHeight = Math.max(maxHeight, ACTOR_HEIGHT + 50);
|
||||
}
|
||||
|
||||
// Position system boundaries
|
||||
const boundaryPositions: SystemBoundaryPosition[] = systemBoundaries.map((boundary, index) => {
|
||||
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
|
||||
const boundaryHeight = 150;
|
||||
|
||||
const position: SystemBoundaryPosition = {
|
||||
name: boundary.name,
|
||||
x: currentX + index * (boundaryWidth + 50),
|
||||
y: currentY,
|
||||
width: boundaryWidth,
|
||||
height: boundaryHeight,
|
||||
metadata: boundary.metadata,
|
||||
useCases: boundary.useCases.map((useCase, ucIndex) => ({
|
||||
name: useCase.name,
|
||||
x: currentX + index * (boundaryWidth + 50) + 20 + ucIndex * 100,
|
||||
y: currentY + 40,
|
||||
width: 80,
|
||||
height: 40
|
||||
}))
|
||||
};
|
||||
|
||||
return position;
|
||||
});
|
||||
|
||||
if (systemBoundaries.length > 0) {
|
||||
const totalBoundaryWidth = systemBoundaries.reduce((sum, boundary, index) => {
|
||||
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
|
||||
return sum + boundaryWidth + (index > 0 ? 50 : 0);
|
||||
}, 0);
|
||||
currentX += totalBoundaryWidth;
|
||||
maxHeight = Math.max(maxHeight, 150);
|
||||
}
|
||||
|
||||
// Position standalone nodes
|
||||
|
||||
const nodePositions: NodePosition[] = [];
|
||||
if (nodes.length > 0) {
|
||||
currentX += 50; // Add some spacing
|
||||
nodes.forEach((node, index) => {
|
||||
const nodeWidth = Math.max(100, node.label.length * 8);
|
||||
const nodeHeight = 40;
|
||||
|
||||
nodePositions.push({
|
||||
name: node.id,
|
||||
label: node.label,
|
||||
x: currentX,
|
||||
y: MARGIN + 50,
|
||||
width: nodeWidth,
|
||||
height: nodeHeight
|
||||
});
|
||||
|
||||
currentX += nodeWidth + 50;
|
||||
});
|
||||
maxHeight = Math.max(maxHeight, 90);
|
||||
}
|
||||
|
||||
// Create main group
|
||||
const g = svg.append('g').attr('class', 'usecase-diagram');
|
||||
|
||||
// Default configuration
|
||||
const conf = {
|
||||
actorFontSize: '14px',
|
||||
actorFontFamily: 'Arial, sans-serif',
|
||||
actorFontWeight: 'normal'
|
||||
};
|
||||
|
||||
// Draw all actors
|
||||
actorPositions.forEach((actorPos) => {
|
||||
const height = drawActorTypeActor(g, actorPos, conf);
|
||||
maxHeight = Math.max(maxHeight, height);
|
||||
});
|
||||
|
||||
// Draw system boundaries
|
||||
boundaryPositions.forEach((boundaryPos) => {
|
||||
drawSystemBoundary(g, boundaryPos, conf);
|
||||
});
|
||||
|
||||
// Draw standalone nodes
|
||||
nodePositions.forEach((nodePos) => {
|
||||
drawNode(g, nodePos);
|
||||
});
|
||||
|
||||
// Draw relationships (arrows)
|
||||
relationships.forEach((relationship) => {
|
||||
drawRelationship(g, relationship, actorPositions, boundaryPositions, conf);
|
||||
});
|
||||
|
||||
// Draw node relationships (arrows to standalone nodes)
|
||||
nodeRelationships.forEach((relationship) => {
|
||||
drawNodeRelationship(g, relationship, actorPositions, nodePositions, conf);
|
||||
});
|
||||
|
||||
// Draw inline relationships (from inline actor-node definitions)
|
||||
inlineRelationships.forEach((relationship) => {
|
||||
drawInlineRelationship(g, relationship, actorPositions, nodePositions, conf);
|
||||
});
|
||||
|
||||
// Calculate total dimensions
|
||||
let totalWidth = MARGIN;
|
||||
if (actors.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, actorPositions[actorPositions.length - 1].x + actorPositions[actorPositions.length - 1].width + MARGIN);
|
||||
}
|
||||
if (systemBoundaries.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, boundaryPositions[boundaryPositions.length - 1].x + boundaryPositions[boundaryPositions.length - 1].width + MARGIN);
|
||||
}
|
||||
if (nodePositions.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, nodePositions[nodePositions.length - 1].x + nodePositions[nodePositions.length - 1].width + MARGIN);
|
||||
}
|
||||
|
||||
const totalHeight = MARGIN + maxHeight + MARGIN;
|
||||
|
||||
// Set SVG dimensions
|
||||
svg.attr('width', totalWidth);
|
||||
svg.attr('height', totalHeight);
|
||||
svg.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
|
||||
};
|
||||
|
||||
export default {
|
||||
draw,
|
||||
};
|
@@ -151,35 +151,6 @@ erDiagram
|
||||
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
||||
```
|
||||
|
||||
### Aggregation
|
||||
|
||||
Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints.
|
||||
|
||||
| Value | Alias for | Description |
|
||||
| :---: | :------------------: | ------------------------------ |
|
||||
| <> | _aggregation_ | Basic aggregation (solid line) |
|
||||
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
PROJECT <>.. TASK : manages
|
||||
TEAM <> MEMBER : consists_of
|
||||
```
|
||||
|
||||
In these examples:
|
||||
|
||||
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation)
|
||||
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation)
|
||||
- `TEAM <> MEMBER` shows that a team consists of members (aggregation)
|
||||
|
||||
**Aggregation vs Association**
|
||||
|
||||
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently
|
||||
- **Association** (`||--`, `}o--`): General relationship between entities
|
||||
|
||||
### Attributes
|
||||
|
||||
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:
|
||||
|
@@ -19,7 +19,9 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist src/language/generated",
|
||||
"langium:generate": "langium generate",
|
||||
"langium:watch": "langium generate --watch"
|
||||
"langium:watch": "langium generate --watch",
|
||||
"antlr:generate": "antlr4ts -visitor -listener -o src/language/useCase/generated src/language/useCase/Usecase.g4",
|
||||
"generate": "npm run langium:generate && npm run antlr:generate"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,6 +35,8 @@
|
||||
"ast"
|
||||
],
|
||||
"dependencies": {
|
||||
"antlr4ts": "0.5.0-alpha.4",
|
||||
"antlr4ts-cli": "0.5.0-alpha.4",
|
||||
"langium": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -45,3 +45,4 @@ export * from './pie/index.js';
|
||||
export * from './architecture/index.js';
|
||||
export * from './radar/index.js';
|
||||
export * from './treemap/index.js';
|
||||
export * from './useCase/index.js';
|
||||
|
70
packages/parser/src/language/useCase/.antlr/Usecase.interp
Normal file
70
packages/parser/src/language/useCase/.antlr/Usecase.interp
Normal file
File diff suppressed because one or more lines are too long
29
packages/parser/src/language/useCase/.antlr/Usecase.tokens
Normal file
29
packages/parser/src/language/useCase/.antlr/Usecase.tokens
Normal file
@@ -0,0 +1,29 @@
|
||||
USECASE_START=1
|
||||
ACTOR=2
|
||||
SYSTEM_BOUNDARY=3
|
||||
END=4
|
||||
ARROW=5
|
||||
LABELED_ARROW=6
|
||||
AT=7
|
||||
LBRACE=8
|
||||
RBRACE=9
|
||||
LPAREN=10
|
||||
RPAREN=11
|
||||
COMMA=12
|
||||
COLON=13
|
||||
STRING=14
|
||||
IDENTIFIER=15
|
||||
NEWLINE=16
|
||||
WS=17
|
||||
COMMENT=18
|
||||
'usecase'=1
|
||||
'actor'=2
|
||||
'systemBoundary'=3
|
||||
'end'=4
|
||||
'@'=7
|
||||
'{'=8
|
||||
'}'=9
|
||||
'('=10
|
||||
')'=11
|
||||
','=12
|
||||
':'=13
|
@@ -0,0 +1,71 @@
|
||||
token literal names:
|
||||
null
|
||||
'usecase'
|
||||
'actor'
|
||||
'systemBoundary'
|
||||
'end'
|
||||
null
|
||||
null
|
||||
'@'
|
||||
'{'
|
||||
'}'
|
||||
'('
|
||||
')'
|
||||
','
|
||||
':'
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
|
||||
token symbolic names:
|
||||
null
|
||||
USECASE_START
|
||||
ACTOR
|
||||
SYSTEM_BOUNDARY
|
||||
END
|
||||
ARROW
|
||||
LABELED_ARROW
|
||||
AT
|
||||
LBRACE
|
||||
RBRACE
|
||||
LPAREN
|
||||
RPAREN
|
||||
COMMA
|
||||
COLON
|
||||
STRING
|
||||
IDENTIFIER
|
||||
NEWLINE
|
||||
WS
|
||||
COMMENT
|
||||
|
||||
rule names:
|
||||
USECASE_START
|
||||
ACTOR
|
||||
SYSTEM_BOUNDARY
|
||||
END
|
||||
ARROW
|
||||
LABELED_ARROW
|
||||
AT
|
||||
LBRACE
|
||||
RBRACE
|
||||
LPAREN
|
||||
RPAREN
|
||||
COMMA
|
||||
COLON
|
||||
STRING
|
||||
IDENTIFIER
|
||||
NEWLINE
|
||||
WS
|
||||
COMMENT
|
||||
|
||||
channel names:
|
||||
DEFAULT_TOKEN_CHANNEL
|
||||
HIDDEN
|
||||
|
||||
mode names:
|
||||
DEFAULT_MODE
|
||||
|
||||
atn:
|
||||
[4, 0, 18, 154, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 76, 8, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 93, 8, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 111, 8, 13, 10, 13, 12, 13, 114, 9, 13, 1, 13, 1, 13, 1, 13, 5, 13, 119, 8, 13, 10, 13, 12, 13, 122, 9, 13, 1, 13, 3, 13, 125, 8, 13, 1, 14, 1, 14, 5, 14, 129, 8, 14, 10, 14, 12, 14, 132, 9, 14, 1, 15, 4, 15, 135, 8, 15, 11, 15, 12, 15, 136, 1, 16, 4, 16, 140, 8, 16, 11, 16, 12, 16, 141, 1, 16, 1, 16, 1, 17, 1, 17, 5, 17, 148, 8, 17, 10, 17, 12, 17, 151, 9, 17, 1, 17, 1, 17, 0, 0, 18, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 1, 0, 6, 3, 0, 10, 10, 13, 13, 34, 34, 3, 0, 10, 10, 13, 13, 39, 39, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 10, 10, 13, 13, 2, 0, 9, 9, 32, 32, 162, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 1, 37, 1, 0, 0, 0, 3, 45, 1, 0, 0, 0, 5, 51, 1, 0, 0, 0, 7, 66, 1, 0, 0, 0, 9, 75, 1, 0, 0, 0, 11, 92, 1, 0, 0, 0, 13, 94, 1, 0, 0, 0, 15, 96, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0, 19, 100, 1, 0, 0, 0, 21, 102, 1, 0, 0, 0, 23, 104, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 124, 1, 0, 0, 0, 29, 126, 1, 0, 0, 0, 31, 134, 1, 0, 0, 0, 33, 139, 1, 0, 0, 0, 35, 145, 1, 0, 0, 0, 37, 38, 5, 117, 0, 0, 38, 39, 5, 115, 0, 0, 39, 40, 5, 101, 0, 0, 40, 41, 5, 99, 0, 0, 41, 42, 5, 97, 0, 0, 42, 43, 5, 115, 0, 0, 43, 44, 5, 101, 0, 0, 44, 2, 1, 0, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 99, 0, 0, 47, 48, 5, 116, 0, 0, 48, 49, 5, 111, 0, 0, 49, 50, 5, 114, 0, 0, 50, 4, 1, 0, 0, 0, 51, 52, 5, 115, 0, 0, 52, 53, 5, 121, 0, 0, 53, 54, 5, 115, 0, 0, 54, 55, 5, 116, 0, 0, 55, 56, 5, 101, 0, 0, 56, 57, 5, 109, 0, 0, 57, 58, 5, 66, 0, 0, 58, 59, 5, 111, 0, 0, 59, 60, 5, 117, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 100, 0, 0, 62, 63, 5, 97, 0, 0, 63, 64, 5, 114, 0, 0, 64, 65, 5, 121, 0, 0, 65, 6, 1, 0, 0, 0, 66, 67, 5, 101, 0, 0, 67, 68, 5, 110, 0, 0, 68, 69, 5, 100, 0, 0, 69, 8, 1, 0, 0, 0, 70, 71, 5, 45, 0, 0, 71, 72, 5, 45, 0, 0, 72, 76, 5, 62, 0, 0, 73, 74, 5, 45, 0, 0, 74, 76, 5, 62, 0, 0, 75, 70, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 10, 1, 0, 0, 0, 77, 78, 5, 45, 0, 0, 78, 79, 5, 45, 0, 0, 79, 80, 1, 0, 0, 0, 80, 81, 3, 29, 14, 0, 81, 82, 5, 45, 0, 0, 82, 83, 5, 45, 0, 0, 83, 84, 5, 62, 0, 0, 84, 93, 1, 0, 0, 0, 85, 86, 5, 45, 0, 0, 86, 87, 5, 45, 0, 0, 87, 88, 1, 0, 0, 0, 88, 89, 3, 29, 14, 0, 89, 90, 5, 45, 0, 0, 90, 91, 5, 62, 0, 0, 91, 93, 1, 0, 0, 0, 92, 77, 1, 0, 0, 0, 92, 85, 1, 0, 0, 0, 93, 12, 1, 0, 0, 0, 94, 95, 5, 64, 0, 0, 95, 14, 1, 0, 0, 0, 96, 97, 5, 123, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 125, 0, 0, 99, 18, 1, 0, 0, 0, 100, 101, 5, 40, 0, 0, 101, 20, 1, 0, 0, 0, 102, 103, 5, 41, 0, 0, 103, 22, 1, 0, 0, 0, 104, 105, 5, 44, 0, 0, 105, 24, 1, 0, 0, 0, 106, 107, 5, 58, 0, 0, 107, 26, 1, 0, 0, 0, 108, 112, 5, 34, 0, 0, 109, 111, 8, 0, 0, 0, 110, 109, 1, 0, 0, 0, 111, 114, 1, 0, 0, 0, 112, 110, 1, 0, 0, 0, 112, 113, 1, 0, 0, 0, 113, 115, 1, 0, 0, 0, 114, 112, 1, 0, 0, 0, 115, 125, 5, 34, 0, 0, 116, 120, 5, 39, 0, 0, 117, 119, 8, 1, 0, 0, 118, 117, 1, 0, 0, 0, 119, 122, 1, 0, 0, 0, 120, 118, 1, 0, 0, 0, 120, 121, 1, 0, 0, 0, 121, 123, 1, 0, 0, 0, 122, 120, 1, 0, 0, 0, 123, 125, 5, 39, 0, 0, 124, 108, 1, 0, 0, 0, 124, 116, 1, 0, 0, 0, 125, 28, 1, 0, 0, 0, 126, 130, 7, 2, 0, 0, 127, 129, 7, 3, 0, 0, 128, 127, 1, 0, 0, 0, 129, 132, 1, 0, 0, 0, 130, 128, 1, 0, 0, 0, 130, 131, 1, 0, 0, 0, 131, 30, 1, 0, 0, 0, 132, 130, 1, 0, 0, 0, 133, 135, 7, 4, 0, 0, 134, 133, 1, 0, 0, 0, 135, 136, 1, 0, 0, 0, 136, 134, 1, 0, 0, 0, 136, 137, 1, 0, 0, 0, 137, 32, 1, 0, 0, 0, 138, 140, 7, 5, 0, 0, 139, 138, 1, 0, 0, 0, 140, 141, 1, 0, 0, 0, 141, 139, 1, 0, 0, 0, 141, 142, 1, 0, 0, 0, 142, 143, 1, 0, 0, 0, 143, 144, 6, 16, 0, 0, 144, 34, 1, 0, 0, 0, 145, 149, 5, 37, 0, 0, 146, 148, 8, 4, 0, 0, 147, 146, 1, 0, 0, 0, 148, 151, 1, 0, 0, 0, 149, 147, 1, 0, 0, 0, 149, 150, 1, 0, 0, 0, 150, 152, 1, 0, 0, 0, 151, 149, 1, 0, 0, 0, 152, 153, 6, 17, 0, 0, 153, 36, 1, 0, 0, 0, 10, 0, 75, 92, 112, 120, 124, 130, 136, 141, 149, 1, 6, 0, 0]
|
213
packages/parser/src/language/useCase/.antlr/UsecaseLexer.java
Normal file
213
packages/parser/src/language/useCase/.antlr/UsecaseLexer.java
Normal file
@@ -0,0 +1,213 @@
|
||||
// Generated from /home/omkar-kadam/Public/mermaid/mermaid/packages/parser/src/language/useCase/Usecase.g4 by ANTLR 4.13.1
|
||||
import org.antlr.v4.runtime.Lexer;
|
||||
import org.antlr.v4.runtime.CharStream;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.TokenStream;
|
||||
import org.antlr.v4.runtime.*;
|
||||
import org.antlr.v4.runtime.atn.*;
|
||||
import org.antlr.v4.runtime.dfa.DFA;
|
||||
import org.antlr.v4.runtime.misc.*;
|
||||
|
||||
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"})
|
||||
public class UsecaseLexer extends Lexer {
|
||||
static { RuntimeMetaData.checkVersion("4.13.1", RuntimeMetaData.VERSION); }
|
||||
|
||||
protected static final DFA[] _decisionToDFA;
|
||||
protected static final PredictionContextCache _sharedContextCache =
|
||||
new PredictionContextCache();
|
||||
public static final int
|
||||
USECASE_START=1, ACTOR=2, SYSTEM_BOUNDARY=3, END=4, ARROW=5, LABELED_ARROW=6,
|
||||
AT=7, LBRACE=8, RBRACE=9, LPAREN=10, RPAREN=11, COMMA=12, COLON=13, STRING=14,
|
||||
IDENTIFIER=15, NEWLINE=16, WS=17, COMMENT=18;
|
||||
public static String[] channelNames = {
|
||||
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
|
||||
};
|
||||
|
||||
public static String[] modeNames = {
|
||||
"DEFAULT_MODE"
|
||||
};
|
||||
|
||||
private static String[] makeRuleNames() {
|
||||
return new String[] {
|
||||
"USECASE_START", "ACTOR", "SYSTEM_BOUNDARY", "END", "ARROW", "LABELED_ARROW",
|
||||
"AT", "LBRACE", "RBRACE", "LPAREN", "RPAREN", "COMMA", "COLON", "STRING",
|
||||
"IDENTIFIER", "NEWLINE", "WS", "COMMENT"
|
||||
};
|
||||
}
|
||||
public static final String[] ruleNames = makeRuleNames();
|
||||
|
||||
private static String[] makeLiteralNames() {
|
||||
return new String[] {
|
||||
null, "'usecase'", "'actor'", "'systemBoundary'", "'end'", null, null,
|
||||
"'@'", "'{'", "'}'", "'('", "')'", "','", "':'"
|
||||
};
|
||||
}
|
||||
private static final String[] _LITERAL_NAMES = makeLiteralNames();
|
||||
private static String[] makeSymbolicNames() {
|
||||
return new String[] {
|
||||
null, "USECASE_START", "ACTOR", "SYSTEM_BOUNDARY", "END", "ARROW", "LABELED_ARROW",
|
||||
"AT", "LBRACE", "RBRACE", "LPAREN", "RPAREN", "COMMA", "COLON", "STRING",
|
||||
"IDENTIFIER", "NEWLINE", "WS", "COMMENT"
|
||||
};
|
||||
}
|
||||
private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames();
|
||||
public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #VOCABULARY} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String[] tokenNames;
|
||||
static {
|
||||
tokenNames = new String[_SYMBOLIC_NAMES.length];
|
||||
for (int i = 0; i < tokenNames.length; i++) {
|
||||
tokenNames[i] = VOCABULARY.getLiteralName(i);
|
||||
if (tokenNames[i] == null) {
|
||||
tokenNames[i] = VOCABULARY.getSymbolicName(i);
|
||||
}
|
||||
|
||||
if (tokenNames[i] == null) {
|
||||
tokenNames[i] = "<INVALID>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public String[] getTokenNames() {
|
||||
return tokenNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public Vocabulary getVocabulary() {
|
||||
return VOCABULARY;
|
||||
}
|
||||
|
||||
|
||||
public UsecaseLexer(CharStream input) {
|
||||
super(input);
|
||||
_interp = new LexerATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGrammarFileName() { return "Usecase.g4"; }
|
||||
|
||||
@Override
|
||||
public String[] getRuleNames() { return ruleNames; }
|
||||
|
||||
@Override
|
||||
public String getSerializedATN() { return _serializedATN; }
|
||||
|
||||
@Override
|
||||
public String[] getChannelNames() { return channelNames; }
|
||||
|
||||
@Override
|
||||
public String[] getModeNames() { return modeNames; }
|
||||
|
||||
@Override
|
||||
public ATN getATN() { return _ATN; }
|
||||
|
||||
public static final String _serializedATN =
|
||||
"\u0004\u0000\u0012\u009a\u0006\uffff\uffff\u0002\u0000\u0007\u0000\u0002"+
|
||||
"\u0001\u0007\u0001\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002"+
|
||||
"\u0004\u0007\u0004\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002"+
|
||||
"\u0007\u0007\u0007\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002"+
|
||||
"\u000b\u0007\u000b\u0002\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e"+
|
||||
"\u0002\u000f\u0007\u000f\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011"+
|
||||
"\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000"+
|
||||
"\u0001\u0000\u0001\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"+
|
||||
"\u0001\u0001\u0001\u0001\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
|
||||
"\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
|
||||
"\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0003"+
|
||||
"\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0004\u0001\u0004\u0001\u0004"+
|
||||
"\u0001\u0004\u0001\u0004\u0003\u0004L\b\u0004\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0003\u0005]\b\u0005\u0001\u0006\u0001\u0006\u0001\u0007"+
|
||||
"\u0001\u0007\u0001\b\u0001\b\u0001\t\u0001\t\u0001\n\u0001\n\u0001\u000b"+
|
||||
"\u0001\u000b\u0001\f\u0001\f\u0001\r\u0001\r\u0005\ro\b\r\n\r\f\rr\t\r"+
|
||||
"\u0001\r\u0001\r\u0001\r\u0005\rw\b\r\n\r\f\rz\t\r\u0001\r\u0003\r}\b"+
|
||||
"\r\u0001\u000e\u0001\u000e\u0005\u000e\u0081\b\u000e\n\u000e\f\u000e\u0084"+
|
||||
"\t\u000e\u0001\u000f\u0004\u000f\u0087\b\u000f\u000b\u000f\f\u000f\u0088"+
|
||||
"\u0001\u0010\u0004\u0010\u008c\b\u0010\u000b\u0010\f\u0010\u008d\u0001"+
|
||||
"\u0010\u0001\u0010\u0001\u0011\u0001\u0011\u0005\u0011\u0094\b\u0011\n"+
|
||||
"\u0011\f\u0011\u0097\t\u0011\u0001\u0011\u0001\u0011\u0000\u0000\u0012"+
|
||||
"\u0001\u0001\u0003\u0002\u0005\u0003\u0007\u0004\t\u0005\u000b\u0006\r"+
|
||||
"\u0007\u000f\b\u0011\t\u0013\n\u0015\u000b\u0017\f\u0019\r\u001b\u000e"+
|
||||
"\u001d\u000f\u001f\u0010!\u0011#\u0012\u0001\u0000\u0006\u0003\u0000\n"+
|
||||
"\n\r\r\"\"\u0003\u0000\n\n\r\r\'\'\u0003\u0000AZ__az\u0004\u000009AZ_"+
|
||||
"_az\u0002\u0000\n\n\r\r\u0002\u0000\t\t \u00a2\u0000\u0001\u0001\u0000"+
|
||||
"\u0000\u0000\u0000\u0003\u0001\u0000\u0000\u0000\u0000\u0005\u0001\u0000"+
|
||||
"\u0000\u0000\u0000\u0007\u0001\u0000\u0000\u0000\u0000\t\u0001\u0000\u0000"+
|
||||
"\u0000\u0000\u000b\u0001\u0000\u0000\u0000\u0000\r\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u000f\u0001\u0000\u0000\u0000\u0000\u0011\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u0013\u0001\u0000\u0000\u0000\u0000\u0015\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u0017\u0001\u0000\u0000\u0000\u0000\u0019\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u001b\u0001\u0000\u0000\u0000\u0000\u001d\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u001f\u0001\u0000\u0000\u0000\u0000!\u0001\u0000\u0000\u0000\u0000"+
|
||||
"#\u0001\u0000\u0000\u0000\u0001%\u0001\u0000\u0000\u0000\u0003-\u0001"+
|
||||
"\u0000\u0000\u0000\u00053\u0001\u0000\u0000\u0000\u0007B\u0001\u0000\u0000"+
|
||||
"\u0000\tK\u0001\u0000\u0000\u0000\u000b\\\u0001\u0000\u0000\u0000\r^\u0001"+
|
||||
"\u0000\u0000\u0000\u000f`\u0001\u0000\u0000\u0000\u0011b\u0001\u0000\u0000"+
|
||||
"\u0000\u0013d\u0001\u0000\u0000\u0000\u0015f\u0001\u0000\u0000\u0000\u0017"+
|
||||
"h\u0001\u0000\u0000\u0000\u0019j\u0001\u0000\u0000\u0000\u001b|\u0001"+
|
||||
"\u0000\u0000\u0000\u001d~\u0001\u0000\u0000\u0000\u001f\u0086\u0001\u0000"+
|
||||
"\u0000\u0000!\u008b\u0001\u0000\u0000\u0000#\u0091\u0001\u0000\u0000\u0000"+
|
||||
"%&\u0005u\u0000\u0000&\'\u0005s\u0000\u0000\'(\u0005e\u0000\u0000()\u0005"+
|
||||
"c\u0000\u0000)*\u0005a\u0000\u0000*+\u0005s\u0000\u0000+,\u0005e\u0000"+
|
||||
"\u0000,\u0002\u0001\u0000\u0000\u0000-.\u0005a\u0000\u0000./\u0005c\u0000"+
|
||||
"\u0000/0\u0005t\u0000\u000001\u0005o\u0000\u000012\u0005r\u0000\u0000"+
|
||||
"2\u0004\u0001\u0000\u0000\u000034\u0005s\u0000\u000045\u0005y\u0000\u0000"+
|
||||
"56\u0005s\u0000\u000067\u0005t\u0000\u000078\u0005e\u0000\u000089\u0005"+
|
||||
"m\u0000\u00009:\u0005B\u0000\u0000:;\u0005o\u0000\u0000;<\u0005u\u0000"+
|
||||
"\u0000<=\u0005n\u0000\u0000=>\u0005d\u0000\u0000>?\u0005a\u0000\u0000"+
|
||||
"?@\u0005r\u0000\u0000@A\u0005y\u0000\u0000A\u0006\u0001\u0000\u0000\u0000"+
|
||||
"BC\u0005e\u0000\u0000CD\u0005n\u0000\u0000DE\u0005d\u0000\u0000E\b\u0001"+
|
||||
"\u0000\u0000\u0000FG\u0005-\u0000\u0000GH\u0005-\u0000\u0000HL\u0005>"+
|
||||
"\u0000\u0000IJ\u0005-\u0000\u0000JL\u0005>\u0000\u0000KF\u0001\u0000\u0000"+
|
||||
"\u0000KI\u0001\u0000\u0000\u0000L\n\u0001\u0000\u0000\u0000MN\u0005-\u0000"+
|
||||
"\u0000NO\u0005-\u0000\u0000OP\u0001\u0000\u0000\u0000PQ\u0003\u001d\u000e"+
|
||||
"\u0000QR\u0005-\u0000\u0000RS\u0005-\u0000\u0000ST\u0005>\u0000\u0000"+
|
||||
"T]\u0001\u0000\u0000\u0000UV\u0005-\u0000\u0000VW\u0005-\u0000\u0000W"+
|
||||
"X\u0001\u0000\u0000\u0000XY\u0003\u001d\u000e\u0000YZ\u0005-\u0000\u0000"+
|
||||
"Z[\u0005>\u0000\u0000[]\u0001\u0000\u0000\u0000\\M\u0001\u0000\u0000\u0000"+
|
||||
"\\U\u0001\u0000\u0000\u0000]\f\u0001\u0000\u0000\u0000^_\u0005@\u0000"+
|
||||
"\u0000_\u000e\u0001\u0000\u0000\u0000`a\u0005{\u0000\u0000a\u0010\u0001"+
|
||||
"\u0000\u0000\u0000bc\u0005}\u0000\u0000c\u0012\u0001\u0000\u0000\u0000"+
|
||||
"de\u0005(\u0000\u0000e\u0014\u0001\u0000\u0000\u0000fg\u0005)\u0000\u0000"+
|
||||
"g\u0016\u0001\u0000\u0000\u0000hi\u0005,\u0000\u0000i\u0018\u0001\u0000"+
|
||||
"\u0000\u0000jk\u0005:\u0000\u0000k\u001a\u0001\u0000\u0000\u0000lp\u0005"+
|
||||
"\"\u0000\u0000mo\b\u0000\u0000\u0000nm\u0001\u0000\u0000\u0000or\u0001"+
|
||||
"\u0000\u0000\u0000pn\u0001\u0000\u0000\u0000pq\u0001\u0000\u0000\u0000"+
|
||||
"qs\u0001\u0000\u0000\u0000rp\u0001\u0000\u0000\u0000s}\u0005\"\u0000\u0000"+
|
||||
"tx\u0005\'\u0000\u0000uw\b\u0001\u0000\u0000vu\u0001\u0000\u0000\u0000"+
|
||||
"wz\u0001\u0000\u0000\u0000xv\u0001\u0000\u0000\u0000xy\u0001\u0000\u0000"+
|
||||
"\u0000y{\u0001\u0000\u0000\u0000zx\u0001\u0000\u0000\u0000{}\u0005\'\u0000"+
|
||||
"\u0000|l\u0001\u0000\u0000\u0000|t\u0001\u0000\u0000\u0000}\u001c\u0001"+
|
||||
"\u0000\u0000\u0000~\u0082\u0007\u0002\u0000\u0000\u007f\u0081\u0007\u0003"+
|
||||
"\u0000\u0000\u0080\u007f\u0001\u0000\u0000\u0000\u0081\u0084\u0001\u0000"+
|
||||
"\u0000\u0000\u0082\u0080\u0001\u0000\u0000\u0000\u0082\u0083\u0001\u0000"+
|
||||
"\u0000\u0000\u0083\u001e\u0001\u0000\u0000\u0000\u0084\u0082\u0001\u0000"+
|
||||
"\u0000\u0000\u0085\u0087\u0007\u0004\u0000\u0000\u0086\u0085\u0001\u0000"+
|
||||
"\u0000\u0000\u0087\u0088\u0001\u0000\u0000\u0000\u0088\u0086\u0001\u0000"+
|
||||
"\u0000\u0000\u0088\u0089\u0001\u0000\u0000\u0000\u0089 \u0001\u0000\u0000"+
|
||||
"\u0000\u008a\u008c\u0007\u0005\u0000\u0000\u008b\u008a\u0001\u0000\u0000"+
|
||||
"\u0000\u008c\u008d\u0001\u0000\u0000\u0000\u008d\u008b\u0001\u0000\u0000"+
|
||||
"\u0000\u008d\u008e\u0001\u0000\u0000\u0000\u008e\u008f\u0001\u0000\u0000"+
|
||||
"\u0000\u008f\u0090\u0006\u0010\u0000\u0000\u0090\"\u0001\u0000\u0000\u0000"+
|
||||
"\u0091\u0095\u0005%\u0000\u0000\u0092\u0094\b\u0004\u0000\u0000\u0093"+
|
||||
"\u0092\u0001\u0000\u0000\u0000\u0094\u0097\u0001\u0000\u0000\u0000\u0095"+
|
||||
"\u0093\u0001\u0000\u0000\u0000\u0095\u0096\u0001\u0000\u0000\u0000\u0096"+
|
||||
"\u0098\u0001\u0000\u0000\u0000\u0097\u0095\u0001\u0000\u0000\u0000\u0098"+
|
||||
"\u0099\u0006\u0011\u0000\u0000\u0099$\u0001\u0000\u0000\u0000\n\u0000"+
|
||||
"K\\px|\u0082\u0088\u008d\u0095\u0001\u0006\u0000\u0000";
|
||||
public static final ATN _ATN =
|
||||
new ATNDeserializer().deserialize(_serializedATN.toCharArray());
|
||||
static {
|
||||
_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
|
||||
for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
|
||||
_decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
USECASE_START=1
|
||||
ACTOR=2
|
||||
SYSTEM_BOUNDARY=3
|
||||
END=4
|
||||
ARROW=5
|
||||
LABELED_ARROW=6
|
||||
AT=7
|
||||
LBRACE=8
|
||||
RBRACE=9
|
||||
LPAREN=10
|
||||
RPAREN=11
|
||||
COMMA=12
|
||||
COLON=13
|
||||
STRING=14
|
||||
IDENTIFIER=15
|
||||
NEWLINE=16
|
||||
WS=17
|
||||
COMMENT=18
|
||||
'usecase'=1
|
||||
'actor'=2
|
||||
'systemBoundary'=3
|
||||
'end'=4
|
||||
'@'=7
|
||||
'{'=8
|
||||
'}'=9
|
||||
'('=10
|
||||
')'=11
|
||||
','=12
|
||||
':'=13
|
1574
packages/parser/src/language/useCase/.antlr/UsecaseParser.java
Normal file
1574
packages/parser/src/language/useCase/.antlr/UsecaseParser.java
Normal file
File diff suppressed because it is too large
Load Diff
184
packages/parser/src/language/useCase/Usecase.g4
Normal file
184
packages/parser/src/language/useCase/Usecase.g4
Normal file
@@ -0,0 +1,184 @@
|
||||
grammar Usecase;
|
||||
|
||||
// Parser rules
|
||||
usecaseDiagram
|
||||
: USECASE_START NEWLINE* statement* EOF
|
||||
;
|
||||
|
||||
statement
|
||||
: actor NEWLINE*
|
||||
| systemBoundary NEWLINE*
|
||||
| systemBoundaryMetadata NEWLINE*
|
||||
| useCase NEWLINE*
|
||||
| relationship NEWLINE*
|
||||
| actorRelationship NEWLINE*
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
relationship
|
||||
: actorName ARROW target
|
||||
| actorName LABELED_ARROW target
|
||||
;
|
||||
|
||||
actorRelationship
|
||||
: ACTOR actorName ARROW target
|
||||
| ACTOR actorName LABELED_ARROW target
|
||||
;
|
||||
|
||||
target
|
||||
: useCaseName
|
||||
| nodeDefinition
|
||||
;
|
||||
|
||||
nodeDefinition
|
||||
: nodeId LPAREN nodeLabel RPAREN
|
||||
;
|
||||
|
||||
nodeId
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
nodeLabel
|
||||
: IDENTIFIER (WS IDENTIFIER)*
|
||||
| STRING
|
||||
;
|
||||
|
||||
actorName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
systemBoundary
|
||||
: SYSTEM_BOUNDARY boundaryName LBRACE NEWLINE* boundaryContent* RBRACE
|
||||
| SYSTEM_BOUNDARY boundaryName NEWLINE* boundaryContent* END
|
||||
;
|
||||
|
||||
systemBoundaryMetadata
|
||||
: boundaryName AT LBRACE metadataContent RBRACE
|
||||
;
|
||||
|
||||
boundaryContent
|
||||
: useCase NEWLINE*
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
useCase
|
||||
: useCaseName
|
||||
;
|
||||
|
||||
boundaryName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
useCaseName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
actor
|
||||
: ACTOR actorList
|
||||
;
|
||||
|
||||
actorList
|
||||
: actorDefinition (COMMA actorDefinition)*
|
||||
;
|
||||
|
||||
actorDefinition
|
||||
: actorName metadata?
|
||||
;
|
||||
|
||||
metadata
|
||||
: AT LBRACE metadataContent RBRACE
|
||||
;
|
||||
|
||||
metadataContent
|
||||
: metadataPair (COMMA metadataPair)*
|
||||
|
|
||||
;
|
||||
|
||||
metadataPair
|
||||
: metadataKey COLON metadataValue
|
||||
;
|
||||
|
||||
metadataKey
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
metadataValue
|
||||
: STRING
|
||||
| IDENTIFIER
|
||||
;
|
||||
|
||||
// Lexer rules
|
||||
USECASE_START
|
||||
: 'usecase'
|
||||
;
|
||||
|
||||
ACTOR
|
||||
: 'actor'
|
||||
;
|
||||
|
||||
SYSTEM_BOUNDARY
|
||||
: 'systemBoundary'
|
||||
;
|
||||
|
||||
END
|
||||
: 'end'
|
||||
;
|
||||
|
||||
ARROW
|
||||
: '-->'
|
||||
| '->'
|
||||
;
|
||||
|
||||
LABELED_ARROW
|
||||
: '--' IDENTIFIER '-->'
|
||||
| '--' IDENTIFIER '->'
|
||||
;
|
||||
|
||||
AT
|
||||
: '@'
|
||||
;
|
||||
|
||||
LBRACE
|
||||
: '{'
|
||||
;
|
||||
|
||||
RBRACE
|
||||
: '}'
|
||||
;
|
||||
|
||||
LPAREN
|
||||
: '('
|
||||
;
|
||||
|
||||
RPAREN
|
||||
: ')'
|
||||
;
|
||||
|
||||
COMMA
|
||||
: ','
|
||||
;
|
||||
|
||||
COLON
|
||||
: ':'
|
||||
;
|
||||
|
||||
STRING
|
||||
: '"' (~["\r\n])* '"'
|
||||
| '\'' (~['\r\n])* '\''
|
||||
;
|
||||
|
||||
IDENTIFIER
|
||||
: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
;
|
||||
|
||||
NEWLINE
|
||||
: [\r\n]+
|
||||
;
|
||||
|
||||
WS
|
||||
: [ \t]+ -> skip
|
||||
;
|
||||
|
||||
COMMENT
|
||||
: '%' ~[\r\n]* -> skip
|
||||
;
|
2
packages/parser/src/language/useCase/index.ts
Normal file
2
packages/parser/src/language/useCase/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { parseUsecase } from './usecaseParser.js';
|
||||
export * from './usecaseTypes.js';
|
387
packages/parser/src/language/useCase/test.ts
Normal file
387
packages/parser/src/language/useCase/test.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { parseUsecase } from './usecaseParser.js';
|
||||
|
||||
// Test basic usecase diagram parsing
|
||||
function testBasicUsecaseParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
actor Developer2
|
||||
actor Developer3`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Basic Usecase Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test simple usecase diagram
|
||||
function testSimpleUsecaseParsing() {
|
||||
const input = `usecase
|
||||
actor User
|
||||
actor Admin`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Simple Usecase Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test metadata parsing
|
||||
function testMetadataParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon : 'icon_name', place: "sample place" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Metadata Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test complex metadata parsing
|
||||
function testComplexMetadataParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon : 'icon_name', type : 'hollow', place: "sample place", material:"sample" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Complex Metadata Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed actors (with and without metadata)
|
||||
function testMixedActorsParsing() {
|
||||
const input = `usecase
|
||||
actor User
|
||||
actor Developer1@{ icon : 'dev_icon' }
|
||||
actor Admin@{ type: 'admin', place: "office" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Actors Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test multiple actors in single line
|
||||
function testMultipleActorsSingleLine() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2, Developer3`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Multiple Actors Single Line:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test multiple actors with metadata
|
||||
function testMultipleActorsWithMetadata() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon: 'dev' }, Developer2, Developer3@{ type: 'admin' }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Multiple Actors With Metadata:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test five actors in single line
|
||||
function testFiveActorsSingleLine() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2, Developer3, Developer4, Developer5`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Five Actors Single Line:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test system boundary parsing
|
||||
function testSystemBoundaryParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks
|
||||
coding
|
||||
testing
|
||||
deploying
|
||||
end`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test System Boundary Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed actors and system boundaries
|
||||
function testMixedActorsAndBoundaries() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2
|
||||
systemBoundary Tasks
|
||||
coding
|
||||
testing
|
||||
end
|
||||
actor Admin`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Actors and Boundaries:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test curly brace system boundary parsing
|
||||
function testCurlyBraceSystemBoundary() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Curly Brace System Boundary:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test relationship parsing
|
||||
function testRelationshipParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}
|
||||
Developer1 --> playing
|
||||
Developer1 --> reviewing`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Relationship Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test complete example
|
||||
function testCompleteExample() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}
|
||||
Developer1 --> playing
|
||||
Developer1 --> reviewing`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Complete Example:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test node definitions
|
||||
function testNodeDefinitions() {
|
||||
const input = `usecase
|
||||
actor Tester1
|
||||
Tester1 --> c(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Node Definitions:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test inline actor-node relationships
|
||||
function testInlineActorNodeRelationships() {
|
||||
const input = `usecase
|
||||
actor Developer1 --> a(Go through code)
|
||||
actor Developer2 --> b(Go through implementation)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Inline Actor-Node Relationships:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed syntax
|
||||
function testMixedSyntax() {
|
||||
const input = `usecase
|
||||
actor Tester1
|
||||
Tester1 --> c(Go through testing)
|
||||
actor Developer1 --> a(Go through code)
|
||||
actor Developer2 --> b(Go through implementation)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Syntax:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test edge labels
|
||||
function testEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
Developer1 --task2--> c(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test edge labels with inline syntax
|
||||
function testInlineEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1 --task1--> a(Go through code)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Inline Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed edge labels and regular arrows
|
||||
function testMixedEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
actor Tester1
|
||||
Developer1 --task1--> a(Go through code)
|
||||
Tester1 --> b(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Run tests
|
||||
console.log('Running Usecase Parser Tests...\n');
|
||||
testBasicUsecaseParsing();
|
||||
testSimpleUsecaseParsing();
|
||||
testMetadataParsing();
|
||||
testComplexMetadataParsing();
|
||||
testMixedActorsParsing();
|
||||
testMultipleActorsSingleLine();
|
||||
testMultipleActorsWithMetadata();
|
||||
testFiveActorsSingleLine();
|
||||
testSystemBoundaryParsing();
|
||||
testMixedActorsAndBoundaries();
|
||||
testCurlyBraceSystemBoundary();
|
||||
testRelationshipParsing();
|
||||
testCompleteExample();
|
||||
testNodeDefinitions();
|
||||
testInlineActorNodeRelationships();
|
||||
testMixedSyntax();
|
||||
testEdgeLabels();
|
||||
testInlineEdgeLabels();
|
||||
testMixedEdgeLabels();
|
||||
console.log('Tests completed.');
|
752
packages/parser/src/language/useCase/usecaseParser.ts
Normal file
752
packages/parser/src/language/useCase/usecaseParser.ts
Normal file
@@ -0,0 +1,752 @@
|
||||
// Simple tokenizer and parser for usecase diagrams
|
||||
// This approach is more compatible with the mermaid build system
|
||||
import type {
|
||||
UsecaseDiagram,
|
||||
Statement,
|
||||
Actor,
|
||||
Usecase,
|
||||
SystemBoundary,
|
||||
SystemBoundaryMetadata,
|
||||
ActorUseCaseRelationship,
|
||||
Node,
|
||||
ActorNodeRelationship,
|
||||
InlineActorNodeRelationship,
|
||||
ParseResult
|
||||
} from './usecaseTypes.js';
|
||||
|
||||
// Token types
|
||||
enum TokenType {
|
||||
USECASE_START = 'USECASE_START',
|
||||
ACTOR = 'ACTOR',
|
||||
SYSTEM_BOUNDARY = 'SYSTEM_BOUNDARY',
|
||||
END = 'END',
|
||||
ARROW = 'ARROW',
|
||||
LABELED_ARROW = 'LABELED_ARROW',
|
||||
AT = 'AT',
|
||||
LBRACE = 'LBRACE',
|
||||
RBRACE = 'RBRACE',
|
||||
LPAREN = 'LPAREN',
|
||||
RPAREN = 'RPAREN',
|
||||
COMMA = 'COMMA',
|
||||
COLON = 'COLON',
|
||||
STRING = 'STRING',
|
||||
IDENTIFIER = 'IDENTIFIER',
|
||||
NEWLINE = 'NEWLINE',
|
||||
EOF = 'EOF'
|
||||
}
|
||||
|
||||
interface Token {
|
||||
type: TokenType;
|
||||
value: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
class UsecaseLexer {
|
||||
private input: string;
|
||||
private position: number = 0;
|
||||
private line: number = 1;
|
||||
private column: number = 1;
|
||||
|
||||
constructor(input: string) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
tokenize(): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
|
||||
while (this.position < this.input.length) {
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.position >= this.input.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const token = this.nextToken();
|
||||
if (token) {
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
type: TokenType.EOF,
|
||||
value: '',
|
||||
line: this.line,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private nextToken(): Token | null {
|
||||
const startLine = this.line;
|
||||
const startColumn = this.column;
|
||||
|
||||
// Skip comments
|
||||
if (this.peek() === '%') {
|
||||
this.skipComment();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Newlines
|
||||
if (this.peek() === '\n' || this.peek() === '\r') {
|
||||
this.advance();
|
||||
if (this.peek() === '\n') {
|
||||
this.advance();
|
||||
}
|
||||
this.line++;
|
||||
this.column = 1;
|
||||
return {
|
||||
type: TokenType.NEWLINE,
|
||||
value: '\n',
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Strings
|
||||
if (this.peek() === '"' || this.peek() === "'") {
|
||||
return this.readString(startLine, startColumn);
|
||||
}
|
||||
|
||||
// Arrow tokens (-->, ->, --label-->, --label->)
|
||||
if (this.peek() === '-') {
|
||||
if (this.peek(1) === '-') {
|
||||
// Check for labeled arrow: --label--> or --label->
|
||||
const labeledArrowMatch = this.tryParseLabeledArrow();
|
||||
if (labeledArrowMatch) {
|
||||
return labeledArrowMatch;
|
||||
}
|
||||
|
||||
// Regular arrow: -->
|
||||
if (this.peek(2) === '>') {
|
||||
this.advance(3);
|
||||
return { type: TokenType.ARROW, value: '-->', line: startLine, column: startColumn };
|
||||
}
|
||||
} else if (this.peek(1) === '>') {
|
||||
// Regular arrow: ->
|
||||
this.advance(2);
|
||||
return { type: TokenType.ARROW, value: '->', line: startLine, column: startColumn };
|
||||
}
|
||||
}
|
||||
|
||||
// Single character tokens
|
||||
switch (this.peek()) {
|
||||
case '@':
|
||||
this.advance();
|
||||
return { type: TokenType.AT, value: '@', line: startLine, column: startColumn };
|
||||
case '{':
|
||||
this.advance();
|
||||
return { type: TokenType.LBRACE, value: '{', line: startLine, column: startColumn };
|
||||
case '}':
|
||||
this.advance();
|
||||
return { type: TokenType.RBRACE, value: '}', line: startLine, column: startColumn };
|
||||
case ',':
|
||||
this.advance();
|
||||
return { type: TokenType.COMMA, value: ',', line: startLine, column: startColumn };
|
||||
case ':':
|
||||
this.advance();
|
||||
return { type: TokenType.COLON, value: ':', line: startLine, column: startColumn };
|
||||
case '(':
|
||||
this.advance();
|
||||
return { type: TokenType.LPAREN, value: '(', line: startLine, column: startColumn };
|
||||
case ')':
|
||||
this.advance();
|
||||
return { type: TokenType.RPAREN, value: ')', line: startLine, column: startColumn };
|
||||
}
|
||||
|
||||
// Keywords and identifiers
|
||||
if (this.isAlpha(this.peek())) {
|
||||
return this.readIdentifierOrKeyword(startLine, startColumn);
|
||||
}
|
||||
|
||||
// Skip unknown characters
|
||||
this.advance();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private readIdentifierOrKeyword(line: number, column: number): Token {
|
||||
let value = '';
|
||||
|
||||
while (this.position < this.input.length &&
|
||||
(this.isAlphaNumeric(this.peek()) || this.peek() === '_')) {
|
||||
value += this.peek();
|
||||
this.advance();
|
||||
}
|
||||
|
||||
// Check for keywords
|
||||
const type = this.getKeywordType(value);
|
||||
|
||||
return {
|
||||
type,
|
||||
value,
|
||||
line,
|
||||
column
|
||||
};
|
||||
}
|
||||
|
||||
private readString(line: number, column: number): Token {
|
||||
const quote = this.peek();
|
||||
this.advance(); // Skip opening quote
|
||||
|
||||
let value = '';
|
||||
while (this.position < this.input.length && this.peek() !== quote) {
|
||||
value += this.peek();
|
||||
this.advance();
|
||||
}
|
||||
|
||||
if (this.peek() === quote) {
|
||||
this.advance(); // Skip closing quote
|
||||
}
|
||||
|
||||
return {
|
||||
type: TokenType.STRING,
|
||||
value: value, // Return the content without quotes
|
||||
line,
|
||||
column
|
||||
};
|
||||
}
|
||||
|
||||
private getKeywordType(value: string): TokenType {
|
||||
switch (value.toLowerCase()) {
|
||||
case 'usecase': return TokenType.USECASE_START;
|
||||
case 'actor': return TokenType.ACTOR;
|
||||
case 'systemboundary': return TokenType.SYSTEM_BOUNDARY;
|
||||
case 'end': return TokenType.END;
|
||||
default: return TokenType.IDENTIFIER;
|
||||
}
|
||||
}
|
||||
|
||||
private skipWhitespace(): void {
|
||||
while (this.position < this.input.length &&
|
||||
(this.peek() === ' ' || this.peek() === '\t')) {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private skipComment(): void {
|
||||
while (this.position < this.input.length &&
|
||||
this.peek() !== '\n' && this.peek() !== '\r') {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private peek(offset: number = 0): string {
|
||||
const pos = this.position + offset;
|
||||
return pos < this.input.length ? this.input[pos] : '';
|
||||
}
|
||||
|
||||
private tryParseLabeledArrow(): Token | null {
|
||||
// Try to parse --label--> or --label->
|
||||
const startPos = this.position;
|
||||
const startLine = this.line;
|
||||
const startColumn = this.column;
|
||||
|
||||
// Skip initial '--'
|
||||
if (this.peek() !== '-' || this.peek(1) !== '-') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pos = 2;
|
||||
let label = '';
|
||||
|
||||
// Read the label
|
||||
while (pos < this.input.length - this.position) {
|
||||
const char = this.peek(pos);
|
||||
if (char === '-') {
|
||||
// Check if this is the end pattern
|
||||
if (this.peek(pos + 1) === '-' && this.peek(pos + 2) === '>') {
|
||||
// Found --label-->
|
||||
this.advance(pos + 3);
|
||||
return {
|
||||
type: TokenType.LABELED_ARROW,
|
||||
value: `--${label}-->`,
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
} else if (this.peek(pos + 1) === '>') {
|
||||
// Found --label->
|
||||
this.advance(pos + 2);
|
||||
return {
|
||||
type: TokenType.LABELED_ARROW,
|
||||
value: `--${label}->`,
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
} else {
|
||||
label += char;
|
||||
pos++;
|
||||
}
|
||||
} else if (char.match(/[a-zA-Z0-9_]/)) {
|
||||
label += char;
|
||||
pos++;
|
||||
} else {
|
||||
// Invalid character in label
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private advance(count: number = 1): void {
|
||||
for (let i = 0; i < count && this.position < this.input.length; i++) {
|
||||
this.position++;
|
||||
this.column++;
|
||||
}
|
||||
}
|
||||
|
||||
private isAlpha(char: string): boolean {
|
||||
return /[a-zA-Z]/.test(char);
|
||||
}
|
||||
|
||||
private isAlphaNumeric(char: string): boolean {
|
||||
return /[a-zA-Z0-9]/.test(char);
|
||||
}
|
||||
}
|
||||
|
||||
class UsecaseParser {
|
||||
private tokens: Token[];
|
||||
private position: number = 0;
|
||||
|
||||
constructor(tokens: Token[]) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
parse(): UsecaseDiagram {
|
||||
const statements: Statement[] = [];
|
||||
|
||||
// Expect 'usecase' keyword at the start
|
||||
this.consume(TokenType.USECASE_START);
|
||||
this.skipNewlines();
|
||||
|
||||
while (!this.isAtEnd()) {
|
||||
this.skipNewlines();
|
||||
|
||||
if (this.isAtEnd()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const parsedStatements = this.parseStatement();
|
||||
if (parsedStatements) {
|
||||
if (Array.isArray(parsedStatements)) {
|
||||
statements.push(...parsedStatements);
|
||||
} else {
|
||||
statements.push(parsedStatements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'usecaseDiagram',
|
||||
statements
|
||||
};
|
||||
}
|
||||
|
||||
private parseStatement(): Statement | Statement[] | null {
|
||||
const token = this.peek();
|
||||
|
||||
switch (token.type) {
|
||||
case TokenType.ACTOR:
|
||||
return this.parseActorStatement();
|
||||
case TokenType.SYSTEM_BOUNDARY:
|
||||
return this.parseSystemBoundary();
|
||||
case TokenType.IDENTIFIER:
|
||||
// Look ahead to see if this is a systemBoundaryMetadata, relationship, or use case
|
||||
if (this.isSystemBoundaryMetadata()) {
|
||||
return this.parseSystemBoundaryMetadata();
|
||||
} else if (this.isRelationship()) {
|
||||
return this.parseRelationship();
|
||||
} else {
|
||||
return this.parseUseCase();
|
||||
}
|
||||
default:
|
||||
this.advance(); // Skip unknown tokens
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseActorStatement(): Statement | Statement[] {
|
||||
this.consume(TokenType.ACTOR);
|
||||
|
||||
// Check if this is an inline actor-node relationship
|
||||
// Look ahead: IDENTIFIER ARROW IDENTIFIER LPAREN
|
||||
if (this.isInlineActorNodeRelationship()) {
|
||||
return this.parseInlineActorNodeRelationship();
|
||||
}
|
||||
|
||||
const actors: Actor[] = [];
|
||||
|
||||
// Parse first actor
|
||||
actors.push(this.parseActorDefinition());
|
||||
|
||||
// Parse additional actors separated by commas
|
||||
while (this.check(TokenType.COMMA)) {
|
||||
this.consume(TokenType.COMMA);
|
||||
actors.push(this.parseActorDefinition());
|
||||
}
|
||||
|
||||
return actors;
|
||||
}
|
||||
|
||||
private parseActorDefinition(): Actor {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let metadata: Record<string, string> | undefined;
|
||||
|
||||
// Check for optional metadata
|
||||
if (this.check(TokenType.AT)) {
|
||||
metadata = this.parseMetadata();
|
||||
}
|
||||
|
||||
const actor: Actor = { type: 'actor', name };
|
||||
if (metadata) {
|
||||
actor.metadata = metadata;
|
||||
}
|
||||
|
||||
return actor;
|
||||
}
|
||||
|
||||
private parseSystemBoundary(): SystemBoundary {
|
||||
this.consume(TokenType.SYSTEM_BOUNDARY);
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
// Skip newlines after opening brace
|
||||
this.skipNewlines();
|
||||
|
||||
const useCases: Usecase[] = [];
|
||||
|
||||
// Parse use cases until we hit closing brace
|
||||
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
||||
this.skipNewlines();
|
||||
|
||||
if (this.check(TokenType.RBRACE) || this.isAtEnd()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.check(TokenType.IDENTIFIER)) {
|
||||
const useCase = this.parseUseCase();
|
||||
if (useCase) {
|
||||
useCases.push(useCase as Usecase);
|
||||
}
|
||||
} else {
|
||||
this.advance(); // Skip unknown tokens
|
||||
}
|
||||
}
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
|
||||
return {
|
||||
type: 'systemBoundary',
|
||||
name,
|
||||
useCases
|
||||
};
|
||||
}
|
||||
|
||||
private parseSystemBoundaryMetadata(): SystemBoundaryMetadata {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.AT);
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse metadata content
|
||||
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
||||
if (this.check(TokenType.IDENTIFIER)) {
|
||||
const key = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.COLON);
|
||||
|
||||
let value = '';
|
||||
if (this.check(TokenType.STRING)) {
|
||||
value = this.consume(TokenType.STRING).value;
|
||||
// Remove quotes from string value
|
||||
value = value.slice(1, -1);
|
||||
} else if (this.check(TokenType.IDENTIFIER)) {
|
||||
value = this.consume(TokenType.IDENTIFIER).value;
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
|
||||
// Optional comma
|
||||
if (this.check(TokenType.COMMA)) {
|
||||
this.advance();
|
||||
}
|
||||
} else {
|
||||
this.advance(); // Skip unknown tokens
|
||||
}
|
||||
}
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
private parseUseCase(): Usecase {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
return {
|
||||
type: 'usecase',
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
private isRelationship(): boolean {
|
||||
// Look ahead to see if there's an arrow after the identifier
|
||||
const currentPos = this.position;
|
||||
this.advance(); // Skip the identifier
|
||||
const hasArrow = this.check(TokenType.ARROW) || this.check(TokenType.LABELED_ARROW);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasArrow;
|
||||
}
|
||||
|
||||
private isSystemBoundaryMetadata(): boolean {
|
||||
// Look ahead to see if there's an @ after the identifier
|
||||
const currentPos = this.position;
|
||||
this.advance(); // Skip the identifier
|
||||
const hasAt = this.check(TokenType.AT);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasAt;
|
||||
}
|
||||
|
||||
private parseRelationship(): ActorUseCaseRelationship | ActorNodeRelationship {
|
||||
const from = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let arrowToken: Token;
|
||||
let label: string | undefined;
|
||||
|
||||
if (this.check(TokenType.LABELED_ARROW)) {
|
||||
arrowToken = this.consume(TokenType.LABELED_ARROW);
|
||||
// Extract label from --label--> or --label->
|
||||
const arrowValue = arrowToken.value;
|
||||
const match = arrowValue.match(/^--(.+?)-+>$/);
|
||||
if (match) {
|
||||
label = match[1];
|
||||
}
|
||||
} else {
|
||||
arrowToken = this.consume(TokenType.ARROW);
|
||||
}
|
||||
|
||||
// Check if target is a node definition (ID followed by parentheses)
|
||||
if (this.isNodeDefinition()) {
|
||||
const node = this.parseNodeDefinition();
|
||||
return {
|
||||
type: 'actorNodeRelationship',
|
||||
from,
|
||||
to: node.id,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
} else {
|
||||
const to = this.consume(TokenType.IDENTIFIER).value;
|
||||
return {
|
||||
type: 'actorUseCaseRelationship',
|
||||
from,
|
||||
to,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private isInlineActorNodeRelationship(): boolean {
|
||||
// Look ahead: IDENTIFIER (ARROW|LABELED_ARROW) IDENTIFIER LPAREN
|
||||
const currentPos = this.position;
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip actor name
|
||||
|
||||
if (!this.check(TokenType.ARROW) && !this.check(TokenType.LABELED_ARROW)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip arrow
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip node ID
|
||||
|
||||
const hasLParen = this.check(TokenType.LPAREN);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasLParen;
|
||||
}
|
||||
|
||||
private parseInlineActorNodeRelationship(): InlineActorNodeRelationship {
|
||||
const actor = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let arrowToken: Token;
|
||||
let label: string | undefined;
|
||||
|
||||
if (this.check(TokenType.LABELED_ARROW)) {
|
||||
arrowToken = this.consume(TokenType.LABELED_ARROW);
|
||||
// Extract label from --label--> or --label->
|
||||
const arrowValue = arrowToken.value;
|
||||
const match = arrowValue.match(/^--(.+?)-+>$/);
|
||||
if (match) {
|
||||
label = match[1];
|
||||
}
|
||||
} else {
|
||||
arrowToken = this.consume(TokenType.ARROW);
|
||||
}
|
||||
|
||||
const node = this.parseNodeDefinition();
|
||||
|
||||
return {
|
||||
type: 'inlineActorNodeRelationship',
|
||||
actor,
|
||||
node,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
}
|
||||
|
||||
private isNodeDefinition(): boolean {
|
||||
// Look ahead: IDENTIFIER LPAREN
|
||||
const currentPos = this.position;
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip node ID
|
||||
|
||||
const hasLParen = this.check(TokenType.LPAREN);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasLParen;
|
||||
}
|
||||
|
||||
private parseNodeDefinition(): Node {
|
||||
const id = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.LPAREN);
|
||||
|
||||
// Parse node label (can be multiple words or a string)
|
||||
let label = '';
|
||||
if (this.check(TokenType.STRING)) {
|
||||
label = this.consume(TokenType.STRING).value;
|
||||
// Remove quotes
|
||||
label = label.slice(1, -1);
|
||||
} else {
|
||||
// Parse multiple identifiers as label
|
||||
const labelParts: string[] = [];
|
||||
while (this.check(TokenType.IDENTIFIER) && !this.check(TokenType.RPAREN)) {
|
||||
labelParts.push(this.consume(TokenType.IDENTIFIER).value);
|
||||
}
|
||||
label = labelParts.join(' ');
|
||||
}
|
||||
|
||||
this.consume(TokenType.RPAREN);
|
||||
|
||||
return {
|
||||
type: 'node',
|
||||
id,
|
||||
label
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
private parseMetadata(): Record<string, string> {
|
||||
this.consume(TokenType.AT);
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Handle empty metadata
|
||||
if (this.check(TokenType.RBRACE)) {
|
||||
this.consume(TokenType.RBRACE);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// Parse key-value pairs
|
||||
do {
|
||||
const key = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.COLON);
|
||||
|
||||
let value: string;
|
||||
if (this.check(TokenType.STRING)) {
|
||||
value = this.consume(TokenType.STRING).value;
|
||||
} else {
|
||||
value = this.consume(TokenType.IDENTIFIER).value;
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
|
||||
// Check for comma (more pairs) or closing brace
|
||||
if (this.check(TokenType.COMMA)) {
|
||||
this.consume(TokenType.COMMA);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (!this.check(TokenType.RBRACE) && !this.isAtEnd());
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private skipNewlines(): void {
|
||||
while (this.check(TokenType.NEWLINE)) {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private peek(): Token {
|
||||
return this.tokens[this.position];
|
||||
}
|
||||
|
||||
private advance(): Token {
|
||||
if (!this.isAtEnd()) {
|
||||
this.position++;
|
||||
}
|
||||
return this.tokens[this.position - 1];
|
||||
}
|
||||
|
||||
private check(type: TokenType): boolean {
|
||||
if (this.isAtEnd()) return false;
|
||||
return this.peek().type === type;
|
||||
}
|
||||
|
||||
private consume(type: TokenType): Token {
|
||||
if (this.check(type)) {
|
||||
return this.advance();
|
||||
}
|
||||
|
||||
const current = this.peek();
|
||||
throw new Error(`Expected ${type}, got ${current.type} at line ${current.line}`);
|
||||
}
|
||||
|
||||
private isAtEnd(): boolean {
|
||||
return this.position >= this.tokens.length || this.peek().type === TokenType.EOF;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseUsecase(input: string): ParseResult {
|
||||
try {
|
||||
const lexer = new UsecaseLexer(input);
|
||||
const tokens = lexer.tokenize();
|
||||
const parser = new UsecaseParser(tokens);
|
||||
const ast = parser.parse();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
ast
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [error instanceof Error ? error.message : String(error)]
|
||||
};
|
||||
}
|
||||
}
|
113
packages/parser/src/language/useCase/usecaseTypes.ts
Normal file
113
packages/parser/src/language/useCase/usecaseTypes.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// AST types for usecase diagrams
|
||||
|
||||
export interface UsecaseDiagram {
|
||||
type: 'usecaseDiagram';
|
||||
statements: Statement[];
|
||||
}
|
||||
|
||||
export type Statement = Actor | SystemBoundary | SystemBoundaryMetadata | Usecase | Relationship | ActorUseCaseRelationship | Node | ActorNodeRelationship | InlineActorNodeRelationship;
|
||||
|
||||
export interface Title {
|
||||
type: 'title';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface AccDescr {
|
||||
type: 'accDescr';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface AccTitle {
|
||||
type: 'accTitle';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface Actor {
|
||||
type: 'actor';
|
||||
name: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Usecase {
|
||||
type: 'usecase';
|
||||
name: string;
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
export interface SystemBoundary {
|
||||
type: 'systemBoundary';
|
||||
name: string;
|
||||
useCases: Usecase[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface SystemBoundaryMetadata {
|
||||
type: 'systemBoundaryMetadata';
|
||||
name: string; // boundary name
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ActorUseCaseRelationship {
|
||||
type: 'actorUseCaseRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // use case name
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
type: 'node';
|
||||
id: string; // node ID (e.g., 'a', 'b', 'c')
|
||||
label: string; // node label (e.g., 'Go through code')
|
||||
}
|
||||
|
||||
export interface ActorNodeRelationship {
|
||||
type: 'actorNodeRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // node ID
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface InlineActorNodeRelationship {
|
||||
type: 'inlineActorNodeRelationship';
|
||||
actor: string; // actor name
|
||||
node: Node; // node definition
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface Relationship {
|
||||
type: 'relationship';
|
||||
from: string;
|
||||
to: string;
|
||||
relationshipType: RelationshipType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
type: 'note';
|
||||
position: NotePosition;
|
||||
target: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type RelationshipType =
|
||||
| 'arrow-left'
|
||||
| 'arrow-right'
|
||||
| 'arrow-both'
|
||||
| 'extends'
|
||||
| 'includes';
|
||||
|
||||
export type NotePosition =
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'top'
|
||||
| 'bottom';
|
||||
|
||||
// Parser result type
|
||||
export interface ParseResult {
|
||||
success: boolean;
|
||||
ast?: UsecaseDiagram;
|
||||
errors?: string[];
|
||||
}
|
Reference in New Issue
Block a user