Compare commits

..

9 Commits

Author SHA1 Message Date
omkarht
8dcddaf314 fix: refactored image and icon test case files
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-25 15:34:30 +05:30
omkarht
bd19523f5d Merge branch 'develop' of https://github.com/mermaid-js/mermaid into 6623-add-image-icons-support-to-sequence-diagram 2025-08-25 15:21:24 +05:30
omkarht
6b64b92b6e docs: add documentation
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-14 12:58:24 +05:30
omkarht
41e3d2449a test: add test cases for image and icon support
Some optional description over here if you need to add more info

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-14 12:49:43 +05:30
omkarht
0386ed6b32 fix: refactored code
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-14 12:40:37 +05:30
omkarht
97481b4d11 Merge branch '6623-add-image-icons-support-to-sequence-diagram' of https://github.com/mermaid-js/mermaid into 6623-add-image-icons-support-to-sequence-diagram 2025-08-13 14:12:18 +05:30
omkarht
b8b120939e chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-13 14:11:54 +05:30
autofix-ci[bot]
71e4d62153 [autofix.ci] apply automated fixes 2025-08-13 07:57:40 +00:00
omkarht
b867370f1b 6623: add image and icon support for sequence diagrams
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-13 13:21:06 +05:30
50 changed files with 899 additions and 7071 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Add custom images and icons support for sequence diagram actors

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Render newlines as spaces in class diagrams

View File

@@ -23,6 +23,9 @@ env:
jobs:
e2e-applitools:
runs-on: ubuntu-latest
container:
image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1
options: --user 1001
steps:
- if: ${{ ! env.USE_APPLI }}
name: Warn if not using Applitools

View File

@@ -58,7 +58,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Commit and create pull request
uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a
uses: peter-evans/create-pull-request@cb4d3bfce175d44325c6b7697f81e0afe8a79bdf
with:
add-paths: |
cypress/timings.json

View File

@@ -524,18 +524,5 @@ 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' } }
);
});
});
});

View File

@@ -0,0 +1,118 @@
import { imgSnapshotTest } from '../../helpers/util';
const looks = ['classic'] as const;
looks.forEach((look) => {
describe(`SequenceDiagram icon participants in ${look} look`, () => {
it(`single participant with icon`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
Note over Bob: Icon participant`;
imgSnapshotTest(diagram, { look });
});
it(`two participants, one icon and one normal`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: Hello`;
imgSnapshotTest(diagram, { look });
});
it(`two icon participants`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice@{ type: "icon", icon: "fa:user" }
Bob->>Alice: Hello
Alice-->>Bob: Hi`;
imgSnapshotTest(diagram, { look });
});
it(`with markdown htmlLabels:true content`, () => {
// html/markdown in messages/notes (participants themselves don't support label/form/w/h)
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
imgSnapshotTest(diagram, { look });
});
it(`with markdown htmlLabels:false content`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
imgSnapshotTest(diagram, {
look,
htmlLabels: false,
flowchart: { htmlLabels: false },
});
});
it(`with styles applied to participant`, () => {
// style by participant id
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: Styled participant
`;
imgSnapshotTest(diagram, { look });
});
it(`with classDef and class application`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: Classed participant
`;
imgSnapshotTest(diagram, { look });
});
});
});
// Colored emoji icon tests (analogous to the flowchart colored icon tests), no direction line.
describe('SequenceDiagram colored icon participant', () => {
it('colored emoji icon without styles', () => {
const icon = 'fluent-emoji:tropical-fish';
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "${icon}" }
Note over Bob: colored emoji icon
`;
imgSnapshotTest(diagram);
});
it('colored emoji icon with styles', () => {
const icon = 'fluent-emoji:tropical-fish';
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "${icon}" }
`;
imgSnapshotTest(diagram);
});
});
// Mixed scenario: multiple interactions, still no direction line.
describe('SequenceDiagram icon participant with multiple interactions', () => {
const icon = 'fa:bell-slash';
it('icon participant interacts with two normal participants', () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "${icon}" }
participant Alice
participant Carol
Bob->>Alice: Ping
Alice-->>Bob: Pong
Bob->>Carol: Notify
Note right of Bob: Icon side note`;
imgSnapshotTest(diagram);
});
});

View File

@@ -0,0 +1,94 @@
import { imgSnapshotTest } from '../../helpers/util';
const looks = ['classic'] as const;
looks.forEach((look) => {
describe(`SequenceDiagram image participants in ${look} look`, () => {
it(`single participant with image`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
Note over Bob: Image participant`;
imgSnapshotTest(diagram, { look });
});
it(`two participants, one image and one normal`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: Hello`;
imgSnapshotTest(diagram, { look });
});
it(`two image participants`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
Bob->>Alice: Hello
Alice-->>Bob: Hi`;
imgSnapshotTest(diagram, { look });
});
it(`with markdown htmlLabels:true content`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
imgSnapshotTest(diagram, { look });
});
it(`with markdown htmlLabels:false content`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
imgSnapshotTest(diagram, {
look,
htmlLabels: false,
flowchart: { htmlLabels: false },
});
});
it(`with styles applied to participant`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: Styled participant
`;
imgSnapshotTest(diagram, { look });
});
it(`with classDef and class application`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: Classed participant
`;
imgSnapshotTest(diagram, { look });
});
});
});
// Mixed scenario: multiple interactions, still no direction line.
describe('SequenceDiagram image participant with multiple interactions', () => {
const imageUrl = 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg';
it('image participant interacts with two normal participants', () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "${imageUrl}" }
participant Alice
participant Carol
Bob->>Alice: Ping
Alice-->>Bob: Pong
Bob->>Carol: Notify
Note right of Bob: Image side note`;
imgSnapshotTest(diagram);
});
});

View File

@@ -2,223 +2,219 @@
"durations": [
{
"spec": "cypress/integration/other/configuration.spec.js",
"duration": 6162
"duration": 6297
},
{
"spec": "cypress/integration/other/external-diagrams.spec.js",
"duration": 2148
"duration": 2187
},
{
"spec": "cypress/integration/other/ghsa.spec.js",
"duration": 3585
"duration": 3509
},
{
"spec": "cypress/integration/other/iife.spec.js",
"duration": 2099
"duration": 2218
},
{
"spec": "cypress/integration/other/interaction.spec.js",
"duration": 12119
"duration": 12104
},
{
"spec": "cypress/integration/other/rerender.spec.js",
"duration": 2063
"duration": 2151
},
{
"spec": "cypress/integration/other/xss.spec.js",
"duration": 31921
"duration": 33064
},
{
"spec": "cypress/integration/rendering/appli.spec.js",
"duration": 3385
"duration": 3488
},
{
"spec": "cypress/integration/rendering/architecture.spec.ts",
"duration": 108
"duration": 106
},
{
"spec": "cypress/integration/rendering/block.spec.js",
"duration": 18063
"duration": 18317
},
{
"spec": "cypress/integration/rendering/c4.spec.js",
"duration": 5519
"duration": 5592
},
{
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
"duration": 40040
"duration": 39358
},
{
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
"duration": 38665
"duration": 37160
},
{
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
"duration": 22836
"duration": 23660
},
{
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
"duration": 37096
"duration": 36866
},
{
"spec": "cypress/integration/rendering/classDiagram.spec.js",
"duration": 16452
"duration": 17334
},
{
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
"duration": 10387
"duration": 9871
},
{
"spec": "cypress/integration/rendering/current.spec.js",
"duration": 2803
"duration": 2833
},
{
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
"duration": 86891
"duration": 85321
},
{
"spec": "cypress/integration/rendering/erDiagram.spec.js",
"duration": 15206
"duration": 15673
},
{
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
"duration": 3540
"duration": 3724
},
{
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
"duration": 41975
"duration": 41178
},
{
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
"duration": 30909
"duration": 29966
},
{
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
"duration": 7881
"duration": 7689
},
{
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
"duration": 24294
"duration": 24709
},
{
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
"duration": 47652
"duration": 45565
},
{
"spec": "cypress/integration/rendering/flowchart.spec.js",
"duration": 32049
"duration": 31144
},
{
"spec": "cypress/integration/rendering/gantt.spec.js",
"duration": 20248
"duration": 20808
},
{
"spec": "cypress/integration/rendering/gitGraph.spec.js",
"duration": 51202
"duration": 49985
},
{
"spec": "cypress/integration/rendering/iconShape.spec.ts",
"duration": 283546
"duration": 273272
},
{
"spec": "cypress/integration/rendering/imageShape.spec.ts",
"duration": 57257
"duration": 55880
},
{
"spec": "cypress/integration/rendering/info.spec.ts",
"duration": 3352
"duration": 3271
},
{
"spec": "cypress/integration/rendering/journey.spec.js",
"duration": 7423
"duration": 7293
},
{
"spec": "cypress/integration/rendering/kanban.spec.ts",
"duration": 7804
"duration": 7861
},
{
"spec": "cypress/integration/rendering/katex.spec.js",
"duration": 3847
"duration": 3922
},
{
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
"duration": 2637
"duration": 2726
},
{
"spec": "cypress/integration/rendering/mindmap.spec.ts",
"duration": 11658
"duration": 11670
},
{
"spec": "cypress/integration/rendering/newShapes.spec.ts",
"duration": 149500
"duration": 146020
},
{
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
"duration": 115427
"duration": 114244
},
{
"spec": "cypress/integration/rendering/packet.spec.ts",
"duration": 4801
"duration": 5036
},
{
"spec": "cypress/integration/rendering/pie.spec.ts",
"duration": 6786
"duration": 6545
},
{
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
"duration": 9422
"duration": 9097
},
{
"spec": "cypress/integration/rendering/radar.spec.js",
"duration": 5652
"duration": 5676
},
{
"spec": "cypress/integration/rendering/requirement.spec.js",
"duration": 2787
"duration": 2795
},
{
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
"duration": 53631
"duration": 51660
},
{
"spec": "cypress/integration/rendering/sankey.spec.ts",
"duration": 7075
},
{
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
"duration": 20446
"duration": 6957
},
{
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
"duration": 37326
"duration": 36026
},
{
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
"duration": 29208
"duration": 29551
},
{
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
"duration": 16328
"duration": 17364
},
{
"spec": "cypress/integration/rendering/theme.spec.js",
"duration": 30541
"duration": 30209
},
{
"spec": "cypress/integration/rendering/timeline.spec.ts",
"duration": 8611
"duration": 8699
},
{
"spec": "cypress/integration/rendering/treemap.spec.ts",
"duration": 11878
"duration": 12168
},
{
"spec": "cypress/integration/rendering/xyChart.spec.js",
"duration": 20400
"duration": 21453
},
{
"spec": "cypress/integration/rendering/zenuml.spec.js",
"duration": 3528
"duration": 3577
}
]
}

View File

@@ -6,7 +6,7 @@
# Frequently Asked Questions
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
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)

View File

@@ -19,7 +19,6 @@
- [addDirective](functions/addDirective.md)
- [getConfig](functions/getConfig.md)
- [getSiteConfig](functions/getSiteConfig.md)
- [getUserDefinedConfig](functions/getUserDefinedConfig.md)
- [reset](functions/reset.md)
- [sanitize](functions/sanitize.md)
- [saveConfigFromInitialize](functions/saveConfigFromInitialize.md)

View File

@@ -1,19 +0,0 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md).
[**mermaid**](../../README.md)
---
# Function: getUserDefinedConfig()
> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
Defined in: [packages/mermaid/src/config.ts:252](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L252)
## Returns
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)

View File

@@ -10,7 +10,7 @@
# Interface: ParseOptions
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mer
> `optional` **suppressErrors**: `boolean`
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77)
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
The `parseError` function will not be called.

View File

@@ -10,7 +10,7 @@
# Interface: ParseResult
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mer
> **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
The config passed as YAML frontmatter or directives
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
The diagram type, e.g. 'flowchart', 'sequence', etc.

View File

@@ -10,7 +10,7 @@
# Interface: RenderResult
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/me
> `optional` **bindFunctions**: (`element`) => `void`
Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
Bind function to be called after the svg has been inserted into the DOM.
This is necessary for adding event listeners to the elements in the svg.
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
The diagram type, e.g. 'flowchart', 'sequence', etc.
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
> **svg**: `string`
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
The svg code for the rendered graph.

View File

@@ -983,23 +983,11 @@ flowchart TD
- `b`
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
- `on`
- `off`
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
```mermaid-example
flowchart TD
%% My image with a constrained aspect ratio
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
```
```mermaid
flowchart TD
%% My image with a constrained aspect ratio
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
```
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
## Links between nodes

View File

@@ -194,6 +194,46 @@ sequenceDiagram
Bob->>Alice: Queue response
```
### Icon
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
participant Bob
Alice->>Bob: Icon participant
Bob->>Alice: Response to icon
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
participant Bob
Alice->>Bob: Icon participant
Bob->>Alice: Response to icon
```
### Image
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Bob
Alice->>Bob: Image participant
Bob->>Alice: Response to image
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Bob
Alice->>Bob: Image participant
Bob->>Alice: Response to image
```
### Aliases
The actor can have a convenient identifier and a descriptive label.

View File

@@ -68,7 +68,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "^7.0.4",
"@iconify/utils": "^3.0.1",
"@iconify/utils": "^2.1.33",
"@mermaid-js/parser": "workspace:^",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.3",

View File

@@ -78,187 +78,3 @@ describe('when working with site config', () => {
expect(config_4.altFontFamily).toBeUndefined();
});
});
describe('getUserDefinedConfig', () => {
beforeEach(() => {
configApi.reset();
});
it('should return empty object when no user config is defined', () => {
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toEqual({});
});
it('should return config from initialize only', () => {
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial' };
configApi.saveConfigFromInitialize(initConfig);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toEqual(initConfig);
});
it('should return config from directives only', () => {
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
const directive2: MermaidConfig = { theme: 'forest' };
configApi.addDirective(directive1);
configApi.addDirective(directive2);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"fontFamily": "Arial",
"fontSize": 14,
"layout": "elk",
"theme": "forest",
}
`);
});
it('should combine initialize config and directives', () => {
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial', layout: 'dagre' };
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
const directive2: MermaidConfig = { theme: 'forest' };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive1);
configApi.addDirective(directive2);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"fontFamily": "Arial",
"fontSize": 14,
"layout": "elk",
"theme": "forest",
}
`);
});
it('should handle nested config objects properly', () => {
const initConfig: MermaidConfig = {
flowchart: { nodeSpacing: 50, rankSpacing: 100 },
theme: 'default',
};
const directive: MermaidConfig = {
flowchart: { nodeSpacing: 75, curve: 'basis' },
mindmap: { padding: 20 },
};
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"flowchart": {
"curve": "basis",
"nodeSpacing": 75,
"rankSpacing": 100,
},
"mindmap": {
"padding": 20,
},
"theme": "default",
}
`);
});
it('should handle complex nested overrides', () => {
const initConfig: MermaidConfig = {
flowchart: {
nodeSpacing: 50,
rankSpacing: 100,
curve: 'linear',
},
theme: 'default',
};
const directive1: MermaidConfig = {
flowchart: {
nodeSpacing: 75,
},
fontSize: 12,
};
const directive2: MermaidConfig = {
flowchart: {
curve: 'basis',
nodeSpacing: 100,
},
mindmap: {
padding: 15,
},
};
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive1);
configApi.addDirective(directive2);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"flowchart": {
"curve": "basis",
"nodeSpacing": 100,
"rankSpacing": 100,
},
"fontSize": 12,
"mindmap": {
"padding": 15,
},
"theme": "default",
}
`);
});
it('should return independent copies (not references)', () => {
const initConfig: MermaidConfig = { theme: 'dark', flowchart: { nodeSpacing: 50 } };
configApi.saveConfigFromInitialize(initConfig);
const userConfig1 = configApi.getUserDefinedConfig();
const userConfig2 = configApi.getUserDefinedConfig();
userConfig1.theme = 'neutral';
userConfig1.flowchart!.nodeSpacing = 999;
expect(userConfig2).toMatchInlineSnapshot(`
{
"flowchart": {
"nodeSpacing": 50,
},
"theme": "dark",
}
`);
});
it('should handle edge cases with undefined values', () => {
const initConfig: MermaidConfig = { theme: 'dark', layout: undefined };
const directive: MermaidConfig = { fontSize: 14, fontFamily: undefined };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"fontSize": 14,
"layout": undefined,
"theme": "dark",
}
`);
});
it('should retain config from initialize after reset', () => {
const initConfig: MermaidConfig = { theme: 'dark' };
const directive: MermaidConfig = { layout: 'elk' };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"layout": "elk",
"theme": "dark",
}
`);
configApi.reset();
});
});

View File

@@ -248,17 +248,3 @@ const checkConfig = (config: MermaidConfig) => {
issueWarning('LAZY_LOAD_DEPRECATED');
}
};
export const getUserDefinedConfig = (): MermaidConfig => {
let userConfig: MermaidConfig = {};
if (configFromInitialize) {
userConfig = assignWithDepth(userConfig, configFromInitialize);
}
for (const d of directives) {
userConfig = assignWithDepth(userConfig, d);
}
return userConfig;
};

View File

@@ -28,7 +28,6 @@ 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;
@@ -102,7 +101,6 @@ export const addDiagrams = () => {
xychart,
block,
radar,
treemap,
usecase
treemap
);
};

View File

@@ -13,8 +13,7 @@ import {
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
import type { ParticipantMetaData } from '../../types.js';
import type { Actor, AddMessageParams, Box, Message, Note, ParticipantMetaData } from './types.js';
interface SequenceState {
prevActor?: string;
@@ -86,6 +85,8 @@ export const PARTICIPANT_TYPE = {
ENTITY: 'entity',
PARTICIPANT: 'participant',
QUEUE: 'queue',
ICON: 'icon',
IMAGE: 'image',
} as const;
export class SequenceDB implements DiagramDB {
@@ -186,6 +187,7 @@ export class SequenceDB implements DiagramDB {
actorCnt: null,
rectData: null,
type: type ?? 'participant',
doc: doc,
});
if (this.state.records.prevActor) {
const prevActorInRecords = this.state.records.actors.get(this.state.records.prevActor);

View File

@@ -2326,4 +2326,73 @@ Bob->>Alice:Got it!
expect(actors.get('E').description).toBe('E');
});
});
describe('image and icon participant parsing', () => {
it('should parse image participant with image URL', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
Bob->>Bob: test
`);
const actors = diagram.db.getActors();
expect(actors.get('Bob').type).toBe('image');
expect(actors.get('Bob').doc.image).toBe(
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
);
});
it('should parse icon participant with icon name', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Alice@{ "type": "icon", "icon": "fa:bell" }
Alice->>Alice: test
`);
const actors = diagram.db.getActors();
expect(actors.get('Alice').type).toBe('icon');
expect(actors.get('Alice').doc.icon).toBe('fa:bell');
});
it('should parse two image participants', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg" }
Bob->>Alice: Hello
Alice-->>Bob: Hi
`);
const actors = diagram.db.getActors();
expect(actors.get('Bob').type).toBe('image');
expect(actors.get('Bob').doc.image).toBe(
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
);
expect(actors.get('Alice').type).toBe('image');
expect(actors.get('Alice').doc.image).toBe(
'https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg'
);
});
it('should parse image participant with normal participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: Hello
`);
const actors = diagram.db.getActors();
expect(actors.get('Bob').type).toBe('image');
expect(actors.get('Alice').type).toBe('participant');
});
it('should parse icon participant with normal participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Bob@{ "type": "icon", "icon": "fa:bell" }
participant Alice
Bob->>Alice: Hello
`);
const actors = diagram.db.getActors();
expect(actors.get('Bob').type).toBe('icon');
expect(actors.get('Alice').type).toBe('participant');
});
});
});

View File

@@ -752,6 +752,8 @@ function adjustCreatedDestroyedData(
PARTICIPANT_TYPE.CONTROL,
PARTICIPANT_TYPE.ENTITY,
PARTICIPANT_TYPE.DATABASE,
PARTICIPANT_TYPE.ICON,
PARTICIPANT_TYPE.IMAGE,
];
// if it is a create message

View File

@@ -1,5 +1,6 @@
import { sanitizeUrl } from '@braintree/sanitize-url';
import * as configApi from '../../config.js';
import { getIconSVG } from '../../rendering-util/icons.js';
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
import common, {
calculateMathMLDimensions,
@@ -322,6 +323,89 @@ export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => {
});
};
const drawActorTypeIcon = async function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + actor.height / 2;
const line = elem.append('g').lower();
if (!isFooter) {
actorCnt++;
line
.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY + 25)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
actor.actorCnt = actorCnt;
}
const actElem = elem.append('g');
let cssClass = 'actor-icon';
if (isFooter) {
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssClass += ` ${TOP_ACTOR_CLASS}`;
}
actElem.attr('class', cssClass);
actElem.attr('name', actor.name);
// Define the size of the square and icon
const iconSize = actor.width / 5;
const squareX = center - iconSize / 2;
const squareY = !isFooter ? actorY + 10 : actorY;
// Draw a square rectangle for the actor icon background
actElem
.append('rect')
.attr('x', squareX)
.attr('y', squareY)
.attr('width', iconSize)
.attr('height', iconSize)
.attr('rx', 3) // rounded corners, optional
.attr('ry', 3)
.attr('fill', 'none'); // light gray background or customize as needed
// Render icon SVG inside the rectangle
const iconGroup = actElem.append('g').attr('transform', `translate(${squareX}, ${squareY})`);
iconGroup
.append('svg')
.attr('width', iconSize)
.attr('height', iconSize)
.html(
`<g>${await getIconSVG(actor.doc.icon, {
height: iconSize,
width: iconSize,
fallbackPrefix: '',
})}</g>`
);
// Add text label below icon
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
actElem,
actor.x,
actorY + (!isFooter ? 40 : 30), // positioning below the square icon
actor.width,
20,
{ class: 'actor-icon-text' },
conf
);
const bounds = actElem.node().getBBox();
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
return actor.height;
};
/**
* Draws an actor in the diagram with the attached line
*
@@ -414,6 +498,174 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
return height;
};
const drawActorTypeImage = async function (elem, actor, conf, isFooter) {
const img = new Image();
img.src = actor.doc.image ?? '';
await img.decode();
const imageNaturalWidth = Number(img.naturalWidth.toString().replace('px', ''));
const imageNaturalHeight = Number(img.naturalHeight.toString().replace('px', ''));
actor.doc.imageAspectRatio = imageNaturalWidth / imageNaturalHeight;
// Calculate image dimensions with proper sizing logic
let imageWidth, imageHeight;
// Check if custom dimensions are provided and valid
const hasValidCustomDimensions =
actor.doc.height && actor.doc.height > 10 && actor.doc.width && actor.doc.width > 10;
if (hasValidCustomDimensions) {
if (actor.doc.constraint === 'on') {
// Maintain aspect ratio with constraint
const customAspectRatio = imageNaturalWidth / imageNaturalHeight;
if (customAspectRatio > actor.doc.imageAspectRatio) {
// Width is the limiting factor
imageHeight = actor.doc.height;
imageWidth = actor.doc.height * actor.doc.imageAspectRatio;
} else {
// Height is the limiting factor
imageWidth = actor.doc.width;
imageHeight = actor.doc.width / actor.doc.imageAspectRatio;
}
} else {
// Use custom dimensions without maintaining aspect ratio
imageWidth = actor.doc.width;
imageHeight = actor.doc.height;
}
} else {
// Use default sizing based on actor width
const defaultImageSize = actor.width / 3.5;
// Ensure minimum and maximum sizes
const minSize = 30;
const maxSize = actor.width * 0.8;
if (actor.doc.imageAspectRatio >= 1) {
// Landscape or square image
imageWidth = Math.min(Math.max(defaultImageSize, minSize), maxSize);
imageHeight = imageWidth / actor.doc.imageAspectRatio;
} else {
// Portrait image
imageHeight = Math.min(Math.max(defaultImageSize, minSize), maxSize);
imageWidth = imageHeight * actor.doc.imageAspectRatio;
}
// Ensure the image doesn't exceed actor bounds
if (imageWidth > actor.width * 0.9) {
imageWidth = actor.width * 0.9;
imageHeight = imageWidth / actor.doc.imageAspectRatio;
}
}
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + imageHeight;
// Calculate positioning
const squareX = center - imageWidth / 2;
let squareY;
if (isFooter) {
squareY = actorY + (imageHeight - imageHeight * 1);
} else {
squareY = actorY + 5;
}
// Calculate text position based on image position and size
const textY = !isFooter ? squareY + imageHeight : actorY + imageHeight; // Place text below image for header
// Draw actor line for non-footer elements
const x = center;
const y = centerY + (isFooter ? 0 : imageHeight / 2); // Adjust line start based on image height
const line = elem.append('g').lower();
if (!isFooter) {
actorCnt++;
line
.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', x)
.attr('y1', y) // Adjust line start based on image height
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
actor.actorCnt = actorCnt;
}
const actElem = elem.append('g');
let cssClass = 'actor-image';
if (isFooter) {
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssClass += ` ${TOP_ACTOR_CLASS}`;
}
actElem.attr('class', cssClass);
actElem.attr('name', actor.name);
// Draw background rectangle for the actor image
actElem
.append('rect')
.attr('x', squareX)
.attr('y', squareY)
.attr('width', imageWidth)
.attr('height', imageHeight)
.attr('rx', 3)
.attr('ry', 3)
.attr('fill', 'white')
.attr('stroke', '#ddd')
.attr('stroke-width', '1px');
// Create clipping path for the image
const clipId = `clip-actor-${actorCnt || 'footer'}-${Math.random().toString(36).substr(2, 9)}`;
actElem
.append('defs')
.append('clipPath')
.attr('id', clipId)
.append('rect')
.attr('x', squareX)
.attr('y', squareY)
.attr('width', imageWidth)
.attr('height', imageHeight)
.attr('rx', 3)
.attr('ry', 3);
// Render image inside the rectangle
const imageGroup = actElem.append('g');
if (actor.doc.image) {
imageGroup
.append('image')
.attr('x', squareX)
.attr('y', squareY)
.attr('width', imageWidth)
.attr('height', imageHeight)
.attr('href', img.src)
.attr('preserveAspectRatio', actor.doc.constraint === 'on' ? 'xMidYMid meet' : 'none');
}
// Add text label
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
actElem,
actor.x,
textY,
actor.width,
20,
{ class: 'actor-image-text' },
conf
);
// Calculate final bounds and update actor height
const bounds = actElem.node().getBBox();
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
return actor.height;
};
/**
* Draws an actor in the diagram with the attached line
@@ -1122,6 +1374,10 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
return await drawActorTypeCollections(elem, actor, conf, isFooter);
case 'queue':
return await drawActorTypeQueue(elem, actor, conf, isFooter);
case 'icon':
return await drawActorTypeIcon(elem, actor, conf, isFooter);
case 'image':
return await drawActorTypeImage(elem, actor, conf, isFooter);
}
};

View File

@@ -17,6 +17,9 @@ export interface Actor {
actorCnt: number | null;
rectData: unknown;
type: string;
doc?: ParticipantMetaData; // For documentation
iconName?: string; // For icon type
imgSrc?: string; // For img type
}
export interface Message {
@@ -90,3 +93,20 @@ export interface Note {
message: string;
wrap: boolean;
}
export interface ParticipantMetaData {
type?:
| 'actor'
| 'participant'
| 'boundary'
| 'control'
| 'entity'
| 'database'
| 'collections'
| 'queue'
| 'icon'
| 'img';
icon?: string;
img?: string;
form?: string;
}

View File

@@ -1,124 +0,0 @@
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;

View File

@@ -1,586 +0,0 @@
// 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
};
}
}

View File

@@ -1,24 +0,0 @@
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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +0,0 @@
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();
}
},
};

View File

@@ -1,619 +0,0 @@
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,
};

View File

@@ -31,7 +31,7 @@
"fast-glob": "^3.3.3",
"https-localhost": "^4.7.1",
"pathe": "^2.0.3",
"unocss": "^66.4.2",
"unocss": "^66.0.0",
"unplugin-vue-components": "^28.4.0",
"vite": "^6.1.1",
"vite-plugin-pwa": "^1.0.0",

View File

@@ -590,17 +590,11 @@ flowchart TD
- `b`
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
- `on`
- `off`
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
```mermaid
flowchart TD
%% My image with a constrained aspect ratio
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
```
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
## Links between nodes

View File

@@ -118,6 +118,30 @@ sequenceDiagram
Bob->>Alice: Queue response
```
### Icon
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
participant Bob
Alice->>Bob: Icon participant
Bob->>Alice: Response to icon
```
### Image
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Bob
Alice->>Bob: Image participant
Bob->>Alice: Response to image
```
### Aliases
The actor can have a convenient identifier and a descriptive label.

View File

@@ -41,6 +41,7 @@ import { decodeEntities, encodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js';
import { StateDB } from './diagrams/state/stateDb.js';
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
import { select } from 'd3';
import { JSDOM } from 'jsdom';
/**
@@ -49,6 +50,7 @@ import { JSDOM } from 'jsdom';
*/
// -------------------------------------------------------------------------------------
describe('mermaidAPI', () => {
describe('encodeEntities', () => {
it('removes the ending ; from style [text1]:[optional word]#[text2]; with ', () => {
@@ -911,241 +913,4 @@ graph TD;A--x|text including URL space|B;`)
expect(sequenceDiagram1.db.getActors()).not.toEqual(sequenceDiagram2.db.getActors());
});
});
describe('mermaidAPI config precedence', () => {
const id = 'mermaid-config-test';
beforeEach(() => {
mermaidAPI.globalReset();
});
jsdomIt('renders with YAML config taking precedence over initialize config', async () => {
mermaid.initialize({
theme: 'forest',
fontFamily: 'Arial',
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
flowchart: { htmlLabels: false },
});
const diagramText = `---
config:
theme: base
fontFamily: Courier
themeVariables:
fontFamily: "Courier New"
fontSize: "20px"
flowchart:
htmlLabels: true
---
flowchart TD
A --> B
`;
const { svg } = await mermaidAPI.render('yaml-over-init', diagramText);
const config = mermaidAPI.getConfig();
expect(config.theme).toBe('base');
expect(config.fontFamily).toBe('Courier');
expect(config.themeVariables.fontFamily).toBe('Courier New');
expect(config.themeVariables.fontSize).toBe('20px');
expect(config.flowchart?.htmlLabels).toBe(true);
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
expect(svgNode).not.toBeNull();
});
jsdomIt(
'renders with YAML themeVariables fully overriding initialize themeVariables',
async () => {
mermaid.initialize({
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
});
const diagramText = `---
config:
themeVariables:
fontFamily: "Courier New"
fontSize: "20px"
---
flowchart TD
A --> B
`;
const { svg } = await mermaidAPI.render(id, diagramText);
const config = mermaidAPI.getConfig();
expect(config.themeVariables.fontFamily).toBe('Courier New');
expect(config.themeVariables.fontSize).toBe('20px');
expect(config.themeVariables.fontFamily).not.toBe('Arial');
expect(config.themeVariables.fontSize).not.toBe('16px');
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
expect(svgNode).not.toBeNull();
}
);
jsdomIt(
'renders with YAML themeVariables overriding only provided keys and keeping others from initialize',
async () => {
mermaid.initialize({
theme: 'forest',
fontFamily: 'Arial',
themeVariables: { fontFamily: 'Arial', fontSize: '16px', colorPrimary: '#ff0000' },
});
const diagramText = `---
config:
themeVariables:
fontFamily: "Courier New"
---
flowchart TD
A --> B
`;
const { svg } = await mermaidAPI.render(id, diagramText);
const config = mermaidAPI.getConfig();
expect(config.themeVariables.fontFamily).toBe('Courier New');
expect(config.themeVariables.fontSize).toBe('16px');
expect(config.themeVariables.colorPrimary).toBe('#ff0000');
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
expect(svgNode).not.toBeNull();
}
);
jsdomIt(
'renders with YAML config (no themeVariables) and falls back to initialize themeVariables',
async () => {
mermaid.initialize({
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
});
const diagramText = `---
config:
theme: base
---
flowchart TD
A --> B
`;
const { svg } = await mermaidAPI.render(id, diagramText);
const config = mermaidAPI.getConfig();
expect(config.themeVariables.fontFamily).toBe('Arial');
expect(config.themeVariables.fontSize).toBe('16px');
expect(config.theme).toBe('base');
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
expect(svgNode).not.toBeNull();
}
);
jsdomIt(
'renders with full YAML config block taking full precedence over initialize config',
async () => {
mermaid.initialize({
theme: 'forest',
fontFamily: 'Arial',
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
flowchart: { htmlLabels: false },
});
const diagramText = `---
config:
theme: base
fontFamily: Courier
themeVariables:
fontFamily: "Courier New"
fontSize: "20px"
flowchart:
htmlLabels: true
---
flowchart TD
A --> B
`;
const { svg } = await mermaidAPI.render('yaml-over-init', diagramText);
const config = mermaidAPI.getConfig();
expect(config.theme).toBe('base');
expect(config.fontFamily).toBe('Courier');
expect(config.themeVariables.fontFamily).toBe('Courier New');
expect(config.themeVariables.fontSize).toBe('20px');
expect(config.flowchart?.htmlLabels).toBe(true);
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
expect(svgNode).not.toBeNull();
}
);
jsdomIt(
'renders with YAML config (no themeVariables) and falls back to initialize themeVariables (duplicate scenario)',
async () => {
mermaid.initialize({
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
});
const diagramText = `---
config:
theme: base
---
flowchart TD
A --> B
`;
await mermaidAPI.render(id, diagramText);
const config = mermaidAPI.getConfig();
expect(config.themeVariables.fontFamily).toBe('Arial');
expect(config.themeVariables.fontSize).toBe('16px');
expect(config.theme).toBe('base');
}
);
jsdomIt('renders with no YAML config so initialize config is fully applied', async () => {
mermaid.initialize({
theme: 'forest',
fontFamily: 'Arial',
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
});
const diagramText = `
flowchart TD
A --> B
`;
await mermaidAPI.render(id, diagramText);
const config = mermaidAPI.getConfig();
expect(config.theme).toBe('forest');
expect(config.fontFamily).toBe('Arial');
expect(config.themeVariables.fontFamily).toBe('Arial');
expect(config.themeVariables.fontSize).toBe('16px');
});
jsdomIt(
'renders with empty YAML config block and falls back to initialize config',
async () => {
mermaid.initialize({
theme: 'dark',
themeVariables: { fontFamily: 'Times', fontSize: '14px' },
});
const diagramText = `---
config: {}
---
flowchart TD
A --> B
`;
await mermaidAPI.render(id, diagramText);
const config = mermaidAPI.getConfig();
expect(config.theme).toBe('dark');
expect(config.themeVariables.fontFamily).toBe('Times');
expect(config.themeVariables.fontSize).toBe('14px');
}
);
});
});

View File

@@ -13,18 +13,6 @@ export interface NodeMetaData {
ticket?: string;
}
export interface ParticipantMetaData {
type?:
| 'actor'
| 'participant'
| 'boundary'
| 'control'
| 'entity'
| 'database'
| 'collections'
| 'queue';
}
export interface EdgeMetaData {
animation?: 'fast' | 'slow';
animate?: boolean;

View File

@@ -19,9 +19,7 @@
"scripts": {
"clean": "rimraf dist src/language/generated",
"langium:generate": "langium generate",
"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"
"langium:watch": "langium generate --watch"
},
"repository": {
"type": "git",
@@ -35,8 +33,6 @@
"ast"
],
"dependencies": {
"antlr4ts": "0.5.0-alpha.4",
"antlr4ts-cli": "0.5.0-alpha.4",
"langium": "3.3.1"
},
"devDependencies": {

View File

@@ -45,4 +45,3 @@ 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';

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +0,0 @@
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

View File

@@ -1,71 +0,0 @@
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]

View File

@@ -1,213 +0,0 @@
// 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);
}
}
}

View File

@@ -1,29 +0,0 @@
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,184 +0,0 @@
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
;

View File

@@ -1,2 +0,0 @@
export { parseUsecase } from './usecaseParser.js';
export * from './usecaseTypes.js';

View File

@@ -1,387 +0,0 @@
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.');

View File

@@ -1,752 +0,0 @@
// 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)]
};
}
}

View File

@@ -1,113 +0,0 @@
// 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[];
}

440
pnpm-lock.yaml generated
View File

@@ -227,8 +227,8 @@ importers:
specifier: ^7.0.4
version: 7.1.0
'@iconify/utils':
specifier: ^3.0.1
version: 3.0.1
specifier: ^2.1.33
version: 2.3.0
'@mermaid-js/parser':
specifier: workspace:^
version: link:../parser
@@ -499,8 +499,8 @@ importers:
specifier: ^2.0.3
version: 2.0.3
unocss:
specifier: ^66.4.2
version: 66.4.2(postcss@8.5.6)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))
specifier: ^66.0.0
version: 66.0.0(postcss@8.5.6)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.4.0(@babel/parser@7.28.0)(vue@3.5.13(typescript@5.7.3))
@@ -647,11 +647,11 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
'@antfu/install-pkg@1.0.0':
resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==}
'@antfu/utils@9.2.0':
resolution: {integrity: sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==}
'@antfu/utils@8.1.1':
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
'@apideck/better-ajv-errors@0.3.6':
resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
@@ -2460,8 +2460,8 @@ packages:
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
'@iconify/utils@3.0.1':
resolution: {integrity: sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==}
'@iconify/utils@2.3.0':
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
@@ -2740,9 +2740,6 @@ packages:
'@polka/url@1.0.0-next.28':
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
'@quansync/fs@0.1.4':
resolution: {integrity: sha512-vy/41FCdnIalPTQCb2Wl0ic1caMdzGus4ktDp+gpZesQNydXcx8nhh8qB3qMPbGkictOTaXgXEUUfQEm8DQYoA==}
'@react-aria/focus@3.21.0':
resolution: {integrity: sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA==}
peerDependencies:
@@ -3541,94 +3538,88 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@unocss/astro@66.4.2':
resolution: {integrity: sha512-En3AKHwkiPxtZT95vkVrNiRYrB+DFVCikew6/dMMCWDWVKK0+5tEVUTzR1ak3+YnzAXl0NpWj8D4zHb0PxOs/A==}
'@unocss/astro@66.0.0':
resolution: {integrity: sha512-GBhXT6JPqXjDXoJZTXhySk83NgOt0UigChqrUUdG4x7Z+DVYkDBION8vZUJjw0OdIaxNQ4euGWu4GDsMF6gQQg==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
peerDependenciesMeta:
vite:
optional: true
'@unocss/cli@66.4.2':
resolution: {integrity: sha512-WsXzrB0SHbSt2nOHtD5QM91VN8j38+wObqyGcoIhtBSugqzsc+t7AdPkxV/ZaYgtPAz87bR0WFEVKcbiBRnmJw==}
'@unocss/cli@66.0.0':
resolution: {integrity: sha512-KVQiskoOjVkLVpNaG6WpLa4grPplrZROYZJVIUYSTqZyZRFNSvjttHcsCwpoWUEUdEombPtVZl8FrXePjY5IiQ==}
engines: {node: '>=14'}
hasBin: true
'@unocss/config@66.4.2':
resolution: {integrity: sha512-plji1gNGSzlWjuV2Uh0q6Dt5ZlNkOKCHpgxekW9J458WghGAMBeXgB9uNpWg6flilqP1g0GJQv+XvJcSkYRGpQ==}
'@unocss/config@66.0.0':
resolution: {integrity: sha512-nFRGop/guBa4jLkrgXjaRDm5JPz4x3YpP10m5IQkHpHwlnHUVn1L9smyPl04ohYWhYn9ZcAHgR28Ih2jwta8hw==}
engines: {node: '>=14'}
'@unocss/core@66.4.2':
resolution: {integrity: sha512-cYgMQrLhB9nRekv5c+yPDDa+5dzlMkA2UMQRil0s5D9Lb5n7NsCMcr6+nfxkcSYVLy92SbwDV45c6T7vIxFTOA==}
'@unocss/core@66.0.0':
resolution: {integrity: sha512-PdVbSMHNDDkr++9nkqzsZRAkaU84gxMTEgYbqI7dt2p1DXp/5tomVtmMsr2/whXGYKRiUc0xZ3p4Pzraz8TcXA==}
'@unocss/extractor-arbitrary-variants@66.4.2':
resolution: {integrity: sha512-T/eSeodfAp7HaWnQGqVLOsW4PbKUAvuybNRyvFWThMneM2qo+dOo3kFnA5my9ULAmRSFsAlyB1DnupD3qv5Klg==}
'@unocss/extractor-arbitrary-variants@66.0.0':
resolution: {integrity: sha512-vlkOIOuwBfaFBJcN6o7+obXjigjOlzVFN/jT6pG1WXbQDTRZ021jeF3i9INdb9D/0cQHSeDvNgi1TJ5oUxfiow==}
'@unocss/inspector@66.4.2':
resolution: {integrity: sha512-ugcJK8r2ypM4eIdgetVn8RhfKrbA3AF3OQ/RohK5PPk2UPDAScqabzYpfdNW4eYQsBOZOgoiqWtnfc8weqo8LQ==}
'@unocss/inspector@66.0.0':
resolution: {integrity: sha512-mkIxieVm0kMOKw+E4ABpIerihYMdjgq9A92RD5h2+W/ebpxTEw5lTTK1xcMLiAlmOrVYMQKjpgPeu3vQmDyGZQ==}
'@unocss/postcss@66.4.2':
resolution: {integrity: sha512-tu4lnh6K27pIAuaQHlFlhXin8korwC0r1kQl00YMmF3THiX7orXkTP6xWGcQwnkbx4uQz1dw+tBimYxeaAMrhA==}
'@unocss/postcss@66.0.0':
resolution: {integrity: sha512-6bi+ujzh8I1PJwtmHX71LH8z/H9+vPxeYD4XgFihyU1k4Y6MVhjr7giGjLX4yP27IP+NsVyotD22V7by/dBVEA==}
engines: {node: '>=14'}
peerDependencies:
postcss: ^8.4.21
'@unocss/preset-attributify@66.4.2':
resolution: {integrity: sha512-DwFJJkkawmHpjo3pGQE8FyoPsvhbxh+QMvvaAdYpo+iZ5HRkeDml9SOj7u6SGTcmbNyI+QR61s0KM8fxx6HcVQ==}
'@unocss/preset-attributify@66.0.0':
resolution: {integrity: sha512-eYsOgmcDoiIgGAepIwRX+DKGYxc/wm0r4JnDuZdz29AB+A6oY/FGHS1BVt4rq9ny4B5PofP4p6Rty+vwD9rigw==}
'@unocss/preset-icons@66.4.2':
resolution: {integrity: sha512-qJx9gmesrvrmoTe9Mqoidihad8hm2MSD4QAezhfDSAyllioJOgyT0Bev/IEWAbehe9jtqYIh8v1oCerBPbGn6Q==}
'@unocss/preset-icons@66.0.0':
resolution: {integrity: sha512-6ObwTvEGuPBbKWRoMMiDioHtwwQTFI5oojFLJ32Y8tW6TdXvBLkO88d7qpgQxEjgVt4nJrqF1WEfR4niRgBm0Q==}
'@unocss/preset-mini@66.4.2':
resolution: {integrity: sha512-Ry+5hM+XLmT8HrEb182mUfcZuyrZ8xR+TBe72DBcliJ1DhOV3K67TCxwQucfb0zHbGV71HNWdPmHsLKxPDgweQ==}
'@unocss/preset-mini@66.0.0':
resolution: {integrity: sha512-d62eACnuKtR0dwCFOQXgvw5VLh5YSyK56xCzpHkh0j0GstgfDLfKTys0T/XVAAvdSvAy/8A8vhSNJ4PlIc9V2A==}
'@unocss/preset-tagify@66.4.2':
resolution: {integrity: sha512-dECS09LqWJY4sYpgPUH2OAUftWU/tiZPR2XDRoTngeGU37GxSN+1sWtSmB7vwDm3C7opsdVUN20he8F1LUNubw==}
'@unocss/preset-tagify@66.0.0':
resolution: {integrity: sha512-GGYGyWxaevh0jN0NoATVO1Qe7DFXM3ykLxchlXmG6/zy963pZxItg/njrKnxE9la4seCdxpFH7wQBa68imwwdA==}
'@unocss/preset-typography@66.4.2':
resolution: {integrity: sha512-ZOKRuR5+V0r30QTVq04/6ZoIw75me3V25v2dU2YWJXIzwpMKmQ9TUN/M1yeiEUFfXjOaruWX6Ad6CvAw2MlCew==}
'@unocss/preset-typography@66.0.0':
resolution: {integrity: sha512-apjckP5nPU5mtaHTCzz5u/dK9KJWwJ2kOFCVk0+a/KhUWmnqnzmjRYZlEuWxxr5QxTdCW+9cIoRDSA0lYZS5tg==}
'@unocss/preset-uno@66.4.2':
resolution: {integrity: sha512-1MFtPivGcpqRQFWdjtP40Enop1y3XDb3tlZXoMQUX0IGLG8HJOT+lfQx/Xl9t73ShJ8aAJ/l6qTxC43ZGNACzA==}
'@unocss/preset-uno@66.0.0':
resolution: {integrity: sha512-qgoZ/hzTI32bQvcyjcwvv1X/dbPlmQNehzgjUaL7QFT0q0/CN/SRpysfzoQ8DLl2se9T+YCOS9POx3KrpIiYSQ==}
'@unocss/preset-web-fonts@66.4.2':
resolution: {integrity: sha512-4FYmleeRoM8r2DqGl6dfIjnX57tepcfZCvVfeCqYnk7475Yddmv1OYkoMjkWMnkK9MzdSxsFwHMU6CIUTmFTzQ==}
'@unocss/preset-web-fonts@66.0.0':
resolution: {integrity: sha512-9MzfDc6AJILN4Kq7Z91FfFbizBOYgw3lJd2UwqIs3PDYWG5iH5Zv5zhx6jelZVqEW5uWcIARYEEg2m4stZO1ZA==}
'@unocss/preset-wind3@66.4.2':
resolution: {integrity: sha512-0Aye/PaT08M/cQhPnGKn93iEVoRJbym0/1eomMvXoL+8oc7DVry35ws06r5CLu5h1sXI6UmS6sejoePFlSkLJQ==}
'@unocss/preset-wind3@66.0.0':
resolution: {integrity: sha512-WAGRmpi1sb2skvYn9DBQUvhfqrJ+VmQmn5ZGsT2ewvsk7HFCvVLAMzZeKrrTQepeNBRhg6HzFDDi8yg6yB5c9g==}
'@unocss/preset-wind4@66.4.2':
resolution: {integrity: sha512-F4RZsDqIpnSevD9hY353+Tw5gxpJuHA5HwdKjLnC/TnT9VKKVmV7qUEZ6M0jEuAk1kz2x3/ngnQ9Ftw+C2L84A==}
'@unocss/preset-wind@66.4.2':
resolution: {integrity: sha512-z/rFYFINNqmBtl3Dh+7UCKpPnPkxM7IIUGszMnvdntky9uhLauJ11dt/Puir73sM2cAfywfgvnHyZ00m0pg7rA==}
'@unocss/preset-wind@66.0.0':
resolution: {integrity: sha512-FtvGpHnGC7FiyKJavPnn5y9lsaoWRhXlujCqlT5Bw63kKhMNr0ogKySBpenUhJOhWhVM0OQXn2nZ3GZRxW2qpw==}
'@unocss/reset@66.0.0':
resolution: {integrity: sha512-YLFz/5yT7mFJC8JSmIUA5+bS3CBCJbtztOw+8rWzjQr/BEVSGuihWUUpI2Df6VVxXIXxKanZR6mIl59yvf+GEA==}
'@unocss/reset@66.4.2':
resolution: {integrity: sha512-s3Kq4Q6a/d3/jYe6HTCfXUx7zYAYufetId5n66DZHzQxpeu6CoBS83+b37STTKsw27SOgV28cPJlJtZ6/D6Bhw==}
'@unocss/rule-utils@66.4.2':
resolution: {integrity: sha512-7z3IuajwXhy2cx3E0IGOFXIiuKC79/jzm4Tt56TC68nXLh/etlH0fKhxVwkZ/HbcQRpVwWyDRNcbh29pmA3DwQ==}
'@unocss/rule-utils@66.0.0':
resolution: {integrity: sha512-UJ51YHbwxYTGyj35ugsPlOT4gaa7tCbXdywZ3m5Nn0JgywwIqGmBFyiN9ZjHBHfJuDxmmPd6lxojoBscih/WMQ==}
engines: {node: '>=14'}
'@unocss/transformer-attributify-jsx@66.4.2':
resolution: {integrity: sha512-de6LzoyW1tkdOftlCrj6z8wEb4j6l1sqmOU1nYKkYHw7luLFGxRUELC7iujlI9KmylbM02bcKfLETAfJy/je2w==}
'@unocss/transformer-attributify-jsx@66.0.0':
resolution: {integrity: sha512-jS7szFXXC6RjTv9wo0NACskf618w981bkbyQ5izRO7Ha47sNpHhHDpaltnG7SR9qV4cCtGalOw4onVMHsRKwRg==}
'@unocss/transformer-compile-class@66.4.2':
resolution: {integrity: sha512-+oiIrV8c3T7qiJdICr6YsEWik5sjbWirXF0mlpcBvZu2HyV559hvHjzuWKr/fl7xYYZKDL9FvddbqWo3DOXh3Q==}
'@unocss/transformer-compile-class@66.0.0':
resolution: {integrity: sha512-ytUIE0nAcHRMACuTXkHp8auZ483DXrOZw99jk3FJ+aFjpD/pVSFmX14AWJ7bqPFObxb4SLFs6KhQma30ESC22A==}
'@unocss/transformer-directives@66.4.2':
resolution: {integrity: sha512-7m/dTrCUkBkZeSRKPxPEo65Rav239orQSLq6sztwZhoA4x/6H8r58xCkAK0qC9VEalyerpCpyarU3sKN4+ehNg==}
'@unocss/transformer-directives@66.0.0':
resolution: {integrity: sha512-utcg7m2Foi7uHrU5WHadNuJ0a3qWG8tZNkQMi+m0DQpX6KWfuDtDn0zDZ1X+z5lmiB3WGSJERRrsvZbj1q50Mw==}
'@unocss/transformer-variant-group@66.4.2':
resolution: {integrity: sha512-SbPDbZUrhQyL4CpvnpvUfrr1DFq8AKf8ofPGbMJDm5S2TInQ34vFaIrhNroGR0szntMZRH5Zlkq6LtVUKDRs5g==}
'@unocss/transformer-variant-group@66.0.0':
resolution: {integrity: sha512-1BLjNWtAnR1JAcQGw0TS+nGrVoB9aznzvVZRoTx23dtRr3btvgKPHb8LrD48eD/p8Dtw9j3WfuxMDKXKegKDLg==}
'@unocss/vite@66.4.2':
resolution: {integrity: sha512-7eON9iPF3qWzuI+M6u0kq7K3y9nEbimZlLj01nGoqrgSGxEsyJpP01QQQsmT7FPRiZzRMJv7BiKMEyDQSuRRCA==}
'@unocss/vite@66.0.0':
resolution: {integrity: sha512-IVcPX8xL+2edyXKt4tp9yu5A6gcbPVCsspfcL0XgziCr01kS+4qSoZ90F3IUs3hXc/AyO5eCpRtGFMPLpOjXQg==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
@@ -4788,15 +4779,12 @@ packages:
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
confbox@0.2.2:
resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
connect-history-api-fallback@2.0.0:
resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==}
engines: {node: '>=0.8'}
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
consola@3.4.0:
resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==}
engines: {node: ^14.18.0 || >=16.10.0}
console.table@0.10.0:
@@ -5864,9 +5852,6 @@ packages:
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
engines: {node: '>= 18'}
exsolve@1.0.7:
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
@@ -7291,10 +7276,6 @@ packages:
resolution: {integrity: sha512-bbgPw/wmroJsil/GgL4qjDzs5YLTBMQ99weRsok1XCDccQeehbHA/I1oRvk2NPtr7KGZgT/Y5tPRnAtMqeG2Kg==}
engines: {node: '>=14'}
local-pkg@1.1.1:
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'}
locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
@@ -8059,9 +8040,6 @@ packages:
package-manager-detector@0.2.9:
resolution: {integrity: sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==}
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@@ -8245,9 +8223,6 @@ packages:
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
pkg-types@2.2.0:
resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==}
plist@3.1.0:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'}
@@ -8430,9 +8405,6 @@ packages:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
quansync@0.2.10:
resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -9024,7 +8996,6 @@ packages:
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
@@ -9356,9 +9327,6 @@ packages:
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyexec@1.0.1:
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
tinyglobby@0.2.12:
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
engines: {node: '>=12.0.0'}
@@ -9579,8 +9547,8 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
unconfig@7.3.2:
resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==}
unconfig@7.0.0:
resolution: {integrity: sha512-G5CJSoG6ZTxgzCJblEfgpdRK2tos9+UdD2WtecDUVfImzQ0hFjwpH5RVvGMhP4pRpC9ML7NrC4qBsBl0Ttj35A==}
underscore@1.1.7:
resolution: {integrity: sha512-w4QtCHoLBXw1mjofIDoMyexaEdWGMedWNDhlWTtT1V1lCRqi65Pnoygkh6+WRdr+Bm8ldkBNkNeCsXGMlQS9HQ==}
@@ -9657,12 +9625,12 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
unocss@66.4.2:
resolution: {integrity: sha512-PsZ+4XF/ekiParR7PZEM7AchvHJ78EIfOXlqTPflTOXCYgZ77kG9NaIaIf4lHRevY+rRTyrHrjxdg1Ern2j8qw==}
unocss@66.0.0:
resolution: {integrity: sha512-SHstiv1s7zGPSjzOsADzlwRhQM+6817+OqQE3Fv+N/nn2QLNx1bi3WXybFfz5tWkzBtyTZlwdPmeecsIs1yOCA==}
engines: {node: '>=14'}
peerDependencies:
'@unocss/webpack': 66.4.2
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
'@unocss/webpack': 66.0.0
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
peerDependenciesMeta:
'@unocss/webpack':
optional: true
@@ -10006,8 +9974,10 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-flow-layout@0.2.0:
resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==}
vue-flow-layout@0.1.1:
resolution: {integrity: sha512-JdgRRUVrN0Y2GosA0M68DEbKlXMqJ7FQgsK8CjQD2vxvNSqAU6PZEpi4cfcTVtfM2GVOMjHo7GKKLbXxOBqDqA==}
peerDependencies:
vue: ^3.4.37
vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
@@ -10528,12 +10498,12 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
'@antfu/install-pkg@1.1.0':
'@antfu/install-pkg@1.0.0':
dependencies:
package-manager-detector: 1.3.0
tinyexec: 1.0.1
package-manager-detector: 0.2.9
tinyexec: 0.3.2
'@antfu/utils@9.2.0': {}
'@antfu/utils@8.1.1': {}
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
dependencies:
@@ -10886,7 +10856,7 @@ snapshots:
'@babel/generator@7.27.1':
dependencies:
'@babel/parser': 7.28.0
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
@@ -10985,7 +10955,7 @@ snapshots:
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.28.0
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10995,7 +10965,7 @@ snapshots:
'@babel/core': 7.27.1
'@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.28.0
'@babel/traverse': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -12110,14 +12080,14 @@ snapshots:
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.28.0
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@babel/traverse@7.27.1':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.27.1
'@babel/parser': 7.28.0
'@babel/parser': 7.27.2
'@babel/template': 7.27.2
'@babel/types': 7.27.1
debug: 4.4.1(supports-color@8.1.1)
@@ -12703,7 +12673,7 @@ snapshots:
'@babel/preset-env': 7.27.2(@babel/core@7.27.1)
babel-loader: 9.2.1(@babel/core@7.27.1)(webpack@5.95.0(esbuild@0.25.0))
bluebird: 3.7.1
debug: 4.4.1(supports-color@8.1.1)
debug: 4.4.0
lodash: 4.17.21
webpack: 5.95.0(esbuild@0.25.0)
transitivePeerDependencies:
@@ -13141,15 +13111,15 @@ snapshots:
'@iconify/types@2.0.0': {}
'@iconify/utils@3.0.1':
'@iconify/utils@2.3.0':
dependencies:
'@antfu/install-pkg': 1.1.0
'@antfu/utils': 9.2.0
'@antfu/install-pkg': 1.0.0
'@antfu/utils': 8.1.1
'@iconify/types': 2.0.0
debug: 4.4.1(supports-color@8.1.1)
debug: 4.4.0
globals: 15.15.0
kolorist: 1.8.0
local-pkg: 1.1.1
local-pkg: 1.0.0
mlly: 1.7.4
transitivePeerDependencies:
- supports-color
@@ -13531,10 +13501,6 @@ snapshots:
'@polka/url@1.0.0-next.28': {}
'@quansync/fs@0.1.4':
dependencies:
quansync: 0.2.10
'@react-aria/focus@3.21.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@react-aria/interactions': 3.25.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -13874,7 +13840,7 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.28.0
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@types/babel__generator': 7.6.8
'@types/babel__template': 7.4.4
@@ -13886,7 +13852,7 @@ snapshots:
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.28.0
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@types/babel__traverse@7.20.6':
@@ -14392,157 +14358,150 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@unocss/astro@66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))':
'@unocss/astro@66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@unocss/core': 66.4.2
'@unocss/reset': 66.4.2
'@unocss/vite': 66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))
'@unocss/core': 66.0.0
'@unocss/reset': 66.0.0
'@unocss/vite': 66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))
optionalDependencies:
vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)
transitivePeerDependencies:
- vue
'@unocss/cli@66.4.2':
'@unocss/cli@66.0.0':
dependencies:
'@ampproject/remapping': 2.3.0
'@unocss/config': 66.4.2
'@unocss/core': 66.4.2
'@unocss/preset-uno': 66.4.2
'@unocss/config': 66.0.0
'@unocss/core': 66.0.0
'@unocss/preset-uno': 66.0.0
cac: 6.7.14
chokidar: 3.6.0
colorette: 2.0.20
consola: 3.4.2
consola: 3.4.0
magic-string: 0.30.17
pathe: 2.0.3
perfect-debounce: 1.0.0
tinyglobby: 0.2.14
tinyglobby: 0.2.12
unplugin-utils: 0.2.4
'@unocss/config@66.4.2':
'@unocss/config@66.0.0':
dependencies:
'@unocss/core': 66.4.2
unconfig: 7.3.2
'@unocss/core': 66.0.0
unconfig: 7.0.0
'@unocss/core@66.4.2': {}
'@unocss/core@66.0.0': {}
'@unocss/extractor-arbitrary-variants@66.4.2':
'@unocss/extractor-arbitrary-variants@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/core': 66.0.0
'@unocss/inspector@66.4.2':
'@unocss/inspector@66.0.0(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@unocss/core': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/core': 66.0.0
'@unocss/rule-utils': 66.0.0
colorette: 2.0.20
gzip-size: 6.0.0
sirv: 3.0.1
vue-flow-layout: 0.2.0
vue-flow-layout: 0.1.1(vue@3.5.13(typescript@5.7.3))
transitivePeerDependencies:
- vue
'@unocss/postcss@66.4.2(postcss@8.5.6)':
'@unocss/postcss@66.0.0(postcss@8.5.6)':
dependencies:
'@unocss/config': 66.4.2
'@unocss/core': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/config': 66.0.0
'@unocss/core': 66.0.0
'@unocss/rule-utils': 66.0.0
css-tree: 3.1.0
postcss: 8.5.6
tinyglobby: 0.2.14
tinyglobby: 0.2.12
'@unocss/preset-attributify@66.4.2':
'@unocss/preset-attributify@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/core': 66.0.0
'@unocss/preset-icons@66.4.2':
'@unocss/preset-icons@66.0.0':
dependencies:
'@iconify/utils': 3.0.1
'@unocss/core': 66.4.2
'@iconify/utils': 2.3.0
'@unocss/core': 66.0.0
ofetch: 1.4.1
transitivePeerDependencies:
- supports-color
'@unocss/preset-mini@66.4.2':
'@unocss/preset-mini@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/extractor-arbitrary-variants': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/core': 66.0.0
'@unocss/extractor-arbitrary-variants': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/preset-tagify@66.4.2':
'@unocss/preset-tagify@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/core': 66.0.0
'@unocss/preset-typography@66.4.2':
'@unocss/preset-typography@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/preset-mini': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/core': 66.0.0
'@unocss/preset-mini': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/preset-uno@66.4.2':
'@unocss/preset-uno@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/preset-wind3': 66.4.2
'@unocss/core': 66.0.0
'@unocss/preset-wind3': 66.0.0
'@unocss/preset-web-fonts@66.4.2':
'@unocss/preset-web-fonts@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/core': 66.0.0
ofetch: 1.4.1
'@unocss/preset-wind3@66.4.2':
'@unocss/preset-wind3@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/preset-mini': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/core': 66.0.0
'@unocss/preset-mini': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/preset-wind4@66.4.2':
'@unocss/preset-wind@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/extractor-arbitrary-variants': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/preset-wind@66.4.2':
dependencies:
'@unocss/core': 66.4.2
'@unocss/preset-wind3': 66.4.2
'@unocss/core': 66.0.0
'@unocss/preset-wind3': 66.0.0
'@unocss/reset@66.0.0': {}
'@unocss/reset@66.4.2': {}
'@unocss/rule-utils@66.4.2':
'@unocss/rule-utils@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/core': 66.0.0
magic-string: 0.30.17
'@unocss/transformer-attributify-jsx@66.4.2':
'@unocss/transformer-attributify-jsx@66.0.0':
dependencies:
'@babel/parser': 7.28.0
'@babel/traverse': 7.28.0
'@unocss/core': 66.4.2
transitivePeerDependencies:
- supports-color
'@unocss/core': 66.0.0
'@unocss/transformer-compile-class@66.4.2':
'@unocss/transformer-compile-class@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/core': 66.0.0
'@unocss/transformer-directives@66.4.2':
'@unocss/transformer-directives@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/core': 66.0.0
'@unocss/rule-utils': 66.0.0
css-tree: 3.1.0
'@unocss/transformer-variant-group@66.4.2':
'@unocss/transformer-variant-group@66.0.0':
dependencies:
'@unocss/core': 66.4.2
'@unocss/core': 66.0.0
'@unocss/vite@66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))':
'@unocss/vite@66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@ampproject/remapping': 2.3.0
'@unocss/config': 66.4.2
'@unocss/core': 66.4.2
'@unocss/inspector': 66.4.2
'@unocss/config': 66.0.0
'@unocss/core': 66.0.0
'@unocss/inspector': 66.0.0(vue@3.5.13(typescript@5.7.3))
chokidar: 3.6.0
magic-string: 0.30.17
pathe: 2.0.3
tinyglobby: 0.2.14
tinyglobby: 0.2.12
unplugin-utils: 0.2.4
vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)
transitivePeerDependencies:
- vue
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
optional: true
@@ -15812,11 +15771,9 @@ snapshots:
confbox@0.1.8: {}
confbox@0.2.2: {}
connect-history-api-fallback@2.0.0: {}
consola@3.4.2: {}
consola@3.4.0: {}
console.table@0.10.0:
dependencies:
@@ -17240,8 +17197,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
exsolve@1.0.7: {}
extend@3.0.2: {}
extendable-error@0.1.7: {}
@@ -17447,7 +17402,7 @@ snapshots:
'@actions/core': 1.11.1
arg: 5.0.2
console.table: 0.10.0
debug: 4.4.1(supports-color@8.1.1)
debug: 4.4.0
find-test-names: 1.29.5(@babel/core@7.27.1)
globby: 11.1.0
minimatch: 3.1.2
@@ -18348,7 +18303,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.4.1(supports-color@8.1.1)
debug: 4.4.0
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -18981,12 +18936,6 @@ snapshots:
mlly: 1.7.4
pkg-types: 1.3.1
local-pkg@1.1.1:
dependencies:
mlly: 1.7.4
pkg-types: 2.2.0
quansync: 0.2.10
locate-path@3.0.0:
dependencies:
p-locate: 3.0.0
@@ -19666,7 +19615,7 @@ snapshots:
node-source-walk@7.0.0:
dependencies:
'@babel/parser': 7.28.0
'@babel/parser': 7.27.2
nomnom@1.5.2:
dependencies:
@@ -19924,8 +19873,6 @@ snapshots:
package-manager-detector@0.2.9: {}
package-manager-detector@1.3.0: {}
pako@1.0.11: {}
pako@2.1.0: {}
@@ -20100,12 +20047,6 @@ snapshots:
mlly: 1.7.4
pathe: 2.0.3
pkg-types@2.2.0:
dependencies:
confbox: 0.2.2
exsolve: 1.0.7
pathe: 2.0.3
plist@3.1.0:
dependencies:
'@xmldom/xmldom': 0.8.10
@@ -20287,8 +20228,6 @@ snapshots:
dependencies:
side-channel: 1.1.0
quansync@0.2.10: {}
queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}
@@ -21078,7 +21017,7 @@ snapshots:
spdy@4.0.2:
dependencies:
debug: 4.4.1(supports-color@8.1.1)
debug: 4.4.0
handle-thing: 2.0.1
http-deceiver: 1.2.7
select-hose: 2.0.0
@@ -21458,8 +21397,6 @@ snapshots:
tinyexec@0.3.2: {}
tinyexec@1.0.1: {}
tinyglobby@0.2.12:
dependencies:
fdir: 6.4.3(picomatch@4.0.2)
@@ -21662,12 +21599,11 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
unconfig@7.3.2:
unconfig@7.0.0:
dependencies:
'@quansync/fs': 0.1.4
'@antfu/utils': 8.1.1
defu: 6.1.4
jiti: 2.4.2
quansync: 0.2.10
underscore@1.1.7: {}
@@ -21753,32 +21689,32 @@ snapshots:
universalify@2.0.1: {}
unocss@66.4.2(postcss@8.5.6)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)):
unocss@66.0.0(postcss@8.5.6)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@unocss/astro': 66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))
'@unocss/cli': 66.4.2
'@unocss/core': 66.4.2
'@unocss/postcss': 66.4.2(postcss@8.5.6)
'@unocss/preset-attributify': 66.4.2
'@unocss/preset-icons': 66.4.2
'@unocss/preset-mini': 66.4.2
'@unocss/preset-tagify': 66.4.2
'@unocss/preset-typography': 66.4.2
'@unocss/preset-uno': 66.4.2
'@unocss/preset-web-fonts': 66.4.2
'@unocss/preset-wind': 66.4.2
'@unocss/preset-wind3': 66.4.2
'@unocss/preset-wind4': 66.4.2
'@unocss/transformer-attributify-jsx': 66.4.2
'@unocss/transformer-compile-class': 66.4.2
'@unocss/transformer-directives': 66.4.2
'@unocss/transformer-variant-group': 66.4.2
'@unocss/vite': 66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))
'@unocss/astro': 66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))
'@unocss/cli': 66.0.0
'@unocss/core': 66.0.0
'@unocss/postcss': 66.0.0(postcss@8.5.6)
'@unocss/preset-attributify': 66.0.0
'@unocss/preset-icons': 66.0.0
'@unocss/preset-mini': 66.0.0
'@unocss/preset-tagify': 66.0.0
'@unocss/preset-typography': 66.0.0
'@unocss/preset-uno': 66.0.0
'@unocss/preset-web-fonts': 66.0.0
'@unocss/preset-wind': 66.0.0
'@unocss/preset-wind3': 66.0.0
'@unocss/transformer-attributify-jsx': 66.0.0
'@unocss/transformer-compile-class': 66.0.0
'@unocss/transformer-directives': 66.0.0
'@unocss/transformer-variant-group': 66.0.0
'@unocss/vite': 66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))
optionalDependencies:
vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)
transitivePeerDependencies:
- postcss
- supports-color
- vue
unpipe@1.0.0: {}
@@ -22115,7 +22051,9 @@ snapshots:
vscode-uri@3.1.0: {}
vue-flow-layout@0.2.0: {}
vue-flow-layout@0.1.1(vue@3.5.13(typescript@5.7.3)):
dependencies:
vue: 3.5.13(typescript@5.7.3)
vue@3.5.13(typescript@5.7.3):
dependencies: