Compare commits

..

30 Commits

Author SHA1 Message Date
omkarht
89b29898d2 implemented demo usecase diagram with antlr flow 2025-09-02 14:05:20 +05:30
Shubham P
2972bf25bf Merge pull request #6902 from matt-baker-agd-systems/patch-1
Update Title FAQ based on latest info
2025-09-01 12:32:55 +00:00
Shubham P
6b1a7a9e1a Merge pull request #6905 from mermaid-js/6780-class-diagram-newline-whitespace
6780: treat newline characters as whitespace in class diagrams
2025-09-01 12:20:42 +00:00
darshanr0107
33bc4a0b4e chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-29 19:34:29 +05:30
darshanr0107
c6f25167a2 fix: handle newline characters as whitespace
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-29 19:21:05 +05:30
Sidharth Vinod
82800a2c84 Merge pull request #6903 from mermaid-js/fix-userconfig-from-initialize
test: ensure YAML config takes precedence over initialize config in mermaidAPI
2025-08-29 06:21:44 +00:00
Sidharth Vinod
27e700debd Merge pull request #6846 from RSS1102/chore/upgrade/unocss-iconify
chore: upgrade `unocss@66.4.2` and `@iconify/utils@3.0.1`
2025-08-29 06:19:54 +00:00
darshanr0107
01e47333d5 test: mock SVGElement.getBBox using jsdomit for DOM tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-29 11:31:12 +05:30
阿菜 Cai
d47ba7c2d1 fix: update dependency versions to use caret notation for better compatibility 2025-08-29 04:50:31 +08:00
Sidharth Vinod
b1c4eb3f5c Merge pull request #6901 from mermaid-js/build/fix-applitools-e2e-workflow
build: stop using `cypress/browsers` for Applitools
2025-08-28 21:34:01 +05:30
darshanr0107
310fcd2292 fix: added test cases in mermaidAPI.spec to ensure YAML config takes precedence ovver initialize
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-28 18:24:40 +05:30
阿菜 Cai
04b6fc1280 Merge branch 'develop' into chore/upgrade/unocss-iconify 2025-08-28 17:03:32 +08:00
darshanr0107
f46a151075 Revert "fix: ensure configs from initialize and frontmatter are both handled correctly"
This reverts commit b7e9d02b7c.
2025-08-26 19:50:49 +05:30
darshanr0107
b7e9d02b7c fix: ensure configs from initialize and frontmatter are both handled correctly
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-26 18:10:39 +05:30
autofix-ci[bot]
0ef3130510 [autofix.ci] apply automated fixes 2025-08-26 11:13:39 +00:00
matt-baker-agd-systems
862d40cc3a Update Title FAQ based on latest info 2025-08-26 12:04:26 +01:00
Sidharth Vinod
00f5700320 Merge pull request #6898 from mermaid-js/user-defined-layout-detection
feat: Add getUserDefinedConfig to merge init config and directives
2025-08-26 09:17:40 +00:00
Alois Klink
e32dc8513f build: stop using cypress/browsers for Applitools
I'm running into the following error and I'm hoping this will fix it:
    
    > Your configFile threw an error from: cypress.config.js
    >
    > We stopped running your tests because your config file crashed.
    >
    > Error: connect ECONNREFUSED 127.0.0.1:21077
    >     at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1611:16)

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-26 17:36:43 +09:00
darshanr0107
50127f3ffe fix: review comments related to getUserDefinedConfig tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-26 13:17:38 +05:30
Sidharth Vinod
29bb0e3dca Merge pull request #6900 from mermaid-js/update-timings
Update E2E Timings
2025-08-26 12:46:05 +05:30
Shubham P
1221de4c2d Merge pull request #6896 from mermaid-js/renovate/peter-evans-create-pull-request-digest
chore(deps): update peter-evans/create-pull-request digest to 18e4695
2025-08-26 06:38:36 +00:00
github-actions[bot]
c41e08cb7a chore: update E2E timings 2025-08-26 04:13:51 +00:00
darshanr0107
4760ed8893 fix: add unit tests for getUserDefinedConfig across different scenarios
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-25 19:13:58 +05:30
darshanr0107
31ecf31c2e refactor: remove layout-specific checks and create generic function
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-25 18:26:03 +05:30
renovate[bot]
b52766653c chore(deps): update peter-evans/create-pull-request digest to 18e4695 2025-08-25 11:22:30 +00:00
darshanr0107
8322a63598 feat: add helper to differentiate user-defined layout from default
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-25 16:17:04 +05:30
阿菜 Cai
c0e2d4a23b Merge branch 'develop' into chore/upgrade/unocss-iconify 2025-08-20 14:48:58 +08:00
阿菜 Cai
f7a0844a31 fix(pnpm-lock): update roughjs patch hash to correct value 2025-08-17 19:51:10 +08:00
阿菜 Cai
2817383714 Merge branch 'develop' into chore/upgrade/unocss-iconify 2025-08-17 18:40:40 +08:00
RSS1102
80c6faf4d5 chore: upgrade unocss@66.4.2 and @iconify/utils@3.0.1 2025-08-12 19:36:55 +08:00
38 changed files with 7067 additions and 424 deletions

View File

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

View File

@@ -1,5 +0,0 @@
---
'mermaid': major
---
Currently, HTML tags such as <em>, <strong>, <sup>, <a>, <ul>, and <li> are supported in Flowchart and Class diagram labels but not in Timeline diagrams. This change introduces support for basic HTML formatting in Timeline labels, enabling richer text formatting and better usability for multi-line content like descriptions, footnotes, and styled annotations

View File

@@ -23,9 +23,6 @@ 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@cb4d3bfce175d44325c6b7697f81e0afe8a79bdf
uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a
with:
add-paths: |
cypress/timings.json

View File

@@ -524,5 +524,18 @@ describe('Class diagram', () => {
`,
{}
);
it('should handle an empty class body with empty braces', () => {
imgSnapshotTest(
` classDiagram
class FooBase~T~ {}
class Bar {
+Zip
+Zap()
}
FooBase <|-- Ba
`,
{ flowchart: { defaultRenderer: 'elk' } }
);
});
});
});

View File

@@ -225,24 +225,4 @@ timeline
{}
);
});
it('13: should render markdown htmlLabels', () => {
imgSnapshotTest(
`---
config:
theme: forest
---
timeline
title Timeline of Industrial Revolution
section 17th-20th century
Industry 1.0 : Machinery, Water power, Steam <br>power
Industry 2.0 : Electricity, <strong>Internal combustion engine </strong>, Mass production
Industry 3.0 : Electronics, Computers, Automation
section 21st century
Industry 4.0 : Internet, Robotics, Internet of Things
Industry 5.0 : Artificial intelligence, Big data, 3D printing
`,
{}
);
});
});

View File

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

View File

@@ -6,7 +6,7 @@
# Frequently Asked Questions
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)

View File

@@ -19,6 +19,7 @@
- [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

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

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

View File

@@ -78,3 +78,187 @@ 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,3 +248,17 @@ 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,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
import { treemap } from '../diagrams/treemap/detector.js';
import usecase from '../diagrams/useCase/useCaseDetector.js';
import '../type.d.ts';
let hasLoadedDiagrams = false;
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
xychart,
block,
radar,
treemap
treemap,
usecase
);
};

View File

@@ -1,91 +1,6 @@
import { arc as d3arc, select } from 'd3';
import { createText } from '../../rendering-util/createText.js';
import DOMPurify from 'dompurify';
const MAX_SECTIONS = 12;
/**
* Process HTML content in node descriptions
* @param {object} textElem - The SVG element to append text to
* @param {object} node - The node object containing description and dimensions
* @param {object} conf - Configuration object
* @param {boolean} isVirtual - Whether this is for virtual height calculation
*/
const processHtmlContent = async function (textElem, node, conf, isVirtual = false) {
// Create temporary text to get initial dimensions
const sanitizedHtml = DOMPurify.sanitize(node.descr, { ALLOWED_TAGS: [] });
const tempText = textElem
.append('text')
.text(sanitizedHtml)
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.call(wrap, node.width);
if (!isVirtual) {
tempText.attr('visibility', 'hidden');
}
const bbox = tempText.node().getBBox();
tempText.remove();
// Create the actual HTML content
const textObj = await createText(
textElem,
node.descr,
{
useHtmlLabels: true,
width: node.width,
classes: 'timeline-node-label',
isNode: true,
},
conf
);
if (!isVirtual) {
select(textObj).attr('transform', 'translate(0, 0)');
}
// Process the foreign object
const foreignObject = textElem.select('foreignObject');
if (foreignObject.node()) {
foreignObject.attr('width', `${10 * node.width}px`).attr('height', `${10 * node.width}px`);
const div = foreignObject.select('div');
if (div.node()) {
div
.style('display', 'table-cell')
.style('white-space', 'nowrap')
.style('line-height', '1.5')
.style('max-width', node.width + 'px')
.style('text-align', 'center');
let divBBox = div.node().getBoundingClientRect();
if (divBBox.width === node.width) {
div
.style('display', 'table')
.style('white-space', 'break-spaces')
.style('width', node.width + 'px');
divBBox = div.node().getBoundingClientRect();
}
foreignObject.attr('width', node.width).attr('height', divBBox.height);
if (!isVirtual) {
foreignObject.attr('x', -node.width / 2).attr('y', 3);
div.style('width', node.width + 'px');
}
bbox.height = divBBox.height;
}
}
return bbox;
};
export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect');
rectElem.attr('x', rectData.x);
@@ -494,9 +409,6 @@ const _drawTextCandidateFunc = (function () {
.style('display', 'table-cell')
.style('text-align', 'center')
.style('vertical-align', 'middle')
.style('word-wrap', 'break-word')
.style('overflow-wrap', 'break-word')
.style('white-space', 'normal')
.text(content);
byTspan(content, body, x, y, width, height, textAttrs, conf);
@@ -581,7 +493,7 @@ function wrap(text, width) {
});
}
export const drawNode = async function (elem, node, fullSection, conf) {
export const drawNode = function (elem, node, fullSection, conf) {
const section = (fullSection % MAX_SECTIONS) - 1;
const nodeElem = elem.append('g');
node.section = section;
@@ -594,28 +506,19 @@ export const drawNode = async function (elem, node, fullSection, conf) {
// Create the wrapped text element
const textElem = nodeElem.append('g');
const hasHtml = /<[a-z][\S\s]*>/i.test(node.descr);
if (hasHtml) {
const bbox = await processHtmlContent(textElem, node, conf, false);
node.height = bbox.height + node.padding;
node.height = Math.max(node.height, node.maxHeight);
node.width = node.width + 2 * node.padding;
} else {
const txt = textElem
.append('text')
.text(node.descr)
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.call(wrap, node.width);
const bbox = txt.node().getBBox();
const fontSize = conf.fontSize?.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
node.height = bbox.height + fontSize * 1.1 * 0.5 + node.padding;
node.height = Math.max(node.height, node.maxHeight);
node.width = node.width + 2 * node.padding;
}
const txt = textElem
.append('text')
.text(node.descr)
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.call(wrap, node.width);
const bbox = txt.node().getBBox();
const fontSize = conf.fontSize?.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
node.height = bbox.height + fontSize * 1.1 * 0.5 + node.padding;
node.height = Math.max(node.height, node.maxHeight);
node.width = node.width + 2 * node.padding;
textElem.attr('transform', 'translate(' + node.width / 2 + ', ' + node.padding / 2 + ')');
@@ -625,25 +528,17 @@ export const drawNode = async function (elem, node, fullSection, conf) {
return node;
};
export const getVirtualNodeHeight = async function (elem, node, conf) {
export const getVirtualNodeHeight = function (elem, node, conf) {
const textElem = elem.append('g');
const hasHtml = /<[a-z][\S\s]*>/i.test(node.descr);
let bbox;
if (hasHtml) {
bbox = await processHtmlContent(textElem, node, conf, true);
} else {
const txt = textElem
.append('text')
.text(node.descr)
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.call(wrap, node.width);
bbox = txt.node().getBBox();
}
const txt = textElem
.append('text')
.text(node.descr)
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.call(wrap, node.width);
const bbox = txt.node().getBBox();
const fontSize = conf.fontSize?.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
textElem.remove();
return bbox.height + fontSize * 1.1 * 0.5 + node.padding;

View File

@@ -25,7 +25,7 @@ interface TimelineTask {
score: number;
events: string[];
}
export const draw = async function (text: string, id: string, version: string, diagObj: Diagram) {
export const draw = function (text: string, id: string, version: string, diagObj: Diagram) {
//1. Fetch the configuration
const conf = getConfig();
const LEFT_MARGIN = conf.timeline?.leftMargin ?? 50;
@@ -76,7 +76,7 @@ export const draw = async function (text: string, id: string, version: string, d
let hasSections = true;
//Calculate the max height of the sections
for (const section of sections) {
sections.forEach(function (section: string) {
const sectionNode: Block<string, number> = {
number: sectionNumber,
descr: section,
@@ -85,10 +85,10 @@ export const draw = async function (text: string, id: string, version: string, d
padding: 20,
maxHeight: maxSectionHeight,
};
const sectionHeight = await svgDraw.getVirtualNodeHeight(svg, sectionNode, conf);
const sectionHeight = svgDraw.getVirtualNodeHeight(svg, sectionNode, conf);
log.debug('sectionHeight before draw', sectionHeight);
maxSectionHeight = Math.max(maxSectionHeight, sectionHeight + 20);
}
});
//tasks length and maxEventCount
let maxEventCount = 0;
@@ -106,7 +106,7 @@ export const draw = async function (text: string, id: string, version: string, d
padding: 20,
maxHeight: maxTaskHeight,
};
const taskHeight = await svgDraw.getVirtualNodeHeight(svg, taskNode, conf);
const taskHeight = svgDraw.getVirtualNodeHeight(svg, taskNode, conf);
log.debug('taskHeight before draw', taskHeight);
maxTaskHeight = Math.max(maxTaskHeight, taskHeight + 20);
@@ -123,8 +123,9 @@ export const draw = async function (text: string, id: string, version: string, d
padding: 20,
maxHeight: 50,
};
maxEventLineLengthTemp += await svgDraw.getVirtualNodeHeight(svg, eventNode, conf);
maxEventLineLengthTemp += svgDraw.getVirtualNodeHeight(svg, eventNode, conf);
}
// Add spacing between events (10px per event except the last one)
if (task.events.length > 0) {
maxEventLineLengthTemp += (task.events.length - 1) * 10;
}
@@ -135,7 +136,7 @@ export const draw = async function (text: string, id: string, version: string, d
log.debug('maxTaskHeight before draw', maxTaskHeight);
if (sections && sections.length > 0) {
for (const section of sections) {
sections.forEach((section) => {
//filter task where tasks.section == section
const tasksForSection = tasks.filter((task) => task.section === section);
@@ -149,7 +150,7 @@ export const draw = async function (text: string, id: string, version: string, d
};
log.debug('sectionNode', sectionNode);
const sectionNodeWrapper = svg.append('g');
const node = await svgDraw.drawNode(sectionNodeWrapper, sectionNode, sectionNumber, conf);
const node = svgDraw.drawNode(sectionNodeWrapper, sectionNode, sectionNumber, conf);
log.debug('sectionNode output', node);
sectionNodeWrapper.attr('transform', `translate(${masterX}, ${sectionBeginY})`);
@@ -158,7 +159,7 @@ export const draw = async function (text: string, id: string, version: string, d
//draw tasks for this section
if (tasksForSection.length > 0) {
await drawTasks(
drawTasks(
svg,
tasksForSection,
sectionNumber,
@@ -177,11 +178,11 @@ export const draw = async function (text: string, id: string, version: string, d
masterY = sectionBeginY;
sectionNumber++;
}
});
} else {
//draw tasks
hasSections = false;
await drawTasks(
drawTasks(
svg,
tasks,
sectionNumber,
@@ -235,7 +236,7 @@ export const draw = async function (text: string, id: string, version: string, d
// addSVGAccessibilityFields(diagObj.db, diagram, id);
};
export const drawTasks = async function (
export const drawTasks = function (
diagram: Selection<SVGElement, unknown, null, undefined>,
tasks: TimelineTask[],
sectionColor: number,
@@ -264,7 +265,7 @@ export const drawTasks = async function (
// create task wrapper
const taskWrapper = diagram.append('g').attr('class', 'taskWrapper');
const node = await svgDraw.drawNode(taskWrapper, taskNode, sectionColor, conf);
const node = svgDraw.drawNode(taskWrapper, taskNode, sectionColor, conf);
const taskHeight = node.height;
//log task height
log.debug('taskHeight after draw', taskHeight);
@@ -281,7 +282,7 @@ export const drawTasks = async function (
//add margin to task
masterY += 100;
lineLength =
lineLength + (await drawEvents(diagram, task.events, sectionColor, masterX, masterY, conf));
lineLength + drawEvents(diagram, task.events, sectionColor, masterX, masterY, conf);
masterY -= 100;
lineWrapper
@@ -306,7 +307,7 @@ export const drawTasks = async function (
masterY = masterY - 10;
};
export const drawEvents = async function (
export const drawEvents = function (
diagram: Selection<SVGElement, unknown, null, undefined>,
events: string[],
sectionColor: number,
@@ -333,7 +334,7 @@ export const drawEvents = async function (
log.debug('eventNode', eventNode);
// create event wrapper
const eventWrapper = diagram.append('g').attr('class', 'eventWrapper');
const node = await svgDraw.drawNode(eventWrapper, eventNode, sectionColor, conf);
const node = svgDraw.drawNode(eventWrapper, eventNode, sectionColor, conf);
const eventHeight = node.height;
maxEventHeight = maxEventHeight + eventHeight;
eventWrapper.attr('transform', `translate(${masterX}, ${masterY})`);

View File

@@ -0,0 +1,124 @@
const getStyles = (options) =>
`
.usecase-diagram {
font-family: ${options.fontFamily};
font-size: ${options.fontSize};
}
/* Actor styles */
.usecase-actor-man {
stroke: ${options.actorBorder};
fill: ${options.actorBkg};
stroke-width: 2px;
}
.usecase-actor-man circle {
fill: ${options.useCaseActorBkg};
stroke: ${options.useCaseActorBorder};
stroke-width: 2px;
}
.usecase-actor-man line {
stroke: ${options.useCaseActorBorder};
stroke-width: 2px;
stroke-linecap: round;
}
.usecase-actor-man text {
font-family: ${options.fontFamily};
font-size: 14px;
font-weight: normal;
fill: ${options.useCaseActorTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* Use case styles */
.usecase-usecase {
fill: ${options.useCaseUseCaseBkg};
stroke: ${options.useCaseUseCaseBorder};
stroke-width: 1px;
}
.usecase-usecase text {
font-family: ${options.fontFamily};
font-size: 12px;
fill: ${options.useCaseUseCaseTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* System boundary styles */
.usecase-system-boundary {
fill: ${options.useCaseSystemBoundaryBkg};
stroke: ${options.useCaseSystemBoundaryBorder};
stroke-width: 2px;
stroke-dasharray: 5,5;
}
.usecase-system-boundary text {
font-family: ${options.fontFamily};
font-size: 14px;
font-weight: bold;
fill: ${options.useCaseSystemBoundaryTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* Arrow and relationship styles */
.usecase-arrow {
stroke: ${'red'};
stroke-width: 2px;
fill: none;
}
.usecase-arrow-label {
font-family: ${options.fontFamily};
font-size: 12px;
fill: ${options.useCaseArrowTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* Node styles for standalone nodes */
.usecase-node {
fill: ${options.useCaseUseCaseBkg};
stroke: ${options.useCaseUseCaseBorder};
stroke-width: 1px;
}
.usecase-node text {
font-family: ${options.fontFamily};
font-size: 12px;
fill: ${options.useCaseUseCaseTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* Hover effects */
.usecase-actor-man:hover circle {
fill: ${options.useCaseActorBkg};
stroke: ${options.useCaseArrowColor};
}
.usecase-actor-man:hover line {
stroke: ${options.useCaseArrowColor};
}
.usecase-actor-man:hover text {
fill: ${options.useCaseArrowColor};
font-weight: bold;
}
.usecase-usecase:hover {
fill: ${options.useCaseSystemBoundaryBkg};
stroke: ${options.useCaseArrowColor};
}
.usecase-usecase:hover text {
fill: ${options.useCaseArrowColor};
font-weight: bold;
}
`;
export default getStyles;

View File

@@ -0,0 +1,586 @@
// Simple actor type for useCase diagrams
interface Actor {
type: 'actor';
name: string;
metadata?: Record<string, string>;
}
// Simple use case type
interface UseCase {
type: 'useCase';
name: string;
}
// System boundary type
interface SystemBoundary {
type: 'systemBoundary';
name: string;
useCases: UseCase[];
metadata?: Record<string, string>;
}
// System boundary metadata type
interface SystemBoundaryMetadata {
type: 'systemBoundaryMetadata';
name: string; // boundary name
metadata: Record<string, string>;
}
// Actor-UseCase relationship type
interface ActorUseCaseRelationship {
type: 'actorUseCaseRelationship';
from: string; // actor name
to: string; // use case name
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
// Node type
interface Node {
type: 'node';
id: string; // node ID (e.g., 'a', 'b', 'c')
label: string; // node label (e.g., 'Go through code')
}
// Actor-Node relationship type
interface ActorNodeRelationship {
type: 'actorNodeRelationship';
from: string; // actor name
to: string; // node ID
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
// Inline Actor-Node relationship type
interface InlineActorNodeRelationship {
type: 'inlineActorNodeRelationship';
actor: string; // actor name
node: Node; // node definition
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
export class UseCaseDB {
private actors: Actor[] = [];
private systemBoundaries: SystemBoundary[] = [];
private systemBoundaryMetadata: SystemBoundaryMetadata[] = [];
private useCases: UseCase[] = [];
private relationships: ActorUseCaseRelationship[] = [];
private nodes: Node[] = [];
private nodeRelationships: ActorNodeRelationship[] = [];
private inlineRelationships: InlineActorNodeRelationship[] = [];
constructor() {
this.clear();
}
clear(): void {
this.actors = [];
this.systemBoundaries = [];
this.systemBoundaryMetadata = [];
this.useCases = [];
this.relationships = [];
this.nodes = [];
this.nodeRelationships = [];
this.inlineRelationships = [];
}
addActor(actor: Actor): void {
this.actors.push(actor);
}
addSystemBoundary(boundary: SystemBoundary): void {
this.systemBoundaries.push(boundary);
}
addSystemBoundaryMetadata(metadata: SystemBoundaryMetadata): void {
this.systemBoundaryMetadata.push(metadata);
// Apply metadata to existing system boundary
const boundary = this.systemBoundaries.find(b => b.name === metadata.name);
if (boundary) {
boundary.metadata = metadata.metadata;
}
}
addUseCase(useCase: UseCase): void {
this.useCases.push(useCase);
}
addRelationship(relationship: ActorUseCaseRelationship): void {
this.relationships.push(relationship);
}
addNode(node: Node): void {
this.nodes.push(node);
}
addNodeRelationship(relationship: ActorNodeRelationship): void {
this.nodeRelationships.push(relationship);
}
addInlineRelationship(relationship: InlineActorNodeRelationship): void {
this.inlineRelationships.push(relationship);
// Also add the node and actor separately
this.addNode(relationship.node);
// Add actor if not already exists
const actorExists = this.actors.some(actor => actor.name === relationship.actor);
if (!actorExists) {
this.addActor({
type: 'actor',
name: relationship.actor
});
}
}
getActors(): Actor[] {
return this.actors;
}
getSystemBoundaries(): SystemBoundary[] {
return this.systemBoundaries;
}
getSystemBoundaryMetadata(): SystemBoundaryMetadata[] {
return this.systemBoundaryMetadata;
}
getUseCases(): UseCase[] {
return this.useCases;
}
getRelationships(): ActorUseCaseRelationship[] {
return this.relationships;
}
getNodes(): Node[] {
return this.nodes;
}
getNodeRelationships(): ActorNodeRelationship[] {
return this.nodeRelationships;
}
getInlineRelationships(): InlineActorNodeRelationship[] {
return this.inlineRelationships;
}
parse(text: string): void {
this.clear();
// For now, use the simple parser with enhanced metadata support
// TODO: Integrate ANTLR parser in the future
// Simple parser for usecase diagrams (fallback)
const lines = text.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('%'));
let foundUsecase = false;
let inSystemBoundary = false;
let currentBoundary: SystemBoundary | null = null;
let inMetadataBlock = false;
let currentMetadataName = '';
let currentMetadataContent = '';
for (const line of lines) {
if (line === 'usecase') {
foundUsecase = true;
continue;
}
if (!foundUsecase) {
continue
};
if (line.startsWith('actor ')) {
const actorPart = line.substring(6).trim();
if (actorPart) {
// Check if this is an inline actor-node relationship
if (this.isInlineActorNodeRelationshipLine(actorPart)) {
const relationship = this.parseInlineActorNodeRelationshipLine(actorPart);
if (relationship) {
this.addInlineRelationship(relationship);
}
} else {
const actors = this.parseActorList(actorPart);
actors.forEach((actor: Actor) => this.addActor(actor));
}
}
} else if (line.startsWith('systemBoundary ')) {
const boundaryPart = line.substring(15).trim();
if (boundaryPart.endsWith(' {')) {
// New curly brace syntax: systemBoundary Name {
const boundaryName = boundaryPart.substring(0, boundaryPart.length - 2).trim();
currentBoundary = {
type: 'systemBoundary',
name: boundaryName,
useCases: []
};
inSystemBoundary = true;
} else if (boundaryPart) {
// Old syntax: systemBoundary Name (followed by 'end')
currentBoundary = {
type: 'systemBoundary',
name: boundaryPart,
useCases: []
};
inSystemBoundary = true;
}
} else if (line === 'end' || (line === '}' && !inMetadataBlock)) {
if (inSystemBoundary && currentBoundary) {
this.addSystemBoundary(currentBoundary);
currentBoundary = null;
inSystemBoundary = false;
}
} else if (inSystemBoundary && currentBoundary && line) {
// This is a use case inside the system boundary
const useCase: UseCase = {
type: 'useCase',
name: line
};
currentBoundary.useCases.push(useCase);
} else if (line && !inSystemBoundary) {
// Handle multi-line metadata blocks
if (inMetadataBlock) {
if (line.includes('}')) {
// End of metadata block
currentMetadataContent += line.replace('}', '').trim();
const metadata = this.parseMetadataContent(currentMetadataName, currentMetadataContent);
if (metadata) {
this.addSystemBoundaryMetadata(metadata);
}
inMetadataBlock = false;
currentMetadataName = '';
currentMetadataContent = '';
} else {
// Continue collecting metadata content
currentMetadataContent += line.trim() + ' ';
}
} else if (line.includes('@{')) {
// Start of metadata block
const match = line.match(/^(\w+)@\{(.*)$/);
if (match) {
currentMetadataName = match[1];
const content = match[2].trim();
if (content.includes('}')) {
// Single line metadata
const metadata = this.parseMetadataContent(currentMetadataName, content.replace('}', ''));
if (metadata) {
this.addSystemBoundaryMetadata(metadata);
}
} else {
// Multi-line metadata
inMetadataBlock = true;
currentMetadataContent = content + ' ';
}
}
} else if (this.isRelationshipLine(line)) {
// Check if this is a relationship (actor --> usecase or actor --> node)
const relationship = this.parseRelationshipLine(line);
if (relationship) {
if (relationship.type === 'actorUseCaseRelationship') {
this.addRelationship(relationship);
} else if (relationship.type === 'actorNodeRelationship') {
this.addNodeRelationship(relationship);
}
}
} else {
// This is a standalone use case
const useCase: UseCase = {
type: 'useCase',
name: line
};
this.addUseCase(useCase);
}
}
}
}
private parseActorList(actorPart: string): Actor[] {
// Smart split by comma that respects metadata braces
const actorNames = this.smartSplitActors(actorPart);
return actorNames.map(actorName => this.parseActorWithMetadata(actorName));
}
private smartSplitActors(input: string): string[] {
const actors: string[] = [];
let current = '';
let braceDepth = 0;
let inQuotes = false;
let quoteChar = '';
for (const char of input) {
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
current += char;
} else if (inQuotes && char === quoteChar) {
inQuotes = false;
quoteChar = '';
current += char;
} else if (!inQuotes && char === '{') {
braceDepth++;
current += char;
} else if (!inQuotes && char === '}') {
braceDepth--;
current += char;
} else if (!inQuotes && char === ',' && braceDepth === 0) {
// This is a real separator, not inside metadata
if (current.trim()) {
actors.push(current.trim());
}
current = '';
} else {
current += char;
}
}
// Add the last actor
if (current.trim()) {
actors.push(current.trim());
}
return actors;
}
private parseActorWithMetadata(actorPart: string): Actor {
// Check if there's metadata (contains @{...})
const metadataRegex = /^([^@]+)@{([^}]*)}$/;
const metadataMatch = metadataRegex.exec(actorPart);
if (metadataMatch) {
const name = metadataMatch[1].trim();
const metadataStr = metadataMatch[2].trim();
const metadata = this.parseMetadataString(metadataStr);
return {
type: 'actor',
name,
metadata
};
} else {
// No metadata, just return the name
return {
type: 'actor',
name: actorPart
};
}
}
private parseMetadataString(metadataStr: string): Record<string, string> {
const metadata: Record<string, string> = {};
if (!metadataStr.trim()) {
return metadata;
}
// Split by comma and parse key-value pairs
const pairs = metadataStr.split(',');
for (const pair of pairs) {
const colonIndex = pair.indexOf(':');
if (colonIndex > 0) {
const key = pair.substring(0, colonIndex).trim();
let value = pair.substring(colonIndex + 1).trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
metadata[key] = value;
}
}
return metadata;
}
private isRelationshipLine(line: string): boolean {
return line.includes('-->') || line.includes('->');
}
private parseRelationshipLine(line: string): ActorUseCaseRelationship | ActorNodeRelationship | null {
let arrow = '';
let label: string | undefined;
let parts: string[] = [];
// Check for labeled arrows first (--label--> or --label->)
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
if (labeledArrowMatch) {
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
arrow = labeledArrowMatch[2];
// Extract label from arrow
const labelMatch = arrow.match(/^--(\w+)--?>$/);
if (labelMatch) {
label = labelMatch[1];
}
} else if (line.includes('-->')) {
arrow = '-->';
parts = line.split('-->').map(part => part.trim());
} else if (line.includes('->')) {
arrow = '->';
parts = line.split('->').map(part => part.trim());
}
if (parts.length === 2 && parts[0] && parts[1]) {
// Check if target is a node definition (contains parentheses)
if (this.isNodeDefinitionString(parts[1])) {
const node = this.parseNodeDefinitionString(parts[1]);
if (node) {
this.addNode(node);
return {
type: 'actorNodeRelationship',
from: parts[0],
to: node.id,
arrow,
label
};
}
} else {
return {
type: 'actorUseCaseRelationship',
from: parts[0],
to: parts[1],
arrow,
label
};
}
}
return null;
}
private isInlineActorNodeRelationshipLine(line: string): boolean {
// Check for pattern: ActorName --> nodeId(label) or ActorName --label--> nodeId(label)
const hasArrow = line.includes('-->') || line.includes('->') || !!line.match(/--\w+-->/);
const hasNodeDefinition = line.includes('(') && line.includes(')');
return hasArrow && hasNodeDefinition;
}
private parseInlineActorNodeRelationshipLine(line: string): InlineActorNodeRelationship | null {
let arrow = '';
let label: string | undefined;
let parts: string[] = [];
// Check for labeled arrows first (--label--> or --label->)
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
if (labeledArrowMatch) {
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
arrow = labeledArrowMatch[2];
// Extract label from arrow
const labelMatch = arrow.match(/^--(\w+)--?>$/);
if (labelMatch) {
label = labelMatch[1];
}
} else if (line.includes('-->')) {
arrow = '-->';
parts = line.split('-->').map(part => part.trim());
} else if (line.includes('->')) {
arrow = '->';
parts = line.split('->').map(part => part.trim());
}
if (parts.length === 2 && parts[0] && parts[1]) {
const node = this.parseNodeDefinitionString(parts[1]);
if (node) {
return {
type: 'inlineActorNodeRelationship',
actor: parts[0],
node,
arrow,
label
};
}
}
return null;
}
private isNodeDefinitionString(str: string): boolean {
return str.includes('(') && str.includes(')');
}
private parseNodeDefinitionString(str: string): Node | null {
const match = str.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\((.+)\)$/);
if (match) {
return {
type: 'node',
id: match[1],
label: match[2]
};
}
return null;
}
private isSystemBoundaryMetadataLine(line: string): boolean {
// Check for pattern: boundaryName@{...}
return line.includes('@{') && line.includes('}');
}
private parseSystemBoundaryMetadataLine(line: string): SystemBoundaryMetadata | null {
// Parse pattern: boundaryName@{key: value, key2: value2}
const match = line.match(/^(\w+)@\{(.+)\}$/);
if (!match) {
return null;
}
const name = match[1];
const metadataContent = match[2];
const metadata: Record<string, string> = {};
// Parse key-value pairs
const pairs = metadataContent.split(',').map(pair => pair.trim());
for (const pair of pairs) {
const colonIndex = pair.indexOf(':');
if (colonIndex > 0) {
const key = pair.substring(0, colonIndex).trim();
let value = pair.substring(colonIndex + 1).trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
metadata[key] = value;
}
}
return {
type: 'systemBoundaryMetadata',
name,
metadata
};
}
private parseMetadataContent(name: string, content: string): SystemBoundaryMetadata | null {
const metadata: Record<string, string> = {};
// Parse key-value pairs from content
const pairs = content.split(',').map(pair => pair.trim()).filter(pair => pair);
for (const pair of pairs) {
const colonIndex = pair.indexOf(':');
if (colonIndex > 0) {
const key = pair.substring(0, colonIndex).trim();
let value = pair.substring(colonIndex + 1).trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
metadata[key] = value;
}
}
return {
type: 'systemBoundaryMetadata',
name,
metadata
};
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { UseCaseDB } from './useCaseDb.js';
import styles from './styles.js';
import renderer from './useCaseRenderer.js';
// Shared database instance
let db: UseCaseDB;
// Create a simple parser that integrates with our custom parser
const parser = {
parse: (text: string) => {
// Use the shared database instance
db.parse(text);
},
};
export const diagram: DiagramDefinition = {
parser,
get db() {
if (!db) {
db = new UseCaseDB();
}
return db;
},
renderer,
styles,
init: (cnf) => {
// Initialize configuration if needed
if (!db) {
db = new UseCaseDB();
}
},
};

View File

@@ -0,0 +1,619 @@
import { select } from 'd3';
import type { Diagram } from '../../Diagram.js';
import type { UseCaseDB } from './useCaseDb.js';
import { log } from '../../logger.js';
// Position interfaces
interface NodePosition {
name: string; // node ID (for relationship matching)
label: string; // node label (for display)
x: number;
y: number;
width: number;
height: number;
}
// Constants for actor rendering
const ACTOR_TYPE_WIDTH = 36; // 18 * 2 from sequence diagram
const ACTOR_MAN_FIGURE_CLASS = 'usecase-actor-man';
const ACTOR_SPACING = 120; // Horizontal spacing between actors
const ACTOR_HEIGHT = 80; // Height of actor figure
const MARGIN = 50; // Margin around the diagram
// Simple actor interface for positioning
interface ActorPosition {
name: string;
x: number;
y: number;
width: number;
height: number;
metadata?: Record<string, string>;
}
// System boundary interface for positioning
interface SystemBoundaryPosition {
name: string;
x: number;
y: number;
width: number;
height: number;
useCases: UseCasePosition[];
metadata?: Record<string, string>;
}
// Use case interface for positioning
interface UseCasePosition {
name: string;
x: number;
y: number;
width: number;
height: number;
}
/**
* Draws a stick figure actor similar to sequence diagrams but optimized for useCase
*/
const drawActorTypeActor = (elem: any, actor: ActorPosition, conf: any): number => {
const center = actor.x + actor.width / 2;
const actorY = actor.y;
// Create actor group
const actElem = elem.append('g');
actElem.attr('class', ACTOR_MAN_FIGURE_CLASS);
actElem.attr('name', actor.name);
// Draw stick figure
// Head (circle)
actElem
.append('circle')
.attr('cx', center)
.attr('cy', actorY + 15)
.attr('r', 10);
// Body (torso line)
actElem
.append('line')
.attr('x1', center)
.attr('y1', actorY + 25)
.attr('x2', center)
.attr('y2', actorY + 50)
.style('stroke', 'black');
// Arms (horizontal line)
actElem
.append('line')
.attr('x1', center - ACTOR_TYPE_WIDTH / 2)
.attr('y1', actorY + 35)
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
.style('stroke', 'black')
.attr('y2', actorY + 35);
// Left leg
actElem
.append('line')
.attr('x1', center)
.attr('y1', actorY + 50)
.attr('x2', center - ACTOR_TYPE_WIDTH / 2)
.style('stroke', 'black')
.attr('y2', actorY + 70);
// Right leg
actElem
.append('line')
.attr('x1', center)
.attr('y1', actorY + 50)
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
.attr('y2', actorY + 70)
.style('stroke', 'black');
// Actor name text
const textY = actorY + ACTOR_HEIGHT + 15;
drawActorText(actor.name, actElem, actor.x, textY, actor.width, 20);
return ACTOR_HEIGHT; // Total height including text and metadata
};
/**
* Draws text for actor name - simplified version of sequence diagram text drawing
*/
const drawActorText = (content: string, g: any, x: number, y: number, width: number, height: number): void => {
g.append('text')
.attr('x', x + width / 2)
.attr('y', y + height / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.text(content);
};
/**
* Draws a system boundary box with use cases inside
*/
const drawSystemBoundary = (g: any, boundary: SystemBoundaryPosition, conf: any): void => {
// Determine boundary type from metadata (default to 'rect')
const boundaryType = boundary.metadata?.type || 'rect';
if (boundaryType === 'package') {
// Draw package-style boundary with title box
const titleHeight = 25;
const titleWidth = Math.max(100, boundary.name.length * 8 + 20);
// Draw main boundary rectangle
g.append('rect')
.attr('x', boundary.x)
.attr('y', boundary.y + titleHeight)
.attr('width', boundary.width)
.attr('height', boundary.height - titleHeight)
.attr('class', 'usecase-system-boundary')
.attr('fill', 'none')
.attr('stroke', '#333')
.attr('stroke-width', 2);
// Draw title box
g.append('rect')
.attr('x', boundary.x)
.attr('y', boundary.y)
.attr('width', titleWidth)
.attr('height', titleHeight)
.attr('class', 'usecase-system-boundary')
.attr('fill', 'none')
.attr('stroke', '#333')
.attr('stroke-width', 2);
// Draw title text
g.append('text')
.attr('x', boundary.x + titleWidth / 2)
.attr('y', boundary.y + titleHeight / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.style('font-size', '14px')
.style('font-weight', 'bold')
.style('font-family', 'Arial, sans-serif')
.style('fill', '#333')
.text(boundary.name);
} else {
// Draw rect-style boundary (default)
g.append('rect')
.attr('x', boundary.x)
.attr('y', boundary.y)
.attr('width', boundary.width)
.attr('height', boundary.height)
.attr('fill', 'none')
.attr('stroke', '#333')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '5,5');
// Draw boundary title
g.append('text')
.attr('x', boundary.x + 10)
.attr('y', boundary.y + 20)
.style('font-size', '16px')
.style('font-weight', 'bold')
.style('font-family', 'Arial, sans-serif')
.style('fill', '#333')
.text(boundary.name);
}
// Draw use cases inside the boundary
boundary.useCases.forEach((useCase) => {
// Draw use case oval
g.append('ellipse')
.attr('cx', useCase.x + useCase.width / 2)
.attr('cy', useCase.y + useCase.height / 2)
.attr('rx', useCase.width / 2)
.attr('ry', useCase.height / 2)
.attr('class', 'usecase-usecase')
.attr('fill', 'none')
.attr('stroke', '#333');
// Draw use case text
g.append('text')
.attr('x', useCase.x + useCase.width / 2)
.attr('y', useCase.y + useCase.height / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.text(useCase.name);
});
};
/**
* Draws a standalone node as an oval
*/
const drawNode = (g: any, nodePos: NodePosition): void => {
const nodeGroup = g.append('g').attr('class', `node-${nodePos.name}`);
// Draw oval background
nodeGroup.append('ellipse')
.attr('cx', nodePos.x + nodePos.width / 2)
.attr('cy', nodePos.y + nodePos.height / 2)
.attr('rx', nodePos.width / 2)
.attr('ry', nodePos.height / 2)
.attr('fill', 'none')
.attr('stroke', '#333')
.attr('class', 'usecase-node');
// Add node label
nodeGroup.append('text')
.attr('x', nodePos.x + nodePos.width / 2)
.attr('y', nodePos.y + nodePos.height / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.text(nodePos.label);
};
/**
* Draws an arrow relationship between entities (actor-to-usecase or actor-to-actor)
*/
const drawRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], boundaryPositions: SystemBoundaryPosition[], conf: any): void => {
// Find the source entity (always an actor)
const fromEntity = actorPositions.find(a => a.name === relationship.from);
if (!fromEntity) {
return;
}
// Find the target entity (could be a use case or another actor)
let toEntity: UseCasePosition | ActorPosition | undefined;
let isTargetUseCase = false;
// First check if target is a use case in system boundaries
for (const boundary of boundaryPositions) {
toEntity = boundary.useCases.find(uc => uc.name === relationship.to);
if (toEntity) {
isTargetUseCase = true;
break;
}
}
// If not found in boundaries, check if target is another actor
toEntity ??= actorPositions.find(a => a.name === relationship.to);
if (!toEntity) {
return;
}
// Calculate connection points
const fromCenterX = fromEntity.x + fromEntity.width / 2;
const fromCenterY = fromEntity.y + fromEntity.height / 2;
// For use cases, connect to the edge (left side), for actors connect to center
const toCenterX = isTargetUseCase ? toEntity.x : toEntity.x + toEntity.width / 2;
const toCenterY = isTargetUseCase ? toEntity.y + toEntity.height / 2 : toEntity.y + toEntity.height / 2;
// Draw arrow line
g.append('line')
.attr('x1', fromCenterX)
.attr('y1', fromCenterY)
.attr('x2', toCenterX)
.attr('y2', toCenterY)
.attr('class', 'usecase-arrow')
.attr('stroke', '#333')
.attr('marker-end', 'url(#arrowhead)');
// Add edge label if present
if (relationship.label) {
const midX = (fromCenterX + toCenterX) / 2;
const midY = (fromCenterY + toCenterY) / 2;
g.append('text')
.attr('x', midX)
.attr('y', midY - 5)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('class', 'usecase-arrow-label')
.attr('stroke', '#333')
.attr('font-weight', 200)
.text(relationship.label);
}
// Add arrowhead marker definition if not already added
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
if (defs.select('#arrowhead').empty()) {
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 3)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,0 L0,6 L9,3 z')
.attr('fill', '#333');
}
};
/**
* Draws an arrow relationship between an actor and a standalone node
*/
const drawNodeRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
// Find the actor position
const actor = actorPositions.find(a => a.name === relationship.from);
if (!actor) {return};
// Find the node position
const node = nodePositions.find(n => n.name === relationship.to);
if (!node) {return};
// Calculate connection points
const actorCenterX = actor.x + actor.width / 2;
const actorCenterY = actor.y + actor.height / 2;
// For nodes (which are like use cases), connect to the edge (left side)
const nodeCenterX = node.x;
const nodeCenterY = node.y + node.height / 2;
// Draw arrow line
g.append('line')
.attr('x1', actorCenterX)
.attr('y1', actorCenterY)
.attr('x2', nodeCenterX)
.attr('y2', nodeCenterY)
.attr('stroke', '#333')
.attr('stroke-width', 2)
.attr('marker-end', 'url(#arrowhead)');
// Add edge label if present
if (relationship.label) {
const midX = (actorCenterX + nodeCenterX) / 2;
const midY = (actorCenterY + nodeCenterY) / 2;
g.append('text')
.attr('x', midX)
.attr('y', midY - 5)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', '12px')
.attr('font-family', 'Arial, sans-serif')
.attr('fill', '#333')
.text(relationship.label);
}
// Add arrowhead marker definition if not already added
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
if (defs.select('#arrowhead').empty()) {
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 3)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,0 L0,6 L9,3 z')
.attr('fill', '#333');
}
};
/**
* Draws an arrow relationship from an inline actor-node definition
*/
const drawInlineRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
// Find the actor position
const actor = actorPositions.find(a => a.name === relationship.actor);
if (!actor) {return};
// Find the node position by node ID
const node = nodePositions.find(n => n.name === relationship.node.id);
if (!node) {return};
// Calculate connection points
const actorCenterX = actor.x + actor.width / 2;
const actorCenterY = actor.y + actor.height / 2;
// For nodes (which are like use cases), connect to the edge (left side)
const nodeCenterX = node.x;
const nodeCenterY = node.y + node.height / 2;
// Draw arrow line
g.append('line')
.attr('x1', actorCenterX)
.attr('y1', actorCenterY)
.attr('x2', nodeCenterX)
.attr('y2', nodeCenterY)
.attr('stroke', '#333')
.attr('stroke-width', 1)
.attr('marker-end', 'url(#arrowhead)');
// Add edge label if present
if (relationship.label) {
const midX = (actorCenterX + nodeCenterX) / 2;
const midY = (actorCenterY + nodeCenterY) / 2;
g.append('text')
.attr('x', midX)
.attr('y', midY - 5)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', '12px')
.attr('font-family', 'Arial, sans-serif')
.attr('fill', '#333')
.text(relationship.label);
}
// Add arrowhead marker definition if not already added
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
if (defs.select('#arrowhead').empty()) {
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 3)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,0 L0,6 L9,3 z')
.attr('fill', '#333');
}
};
/**
* Main draw function for useCase diagrams
*/
const draw = (text: string, id: string, version: string, diagram: Diagram): void => {
const db = diagram.db as UseCaseDB;
log.debug('Drawing useCase diagram', id);
const actors = db.getActors();
const systemBoundaries = db.getSystemBoundaries();
const useCases = db.getUseCases();
const relationships = db.getRelationships();
const nodes = db.getNodes();
const nodeRelationships = db.getNodeRelationships();
const inlineRelationships = db.getInlineRelationships();
// Create SVG container - use the same approach as other diagrams
const svg = select(`[id="${id}"]`);
svg.selectAll('*').remove();
if (actors.length === 0 && systemBoundaries.length === 0 && useCases.length === 0 && relationships.length === 0 && nodes.length === 0 && nodeRelationships.length === 0 && inlineRelationships.length === 0) {
// Empty diagram
svg.attr('width', 200);
svg.attr('height', 100);
return;
}
// Calculate layout
let currentX = MARGIN;
let currentY = MARGIN;
let maxHeight = 0;
// Position actors
const actorPositions: ActorPosition[] = actors.map((actor, index) => ({
name: actor.name,
x: currentX + index * ACTOR_SPACING,
y: currentY,
width: ACTOR_TYPE_WIDTH + 20, // Extra width for text
height: ACTOR_HEIGHT,
metadata: actor.metadata
}));
if (actors.length > 0) {
currentX += actors.length * ACTOR_SPACING;
maxHeight = Math.max(maxHeight, ACTOR_HEIGHT + 50);
}
// Position system boundaries
const boundaryPositions: SystemBoundaryPosition[] = systemBoundaries.map((boundary, index) => {
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
const boundaryHeight = 150;
const position: SystemBoundaryPosition = {
name: boundary.name,
x: currentX + index * (boundaryWidth + 50),
y: currentY,
width: boundaryWidth,
height: boundaryHeight,
metadata: boundary.metadata,
useCases: boundary.useCases.map((useCase, ucIndex) => ({
name: useCase.name,
x: currentX + index * (boundaryWidth + 50) + 20 + ucIndex * 100,
y: currentY + 40,
width: 80,
height: 40
}))
};
return position;
});
if (systemBoundaries.length > 0) {
const totalBoundaryWidth = systemBoundaries.reduce((sum, boundary, index) => {
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
return sum + boundaryWidth + (index > 0 ? 50 : 0);
}, 0);
currentX += totalBoundaryWidth;
maxHeight = Math.max(maxHeight, 150);
}
// Position standalone nodes
const nodePositions: NodePosition[] = [];
if (nodes.length > 0) {
currentX += 50; // Add some spacing
nodes.forEach((node, index) => {
const nodeWidth = Math.max(100, node.label.length * 8);
const nodeHeight = 40;
nodePositions.push({
name: node.id,
label: node.label,
x: currentX,
y: MARGIN + 50,
width: nodeWidth,
height: nodeHeight
});
currentX += nodeWidth + 50;
});
maxHeight = Math.max(maxHeight, 90);
}
// Create main group
const g = svg.append('g').attr('class', 'usecase-diagram');
// Default configuration
const conf = {
actorFontSize: '14px',
actorFontFamily: 'Arial, sans-serif',
actorFontWeight: 'normal'
};
// Draw all actors
actorPositions.forEach((actorPos) => {
const height = drawActorTypeActor(g, actorPos, conf);
maxHeight = Math.max(maxHeight, height);
});
// Draw system boundaries
boundaryPositions.forEach((boundaryPos) => {
drawSystemBoundary(g, boundaryPos, conf);
});
// Draw standalone nodes
nodePositions.forEach((nodePos) => {
drawNode(g, nodePos);
});
// Draw relationships (arrows)
relationships.forEach((relationship) => {
drawRelationship(g, relationship, actorPositions, boundaryPositions, conf);
});
// Draw node relationships (arrows to standalone nodes)
nodeRelationships.forEach((relationship) => {
drawNodeRelationship(g, relationship, actorPositions, nodePositions, conf);
});
// Draw inline relationships (from inline actor-node definitions)
inlineRelationships.forEach((relationship) => {
drawInlineRelationship(g, relationship, actorPositions, nodePositions, conf);
});
// Calculate total dimensions
let totalWidth = MARGIN;
if (actors.length > 0) {
totalWidth = Math.max(totalWidth, actorPositions[actorPositions.length - 1].x + actorPositions[actorPositions.length - 1].width + MARGIN);
}
if (systemBoundaries.length > 0) {
totalWidth = Math.max(totalWidth, boundaryPositions[boundaryPositions.length - 1].x + boundaryPositions[boundaryPositions.length - 1].width + MARGIN);
}
if (nodePositions.length > 0) {
totalWidth = Math.max(totalWidth, nodePositions[nodePositions.length - 1].x + nodePositions[nodePositions.length - 1].width + MARGIN);
}
const totalHeight = MARGIN + maxHeight + MARGIN;
// Set SVG dimensions
svg.attr('width', totalWidth);
svg.attr('height', totalHeight);
svg.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
};
export default {
draw,
};

View File

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

View File

@@ -41,7 +41,6 @@ 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';
/**
@@ -50,7 +49,6 @@ import { JSDOM } from 'jsdom';
*/
// -------------------------------------------------------------------------------------
describe('mermaidAPI', () => {
describe('encodeEntities', () => {
it('removes the ending ; from style [text1]:[optional word]#[text2]; with ', () => {
@@ -913,4 +911,241 @@ 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

@@ -19,7 +19,9 @@
"scripts": {
"clean": "rimraf dist src/language/generated",
"langium:generate": "langium generate",
"langium:watch": "langium generate --watch"
"langium:watch": "langium generate --watch",
"antlr:generate": "antlr4ts -visitor -listener -o src/language/useCase/generated src/language/useCase/Usecase.g4",
"generate": "npm run langium:generate && npm run antlr:generate"
},
"repository": {
"type": "git",
@@ -33,6 +35,8 @@
"ast"
],
"dependencies": {
"antlr4ts": "0.5.0-alpha.4",
"antlr4ts-cli": "0.5.0-alpha.4",
"langium": "3.3.1"
},
"devDependencies": {

View File

@@ -45,3 +45,4 @@ export * from './pie/index.js';
export * from './architecture/index.js';
export * from './radar/index.js';
export * from './treemap/index.js';
export * from './useCase/index.js';

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
USECASE_START=1
ACTOR=2
SYSTEM_BOUNDARY=3
END=4
ARROW=5
LABELED_ARROW=6
AT=7
LBRACE=8
RBRACE=9
LPAREN=10
RPAREN=11
COMMA=12
COLON=13
STRING=14
IDENTIFIER=15
NEWLINE=16
WS=17
COMMENT=18
'usecase'=1
'actor'=2
'systemBoundary'=3
'end'=4
'@'=7
'{'=8
'}'=9
'('=10
')'=11
','=12
':'=13

View File

@@ -0,0 +1,71 @@
token literal names:
null
'usecase'
'actor'
'systemBoundary'
'end'
null
null
'@'
'{'
'}'
'('
')'
','
':'
null
null
null
null
null
token symbolic names:
null
USECASE_START
ACTOR
SYSTEM_BOUNDARY
END
ARROW
LABELED_ARROW
AT
LBRACE
RBRACE
LPAREN
RPAREN
COMMA
COLON
STRING
IDENTIFIER
NEWLINE
WS
COMMENT
rule names:
USECASE_START
ACTOR
SYSTEM_BOUNDARY
END
ARROW
LABELED_ARROW
AT
LBRACE
RBRACE
LPAREN
RPAREN
COMMA
COLON
STRING
IDENTIFIER
NEWLINE
WS
COMMENT
channel names:
DEFAULT_TOKEN_CHANNEL
HIDDEN
mode names:
DEFAULT_MODE
atn:
[4, 0, 18, 154, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 76, 8, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 93, 8, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 111, 8, 13, 10, 13, 12, 13, 114, 9, 13, 1, 13, 1, 13, 1, 13, 5, 13, 119, 8, 13, 10, 13, 12, 13, 122, 9, 13, 1, 13, 3, 13, 125, 8, 13, 1, 14, 1, 14, 5, 14, 129, 8, 14, 10, 14, 12, 14, 132, 9, 14, 1, 15, 4, 15, 135, 8, 15, 11, 15, 12, 15, 136, 1, 16, 4, 16, 140, 8, 16, 11, 16, 12, 16, 141, 1, 16, 1, 16, 1, 17, 1, 17, 5, 17, 148, 8, 17, 10, 17, 12, 17, 151, 9, 17, 1, 17, 1, 17, 0, 0, 18, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 1, 0, 6, 3, 0, 10, 10, 13, 13, 34, 34, 3, 0, 10, 10, 13, 13, 39, 39, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 10, 10, 13, 13, 2, 0, 9, 9, 32, 32, 162, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 1, 37, 1, 0, 0, 0, 3, 45, 1, 0, 0, 0, 5, 51, 1, 0, 0, 0, 7, 66, 1, 0, 0, 0, 9, 75, 1, 0, 0, 0, 11, 92, 1, 0, 0, 0, 13, 94, 1, 0, 0, 0, 15, 96, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0, 19, 100, 1, 0, 0, 0, 21, 102, 1, 0, 0, 0, 23, 104, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 124, 1, 0, 0, 0, 29, 126, 1, 0, 0, 0, 31, 134, 1, 0, 0, 0, 33, 139, 1, 0, 0, 0, 35, 145, 1, 0, 0, 0, 37, 38, 5, 117, 0, 0, 38, 39, 5, 115, 0, 0, 39, 40, 5, 101, 0, 0, 40, 41, 5, 99, 0, 0, 41, 42, 5, 97, 0, 0, 42, 43, 5, 115, 0, 0, 43, 44, 5, 101, 0, 0, 44, 2, 1, 0, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 99, 0, 0, 47, 48, 5, 116, 0, 0, 48, 49, 5, 111, 0, 0, 49, 50, 5, 114, 0, 0, 50, 4, 1, 0, 0, 0, 51, 52, 5, 115, 0, 0, 52, 53, 5, 121, 0, 0, 53, 54, 5, 115, 0, 0, 54, 55, 5, 116, 0, 0, 55, 56, 5, 101, 0, 0, 56, 57, 5, 109, 0, 0, 57, 58, 5, 66, 0, 0, 58, 59, 5, 111, 0, 0, 59, 60, 5, 117, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 100, 0, 0, 62, 63, 5, 97, 0, 0, 63, 64, 5, 114, 0, 0, 64, 65, 5, 121, 0, 0, 65, 6, 1, 0, 0, 0, 66, 67, 5, 101, 0, 0, 67, 68, 5, 110, 0, 0, 68, 69, 5, 100, 0, 0, 69, 8, 1, 0, 0, 0, 70, 71, 5, 45, 0, 0, 71, 72, 5, 45, 0, 0, 72, 76, 5, 62, 0, 0, 73, 74, 5, 45, 0, 0, 74, 76, 5, 62, 0, 0, 75, 70, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 10, 1, 0, 0, 0, 77, 78, 5, 45, 0, 0, 78, 79, 5, 45, 0, 0, 79, 80, 1, 0, 0, 0, 80, 81, 3, 29, 14, 0, 81, 82, 5, 45, 0, 0, 82, 83, 5, 45, 0, 0, 83, 84, 5, 62, 0, 0, 84, 93, 1, 0, 0, 0, 85, 86, 5, 45, 0, 0, 86, 87, 5, 45, 0, 0, 87, 88, 1, 0, 0, 0, 88, 89, 3, 29, 14, 0, 89, 90, 5, 45, 0, 0, 90, 91, 5, 62, 0, 0, 91, 93, 1, 0, 0, 0, 92, 77, 1, 0, 0, 0, 92, 85, 1, 0, 0, 0, 93, 12, 1, 0, 0, 0, 94, 95, 5, 64, 0, 0, 95, 14, 1, 0, 0, 0, 96, 97, 5, 123, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 125, 0, 0, 99, 18, 1, 0, 0, 0, 100, 101, 5, 40, 0, 0, 101, 20, 1, 0, 0, 0, 102, 103, 5, 41, 0, 0, 103, 22, 1, 0, 0, 0, 104, 105, 5, 44, 0, 0, 105, 24, 1, 0, 0, 0, 106, 107, 5, 58, 0, 0, 107, 26, 1, 0, 0, 0, 108, 112, 5, 34, 0, 0, 109, 111, 8, 0, 0, 0, 110, 109, 1, 0, 0, 0, 111, 114, 1, 0, 0, 0, 112, 110, 1, 0, 0, 0, 112, 113, 1, 0, 0, 0, 113, 115, 1, 0, 0, 0, 114, 112, 1, 0, 0, 0, 115, 125, 5, 34, 0, 0, 116, 120, 5, 39, 0, 0, 117, 119, 8, 1, 0, 0, 118, 117, 1, 0, 0, 0, 119, 122, 1, 0, 0, 0, 120, 118, 1, 0, 0, 0, 120, 121, 1, 0, 0, 0, 121, 123, 1, 0, 0, 0, 122, 120, 1, 0, 0, 0, 123, 125, 5, 39, 0, 0, 124, 108, 1, 0, 0, 0, 124, 116, 1, 0, 0, 0, 125, 28, 1, 0, 0, 0, 126, 130, 7, 2, 0, 0, 127, 129, 7, 3, 0, 0, 128, 127, 1, 0, 0, 0, 129, 132, 1, 0, 0, 0, 130, 128, 1, 0, 0, 0, 130, 131, 1, 0, 0, 0, 131, 30, 1, 0, 0, 0, 132, 130, 1, 0, 0, 0, 133, 135, 7, 4, 0, 0, 134, 133, 1, 0, 0, 0, 135, 136, 1, 0, 0, 0, 136, 134, 1, 0, 0, 0, 136, 137, 1, 0, 0, 0, 137, 32, 1, 0, 0, 0, 138, 140, 7, 5, 0, 0, 139, 138, 1, 0, 0, 0, 140, 141, 1, 0, 0, 0, 141, 139, 1, 0, 0, 0, 141, 142, 1, 0, 0, 0, 142, 143, 1, 0, 0, 0, 143, 144, 6, 16, 0, 0, 144, 34, 1, 0, 0, 0, 145, 149, 5, 37, 0, 0, 146, 148, 8, 4, 0, 0, 147, 146, 1, 0, 0, 0, 148, 151, 1, 0, 0, 0, 149, 147, 1, 0, 0, 0, 149, 150, 1, 0, 0, 0, 150, 152, 1, 0, 0, 0, 151, 149, 1, 0, 0, 0, 152, 153, 6, 17, 0, 0, 153, 36, 1, 0, 0, 0, 10, 0, 75, 92, 112, 120, 124, 130, 136, 141, 149, 1, 6, 0, 0]

View File

@@ -0,0 +1,213 @@
// Generated from /home/omkar-kadam/Public/mermaid/mermaid/packages/parser/src/language/useCase/Usecase.g4 by ANTLR 4.13.1
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.atn.*;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.misc.*;
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"})
public class UsecaseLexer extends Lexer {
static { RuntimeMetaData.checkVersion("4.13.1", RuntimeMetaData.VERSION); }
protected static final DFA[] _decisionToDFA;
protected static final PredictionContextCache _sharedContextCache =
new PredictionContextCache();
public static final int
USECASE_START=1, ACTOR=2, SYSTEM_BOUNDARY=3, END=4, ARROW=5, LABELED_ARROW=6,
AT=7, LBRACE=8, RBRACE=9, LPAREN=10, RPAREN=11, COMMA=12, COLON=13, STRING=14,
IDENTIFIER=15, NEWLINE=16, WS=17, COMMENT=18;
public static String[] channelNames = {
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
};
public static String[] modeNames = {
"DEFAULT_MODE"
};
private static String[] makeRuleNames() {
return new String[] {
"USECASE_START", "ACTOR", "SYSTEM_BOUNDARY", "END", "ARROW", "LABELED_ARROW",
"AT", "LBRACE", "RBRACE", "LPAREN", "RPAREN", "COMMA", "COLON", "STRING",
"IDENTIFIER", "NEWLINE", "WS", "COMMENT"
};
}
public static final String[] ruleNames = makeRuleNames();
private static String[] makeLiteralNames() {
return new String[] {
null, "'usecase'", "'actor'", "'systemBoundary'", "'end'", null, null,
"'@'", "'{'", "'}'", "'('", "')'", "','", "':'"
};
}
private static final String[] _LITERAL_NAMES = makeLiteralNames();
private static String[] makeSymbolicNames() {
return new String[] {
null, "USECASE_START", "ACTOR", "SYSTEM_BOUNDARY", "END", "ARROW", "LABELED_ARROW",
"AT", "LBRACE", "RBRACE", "LPAREN", "RPAREN", "COMMA", "COLON", "STRING",
"IDENTIFIER", "NEWLINE", "WS", "COMMENT"
};
}
private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames();
public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
/**
* @deprecated Use {@link #VOCABULARY} instead.
*/
@Deprecated
public static final String[] tokenNames;
static {
tokenNames = new String[_SYMBOLIC_NAMES.length];
for (int i = 0; i < tokenNames.length; i++) {
tokenNames[i] = VOCABULARY.getLiteralName(i);
if (tokenNames[i] == null) {
tokenNames[i] = VOCABULARY.getSymbolicName(i);
}
if (tokenNames[i] == null) {
tokenNames[i] = "<INVALID>";
}
}
}
@Override
@Deprecated
public String[] getTokenNames() {
return tokenNames;
}
@Override
public Vocabulary getVocabulary() {
return VOCABULARY;
}
public UsecaseLexer(CharStream input) {
super(input);
_interp = new LexerATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
}
@Override
public String getGrammarFileName() { return "Usecase.g4"; }
@Override
public String[] getRuleNames() { return ruleNames; }
@Override
public String getSerializedATN() { return _serializedATN; }
@Override
public String[] getChannelNames() { return channelNames; }
@Override
public String[] getModeNames() { return modeNames; }
@Override
public ATN getATN() { return _ATN; }
public static final String _serializedATN =
"\u0004\u0000\u0012\u009a\u0006\uffff\uffff\u0002\u0000\u0007\u0000\u0002"+
"\u0001\u0007\u0001\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002"+
"\u0004\u0007\u0004\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002"+
"\u0007\u0007\u0007\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002"+
"\u000b\u0007\u000b\u0002\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e"+
"\u0002\u000f\u0007\u000f\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011"+
"\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000"+
"\u0001\u0000\u0001\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"+
"\u0001\u0001\u0001\u0001\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
"\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
"\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0003"+
"\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0004\u0001\u0004\u0001\u0004"+
"\u0001\u0004\u0001\u0004\u0003\u0004L\b\u0004\u0001\u0005\u0001\u0005"+
"\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
"\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
"\u0001\u0005\u0003\u0005]\b\u0005\u0001\u0006\u0001\u0006\u0001\u0007"+
"\u0001\u0007\u0001\b\u0001\b\u0001\t\u0001\t\u0001\n\u0001\n\u0001\u000b"+
"\u0001\u000b\u0001\f\u0001\f\u0001\r\u0001\r\u0005\ro\b\r\n\r\f\rr\t\r"+
"\u0001\r\u0001\r\u0001\r\u0005\rw\b\r\n\r\f\rz\t\r\u0001\r\u0003\r}\b"+
"\r\u0001\u000e\u0001\u000e\u0005\u000e\u0081\b\u000e\n\u000e\f\u000e\u0084"+
"\t\u000e\u0001\u000f\u0004\u000f\u0087\b\u000f\u000b\u000f\f\u000f\u0088"+
"\u0001\u0010\u0004\u0010\u008c\b\u0010\u000b\u0010\f\u0010\u008d\u0001"+
"\u0010\u0001\u0010\u0001\u0011\u0001\u0011\u0005\u0011\u0094\b\u0011\n"+
"\u0011\f\u0011\u0097\t\u0011\u0001\u0011\u0001\u0011\u0000\u0000\u0012"+
"\u0001\u0001\u0003\u0002\u0005\u0003\u0007\u0004\t\u0005\u000b\u0006\r"+
"\u0007\u000f\b\u0011\t\u0013\n\u0015\u000b\u0017\f\u0019\r\u001b\u000e"+
"\u001d\u000f\u001f\u0010!\u0011#\u0012\u0001\u0000\u0006\u0003\u0000\n"+
"\n\r\r\"\"\u0003\u0000\n\n\r\r\'\'\u0003\u0000AZ__az\u0004\u000009AZ_"+
"_az\u0002\u0000\n\n\r\r\u0002\u0000\t\t \u00a2\u0000\u0001\u0001\u0000"+
"\u0000\u0000\u0000\u0003\u0001\u0000\u0000\u0000\u0000\u0005\u0001\u0000"+
"\u0000\u0000\u0000\u0007\u0001\u0000\u0000\u0000\u0000\t\u0001\u0000\u0000"+
"\u0000\u0000\u000b\u0001\u0000\u0000\u0000\u0000\r\u0001\u0000\u0000\u0000"+
"\u0000\u000f\u0001\u0000\u0000\u0000\u0000\u0011\u0001\u0000\u0000\u0000"+
"\u0000\u0013\u0001\u0000\u0000\u0000\u0000\u0015\u0001\u0000\u0000\u0000"+
"\u0000\u0017\u0001\u0000\u0000\u0000\u0000\u0019\u0001\u0000\u0000\u0000"+
"\u0000\u001b\u0001\u0000\u0000\u0000\u0000\u001d\u0001\u0000\u0000\u0000"+
"\u0000\u001f\u0001\u0000\u0000\u0000\u0000!\u0001\u0000\u0000\u0000\u0000"+
"#\u0001\u0000\u0000\u0000\u0001%\u0001\u0000\u0000\u0000\u0003-\u0001"+
"\u0000\u0000\u0000\u00053\u0001\u0000\u0000\u0000\u0007B\u0001\u0000\u0000"+
"\u0000\tK\u0001\u0000\u0000\u0000\u000b\\\u0001\u0000\u0000\u0000\r^\u0001"+
"\u0000\u0000\u0000\u000f`\u0001\u0000\u0000\u0000\u0011b\u0001\u0000\u0000"+
"\u0000\u0013d\u0001\u0000\u0000\u0000\u0015f\u0001\u0000\u0000\u0000\u0017"+
"h\u0001\u0000\u0000\u0000\u0019j\u0001\u0000\u0000\u0000\u001b|\u0001"+
"\u0000\u0000\u0000\u001d~\u0001\u0000\u0000\u0000\u001f\u0086\u0001\u0000"+
"\u0000\u0000!\u008b\u0001\u0000\u0000\u0000#\u0091\u0001\u0000\u0000\u0000"+
"%&\u0005u\u0000\u0000&\'\u0005s\u0000\u0000\'(\u0005e\u0000\u0000()\u0005"+
"c\u0000\u0000)*\u0005a\u0000\u0000*+\u0005s\u0000\u0000+,\u0005e\u0000"+
"\u0000,\u0002\u0001\u0000\u0000\u0000-.\u0005a\u0000\u0000./\u0005c\u0000"+
"\u0000/0\u0005t\u0000\u000001\u0005o\u0000\u000012\u0005r\u0000\u0000"+
"2\u0004\u0001\u0000\u0000\u000034\u0005s\u0000\u000045\u0005y\u0000\u0000"+
"56\u0005s\u0000\u000067\u0005t\u0000\u000078\u0005e\u0000\u000089\u0005"+
"m\u0000\u00009:\u0005B\u0000\u0000:;\u0005o\u0000\u0000;<\u0005u\u0000"+
"\u0000<=\u0005n\u0000\u0000=>\u0005d\u0000\u0000>?\u0005a\u0000\u0000"+
"?@\u0005r\u0000\u0000@A\u0005y\u0000\u0000A\u0006\u0001\u0000\u0000\u0000"+
"BC\u0005e\u0000\u0000CD\u0005n\u0000\u0000DE\u0005d\u0000\u0000E\b\u0001"+
"\u0000\u0000\u0000FG\u0005-\u0000\u0000GH\u0005-\u0000\u0000HL\u0005>"+
"\u0000\u0000IJ\u0005-\u0000\u0000JL\u0005>\u0000\u0000KF\u0001\u0000\u0000"+
"\u0000KI\u0001\u0000\u0000\u0000L\n\u0001\u0000\u0000\u0000MN\u0005-\u0000"+
"\u0000NO\u0005-\u0000\u0000OP\u0001\u0000\u0000\u0000PQ\u0003\u001d\u000e"+
"\u0000QR\u0005-\u0000\u0000RS\u0005-\u0000\u0000ST\u0005>\u0000\u0000"+
"T]\u0001\u0000\u0000\u0000UV\u0005-\u0000\u0000VW\u0005-\u0000\u0000W"+
"X\u0001\u0000\u0000\u0000XY\u0003\u001d\u000e\u0000YZ\u0005-\u0000\u0000"+
"Z[\u0005>\u0000\u0000[]\u0001\u0000\u0000\u0000\\M\u0001\u0000\u0000\u0000"+
"\\U\u0001\u0000\u0000\u0000]\f\u0001\u0000\u0000\u0000^_\u0005@\u0000"+
"\u0000_\u000e\u0001\u0000\u0000\u0000`a\u0005{\u0000\u0000a\u0010\u0001"+
"\u0000\u0000\u0000bc\u0005}\u0000\u0000c\u0012\u0001\u0000\u0000\u0000"+
"de\u0005(\u0000\u0000e\u0014\u0001\u0000\u0000\u0000fg\u0005)\u0000\u0000"+
"g\u0016\u0001\u0000\u0000\u0000hi\u0005,\u0000\u0000i\u0018\u0001\u0000"+
"\u0000\u0000jk\u0005:\u0000\u0000k\u001a\u0001\u0000\u0000\u0000lp\u0005"+
"\"\u0000\u0000mo\b\u0000\u0000\u0000nm\u0001\u0000\u0000\u0000or\u0001"+
"\u0000\u0000\u0000pn\u0001\u0000\u0000\u0000pq\u0001\u0000\u0000\u0000"+
"qs\u0001\u0000\u0000\u0000rp\u0001\u0000\u0000\u0000s}\u0005\"\u0000\u0000"+
"tx\u0005\'\u0000\u0000uw\b\u0001\u0000\u0000vu\u0001\u0000\u0000\u0000"+
"wz\u0001\u0000\u0000\u0000xv\u0001\u0000\u0000\u0000xy\u0001\u0000\u0000"+
"\u0000y{\u0001\u0000\u0000\u0000zx\u0001\u0000\u0000\u0000{}\u0005\'\u0000"+
"\u0000|l\u0001\u0000\u0000\u0000|t\u0001\u0000\u0000\u0000}\u001c\u0001"+
"\u0000\u0000\u0000~\u0082\u0007\u0002\u0000\u0000\u007f\u0081\u0007\u0003"+
"\u0000\u0000\u0080\u007f\u0001\u0000\u0000\u0000\u0081\u0084\u0001\u0000"+
"\u0000\u0000\u0082\u0080\u0001\u0000\u0000\u0000\u0082\u0083\u0001\u0000"+
"\u0000\u0000\u0083\u001e\u0001\u0000\u0000\u0000\u0084\u0082\u0001\u0000"+
"\u0000\u0000\u0085\u0087\u0007\u0004\u0000\u0000\u0086\u0085\u0001\u0000"+
"\u0000\u0000\u0087\u0088\u0001\u0000\u0000\u0000\u0088\u0086\u0001\u0000"+
"\u0000\u0000\u0088\u0089\u0001\u0000\u0000\u0000\u0089 \u0001\u0000\u0000"+
"\u0000\u008a\u008c\u0007\u0005\u0000\u0000\u008b\u008a\u0001\u0000\u0000"+
"\u0000\u008c\u008d\u0001\u0000\u0000\u0000\u008d\u008b\u0001\u0000\u0000"+
"\u0000\u008d\u008e\u0001\u0000\u0000\u0000\u008e\u008f\u0001\u0000\u0000"+
"\u0000\u008f\u0090\u0006\u0010\u0000\u0000\u0090\"\u0001\u0000\u0000\u0000"+
"\u0091\u0095\u0005%\u0000\u0000\u0092\u0094\b\u0004\u0000\u0000\u0093"+
"\u0092\u0001\u0000\u0000\u0000\u0094\u0097\u0001\u0000\u0000\u0000\u0095"+
"\u0093\u0001\u0000\u0000\u0000\u0095\u0096\u0001\u0000\u0000\u0000\u0096"+
"\u0098\u0001\u0000\u0000\u0000\u0097\u0095\u0001\u0000\u0000\u0000\u0098"+
"\u0099\u0006\u0011\u0000\u0000\u0099$\u0001\u0000\u0000\u0000\n\u0000"+
"K\\px|\u0082\u0088\u008d\u0095\u0001\u0006\u0000\u0000";
public static final ATN _ATN =
new ATNDeserializer().deserialize(_serializedATN.toCharArray());
static {
_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
_decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
}
}
}

View File

@@ -0,0 +1,29 @@
USECASE_START=1
ACTOR=2
SYSTEM_BOUNDARY=3
END=4
ARROW=5
LABELED_ARROW=6
AT=7
LBRACE=8
RBRACE=9
LPAREN=10
RPAREN=11
COMMA=12
COLON=13
STRING=14
IDENTIFIER=15
NEWLINE=16
WS=17
COMMENT=18
'usecase'=1
'actor'=2
'systemBoundary'=3
'end'=4
'@'=7
'{'=8
'}'=9
'('=10
')'=11
','=12
':'=13

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
grammar Usecase;
// Parser rules
usecaseDiagram
: USECASE_START NEWLINE* statement* EOF
;
statement
: actor NEWLINE*
| systemBoundary NEWLINE*
| systemBoundaryMetadata NEWLINE*
| useCase NEWLINE*
| relationship NEWLINE*
| actorRelationship NEWLINE*
| NEWLINE
;
relationship
: actorName ARROW target
| actorName LABELED_ARROW target
;
actorRelationship
: ACTOR actorName ARROW target
| ACTOR actorName LABELED_ARROW target
;
target
: useCaseName
| nodeDefinition
;
nodeDefinition
: nodeId LPAREN nodeLabel RPAREN
;
nodeId
: IDENTIFIER
;
nodeLabel
: IDENTIFIER (WS IDENTIFIER)*
| STRING
;
actorName
: IDENTIFIER
;
systemBoundary
: SYSTEM_BOUNDARY boundaryName LBRACE NEWLINE* boundaryContent* RBRACE
| SYSTEM_BOUNDARY boundaryName NEWLINE* boundaryContent* END
;
systemBoundaryMetadata
: boundaryName AT LBRACE metadataContent RBRACE
;
boundaryContent
: useCase NEWLINE*
| NEWLINE
;
useCase
: useCaseName
;
boundaryName
: IDENTIFIER
;
useCaseName
: IDENTIFIER
;
actor
: ACTOR actorList
;
actorList
: actorDefinition (COMMA actorDefinition)*
;
actorDefinition
: actorName metadata?
;
metadata
: AT LBRACE metadataContent RBRACE
;
metadataContent
: metadataPair (COMMA metadataPair)*
|
;
metadataPair
: metadataKey COLON metadataValue
;
metadataKey
: IDENTIFIER
;
metadataValue
: STRING
| IDENTIFIER
;
// Lexer rules
USECASE_START
: 'usecase'
;
ACTOR
: 'actor'
;
SYSTEM_BOUNDARY
: 'systemBoundary'
;
END
: 'end'
;
ARROW
: '-->'
| '->'
;
LABELED_ARROW
: '--' IDENTIFIER '-->'
| '--' IDENTIFIER '->'
;
AT
: '@'
;
LBRACE
: '{'
;
RBRACE
: '}'
;
LPAREN
: '('
;
RPAREN
: ')'
;
COMMA
: ','
;
COLON
: ':'
;
STRING
: '"' (~["\r\n])* '"'
| '\'' (~['\r\n])* '\''
;
IDENTIFIER
: [a-zA-Z_][a-zA-Z0-9_]*
;
NEWLINE
: [\r\n]+
;
WS
: [ \t]+ -> skip
;
COMMENT
: '%' ~[\r\n]* -> skip
;

View File

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

View File

@@ -0,0 +1,387 @@
import { parseUsecase } from './usecaseParser.js';
// Test basic usecase diagram parsing
function testBasicUsecaseParsing() {
const input = `usecase
actor Developer1
actor Developer2
actor Developer3`;
const result = parseUsecase(input);
console.log('Test Basic Usecase Parsing:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test simple usecase diagram
function testSimpleUsecaseParsing() {
const input = `usecase
actor User
actor Admin`;
const result = parseUsecase(input);
console.log('Test Simple Usecase Parsing:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test metadata parsing
function testMetadataParsing() {
const input = `usecase
actor Developer1@{ icon : 'icon_name', place: "sample place" }`;
const result = parseUsecase(input);
console.log('Test Metadata Parsing:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test complex metadata parsing
function testComplexMetadataParsing() {
const input = `usecase
actor Developer1@{ icon : 'icon_name', type : 'hollow', place: "sample place", material:"sample" }`;
const result = parseUsecase(input);
console.log('Test Complex Metadata Parsing:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test mixed actors (with and without metadata)
function testMixedActorsParsing() {
const input = `usecase
actor User
actor Developer1@{ icon : 'dev_icon' }
actor Admin@{ type: 'admin', place: "office" }`;
const result = parseUsecase(input);
console.log('Test Mixed Actors Parsing:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test multiple actors in single line
function testMultipleActorsSingleLine() {
const input = `usecase
actor Developer1, Developer2, Developer3`;
const result = parseUsecase(input);
console.log('Test Multiple Actors Single Line:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test multiple actors with metadata
function testMultipleActorsWithMetadata() {
const input = `usecase
actor Developer1@{ icon: 'dev' }, Developer2, Developer3@{ type: 'admin' }`;
const result = parseUsecase(input);
console.log('Test Multiple Actors With Metadata:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test five actors in single line
function testFiveActorsSingleLine() {
const input = `usecase
actor Developer1, Developer2, Developer3, Developer4, Developer5`;
const result = parseUsecase(input);
console.log('Test Five Actors Single Line:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test system boundary parsing
function testSystemBoundaryParsing() {
const input = `usecase
actor Developer1
systemBoundary Tasks
coding
testing
deploying
end`;
const result = parseUsecase(input);
console.log('Test System Boundary Parsing:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test mixed actors and system boundaries
function testMixedActorsAndBoundaries() {
const input = `usecase
actor Developer1, Developer2
systemBoundary Tasks
coding
testing
end
actor Admin`;
const result = parseUsecase(input);
console.log('Test Mixed Actors and Boundaries:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test curly brace system boundary parsing
function testCurlyBraceSystemBoundary() {
const input = `usecase
actor Developer1
systemBoundary Tasks {
playing
reviewing
}`;
const result = parseUsecase(input);
console.log('Test Curly Brace System Boundary:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test relationship parsing
function testRelationshipParsing() {
const input = `usecase
actor Developer1
systemBoundary Tasks {
playing
reviewing
}
Developer1 --> playing
Developer1 --> reviewing`;
const result = parseUsecase(input);
console.log('Test Relationship Parsing:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test complete example
function testCompleteExample() {
const input = `usecase
actor Developer1
systemBoundary Tasks {
playing
reviewing
}
Developer1 --> playing
Developer1 --> reviewing`;
const result = parseUsecase(input);
console.log('Test Complete Example:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test node definitions
function testNodeDefinitions() {
const input = `usecase
actor Tester1
Tester1 --> c(Go through testing)`;
const result = parseUsecase(input);
console.log('Test Node Definitions:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test inline actor-node relationships
function testInlineActorNodeRelationships() {
const input = `usecase
actor Developer1 --> a(Go through code)
actor Developer2 --> b(Go through implementation)`;
const result = parseUsecase(input);
console.log('Test Inline Actor-Node Relationships:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test mixed syntax
function testMixedSyntax() {
const input = `usecase
actor Tester1
Tester1 --> c(Go through testing)
actor Developer1 --> a(Go through code)
actor Developer2 --> b(Go through implementation)`;
const result = parseUsecase(input);
console.log('Test Mixed Syntax:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test edge labels
function testEdgeLabels() {
const input = `usecase
actor Developer1
Developer1 --task2--> c(Go through testing)`;
const result = parseUsecase(input);
console.log('Test Edge Labels:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test edge labels with inline syntax
function testInlineEdgeLabels() {
const input = `usecase
actor Developer1 --task1--> a(Go through code)`;
const result = parseUsecase(input);
console.log('Test Inline Edge Labels:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Test mixed edge labels and regular arrows
function testMixedEdgeLabels() {
const input = `usecase
actor Developer1
actor Tester1
Developer1 --task1--> a(Go through code)
Tester1 --> b(Go through testing)`;
const result = parseUsecase(input);
console.log('Test Mixed Edge Labels:');
console.log('Success:', result.success);
if (result.success && result.ast) {
console.log('Statements:', result.ast.statements.length);
console.log('AST:', JSON.stringify(result.ast, null, 2));
} else {
console.log('Errors:', result.errors);
}
console.log('---');
}
// Run tests
console.log('Running Usecase Parser Tests...\n');
testBasicUsecaseParsing();
testSimpleUsecaseParsing();
testMetadataParsing();
testComplexMetadataParsing();
testMixedActorsParsing();
testMultipleActorsSingleLine();
testMultipleActorsWithMetadata();
testFiveActorsSingleLine();
testSystemBoundaryParsing();
testMixedActorsAndBoundaries();
testCurlyBraceSystemBoundary();
testRelationshipParsing();
testCompleteExample();
testNodeDefinitions();
testInlineActorNodeRelationships();
testMixedSyntax();
testEdgeLabels();
testInlineEdgeLabels();
testMixedEdgeLabels();
console.log('Tests completed.');

View File

@@ -0,0 +1,752 @@
// Simple tokenizer and parser for usecase diagrams
// This approach is more compatible with the mermaid build system
import type {
UsecaseDiagram,
Statement,
Actor,
Usecase,
SystemBoundary,
SystemBoundaryMetadata,
ActorUseCaseRelationship,
Node,
ActorNodeRelationship,
InlineActorNodeRelationship,
ParseResult
} from './usecaseTypes.js';
// Token types
enum TokenType {
USECASE_START = 'USECASE_START',
ACTOR = 'ACTOR',
SYSTEM_BOUNDARY = 'SYSTEM_BOUNDARY',
END = 'END',
ARROW = 'ARROW',
LABELED_ARROW = 'LABELED_ARROW',
AT = 'AT',
LBRACE = 'LBRACE',
RBRACE = 'RBRACE',
LPAREN = 'LPAREN',
RPAREN = 'RPAREN',
COMMA = 'COMMA',
COLON = 'COLON',
STRING = 'STRING',
IDENTIFIER = 'IDENTIFIER',
NEWLINE = 'NEWLINE',
EOF = 'EOF'
}
interface Token {
type: TokenType;
value: string;
line: number;
column: number;
}
class UsecaseLexer {
private input: string;
private position: number = 0;
private line: number = 1;
private column: number = 1;
constructor(input: string) {
this.input = input;
}
tokenize(): Token[] {
const tokens: Token[] = [];
while (this.position < this.input.length) {
this.skipWhitespace();
if (this.position >= this.input.length) {
break;
}
const token = this.nextToken();
if (token) {
tokens.push(token);
}
}
tokens.push({
type: TokenType.EOF,
value: '',
line: this.line,
column: this.column
});
return tokens;
}
private nextToken(): Token | null {
const startLine = this.line;
const startColumn = this.column;
// Skip comments
if (this.peek() === '%') {
this.skipComment();
return null;
}
// Newlines
if (this.peek() === '\n' || this.peek() === '\r') {
this.advance();
if (this.peek() === '\n') {
this.advance();
}
this.line++;
this.column = 1;
return {
type: TokenType.NEWLINE,
value: '\n',
line: startLine,
column: startColumn
};
}
// Strings
if (this.peek() === '"' || this.peek() === "'") {
return this.readString(startLine, startColumn);
}
// Arrow tokens (-->, ->, --label-->, --label->)
if (this.peek() === '-') {
if (this.peek(1) === '-') {
// Check for labeled arrow: --label--> or --label->
const labeledArrowMatch = this.tryParseLabeledArrow();
if (labeledArrowMatch) {
return labeledArrowMatch;
}
// Regular arrow: -->
if (this.peek(2) === '>') {
this.advance(3);
return { type: TokenType.ARROW, value: '-->', line: startLine, column: startColumn };
}
} else if (this.peek(1) === '>') {
// Regular arrow: ->
this.advance(2);
return { type: TokenType.ARROW, value: '->', line: startLine, column: startColumn };
}
}
// Single character tokens
switch (this.peek()) {
case '@':
this.advance();
return { type: TokenType.AT, value: '@', line: startLine, column: startColumn };
case '{':
this.advance();
return { type: TokenType.LBRACE, value: '{', line: startLine, column: startColumn };
case '}':
this.advance();
return { type: TokenType.RBRACE, value: '}', line: startLine, column: startColumn };
case ',':
this.advance();
return { type: TokenType.COMMA, value: ',', line: startLine, column: startColumn };
case ':':
this.advance();
return { type: TokenType.COLON, value: ':', line: startLine, column: startColumn };
case '(':
this.advance();
return { type: TokenType.LPAREN, value: '(', line: startLine, column: startColumn };
case ')':
this.advance();
return { type: TokenType.RPAREN, value: ')', line: startLine, column: startColumn };
}
// Keywords and identifiers
if (this.isAlpha(this.peek())) {
return this.readIdentifierOrKeyword(startLine, startColumn);
}
// Skip unknown characters
this.advance();
return null;
}
private readIdentifierOrKeyword(line: number, column: number): Token {
let value = '';
while (this.position < this.input.length &&
(this.isAlphaNumeric(this.peek()) || this.peek() === '_')) {
value += this.peek();
this.advance();
}
// Check for keywords
const type = this.getKeywordType(value);
return {
type,
value,
line,
column
};
}
private readString(line: number, column: number): Token {
const quote = this.peek();
this.advance(); // Skip opening quote
let value = '';
while (this.position < this.input.length && this.peek() !== quote) {
value += this.peek();
this.advance();
}
if (this.peek() === quote) {
this.advance(); // Skip closing quote
}
return {
type: TokenType.STRING,
value: value, // Return the content without quotes
line,
column
};
}
private getKeywordType(value: string): TokenType {
switch (value.toLowerCase()) {
case 'usecase': return TokenType.USECASE_START;
case 'actor': return TokenType.ACTOR;
case 'systemboundary': return TokenType.SYSTEM_BOUNDARY;
case 'end': return TokenType.END;
default: return TokenType.IDENTIFIER;
}
}
private skipWhitespace(): void {
while (this.position < this.input.length &&
(this.peek() === ' ' || this.peek() === '\t')) {
this.advance();
}
}
private skipComment(): void {
while (this.position < this.input.length &&
this.peek() !== '\n' && this.peek() !== '\r') {
this.advance();
}
}
private peek(offset: number = 0): string {
const pos = this.position + offset;
return pos < this.input.length ? this.input[pos] : '';
}
private tryParseLabeledArrow(): Token | null {
// Try to parse --label--> or --label->
const startPos = this.position;
const startLine = this.line;
const startColumn = this.column;
// Skip initial '--'
if (this.peek() !== '-' || this.peek(1) !== '-') {
return null;
}
let pos = 2;
let label = '';
// Read the label
while (pos < this.input.length - this.position) {
const char = this.peek(pos);
if (char === '-') {
// Check if this is the end pattern
if (this.peek(pos + 1) === '-' && this.peek(pos + 2) === '>') {
// Found --label-->
this.advance(pos + 3);
return {
type: TokenType.LABELED_ARROW,
value: `--${label}-->`,
line: startLine,
column: startColumn
};
} else if (this.peek(pos + 1) === '>') {
// Found --label->
this.advance(pos + 2);
return {
type: TokenType.LABELED_ARROW,
value: `--${label}->`,
line: startLine,
column: startColumn
};
} else {
label += char;
pos++;
}
} else if (char.match(/[a-zA-Z0-9_]/)) {
label += char;
pos++;
} else {
// Invalid character in label
return null;
}
}
return null;
}
private advance(count: number = 1): void {
for (let i = 0; i < count && this.position < this.input.length; i++) {
this.position++;
this.column++;
}
}
private isAlpha(char: string): boolean {
return /[a-zA-Z]/.test(char);
}
private isAlphaNumeric(char: string): boolean {
return /[a-zA-Z0-9]/.test(char);
}
}
class UsecaseParser {
private tokens: Token[];
private position: number = 0;
constructor(tokens: Token[]) {
this.tokens = tokens;
}
parse(): UsecaseDiagram {
const statements: Statement[] = [];
// Expect 'usecase' keyword at the start
this.consume(TokenType.USECASE_START);
this.skipNewlines();
while (!this.isAtEnd()) {
this.skipNewlines();
if (this.isAtEnd()) {
break;
}
const parsedStatements = this.parseStatement();
if (parsedStatements) {
if (Array.isArray(parsedStatements)) {
statements.push(...parsedStatements);
} else {
statements.push(parsedStatements);
}
}
}
return {
type: 'usecaseDiagram',
statements
};
}
private parseStatement(): Statement | Statement[] | null {
const token = this.peek();
switch (token.type) {
case TokenType.ACTOR:
return this.parseActorStatement();
case TokenType.SYSTEM_BOUNDARY:
return this.parseSystemBoundary();
case TokenType.IDENTIFIER:
// Look ahead to see if this is a systemBoundaryMetadata, relationship, or use case
if (this.isSystemBoundaryMetadata()) {
return this.parseSystemBoundaryMetadata();
} else if (this.isRelationship()) {
return this.parseRelationship();
} else {
return this.parseUseCase();
}
default:
this.advance(); // Skip unknown tokens
return null;
}
}
private parseActorStatement(): Statement | Statement[] {
this.consume(TokenType.ACTOR);
// Check if this is an inline actor-node relationship
// Look ahead: IDENTIFIER ARROW IDENTIFIER LPAREN
if (this.isInlineActorNodeRelationship()) {
return this.parseInlineActorNodeRelationship();
}
const actors: Actor[] = [];
// Parse first actor
actors.push(this.parseActorDefinition());
// Parse additional actors separated by commas
while (this.check(TokenType.COMMA)) {
this.consume(TokenType.COMMA);
actors.push(this.parseActorDefinition());
}
return actors;
}
private parseActorDefinition(): Actor {
const name = this.consume(TokenType.IDENTIFIER).value;
let metadata: Record<string, string> | undefined;
// Check for optional metadata
if (this.check(TokenType.AT)) {
metadata = this.parseMetadata();
}
const actor: Actor = { type: 'actor', name };
if (metadata) {
actor.metadata = metadata;
}
return actor;
}
private parseSystemBoundary(): SystemBoundary {
this.consume(TokenType.SYSTEM_BOUNDARY);
const name = this.consume(TokenType.IDENTIFIER).value;
this.consume(TokenType.LBRACE);
// Skip newlines after opening brace
this.skipNewlines();
const useCases: Usecase[] = [];
// Parse use cases until we hit closing brace
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
this.skipNewlines();
if (this.check(TokenType.RBRACE) || this.isAtEnd()) {
break;
}
if (this.check(TokenType.IDENTIFIER)) {
const useCase = this.parseUseCase();
if (useCase) {
useCases.push(useCase as Usecase);
}
} else {
this.advance(); // Skip unknown tokens
}
}
this.consume(TokenType.RBRACE);
return {
type: 'systemBoundary',
name,
useCases
};
}
private parseSystemBoundaryMetadata(): SystemBoundaryMetadata {
const name = this.consume(TokenType.IDENTIFIER).value;
this.consume(TokenType.AT);
this.consume(TokenType.LBRACE);
const metadata: Record<string, string> = {};
// Parse metadata content
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
if (this.check(TokenType.IDENTIFIER)) {
const key = this.consume(TokenType.IDENTIFIER).value;
this.consume(TokenType.COLON);
let value = '';
if (this.check(TokenType.STRING)) {
value = this.consume(TokenType.STRING).value;
// Remove quotes from string value
value = value.slice(1, -1);
} else if (this.check(TokenType.IDENTIFIER)) {
value = this.consume(TokenType.IDENTIFIER).value;
}
metadata[key] = value;
// Optional comma
if (this.check(TokenType.COMMA)) {
this.advance();
}
} else {
this.advance(); // Skip unknown tokens
}
}
this.consume(TokenType.RBRACE);
return {
type: 'systemBoundaryMetadata',
name,
metadata
};
}
private parseUseCase(): Usecase {
const name = this.consume(TokenType.IDENTIFIER).value;
return {
type: 'usecase',
name
};
}
private isRelationship(): boolean {
// Look ahead to see if there's an arrow after the identifier
const currentPos = this.position;
this.advance(); // Skip the identifier
const hasArrow = this.check(TokenType.ARROW) || this.check(TokenType.LABELED_ARROW);
this.position = currentPos; // Reset position
return hasArrow;
}
private isSystemBoundaryMetadata(): boolean {
// Look ahead to see if there's an @ after the identifier
const currentPos = this.position;
this.advance(); // Skip the identifier
const hasAt = this.check(TokenType.AT);
this.position = currentPos; // Reset position
return hasAt;
}
private parseRelationship(): ActorUseCaseRelationship | ActorNodeRelationship {
const from = this.consume(TokenType.IDENTIFIER).value;
let arrowToken: Token;
let label: string | undefined;
if (this.check(TokenType.LABELED_ARROW)) {
arrowToken = this.consume(TokenType.LABELED_ARROW);
// Extract label from --label--> or --label->
const arrowValue = arrowToken.value;
const match = arrowValue.match(/^--(.+?)-+>$/);
if (match) {
label = match[1];
}
} else {
arrowToken = this.consume(TokenType.ARROW);
}
// Check if target is a node definition (ID followed by parentheses)
if (this.isNodeDefinition()) {
const node = this.parseNodeDefinition();
return {
type: 'actorNodeRelationship',
from,
to: node.id,
arrow: arrowToken.value,
label
};
} else {
const to = this.consume(TokenType.IDENTIFIER).value;
return {
type: 'actorUseCaseRelationship',
from,
to,
arrow: arrowToken.value,
label
};
}
}
private isInlineActorNodeRelationship(): boolean {
// Look ahead: IDENTIFIER (ARROW|LABELED_ARROW) IDENTIFIER LPAREN
const currentPos = this.position;
if (!this.check(TokenType.IDENTIFIER)) {
this.position = currentPos;
return false;
}
this.advance(); // Skip actor name
if (!this.check(TokenType.ARROW) && !this.check(TokenType.LABELED_ARROW)) {
this.position = currentPos;
return false;
}
this.advance(); // Skip arrow
if (!this.check(TokenType.IDENTIFIER)) {
this.position = currentPos;
return false;
}
this.advance(); // Skip node ID
const hasLParen = this.check(TokenType.LPAREN);
this.position = currentPos; // Reset position
return hasLParen;
}
private parseInlineActorNodeRelationship(): InlineActorNodeRelationship {
const actor = this.consume(TokenType.IDENTIFIER).value;
let arrowToken: Token;
let label: string | undefined;
if (this.check(TokenType.LABELED_ARROW)) {
arrowToken = this.consume(TokenType.LABELED_ARROW);
// Extract label from --label--> or --label->
const arrowValue = arrowToken.value;
const match = arrowValue.match(/^--(.+?)-+>$/);
if (match) {
label = match[1];
}
} else {
arrowToken = this.consume(TokenType.ARROW);
}
const node = this.parseNodeDefinition();
return {
type: 'inlineActorNodeRelationship',
actor,
node,
arrow: arrowToken.value,
label
};
}
private isNodeDefinition(): boolean {
// Look ahead: IDENTIFIER LPAREN
const currentPos = this.position;
if (!this.check(TokenType.IDENTIFIER)) {
this.position = currentPos;
return false;
}
this.advance(); // Skip node ID
const hasLParen = this.check(TokenType.LPAREN);
this.position = currentPos; // Reset position
return hasLParen;
}
private parseNodeDefinition(): Node {
const id = this.consume(TokenType.IDENTIFIER).value;
this.consume(TokenType.LPAREN);
// Parse node label (can be multiple words or a string)
let label = '';
if (this.check(TokenType.STRING)) {
label = this.consume(TokenType.STRING).value;
// Remove quotes
label = label.slice(1, -1);
} else {
// Parse multiple identifiers as label
const labelParts: string[] = [];
while (this.check(TokenType.IDENTIFIER) && !this.check(TokenType.RPAREN)) {
labelParts.push(this.consume(TokenType.IDENTIFIER).value);
}
label = labelParts.join(' ');
}
this.consume(TokenType.RPAREN);
return {
type: 'node',
id,
label
};
}
private parseMetadata(): Record<string, string> {
this.consume(TokenType.AT);
this.consume(TokenType.LBRACE);
const metadata: Record<string, string> = {};
// Handle empty metadata
if (this.check(TokenType.RBRACE)) {
this.consume(TokenType.RBRACE);
return metadata;
}
// Parse key-value pairs
do {
const key = this.consume(TokenType.IDENTIFIER).value;
this.consume(TokenType.COLON);
let value: string;
if (this.check(TokenType.STRING)) {
value = this.consume(TokenType.STRING).value;
} else {
value = this.consume(TokenType.IDENTIFIER).value;
}
metadata[key] = value;
// Check for comma (more pairs) or closing brace
if (this.check(TokenType.COMMA)) {
this.consume(TokenType.COMMA);
} else {
break;
}
} while (!this.check(TokenType.RBRACE) && !this.isAtEnd());
this.consume(TokenType.RBRACE);
return metadata;
}
private skipNewlines(): void {
while (this.check(TokenType.NEWLINE)) {
this.advance();
}
}
private peek(): Token {
return this.tokens[this.position];
}
private advance(): Token {
if (!this.isAtEnd()) {
this.position++;
}
return this.tokens[this.position - 1];
}
private check(type: TokenType): boolean {
if (this.isAtEnd()) return false;
return this.peek().type === type;
}
private consume(type: TokenType): Token {
if (this.check(type)) {
return this.advance();
}
const current = this.peek();
throw new Error(`Expected ${type}, got ${current.type} at line ${current.line}`);
}
private isAtEnd(): boolean {
return this.position >= this.tokens.length || this.peek().type === TokenType.EOF;
}
}
export function parseUsecase(input: string): ParseResult {
try {
const lexer = new UsecaseLexer(input);
const tokens = lexer.tokenize();
const parser = new UsecaseParser(tokens);
const ast = parser.parse();
return {
success: true,
ast
};
} catch (error) {
return {
success: false,
errors: [error instanceof Error ? error.message : String(error)]
};
}
}

View File

@@ -0,0 +1,113 @@
// AST types for usecase diagrams
export interface UsecaseDiagram {
type: 'usecaseDiagram';
statements: Statement[];
}
export type Statement = Actor | SystemBoundary | SystemBoundaryMetadata | Usecase | Relationship | ActorUseCaseRelationship | Node | ActorNodeRelationship | InlineActorNodeRelationship;
export interface Title {
type: 'title';
text: string;
}
export interface AccDescr {
type: 'accDescr';
text: string;
}
export interface AccTitle {
type: 'accTitle';
text: string;
}
export interface Actor {
type: 'actor';
name: string;
metadata?: Record<string, string>;
}
export interface Usecase {
type: 'usecase';
name: string;
alias?: string;
}
export interface SystemBoundary {
type: 'systemBoundary';
name: string;
useCases: Usecase[];
metadata?: Record<string, string>;
}
export interface SystemBoundaryMetadata {
type: 'systemBoundaryMetadata';
name: string; // boundary name
metadata: Record<string, string>;
}
export interface ActorUseCaseRelationship {
type: 'actorUseCaseRelationship';
from: string; // actor name
to: string; // use case name
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
export interface Node {
type: 'node';
id: string; // node ID (e.g., 'a', 'b', 'c')
label: string; // node label (e.g., 'Go through code')
}
export interface ActorNodeRelationship {
type: 'actorNodeRelationship';
from: string; // actor name
to: string; // node ID
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
export interface InlineActorNodeRelationship {
type: 'inlineActorNodeRelationship';
actor: string; // actor name
node: Node; // node definition
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
export interface Relationship {
type: 'relationship';
from: string;
to: string;
relationshipType: RelationshipType;
label?: string;
}
export interface Note {
type: 'note';
position: NotePosition;
target: string;
text: string;
}
export type RelationshipType =
| 'arrow-left'
| 'arrow-right'
| 'arrow-both'
| 'extends'
| 'includes';
export type NotePosition =
| 'left'
| 'right'
| 'top'
| 'bottom';
// Parser result type
export interface ParseResult {
success: boolean;
ast?: UsecaseDiagram;
errors?: string[];
}

440
pnpm-lock.yaml generated
View File

@@ -227,8 +227,8 @@ importers:
specifier: ^7.0.4
version: 7.1.0
'@iconify/utils':
specifier: ^2.1.33
version: 2.3.0
specifier: ^3.0.1
version: 3.0.1
'@mermaid-js/parser':
specifier: workspace:^
version: link:../parser
@@ -499,8 +499,8 @@ importers:
specifier: ^2.0.3
version: 2.0.3
unocss:
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))
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))
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.0.0':
resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==}
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
'@antfu/utils@8.1.1':
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
'@antfu/utils@9.2.0':
resolution: {integrity: sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==}
'@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@2.3.0':
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
'@iconify/utils@3.0.1':
resolution: {integrity: sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
@@ -2740,6 +2740,9 @@ 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:
@@ -3538,88 +3541,94 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@unocss/astro@66.0.0':
resolution: {integrity: sha512-GBhXT6JPqXjDXoJZTXhySk83NgOt0UigChqrUUdG4x7Z+DVYkDBION8vZUJjw0OdIaxNQ4euGWu4GDsMF6gQQg==}
'@unocss/astro@66.4.2':
resolution: {integrity: sha512-En3AKHwkiPxtZT95vkVrNiRYrB+DFVCikew6/dMMCWDWVKK0+5tEVUTzR1ak3+YnzAXl0NpWj8D4zHb0PxOs/A==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
peerDependenciesMeta:
vite:
optional: true
'@unocss/cli@66.0.0':
resolution: {integrity: sha512-KVQiskoOjVkLVpNaG6WpLa4grPplrZROYZJVIUYSTqZyZRFNSvjttHcsCwpoWUEUdEombPtVZl8FrXePjY5IiQ==}
'@unocss/cli@66.4.2':
resolution: {integrity: sha512-WsXzrB0SHbSt2nOHtD5QM91VN8j38+wObqyGcoIhtBSugqzsc+t7AdPkxV/ZaYgtPAz87bR0WFEVKcbiBRnmJw==}
engines: {node: '>=14'}
hasBin: true
'@unocss/config@66.0.0':
resolution: {integrity: sha512-nFRGop/guBa4jLkrgXjaRDm5JPz4x3YpP10m5IQkHpHwlnHUVn1L9smyPl04ohYWhYn9ZcAHgR28Ih2jwta8hw==}
'@unocss/config@66.4.2':
resolution: {integrity: sha512-plji1gNGSzlWjuV2Uh0q6Dt5ZlNkOKCHpgxekW9J458WghGAMBeXgB9uNpWg6flilqP1g0GJQv+XvJcSkYRGpQ==}
engines: {node: '>=14'}
'@unocss/core@66.0.0':
resolution: {integrity: sha512-PdVbSMHNDDkr++9nkqzsZRAkaU84gxMTEgYbqI7dt2p1DXp/5tomVtmMsr2/whXGYKRiUc0xZ3p4Pzraz8TcXA==}
'@unocss/core@66.4.2':
resolution: {integrity: sha512-cYgMQrLhB9nRekv5c+yPDDa+5dzlMkA2UMQRil0s5D9Lb5n7NsCMcr6+nfxkcSYVLy92SbwDV45c6T7vIxFTOA==}
'@unocss/extractor-arbitrary-variants@66.0.0':
resolution: {integrity: sha512-vlkOIOuwBfaFBJcN6o7+obXjigjOlzVFN/jT6pG1WXbQDTRZ021jeF3i9INdb9D/0cQHSeDvNgi1TJ5oUxfiow==}
'@unocss/extractor-arbitrary-variants@66.4.2':
resolution: {integrity: sha512-T/eSeodfAp7HaWnQGqVLOsW4PbKUAvuybNRyvFWThMneM2qo+dOo3kFnA5my9ULAmRSFsAlyB1DnupD3qv5Klg==}
'@unocss/inspector@66.0.0':
resolution: {integrity: sha512-mkIxieVm0kMOKw+E4ABpIerihYMdjgq9A92RD5h2+W/ebpxTEw5lTTK1xcMLiAlmOrVYMQKjpgPeu3vQmDyGZQ==}
'@unocss/inspector@66.4.2':
resolution: {integrity: sha512-ugcJK8r2ypM4eIdgetVn8RhfKrbA3AF3OQ/RohK5PPk2UPDAScqabzYpfdNW4eYQsBOZOgoiqWtnfc8weqo8LQ==}
'@unocss/postcss@66.0.0':
resolution: {integrity: sha512-6bi+ujzh8I1PJwtmHX71LH8z/H9+vPxeYD4XgFihyU1k4Y6MVhjr7giGjLX4yP27IP+NsVyotD22V7by/dBVEA==}
'@unocss/postcss@66.4.2':
resolution: {integrity: sha512-tu4lnh6K27pIAuaQHlFlhXin8korwC0r1kQl00YMmF3THiX7orXkTP6xWGcQwnkbx4uQz1dw+tBimYxeaAMrhA==}
engines: {node: '>=14'}
peerDependencies:
postcss: ^8.4.21
'@unocss/preset-attributify@66.0.0':
resolution: {integrity: sha512-eYsOgmcDoiIgGAepIwRX+DKGYxc/wm0r4JnDuZdz29AB+A6oY/FGHS1BVt4rq9ny4B5PofP4p6Rty+vwD9rigw==}
'@unocss/preset-attributify@66.4.2':
resolution: {integrity: sha512-DwFJJkkawmHpjo3pGQE8FyoPsvhbxh+QMvvaAdYpo+iZ5HRkeDml9SOj7u6SGTcmbNyI+QR61s0KM8fxx6HcVQ==}
'@unocss/preset-icons@66.0.0':
resolution: {integrity: sha512-6ObwTvEGuPBbKWRoMMiDioHtwwQTFI5oojFLJ32Y8tW6TdXvBLkO88d7qpgQxEjgVt4nJrqF1WEfR4niRgBm0Q==}
'@unocss/preset-icons@66.4.2':
resolution: {integrity: sha512-qJx9gmesrvrmoTe9Mqoidihad8hm2MSD4QAezhfDSAyllioJOgyT0Bev/IEWAbehe9jtqYIh8v1oCerBPbGn6Q==}
'@unocss/preset-mini@66.0.0':
resolution: {integrity: sha512-d62eACnuKtR0dwCFOQXgvw5VLh5YSyK56xCzpHkh0j0GstgfDLfKTys0T/XVAAvdSvAy/8A8vhSNJ4PlIc9V2A==}
'@unocss/preset-mini@66.4.2':
resolution: {integrity: sha512-Ry+5hM+XLmT8HrEb182mUfcZuyrZ8xR+TBe72DBcliJ1DhOV3K67TCxwQucfb0zHbGV71HNWdPmHsLKxPDgweQ==}
'@unocss/preset-tagify@66.0.0':
resolution: {integrity: sha512-GGYGyWxaevh0jN0NoATVO1Qe7DFXM3ykLxchlXmG6/zy963pZxItg/njrKnxE9la4seCdxpFH7wQBa68imwwdA==}
'@unocss/preset-tagify@66.4.2':
resolution: {integrity: sha512-dECS09LqWJY4sYpgPUH2OAUftWU/tiZPR2XDRoTngeGU37GxSN+1sWtSmB7vwDm3C7opsdVUN20he8F1LUNubw==}
'@unocss/preset-typography@66.0.0':
resolution: {integrity: sha512-apjckP5nPU5mtaHTCzz5u/dK9KJWwJ2kOFCVk0+a/KhUWmnqnzmjRYZlEuWxxr5QxTdCW+9cIoRDSA0lYZS5tg==}
'@unocss/preset-typography@66.4.2':
resolution: {integrity: sha512-ZOKRuR5+V0r30QTVq04/6ZoIw75me3V25v2dU2YWJXIzwpMKmQ9TUN/M1yeiEUFfXjOaruWX6Ad6CvAw2MlCew==}
'@unocss/preset-uno@66.0.0':
resolution: {integrity: sha512-qgoZ/hzTI32bQvcyjcwvv1X/dbPlmQNehzgjUaL7QFT0q0/CN/SRpysfzoQ8DLl2se9T+YCOS9POx3KrpIiYSQ==}
'@unocss/preset-uno@66.4.2':
resolution: {integrity: sha512-1MFtPivGcpqRQFWdjtP40Enop1y3XDb3tlZXoMQUX0IGLG8HJOT+lfQx/Xl9t73ShJ8aAJ/l6qTxC43ZGNACzA==}
'@unocss/preset-web-fonts@66.0.0':
resolution: {integrity: sha512-9MzfDc6AJILN4Kq7Z91FfFbizBOYgw3lJd2UwqIs3PDYWG5iH5Zv5zhx6jelZVqEW5uWcIARYEEg2m4stZO1ZA==}
'@unocss/preset-web-fonts@66.4.2':
resolution: {integrity: sha512-4FYmleeRoM8r2DqGl6dfIjnX57tepcfZCvVfeCqYnk7475Yddmv1OYkoMjkWMnkK9MzdSxsFwHMU6CIUTmFTzQ==}
'@unocss/preset-wind3@66.0.0':
resolution: {integrity: sha512-WAGRmpi1sb2skvYn9DBQUvhfqrJ+VmQmn5ZGsT2ewvsk7HFCvVLAMzZeKrrTQepeNBRhg6HzFDDi8yg6yB5c9g==}
'@unocss/preset-wind3@66.4.2':
resolution: {integrity: sha512-0Aye/PaT08M/cQhPnGKn93iEVoRJbym0/1eomMvXoL+8oc7DVry35ws06r5CLu5h1sXI6UmS6sejoePFlSkLJQ==}
'@unocss/preset-wind@66.0.0':
resolution: {integrity: sha512-FtvGpHnGC7FiyKJavPnn5y9lsaoWRhXlujCqlT5Bw63kKhMNr0ogKySBpenUhJOhWhVM0OQXn2nZ3GZRxW2qpw==}
'@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/reset@66.0.0':
resolution: {integrity: sha512-YLFz/5yT7mFJC8JSmIUA5+bS3CBCJbtztOw+8rWzjQr/BEVSGuihWUUpI2Df6VVxXIXxKanZR6mIl59yvf+GEA==}
'@unocss/rule-utils@66.0.0':
resolution: {integrity: sha512-UJ51YHbwxYTGyj35ugsPlOT4gaa7tCbXdywZ3m5Nn0JgywwIqGmBFyiN9ZjHBHfJuDxmmPd6lxojoBscih/WMQ==}
'@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==}
engines: {node: '>=14'}
'@unocss/transformer-attributify-jsx@66.0.0':
resolution: {integrity: sha512-jS7szFXXC6RjTv9wo0NACskf618w981bkbyQ5izRO7Ha47sNpHhHDpaltnG7SR9qV4cCtGalOw4onVMHsRKwRg==}
'@unocss/transformer-attributify-jsx@66.4.2':
resolution: {integrity: sha512-de6LzoyW1tkdOftlCrj6z8wEb4j6l1sqmOU1nYKkYHw7luLFGxRUELC7iujlI9KmylbM02bcKfLETAfJy/je2w==}
'@unocss/transformer-compile-class@66.0.0':
resolution: {integrity: sha512-ytUIE0nAcHRMACuTXkHp8auZ483DXrOZw99jk3FJ+aFjpD/pVSFmX14AWJ7bqPFObxb4SLFs6KhQma30ESC22A==}
'@unocss/transformer-compile-class@66.4.2':
resolution: {integrity: sha512-+oiIrV8c3T7qiJdICr6YsEWik5sjbWirXF0mlpcBvZu2HyV559hvHjzuWKr/fl7xYYZKDL9FvddbqWo3DOXh3Q==}
'@unocss/transformer-directives@66.0.0':
resolution: {integrity: sha512-utcg7m2Foi7uHrU5WHadNuJ0a3qWG8tZNkQMi+m0DQpX6KWfuDtDn0zDZ1X+z5lmiB3WGSJERRrsvZbj1q50Mw==}
'@unocss/transformer-directives@66.4.2':
resolution: {integrity: sha512-7m/dTrCUkBkZeSRKPxPEo65Rav239orQSLq6sztwZhoA4x/6H8r58xCkAK0qC9VEalyerpCpyarU3sKN4+ehNg==}
'@unocss/transformer-variant-group@66.0.0':
resolution: {integrity: sha512-1BLjNWtAnR1JAcQGw0TS+nGrVoB9aznzvVZRoTx23dtRr3btvgKPHb8LrD48eD/p8Dtw9j3WfuxMDKXKegKDLg==}
'@unocss/transformer-variant-group@66.4.2':
resolution: {integrity: sha512-SbPDbZUrhQyL4CpvnpvUfrr1DFq8AKf8ofPGbMJDm5S2TInQ34vFaIrhNroGR0szntMZRH5Zlkq6LtVUKDRs5g==}
'@unocss/vite@66.0.0':
resolution: {integrity: sha512-IVcPX8xL+2edyXKt4tp9yu5A6gcbPVCsspfcL0XgziCr01kS+4qSoZ90F3IUs3hXc/AyO5eCpRtGFMPLpOjXQg==}
'@unocss/vite@66.4.2':
resolution: {integrity: sha512-7eON9iPF3qWzuI+M6u0kq7K3y9nEbimZlLj01nGoqrgSGxEsyJpP01QQQsmT7FPRiZzRMJv7BiKMEyDQSuRRCA==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
@@ -4779,12 +4788,15 @@ 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.0:
resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==}
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
console.table@0.10.0:
@@ -5852,6 +5864,9 @@ 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==}
@@ -7276,6 +7291,10 @@ 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'}
@@ -8040,6 +8059,9 @@ 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==}
@@ -8223,6 +8245,9 @@ 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'}
@@ -8405,6 +8430,9 @@ 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==}
@@ -8996,6 +9024,7 @@ 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==}
@@ -9327,6 +9356,9 @@ 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'}
@@ -9547,8 +9579,8 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
unconfig@7.0.0:
resolution: {integrity: sha512-G5CJSoG6ZTxgzCJblEfgpdRK2tos9+UdD2WtecDUVfImzQ0hFjwpH5RVvGMhP4pRpC9ML7NrC4qBsBl0Ttj35A==}
unconfig@7.3.2:
resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==}
underscore@1.1.7:
resolution: {integrity: sha512-w4QtCHoLBXw1mjofIDoMyexaEdWGMedWNDhlWTtT1V1lCRqi65Pnoygkh6+WRdr+Bm8ldkBNkNeCsXGMlQS9HQ==}
@@ -9625,12 +9657,12 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
unocss@66.0.0:
resolution: {integrity: sha512-SHstiv1s7zGPSjzOsADzlwRhQM+6817+OqQE3Fv+N/nn2QLNx1bi3WXybFfz5tWkzBtyTZlwdPmeecsIs1yOCA==}
unocss@66.4.2:
resolution: {integrity: sha512-PsZ+4XF/ekiParR7PZEM7AchvHJ78EIfOXlqTPflTOXCYgZ77kG9NaIaIf4lHRevY+rRTyrHrjxdg1Ern2j8qw==}
engines: {node: '>=14'}
peerDependencies:
'@unocss/webpack': 66.0.0
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
'@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
peerDependenciesMeta:
'@unocss/webpack':
optional: true
@@ -9974,10 +10006,8 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-flow-layout@0.1.1:
resolution: {integrity: sha512-JdgRRUVrN0Y2GosA0M68DEbKlXMqJ7FQgsK8CjQD2vxvNSqAU6PZEpi4cfcTVtfM2GVOMjHo7GKKLbXxOBqDqA==}
peerDependencies:
vue: ^3.4.37
vue-flow-layout@0.2.0:
resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==}
vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
@@ -10498,12 +10528,12 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
'@antfu/install-pkg@1.0.0':
'@antfu/install-pkg@1.1.0':
dependencies:
package-manager-detector: 0.2.9
tinyexec: 0.3.2
package-manager-detector: 1.3.0
tinyexec: 1.0.1
'@antfu/utils@8.1.1': {}
'@antfu/utils@9.2.0': {}
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
dependencies:
@@ -10856,7 +10886,7 @@ snapshots:
'@babel/generator@7.27.1':
dependencies:
'@babel/parser': 7.27.2
'@babel/parser': 7.28.0
'@babel/types': 7.27.1
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
@@ -10955,7 +10985,7 @@ snapshots:
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.27.1
'@babel/traverse': 7.28.0
'@babel/types': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10965,7 +10995,7 @@ snapshots:
'@babel/core': 7.27.1
'@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.27.1
'@babel/traverse': 7.28.0
transitivePeerDependencies:
- supports-color
@@ -12080,14 +12110,14 @@ snapshots:
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.27.2
'@babel/parser': 7.28.0
'@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.27.2
'@babel/parser': 7.28.0
'@babel/template': 7.27.2
'@babel/types': 7.27.1
debug: 4.4.1(supports-color@8.1.1)
@@ -12673,7 +12703,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.0
debug: 4.4.1(supports-color@8.1.1)
lodash: 4.17.21
webpack: 5.95.0(esbuild@0.25.0)
transitivePeerDependencies:
@@ -13111,15 +13141,15 @@ snapshots:
'@iconify/types@2.0.0': {}
'@iconify/utils@2.3.0':
'@iconify/utils@3.0.1':
dependencies:
'@antfu/install-pkg': 1.0.0
'@antfu/utils': 8.1.1
'@antfu/install-pkg': 1.1.0
'@antfu/utils': 9.2.0
'@iconify/types': 2.0.0
debug: 4.4.0
debug: 4.4.1(supports-color@8.1.1)
globals: 15.15.0
kolorist: 1.8.0
local-pkg: 1.0.0
local-pkg: 1.1.1
mlly: 1.7.4
transitivePeerDependencies:
- supports-color
@@ -13501,6 +13531,10 @@ 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)
@@ -13840,7 +13874,7 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.27.2
'@babel/parser': 7.28.0
'@babel/types': 7.27.1
'@types/babel__generator': 7.6.8
'@types/babel__template': 7.4.4
@@ -13852,7 +13886,7 @@ snapshots:
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.27.2
'@babel/parser': 7.28.0
'@babel/types': 7.27.1
'@types/babel__traverse@7.20.6':
@@ -14358,150 +14392,157 @@ snapshots:
'@ungap/structured-clone@1.3.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/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))':
dependencies:
'@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))
'@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))
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.0.0':
'@unocss/cli@66.4.2':
dependencies:
'@ampproject/remapping': 2.3.0
'@unocss/config': 66.0.0
'@unocss/core': 66.0.0
'@unocss/preset-uno': 66.0.0
'@unocss/config': 66.4.2
'@unocss/core': 66.4.2
'@unocss/preset-uno': 66.4.2
cac: 6.7.14
chokidar: 3.6.0
colorette: 2.0.20
consola: 3.4.0
consola: 3.4.2
magic-string: 0.30.17
pathe: 2.0.3
perfect-debounce: 1.0.0
tinyglobby: 0.2.12
tinyglobby: 0.2.14
unplugin-utils: 0.2.4
'@unocss/config@66.0.0':
'@unocss/config@66.4.2':
dependencies:
'@unocss/core': 66.0.0
unconfig: 7.0.0
'@unocss/core': 66.4.2
unconfig: 7.3.2
'@unocss/core@66.0.0': {}
'@unocss/core@66.4.2': {}
'@unocss/extractor-arbitrary-variants@66.0.0':
'@unocss/extractor-arbitrary-variants@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/core': 66.4.2
'@unocss/inspector@66.0.0(vue@3.5.13(typescript@5.7.3))':
'@unocss/inspector@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/core': 66.4.2
'@unocss/rule-utils': 66.4.2
colorette: 2.0.20
gzip-size: 6.0.0
sirv: 3.0.1
vue-flow-layout: 0.1.1(vue@3.5.13(typescript@5.7.3))
transitivePeerDependencies:
- vue
vue-flow-layout: 0.2.0
'@unocss/postcss@66.0.0(postcss@8.5.6)':
'@unocss/postcss@66.4.2(postcss@8.5.6)':
dependencies:
'@unocss/config': 66.0.0
'@unocss/core': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/config': 66.4.2
'@unocss/core': 66.4.2
'@unocss/rule-utils': 66.4.2
css-tree: 3.1.0
postcss: 8.5.6
tinyglobby: 0.2.12
tinyglobby: 0.2.14
'@unocss/preset-attributify@66.0.0':
'@unocss/preset-attributify@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/core': 66.4.2
'@unocss/preset-icons@66.0.0':
'@unocss/preset-icons@66.4.2':
dependencies:
'@iconify/utils': 2.3.0
'@unocss/core': 66.0.0
'@iconify/utils': 3.0.1
'@unocss/core': 66.4.2
ofetch: 1.4.1
transitivePeerDependencies:
- supports-color
'@unocss/preset-mini@66.0.0':
'@unocss/preset-mini@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/extractor-arbitrary-variants': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/core': 66.4.2
'@unocss/extractor-arbitrary-variants': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/preset-tagify@66.0.0':
'@unocss/preset-tagify@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/core': 66.4.2
'@unocss/preset-typography@66.0.0':
'@unocss/preset-typography@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/preset-mini': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/core': 66.4.2
'@unocss/preset-mini': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/preset-uno@66.0.0':
'@unocss/preset-uno@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/preset-wind3': 66.0.0
'@unocss/core': 66.4.2
'@unocss/preset-wind3': 66.4.2
'@unocss/preset-web-fonts@66.0.0':
'@unocss/preset-web-fonts@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/core': 66.4.2
ofetch: 1.4.1
'@unocss/preset-wind3@66.0.0':
'@unocss/preset-wind3@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/preset-mini': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/core': 66.4.2
'@unocss/preset-mini': 66.4.2
'@unocss/rule-utils': 66.4.2
'@unocss/preset-wind@66.0.0':
'@unocss/preset-wind4@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/preset-wind3': 66.0.0
'@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/reset@66.0.0': {}
'@unocss/rule-utils@66.0.0':
'@unocss/reset@66.4.2': {}
'@unocss/rule-utils@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/core': 66.4.2
magic-string: 0.30.17
'@unocss/transformer-attributify-jsx@66.0.0':
'@unocss/transformer-attributify-jsx@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@babel/parser': 7.28.0
'@babel/traverse': 7.28.0
'@unocss/core': 66.4.2
transitivePeerDependencies:
- supports-color
'@unocss/transformer-compile-class@66.0.0':
'@unocss/transformer-compile-class@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/core': 66.4.2
'@unocss/transformer-directives@66.0.0':
'@unocss/transformer-directives@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/rule-utils': 66.0.0
'@unocss/core': 66.4.2
'@unocss/rule-utils': 66.4.2
css-tree: 3.1.0
'@unocss/transformer-variant-group@66.0.0':
'@unocss/transformer-variant-group@66.4.2':
dependencies:
'@unocss/core': 66.0.0
'@unocss/core': 66.4.2
'@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))':
'@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))':
dependencies:
'@ampproject/remapping': 2.3.0
'@unocss/config': 66.0.0
'@unocss/core': 66.0.0
'@unocss/inspector': 66.0.0(vue@3.5.13(typescript@5.7.3))
'@unocss/config': 66.4.2
'@unocss/core': 66.4.2
'@unocss/inspector': 66.4.2
chokidar: 3.6.0
magic-string: 0.30.17
tinyglobby: 0.2.12
pathe: 2.0.3
tinyglobby: 0.2.14
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
@@ -15771,9 +15812,11 @@ snapshots:
confbox@0.1.8: {}
confbox@0.2.2: {}
connect-history-api-fallback@2.0.0: {}
consola@3.4.0: {}
consola@3.4.2: {}
console.table@0.10.0:
dependencies:
@@ -17197,6 +17240,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
exsolve@1.0.7: {}
extend@3.0.2: {}
extendable-error@0.1.7: {}
@@ -17402,7 +17447,7 @@ snapshots:
'@actions/core': 1.11.1
arg: 5.0.2
console.table: 0.10.0
debug: 4.4.0
debug: 4.4.1(supports-color@8.1.1)
find-test-names: 1.29.5(@babel/core@7.27.1)
globby: 11.1.0
minimatch: 3.1.2
@@ -18303,7 +18348,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.4.0
debug: 4.4.1(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -18936,6 +18981,12 @@ 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
@@ -19615,7 +19666,7 @@ snapshots:
node-source-walk@7.0.0:
dependencies:
'@babel/parser': 7.27.2
'@babel/parser': 7.28.0
nomnom@1.5.2:
dependencies:
@@ -19873,6 +19924,8 @@ snapshots:
package-manager-detector@0.2.9: {}
package-manager-detector@1.3.0: {}
pako@1.0.11: {}
pako@2.1.0: {}
@@ -20047,6 +20100,12 @@ 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
@@ -20228,6 +20287,8 @@ snapshots:
dependencies:
side-channel: 1.1.0
quansync@0.2.10: {}
queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}
@@ -21017,7 +21078,7 @@ snapshots:
spdy@4.0.2:
dependencies:
debug: 4.4.0
debug: 4.4.1(supports-color@8.1.1)
handle-thing: 2.0.1
http-deceiver: 1.2.7
select-hose: 2.0.0
@@ -21397,6 +21458,8 @@ snapshots:
tinyexec@0.3.2: {}
tinyexec@1.0.1: {}
tinyglobby@0.2.12:
dependencies:
fdir: 6.4.3(picomatch@4.0.2)
@@ -21599,11 +21662,12 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
unconfig@7.0.0:
unconfig@7.3.2:
dependencies:
'@antfu/utils': 8.1.1
'@quansync/fs': 0.1.4
defu: 6.1.4
jiti: 2.4.2
quansync: 0.2.10
underscore@1.1.7: {}
@@ -21689,32 +21753,32 @@ snapshots:
universalify@2.0.1: {}
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)):
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)):
dependencies:
'@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))
'@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))
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: {}
@@ -22051,9 +22115,7 @@ snapshots:
vscode-uri@3.1.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-flow-layout@0.2.0: {}
vue@3.5.13(typescript@5.7.3):
dependencies: