Compare commits

..

4 Commits

Author SHA1 Message Date
omkarht
b7ff1920a9 fix: add ellipse shape to the shapes table in documentation
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-09 19:32:10 +05:30
omkarht
58c06ed770 fix: exclude generated files from circular dependency check
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-09 19:19:55 +05:30
omkarht
5e05f91b7d fix: remove TypeScript error suppression for broken ellipse rendering 2025-10-09 18:55:01 +05:30
omkarht
5fd06ccbac 6806: Adding support for the new use-case diagram type
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-09 18:48:11 +05:30
47 changed files with 5407 additions and 63 deletions

92
.build/antlr-cli.ts Normal file
View File

@@ -0,0 +1,92 @@
/* eslint-disable no-console */
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { resolve, dirname } from 'node:path';
import { readFile, mkdir, access } from 'node:fs/promises';
import { existsSync } from 'node:fs';
const execAsync = promisify(exec);
interface AntlrGrammarConfig {
id: string;
grammar: string;
outputDir: string;
language: string;
generateVisitor?: boolean;
generateListener?: boolean;
}
interface AntlrConfig {
projectName: string;
grammars: AntlrGrammarConfig[];
mode: string;
}
export async function generateFromConfig(configFile: string): Promise<void> {
const configPath = resolve(configFile);
if (!existsSync(configPath)) {
throw new Error(`ANTLR config file not found: ${configPath}`);
}
const configContent = await readFile(configPath, 'utf-8');
const config: AntlrConfig = JSON.parse(configContent);
const configDir = dirname(configPath);
for (const grammarConfig of config.grammars) {
await generateGrammar(grammarConfig, configDir);
}
}
async function generateGrammar(grammarConfig: AntlrGrammarConfig, baseDir: string): Promise<void> {
const grammarFile = resolve(baseDir, grammarConfig.grammar);
const outputDir = resolve(baseDir, grammarConfig.outputDir);
// Check if grammar file exists
try {
await access(grammarFile);
} catch {
throw new Error(`Grammar file not found: ${grammarFile}`);
}
// Ensure output directory exists
await mkdir(outputDir, { recursive: true });
// Build ANTLR command arguments
// eslint-disable-next-line @cspell/spellchecker
const args = ['antlr-ng', `-Dlanguage=${grammarConfig.language}`];
if (grammarConfig.generateVisitor) {
args.push('--generate-visitor');
}
if (grammarConfig.generateListener) {
args.push('--generate-listener');
}
args.push('-o', `"${outputDir}"`, `"${grammarFile}"`);
const command = `npx ${args.join(' ')}`;
try {
await execAsync(command);
console.log(`Generated ANTLR files for ${grammarConfig.id}`);
} catch (error) {
console.error(`Failed to generate ANTLR files for ${grammarConfig.id}:`);
throw error;
}
}
// CLI interface
if (import.meta.url === `file://${process.argv[1]}`) {
const configFile = process.argv[2] || './packages/parser/antlr-config.json';
try {
await generateFromConfig(configFile);
console.log('ANTLR generation completed successfully!');
} catch (error) {
console.error('ANTLR generation failed:', error.message);
}
}

5
.build/generateAntlr.ts Normal file
View File

@@ -0,0 +1,5 @@
import { generateFromConfig } from './antlr-cli.js';
export async function generateAntlr() {
await generateFromConfig('./packages/parser/antlr-config.json');
}

View File

@@ -28,6 +28,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'packet',
'architecture',
'radar',
'usecase',
] as const;
/**

View File

@@ -143,6 +143,9 @@ typeof
typestr
unshift
urlsafe
usecase
Usecase
USECASE
verifymethod
VERIFYMTHD
WARN_DOCSDIR_DOESNT_MATCH

View File

@@ -4,6 +4,7 @@ import { packageOptions } from '../.build/common.js';
import { generateLangium } from '../.build/generateLangium.js';
import type { MermaidBuildOptions } from './util.js';
import { defaultOptions, getBuildConfig } from './util.js';
import { generateAntlr } from '../.build/generateAntlr.js';
const shouldVisualize = process.argv.includes('--visualize');
@@ -95,6 +96,7 @@ const buildTinyMermaid = async () => {
const main = async () => {
await generateLangium();
await generateAntlr();
await mkdir('stats', { recursive: true });
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
// it should build `parser` before `mermaid` because it's a dependency

5
.gitignore vendored
View File

@@ -51,5 +51,8 @@ demos/dev/**
tsx-0/**
vite.config.ts.timestamp-*
# autogenereated by langium-cli
# autogenereated by langium-cli and antlr-cli
generated/
# autogenereated by antlr-cli
.antlr/

View File

@@ -10,6 +10,7 @@ import type { TemplateType } from 'rollup-plugin-visualizer/dist/plugin/template
import istanbul from 'vite-plugin-istanbul';
import { packageOptions } from '../.build/common.js';
import { generateLangium } from '../.build/generateLangium.js';
import { generateAntlr } from '../.build/generateAntlr.js';
const visualize = process.argv.includes('--visualize');
const watch = process.argv.includes('--watch');
@@ -123,6 +124,7 @@ const main = async () => {
};
await generateLangium();
await generateAntlr();
if (watch) {
await build(getBuildConfig({ minify: false, watch, core: false, entryName: 'parser' }));

View File

@@ -0,0 +1,425 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
describe('Usecase diagram', () => {
it('should render a simple usecase diagram with actors and use cases', () => {
imgSnapshotTest(
`
usecase
actor User
actor Admin
User --> Login
Admin --> "Manage Users"
User --> "View Profile"
`
);
});
it('should render usecase diagram with quoted actor names', () => {
imgSnapshotTest(
`usecase
actor "Customer Service"
actor "System Administrator"
"Customer Service" --> "Handle Tickets"
"System Administrator" --> "Manage System"
`
);
});
it('should render usecase diagram with different arrow types', () => {
imgSnapshotTest(
`usecase
actor User
actor Admin
User --> Login
Admin <-- Logout
User -- "View Data"
`
);
});
it('should render usecase diagram with edge labels', () => {
imgSnapshotTest(
`usecase
actor Developer
actor Manager
Developer --important--> "Write Code"
Manager --review--> "Code Review"
Developer --urgent--> Manager
`
);
});
it('should render usecase diagram with node ID syntax', () => {
imgSnapshotTest(
`usecase
actor User
User --> a(Login)
User --> b("View Profile")
User --> c("Update Settings")
`
);
});
it('should render usecase diagram with comma-separated actors', () => {
imgSnapshotTest(
`usecase
actor "Customer Service", "Technical Support", "Sales Team"
actor SystemAdmin
"Customer Service" --> "Handle Tickets"
"Technical Support" --> "Resolve Issues"
"Sales Team" --> "Process Orders"
SystemAdmin --> "Manage System"
`
);
});
it('should render usecase diagram with actor metadata', () => {
imgSnapshotTest(
`usecase
actor User@{ "type" : "primary", "icon" : "user" }
actor Admin@{ "type" : "secondary", "icon" : "admin" }
actor System@{ "type" : "hollow", "icon" : "system" }
User --> Login
Admin --> "Manage Users"
System --> "Process Data"
`
);
});
it('should render usecase diagram with system boundaries (rect type)', () => {
imgSnapshotTest(
`usecase
actor Admin, User
systemBoundary "Authentication"
Login
Logout
"Reset Password"
end
"Authentication"@{ type: rect }
Admin --> Login
User --> Login
User --> "Reset Password"
`
);
});
it('should render usecase diagram with system boundaries (package type)', () => {
imgSnapshotTest(
`usecase
actor Admin, User
systemBoundary "Authentication"
Login
Logout
"Reset Password"
end
"Authentication"@{ type: package }
Admin --> Login
User --> Login
User --> "Reset Password"
`
);
});
it('should render complex usecase diagram with all features', () => {
imgSnapshotTest(
`usecase
actor "Customer Service"@{ "type" : "primary", "icon" : "user" }
actor "System Admin"@{ "type" : "secondary", "icon" : "admin" }
actor "Database"@{ "type" : "hollow", "icon" : "database" }
systemBoundary "Customer Support System"
"Handle Tickets"
"View Customer Info"
end
"Customer Support System"@{ type: package }
systemBoundary "Administration"
"User Management"
"System Config"
end
"Customer Service" --priority--> "Handle Tickets"
"Customer Service" --> "View Customer Info"
"System Admin" --manage--> "User Management"
"System Admin" --> "System Config"
"Database" <-- "Handle Tickets"
"Database" <-- "View Customer Info"
"Database" <-- "User Management"
`
);
});
it('should render usecase diagram with actor-to-actor relationships', () => {
imgSnapshotTest(
`usecase
actor Manager
actor Developer
actor Tester
Manager --supervises--> Developer
Manager --coordinates--> Tester
Developer --collaborates--> Tester
Developer --> "Write Code"
Tester --> "Test Code"
Manager --> "Review Progress"
`
);
});
it('should render usecase diagram with mixed relationship types', () => {
imgSnapshotTest(
`usecase
actor User
actor Admin
User --> "Basic Login"
Admin --> "Advanced Login"
User --includes--> "View Profile"
Admin --extends--> "Manage Profiles"
"Basic Login" <-- "Advanced Login"
`
);
});
it('should render usecase diagram with long labels and text wrapping', () => {
imgSnapshotTest(
`usecase
actor "Customer Service Representative"
actor "System Administrator with Extended Privileges"
"Customer Service Representative" --Process--> "Handle Complex Customer Support Tickets"
"System Administrator with Extended Privileges" --> "Manage System Configuration and User Permissions"
`
);
});
it('should render usecase diagram with special characters in names', () => {
imgSnapshotTest(
`usecase
actor "User@Company.com"
actor "Admin (Level-1)"
"User@Company.com" --> a("Login & Authenticate")
"Admin (Level-1)" --> b("Manage Users & Permissions")
`
);
});
it('should render usecase diagram when useMaxWidth is true (default)', () => {
renderGraph(
`usecase
actor User
actor Admin
User --> Login
Admin --> "Manage System"
User --> "View Profile"
`,
{ usecase: { useMaxWidth: true } }
);
cy.get('svg').should((svg) => {
expect(svg).to.have.attr('width', '100%');
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
});
});
it('should render usecase diagram when useMaxWidth is false', () => {
renderGraph(
`usecase
actor User
actor Admin
User --> Login
Admin --> "Manage System"
`,
{ usecase: { useMaxWidth: false } }
);
cy.get('svg').should((svg) => {
const width = parseFloat(svg.attr('width'));
expect(width).to.be.greaterThan(200);
expect(svg).to.not.have.attr('style');
});
});
it('should render empty usecase diagram', () => {
imgSnapshotTest(`usecase`);
});
it('should render usecase diagram with only actors', () => {
imgSnapshotTest(
`usecase
actor User
actor Admin
actor Guest
`
);
});
it('should render usecase diagram with implicit use case creation', () => {
imgSnapshotTest(
`usecase
actor User
User --> Login
User --> Register
User --> "Forgot Password"
`
);
});
it('should render usecase diagram with nested system boundaries', () => {
imgSnapshotTest(
`usecase
actor User
actor Admin
systemBoundary "Main System"
Login
Logout
"Create User"
"Delete User"
end
User --> Login
User --> Logout
Admin --> "Create User"
Admin --> "Delete User"
`
);
});
it('should render usecase diagram with multiple edge labels on same relationship', () => {
imgSnapshotTest(
`usecase
actor Developer
actor Manager
Developer --"code review"--> Manager
Developer --"status update"--> Manager
Manager --"feedback"--> Developer
Manager --"approval"--> Developer
`
);
});
it('should render usecase diagram with various actor icon types', () => {
imgSnapshotTest(
`usecase
actor User@{ "icon": "user" }
actor Admin@{ "icon": "admin" }
actor Database@{ "icon": "database" }
actor API@{ "icon": "api" }
actor Mobile@{ "icon": "mobile" }
actor Web@{ "icon": "web" }
User --> "Access System"
Admin --> "Manage System"
Database --> "Store Data"
API --> "Provide Services"
Mobile --> "Mobile Access"
Web --> "Web Access"
`
);
});
it('should render usecase diagram with mixed arrow directions and labels', () => {
imgSnapshotTest(
`usecase
actor User
actor System
actor Admin
User --request--> System
System --response--> User
System <--monitor-- Admin
Admin --configure--> System
User -- "direct access" -- Admin
`
);
});
it('should render usecase diagram with boundary-less use cases', () => {
imgSnapshotTest(
`usecase
actor User
actor Admin
systemBoundary "Secure Area"
"Admin Panel"
"User Management"
end
User --> "Public Login"
User --> "Guest Access"
Admin --> "Public Login"
Admin --> "Admin Panel"
Admin --> "User Management"
`
);
});
it('should render usecase diagram with complex metadata combinations', () => {
imgSnapshotTest(
`usecase
actor "Primary User"@{ "type": "primary", "icon": "user", "fillColor": "lightblue" }
actor "Secondary User"@{ "type": "secondary", "icon": "client", "strokeColor": "red" }
actor "System Service"@{ "type": "hollow", "icon": "service", "strokeWidth": "3" }
"Primary User" --"high priority"--> a("Critical Process")
"Secondary User" --"low priority"--> b("Background Task")
"System Service" --"automated"--> c("System Maintenance")
`
);
});
it('should render usecase diagram with Unicode characters', () => {
imgSnapshotTest(
`usecase
actor "用户"@{ "icon": "user" }
actor "管理员"@{ "icon": "admin" }
"用户" --"登录"--> "系统访问"
"管理员" --"管理"--> "用户管理"
"用户" --> "数据查看"
`
);
});
it('should render large usecase diagram with many elements', () => {
imgSnapshotTest(
`usecase
actor User1, User2, User3, User4
actor Admin1, Admin2
actor System1@{ "icon": "system" }
actor System2@{ "icon": "database" }
systemBoundary "Module A"
"Feature A1"
"Feature A2"
"Admin A1"
end
"Module A"@{ type: package }
systemBoundary "Module B"
"Feature B1"
"Feature B2"
"Admin B1"
end
User1 --> "Feature A1"
User2 --> "Feature A2"
Admin1 --> "Admin A1"
User3 --> "Feature B1"
User4 --> "Feature B2"
Admin2 --> "Admin B1"
System1 <-- "Feature A1"
System1 <-- "Feature B1"
System2 <-- "Admin A1"
System2 <-- "Admin B1"
User1 --"collaborates"--> User2
Admin1 --"supervises"--> Admin2
`
);
});
});

234
demos/usecase.html Normal file
View File

@@ -0,0 +1,234 @@
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
rel="stylesheet"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Recursive:wght@300..1000&display=swap"
rel="stylesheet"
/>
<style>
.recursive-500 {
font-family: 'Recursive', serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings:
'slnt' 0,
'CASL' 0,
'CRSV' 0.5,
'MONO' 0;
}
body {
/* background: rgb(221, 208, 208); */
/* background: #333; */
/* font-family: 'Arial'; */
font-family: 'Recursive', serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings:
'slnt' 0,
'CASL' 0,
'CRSV' 0.5,
'MONO' 0;
/* color: white; */
/* font-size: 18px !important; */
}
.gridify.tiny {
background-image:
linear-gradient(transparent 11px, rgba(220, 220, 200, 0.8) 12px, transparent 12px),
linear-gradient(90deg, transparent 11px, rgba(220, 220, 200, 0.8) 12px, transparent 12px);
background-size:
100% 12px,
12px 100%;
}
.gridify.dots {
background-image: radial-gradient(
circle at center,
rgba(220, 220, 200, 0.8) 1px,
transparent 1px
);
background-size: 24px 24px;
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
font-size: 16px !important;
font-family: 'Recursive', serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings:
'slnt' 0,
'CASL' 0,
'CRSV' 0.5,
'MONO' 0;
}
pre {
width: 100%;
/*box-shadow: 4px 4px 0px 0px #0000000F;*/
}
</style>
</head>
<body class="gridify dots">
<p class="mb-20">Test Diagram</p>
<div class="w-full h-64">
<pre id="diagram4" class="mermaid">
usecase
direction LR
actor User1, User2, User3, User4
actor Admin1, Admin2
actor System1@{ "icon": "bell" }
actor System2@{ "icon": "database" }
systemBoundary "Module A"
"Feature A1"
"Feature A2"
"Admin A1"
end
"Module A"@{ type: package }
systemBoundary "Module B"
"Feature B1"
"Feature B2"
"Admin B1"
end
User1 --important--> "Feature A1"
User2 --> "Feature A2"
Admin1 --> "Admin A1"
User3 --> "Feature B1"
User4 --> "Feature B2"
Admin2 --> "Admin B1"
System1 <-- "Feature A1"
System1 <-- "Feature B1"
System2 <-- "Admin A1"
System2 <-- "Admin B1"
User1 --"collaborates"--> User2
Admin1 --"supervises"--> Admin2
</pre
>
</div>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
const staticBellIconPack = {
prefix: 'fa6-regular',
icons: {
bell: {
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
width: 448,
},
},
width: 512,
height: 512,
};
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()),
},
{
name: 'fa',
loader: () =>
fetch('https://unpkg.com/@iconify-json/fa6-solid/icons.json').then((res) => res.json()),
},
]);
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
window.callback = function () {
alert('A callback was triggered');
};
function callback() {
alert('It worked');
}
await mermaid.initialize({
startOnLoad: false,
theme: 'default',
// theme: 'forest',
// handDrawnSeed: 12,
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
layout: 'dagre',
// layout: 'elk',
// layout: 'fixed',
// htmlLabels: false,
flowchart: { titleTopMargin: 10 },
// fontFamily: 'Caveat',
// fontFamily: 'Kalam',
// fontFamily: 'courier',
fontFamily: 'Recursive',
sequence: {
actorFontFamily: 'courier',
noteFontFamily: 'courier',
messageFontFamily: 'courier',
},
kanban: {
htmlLabels: false,
},
fontSize: 16,
logLevel: 0,
securityLevel: 'loose',
callback,
});
// setTimeout(() => {
mermaid.init(undefined, document.querySelectorAll('.mermaid'));
// }, 1000);
mermaid.parseError = function (err, hash) {
console.error('In parse error:');
console.error(err);
};
</script>
</body>
</html>

View File

@@ -12,4 +12,4 @@
> `const` **configKeys**: `Set`<`string`>
Defined in: [packages/mermaid/src/defaultConfig.ts:292](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L292)
Defined in: [packages/mermaid/src/defaultConfig.ts:295](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L295)

View File

@@ -105,7 +105,7 @@ You can set this attribute to base the seed on a static string.
> `optional` **dompurifyConfig**: `Config`
Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
---
@@ -179,7 +179,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
> `optional` **fontSize**: `number`
Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
---
@@ -292,7 +292,7 @@ Defines which main look to use for the diagram.
> `optional` **markdownAutoWrap**: `boolean`
Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
Defined in: [packages/mermaid/src/config.type.ts:217](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L217)
---
@@ -424,7 +424,7 @@ Defined in: [packages/mermaid/src/config.type.ts:198](https://github.com/mermaid
> `optional` **suppressErrorRendering**: `boolean`
Defined in: [packages/mermaid/src/config.type.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L222)
Defined in: [packages/mermaid/src/config.type.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223)
Suppresses inserting 'Syntax error' diagram in the DOM.
This is useful when you want to control how to handle syntax errors in your application.
@@ -466,11 +466,19 @@ Defined in: [packages/mermaid/src/config.type.ts:196](https://github.com/mermaid
---
### usecase?
> `optional` **usecase**: `UsecaseDiagramConfig`
Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
---
### wrap?
> `optional` **wrap**: `boolean`
Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
---

View File

@@ -344,6 +344,7 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
| Display | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` |
| Divided Process | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-process`, `divided-rectangle` |
| Document | Document | `doc` | Represents a document | `doc`, `document` |
| Ellipse | Ellipse | `ellipse` | Ellipse shape | `oval` |
| Event | Rounded Rectangle | `rounded` | Represents an event | `event` |
| Extract | Triangle | `tri` | Extraction process | `extract`, `triangle` |
| Fork/Join | Filled Rectangle | `fork` | Fork or join in process flow | `join` |

View File

@@ -0,0 +1,51 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'usecase',
name: 'Use Case Diagram',
description: 'Visualize system functionality and user interactions',
examples: [
{
title: 'Basic Use Case',
isDefault: true,
code: `usecase
actor User
actor Admin
User --> (Login)
User --> (View Profile)
Admin --> (Manage Users)
Admin --> (View Reports)`,
},
{
title: 'System Boundary',
code: `usecase
actor Customer
actor Support
SystemBoundary@{ type: rect } "E-commerce System" {
Customer --> (Browse Products)
Customer --> (Place Order)
Customer --> (Track Order)
}
SystemBoundary@{ type: package } "Admin Panel" {
Support --> (Process Orders)
Support --> (Handle Returns)
}`,
},
{
title: 'Actor Relationships',
code: `usecase
actor Developer1
actor Developer2
actor Manager
Developer1 --> (Write Code)
Developer2 --> (Review Code)
Manager --> (Approve Release)
Developer1 --> Developer2
Manager --> Developer1`,
},
],
} satisfies DiagramMetadata;

View File

@@ -21,6 +21,7 @@ import quadrantChart from './examples/quadrant-chart.js';
import packetDiagram from './examples/packet.js';
import blockDiagram from './examples/block.js';
import treemapDiagram from './examples/treemap.js';
import usecaseDiagram from './examples/usecase.js';
export const diagramData: DiagramMetadata[] = [
flowChart,
@@ -45,4 +46,5 @@ export const diagramData: DiagramMetadata[] = [
packetDiagram,
blockDiagram,
treemapDiagram,
usecaseDiagram,
];

View File

@@ -47,7 +47,7 @@
"docs:verify-version": "tsx scripts/update-release-version.mts --verify",
"types:build-config": "tsx scripts/create-types-from-json-schema.mts",
"types:verify-config": "tsx scripts/create-types-from-json-schema.mts --verify",
"checkCircle": "npx madge --circular ./src",
"checkCircle": "npx madge --circular ./src --exclude '.*generated.*'",
"prepublishOnly": "pnpm docs:verify-version"
},
"repository": {

View File

@@ -189,6 +189,7 @@ This Markdown should be kept.
| Display | Curved Trapezoid | \`curv-trap\` | Represents a display | \`curved-trapezoid\`, \`display\` |
| Divided Process | Divided Rectangle | \`div-rect\` | Divided process shape | \`div-proc\`, \`divided-process\`, \`divided-rectangle\` |
| Document | Document | \`doc\` | Represents a document | \`doc\`, \`document\` |
| Ellipse | Ellipse | \`ellipse\` | Ellipse shape | \`oval\` |
| Event | Rounded Rectangle | \`rounded\` | Represents an event | \`event\` |
| Extract | Triangle | \`tri\` | Extraction process | \`extract\`, \`triangle\` |
| Fork/Join | Filled Rectangle | \`fork\` | Fork or join in process flow | \`join\` |

View File

@@ -210,6 +210,7 @@ export interface MermaidConfig {
packet?: PacketDiagramConfig;
block?: BlockDiagramConfig;
radar?: RadarDiagramConfig;
usecase?: UsecaseDiagramConfig;
dompurifyConfig?: DOMPurifyConfiguration;
wrap?: boolean;
fontSize?: number;
@@ -1623,6 +1624,50 @@ export interface RadarDiagramConfig extends BaseDiagramConfig {
*/
curveTension?: number;
}
/**
* The object containing configurations specific for usecase diagrams.
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "UsecaseDiagramConfig".
*/
export interface UsecaseDiagramConfig extends BaseDiagramConfig {
/**
* Font size for actor labels
*/
actorFontSize?: number;
/**
* Font family for actor labels
*/
actorFontFamily?: string;
/**
* Font weight for actor labels
*/
actorFontWeight?: string;
/**
* Font size for usecase labels
*/
usecaseFontSize?: number;
/**
* Font family for usecase labels
*/
usecaseFontFamily?: string;
/**
* Font weight for usecase labels
*/
usecaseFontWeight?: string;
/**
* Margin around actors
*/
actorMargin?: number;
/**
* Margin around use cases
*/
usecaseMargin?: number;
/**
* Padding around the entire diagram
*/
diagramPadding?: number;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "FontConfig".

View File

@@ -264,6 +264,9 @@ const config: RequiredDeep<MermaidConfig> = {
radar: {
...defaultConfigJson.radar,
},
usecase: {
...defaultConfigJson.usecase,
},
treemap: {
useMaxWidth: true,
padding: 10,

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

@@ -962,7 +962,6 @@ You have to call mermaid.initialize.`
case 'round':
return 'roundedRect';
case 'ellipse':
// @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976
return 'ellipse';
default:
return vertex.type;

View File

@@ -0,0 +1,77 @@
const getStyles = (options: any) =>
`
.actor {
stroke: ${options.primaryColor};
fill: ${options.primaryColor};
}
.actor-label {
fill: ${options.primaryTextColor};
font-family: ${options.fontFamily};
font-size: 14px;
font-weight: normal;
}
.usecase {
stroke: ${options.primaryColor};
fill: ${options.primaryColor};
}
.usecase-label {
fill: ${options.primaryTextColor};
font-family: ${options.fontFamily};
font-size: 12px;
font-weight: normal;
}
/* Ellipse shape styling for use cases */
.usecase-element ellipse {
fill: ${options.mainBkg ?? '#ffffff'};
stroke: ${options.primaryColor};
stroke-width: 2px;
}
.usecase-element .label {
fill: ${options.primaryTextColor};
font-family: ${options.fontFamily};
font-size: 12px;
font-weight: normal;
text-anchor: middle;
dominant-baseline: central;
}
/* General ellipse styling */
.node ellipse {
fill: ${options.mainBkg ?? '#ffffff'};
stroke: ${options.nodeBorder ?? options.primaryColor};
stroke-width: 1px;
}
.relationship {
stroke: ${options.lineColor};
fill: none;
}
& .marker {
fill: ${options.lineColor};
stroke: ${options.lineColor};
}
.relationship-label {
fill: ${options.primaryTextColor};
font-family: ${options.fontFamily};
font-size: 10px;
font-weight: normal;
}
.nodeLabel, .edgeLabel {
color: ${options.classText};
}
.system-boundary {
fill: ${options.clusterBkg};
stroke: ${options.clusterBorder};
stroke-width: 1px;
}
`;
export default getStyles;

View File

@@ -0,0 +1,98 @@
import { vi } from 'vitest';
import { setSiteConfig } from '../../diagram-api/diagramAPI.js';
import mermaidAPI from '../../mermaidAPI.js';
import { Diagram } from '../../Diagram.js';
import { addDiagrams } from '../../diagram-api/diagram-orchestration.js';
beforeAll(async () => {
// Is required to load the useCase diagram
await Diagram.fromText('usecase\n actor TestActor');
});
/**
* UseCase diagrams require a basic d3 mock for rendering
*/
vi.mock('d3', () => {
const NewD3 = function (this: any) {
function returnThis(this: any) {
return this;
}
return {
append: function () {
return NewD3();
},
lower: returnThis,
attr: returnThis,
style: returnThis,
text: returnThis,
getBBox: function () {
return {
height: 10,
width: 20,
};
},
};
};
return {
select: function () {
return new (NewD3 as any)();
},
selectAll: function () {
return new (NewD3 as any)();
},
// TODO: In d3 these are CurveFactory types, not strings
curveBasis: 'basis',
curveBasisClosed: 'basisClosed',
curveBasisOpen: 'basisOpen',
curveBumpX: 'bumpX',
curveBumpY: 'bumpY',
curveBundle: 'bundle',
curveCardinalClosed: 'cardinalClosed',
curveCardinalOpen: 'cardinalOpen',
curveCardinal: 'cardinal',
curveCatmullRomClosed: 'catmullRomClosed',
curveCatmullRomOpen: 'catmullRomOpen',
curveCatmullRom: 'catmullRom',
curveLinear: 'linear',
curveLinearClosed: 'linearClosed',
curveMonotoneX: 'monotoneX',
curveMonotoneY: 'monotoneY',
curveNatural: 'natural',
curveStep: 'step',
curveStepAfter: 'stepAfter',
curveStepBefore: 'stepBefore',
};
});
// -------------------------------
addDiagrams();
/**
* @param conf - Configuration object
* @param key - Configuration key
* @param value - Configuration value
*/
function addConf(conf: any, key: any, value: any) {
if (value !== undefined) {
conf[key] = value;
}
return conf;
}
describe('UseCase diagram with ANTLR parser', () => {
it('should parse actors and use cases correctly', async () => {
const diagram = await Diagram.fromText(
`usecase
actor Developer1
actor Developer2
Developer1 --> a("Login System")
Developer2 --> b(Authentication)`
);
expect(diagram).toBeDefined();
expect(diagram.type).toBe('usecase');
});
});

View File

@@ -0,0 +1,306 @@
import { log } from '../../logger.js';
import {
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
} from '../common/commonDb.js';
import type {
UsecaseFields,
UsecaseDB,
Actor,
UseCase,
SystemBoundary,
Relationship,
ActorMetadata,
Direction,
} from './usecaseTypes.js';
import { DEFAULT_DIRECTION } from './usecaseTypes.js';
import type { RequiredDeep } from 'type-fest';
import type { UsecaseDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import { getConfig as getGlobalConfig } from '../../diagram-api/diagramAPI.js';
import type { LayoutData, Node, ClusterNode, Edge } from '../../rendering-util/types.js';
export const DEFAULT_USECASE_CONFIG: Required<UsecaseDiagramConfig> = DEFAULT_CONFIG.usecase;
export const DEFAULT_USECASE_DB: RequiredDeep<UsecaseFields> = {
actors: new Map(),
useCases: new Map(),
systemBoundaries: new Map(),
relationships: [],
direction: DEFAULT_DIRECTION,
config: DEFAULT_USECASE_CONFIG,
} as const;
let actors = new Map<string, Actor>();
let useCases = new Map<string, UseCase>();
let systemBoundaries = new Map<string, SystemBoundary>();
let relationships: Relationship[] = [];
let direction: Direction = DEFAULT_DIRECTION;
const config: Required<UsecaseDiagramConfig> = structuredClone(DEFAULT_USECASE_CONFIG);
const getConfig = (): Required<UsecaseDiagramConfig> => structuredClone(config);
const clear = (): void => {
actors = new Map();
useCases = new Map();
systemBoundaries = new Map();
relationships = [];
direction = DEFAULT_DIRECTION;
commonClear();
};
// Actor management
const addActor = (actor: Actor): void => {
if (!actor.id || !actor.name) {
throw new Error(
`Invalid actor: Actor must have both id and name. Received: ${JSON.stringify(actor)}`
);
}
if (!actors.has(actor.id)) {
actors.set(actor.id, actor);
log.debug(`Added actor: ${actor.id} (${actor.name})`);
} else {
log.debug(`Actor ${actor.id} already exists`);
}
};
const getActors = (): Map<string, Actor> => actors;
const getActor = (id: string): Actor | undefined => actors.get(id);
// UseCase management
const addUseCase = (useCase: UseCase): void => {
if (!useCase.id || !useCase.name) {
throw new Error(
`Invalid use case: Use case must have both id and name. Received: ${JSON.stringify(useCase)}`
);
}
if (!useCases.has(useCase.id)) {
useCases.set(useCase.id, useCase);
log.debug(`Added use case: ${useCase.id} (${useCase.name})`);
} else {
log.debug(`Use case ${useCase.id} already exists`);
}
};
const getUseCases = (): Map<string, UseCase> => useCases;
const getUseCase = (id: string): UseCase | undefined => useCases.get(id);
// SystemBoundary management
const addSystemBoundary = (systemBoundary: SystemBoundary): void => {
if (!systemBoundary.id || !systemBoundary.name) {
throw new Error(
`Invalid system boundary: System boundary must have both id and name. Received: ${JSON.stringify(systemBoundary)}`
);
}
if (!systemBoundaries.has(systemBoundary.id)) {
systemBoundaries.set(systemBoundary.id, systemBoundary);
log.debug(`Added system boundary: ${systemBoundary.name}`);
} else {
log.debug(`System boundary ${systemBoundary.id} already exists`);
}
};
const getSystemBoundaries = (): Map<string, SystemBoundary> => systemBoundaries;
const getSystemBoundary = (id: string): SystemBoundary | undefined => systemBoundaries.get(id);
// Relationship management
const addRelationship = (relationship: Relationship): void => {
// Validate relationship structure
if (!relationship.id || !relationship.from || !relationship.to) {
throw new Error(
`Invalid relationship: Relationship must have id, from, and to fields. Received: ${JSON.stringify(relationship)}`
);
}
if (!relationship.type) {
throw new Error(
`Invalid relationship: Relationship must have a type. Received: ${JSON.stringify(relationship)}`
);
}
// Validate relationship type
const validTypes = ['association', 'include', 'extend'];
if (!validTypes.includes(relationship.type)) {
throw new Error(
`Invalid relationship type: ${relationship.type}. Valid types are: ${validTypes.join(', ')}`
);
}
// Validate arrow type if present
if (relationship.arrowType !== undefined) {
const validArrowTypes = [0, 1, 2]; // SOLID_ARROW, BACK_ARROW, LINE_SOLID
if (!validArrowTypes.includes(relationship.arrowType)) {
throw new Error(
`Invalid arrow type: ${relationship.arrowType}. Valid arrow types are: ${validArrowTypes.join(', ')}`
);
}
}
relationships.push(relationship);
log.debug(
`Added relationship: ${relationship.from} -> ${relationship.to} (${relationship.type})`
);
};
const getRelationships = (): Relationship[] => relationships;
// Direction management
const setDirection = (dir: Direction): void => {
// Normalize TD to TB (same as flowchart)
if (dir === 'TD') {
direction = 'TB';
} else {
direction = dir;
}
log.debug('Direction set to:', direction);
};
const getDirection = (): Direction => direction;
// Convert usecase diagram data to LayoutData format for unified rendering
const getData = (): LayoutData => {
const globalConfig = getGlobalConfig();
const nodes: Node[] = [];
const edges: Edge[] = [];
// Convert actors to nodes
for (const actor of actors.values()) {
const node: Node = {
id: actor.id,
label: actor.name,
description: actor.description ? [actor.description] : undefined,
shape: 'usecaseActor', // Use custom actor shape
isGroup: false,
padding: 10,
look: globalConfig.look,
// Add metadata as data attributes for styling
cssClasses: `usecase-actor ${
actor.metadata && Object.keys(actor.metadata).length > 0
? Object.entries(actor.metadata)
.map(([key, value]) => `actor-${key}-${value}`)
.join(' ')
: ''
}`.trim(),
// Pass actor metadata to the shape handler
metadata: actor.metadata,
} as Node & { metadata?: ActorMetadata };
nodes.push(node);
}
// Convert use cases to nodes
for (const useCase of useCases.values()) {
const node: Node = {
id: useCase.id,
label: useCase.name,
description: useCase.description ? [useCase.description] : undefined,
shape: 'ellipse', // Use ellipse shape for use cases
isGroup: false,
padding: 10,
look: globalConfig.look,
cssClasses: 'usecase-element',
// If use case belongs to a system boundary, set parentId
...(useCase.systemBoundary && { parentId: useCase.systemBoundary }),
};
nodes.push(node);
}
// Convert system boundaries to group nodes
for (const boundary of systemBoundaries.values()) {
const node: ClusterNode & { boundaryType?: string } = {
id: boundary.id,
label: boundary.name,
shape: 'usecaseSystemBoundary', // Use custom usecase system boundary cluster shape
isGroup: true, // System boundaries are clusters (containers for other nodes)
padding: 20,
look: globalConfig.look,
cssClasses: `system-boundary system-boundary-${boundary.type ?? 'rect'}`,
// Pass boundary type to the shape handler
boundaryType: boundary.type,
};
nodes.push(node);
}
// Convert relationships to edges
relationships.forEach((relationship, index) => {
const edge: Edge = {
id: relationship.id || `edge-${index}`,
start: relationship.from,
end: relationship.to,
source: relationship.from,
target: relationship.to,
label: relationship.label,
type: relationship.type,
arrowTypeEnd:
relationship.arrowType === 0
? 'arrow_point' // Forward arrow (-->)
: 'none', // No end arrow for back arrow or line
arrowTypeStart:
relationship.arrowType === 1
? 'arrow_point' // Back arrow (<--)
: 'none', // No start arrow for forward arrow or line
classes: `relationship relationship-${relationship.type}`,
look: globalConfig.look,
thickness: 'normal',
pattern: 'solid',
};
edges.push(edge);
});
return {
nodes,
edges,
config: globalConfig,
// Additional properties that layout algorithms might expect
type: 'usecase',
layoutAlgorithm: 'dagre', // Default layout algorithm
direction: getDirection(), // Use the current direction setting
nodeSpacing: 50, // Default node spacing
rankSpacing: 50, // Default rank spacing
markers: ['arrow_point'], // Arrow point markers used in usecase diagrams
};
};
export const db: UsecaseDB = {
getConfig,
clear,
setDiagramTitle,
getDiagramTitle,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
addActor,
getActors,
getActor,
addUseCase,
getUseCases,
getUseCase,
addSystemBoundary,
getSystemBoundaries,
getSystemBoundary,
addRelationship,
getRelationships,
// Direction management
setDirection,
getDirection,
// Add getData method for unified rendering
getData,
};

View File

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

View File

@@ -0,0 +1,12 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { parser } from './usecaseParser.js';
import { db } from './usecaseDb.js';
import { renderer } from './usecaseRenderer.js';
import styles from './styles.js';
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
styles,
};

View File

@@ -0,0 +1,157 @@
// Import ANTLR parser from the parser package
import { parse } from '@mermaid-js/parser';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import type {
UsecaseDB,
Actor,
UseCase,
SystemBoundary,
Relationship,
ArrowType,
} from './usecaseTypes.js';
import { db } from './usecaseDb.js';
// ANTLR parser result interface
interface UsecaseParseResult {
actors: { id: string; name: string; metadata?: Record<string, string> }[];
useCases: { id: string; name: string; nodeId?: string; systemBoundary?: string }[];
systemBoundaries: { id: string; name: string; useCases: string[]; type?: 'package' | 'rect' }[];
relationships: {
id: string;
from: string;
to: string;
type: 'association' | 'include' | 'extend';
arrowType: number;
label?: string;
}[];
direction?: string;
accDescr?: string;
accTitle?: string;
title?: string;
}
/**
* Parse usecase diagram using ANTLR parser
*/
const parseUsecaseWithAntlr = async (input: string): Promise<UsecaseParseResult> => {
// Use the ANTLR parser from @mermaid-js/parser
const result = (await parse('usecase', input)) as UsecaseParseResult;
return result;
};
/**
* Populate the database with parsed ANTLR results
*/
const populateDb = (ast: UsecaseParseResult, db: UsecaseDB) => {
// Clear existing data
db.clear();
// Add actors (ANTLR result already has id, name, and metadata)
ast.actors.forEach((actorData) => {
const actor: Actor = {
id: actorData.id,
name: actorData.name,
metadata: actorData.metadata,
};
db.addActor(actor);
});
// Add use cases (ANTLR result already has id, name, nodeId, and systemBoundary)
ast.useCases.forEach((useCaseData) => {
const useCase: UseCase = {
id: useCaseData.id,
name: useCaseData.name,
nodeId: useCaseData.nodeId,
systemBoundary: useCaseData.systemBoundary,
};
db.addUseCase(useCase);
});
// Add system boundaries
if (ast.systemBoundaries) {
ast.systemBoundaries.forEach((boundaryData) => {
const systemBoundary: SystemBoundary = {
id: boundaryData.id,
name: boundaryData.name,
useCases: boundaryData.useCases,
type: boundaryData.type || 'rect', // default to 'rect' if not specified
};
db.addSystemBoundary(systemBoundary);
});
}
// Add relationships (ANTLR result already has proper structure)
ast.relationships.forEach((relationshipData) => {
const relationship: Relationship = {
id: relationshipData.id,
from: relationshipData.from,
to: relationshipData.to,
type: relationshipData.type,
arrowType: relationshipData.arrowType as ArrowType,
label: relationshipData.label,
};
db.addRelationship(relationship);
});
// Set direction if provided
if (ast.direction) {
db.setDirection(ast.direction as any);
}
log.debug('Populated usecase database:', {
actors: ast.actors.length,
useCases: ast.useCases.length,
relationships: ast.relationships.length,
direction: ast.direction,
});
};
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
log.debug('Parsing usecase diagram with ANTLR:', input);
try {
// Use our ANTLR parser
const ast: UsecaseParseResult = await parseUsecaseWithAntlr(input);
log.debug('ANTLR parsing result:', ast);
// Populate common database fields
populateCommonDb(ast as any, db);
// Populate the database with validation
populateDb(ast, db);
log.debug('Usecase diagram parsing completed successfully');
} catch (error) {
log.error('Error parsing usecase diagram:', error);
// Check if it's a UsecaseParseError from our ANTLR parser
if (
error &&
typeof error === 'object' &&
'name' in error &&
error.name === 'UsecaseParseError'
) {
// Re-throw the detailed error for better error reporting
throw error;
}
// For other errors, wrap them in a generic error
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const wrappedError = new Error(`Failed to parse usecase diagram: ${errorMessage}`);
// Add hash property for consistency with other diagram types
(wrappedError as any).hash = {
text: input.split('\n')[0] || '',
token: 'unknown',
line: '1',
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
expected: ['valid usecase syntax'],
};
throw wrappedError;
}
},
};

View File

@@ -0,0 +1,48 @@
import type { DrawDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import utils from '../../utils.js';
import type { UsecaseDB } from './usecaseTypes.js';
/**
* Main draw function using unified rendering system
*/
const draw: DrawDefinition = async (_text, id, _version, diag) => {
log.info('Drawing usecase diagram (unified)', id);
const { securityLevel, usecase: conf, layout } = getConfig();
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
// into the Layout data format
const usecaseDb = diag.db as UsecaseDB;
const data4Layout = usecaseDb.getData();
// Create the root SVG - the element is the div containing the SVG element
const svg = getDiagramElement(id, securityLevel);
data4Layout.type = diag.type;
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
data4Layout.nodeSpacing = 50; // Default node spacing
data4Layout.rankSpacing = 50; // Default rank spacing
data4Layout.markers = ['point']; // Use point markers for usecase diagrams
data4Layout.diagramId = id;
log.debug('Usecase layout data:', data4Layout);
// Use the unified rendering system
await render(data4Layout, svg);
const padding = 8;
utils.insertTitle(
svg,
'usecaseDiagramTitleText',
0, // Default title top margin
usecaseDb.getDiagramTitle?.() ?? ''
);
setupViewPortForSVG(svg, padding, 'usecaseDiagram', conf?.useMaxWidth ?? false);
};
export const renderer = { draw };

View File

@@ -0,0 +1,40 @@
const getStyles = (options: any) =>
`
.actor {
stroke: ${options.primaryColor};
fill: ${options.primaryColor};
}
.actor-label {
fill: ${options.primaryTextColor};
font-family: ${options.fontFamily};
font-size: 14px;
font-weight: normal;
}
.usecase {
stroke: ${options.primaryColor};
fill: ${options.primaryColor};
}
.usecase-label {
fill: ${options.primaryTextColor};
font-family: ${options.fontFamily};
font-size: 12px;
font-weight: normal;
}
.relationship {
stroke: ${options.primaryColor};
fill: ${options.primaryColor};
}
.relationship-label {
fill: ${options.primaryTextColor};
font-family: ${options.fontFamily};
font-size: 10px;
font-weight: normal;
}
`;
export default getStyles;

View File

@@ -0,0 +1,94 @@
import type { DiagramDB } from '../../diagram-api/types.js';
import type { UsecaseDiagramConfig } from '../../config.type.js';
import type { LayoutData } from '../../rendering-util/types.js';
export type ActorMetadata = Record<string, string>;
export interface Actor {
id: string;
name: string;
description?: string;
metadata?: ActorMetadata;
}
export interface UseCase {
id: string;
name: string;
description?: string;
nodeId?: string; // Optional node ID (e.g., 'a' in 'a(Go through code)')
systemBoundary?: string; // Optional reference to system boundary
}
export type SystemBoundaryType = 'package' | 'rect';
export interface SystemBoundary {
id: string;
name: string;
useCases: string[]; // Array of use case IDs within this boundary
type?: SystemBoundaryType; // Type of boundary rendering (default: 'rect')
}
// Arrow types for usecase diagrams (matching parser types)
export const ARROW_TYPE = {
SOLID_ARROW: 0, // -->
BACK_ARROW: 1, // <--
LINE_SOLID: 2, // --
} as const;
export type ArrowType = (typeof ARROW_TYPE)[keyof typeof ARROW_TYPE];
export interface Relationship {
id: string;
from: string;
to: string;
type: 'association' | 'include' | 'extend';
arrowType: ArrowType;
label?: string;
}
// Direction types for usecase diagrams
export type Direction = 'TB' | 'TD' | 'BT' | 'RL' | 'LR';
export const DEFAULT_DIRECTION: Direction = 'TB';
export interface UsecaseFields {
actors: Map<string, Actor>;
useCases: Map<string, UseCase>;
systemBoundaries: Map<string, SystemBoundary>;
relationships: Relationship[];
direction: Direction;
config: Required<UsecaseDiagramConfig>;
}
export interface UsecaseDB extends DiagramDB {
getConfig: () => Required<UsecaseDiagramConfig>;
// Actor management
addActor: (actor: Actor) => void;
getActors: () => Map<string, Actor>;
getActor: (id: string) => Actor | undefined;
// UseCase management
addUseCase: (useCase: UseCase) => void;
getUseCases: () => Map<string, UseCase>;
getUseCase: (id: string) => UseCase | undefined;
// SystemBoundary management
addSystemBoundary: (systemBoundary: SystemBoundary) => void;
getSystemBoundaries: () => Map<string, SystemBoundary>;
getSystemBoundary: (id: string) => SystemBoundary | undefined;
// Relationship management
addRelationship: (relationship: Relationship) => void;
getRelationships: () => Relationship[];
// Direction management
setDirection: (direction: Direction) => void;
getDirection: () => Direction;
// Unified rendering support
getData: () => LayoutData;
// Utility methods
clear: () => void;
}

View File

@@ -27,53 +27,6 @@ import { log } from '../../../logger.js';
import { getSubGraphTitleMargins } from '../../../utils/subGraphTitleMargins.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js';
/**
* Apply absolute note positioning after dagre layout
* This fixes the issue where TB and LR directions position notes differently
* by making note positioning truly absolute
*/
const positionNotes = (graph) => {
const noteStatePairs = [];
graph.nodes().forEach((nodeId) => {
const node = graph.node(nodeId);
if (node.position && node.shape === 'note') {
const edges = graph.nodeEdges(nodeId);
for (const edge of edges) {
const otherNodeId = edge.v === nodeId ? edge.w : edge.v;
const otherNode = graph.node(otherNodeId);
if (otherNode && otherNode.shape !== 'note' && otherNode.shape !== 'noteGroup') {
noteStatePairs.push({
noteId: nodeId,
noteNode: node,
stateId: otherNodeId,
stateNode: otherNode,
position: node.position,
});
}
}
}
});
noteStatePairs.forEach(({ noteNode, stateNode, position }) => {
const spacing = 60;
let noteX = noteNode.x;
let noteY = stateNode.y;
if (position === 'right of') {
noteX = stateNode.x + stateNode.width / 2 + spacing + noteNode.width / 2;
} else if (position === 'left of') {
noteX = stateNode.x - stateNode.width / 2 - spacing - noteNode.width / 2;
}
noteNode.x = noteX;
noteNode.y = noteY;
});
};
const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, siteConfig) => {
log.warn('Graph in recursive render:XAX', graphlibJson.write(graph), parentCluster);
const dir = graph.graph().rankdir;
@@ -211,9 +164,6 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
dagreLayout(graph);
// Apply absolute note positioning after dagre layout
positionNotes(graph);
log.info('Graph after layout:', JSON.stringify(graphlibJson.write(graph)));
// Move the nodes to the correct place
let diff = 0;

View File

@@ -459,6 +459,173 @@ const divider = (parent, node) => {
return { cluster: shapeSvg, labelBBox: {} };
};
/**
* Custom cluster shape for usecase system boundaries
* Supports two types: 'rect' (dashed rectangle) and 'package' (UML package notation)
* @param {any} parent
* @param {any} node
* @returns {any} ShapeSvg
*/
const usecaseSystemBoundary = async (parent, node) => {
log.info('Creating usecase system boundary for ', node.id, node);
const siteConfig = getConfig();
const { handDrawnSeed } = siteConfig;
// Add outer g element
const shapeSvg = parent
.insert('g')
.attr('class', 'cluster usecase-system-boundary ' + node.cssClasses)
.attr('id', node.id)
.attr('data-look', node.look);
// Get boundary type from node metadata (default to 'rect')
const boundaryType = node.boundaryType || 'rect';
shapeSvg.attr('data-boundary-type', boundaryType);
const useHtmlLabels = evaluate(siteConfig.flowchart?.htmlLabels);
// Create the label
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label');
const text = await createText(labelEl, node.label, {
style: node.labelStyle,
useHtmlLabels,
isNode: true,
});
// Get the size of the label
let bbox = text.getBBox();
if (evaluate(siteConfig.flowchart?.htmlLabels)) {
const div = text.children[0];
const dv = select(text);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
// Calculate width with padding (similar to rect cluster)
const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;
if (node.width <= bbox.width + node.padding) {
node.diff = (width - node.width) / 2 - node.padding;
} else {
node.diff = -node.padding;
}
const height = node.height;
// Use absolute coordinates from layout engine (like rect cluster does)
const x = node.x - width / 2;
const y = node.y - height / 2;
let boundaryRect;
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
if (boundaryType === 'package') {
// Draw package-type boundary (rectangle with separate name box at top)
const nameBoxWidth = Math.max(80, bbox.width + 20);
const nameBoxHeight = 25;
if (node.look === 'handDrawn') {
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {
stroke: 'black',
strokeWidth: 2,
fill: 'none',
seed: handDrawnSeed,
});
// Draw main boundary rectangle
const roughRect = rc.rectangle(x, y, width, height, options);
boundaryRect = shapeSvg.insert(() => roughRect, ':first-child');
// Draw name box at top-left
const roughNameBox = rc.rectangle(x, y - nameBoxHeight, nameBoxWidth, nameBoxHeight, options);
shapeSvg.insert(() => roughNameBox, ':first-child');
} else {
// Draw main boundary rectangle
boundaryRect = shapeSvg
.insert('rect', ':first-child')
.attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('stroke-width', 2);
// Draw name box at top-left
shapeSvg
.insert('rect', ':first-child')
.attr('x', x)
.attr('y', y - nameBoxHeight)
.attr('width', nameBoxWidth)
.attr('height', nameBoxHeight)
.attr('fill', 'white')
.attr('stroke', 'black')
.attr('stroke-width', 2);
}
// Position label in the center of the name box (using absolute coordinates)
// The name box is at (x, y - nameBoxHeight), so center the label there
labelEl.attr(
'transform',
`translate(${x + nameBoxWidth / 2 - bbox.width / 2}, ${y - nameBoxHeight})`
);
} else {
// Draw rect-type boundary (simple dashed rectangle)
if (node.look === 'handDrawn') {
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {
stroke: 'black',
strokeWidth: 2,
fill: 'none',
strokeLineDash: [5, 5],
seed: handDrawnSeed,
});
const roughRect = rc.rectangle(x, y, width, height, options);
boundaryRect = shapeSvg.insert(() => roughRect, ':first-child');
} else {
// Draw dashed rectangle
boundaryRect = shapeSvg
.insert('rect', ':first-child')
.attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '5,5');
}
// Position label at top-left (using absolute coordinates, same as rect cluster)
labelEl.attr(
'transform',
`translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
);
}
// Get the bounding box of the boundary rectangle
const rectBox = boundaryRect.node().getBBox();
// Set node properties required by layout engine (similar to rect cluster)
node.offsetX = 0;
node.width = rectBox.width;
node.height = rectBox.height;
// Used by layout engine to position subgraph in parent
node.offsetY = bbox.height - node.padding / 2;
// Set intersection function for edge routing
node.intersect = function (point) {
return intersectRect(node, point);
};
// Return cluster object
return {
cluster: shapeSvg,
labelBBox: bbox,
};
};
const squareRect = rect;
const shapes = {
rect,
@@ -467,6 +634,7 @@ const shapes = {
noteGroup,
divider,
kanbanSection,
usecaseSystemBoundary,
};
let clusterElems = new Map();

View File

@@ -14,6 +14,7 @@ import { curvedTrapezoid } from './shapes/curvedTrapezoid.js';
import { cylinder } from './shapes/cylinder.js';
import { dividedRectangle } from './shapes/dividedRect.js';
import { doublecircle } from './shapes/doubleCircle.js';
import { ellipse } from './shapes/ellipse.js';
import { filledCircle } from './shapes/filledCircle.js';
import { flippedTriangle } from './shapes/flippedTriangle.js';
import { forkJoin } from './shapes/forkJoin.js';
@@ -32,6 +33,7 @@ import { lean_right } from './shapes/leanRight.js';
import { lightningBolt } from './shapes/lightningBolt.js';
import { linedCylinder } from './shapes/linedCylinder.js';
import { linedWaveEdgedRect } from './shapes/linedWaveEdgedRect.js';
import { usecaseActor } from './shapes/usecaseActor.js';
import { multiRect } from './shapes/multiRect.js';
import { multiWaveEdgedRectangle } from './shapes/multiWaveEdgedRectangle.js';
import { note } from './shapes/note.js';
@@ -115,6 +117,14 @@ export const shapesDefs = [
aliases: ['terminal', 'pill'],
handler: stadium,
},
{
semanticName: 'Ellipse',
name: 'Ellipse',
shortName: 'ellipse',
description: 'Ellipse shape',
aliases: ['oval'],
handler: ellipse,
},
{
semanticName: 'Subprocess',
name: 'Framed Rectangle',
@@ -507,6 +517,9 @@ const generateShapeMap = () => {
// Requirement diagram
requirementBox,
// Usecase diagram
usecaseActor,
} as const;
const entries = [

View File

@@ -0,0 +1,60 @@
import rough from 'roughjs';
import type { Bounds, D3Selection, Point } from '../../../types.js';
import type { Node } from '../../types.js';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
export async function ellipse<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node));
// Calculate ellipse dimensions with padding
const padding = halfPadding ?? 10;
const radiusX = bbox.width / 2 + padding * 2;
const radiusY = bbox.height / 2 + padding;
let ellipseElem;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const roughNode = rc.ellipse(0, 0, radiusX * 2, radiusY * 2, options);
ellipseElem = shapeSvg.insert(() => roughNode, ':first-child');
ellipseElem.attr('class', 'basic label-container');
if (cssStyles) {
ellipseElem.attr('style', cssStyles);
}
} else {
ellipseElem = shapeSvg
.insert('ellipse', ':first-child')
.attr('class', 'basic label-container')
.attr('style', nodeStyles)
.attr('rx', radiusX)
.attr('ry', radiusY)
.attr('cx', 0)
.attr('cy', 0);
}
node.width = radiusX * 2;
node.height = radiusY * 2;
updateNodeBounds(node, ellipseElem);
node.calcIntersect = function (bounds: Bounds, point: Point) {
const rx = bounds.width / 2;
const ry = bounds.height / 2;
return intersect.ellipse(bounds, rx, ry, point);
};
node.intersect = function (point) {
return intersect.ellipse(node, radiusX, radiusY, point);
};
return shapeSvg;
}

View File

@@ -0,0 +1,245 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getIconSVG } from '../../icons.js';
import rough from 'roughjs';
import type { D3Selection } from '../../../types.js';
/**
* Get actor styling based on metadata
*/
const getActorStyling = (metadata?: Record<string, string>) => {
const defaults = {
fillColor: 'none',
strokeColor: 'black',
strokeWidth: 2,
type: 'solid',
};
if (!metadata) {
return defaults;
}
return {
fillColor: metadata.type === 'hollow' ? 'none' : metadata.fillColor || defaults.fillColor,
strokeColor: metadata.strokeColor || defaults.strokeColor,
strokeWidth: parseInt(metadata.strokeWidth || '2', 10),
type: metadata.type || defaults.type,
};
};
/**
* Draw traditional stick figure
*/
const drawStickFigure = (
actorGroup: D3Selection<SVGGElement>,
styling: ReturnType<typeof getActorStyling>,
node: Node
): void => {
const x = 0; // Center at origin
const y = -10; // Adjust vertical position
if (node.look === 'handDrawn') {
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(actorGroup);
const options = userNodeOverrides(node, {
stroke: styling.strokeColor,
strokeWidth: styling.strokeWidth,
fill: styling.fillColor,
});
// Head (circle)
const head = rc.circle(x, y - 30, 16, options);
actorGroup.insert(() => head, ':first-child');
// Body (line)
const body = rc.line(x, y - 22, x, y + 10, options);
actorGroup.insert(() => body, ':first-child');
// Arms (line)
const arms = rc.line(x - 15, y - 10, x + 15, y - 10, options);
actorGroup.insert(() => arms, ':first-child');
// Left leg
const leftLeg = rc.line(x, y + 10, x - 15, y + 30, options);
actorGroup.insert(() => leftLeg, ':first-child');
// Right leg
const rightLeg = rc.line(x, y + 10, x + 15, y + 30, options);
actorGroup.insert(() => rightLeg, ':first-child');
} else {
// Head (circle)
actorGroup
.append('circle')
.attr('cx', x)
.attr('cy', y - 30)
.attr('r', 8)
.attr('fill', styling.fillColor)
.attr('stroke', styling.strokeColor)
.attr('stroke-width', styling.strokeWidth);
// Body (line)
actorGroup
.append('line')
.attr('x1', x)
.attr('y1', y - 22)
.attr('x2', x)
.attr('y2', y + 10)
.attr('stroke', styling.strokeColor)
.attr('stroke-width', styling.strokeWidth);
// Arms (line)
actorGroup
.append('line')
.attr('x1', x - 15)
.attr('y1', y - 10)
.attr('x2', x + 15)
.attr('y2', y - 10)
.attr('stroke', styling.strokeColor)
.attr('stroke-width', styling.strokeWidth);
// Left leg
actorGroup
.append('line')
.attr('x1', x)
.attr('y1', y + 10)
.attr('x2', x - 15)
.attr('y2', y + 30)
.attr('stroke', styling.strokeColor)
.attr('stroke-width', styling.strokeWidth);
// Right leg
actorGroup
.append('line')
.attr('x1', x)
.attr('y1', y + 10)
.attr('x2', x + 15)
.attr('y2', y + 30)
.attr('stroke', styling.strokeColor)
.attr('stroke-width', styling.strokeWidth);
}
};
/**
* Draw actor with icon representation
*/
const drawActorWithIcon = async (
actorGroup: D3Selection<SVGGElement>,
iconName: string,
styling: ReturnType<typeof getActorStyling>,
node: Node
): Promise<void> => {
const x = 0; // Center at origin
const y = -10; // Adjust vertical position
const iconSize = 50; // Icon size
if (node.look === 'handDrawn') {
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(actorGroup);
const options = userNodeOverrides(node, {
stroke: styling.strokeColor,
strokeWidth: styling.strokeWidth,
fill: styling.fillColor === 'none' ? 'white' : styling.fillColor,
});
// Create a rectangle background for the icon
const iconBg = rc.rectangle(x - 35, y - 40, 50, 50, options);
actorGroup.insert(() => iconBg, ':first-child');
} else {
// Create a rectangle background for the icon
actorGroup
.append('rect')
.attr('x', x - 27.5)
.attr('y', y - 42)
.attr('width', 55)
.attr('height', 55)
.attr('rx', 5)
.attr('fill', styling.fillColor === 'none' ? 'white' : styling.fillColor)
.attr('stroke', styling.strokeColor)
.attr('stroke-width', styling.strokeWidth);
}
// Add icon using getIconSVG (like iconCircle.ts does)
const iconElem = actorGroup.append('g').attr('class', 'actor-icon');
iconElem.html(
`<g>${await getIconSVG(iconName, {
height: iconSize,
width: iconSize,
fallbackPrefix: 'fa',
})}</g>`
);
// Get icon bounding box for positioning
const iconBBox = iconElem.node()?.getBBox();
if (iconBBox) {
const iconWidth = iconBBox.width;
const iconHeight = iconBBox.height;
const iconX = iconBBox.x;
const iconY = iconBBox.y;
// Center the icon in the rectangle
iconElem.attr(
'transform',
`translate(${-iconWidth / 2 - iconX}, ${y - 15 - iconHeight / 2 - iconY})`
);
}
};
/**
* Custom shape handler for usecase actors
*/
export async function usecaseActor<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node
) {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
// Actor dimensions
const actorWidth = 80;
const actorHeight = 70; // Height for the stick figure part
// Get actor metadata from node
const metadata = (node as Node & { metadata?: Record<string, string> }).metadata;
const styling = getActorStyling(metadata);
// Create actor group
const actorGroup = shapeSvg.append('g').attr('class', 'usecase-actor-shape');
// Add metadata as data attributes for CSS styling
if (metadata) {
Object.entries(metadata).forEach(([key, value]) => {
actorGroup.attr(`data-${key}`, value);
});
}
// Check if we should render an icon instead of stick figure
if (metadata?.icon) {
await drawActorWithIcon(actorGroup, metadata.icon, styling, node);
} else {
drawStickFigure(actorGroup, styling, node);
}
// Actor name (always rendered below the figure)
const labelY = actorHeight / 2 + 15; // Position label below the figure
// Calculate label height from the actual text element
const labelBBox = label.node()?.getBBox() ?? { height: 20 };
const labelHeight = labelBBox.height + 10; // Space for label below
const totalHeight = actorHeight + labelHeight;
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${labelY / 2})`
);
// Update node bounds for layout
updateNodeBounds(node, actorGroup);
// Set explicit dimensions for layout algorithm
node.width = actorWidth;
node.height = totalHeight;
return shapeSvg;
}

View File

@@ -56,6 +56,7 @@ required:
- block
- look
- radar
- usecase
properties:
theme:
description: |
@@ -310,6 +311,8 @@ properties:
$ref: '#/$defs/BlockDiagramConfig'
radar:
$ref: '#/$defs/RadarDiagramConfig'
usecase:
$ref: '#/$defs/UsecaseDiagramConfig'
dompurifyConfig:
title: DOM Purify Configuration
description: Configuration options to pass to the `dompurify` library.
@@ -2329,6 +2332,57 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
maximum: 1
default: 0.17
UsecaseDiagramConfig:
title: Usecase Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
description: The object containing configurations specific for usecase diagrams.
type: object
unevaluatedProperties: false
required:
- useMaxWidth
properties:
actorFontSize:
description: Font size for actor labels
type: number
minimum: 1
default: 14
actorFontFamily:
description: Font family for actor labels
type: string
default: '"Open Sans", sans-serif'
actorFontWeight:
description: Font weight for actor labels
type: string
default: 'normal'
usecaseFontSize:
description: Font size for usecase labels
type: number
minimum: 1
default: 12
usecaseFontFamily:
description: Font family for usecase labels
type: string
default: '"Open Sans", sans-serif'
usecaseFontWeight:
description: Font weight for usecase labels
type: string
default: 'normal'
actorMargin:
description: Margin around actors
type: number
minimum: 0
default: 50
usecaseMargin:
description: Margin around use cases
type: number
minimum: 0
default: 50
diagramPadding:
description: Padding around the entire diagram
type: number
minimum: 0
default: 20
FontCalculator:
title: Font Calculator
description: |

View File

@@ -0,0 +1,14 @@
{
"projectName": "Mermaid",
"grammars": [
{
"id": "usecase",
"grammar": "src/language/usecase/Usecase.g4",
"outputDir": "src/language/usecase/generated",
"language": "TypeScript",
"generateVisitor": true,
"generateListener": true
}
],
"mode": "production"
}

View File

@@ -19,7 +19,8 @@
"scripts": {
"clean": "rimraf dist src/language/generated",
"langium:generate": "langium generate",
"langium:watch": "langium generate --watch"
"langium:watch": "langium generate --watch",
"antlr:generate": "tsx ../../.build/antlr-cli.ts antlr-config.json"
},
"repository": {
"type": "git",
@@ -33,9 +34,11 @@
"ast"
],
"dependencies": {
"antlr4ng": "^3.0.7",
"langium": "3.3.1"
},
"devDependencies": {
"antlr-ng": "^1.0.10",
"chevrotain": "^11.0.3"
},
"files": [

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';

View File

@@ -0,0 +1,183 @@
grammar Usecase;
// Parser rules
usecaseDiagram
: 'usecase' NEWLINE* statement* EOF
;
statement
: actorStatement
| relationshipStatement
| systemBoundaryStatement
| systemBoundaryTypeStatement
| directionStatement
| NEWLINE
;
actorStatement
: 'actor' actorList NEWLINE*
;
actorList
: actorName (',' actorName)*
;
actorName
: (IDENTIFIER | STRING) metadata?
;
metadata
: '@' '{' metadataContent '}'
;
metadataContent
: metadataProperty (',' metadataProperty)*
;
metadataProperty
: STRING ':' STRING
;
relationshipStatement
: entityName arrow entityName NEWLINE*
| actorDeclaration arrow entityName NEWLINE*
;
systemBoundaryStatement
: 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE*
;
systemBoundaryName
: IDENTIFIER
| STRING
;
systemBoundaryContent
: usecaseInBoundary NEWLINE*
| NEWLINE
;
usecaseInBoundary
: IDENTIFIER
| STRING
;
systemBoundaryTypeStatement
: systemBoundaryName '@' '{' systemBoundaryTypeContent '}' NEWLINE*
;
systemBoundaryTypeContent
: systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)*
;
systemBoundaryTypeProperty
: 'type' ':' systemBoundaryType
;
systemBoundaryType
: 'package'
| 'rect'
;
entityName
: IDENTIFIER
| STRING
| nodeIdWithLabel
;
actorDeclaration
: 'actor' actorName
;
nodeIdWithLabel
: IDENTIFIER '(' nodeLabel ')'
;
nodeLabel
: IDENTIFIER
| STRING
| nodeLabel IDENTIFIER
| nodeLabel STRING
;
arrow
: SOLID_ARROW
| BACK_ARROW
| LINE_SOLID
| labeledArrow
;
labeledArrow
: LINE_SOLID edgeLabel SOLID_ARROW
| BACK_ARROW edgeLabel LINE_SOLID
| LINE_SOLID edgeLabel LINE_SOLID
;
edgeLabel
: IDENTIFIER
| STRING
;
directionStatement
: 'direction' direction NEWLINE*
;
direction
: 'TB'
| 'TD'
| 'BT'
| 'RL'
| 'LR'
;
// Lexer rules
SOLID_ARROW
: '-->'
;
BACK_ARROW
: '<--'
;
LINE_SOLID
: '--'
;
COMMA
: ','
;
AT
: '@'
;
LBRACE
: '{'
;
RBRACE
: '}'
;
COLON
: ':'
;
IDENTIFIER
: [a-zA-Z_][a-zA-Z0-9_]*
;
STRING
: '"' (~["\r\n])* '"'
| '\'' (~['\r\n])* '\''
;
NEWLINE
: [\r\n]+
;
WS
: [ \t]+ -> skip
;

View File

@@ -0,0 +1,4 @@
export * from './module.js';
export * from './types.js';
export * from './parser.js';
export * from './visitor.js';

View File

@@ -0,0 +1,50 @@
/**
* ANTLR UseCase Module
*
* This module provides dependency injection and service creation
* for the ANTLR-based UseCase parser, following the Langium pattern.
*/
import type { AntlrUsecaseServices } from './types.js';
import { UsecaseAntlrParser } from './parser.js';
import { UsecaseAntlrVisitor } from './visitor.js';
/**
* ANTLR UseCase Module for dependency injection
*/
export const AntlrUsecaseModule = {
parser: () => new UsecaseAntlrParser(),
visitor: () => new UsecaseAntlrVisitor(),
};
/**
* Create the full set of ANTLR UseCase services
*
* This follows the Langium pattern but for ANTLR services
*
* @returns An object with ANTLR UseCase services
*/
export function createAntlrUsecaseServices(): AntlrUsecaseServices {
const parser = new UsecaseAntlrParser();
const visitor = new UsecaseAntlrVisitor();
return {
parser,
visitor,
};
}
/**
* Singleton instance of ANTLR UseCase services
*/
let antlrUsecaseServices: AntlrUsecaseServices | undefined;
/**
* Get or create the singleton ANTLR UseCase services
*/
export function getAntlrUsecaseServices(): AntlrUsecaseServices {
if (!antlrUsecaseServices) {
antlrUsecaseServices = createAntlrUsecaseServices();
}
return antlrUsecaseServices;
}

View File

@@ -0,0 +1,194 @@
/**
* True ANTLR Parser Implementation for UseCase Diagrams
*
* This parser uses the actual ANTLR-generated files from Usecase.g4
* and implements the visitor pattern to build the AST.
*/
import { CharStream, CommonTokenStream, BaseErrorListener } from 'antlr4ng';
import type { RecognitionException, Recognizer } from 'antlr4ng';
import { UsecaseLexer } from './generated/UsecaseLexer.js';
import { UsecaseParser } from './generated/UsecaseParser.js';
import { UsecaseAntlrVisitor } from './visitor.js';
import type { AntlrUsecaseParser, UsecaseParseResult } from './types.js';
/**
* Custom error listener for ANTLR parser to capture syntax errors
*/
class UsecaseErrorListener extends BaseErrorListener {
private errors: string[] = [];
syntaxError(
_recognizer: Recognizer<any>,
_offendingSymbol: any,
line: number,
charPositionInLine: number,
message: string,
_e: RecognitionException | null
): void {
const errorMsg = `Syntax error at line ${line}:${charPositionInLine} - ${message}`;
this.errors.push(errorMsg);
}
reportAmbiguity(): void {
// Optional: handle ambiguity reports
}
reportAttemptingFullContext(): void {
// Optional: handle full context attempts
}
reportContextSensitivity(): void {
// Optional: handle context sensitivity reports
}
getErrors(): string[] {
return this.errors;
}
hasErrors(): boolean {
return this.errors.length > 0;
}
clear(): void {
this.errors = [];
}
}
/**
* Custom error class for usecase parsing errors
*/
export class UsecaseParseError extends Error {
public line?: number;
public column?: number;
public token?: string;
public expected?: string[];
public hash?: Record<string, any>;
constructor(
message: string,
details?: {
line?: number;
column?: number;
token?: string;
expected?: string[];
}
) {
super(message);
this.name = 'UsecaseParseError';
this.line = details?.line;
this.column = details?.column;
this.token = details?.token;
this.expected = details?.expected;
// Create hash object similar to other diagram types
this.hash = {
text: details?.token ?? '',
token: details?.token ?? '',
line: details?.line?.toString() ?? '1',
loc: {
first_line: details?.line ?? 1,
last_line: details?.line ?? 1,
first_column: details?.column ?? 1,
last_column: (details?.column ?? 1) + (details?.token?.length ?? 0),
},
expected: details?.expected ?? [],
};
}
}
/**
* ANTLR-based UseCase parser implementation
*/
export class UsecaseAntlrParser implements AntlrUsecaseParser {
private visitor: UsecaseAntlrVisitor;
private errorListener: UsecaseErrorListener;
constructor() {
this.visitor = new UsecaseAntlrVisitor();
this.errorListener = new UsecaseErrorListener();
}
/**
* Parse UseCase diagram input using true ANTLR parsing
*
* @param input - The UseCase diagram text to parse
* @returns Parsed result with actors, use cases, and relationships
* @throws UsecaseParseError when syntax errors are encountered
*/
parse(input: string): UsecaseParseResult {
// Clear previous errors
this.errorListener.clear();
try {
// Step 1: Create ANTLR input stream
const chars = CharStream.fromString(input);
// Step 2: Create lexer from generated ANTLR lexer
const lexer = new UsecaseLexer(chars);
// Add error listener to lexer
lexer.removeErrorListeners();
lexer.addErrorListener(this.errorListener);
// Step 3: Create token stream
const tokens = new CommonTokenStream(lexer);
// Step 4: Create parser from generated ANTLR parser
const parser = new UsecaseParser(tokens);
// Add error listener to parser
parser.removeErrorListeners();
parser.addErrorListener(this.errorListener);
// Step 5: Parse using the grammar rule: usecaseDiagram
const tree = parser.usecaseDiagram();
// Check for syntax errors before proceeding
if (this.errorListener.hasErrors()) {
const errors = this.errorListener.getErrors();
throw new UsecaseParseError(`Syntax error in usecase diagram: ${errors.join('; ')}`, {
token: 'unknown',
expected: ['valid usecase syntax'],
});
}
// Step 6: Visit the parse tree using our visitor
this.visitor.visitUsecaseDiagram!(tree);
// Step 7: Get the parse result
return this.visitor.getParseResult();
} catch (error) {
if (error instanceof UsecaseParseError) {
throw error;
}
// Handle other types of errors
throw new UsecaseParseError(
`Failed to parse usecase diagram: ${error instanceof Error ? error.message : 'Unknown error'}`,
{
token: 'unknown',
expected: ['valid usecase syntax'],
}
);
}
}
}
/**
* Factory function to create a new ANTLR UseCase parser
*/
export function createUsecaseAntlrParser(): AntlrUsecaseParser {
return new UsecaseAntlrParser();
}
/**
* Convenience function for parsing UseCase diagrams
*
* @param input - The UseCase diagram text to parse
* @returns Parsed result with actors, use cases, and relationships
*/
export function parseUsecaseWithAntlr(input: string): UsecaseParseResult {
const parser = createUsecaseAntlrParser();
return parser.parse(input);
}

View File

@@ -0,0 +1,71 @@
/**
* Type definitions for ANTLR UseCase parser
*/
// Arrow types for usecase diagrams (similar to sequence diagram LINETYPE)
export const ARROW_TYPE = {
SOLID_ARROW: 0, // -->
BACK_ARROW: 1, // <--
LINE_SOLID: 2, // --
} as const;
export type ArrowType = (typeof ARROW_TYPE)[keyof typeof ARROW_TYPE];
export type ActorMetadata = Record<string, string>;
export interface Actor {
id: string;
name: string;
metadata?: ActorMetadata;
}
export interface UseCase {
id: string;
name: string;
nodeId?: string; // Optional node ID (e.g., 'a' in 'a(Go through code)')
systemBoundary?: string; // Optional reference to system boundary
}
export type SystemBoundaryType = 'package' | 'rect';
export interface SystemBoundary {
id: string;
name: string;
useCases: string[]; // Array of use case IDs within this boundary
type?: SystemBoundaryType; // Type of boundary rendering (default: 'rect')
}
export interface Relationship {
id: string;
from: string;
to: string;
type: 'association' | 'include' | 'extend';
arrowType: ArrowType;
label?: string;
}
export interface UsecaseParseResult {
actors: Actor[];
useCases: UseCase[];
systemBoundaries: SystemBoundary[];
relationships: Relationship[];
direction?: string;
accDescr?: string;
accTitle?: string;
title?: string;
}
/**
* ANTLR Parser Services interface
*/
export interface AntlrUsecaseServices {
parser: AntlrUsecaseParser;
visitor: any; // UsecaseAntlrVisitor - using any to avoid circular dependency
}
/**
* ANTLR Parser interface
*/
export interface AntlrUsecaseParser {
parse(input: string): UsecaseParseResult;
}

View File

@@ -0,0 +1,637 @@
/**
* ANTLR Visitor Implementation for UseCase Diagrams
*
* This visitor traverses the ANTLR parse tree and builds the AST
* according to the grammar rules defined in Usecase.g4
*/
import { UsecaseVisitor } from './generated/UsecaseVisitor.js';
import type {
UsecaseDiagramContext,
StatementContext,
ActorStatementContext,
ActorListContext,
RelationshipStatementContext,
SystemBoundaryStatementContext,
SystemBoundaryTypeStatementContext,
SystemBoundaryNameContext,
SystemBoundaryTypeContentContext,
SystemBoundaryTypePropertyContext,
SystemBoundaryTypeContext,
UsecaseInBoundaryContext,
ActorNameContext,
ActorDeclarationContext,
NodeIdWithLabelContext,
NodeLabelContext,
MetadataContext,
MetadataContentContext,
MetadataPropertyContext,
EntityNameContext,
ArrowContext,
LabeledArrowContext,
EdgeLabelContext,
DirectionStatementContext,
DirectionContext,
} from './generated/UsecaseParser.js';
import { ARROW_TYPE } from './types.js';
import type {
Actor,
UseCase,
SystemBoundary,
Relationship,
UsecaseParseResult,
ArrowType,
} from './types.js';
export class UsecaseAntlrVisitor extends UsecaseVisitor<void> {
private actors: Actor[] = [];
private useCases: UseCase[] = [];
private systemBoundaries: SystemBoundary[] = [];
private relationships: Relationship[] = [];
private relationshipCounter = 0;
private direction = 'TB'; // Default direction
constructor() {
super();
// Assign visitor functions as properties
this.visitUsecaseDiagram = this.visitUsecaseDiagramImpl.bind(this);
this.visitStatement = this.visitStatementImpl.bind(this);
this.visitActorStatement = this.visitActorStatementImpl.bind(this);
this.visitRelationshipStatement = this.visitRelationshipStatementImpl.bind(this);
this.visitSystemBoundaryStatement = this.visitSystemBoundaryStatementImpl.bind(this);
this.visitSystemBoundaryTypeStatement = this.visitSystemBoundaryTypeStatementImpl.bind(this);
this.visitDirectionStatement = this.visitDirectionStatementImpl.bind(this);
this.visitActorName = this.visitActorNameImpl.bind(this);
this.visitArrow = this.visitArrowImpl.bind(this);
}
/**
* Visit the root usecaseDiagram rule
* Grammar: usecaseDiagram : 'usecase' statement* EOF ;
*/
visitUsecaseDiagramImpl(ctx: UsecaseDiagramContext): void {
// Reset state
this.actors = [];
this.useCases = [];
this.relationships = [];
this.relationshipCounter = 0;
this.direction = 'TB'; // Reset direction to default
// Visit all statement children
if (ctx.statement) {
const statements = Array.isArray(ctx.statement()) ? ctx.statement() : [ctx.statement()];
for (const statementCtx of statements) {
if (Array.isArray(statementCtx)) {
for (const stmt of statementCtx) {
this.visitStatementImpl(stmt);
}
} else {
this.visitStatementImpl(statementCtx);
}
}
}
}
/**
* Visit statement rule
* Grammar: statement : actorStatement | relationshipStatement | systemBoundaryStatement | systemBoundaryTypeStatement | directionStatement | NEWLINE ;
*/
private visitStatementImpl(ctx: StatementContext): void {
if (ctx.actorStatement?.()) {
this.visitActorStatementImpl(ctx.actorStatement()!);
} else if (ctx.relationshipStatement?.()) {
this.visitRelationshipStatementImpl(ctx.relationshipStatement()!);
} else if (ctx.systemBoundaryStatement?.()) {
this.visitSystemBoundaryStatementImpl(ctx.systemBoundaryStatement()!);
} else if (ctx.systemBoundaryTypeStatement?.()) {
this.visitSystemBoundaryTypeStatementImpl(ctx.systemBoundaryTypeStatement()!);
} else if (ctx.directionStatement?.()) {
this.visitDirectionStatementImpl(ctx.directionStatement()!);
}
// NEWLINE is ignored
}
/**
* Visit actorStatement rule
* Grammar: actorStatement : 'actor' actorList NEWLINE* ;
*/
visitActorStatementImpl(ctx: ActorStatementContext): void {
if (ctx.actorList?.()) {
this.visitActorListImpl(ctx.actorList());
}
}
/**
* Visit actorList rule
* Grammar: actorList : actorName (',' actorName)* ;
*/
visitActorListImpl(ctx: ActorListContext): void {
// Get all actorName contexts from the list
const actorNameContexts = ctx.actorName();
for (const actorNameCtx of actorNameContexts) {
const actorResult = this.visitActorNameImpl(actorNameCtx);
this.actors.push({
id: actorResult.name,
name: actorResult.name,
metadata: actorResult.metadata,
});
}
}
/**
* Visit relationshipStatement rule
* Grammar: relationshipStatement : entityName arrow entityName NEWLINE* | actorDeclaration arrow entityName NEWLINE* ;
*/
visitRelationshipStatementImpl(ctx: RelationshipStatementContext): void {
let from = '';
let to = '';
// Handle different relationship patterns
if (ctx.actorDeclaration?.()) {
// Pattern: actor ActorName --> entityName
from = this.visitActorDeclarationImpl(ctx.actorDeclaration()!);
to = this.visitEntityNameImpl(ctx.entityName(0)!);
} else if (ctx.entityName && ctx.entityName().length >= 2) {
// Pattern: entityName --> entityName
from = this.visitEntityNameImpl(ctx.entityName(0)!);
to = this.visitEntityNameImpl(ctx.entityName(1)!);
}
// Get arrow information (type and optional label)
const arrowInfo = this.visitArrowImpl(ctx.arrow());
// Auto-create use cases for entities that are not actors
this.ensureUseCaseExists(from);
this.ensureUseCaseExists(to);
const relationship: Relationship = {
id: `rel_${this.relationshipCounter++}`,
from,
to,
type: 'association',
arrowType: arrowInfo.arrowType,
};
// Add label if present
if (arrowInfo.label) {
relationship.label = arrowInfo.label;
}
this.relationships.push(relationship);
}
/**
* Ensure a use case exists for the given entity name if it's not an actor
*/
private ensureUseCaseExists(entityName: string): void {
// Check if it's already an actor
const isActor = this.actors.some((actor) => actor.id === entityName);
// If it's not an actor, create it as a use case (if not already exists)
if (!isActor) {
const existingUseCase = this.useCases.some((useCase) => useCase.id === entityName);
if (!existingUseCase) {
this.useCases.push({
id: entityName,
name: entityName,
});
}
}
}
/**
* Visit systemBoundaryStatement rule
* Grammar: systemBoundaryStatement : 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE* ;
*/
visitSystemBoundaryStatementImpl(ctx: SystemBoundaryStatementContext): void {
let boundaryName = '';
// Get the system boundary name
if (ctx.systemBoundaryName?.()) {
boundaryName = this.visitSystemBoundaryNameImpl(ctx.systemBoundaryName());
}
// Collect use cases within this boundary
const useCasesInBoundary: string[] = [];
if (ctx.systemBoundaryContent?.()?.length > 0) {
for (const contentCtx of ctx.systemBoundaryContent()) {
const usecaseInBoundary = contentCtx.usecaseInBoundary?.();
if (usecaseInBoundary) {
const useCaseName = this.visitUsecaseInBoundaryImpl(usecaseInBoundary);
useCasesInBoundary.push(useCaseName);
// Create the use case and mark it as being in this boundary
const existingUseCase = this.useCases.find((uc) => uc.id === useCaseName);
if (existingUseCase) {
existingUseCase.systemBoundary = boundaryName;
} else {
this.useCases.push({
id: useCaseName,
name: useCaseName,
systemBoundary: boundaryName,
});
}
}
}
}
// Create the system boundary with default type
this.systemBoundaries.push({
id: boundaryName,
name: boundaryName,
useCases: useCasesInBoundary,
type: 'rect', // default type
});
}
/**
* Visit systemBoundaryName rule
* Grammar: systemBoundaryName : IDENTIFIER | STRING ;
*/
private visitSystemBoundaryNameImpl(ctx: SystemBoundaryNameContext): string {
const identifier = ctx.IDENTIFIER?.();
if (identifier) {
return identifier.getText();
}
const string = ctx.STRING?.();
if (string) {
const text = string.getText();
// Remove quotes from string
return text.slice(1, -1);
}
return '';
}
/**
* Visit usecaseInBoundary rule
* Grammar: usecaseInBoundary : IDENTIFIER | STRING ;
*/
private visitUsecaseInBoundaryImpl(ctx: UsecaseInBoundaryContext): string {
const identifier = ctx.IDENTIFIER?.();
if (identifier) {
return identifier.getText();
}
const string = ctx.STRING?.();
if (string) {
const text = string.getText();
// Remove quotes from string
return text.slice(1, -1);
}
return '';
}
/**
* Visit systemBoundaryTypeStatement rule
* Grammar: systemBoundaryTypeStatement : systemBoundaryName '\@' '\{' systemBoundaryTypeContent '\}' NEWLINE* ;
*/
visitSystemBoundaryTypeStatementImpl(ctx: SystemBoundaryTypeStatementContext): void {
let boundaryName = '';
// Get the system boundary name
const systemBoundaryName = ctx.systemBoundaryName?.();
if (systemBoundaryName) {
boundaryName = this.visitSystemBoundaryNameImpl(systemBoundaryName);
}
// Get the type configuration
let boundaryType: 'package' | 'rect' = 'rect'; // default
const systemBoundaryTypeContent = ctx.systemBoundaryTypeContent?.();
if (systemBoundaryTypeContent) {
boundaryType = this.visitSystemBoundaryTypeContentImpl(systemBoundaryTypeContent);
}
// Find the existing system boundary and update its type
const existingBoundary = this.systemBoundaries.find((b) => b.id === boundaryName);
if (existingBoundary) {
existingBoundary.type = boundaryType;
}
}
/**
* Visit systemBoundaryTypeContent rule
* Grammar: systemBoundaryTypeContent : systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)* ;
*/
private visitSystemBoundaryTypeContentImpl(
ctx: SystemBoundaryTypeContentContext
): 'package' | 'rect' {
// Get all type properties
const typeProperties = ctx.systemBoundaryTypeProperty();
for (const propCtx of typeProperties) {
const type = this.visitSystemBoundaryTypePropertyImpl(propCtx);
if (type) {
return type;
}
}
return 'rect'; // default
}
/**
* Visit systemBoundaryTypeProperty rule
* Grammar: systemBoundaryTypeProperty : 'type' ':' systemBoundaryType ;
*/
private visitSystemBoundaryTypePropertyImpl(
ctx: SystemBoundaryTypePropertyContext
): 'package' | 'rect' | null {
const systemBoundaryType = ctx.systemBoundaryType?.();
if (systemBoundaryType) {
return this.visitSystemBoundaryTypeImpl(systemBoundaryType);
}
return null;
}
/**
* Visit systemBoundaryType rule
* Grammar: systemBoundaryType : 'package' | 'rect' ;
*/
private visitSystemBoundaryTypeImpl(ctx: SystemBoundaryTypeContext): 'package' | 'rect' {
const text = ctx.getText();
if (text === 'package') {
return 'package';
} else if (text === 'rect') {
return 'rect';
}
return 'rect'; // default
}
/**
* Visit actorName rule
* Grammar: actorName : (IDENTIFIER | STRING) metadata? ;
*/
private visitActorNameImpl(ctx: ActorNameContext): {
name: string;
metadata?: Record<string, string>;
} {
let name = '';
if (ctx.IDENTIFIER?.()) {
name = ctx.IDENTIFIER()!.getText();
} else if (ctx.STRING?.()) {
const text = ctx.STRING()!.getText();
// Remove quotes from string
name = text.slice(1, -1);
}
let metadata = undefined;
if (ctx.metadata?.()) {
metadata = this.visitMetadataImpl(ctx.metadata()!);
}
return { name, metadata };
}
/**
* Visit metadata rule
* Grammar: metadata : '\@' '\{' metadataContent '\}' ;
*/
private visitMetadataImpl(ctx: MetadataContext): Record<string, string> {
const metadataContent = ctx.metadataContent?.();
if (metadataContent) {
return this.visitMetadataContentImpl(metadataContent);
}
return {};
}
/**
* Visit metadataContent rule
* Grammar: metadataContent : metadataProperty (',' metadataProperty)* ;
*/
private visitMetadataContentImpl(ctx: MetadataContentContext): Record<string, string> {
const metadata: Record<string, string> = {};
const properties = ctx.metadataProperty();
for (const property of properties) {
const { key, value } = this.visitMetadataPropertyImpl(property);
metadata[key] = value;
}
return metadata;
}
/**
* Visit metadataProperty rule
* Grammar: metadataProperty : STRING ':' STRING ;
*/
private visitMetadataPropertyImpl(ctx: MetadataPropertyContext): { key: string; value: string } {
const strings = ctx.STRING();
if (strings.length >= 2) {
const key = strings[0].getText().slice(1, -1); // Remove quotes
const value = strings[1].getText().slice(1, -1); // Remove quotes
return { key, value };
}
return { key: '', value: '' };
}
/**
* Visit entityName rule
* Grammar: entityName : IDENTIFIER | STRING | nodeIdWithLabel ;
*/
private visitEntityNameImpl(ctx: EntityNameContext): string {
const identifier = ctx.IDENTIFIER?.();
if (identifier) {
return identifier.getText();
}
const string = ctx.STRING?.();
if (string) {
const text = string.getText();
// Remove quotes from string
return text.slice(1, -1);
}
const nodeIdWithLabel = ctx.nodeIdWithLabel?.();
if (nodeIdWithLabel) {
return this.visitNodeIdWithLabelImpl(nodeIdWithLabel);
}
return '';
}
/**
* Visit actorDeclaration rule
* Grammar: actorDeclaration : 'actor' actorName ;
*/
private visitActorDeclarationImpl(ctx: ActorDeclarationContext): string {
const actorName = ctx.actorName?.();
if (actorName) {
const actorResult = this.visitActorNameImpl(actorName);
// Add the actor if it doesn't already exist
const existingActor = this.actors.find((actor) => actor.id === actorResult.name);
if (!existingActor) {
this.actors.push({
id: actorResult.name,
name: actorResult.name,
metadata: actorResult.metadata,
});
}
return actorResult.name;
}
return '';
}
/**
* Visit nodeIdWithLabel rule
* Grammar: nodeIdWithLabel : IDENTIFIER '(' nodeLabel ')' ;
*/
private visitNodeIdWithLabelImpl(ctx: NodeIdWithLabelContext): string {
let nodeId = '';
let nodeLabel = '';
const identifier = ctx.IDENTIFIER?.();
if (identifier) {
nodeId = identifier.getText();
}
const nodeLabelCtx = ctx.nodeLabel?.();
if (nodeLabelCtx) {
nodeLabel = this.visitNodeLabelImpl(nodeLabelCtx);
}
// Create or update the use case with nodeId and label
const existingUseCase = this.useCases.find((uc) => uc.id === nodeLabel || uc.nodeId === nodeId);
if (existingUseCase) {
// Update existing use case with nodeId if not already set
existingUseCase.nodeId ??= nodeId;
} else {
// Create new use case with nodeId and label
this.useCases.push({
id: nodeLabel,
name: nodeLabel,
nodeId: nodeId,
});
}
return nodeLabel; // Return the label as the entity name for relationships
}
/**
* Visit nodeLabel rule
* Grammar: nodeLabel : IDENTIFIER | STRING | nodeLabel IDENTIFIER | nodeLabel STRING ;
*/
private visitNodeLabelImpl(ctx: NodeLabelContext): string {
const parts: string[] = [];
// Handle recursive nodeLabel structure
const nodeLabel = ctx.nodeLabel?.();
if (nodeLabel) {
parts.push(this.visitNodeLabelImpl(nodeLabel));
}
const identifier = ctx.IDENTIFIER?.();
if (identifier) {
parts.push(identifier.getText());
} else {
const string = ctx.STRING?.();
if (string) {
const text = string.getText();
// Remove quotes from string
parts.push(text.slice(1, -1));
}
}
return parts.join(' ');
}
/**
* Visit arrow rule
* Grammar: arrow : SOLID_ARROW | BACK_ARROW | LINE_SOLID | labeledArrow ;
*/
private visitArrowImpl(ctx: ArrowContext): { arrowType: ArrowType; label?: string } {
// Check if this is a labeled arrow
if (ctx.labeledArrow()) {
return this.visitLabeledArrowImpl(ctx.labeledArrow()!);
}
// Regular arrow without label - determine type from token
if (ctx.SOLID_ARROW()) {
return { arrowType: ARROW_TYPE.SOLID_ARROW };
} else if (ctx.BACK_ARROW()) {
return { arrowType: ARROW_TYPE.BACK_ARROW };
} else if (ctx.LINE_SOLID()) {
return { arrowType: ARROW_TYPE.LINE_SOLID };
}
// Fallback (should not happen with proper grammar)
return { arrowType: ARROW_TYPE.SOLID_ARROW };
}
/**
* Visit labeled arrow rule
* Grammar: labeledArrow : LINE_SOLID edgeLabel SOLID_ARROW | BACK_ARROW edgeLabel LINE_SOLID | LINE_SOLID edgeLabel LINE_SOLID ;
*/
private visitLabeledArrowImpl(ctx: LabeledArrowContext): { arrowType: ArrowType; label: string } {
const label = this.visitEdgeLabelImpl(ctx.edgeLabel());
// Determine arrow type based on the tokens present
if (ctx.SOLID_ARROW()) {
return { arrowType: ARROW_TYPE.SOLID_ARROW, label };
} else if (ctx.BACK_ARROW()) {
return { arrowType: ARROW_TYPE.BACK_ARROW, label };
} else {
return { arrowType: ARROW_TYPE.LINE_SOLID, label };
}
}
/**
* Visit edge label rule
* Grammar: edgeLabel : IDENTIFIER | STRING ;
*/
private visitEdgeLabelImpl(ctx: EdgeLabelContext): string {
const text = ctx.getText();
// Remove quotes if it's a string
if (
(text.startsWith('"') && text.endsWith('"')) ||
(text.startsWith("'") && text.endsWith("'"))
) {
return text.slice(1, -1);
}
return text;
}
/**
* Visit directionStatement rule
* Grammar: directionStatement : 'direction' direction NEWLINE* ;
*/
visitDirectionStatementImpl(ctx: DirectionStatementContext): void {
const directionCtx = ctx.direction?.();
if (directionCtx) {
this.direction = this.visitDirectionImpl(directionCtx);
}
}
/**
* Visit direction rule
* Grammar: direction : 'TB' | 'TD' | 'BT' | 'RL' | 'LR' ;
*/
private visitDirectionImpl(ctx: DirectionContext): string {
const text = ctx.getText();
// Normalize TD to TB (same as flowchart)
if (text === 'TD') {
return 'TB';
}
return text;
}
/**
* Get the parse result after visiting the diagram
*/
getParseResult(): UsecaseParseResult {
return {
actors: this.actors,
useCases: this.useCases,
systemBoundaries: this.systemBoundaries,
relationships: this.relationships,
direction: this.direction,
};
}
}

View File

@@ -1,8 +1,10 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { Info, Packet, Pie, Architecture, GitGraph, Radar, Treemap } from './index.js';
import type { UsecaseParseResult } from './language/usecase/types.js';
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar | UsecaseParseResult;
export type LangiumDiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
const parsers: Record<string, LangiumParser> = {};
const initializers = {
@@ -41,6 +43,9 @@ const initializers = {
const parser = createTreemapServices().Treemap.parser.LangiumParser;
parsers.treemap = parser;
},
usecase: () => {
// ANTLR-based parser - no Langium parser needed
},
} as const;
export async function parse(diagramType: 'info', text: string): Promise<Info>;
@@ -50,7 +55,12 @@ export async function parse(diagramType: 'architecture', text: string): Promise<
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
export async function parse(diagramType: 'radar', text: string): Promise<Radar>;
export async function parse(diagramType: 'treemap', text: string): Promise<Treemap>;
export async function parse(diagramType: 'usecase', text: string): Promise<UsecaseParseResult>;
export async function parse<T extends LangiumDiagramAST>(
diagramType: Exclude<keyof typeof initializers, 'usecase'>,
text: string
): Promise<T>;
export async function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
@@ -59,11 +69,19 @@ export async function parse<T extends DiagramAST>(
if (!initializer) {
throw new Error(`Unknown diagram type: ${diagramType}`);
}
// Handle ANTLR-based parsers separately
if (diagramType === 'usecase') {
const { parseUsecaseWithAntlr } = await import('./language/usecase/index.js');
return parseUsecaseWithAntlr(text) as T;
}
if (!parsers[diagramType]) {
await initializer();
}
const parser: LangiumParser = parsers[diagramType];
const result: ParseResult<T> = parser.parse<T>(text);
const result: ParseResult<T extends LangiumDiagramAST ? T : never> =
parser.parse<T extends LangiumDiagramAST ? T : never>(text);
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
throw new MermaidParseError(result);
}

File diff suppressed because it is too large Load Diff

334
pnpm-lock.yaml generated
View File

@@ -533,12 +533,79 @@ importers:
specifier: ^7.3.0
version: 7.3.0
packages/mermaid/src/vitepress:
dependencies:
'@mdi/font':
specifier: ^7.4.47
version: 7.4.47
'@vueuse/core':
specifier: ^13.1.0
version: 13.9.0(vue@3.5.21(typescript@5.9.2))
font-awesome:
specifier: ^4.7.0
version: 4.7.0
jiti:
specifier: ^2.4.2
version: 2.5.1
mermaid:
specifier: workspace:^
version: link:../..
vue:
specifier: ^3.4.38
version: 3.5.21(typescript@5.9.2)
devDependencies:
'@iconify-json/carbon':
specifier: ^1.1.37
version: 1.2.13
'@unocss/reset':
specifier: ^66.0.0
version: 66.5.1
'@vite-pwa/vitepress':
specifier: ^1.0.0
version: 1.0.0(vite-plugin-pwa@1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0))
'@vitejs/plugin-vue':
specifier: ^6.0.0
version: 6.0.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
fast-glob:
specifier: ^3.3.3
version: 3.3.3
https-localhost:
specifier: ^4.7.1
version: 4.7.1
pathe:
specifier: ^2.0.3
version: 2.0.3
unocss:
specifier: ^66.4.2
version: 66.5.1(postcss@8.5.6)(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.8.0(@babel/parser@7.28.4)(vue@3.5.21(typescript@5.9.2))
vite:
specifier: ^6.1.1
version: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
vite-plugin-pwa:
specifier: ^1.0.0
version: 1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vitepress:
specifier: 1.6.3
version: 1.6.3(@algolia/client-search@5.37.0)(@types/node@22.18.6)(axios@1.12.2)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.44.0)(typescript@5.9.2)
workbox-window:
specifier: ^7.3.0
version: 7.3.0
packages/parser:
dependencies:
antlr4ng:
specifier: ^3.0.7
version: 3.0.16
langium:
specifier: 3.3.1
version: 3.3.1
devDependencies:
antlr-ng:
specifier: ^1.0.10
version: 1.0.10
chevrotain:
specifier: ^11.0.3
version: 11.0.3
@@ -3908,10 +3975,20 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
antlr-ng@1.0.10:
resolution: {integrity: sha512-fw3NdsQP3dabuZrDhKAMewrBsY5KSAcMrvhWBVDmHYegv5D51pypzCYK1PpjaRVKcVeP/5xKfqJY31TvXACOdA==}
hasBin: true
antlr4@4.11.0:
resolution: {integrity: sha512-GUGlpE2JUjAN+G8G5vY+nOoeyNhHsXoIJwP1XF1oRw89vifA1K46T6SEkwLwr7drihN7I/lf0DIjKc4OZvBX8w==}
engines: {node: '>=14'}
antlr4ng@3.0.15:
resolution: {integrity: sha512-VELFqTfcpGI2bj6ScMWuxM3FI6HOsojrgmnw3cCbUtsQ1DNOq32wJsjOt7vLvfIniyyuE1DIYegGcuFmn+jgyw==}
antlr4ng@3.0.16:
resolution: {integrity: sha512-DQuJkC7kX3xunfF4K2KsWTSvoxxslv+FQp/WHQZTJSsH2Ec3QfFmrxC3Nky2ok9yglXn6nHM4zUaVDxcN5f6kA==}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -4475,6 +4552,10 @@ packages:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
commander@14.0.1:
resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==}
engines: {node: '>=20'}
@@ -5608,6 +5689,10 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-printf@1.6.10:
resolution: {integrity: sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==}
engines: {node: '>=10.0'}
fast-querystring@1.1.2:
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
@@ -6076,6 +6161,10 @@ packages:
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
@@ -7054,6 +7143,10 @@ packages:
lunr@2.3.9:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
luxon@3.5.0:
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
engines: {node: '>=12'}
magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
@@ -7712,6 +7805,9 @@ packages:
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@@ -8773,6 +8869,9 @@ packages:
resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
engines: {node: '>=4'}
stringtemplate4ts@1.0.9:
resolution: {integrity: sha512-KYZm2bJlSjynG5Y+L46fkaKBQG6mhV6hb2RBA8dpx3/Vj6G4u7gwXNKYvaN9+QD5sj68/1srtSNDvqEso7MwsQ==}
strip-ansi@3.0.1:
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
engines: {node: '>=0.10.0'}
@@ -8960,6 +9059,9 @@ packages:
thunky@1.1.0:
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -9208,10 +9310,16 @@ packages:
resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
engines: {node: '>=4'}
unicode-properties@1.4.1:
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
unicode-property-aliases-ecmascript@2.2.0:
resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
engines: {node: '>=4'}
unicode-trie@2.0.0:
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
@@ -9411,6 +9519,46 @@ packages:
terser:
optional: true
vite@6.3.6:
resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
jiti: '>=1.21.0'
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vite@7.1.5:
resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -9459,6 +9607,18 @@ packages:
vitepress: ^1.0.0-rc.35
vue: '3'
vitepress@1.6.3:
resolution: {integrity: sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==}
hasBin: true
peerDependencies:
markdown-it-mathjax3: ^4
postcss: ^8
peerDependenciesMeta:
markdown-it-mathjax3:
optional: true
postcss:
optional: true
vitepress@1.6.4:
resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==}
hasBin: true
@@ -13214,6 +13374,14 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@unocss/astro@66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@unocss/core': 66.5.1
'@unocss/reset': 66.5.1
'@unocss/vite': 66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
optionalDependencies:
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
'@unocss/astro@66.5.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@unocss/core': 66.5.1
@@ -13350,6 +13518,19 @@ snapshots:
dependencies:
'@unocss/core': 66.5.1
'@unocss/vite@66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@jridgewell/remapping': 2.3.5
'@unocss/config': 66.5.1
'@unocss/core': 66.5.1
'@unocss/inspector': 66.5.1
chokidar: 3.6.0
magic-string: 0.30.19
pathe: 2.0.3
tinyglobby: 0.2.15
unplugin-utils: 0.3.0
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
'@unocss/vite@66.5.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -13422,6 +13603,10 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
'@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.3(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 1.0.3(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
@@ -13436,6 +13621,12 @@ snapshots:
vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0)
vue: 3.5.21(typescript@5.9.2)
'@vitejs/plugin-vue@6.0.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
vue: 3.5.21(typescript@5.9.2)
'@vitejs/plugin-vue@6.0.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
@@ -13966,8 +14157,19 @@ snapshots:
ansi-styles@6.2.3: {}
antlr-ng@1.0.10:
dependencies:
antlr4ng: 3.0.16
commander: 13.1.0
stringtemplate4ts: 1.0.9
unicode-properties: 1.4.1
antlr4@4.11.0: {}
antlr4ng@3.0.15: {}
antlr4ng@3.0.16: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
@@ -14576,6 +14778,8 @@ snapshots:
commander@12.1.0: {}
commander@13.1.0: {}
commander@14.0.1: {}
commander@2.20.3: {}
@@ -16056,6 +16260,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
fast-printf@1.6.10: {}
fast-querystring@1.1.2:
dependencies:
fast-decode-uri-component: 1.0.1
@@ -16611,6 +16817,8 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
he@1.2.0: {}
highlight.js@10.7.3: {}
hookable@5.5.3: {}
@@ -17785,6 +17993,8 @@ snapshots:
lunr@2.3.9: {}
luxon@3.5.0: {}
magic-string@0.25.9:
dependencies:
sourcemap-codec: 1.4.8
@@ -18631,6 +18841,8 @@ snapshots:
package-manager-detector@1.3.0: {}
pako@0.2.9: {}
pako@1.0.11: {}
pako@2.1.0: {}
@@ -19887,6 +20099,13 @@ snapshots:
is-obj: 1.0.1
is-regexp: 1.0.0
stringtemplate4ts@1.0.9:
dependencies:
antlr4ng: 3.0.15
fast-printf: 1.6.10
he: 1.2.0
luxon: 3.5.0
strip-ansi@3.0.1:
dependencies:
ansi-regex: 2.1.1
@@ -20099,6 +20318,8 @@ snapshots:
thunky@1.1.0: {}
tiny-inflate@1.0.3: {}
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@@ -20322,8 +20543,18 @@ snapshots:
unicode-match-property-value-ecmascript@2.2.1: {}
unicode-properties@1.4.1:
dependencies:
base64-js: 1.5.1
unicode-trie: 2.0.0
unicode-property-aliases-ecmascript@2.2.0: {}
unicode-trie@2.0.0:
dependencies:
pako: 0.2.9
tiny-inflate: 1.0.3
unicorn-magic@0.3.0: {}
unified@11.0.5:
@@ -20377,6 +20608,33 @@ snapshots:
universalify@2.0.1: {}
unocss@66.5.1(postcss@8.5.6)(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)):
dependencies:
'@unocss/astro': 66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
'@unocss/cli': 66.5.1
'@unocss/core': 66.5.1
'@unocss/postcss': 66.5.1(postcss@8.5.6)
'@unocss/preset-attributify': 66.5.1
'@unocss/preset-icons': 66.5.1
'@unocss/preset-mini': 66.5.1
'@unocss/preset-tagify': 66.5.1
'@unocss/preset-typography': 66.5.1
'@unocss/preset-uno': 66.5.1
'@unocss/preset-web-fonts': 66.5.1
'@unocss/preset-wind': 66.5.1
'@unocss/preset-wind3': 66.5.1
'@unocss/preset-wind4': 66.5.1
'@unocss/transformer-attributify-jsx': 66.5.1
'@unocss/transformer-compile-class': 66.5.1
'@unocss/transformer-directives': 66.5.1
'@unocss/transformer-variant-group': 66.5.1
'@unocss/vite': 66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
optionalDependencies:
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
transitivePeerDependencies:
- postcss
- supports-color
unocss@66.5.1(postcss@8.5.6)(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)):
dependencies:
'@unocss/astro': 66.5.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
@@ -20548,6 +20806,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.3(supports-color@8.1.1)
pretty-bytes: 6.1.1
tinyglobby: 0.2.15
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
workbox-build: 7.3.0(@types/babel__core@7.20.5)
workbox-window: 7.3.0
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.0.3(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.3(supports-color@8.1.1)
@@ -20569,6 +20838,22 @@ snapshots:
fsevents: 2.3.3
terser: 5.44.0
vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1):
dependencies:
esbuild: 0.25.10
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.50.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.18.6
fsevents: 2.3.3
jiti: 2.5.1
terser: 5.44.0
tsx: 4.20.5
yaml: 2.8.1
vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1):
dependencies:
esbuild: 0.25.10
@@ -20595,6 +20880,55 @@ snapshots:
vitepress: 1.6.4(@algolia/client-search@5.37.0)(@types/node@22.18.6)(axios@1.12.2)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.44.0)(typescript@5.7.3)
vue: 3.5.21(typescript@5.7.3)
vitepress@1.6.3(@algolia/client-search@5.37.0)(@types/node@22.18.6)(axios@1.12.2)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.44.0)(typescript@5.9.2):
dependencies:
'@docsearch/css': 3.8.2
'@docsearch/js': 3.8.2(@algolia/client-search@5.37.0)(search-insights@2.17.3)
'@iconify-json/simple-icons': 1.2.52
'@shikijs/core': 2.5.0
'@shikijs/transformers': 2.5.0
'@shikijs/types': 2.5.0
'@types/markdown-it': 14.1.2
'@vitejs/plugin-vue': 5.2.4(vite@5.4.20(@types/node@22.18.6)(terser@5.44.0))(vue@3.5.21(typescript@5.9.2))
'@vue/devtools-api': 7.7.7
'@vue/shared': 3.5.21
'@vueuse/core': 12.8.2(typescript@5.9.2)
'@vueuse/integrations': 12.8.2(axios@1.12.2)(focus-trap@7.6.5)(typescript@5.9.2)
focus-trap: 7.6.5
mark.js: 8.11.1
minisearch: 7.1.2
shiki: 2.5.0
vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0)
vue: 3.5.21(typescript@5.9.2)
optionalDependencies:
postcss: 8.5.6
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/node'
- '@types/react'
- async-validator
- axios
- change-case
- drauu
- fuse.js
- idb-keyval
- jwt-decode
- less
- lightningcss
- nprogress
- qrcode
- react
- react-dom
- sass
- sass-embedded
- search-insights
- sortablejs
- stylus
- sugarss
- terser
- typescript
- universal-cookie
vitepress@1.6.4(@algolia/client-search@5.37.0)(@types/node@22.18.6)(axios@1.12.2)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.44.0)(typescript@5.7.3):
dependencies:
'@docsearch/css': 3.8.2