mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-08 08:39:38 +02:00
Compare commits
76 Commits
6587-class
...
feat/useca
Author | SHA1 | Date | |
---|---|---|---|
![]() |
dc9bfa712d | ||
![]() |
1adc145b18 | ||
![]() |
5362d3f5ba | ||
![]() |
16569b295b | ||
![]() |
11a35c11ee | ||
![]() |
216be22801 | ||
![]() |
781554bf70 | ||
![]() |
1c60d125fd | ||
![]() |
313da2b0df | ||
![]() |
c9b9f4425b | ||
![]() |
5c468c0bf0 | ||
![]() |
994f7df29a | ||
![]() |
dc11b8645c | ||
![]() |
ff2ddae587 | ||
![]() |
58042f1596 | ||
![]() |
181af8167b | ||
![]() |
799d2ed547 | ||
![]() |
08160a74b4 | ||
![]() |
6d221fb3ca | ||
![]() |
8b20907141 | ||
![]() |
d318f1a13c | ||
![]() |
525a7de8ae | ||
![]() |
a459c436c9 | ||
![]() |
1c2a0020bd | ||
![]() |
141c6b3808 | ||
![]() |
8d4ffdf808 | ||
![]() |
32106e259c | ||
![]() |
b36edd557e | ||
![]() |
5e3b5e8f36 | ||
![]() |
764b315dc1 | ||
![]() |
47c0d2d040 | ||
![]() |
ac3b777bf6 | ||
![]() |
166782cd38 | ||
![]() |
b37eb6d0d1 | ||
![]() |
f759f5dcf7 | ||
![]() |
80bcefe321 | ||
![]() |
70cbbe69d8 | ||
![]() |
baf4093e8d | ||
![]() |
fd185f7694 | ||
![]() |
027d7b6368 | ||
![]() |
7986b66a88 | ||
![]() |
edb0edc451 | ||
![]() |
b511a2e9be | ||
![]() |
b80ea26a2b | ||
![]() |
f88986a87d | ||
![]() |
e16f0848ab | ||
![]() |
2812a0d12a | ||
![]() |
25fa26d915 | ||
![]() |
62915183b1 | ||
![]() |
6874ab3fb6 | ||
![]() |
040af4f545 | ||
![]() |
65ca3eabfd | ||
![]() |
8b9bbad842 | ||
![]() |
d2773db7dc | ||
![]() |
0dd46a3543 | ||
![]() |
f81e63663c | ||
![]() |
7109e3a17f | ||
![]() |
e0bd51941e | ||
![]() |
38f4e67ca7 | ||
![]() |
681d829227 | ||
![]() |
164e44c3d9 | ||
![]() |
2e1d156d66 | ||
![]() |
e863ad1547 | ||
![]() |
7091792694 | ||
![]() |
efd94b705d | ||
![]() |
9ec989e633 | ||
![]() |
a716a525c3 | ||
![]() |
11abfc9ae5 | ||
![]() |
227cef05b3 | ||
![]() |
81b0ffb92a | ||
![]() |
1d3681053b | ||
![]() |
93df13898f | ||
![]() |
8314554eb5 | ||
![]() |
b7c03dc27e | ||
![]() |
c7f2f609a9 | ||
![]() |
4c3de3a1ec |
92
.build/antlr-cli.ts
Normal file
92
.build/antlr-cli.ts
Normal 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
5
.build/generateAntlr.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { generateFromConfig } from './antlr-cli.js';
|
||||
|
||||
export async function generateAntlr() {
|
||||
await generateFromConfig('./packages/parser/antlr-config.json');
|
||||
}
|
@@ -28,6 +28,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
||||
'packet',
|
||||
'architecture',
|
||||
'radar',
|
||||
'usecase',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
5
.changeset/deep-times-make.md
Normal file
5
.changeset/deep-times-make.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
Add IDs in architecture diagrams
|
5
.changeset/four-eyes-wish.md
Normal file
5
.changeset/four-eyes-wish.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Ensure edge label color is applied when using classDef with edge IDs
|
5
.changeset/moody-fans-try.md
Normal file
5
.changeset/moody-fans-try.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Resolve gantt chart crash due to invalid array length
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Added support for styling class diagram elements based on stereotype annotations
|
5
.changeset/proud-colts-smell.md
Normal file
5
.changeset/proud-colts-smell.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add IDs in architecture diagrams
|
@@ -143,6 +143,9 @@ typeof
|
||||
typestr
|
||||
unshift
|
||||
urlsafe
|
||||
usecase
|
||||
Usecase
|
||||
USECASE
|
||||
verifymethod
|
||||
VERIFYMTHD
|
||||
WARN_DOCSDIR_DOESNT_MATCH
|
||||
|
@@ -8,6 +8,7 @@ compositTitleSize
|
||||
cose
|
||||
curv
|
||||
doublecircle
|
||||
elem
|
||||
elems
|
||||
gantt
|
||||
gitgraph
|
||||
|
@@ -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
|
||||
|
2
.github/workflows/e2e-timings.yml
vendored
2
.github/workflows/e2e-timings.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and create pull request
|
||||
uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a
|
||||
uses: peter-evans/create-pull-request@915d841dae6a4f191bb78faf61a257411d7be4d2
|
||||
with:
|
||||
add-paths: |
|
||||
cypress/timings.json
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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/
|
||||
|
@@ -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' }));
|
||||
|
@@ -98,12 +98,12 @@ describe('Configuration', () => {
|
||||
it('should handle arrowMarkerAbsolute set to true', () => {
|
||||
renderGraph(
|
||||
`flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
{
|
||||
arrowMarkerAbsolute: true,
|
||||
}
|
||||
@@ -113,8 +113,7 @@ describe('Configuration', () => {
|
||||
cy.get('path')
|
||||
.first()
|
||||
.should('have.attr', 'marker-end')
|
||||
.should('exist')
|
||||
.and('include', 'url(http\\:\\/\\/localhost');
|
||||
.and('include', 'url(http://localhost');
|
||||
});
|
||||
});
|
||||
it('should not taint the initial configuration when using multiple directives', () => {
|
||||
|
@@ -1028,88 +1028,4 @@ class C13["With Città foreign language"]
|
||||
{ logLevel: 1, htmlLabels: true }
|
||||
);
|
||||
});
|
||||
it('should render a full class diagram using interface annotation', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<interface>> Class01
|
||||
Class03 "0" *-- "0..n" Class04
|
||||
Class05 "1" o-- "many" Class06
|
||||
Class07 .. Class08
|
||||
Class09 "many" --> "1" C2 : Where am i?
|
||||
Class09 "0" --* "1..n" C3
|
||||
Class09 --|> Class07
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : #size()
|
||||
Class01 : -int chimp
|
||||
Class01 : +int gorilla
|
||||
Class08 <--> C2: Cool label
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
size()
|
||||
}
|
||||
|
||||
`,
|
||||
{ logLevel: 1, htmlLabels: true }
|
||||
);
|
||||
});
|
||||
it('should render a full class diagram using abstract annotation', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<abstract>> Class01
|
||||
Class03 "0" *-- "0..n" Class04
|
||||
Class05 "1" o-- "many" Class06
|
||||
Class07 .. Class08
|
||||
Class09 "many" --> "1" C2 : Where am i?
|
||||
Class09 "0" --* "1..n" C3
|
||||
Class09 --|> Class07
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : #size()
|
||||
Class01 : -int chimp
|
||||
Class01 : +int gorilla
|
||||
Class08 <--> C2: Cool label
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
size()
|
||||
}
|
||||
|
||||
`,
|
||||
{ logLevel: 1, htmlLabels: true }
|
||||
);
|
||||
});
|
||||
it('should render a full class diagram using enumeration annotation', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<enumeration>> Class01
|
||||
Class03 "0" *-- "0..n" Class04
|
||||
Class05 "1" o-- "many" Class06
|
||||
Class07 .. Class08
|
||||
Class09 "many" --> "1" C2 : Where am i?
|
||||
Class09 "0" --* "1..n" C3
|
||||
Class09 --|> Class07
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : #size()
|
||||
Class01 : -int chimp
|
||||
Class01 : +int gorilla
|
||||
Class08 <--> C2: Cool label
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
size()
|
||||
}
|
||||
|
||||
`,
|
||||
{ logLevel: 1, htmlLabels: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -109,7 +109,7 @@ describe('Flowchart ELK', () => {
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
||||
verifyNumber(maxWidthValue, 380);
|
||||
verifyNumber(maxWidthValue, 380, 15);
|
||||
});
|
||||
});
|
||||
it('8-elk: should render a flowchart when useMaxWidth is false', () => {
|
||||
@@ -128,7 +128,7 @@ describe('Flowchart ELK', () => {
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
// use within because the absolute value can be slightly different depending on the environment ±5%
|
||||
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
|
||||
verifyNumber(width, 380);
|
||||
verifyNumber(width, 380, 15);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
|
@@ -1186,4 +1186,17 @@ end
|
||||
imgSnapshotTest(graph, { htmlLabels: false });
|
||||
});
|
||||
});
|
||||
|
||||
it('V2 - 17: should apply class def colour to edge label', () => {
|
||||
imgSnapshotTest(
|
||||
` graph LR
|
||||
id1(Start) link@-- "Label" -->id2(Stop)
|
||||
style id1 fill:#f9f,stroke:#333,stroke-width:4px
|
||||
|
||||
class id2 myClass
|
||||
classDef myClass fill:#bbf,stroke:#f66,stroke-width:2px,color:white,stroke-dasharray: 5 5
|
||||
class link myClass
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -803,4 +803,34 @@ describe('Gantt diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should handle numeric timestamps with dateFormat x', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
gantt
|
||||
title Process time profile (ms)
|
||||
dateFormat x
|
||||
axisFormat %L
|
||||
tickInterval 250millisecond
|
||||
|
||||
section Pipeline
|
||||
Parse JSON p1: 000, 120
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should handle numeric timestamps with dateFormat X', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
gantt
|
||||
title Process time profile (ms)
|
||||
dateFormat X
|
||||
axisFormat %L
|
||||
tickInterval 250millisecond
|
||||
|
||||
section Pipeline
|
||||
Parse JSON p1: 000, 120
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
425
cypress/integration/rendering/usecase.spec.ts
Normal file
425
cypress/integration/rendering/usecase.spec.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
|
||||
|
||||
describe('Usecase diagram', () => {
|
||||
it('should render a simple usecase diagram with actors and use cases', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin --> "Manage Users"
|
||||
User --> "View Profile"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with quoted actor names', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Customer Service"
|
||||
actor "System Administrator"
|
||||
"Customer Service" --> "Handle Tickets"
|
||||
"System Administrator" --> "Manage System"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with different arrow types', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin <-- Logout
|
||||
User -- "View Data"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with edge labels', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Developer
|
||||
actor Manager
|
||||
Developer --important--> "Write Code"
|
||||
Manager --review--> "Code Review"
|
||||
Developer --urgent--> Manager
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with node ID syntax', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
User --> a(Login)
|
||||
User --> b("View Profile")
|
||||
User --> c("Update Settings")
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with comma-separated actors', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Customer Service", "Technical Support", "Sales Team"
|
||||
actor SystemAdmin
|
||||
"Customer Service" --> "Handle Tickets"
|
||||
"Technical Support" --> "Resolve Issues"
|
||||
"Sales Team" --> "Process Orders"
|
||||
SystemAdmin --> "Manage System"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with actor metadata', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User@{ "type" : "primary", "icon" : "user" }
|
||||
actor Admin@{ "type" : "secondary", "icon" : "admin" }
|
||||
actor System@{ "type" : "hollow", "icon" : "system" }
|
||||
User --> Login
|
||||
Admin --> "Manage Users"
|
||||
System --> "Process Data"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with system boundaries (rect type)', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Admin, User
|
||||
systemBoundary "Authentication"
|
||||
Login
|
||||
Logout
|
||||
"Reset Password"
|
||||
end
|
||||
"Authentication"@{ type: rect }
|
||||
Admin --> Login
|
||||
User --> Login
|
||||
User --> "Reset Password"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with system boundaries (package type)', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Admin, User
|
||||
systemBoundary "Authentication"
|
||||
Login
|
||||
Logout
|
||||
"Reset Password"
|
||||
end
|
||||
"Authentication"@{ type: package }
|
||||
Admin --> Login
|
||||
User --> Login
|
||||
User --> "Reset Password"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render complex usecase diagram with all features', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Customer Service"@{ "type" : "primary", "icon" : "user" }
|
||||
actor "System Admin"@{ "type" : "secondary", "icon" : "admin" }
|
||||
actor "Database"@{ "type" : "hollow", "icon" : "database" }
|
||||
|
||||
systemBoundary "Customer Support System"
|
||||
"Handle Tickets"
|
||||
"View Customer Info"
|
||||
end
|
||||
"Customer Support System"@{ type: package }
|
||||
|
||||
systemBoundary "Administration"
|
||||
"User Management"
|
||||
"System Config"
|
||||
end
|
||||
|
||||
"Customer Service" --priority--> "Handle Tickets"
|
||||
"Customer Service" --> "View Customer Info"
|
||||
"System Admin" --manage--> "User Management"
|
||||
"System Admin" --> "System Config"
|
||||
"Database" <-- "Handle Tickets"
|
||||
"Database" <-- "View Customer Info"
|
||||
"Database" <-- "User Management"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with actor-to-actor relationships', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Manager
|
||||
actor Developer
|
||||
actor Tester
|
||||
|
||||
Manager --supervises--> Developer
|
||||
Manager --coordinates--> Tester
|
||||
Developer --collaborates--> Tester
|
||||
|
||||
Developer --> "Write Code"
|
||||
Tester --> "Test Code"
|
||||
Manager --> "Review Progress"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with mixed relationship types', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
|
||||
User --> "Basic Login"
|
||||
Admin --> "Advanced Login"
|
||||
User --includes--> "View Profile"
|
||||
Admin --extends--> "Manage Profiles"
|
||||
|
||||
"Basic Login" <-- "Advanced Login"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with long labels and text wrapping', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Customer Service Representative"
|
||||
actor "System Administrator with Extended Privileges"
|
||||
|
||||
"Customer Service Representative" --Process--> "Handle Complex Customer Support Tickets"
|
||||
"System Administrator with Extended Privileges" --> "Manage System Configuration and User Permissions"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with special characters in names', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "User@Company.com"
|
||||
actor "Admin (Level-1)"
|
||||
"User@Company.com" --> a("Login & Authenticate")
|
||||
"Admin (Level-1)" --> b("Manage Users & Permissions")
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram when useMaxWidth is true (default)', () => {
|
||||
renderGraph(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin --> "Manage System"
|
||||
User --> "View Profile"
|
||||
`,
|
||||
{ usecase: { useMaxWidth: true } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
expect(svg).to.have.attr('width', '100%');
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render usecase diagram when useMaxWidth is false', () => {
|
||||
renderGraph(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin --> "Manage System"
|
||||
`,
|
||||
{ usecase: { useMaxWidth: false } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
expect(width).to.be.greaterThan(200);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render empty usecase diagram', () => {
|
||||
imgSnapshotTest(`usecase`);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with only actors', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
actor Guest
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with implicit use case creation', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login
|
||||
User --> Register
|
||||
User --> "Forgot Password"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with nested system boundaries', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
|
||||
systemBoundary "Main System"
|
||||
Login
|
||||
Logout
|
||||
"Create User"
|
||||
"Delete User"
|
||||
end
|
||||
|
||||
User --> Login
|
||||
User --> Logout
|
||||
Admin --> "Create User"
|
||||
Admin --> "Delete User"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with multiple edge labels on same relationship', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Developer
|
||||
actor Manager
|
||||
|
||||
Developer --"code review"--> Manager
|
||||
Developer --"status update"--> Manager
|
||||
Manager --"feedback"--> Developer
|
||||
Manager --"approval"--> Developer
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with various actor icon types', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User@{ "icon": "user" }
|
||||
actor Admin@{ "icon": "admin" }
|
||||
actor Database@{ "icon": "database" }
|
||||
actor API@{ "icon": "api" }
|
||||
actor Mobile@{ "icon": "mobile" }
|
||||
actor Web@{ "icon": "web" }
|
||||
|
||||
User --> "Access System"
|
||||
Admin --> "Manage System"
|
||||
Database --> "Store Data"
|
||||
API --> "Provide Services"
|
||||
Mobile --> "Mobile Access"
|
||||
Web --> "Web Access"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with mixed arrow directions and labels', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor System
|
||||
actor Admin
|
||||
|
||||
User --request--> System
|
||||
System --response--> User
|
||||
System <--monitor-- Admin
|
||||
Admin --configure--> System
|
||||
User -- "direct access" -- Admin
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with boundary-less use cases', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
|
||||
systemBoundary "Secure Area"
|
||||
"Admin Panel"
|
||||
"User Management"
|
||||
end
|
||||
|
||||
User --> "Public Login"
|
||||
User --> "Guest Access"
|
||||
Admin --> "Public Login"
|
||||
Admin --> "Admin Panel"
|
||||
Admin --> "User Management"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with complex metadata combinations', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Primary User"@{ "type": "primary", "icon": "user", "fillColor": "lightblue" }
|
||||
actor "Secondary User"@{ "type": "secondary", "icon": "client", "strokeColor": "red" }
|
||||
actor "System Service"@{ "type": "hollow", "icon": "service", "strokeWidth": "3" }
|
||||
|
||||
"Primary User" --"high priority"--> a("Critical Process")
|
||||
"Secondary User" --"low priority"--> b("Background Task")
|
||||
"System Service" --"automated"--> c("System Maintenance")
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with Unicode characters', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "用户"@{ "icon": "user" }
|
||||
actor "管理员"@{ "icon": "admin" }
|
||||
|
||||
"用户" --"登录"--> "系统访问"
|
||||
"管理员" --"管理"--> "用户管理"
|
||||
"用户" --> "数据查看"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render large usecase diagram with many elements', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User1, User2, User3, User4
|
||||
actor Admin1, Admin2
|
||||
actor System1@{ "icon": "system" }
|
||||
actor System2@{ "icon": "database" }
|
||||
|
||||
systemBoundary "Module A"
|
||||
"Feature A1"
|
||||
"Feature A2"
|
||||
"Admin A1"
|
||||
end
|
||||
"Module A"@{ type: package }
|
||||
|
||||
systemBoundary "Module B"
|
||||
"Feature B1"
|
||||
"Feature B2"
|
||||
"Admin B1"
|
||||
end
|
||||
|
||||
User1 --> "Feature A1"
|
||||
User2 --> "Feature A2"
|
||||
Admin1 --> "Admin A1"
|
||||
User3 --> "Feature B1"
|
||||
User4 --> "Feature B2"
|
||||
Admin2 --> "Admin B1"
|
||||
|
||||
System1 <-- "Feature A1"
|
||||
System1 <-- "Feature B1"
|
||||
System2 <-- "Admin A1"
|
||||
System2 <-- "Admin B1"
|
||||
|
||||
User1 --"collaborates"--> User2
|
||||
Admin1 --"supervises"--> Admin2
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
@@ -32,26 +32,8 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&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=Recursive:wght@300..1000&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<style>
|
||||
.recursive-mermaid {
|
||||
font-family: 'Recursive', sans-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; */
|
||||
@@ -63,9 +45,7 @@
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
.mermaid {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
@@ -103,11 +83,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.class2 {
|
||||
fill: red;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
/* tspan {
|
||||
font-size: 6px !important;
|
||||
} */
|
||||
@@ -130,76 +105,194 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre
|
||||
flowchart-elk TB
|
||||
c1-->a2
|
||||
subgraph one
|
||||
a1-->a2
|
||||
end
|
||||
subgraph two
|
||||
b1-->b2
|
||||
end
|
||||
subgraph three
|
||||
c1-->c2
|
||||
end
|
||||
one --> two
|
||||
three --> two
|
||||
two --> c2
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre
|
||||
flowchart TB
|
||||
|
||||
process_C
|
||||
subgraph container_Alpha
|
||||
subgraph process_B
|
||||
pppB
|
||||
end
|
||||
subgraph process_A
|
||||
pppA
|
||||
end
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
process_A-->|messages|container_Beta
|
||||
end
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
a
|
||||
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
b
|
||||
c
|
||||
d
|
||||
B
|
||||
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
D
|
||||
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
flowchart TB
|
||||
subgraph container_Beta
|
||||
process_C
|
||||
end
|
||||
subgraph container_Alpha
|
||||
subgraph process_B
|
||||
pppB
|
||||
end
|
||||
subgraph process_A
|
||||
pppA
|
||||
end
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
process_A-->|messages|container_Beta
|
||||
end
|
||||
|
||||
</pre>
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart TB
|
||||
subgraph container_Beta
|
||||
process_C
|
||||
end
|
||||
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
classDiagram
|
||||
note "I love this diagram!\nDo you love it?"
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<interface>> Class01
|
||||
Class03 "1" *-- "*" Class04
|
||||
Class05 "1" o-- "many" Class06
|
||||
Class07 "1" .. "*" Class08
|
||||
Class09 "1" --> "*" C2 : Where am i?
|
||||
Class09 "*" --* "*" C3
|
||||
Class09 "1" --|> "1" Class07
|
||||
Class12 <|.. Class08
|
||||
Class11 ..>Class12
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : size()
|
||||
Class01 : int chimp
|
||||
Class01 : int gorilla
|
||||
Class01 : -int privateChimp
|
||||
Class01 : +int publicGorilla
|
||||
Class01 : #int protectedMarmoset
|
||||
Class08 <--> C2: Cool label
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
test()
|
||||
}
|
||||
note for Class10 "Cool class\nI said it's very cool class!"
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
requirementDiagram
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart-elk TB
|
||||
internet
|
||||
nat
|
||||
router
|
||||
compute1
|
||||
|
||||
subgraph project
|
||||
router
|
||||
nat
|
||||
subgraph subnet1
|
||||
compute1
|
||||
end
|
||||
end
|
||||
|
||||
%% router --> subnet1
|
||||
subnet1 --> nat
|
||||
%% nat --> internet
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart-elk TB
|
||||
internet
|
||||
nat
|
||||
router
|
||||
lb1
|
||||
lb2
|
||||
compute1
|
||||
compute2
|
||||
subgraph project
|
||||
router
|
||||
nat
|
||||
subgraph subnet1
|
||||
compute1
|
||||
lb1
|
||||
end
|
||||
subgraph subnet2
|
||||
compute2
|
||||
lb2
|
||||
end
|
||||
end
|
||||
internet --> router
|
||||
router --> subnet1 & subnet2
|
||||
subnet1 & subnet2 --> nat --> internet
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
@@ -227,91 +320,35 @@ treemap
|
||||
"Leaf 2.2": 25
|
||||
"Leaf 2.3": 12
|
||||
|
||||
classDef class1 fill:red,color:blue,stroke:#FFD600;
|
||||
</pre>
|
||||
<pre id="diagram5" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: rounded
|
||||
---
|
||||
flowchart LR
|
||||
I["fa:fa-code Text"] -- Mermaid js --> D["Use<br/>the<br/>editor!"]
|
||||
I --> D & D
|
||||
D@{ shape: question}
|
||||
I@{ shape: question}
|
||||
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
treemap:
|
||||
valueFormat: '$0,0'
|
||||
---
|
||||
treemap
|
||||
"Budget"
|
||||
"Operations"
|
||||
"Salaries": 7000
|
||||
"Equipment": 2000
|
||||
"Supplies": 1000
|
||||
"Marketing"
|
||||
"Advertising": 4000
|
||||
"Events": 1000
|
||||
|
||||
</pre
|
||||
>
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
treemap
|
||||
title Accessible Treemap Title
|
||||
"Category A"
|
||||
"Item A1": 10
|
||||
"Item A2": 20
|
||||
"Category B"
|
||||
"Item B1": 15
|
||||
"Item B2": 25
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
a
|
||||
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
b
|
||||
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
flowchart TB
|
||||
A --> n0["1"]
|
||||
A --> n1["2"]
|
||||
A --> n2["3"]
|
||||
A --> n3["4"] --> Q & R & S & T
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart TB
|
||||
A --> n0["1"]
|
||||
A --> n1["2"]
|
||||
A --> n2["3"]
|
||||
A --> n3["4"] --> Q & R & S & T
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
@@ -320,128 +357,112 @@ treemap
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
flowchart:
|
||||
curve: linear
|
||||
---
|
||||
flowchart LR
|
||||
root{mindmap} --- Origins --- Europe
|
||||
Origins --> Asia
|
||||
root --- Background --- Rich
|
||||
Background --- Poor
|
||||
subgraph apa
|
||||
Background
|
||||
Poor
|
||||
end
|
||||
|
||||
|
||||
|
||||
A[A] --> B[B]
|
||||
A[A] --- B([C])
|
||||
A@{ shape: diamond}
|
||||
%%B@{ shape: diamond}
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: linear
|
||||
---
|
||||
flowchart LR
|
||||
root{mindmap} --- Origins --- Europe
|
||||
Origins --> Asia
|
||||
root --- Background --- Rich
|
||||
Background --- Poor
|
||||
|
||||
|
||||
|
||||
A[A] -- Mermaid js --> B[B]
|
||||
A[A] -- Mermaid js --- B[B]
|
||||
A@{ shape: diamond}
|
||||
B@{ shape: diamond}
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart
|
||||
D(("for D"))
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: rounded
|
||||
---
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
e1@{ animate: true}
|
||||
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
|
||||
I --> D & D
|
||||
D@{ shape: question}
|
||||
I@{ shape: question}
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@--> B
|
||||
classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
|
||||
class e1 animate
|
||||
</pre>
|
||||
<h2>infinite</h2>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@--> B
|
||||
classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
|
||||
class e1 animate
|
||||
</pre>
|
||||
<h2>Mermaid - edge-animation-slow</h2>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@--> B
|
||||
e1@{ animation: fast}
|
||||
</pre>
|
||||
<h2>Mermaid - edge-animation-fast</h2>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@--> B
|
||||
classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear;
|
||||
class e1 edge-animation-fast
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: rounded
|
||||
elk:
|
||||
nodePlacementStrategy: NETWORK_SIMPLEX
|
||||
---
|
||||
flowchart LR
|
||||
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
|
||||
D --> I & I
|
||||
a["a"]
|
||||
D@{ shape: trap-b}
|
||||
I@{ shape: lean-l}
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
flowchart LR
|
||||
%% subgraph s1["Untitled subgraph"]
|
||||
C["Evaluate"]
|
||||
%% end
|
||||
|
||||
info </pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
B --> C
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
//curve: linear
|
||||
---
|
||||
flowchart LR
|
||||
%% A ==> B
|
||||
%% A2 --> B2
|
||||
A{A} --> B((Bo boo)) & B & B & B
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
theme: default
|
||||
look: classic
|
||||
---
|
||||
flowchart LR
|
||||
subgraph s1["APA"]
|
||||
D{"Use the editor"}
|
||||
end
|
||||
subgraph S2["S2"]
|
||||
s1
|
||||
I>"fa:fa-code Text"]
|
||||
E["E"]
|
||||
end
|
||||
D -- Mermaid js --> I
|
||||
D --> I & E
|
||||
E --> I
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -466,7 +487,7 @@ config:
|
||||
end
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -479,45 +500,7 @@ config:
|
||||
D-->I
|
||||
D-->I
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart LR
|
||||
a
|
||||
subgraph s0["APA"]
|
||||
subgraph s8["APA"]
|
||||
subgraph s1["APA"]
|
||||
D{"X"}
|
||||
E[Q]
|
||||
end
|
||||
subgraph s3["BAPA"]
|
||||
F[Q]
|
||||
I
|
||||
end
|
||||
D --> I
|
||||
D --> I
|
||||
D --> I
|
||||
|
||||
I{"X"}
|
||||
end
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart LR
|
||||
a
|
||||
D{"Use the editor"}
|
||||
|
||||
D -- Mermaid js --> I{"fa:fa-code Text"}
|
||||
D-->I
|
||||
D-->I
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -556,7 +539,7 @@ flowchart LR
|
||||
n8@{ shape: rect}
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -572,7 +555,7 @@ flowchart LR
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -581,7 +564,7 @@ flowchart LR
|
||||
A{A} --> B & C
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -593,7 +576,7 @@ flowchart LR
|
||||
end
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -611,7 +594,7 @@ flowchart LR
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
kanban:
|
||||
@@ -630,81 +613,81 @@ kanban
|
||||
task3[💻 Develop login feature]@{ ticket: 103 }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||
A:::AClass
|
||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||
A:::AClass
|
||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||
A:::AClass
|
||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
|
||||
A:::AClass
|
||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart LR
|
||||
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
kanban
|
||||
id2[In progress]
|
||||
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
kanban:
|
||||
@@ -768,18 +751,22 @@ kanban
|
||||
alert('It worked');
|
||||
}
|
||||
await mermaid.initialize({
|
||||
// theme: 'forest',
|
||||
// theme: 'base',
|
||||
// theme: 'default',
|
||||
// theme: 'forest',
|
||||
// handDrawnSeed: 12,
|
||||
// look: 'handDrawn',
|
||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||
// layout: 'dagre',
|
||||
// layout: 'elk',
|
||||
layout: 'elk',
|
||||
// layout: 'fixed',
|
||||
// htmlLabels: false,
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
fontFamily: "'Recursive', sans-serif",
|
||||
|
||||
// fontFamily: 'Caveat',
|
||||
// fontFamily: 'Kalam',
|
||||
// fontFamily: 'courier',
|
||||
fontFamily: 'arial',
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
|
@@ -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)
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ExternalDiagramDefinition
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L94)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:94](https://github.com/me
|
||||
|
||||
> **detector**: `DiagramDetector`
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L98)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/me
|
||||
|
||||
> **id**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:95](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L95)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)
|
||||
|
||||
---
|
||||
|
||||
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:95](https://github.com/me
|
||||
|
||||
> **loader**: `DiagramLoader`
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:99](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L99)
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: LayoutLoaderDefinition
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L21)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.co
|
||||
|
||||
> `optional` **algorithm**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:27](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L27)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.co
|
||||
|
||||
> **loader**: `LayoutLoader`
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:23](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L23)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:26](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L26)
|
||||
|
||||
---
|
||||
|
||||
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:23](https://github.co
|
||||
|
||||
> **name**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:22](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L22)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:25](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L25)
|
||||
|
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: RenderOptions
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L7)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L10)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,4 +18,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:7](https://github.com
|
||||
|
||||
> `optional` **algorithm**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:11](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L11)
|
||||
|
@@ -12,4 +12,4 @@
|
||||
|
||||
> **SVG** = `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L126)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)
|
||||
|
@@ -12,4 +12,4 @@
|
||||
|
||||
> **SVGGroup** = `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:130](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L130)
|
||||
|
@@ -38,3 +38,5 @@ Each user journey is split into sections, these describe the part of the task
|
||||
the user is trying to complete.
|
||||
|
||||
Tasks syntax is `Task name: <score>: <comma separated list of actors>`
|
||||
|
||||
Score is a number between 1 and 5, inclusive.
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Mermaid examples package",
|
||||
"author": "Sidharth Vinod",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"module": "./dist/mermaid-examples.core.mjs",
|
||||
"types": "./dist/mermaid.d.ts",
|
||||
|
51
packages/examples/src/examples/usecase.ts
Normal file
51
packages/examples/src/examples/usecase.ts
Normal 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;
|
@@ -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,
|
||||
];
|
||||
|
67
packages/mermaid-layout-elk/src/__tests__/geometry.spec.ts
Normal file
67
packages/mermaid-layout-elk/src/__tests__/geometry.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
intersection,
|
||||
ensureTrulyOutside,
|
||||
makeInsidePoint,
|
||||
tryNodeIntersect,
|
||||
replaceEndpoint,
|
||||
type RectLike,
|
||||
type P,
|
||||
} from '../geometry.js';
|
||||
|
||||
const approx = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps;
|
||||
|
||||
describe('geometry helpers', () => {
|
||||
it('intersection: vertical approach hits bottom border', () => {
|
||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||
const h = rect.height / 2; // 25
|
||||
const outside: P = { x: 0, y: 100 };
|
||||
const inside: P = { x: 0, y: 0 };
|
||||
const res = intersection(rect, outside, inside);
|
||||
expect(approx(res.x, 0)).toBe(true);
|
||||
expect(approx(res.y, h)).toBe(true);
|
||||
});
|
||||
|
||||
it('ensureTrulyOutside nudges near-boundary point outward', () => {
|
||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||
// near bottom boundary (y ~ h)
|
||||
const near: P = { x: 0, y: rect.height / 2 - 0.2 };
|
||||
const out = ensureTrulyOutside(rect, near, 10);
|
||||
expect(out.y).toBeGreaterThan(rect.height / 2);
|
||||
});
|
||||
|
||||
it('makeInsidePoint keeps x for vertical and y from center', () => {
|
||||
const rect: RectLike = { x: 10, y: 5, width: 100, height: 50 };
|
||||
const outside: P = { x: 10, y: 40 };
|
||||
const center: P = { x: 99, y: -123 }; // center y should be used
|
||||
const inside = makeInsidePoint(rect, outside, center);
|
||||
expect(inside.x).toBe(outside.x);
|
||||
expect(inside.y).toBe(center.y);
|
||||
});
|
||||
|
||||
it('tryNodeIntersect returns null for wrong-side intersections', () => {
|
||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||
const outside: P = { x: -50, y: 0 };
|
||||
const node = { intersect: () => ({ x: 10, y: 0 }) } as any; // right side of center
|
||||
const res = tryNodeIntersect(node, rect, outside);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
it('replaceEndpoint dedup removes end/start appropriately', () => {
|
||||
const pts: P[] = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
];
|
||||
// remove duplicate end
|
||||
replaceEndpoint(pts, 'end', { x: 1, y: 1 });
|
||||
expect(pts.length).toBe(1);
|
||||
|
||||
const pts2: P[] = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
];
|
||||
// remove duplicate start
|
||||
replaceEndpoint(pts2, 'start', { x: 0, y: 0 });
|
||||
expect(pts2.length).toBe(1);
|
||||
});
|
||||
});
|
209
packages/mermaid-layout-elk/src/geometry.ts
Normal file
209
packages/mermaid-layout-elk/src/geometry.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/* Geometry utilities extracted from render.ts for reuse and testing */
|
||||
|
||||
export interface P {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface RectLike {
|
||||
x: number; // center x
|
||||
y: number; // center y
|
||||
width: number;
|
||||
height: number;
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
export interface NodeLike {
|
||||
intersect?: (p: P) => P | null;
|
||||
}
|
||||
|
||||
export const EPS = 1;
|
||||
export const PUSH_OUT = 10;
|
||||
|
||||
export const onBorder = (bounds: RectLike, p: P, tol = 0.5): boolean => {
|
||||
const halfW = bounds.width / 2;
|
||||
const halfH = bounds.height / 2;
|
||||
const left = bounds.x - halfW;
|
||||
const right = bounds.x + halfW;
|
||||
const top = bounds.y - halfH;
|
||||
const bottom = bounds.y + halfH;
|
||||
|
||||
const onLeft = Math.abs(p.x - left) <= tol && p.y >= top - tol && p.y <= bottom + tol;
|
||||
const onRight = Math.abs(p.x - right) <= tol && p.y >= top - tol && p.y <= bottom + tol;
|
||||
const onTop = Math.abs(p.y - top) <= tol && p.x >= left - tol && p.x <= right + tol;
|
||||
const onBottom = Math.abs(p.y - bottom) <= tol && p.x >= left - tol && p.x <= right + tol;
|
||||
return onLeft || onRight || onTop || onBottom;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute intersection between a rectangle (center x/y, width/height) and the line
|
||||
* segment from insidePoint -\> outsidePoint. Returns the point on the rectangle border.
|
||||
*
|
||||
* This version avoids snapping to outsidePoint when certain variables evaluate to 0
|
||||
* (previously caused vertical top/bottom cases to miss the border). It only enforces
|
||||
* axis-constant behavior for purely vertical/horizontal approaches.
|
||||
*/
|
||||
export const intersection = (node: RectLike, outsidePoint: P, insidePoint: P): P => {
|
||||
const x = node.x;
|
||||
const y = node.y;
|
||||
|
||||
const dx = Math.abs(x - insidePoint.x);
|
||||
const w = node.width / 2;
|
||||
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
||||
const h = node.height / 2;
|
||||
|
||||
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||
|
||||
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
|
||||
// Intersection is top or bottom of rect.
|
||||
const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
||||
r = (R * q) / Q;
|
||||
const res = {
|
||||
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
|
||||
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
|
||||
};
|
||||
|
||||
// Keep axis-constant special-cases only
|
||||
if (R === 0) {
|
||||
res.x = outsidePoint.x;
|
||||
}
|
||||
if (Q === 0) {
|
||||
res.y = outsidePoint.y;
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
// Intersection on sides of rect
|
||||
if (insidePoint.x < outsidePoint.x) {
|
||||
r = outsidePoint.x - w - x;
|
||||
} else {
|
||||
r = x - w - outsidePoint.x;
|
||||
}
|
||||
const q = (Q * r) / R;
|
||||
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
|
||||
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
|
||||
|
||||
// Only handle axis-constant cases
|
||||
if (R === 0) {
|
||||
_x = outsidePoint.x;
|
||||
}
|
||||
if (Q === 0) {
|
||||
_y = outsidePoint.y;
|
||||
}
|
||||
|
||||
return { x: _x, y: _y };
|
||||
}
|
||||
};
|
||||
|
||||
export const outsideNode = (node: RectLike, point: P): boolean => {
|
||||
const x = node.x;
|
||||
const y = node.y;
|
||||
const dx = Math.abs(point.x - x);
|
||||
const dy = Math.abs(point.y - y);
|
||||
const w = node.width / 2;
|
||||
const h = node.height / 2;
|
||||
return dx >= w || dy >= h;
|
||||
};
|
||||
|
||||
export const ensureTrulyOutside = (bounds: RectLike, p: P, push = PUSH_OUT): P => {
|
||||
const dx = Math.abs(p.x - bounds.x);
|
||||
const dy = Math.abs(p.y - bounds.y);
|
||||
const w = bounds.width / 2;
|
||||
const h = bounds.height / 2;
|
||||
if (Math.abs(dx - w) < EPS || Math.abs(dy - h) < EPS) {
|
||||
const dirX = p.x - bounds.x;
|
||||
const dirY = p.y - bounds.y;
|
||||
const len = Math.sqrt(dirX * dirX + dirY * dirY);
|
||||
if (len > 0) {
|
||||
return {
|
||||
x: bounds.x + (dirX / len) * (len + push),
|
||||
y: bounds.y + (dirY / len) * (len + push),
|
||||
};
|
||||
}
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
export const makeInsidePoint = (bounds: RectLike, outside: P, center: P): P => {
|
||||
const isVertical = Math.abs(outside.x - bounds.x) < EPS;
|
||||
const isHorizontal = Math.abs(outside.y - bounds.y) < EPS;
|
||||
return {
|
||||
x: isVertical
|
||||
? outside.x
|
||||
: outside.x < bounds.x
|
||||
? bounds.x - bounds.width / 4
|
||||
: bounds.x + bounds.width / 4,
|
||||
y: isHorizontal ? outside.y : center.y,
|
||||
};
|
||||
};
|
||||
|
||||
export const tryNodeIntersect = (node: NodeLike, bounds: RectLike, outside: P): P | null => {
|
||||
if (!node?.intersect) {
|
||||
return null;
|
||||
}
|
||||
const res = node.intersect(outside);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
const wrongSide =
|
||||
(outside.x < bounds.x && res.x > bounds.x) || (outside.x > bounds.x && res.x < bounds.x);
|
||||
if (wrongSide) {
|
||||
return null;
|
||||
}
|
||||
const dist = Math.hypot(outside.x - res.x, outside.y - res.y);
|
||||
if (dist <= EPS) {
|
||||
return null;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export const fallbackIntersection = (bounds: RectLike, outside: P, center: P): P => {
|
||||
const inside = makeInsidePoint(bounds, outside, center);
|
||||
return intersection(bounds, outside, inside);
|
||||
};
|
||||
|
||||
export const computeNodeIntersection = (
|
||||
node: NodeLike,
|
||||
bounds: RectLike,
|
||||
outside: P,
|
||||
center: P
|
||||
): P => {
|
||||
const outside2 = ensureTrulyOutside(bounds, outside);
|
||||
return tryNodeIntersect(node, bounds, outside2) ?? fallbackIntersection(bounds, outside2, center);
|
||||
};
|
||||
|
||||
export const replaceEndpoint = (
|
||||
points: P[],
|
||||
which: 'start' | 'end',
|
||||
value: P | null | undefined,
|
||||
tol = 0.1
|
||||
) => {
|
||||
if (!value || points.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (which === 'start') {
|
||||
if (
|
||||
points.length > 0 &&
|
||||
Math.abs(points[0].x - value.x) < tol &&
|
||||
Math.abs(points[0].y - value.y) < tol
|
||||
) {
|
||||
// duplicate start remove it
|
||||
points.shift();
|
||||
} else {
|
||||
points[0] = value;
|
||||
}
|
||||
} else {
|
||||
const last = points.length - 1;
|
||||
if (
|
||||
points.length > 0 &&
|
||||
Math.abs(points[last].x - value.x) < tol &&
|
||||
Math.abs(points[last].y - value.y) < tol
|
||||
) {
|
||||
// duplicate end remove it
|
||||
points.pop();
|
||||
} else {
|
||||
points[last] = value;
|
||||
}
|
||||
}
|
||||
};
|
@@ -1,11 +1,26 @@
|
||||
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
|
||||
// @ts-ignore TODO: Investigate D3 issue
|
||||
import { curveLinear } from 'd3';
|
||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
|
||||
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
|
||||
|
||||
import {
|
||||
type P,
|
||||
type RectLike,
|
||||
outsideNode,
|
||||
computeNodeIntersection,
|
||||
replaceEndpoint,
|
||||
onBorder,
|
||||
} from './geometry.js';
|
||||
|
||||
type Node = LayoutData['nodes'][number];
|
||||
// Used to calculate distances in order to avoid floating number rounding issues when comparing floating numbers
|
||||
const epsilon = 0.0001;
|
||||
|
||||
// Minimal structural type to avoid depending on d3 Selection typings
|
||||
interface D3Selection<T extends Element> {
|
||||
node(): T | null;
|
||||
attr(name: string, value: string): D3Selection<T>;
|
||||
}
|
||||
|
||||
interface LabelData {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -16,18 +31,9 @@ interface LabelData {
|
||||
interface NodeWithVertex extends Omit<Node, 'domId'> {
|
||||
children?: LayoutData['nodes'];
|
||||
labelData?: LabelData;
|
||||
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>;
|
||||
}
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
function distance(p1?: Point, p2?: Point): number {
|
||||
if (!p1 || !p2) {
|
||||
return 0;
|
||||
}
|
||||
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
||||
domId?: D3Selection<SVGAElement | SVGGElement>;
|
||||
}
|
||||
|
||||
export const render = async (
|
||||
data4Layout: LayoutData,
|
||||
svg: SVG,
|
||||
@@ -61,39 +67,26 @@ export const render = async (
|
||||
|
||||
// Add the element to the DOM
|
||||
if (!node.isGroup) {
|
||||
// Create a clean node object for ELK with only the properties it expects
|
||||
const child: NodeWithVertex = {
|
||||
id: node.id,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
// Store the original node data for later use
|
||||
label: node.label,
|
||||
isGroup: node.isGroup,
|
||||
shape: node.shape,
|
||||
padding: node.padding,
|
||||
cssClasses: node.cssClasses,
|
||||
cssStyles: node.cssStyles,
|
||||
look: node.look,
|
||||
// Include parentId for subgraph processing
|
||||
parentId: node.parentId,
|
||||
};
|
||||
const child = node as NodeWithVertex;
|
||||
graph.children.push(child);
|
||||
nodeDb[node.id] = child;
|
||||
nodeDb[node.id] = node;
|
||||
|
||||
const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
|
||||
const boundingBox = childNodeEl.node()!.getBBox();
|
||||
// Store the domId separately for rendering, not in the ELK graph
|
||||
child.domId = childNodeEl;
|
||||
child.calcIntersect = node.calcIntersect;
|
||||
child.width = boundingBox.width;
|
||||
child.height = boundingBox.height;
|
||||
} else {
|
||||
// A subgraph
|
||||
const child: NodeWithVertex & { children: NodeWithVertex[] } = {
|
||||
...node,
|
||||
domId: undefined,
|
||||
children: [],
|
||||
};
|
||||
// Let elk render with the copy
|
||||
graph.children.push(child);
|
||||
// Save the original containing the intersection function
|
||||
nodeDb[node.id] = child;
|
||||
await addVertices(nodeEl, nodeArr, child, node.id);
|
||||
|
||||
@@ -168,7 +161,7 @@ export const render = async (
|
||||
height: node.height,
|
||||
};
|
||||
if (node.isGroup) {
|
||||
log.debug('id abc88 subgraph = ', node.id, node.x, node.y, node.labelData);
|
||||
log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData);
|
||||
const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph');
|
||||
// TODO use faster way of cloning
|
||||
const clusterNode = JSON.parse(JSON.stringify(node));
|
||||
@@ -177,10 +170,10 @@ export const render = async (
|
||||
clusterNode.width = Math.max(clusterNode.width, node.labelData.width);
|
||||
await insertCluster(subgraphEl, clusterNode);
|
||||
|
||||
log.debug('id (UIO)= ', node.id, node.width, node.shape, node.labels);
|
||||
log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels);
|
||||
} else {
|
||||
log.info(
|
||||
'id NODE = ',
|
||||
'Id NODE = ',
|
||||
node.id,
|
||||
node.x,
|
||||
node.y,
|
||||
@@ -222,25 +215,19 @@ export const render = async (
|
||||
});
|
||||
});
|
||||
|
||||
subgraphs.forEach(function (subgraph: { id: string | number }) {
|
||||
const data: any = { id: subgraph.id };
|
||||
if (parentLookupDb.parentById[subgraph.id] !== undefined) {
|
||||
data.parent = parentLookupDb.parentById[subgraph.id];
|
||||
}
|
||||
});
|
||||
return parentLookupDb;
|
||||
};
|
||||
|
||||
const getEdgeStartEndPoint = (edge: any) => {
|
||||
const source: any = edge.start;
|
||||
const target: any = edge.end;
|
||||
// edge.start and edge.end are IDs (string/number) in our layout data
|
||||
const sourceId: string | number = edge.start;
|
||||
const targetId: string | number = edge.end;
|
||||
|
||||
// Save the original source and target
|
||||
const sourceId = source;
|
||||
const targetId = target;
|
||||
const source = sourceId;
|
||||
const target = targetId;
|
||||
|
||||
const startNode = nodeDb[edge.start.id];
|
||||
const endNode = nodeDb[edge.end.id];
|
||||
const startNode = nodeDb[sourceId];
|
||||
const endNode = nodeDb[targetId];
|
||||
|
||||
if (!startNode || !endNode) {
|
||||
return { source, target };
|
||||
@@ -263,6 +250,112 @@ export const render = async (
|
||||
/**
|
||||
* Add edges to graph based on parsed graph definition
|
||||
*/
|
||||
// Edge helper maps and utilities (de-duplicated)
|
||||
const ARROW_MAP: Record<string, [string, string]> = {
|
||||
arrow_open: ['arrow_open', 'arrow_open'],
|
||||
arrow_cross: ['arrow_open', 'arrow_cross'],
|
||||
double_arrow_cross: ['arrow_cross', 'arrow_cross'],
|
||||
arrow_point: ['arrow_open', 'arrow_point'],
|
||||
double_arrow_point: ['arrow_point', 'arrow_point'],
|
||||
arrow_circle: ['arrow_open', 'arrow_circle'],
|
||||
double_arrow_circle: ['arrow_circle', 'arrow_circle'],
|
||||
};
|
||||
|
||||
const computeStroke = (
|
||||
stroke: string | undefined,
|
||||
defaultStyle?: string,
|
||||
defaultLabelStyle?: string
|
||||
) => {
|
||||
// Defaults correspond to 'normal'
|
||||
let thickness = 'normal';
|
||||
let pattern = 'solid';
|
||||
let style = '';
|
||||
let labelStyle = '';
|
||||
|
||||
if (stroke === 'dotted') {
|
||||
pattern = 'dotted';
|
||||
style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
||||
} else if (stroke === 'thick') {
|
||||
thickness = 'thick';
|
||||
style = 'stroke-width: 3.5px;fill:none;';
|
||||
} else {
|
||||
// normal
|
||||
style = defaultStyle ?? 'fill:none;';
|
||||
if (defaultLabelStyle !== undefined) {
|
||||
labelStyle = defaultLabelStyle;
|
||||
}
|
||||
}
|
||||
return { thickness, pattern, style, labelStyle };
|
||||
};
|
||||
|
||||
const getCurve = (edgeInterpolate: any, edgesDefaultInterpolate: any, confCurve: any) => {
|
||||
if (edgeInterpolate !== undefined) {
|
||||
return interpolateToCurve(edgeInterpolate, curveLinear);
|
||||
}
|
||||
if (edgesDefaultInterpolate !== undefined) {
|
||||
return interpolateToCurve(edgesDefaultInterpolate, curveLinear);
|
||||
}
|
||||
// @ts-ignore TODO: fix this
|
||||
return interpolateToCurve(confCurve, curveLinear);
|
||||
};
|
||||
const buildEdgeData = (
|
||||
edge: any,
|
||||
defaults: {
|
||||
defaultStyle?: string;
|
||||
defaultLabelStyle?: string;
|
||||
defaultInterpolate?: any;
|
||||
confCurve: any;
|
||||
},
|
||||
common: any
|
||||
) => {
|
||||
const edgeData: any = { style: '', labelStyle: '' };
|
||||
edgeData.minlen = edge.length || 1;
|
||||
// maintain legacy behavior
|
||||
edge.text = edge.label;
|
||||
|
||||
// Arrowhead fill vs none
|
||||
edgeData.arrowhead = edge.type === 'arrow_open' ? 'none' : 'normal';
|
||||
|
||||
// Arrow types
|
||||
const arrowMap = ARROW_MAP[edge.type] ?? ARROW_MAP.arrow_open;
|
||||
edgeData.arrowTypeStart = arrowMap[0];
|
||||
edgeData.arrowTypeEnd = arrowMap[1];
|
||||
|
||||
// Optional edge label positioning flags
|
||||
edgeData.startLabelRight = edge.startLabelRight;
|
||||
edgeData.endLabelLeft = edge.endLabelLeft;
|
||||
|
||||
// Stroke
|
||||
const strokeRes = computeStroke(edge.stroke, defaults.defaultStyle, defaults.defaultLabelStyle);
|
||||
edgeData.thickness = strokeRes.thickness;
|
||||
edgeData.pattern = strokeRes.pattern;
|
||||
edgeData.style = (edgeData.style || '') + (strokeRes.style || '');
|
||||
edgeData.labelStyle = (edgeData.labelStyle || '') + (strokeRes.labelStyle || '');
|
||||
|
||||
// Curve
|
||||
// @ts-ignore - defaults.confCurve is present at runtime but missing in type
|
||||
edgeData.curve = getCurve(edge.interpolate, defaults.defaultInterpolate, defaults.confCurve);
|
||||
|
||||
// Arrowhead style + labelpos when we have label text
|
||||
const hasText = (edge?.text ?? '') !== '';
|
||||
if (hasText) {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
edgeData.labelpos = 'c';
|
||||
} else if (edge.style !== undefined) {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
}
|
||||
|
||||
edgeData.labelType = edge.labelType;
|
||||
edgeData.label = (edge?.text ?? '').replace(common.lineBreakRegex, '\n');
|
||||
|
||||
if (edge.style === undefined) {
|
||||
edgeData.style = edgeData.style ?? 'stroke: #333; stroke-width: 1.5px;fill:none;';
|
||||
}
|
||||
|
||||
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||
return edgeData;
|
||||
};
|
||||
|
||||
const addEdges = async function (
|
||||
dataForLayout: { edges: any; direction?: string },
|
||||
graph: {
|
||||
@@ -284,7 +377,6 @@ export const render = async (
|
||||
const edges = dataForLayout.edges;
|
||||
const labelsEl = svg.insert('g').attr('class', 'edgeLabels');
|
||||
const linkIdCnt: any = {};
|
||||
const dir = dataForLayout.direction || 'DOWN';
|
||||
let defaultStyle: string | undefined;
|
||||
let defaultLabelStyle: string | undefined;
|
||||
|
||||
@@ -314,105 +406,24 @@ export const render = async (
|
||||
linkIdCnt[linkIdBase]++;
|
||||
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
|
||||
}
|
||||
const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase];
|
||||
const linkId = linkIdBase; // + '_' + linkIdCnt[linkIdBase];
|
||||
edge.id = linkId;
|
||||
log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
|
||||
const linkNameStart = 'LS_' + edge.start;
|
||||
const linkNameEnd = 'LE_' + edge.end;
|
||||
|
||||
const edgeData: any = { style: '', labelStyle: '' };
|
||||
edgeData.minlen = edge.length || 1;
|
||||
edge.text = edge.label;
|
||||
// Set link type for rendering
|
||||
if (edge.type === 'arrow_open') {
|
||||
edgeData.arrowhead = 'none';
|
||||
} else {
|
||||
edgeData.arrowhead = 'normal';
|
||||
}
|
||||
|
||||
// Check of arrow types, placed here in order not to break old rendering
|
||||
edgeData.arrowTypeStart = 'arrow_open';
|
||||
edgeData.arrowTypeEnd = 'arrow_open';
|
||||
|
||||
/* eslint-disable no-fallthrough */
|
||||
switch (edge.type) {
|
||||
case 'double_arrow_cross':
|
||||
edgeData.arrowTypeStart = 'arrow_cross';
|
||||
case 'arrow_cross':
|
||||
edgeData.arrowTypeEnd = 'arrow_cross';
|
||||
break;
|
||||
case 'double_arrow_point':
|
||||
edgeData.arrowTypeStart = 'arrow_point';
|
||||
case 'arrow_point':
|
||||
edgeData.arrowTypeEnd = 'arrow_point';
|
||||
break;
|
||||
case 'double_arrow_circle':
|
||||
edgeData.arrowTypeStart = 'arrow_circle';
|
||||
case 'arrow_circle':
|
||||
edgeData.arrowTypeEnd = 'arrow_circle';
|
||||
break;
|
||||
}
|
||||
|
||||
let style = '';
|
||||
let labelStyle = '';
|
||||
|
||||
edgeData.startLabelRight = edge.startLabelRight;
|
||||
edgeData.endLabelLeft = edge.endLabelLeft;
|
||||
|
||||
switch (edge.stroke) {
|
||||
case 'normal':
|
||||
style = 'fill:none;';
|
||||
if (defaultStyle !== undefined) {
|
||||
style = defaultStyle;
|
||||
}
|
||||
if (defaultLabelStyle !== undefined) {
|
||||
labelStyle = defaultLabelStyle;
|
||||
}
|
||||
edgeData.thickness = 'normal';
|
||||
edgeData.pattern = 'solid';
|
||||
break;
|
||||
case 'dotted':
|
||||
edgeData.thickness = 'normal';
|
||||
edgeData.pattern = 'dotted';
|
||||
edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
||||
break;
|
||||
case 'thick':
|
||||
edgeData.thickness = 'thick';
|
||||
edgeData.pattern = 'solid';
|
||||
edgeData.style = 'stroke-width: 3.5px;fill:none;';
|
||||
break;
|
||||
}
|
||||
|
||||
edgeData.style = edgeData.style += style;
|
||||
edgeData.labelStyle = edgeData.labelStyle += labelStyle;
|
||||
|
||||
const conf = getConfig();
|
||||
if (edge.interpolate !== undefined) {
|
||||
edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
|
||||
} else if (edges.defaultInterpolate !== undefined) {
|
||||
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear);
|
||||
} else {
|
||||
// @ts-ignore TODO: fix this
|
||||
edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
|
||||
}
|
||||
|
||||
if (edge.text === undefined) {
|
||||
if (edge.style !== undefined) {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
}
|
||||
} else {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
edgeData.labelpos = 'c';
|
||||
}
|
||||
|
||||
edgeData.labelType = edge.labelType;
|
||||
edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n');
|
||||
|
||||
if (edge.style === undefined) {
|
||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
|
||||
}
|
||||
|
||||
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||
const edgeData = buildEdgeData(
|
||||
edge,
|
||||
{
|
||||
defaultStyle,
|
||||
defaultLabelStyle,
|
||||
defaultInterpolate: edges.defaultInterpolate,
|
||||
// @ts-ignore - conf.curve exists at runtime but is missing from typing
|
||||
confCurve: conf.curve,
|
||||
},
|
||||
common
|
||||
);
|
||||
|
||||
edgeData.id = linkId;
|
||||
edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
|
||||
@@ -421,13 +432,11 @@ export const render = async (
|
||||
|
||||
// calculate start and end points of the edge, note that the source and target
|
||||
// can be modified for shapes that have ports
|
||||
// @ts-ignore TODO: fix this
|
||||
const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir);
|
||||
|
||||
const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge);
|
||||
log.debug('abc78 source and target', source, target);
|
||||
// Add the edge to the graph
|
||||
graph.edges.push({
|
||||
// @ts-ignore TODO: fix this
|
||||
id: 'e' + edge.start + edge.end,
|
||||
...edge,
|
||||
sources: [source],
|
||||
targets: [target],
|
||||
@@ -461,6 +470,7 @@ export const render = async (
|
||||
case 'RL':
|
||||
return 'LEFT';
|
||||
case 'TB':
|
||||
case 'TD': // TD is an alias for TB in Mermaid
|
||||
return 'DOWN';
|
||||
case 'BT':
|
||||
return 'UP';
|
||||
@@ -484,6 +494,203 @@ export const render = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Node bounds helpers (global)
|
||||
const getEffectiveGroupWidth = (node: any): number => {
|
||||
const labelW = node?.labels?.[0]?.width ?? 0;
|
||||
const padding = node?.padding ?? 0;
|
||||
return Math.max(node.width ?? 0, labelW + padding);
|
||||
};
|
||||
|
||||
const boundsFor = (node: any): RectLike => {
|
||||
const width = node?.isGroup ? getEffectiveGroupWidth(node) : node.width;
|
||||
return {
|
||||
x: node.offset.posX + node.width / 2,
|
||||
y: node.offset.posY + node.height / 2,
|
||||
width,
|
||||
height: node.height,
|
||||
padding: node.padding,
|
||||
};
|
||||
};
|
||||
// Helper utilities for endpoint handling around cutter2
|
||||
type Side = 'start' | 'end';
|
||||
const approxEq = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps;
|
||||
const isCenterApprox = (pt: P, node: { x: number; y: number }) =>
|
||||
approxEq(pt.x, node.x) && approxEq(pt.y, node.y);
|
||||
|
||||
const getCandidateBorderPoint = (
|
||||
points: P[],
|
||||
node: any,
|
||||
side: Side
|
||||
): { candidate: P; centerApprox: boolean } => {
|
||||
if (!points?.length) {
|
||||
return { candidate: { x: node.x, y: node.y } as P, centerApprox: true };
|
||||
}
|
||||
if (side === 'start') {
|
||||
const first = points[0];
|
||||
const centerApprox = isCenterApprox(first, node);
|
||||
const candidate = centerApprox && points.length > 1 ? points[1] : first;
|
||||
return { candidate, centerApprox };
|
||||
} else {
|
||||
const last = points[points.length - 1];
|
||||
const centerApprox = isCenterApprox(last, node);
|
||||
const candidate = centerApprox && points.length > 1 ? points[points.length - 2] : last;
|
||||
return { candidate, centerApprox };
|
||||
}
|
||||
};
|
||||
|
||||
const dropAutoCenterPoint = (points: P[], side: Side, doDrop: boolean) => {
|
||||
if (!doDrop) {
|
||||
return;
|
||||
}
|
||||
if (side === 'start') {
|
||||
if (points.length > 0) {
|
||||
points.shift();
|
||||
}
|
||||
} else {
|
||||
if (points.length > 0) {
|
||||
points.pop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applyStartIntersectionIfNeeded = (points: P[], startNode: any, startBounds: RectLike) => {
|
||||
let firstOutsideStartIndex = -1;
|
||||
for (const [i, p] of points.entries()) {
|
||||
if (outsideNode(startBounds, p)) {
|
||||
firstOutsideStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstOutsideStartIndex !== -1) {
|
||||
const outsidePointForStart = points[firstOutsideStartIndex];
|
||||
const startCenter = points[0];
|
||||
const startIntersection = computeNodeIntersection(
|
||||
startNode,
|
||||
startBounds,
|
||||
outsidePointForStart,
|
||||
startCenter
|
||||
);
|
||||
replaceEndpoint(points, 'start', startIntersection);
|
||||
log.debug('UIO cutter2: start-only intersection applied', { startIntersection });
|
||||
}
|
||||
};
|
||||
|
||||
const applyEndIntersectionIfNeeded = (points: P[], endNode: any, endBounds: RectLike) => {
|
||||
let outsideIndexForEnd = -1;
|
||||
for (let i = points.length - 1; i >= 0; i--) {
|
||||
if (outsideNode(endBounds, points[i])) {
|
||||
outsideIndexForEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (outsideIndexForEnd !== -1) {
|
||||
const outsidePointForEnd = points[outsideIndexForEnd];
|
||||
const endCenter = points[points.length - 1];
|
||||
const endIntersection = computeNodeIntersection(
|
||||
endNode,
|
||||
endBounds,
|
||||
outsidePointForEnd,
|
||||
endCenter
|
||||
);
|
||||
replaceEndpoint(points, 'end', endIntersection);
|
||||
log.debug('UIO cutter2: end-only intersection applied', { endIntersection });
|
||||
}
|
||||
};
|
||||
|
||||
const cutter2 = (startNode: any, endNode: any, _points: any[]) => {
|
||||
const startBounds = boundsFor(startNode);
|
||||
const endBounds = boundsFor(endNode);
|
||||
|
||||
if (_points.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Copy the original points array
|
||||
const points: P[] = [..._points] as P[];
|
||||
|
||||
// The first point is the center of sNode, the last point is the center of eNode
|
||||
const startCenter = points[0];
|
||||
const endCenter = points[points.length - 1];
|
||||
|
||||
// Minimal, structured logging for diagnostics
|
||||
log.debug('PPP cutter2: bounds', { startBounds, endBounds });
|
||||
log.debug('PPP cutter2: original points', _points);
|
||||
|
||||
let firstOutsideStartIndex = -1;
|
||||
|
||||
// Single iteration through the array
|
||||
for (const [i, point] of points.entries()) {
|
||||
if (firstOutsideStartIndex === -1 && outsideNode(startBounds, point)) {
|
||||
firstOutsideStartIndex = i;
|
||||
}
|
||||
if (outsideNode(endBounds, point)) {
|
||||
// keep scanning; we'll also scan from the end for the last outside point
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate intersection with start node if we found a point outside it
|
||||
if (firstOutsideStartIndex !== -1) {
|
||||
const outsidePointForStart = points[firstOutsideStartIndex];
|
||||
const startIntersection = computeNodeIntersection(
|
||||
startNode,
|
||||
startBounds,
|
||||
outsidePointForStart,
|
||||
startCenter
|
||||
);
|
||||
log.debug('UIO cutter2: start intersection', startIntersection);
|
||||
replaceEndpoint(points, 'start', startIntersection);
|
||||
}
|
||||
|
||||
// Calculate intersection with end node
|
||||
let outsidePointForEnd = null;
|
||||
let outsideIndexForEnd = -1;
|
||||
|
||||
for (let i = points.length - 1; i >= 0; i--) {
|
||||
if (outsideNode(endBounds, points[i])) {
|
||||
outsidePointForEnd = points[i];
|
||||
outsideIndexForEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!outsidePointForEnd && points.length > 1) {
|
||||
outsidePointForEnd = points[points.length - 2];
|
||||
outsideIndexForEnd = points.length - 2;
|
||||
}
|
||||
|
||||
if (outsidePointForEnd) {
|
||||
const endIntersection = computeNodeIntersection(
|
||||
endNode,
|
||||
endBounds,
|
||||
outsidePointForEnd,
|
||||
endCenter
|
||||
);
|
||||
log.debug('UIO cutter2: end intersection', { endIntersection, outsideIndexForEnd });
|
||||
replaceEndpoint(points, 'end', endIntersection);
|
||||
}
|
||||
|
||||
// Final cleanup: Check if the last point is too close to the previous point
|
||||
if (points.length > 1) {
|
||||
const lastPoint = points[points.length - 1];
|
||||
const secondLastPoint = points[points.length - 2];
|
||||
const distance = Math.sqrt(
|
||||
(lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2
|
||||
);
|
||||
if (distance < 2) {
|
||||
log.debug('UIO cutter2: trimming tail point (too close)', {
|
||||
distance,
|
||||
lastPoint,
|
||||
secondLastPoint,
|
||||
});
|
||||
points.pop();
|
||||
}
|
||||
}
|
||||
|
||||
log.debug('UIO cutter2: final points', points);
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
// @ts-ignore - ELK is not typed
|
||||
const elk = new ELK();
|
||||
const element = svg.select('g');
|
||||
@@ -495,17 +702,19 @@ export const render = async (
|
||||
id: 'root',
|
||||
layoutOptions: {
|
||||
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
||||
'elk.layered.crossingMinimization.forceNodeModelOrder':
|
||||
data4Layout.config.elk?.forceNodeModelOrder,
|
||||
'elk.layered.considerModelOrder.strategy': data4Layout.config.elk?.considerModelOrder,
|
||||
|
||||
'elk.algorithm': algorithm,
|
||||
'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy,
|
||||
'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges,
|
||||
'elk.direction': 'DOWN',
|
||||
'spacing.baseValue': 35,
|
||||
'spacing.baseValue': 40,
|
||||
'elk.layered.crossingMinimization.forceNodeModelOrder':
|
||||
data4Layout.config.elk?.forceNodeModelOrder,
|
||||
'elk.layered.considerModelOrder.strategy': data4Layout.config.elk?.considerModelOrder,
|
||||
'elk.layered.unnecessaryBendpoints': true,
|
||||
'elk.layered.cycleBreaking.strategy': data4Layout.config.elk?.cycleBreakingStrategy,
|
||||
|
||||
// 'elk.layered.cycleBreaking.strategy': 'GREEDY_MODEL_ORDER',
|
||||
// 'elk.layered.cycleBreaking.strategy': 'MODEL_ORDER',
|
||||
// 'spacing.nodeNode': 20,
|
||||
// 'spacing.nodeNodeBetweenLayers': 25,
|
||||
// 'spacing.edgeNode': 20,
|
||||
@@ -513,22 +722,28 @@ export const render = async (
|
||||
// 'spacing.edgeEdge': 10,
|
||||
// 'spacing.edgeEdgeBetweenLayers': 20,
|
||||
// 'spacing.nodeSelfLoop': 20,
|
||||
|
||||
// Tweaking options
|
||||
// 'nodePlacement.favorStraightEdges': true,
|
||||
// 'elk.layered.nodePlacement.favorStraightEdges': true,
|
||||
// 'nodePlacement.feedbackEdges': true,
|
||||
// 'elk.layered.wrapping.multiEdge.improveCuts': true,
|
||||
// 'elk.layered.wrapping.multiEdge.improveWrappedEdges': true,
|
||||
'elk.layered.wrapping.multiEdge.improveCuts': true,
|
||||
'elk.layered.wrapping.multiEdge.improveWrappedEdges': true,
|
||||
// 'elk.layered.wrapping.strategy': 'MULTI_EDGE',
|
||||
// 'elk.layered.edgeRouting.selfLoopDistribution': 'EQUALLY',
|
||||
// 'elk.layered.mergeHierarchyEdges': true,
|
||||
// 'elk.layered.wrapping.strategy': 'SINGLE_EDGE',
|
||||
'elk.layered.edgeRouting.selfLoopDistribution': 'EQUALLY',
|
||||
'elk.layered.mergeHierarchyEdges': true,
|
||||
|
||||
// 'elk.layered.feedbackEdges': true,
|
||||
// 'elk.layered.crossingMinimization.semiInteractive': true,
|
||||
// 'elk.layered.edgeRouting.splines.sloppy.layerSpacingFactor': 1,
|
||||
// 'elk.layered.edgeRouting.polyline.slopedEdgeZoneWidth': 4.0,
|
||||
// 'elk.layered.wrapping.validify.strategy': 'LOOK_BACK',
|
||||
// 'elk.insideSelfLoops.activate': true,
|
||||
// 'elk.separateConnectedComponents': true,
|
||||
// 'elk.alg.layered.options.EdgeStraighteningStrategy': 'NONE',
|
||||
// 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', // NODES_AND_EDGES
|
||||
// 'elk.layered.considerModelOrder.strategy': 'EDGES', // NODES_AND_EDGES
|
||||
// 'elk.layered.wrapping.cutting.strategy': 'ARD', // NODES_AND_EDGES
|
||||
},
|
||||
children: [],
|
||||
@@ -538,7 +753,7 @@ export const render = async (
|
||||
log.info('Drawing flowchart using v4 renderer', elk);
|
||||
|
||||
// Set the direction of the graph based on the parsed information
|
||||
const dir = data4Layout.direction || 'DOWN';
|
||||
const dir = data4Layout.direction ?? 'DOWN';
|
||||
elkGraph.layoutOptions['elk.direction'] = dir2ElkDirection(dir);
|
||||
|
||||
// Create the lookup db for the subgraphs and their children to used when creating
|
||||
@@ -569,15 +784,16 @@ export const render = async (
|
||||
|
||||
// Subgraph
|
||||
if (parentLookupDb.childrenById[node.id] !== undefined) {
|
||||
// Set label and adjust node width separately (avoid side effects in labels array)
|
||||
node.labels = [
|
||||
{
|
||||
text: node.label,
|
||||
width: node?.labelData?.width || 50,
|
||||
height: node?.labelData?.height || 50,
|
||||
width: node?.labelData?.width ?? 50,
|
||||
height: node?.labelData?.height ?? 50,
|
||||
},
|
||||
(node.width = node.width + 2 * node.padding),
|
||||
log.debug('UIO node label', node?.labelData?.width, node.padding),
|
||||
];
|
||||
node.width = node.width + 2 * node.padding;
|
||||
log.debug('UIO node label', node?.labelData?.width, node.padding);
|
||||
node.layoutOptions = {
|
||||
'spacing.baseValue': 30,
|
||||
'nodeLabels.placement': '[H_CENTER V_TOP, INSIDE]',
|
||||
@@ -641,7 +857,7 @@ export const render = async (
|
||||
try {
|
||||
g = await elk.layout(elkGraph);
|
||||
log.debug('APA01 after - success');
|
||||
log.debug('APA01 layout result:', JSON.stringify(g, null, 2));
|
||||
log.info('APA01 layout result:', JSON.stringify(g, null, 2));
|
||||
} catch (error) {
|
||||
log.error('APA01 ELK layout error:', error);
|
||||
throw error;
|
||||
@@ -702,10 +918,10 @@ export const render = async (
|
||||
// sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width);
|
||||
sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding);
|
||||
// sw = startNode.width;
|
||||
log.debug(
|
||||
log.info(
|
||||
'UIO width',
|
||||
startNode.id,
|
||||
startNode.with,
|
||||
startNode.width,
|
||||
'bbox.width=',
|
||||
bbox.width,
|
||||
'lw=',
|
||||
@@ -725,7 +941,7 @@ export const render = async (
|
||||
log.debug(
|
||||
'UIO width',
|
||||
startNode.id,
|
||||
startNode.with,
|
||||
startNode.width,
|
||||
bbox.width,
|
||||
'EW = ',
|
||||
ew,
|
||||
@@ -733,38 +949,109 @@ export const render = async (
|
||||
startNode.innerHTML
|
||||
);
|
||||
}
|
||||
startNode.x = startNode.offset.posX + startNode.width / 2;
|
||||
startNode.y = startNode.offset.posY + startNode.height / 2;
|
||||
endNode.x = endNode.offset.posX + endNode.width / 2;
|
||||
endNode.y = endNode.offset.posY + endNode.height / 2;
|
||||
|
||||
if (startNode.calcIntersect) {
|
||||
const intersection = startNode.calcIntersect(
|
||||
{
|
||||
x: startNode.offset.posX + startNode.width / 2,
|
||||
y: startNode.offset.posY + startNode.height / 2,
|
||||
width: startNode.width,
|
||||
height: startNode.height,
|
||||
},
|
||||
edge.points[0]
|
||||
);
|
||||
// Only add center points for non-subgraph nodes or when the edge path doesn't already end near the target
|
||||
const shouldAddStartCenter = startNode.shape !== 'rect33';
|
||||
const shouldAddEndCenter = endNode.shape !== 'rect33';
|
||||
|
||||
if (distance(intersection, edge.points[0]) > epsilon) {
|
||||
edge.points.unshift(intersection);
|
||||
}
|
||||
}
|
||||
if (endNode.calcIntersect) {
|
||||
const intersection = endNode.calcIntersect(
|
||||
{
|
||||
x: endNode.offset.posX + endNode.width / 2,
|
||||
y: endNode.offset.posY + endNode.height / 2,
|
||||
width: endNode.width,
|
||||
height: endNode.height,
|
||||
},
|
||||
edge.points[edge.points.length - 1]
|
||||
);
|
||||
|
||||
if (distance(intersection, edge.points[edge.points.length - 1]) > epsilon) {
|
||||
edge.points.push(intersection);
|
||||
}
|
||||
if (shouldAddStartCenter) {
|
||||
edge.points.unshift({
|
||||
x: startNode.x,
|
||||
y: startNode.y,
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldAddEndCenter) {
|
||||
edge.points.push({
|
||||
x: endNode.x,
|
||||
y: endNode.y,
|
||||
});
|
||||
}
|
||||
|
||||
// Debug and sanitize points around cutter2
|
||||
const prevPoints = Array.isArray(edge.points) ? [...edge.points] : [];
|
||||
const endBounds = boundsFor(endNode);
|
||||
log.debug(
|
||||
'PPP cutter2: Points before cutter2:',
|
||||
JSON.stringify(edge.points),
|
||||
'endBounds:',
|
||||
endBounds,
|
||||
onBorder(endBounds, edge.points[edge.points.length - 1])
|
||||
);
|
||||
// Block for reducing variable scope and guardrails for the cutter function
|
||||
{
|
||||
const startBounds = boundsFor(startNode);
|
||||
const endBounds = boundsFor(endNode);
|
||||
|
||||
const startIsGroup = !!startNode?.isGroup;
|
||||
const endIsGroup = !!endNode?.isGroup;
|
||||
|
||||
const { candidate: startCandidate, centerApprox: startCenterApprox } =
|
||||
getCandidateBorderPoint(prevPoints as P[], startNode, 'start');
|
||||
const { candidate: endCandidate, centerApprox: endCenterApprox } =
|
||||
getCandidateBorderPoint(prevPoints as P[], endNode, 'end');
|
||||
|
||||
const skipStart = startIsGroup && onBorder(startBounds, startCandidate);
|
||||
const skipEnd = endIsGroup && onBorder(endBounds, endCandidate);
|
||||
|
||||
dropAutoCenterPoint(prevPoints as P[], 'start', skipStart && startCenterApprox);
|
||||
dropAutoCenterPoint(prevPoints as P[], 'end', skipEnd && endCenterApprox);
|
||||
|
||||
if (skipStart || skipEnd) {
|
||||
if (!skipStart) {
|
||||
applyStartIntersectionIfNeeded(prevPoints as P[], startNode, startBounds);
|
||||
}
|
||||
if (!skipEnd) {
|
||||
applyEndIntersectionIfNeeded(prevPoints as P[], endNode, endBounds);
|
||||
}
|
||||
|
||||
log.debug('PPP cutter2: skipping cutter2 due to on-border group endpoint(s)', {
|
||||
skipStart,
|
||||
skipEnd,
|
||||
startCenterApprox,
|
||||
endCenterApprox,
|
||||
startCandidate,
|
||||
endCandidate,
|
||||
});
|
||||
edge.points = prevPoints;
|
||||
} else {
|
||||
edge.points = cutter2(startNode, endNode, prevPoints);
|
||||
}
|
||||
}
|
||||
log.debug('PPP cutter2: Points after cutter2:', JSON.stringify(edge.points));
|
||||
const hasNaN = (pts: { x: number; y: number }[]) =>
|
||||
pts?.some((p) => !Number.isFinite(p?.x) || !Number.isFinite(p?.y));
|
||||
if (!Array.isArray(edge.points) || edge.points.length < 2 || hasNaN(edge.points)) {
|
||||
log.warn(
|
||||
'POI cutter2: Invalid points from cutter2, falling back to prevPoints',
|
||||
edge.points
|
||||
);
|
||||
// Fallback to previous points and strip any invalid ones just in case
|
||||
const cleaned = prevPoints.filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y));
|
||||
edge.points = cleaned.length >= 2 ? cleaned : prevPoints;
|
||||
}
|
||||
log.debug('UIO cutter2: Points after cutter2 (sanitized):', edge.points);
|
||||
// Remove consecutive duplicate points to avoid zero-length segments in path builders
|
||||
const deduped = edge.points.filter(
|
||||
(p: { x: number; y: number }, i: number, arr: { x: number; y: number }[]) => {
|
||||
if (i === 0) {
|
||||
return true;
|
||||
}
|
||||
const prev = arr[i - 1];
|
||||
return Math.abs(p.x - prev.x) > 1e-6 || Math.abs(p.y - prev.y) > 1e-6;
|
||||
}
|
||||
);
|
||||
if (deduped.length !== edge.points.length) {
|
||||
log.debug('UIO cutter2: removed consecutive duplicate points', {
|
||||
before: edge.points,
|
||||
after: deduped,
|
||||
});
|
||||
}
|
||||
edge.points = deduped;
|
||||
const paths = insertEdge(
|
||||
edgesEl,
|
||||
edge,
|
||||
@@ -772,8 +1059,10 @@ export const render = async (
|
||||
data4Layout.type,
|
||||
startNode,
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
data4Layout.diagramId,
|
||||
true
|
||||
);
|
||||
log.info('APA12 edge points after insert', JSON.stringify(edge.points));
|
||||
|
||||
edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2;
|
||||
edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2;
|
||||
|
@@ -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": {
|
||||
|
@@ -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".
|
||||
|
@@ -264,6 +264,9 @@ const config: RequiredDeep<MermaidConfig> = {
|
||||
radar: {
|
||||
...defaultConfigJson.radar,
|
||||
},
|
||||
usecase: {
|
||||
...defaultConfigJson.usecase,
|
||||
},
|
||||
treemap: {
|
||||
useMaxWidth: true,
|
||||
padding: 10,
|
||||
|
@@ -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
|
||||
);
|
||||
};
|
||||
|
@@ -3,6 +3,7 @@ import type * as d3 from 'd3';
|
||||
import type { SetOptional, SetRequired } from 'type-fest';
|
||||
import type { Diagram } from '../Diagram.js';
|
||||
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
|
||||
import type { DiagramOrientation } from '../diagrams/git/gitGraphTypes.js';
|
||||
|
||||
export interface DiagramMetadata {
|
||||
title?: string;
|
||||
@@ -35,7 +36,8 @@ export interface DiagramDB {
|
||||
getAccTitle?: () => string;
|
||||
setAccDescription?: (description: string) => void;
|
||||
getAccDescription?: () => string;
|
||||
|
||||
getDirection?: () => string | undefined;
|
||||
setDirection?: (dir: DiagramOrientation) => void;
|
||||
setDisplayMode?: (title: string) => void;
|
||||
bindFunctions?: (element: Element) => void;
|
||||
}
|
||||
|
48
packages/mermaid/src/diagrams/architecture/svgDraw.spec.ts
Normal file
48
packages/mermaid/src/diagrams/architecture/svgDraw.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe } from 'vitest';
|
||||
import { draw } from './architectureRenderer.js';
|
||||
import { Diagram } from '../../Diagram.js';
|
||||
import { addDetector } from '../../diagram-api/detectType.js';
|
||||
import architectureDetector from './architectureDetector.js';
|
||||
import { ensureNodeFromSelector, jsdomIt } from '../../tests/util.js';
|
||||
|
||||
const { id, detector, loader } = architectureDetector;
|
||||
|
||||
addDetector(id, detector, loader); // Add architecture schemas to Mermaid
|
||||
|
||||
describe('architecture diagram SVGs', () => {
|
||||
jsdomIt('should add ids', async () => {
|
||||
const svgNode = await drawDiagram(`
|
||||
architecture-beta
|
||||
group api(cloud)[API]
|
||||
|
||||
service db(database)[Database] in api
|
||||
service disk1(disk)[Storage] in api
|
||||
service disk2(disk)[Storage] in api
|
||||
service server(server)[Server] in api
|
||||
|
||||
db:L -- R:server
|
||||
disk1:T -- B:server
|
||||
disk2:T -- B:db
|
||||
`);
|
||||
|
||||
const nodesForGroup = svgNode.querySelectorAll(`#group-api`);
|
||||
expect(nodesForGroup.length).toBe(1);
|
||||
|
||||
const serviceIds = [...svgNode.querySelectorAll(`[id^=service-]`)].map(({ id }) => id).sort();
|
||||
expect(serviceIds).toStrictEqual([
|
||||
'service-db',
|
||||
'service-disk1',
|
||||
'service-disk2',
|
||||
'service-server',
|
||||
]);
|
||||
|
||||
const edgeIds = [...svgNode.querySelectorAll(`.edge[id^=L_]`)].map(({ id }) => id).sort();
|
||||
expect(edgeIds).toStrictEqual(['L_db_server_0', 'L_disk1_server_0', 'L_disk2_db_0']);
|
||||
});
|
||||
});
|
||||
|
||||
async function drawDiagram(diagramText: string): Promise<Element> {
|
||||
const diagram = await Diagram.fromText(diagramText, {});
|
||||
await draw('NOT_USED', 'svg', '1.0.0', diagram);
|
||||
return ensureNodeFromSelector('#svg');
|
||||
}
|
@@ -20,6 +20,7 @@ import {
|
||||
type ArchitectureJunction,
|
||||
type ArchitectureService,
|
||||
} from './architectureTypes.js';
|
||||
import { getEdgeId } from '../../utils.js';
|
||||
|
||||
export const drawEdges = async function (
|
||||
edgesEl: D3Element,
|
||||
@@ -91,7 +92,8 @@ export const drawEdges = async function (
|
||||
|
||||
g.insert('path')
|
||||
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
|
||||
.attr('class', 'edge');
|
||||
.attr('class', 'edge')
|
||||
.attr('id', getEdgeId(source, target, { prefix: 'L' }));
|
||||
|
||||
if (sourceArrow) {
|
||||
const xShift = isArchitectureDirectionX(sourceDir)
|
||||
@@ -206,8 +208,9 @@ export const drawGroups = async function (
|
||||
if (data.type === 'group') {
|
||||
const { h, w, x1, y1 } = node.boundingBox();
|
||||
|
||||
groupsEl
|
||||
.append('rect')
|
||||
const groupsNode = groupsEl.append('rect');
|
||||
groupsNode
|
||||
.attr('id', `group-${data.id}`)
|
||||
.attr('x', x1 + halfIconSize)
|
||||
.attr('y', y1 + halfIconSize)
|
||||
.attr('width', w)
|
||||
@@ -262,6 +265,7 @@ export const drawGroups = async function (
|
||||
')'
|
||||
);
|
||||
}
|
||||
db.setElementForId(data.id, groupsNode);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -342,9 +346,9 @@ export const drawServices = async function (
|
||||
);
|
||||
}
|
||||
|
||||
serviceElem.attr('class', 'architecture-service');
|
||||
serviceElem.attr('id', `service-${service.id}`).attr('class', 'architecture-service');
|
||||
|
||||
const { width, height } = serviceElem._groups[0][0].getBBox();
|
||||
const { width, height } = serviceElem.node().getBBox();
|
||||
service.width = width;
|
||||
service.height = height;
|
||||
db.setElementForId(service.id, serviceElem);
|
||||
|
@@ -36,57 +36,15 @@ export async function textHelper<T extends SVGGraphicsElement>(
|
||||
|
||||
annotationGroup = shapeSvg.insert('g').attr('class', 'annotation-group text');
|
||||
if (node.annotations.length > 0) {
|
||||
const annotation = node.annotations[0].toLowerCase();
|
||||
let isSupported = false;
|
||||
switch (annotation) {
|
||||
case 'interface':
|
||||
case 'abstract':
|
||||
case 'enumeration':
|
||||
isSupported = true;
|
||||
break;
|
||||
}
|
||||
if (!isSupported) {
|
||||
await addText(
|
||||
annotationGroup,
|
||||
{ text: `«${node.annotations[0]}»` } as unknown as ClassMember,
|
||||
0,
|
||||
[]
|
||||
);
|
||||
annotationGroup.style('opacity', '1');
|
||||
}
|
||||
const annotation = node.annotations[0];
|
||||
await addText(annotationGroup, { text: `«${annotation}»` } as unknown as ClassMember, 0);
|
||||
|
||||
const annotationGroupBBox = annotationGroup.node()!.getBBox();
|
||||
annotationGroupHeight = annotationGroupBBox.height;
|
||||
}
|
||||
|
||||
labelGroup = shapeSvg.insert('g').attr('class', 'label-group text');
|
||||
|
||||
// Determine styling based on annotations
|
||||
let labelStyles = [''];
|
||||
let labelClass = '';
|
||||
if (node.annotations && node.annotations.length > 0) {
|
||||
const annotation = node.annotations[0].toLowerCase();
|
||||
switch (annotation) {
|
||||
case 'abstract':
|
||||
labelClass = 'abstract';
|
||||
labelStyles = [];
|
||||
break;
|
||||
case 'enumeration':
|
||||
labelClass = 'enumeration';
|
||||
labelStyles = [];
|
||||
break;
|
||||
case 'interface':
|
||||
labelClass = 'interface';
|
||||
labelStyles = [];
|
||||
break;
|
||||
default:
|
||||
labelClass = '';
|
||||
labelStyles = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Apply the CSS class to the label group
|
||||
labelGroup.attr('class', `label-group text classTitle ${labelClass}`);
|
||||
|
||||
await addText(labelGroup, node, 0, labelStyles);
|
||||
await addText(labelGroup, node, 0, ['font-weight: bolder']);
|
||||
const labelGroupBBox = labelGroup.node()!.getBBox();
|
||||
labelGroupHeight = labelGroupBBox.height;
|
||||
|
||||
@@ -113,7 +71,7 @@ export async function textHelper<T extends SVGGraphicsElement>(
|
||||
// Center annotation
|
||||
if (annotationGroup !== null) {
|
||||
const annotationGroupBBox = annotationGroup.node()!.getBBox();
|
||||
annotationGroup.attr('transform', `translate(${-annotationGroupBBox.width / 2}, 0)`);
|
||||
annotationGroup.attr('transform', `translate(${-annotationGroupBBox.width / 2})`);
|
||||
}
|
||||
|
||||
// Adjust label
|
||||
|
@@ -31,7 +31,7 @@ const getStyles = (options) =>
|
||||
}
|
||||
|
||||
.classTitle {
|
||||
font-weight: normal;
|
||||
font-weight: bolder;
|
||||
}
|
||||
.node rect,
|
||||
.node circle,
|
||||
@@ -148,20 +148,6 @@ g.classGroup line {
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.classTitle.abstract {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.classTitle.enumeration {
|
||||
text-decoration: underline;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.classTitle.interface {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.edgeTerminals {
|
||||
font-size: 11px;
|
||||
|
@@ -268,7 +268,9 @@ const fixTaskDates = function (startTime, endTime, dateFormat, excludes, include
|
||||
|
||||
const getStartDate = function (prevTime, dateFormat, str) {
|
||||
str = str.trim();
|
||||
|
||||
if ((dateFormat.trim() === 'x' || dateFormat.trim() === 'X') && /^\d+$/.test(str)) {
|
||||
return new Date(Number(str));
|
||||
}
|
||||
// Test for after
|
||||
const afterRePattern = /^after\s+(?<ids>[\d\w- ]+)/;
|
||||
const afterStatement = afterRePattern.exec(str);
|
||||
|
@@ -37,6 +37,7 @@ export class MindmapDB {
|
||||
private nodes: MindmapNode[] = [];
|
||||
private count = 0;
|
||||
private elements: Record<number, D3Element> = {};
|
||||
private baseLevel?: number;
|
||||
public readonly nodeType: typeof nodeType;
|
||||
|
||||
constructor() {
|
||||
@@ -54,6 +55,7 @@ export class MindmapDB {
|
||||
this.nodes = [];
|
||||
this.count = 0;
|
||||
this.elements = {};
|
||||
this.baseLevel = undefined;
|
||||
}
|
||||
|
||||
public getParent(level: number): MindmapNode | null {
|
||||
@@ -72,6 +74,17 @@ export class MindmapDB {
|
||||
public addNode(level: number, id: string, descr: string, type: number): void {
|
||||
log.info('addNode', level, id, descr, type);
|
||||
|
||||
let isRoot = false;
|
||||
|
||||
if (this.nodes.length === 0) {
|
||||
this.baseLevel = level;
|
||||
level = 0;
|
||||
isRoot = true;
|
||||
} else if (this.baseLevel !== undefined) {
|
||||
level = level - this.baseLevel;
|
||||
isRoot = false;
|
||||
}
|
||||
|
||||
const conf = getConfig();
|
||||
let padding = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
||||
|
||||
@@ -92,6 +105,7 @@ export class MindmapDB {
|
||||
children: [],
|
||||
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
||||
padding,
|
||||
isRoot,
|
||||
};
|
||||
|
||||
const parent = this.getParent(level);
|
||||
@@ -99,7 +113,7 @@ export class MindmapDB {
|
||||
parent.children.push(node);
|
||||
this.nodes.push(node);
|
||||
} else {
|
||||
if (this.nodes.length === 0) {
|
||||
if (isRoot) {
|
||||
this.nodes.push(node);
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -204,8 +218,7 @@ export class MindmapDB {
|
||||
// Build CSS classes for the node
|
||||
const cssClasses = ['mindmap-node'];
|
||||
|
||||
// Add section-specific classes
|
||||
if (node.level === 0) {
|
||||
if (node.isRoot === true) {
|
||||
// Root node gets special classes
|
||||
cssClasses.push('section-root', 'section--1');
|
||||
} else if (node.section !== undefined) {
|
||||
|
@@ -15,6 +15,7 @@ export interface MindmapNode {
|
||||
icon?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
isRoot?: boolean;
|
||||
}
|
||||
|
||||
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
||||
|
40
packages/mermaid/src/diagrams/usecase/styles.ts
Normal file
40
packages/mermaid/src/diagrams/usecase/styles.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const getStyles = (options: any) =>
|
||||
`
|
||||
.actor {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.actor-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.usecase {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.usecase-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.relationship {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.relationship-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
98
packages/mermaid/src/diagrams/usecase/usecase.spec.ts
Normal file
98
packages/mermaid/src/diagrams/usecase/usecase.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
175
packages/mermaid/src/diagrams/usecase/usecaseDb.ts
Normal file
175
packages/mermaid/src/diagrams/usecase/usecaseDb.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { log } from '../../logger.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
} from '../common/commonDb.js';
|
||||
import type {
|
||||
UsecaseFields,
|
||||
UsecaseDB,
|
||||
Actor,
|
||||
UseCase,
|
||||
SystemBoundary,
|
||||
Relationship,
|
||||
} from './usecaseTypes.js';
|
||||
import type { RequiredDeep } from 'type-fest';
|
||||
import type { UsecaseDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
|
||||
export const DEFAULT_USECASE_CONFIG: Required<UsecaseDiagramConfig> = DEFAULT_CONFIG.usecase;
|
||||
|
||||
export const DEFAULT_USECASE_DB: RequiredDeep<UsecaseFields> = {
|
||||
actors: new Map(),
|
||||
useCases: new Map(),
|
||||
systemBoundaries: new Map(),
|
||||
relationships: [],
|
||||
config: DEFAULT_USECASE_CONFIG,
|
||||
} as const;
|
||||
|
||||
let actors = new Map<string, Actor>();
|
||||
let useCases = new Map<string, UseCase>();
|
||||
let systemBoundaries = new Map<string, SystemBoundary>();
|
||||
let relationships: Relationship[] = [];
|
||||
const config: Required<UsecaseDiagramConfig> = structuredClone(DEFAULT_USECASE_CONFIG);
|
||||
|
||||
const getConfig = (): Required<UsecaseDiagramConfig> => structuredClone(config);
|
||||
|
||||
const clear = (): void => {
|
||||
actors = new Map();
|
||||
useCases = new Map();
|
||||
systemBoundaries = new Map();
|
||||
relationships = [];
|
||||
commonClear();
|
||||
};
|
||||
|
||||
// Actor management
|
||||
const addActor = (actor: Actor): void => {
|
||||
if (!actor.id || !actor.name) {
|
||||
throw new Error(
|
||||
`Invalid actor: Actor must have both id and name. Received: ${JSON.stringify(actor)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!actors.has(actor.id)) {
|
||||
actors.set(actor.id, actor);
|
||||
log.debug(`Added actor: ${actor.id} (${actor.name})`);
|
||||
} else {
|
||||
log.debug(`Actor ${actor.id} already exists`);
|
||||
}
|
||||
};
|
||||
|
||||
const getActors = (): Map<string, Actor> => actors;
|
||||
|
||||
const getActor = (id: string): Actor | undefined => actors.get(id);
|
||||
|
||||
// UseCase management
|
||||
const addUseCase = (useCase: UseCase): void => {
|
||||
if (!useCase.id || !useCase.name) {
|
||||
throw new Error(
|
||||
`Invalid use case: Use case must have both id and name. Received: ${JSON.stringify(useCase)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!useCases.has(useCase.id)) {
|
||||
useCases.set(useCase.id, useCase);
|
||||
log.debug(`Added use case: ${useCase.id} (${useCase.name})`);
|
||||
} else {
|
||||
log.debug(`Use case ${useCase.id} already exists`);
|
||||
}
|
||||
};
|
||||
|
||||
const getUseCases = (): Map<string, UseCase> => useCases;
|
||||
|
||||
const getUseCase = (id: string): UseCase | undefined => useCases.get(id);
|
||||
|
||||
// SystemBoundary management
|
||||
const addSystemBoundary = (systemBoundary: SystemBoundary): void => {
|
||||
if (!systemBoundary.id || !systemBoundary.name) {
|
||||
throw new Error(
|
||||
`Invalid system boundary: System boundary must have both id and name. Received: ${JSON.stringify(systemBoundary)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!systemBoundaries.has(systemBoundary.id)) {
|
||||
systemBoundaries.set(systemBoundary.id, systemBoundary);
|
||||
log.debug(`Added system boundary: ${systemBoundary.name}`);
|
||||
} else {
|
||||
log.debug(`System boundary ${systemBoundary.id} already exists`);
|
||||
}
|
||||
};
|
||||
|
||||
const getSystemBoundaries = (): Map<string, SystemBoundary> => systemBoundaries;
|
||||
|
||||
const getSystemBoundary = (id: string): SystemBoundary | undefined => systemBoundaries.get(id);
|
||||
|
||||
// Relationship management
|
||||
const addRelationship = (relationship: Relationship): void => {
|
||||
// Validate relationship structure
|
||||
if (!relationship.id || !relationship.from || !relationship.to) {
|
||||
throw new Error(
|
||||
`Invalid relationship: Relationship must have id, from, and to fields. Received: ${JSON.stringify(relationship)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!relationship.type) {
|
||||
throw new Error(
|
||||
`Invalid relationship: Relationship must have a type. Received: ${JSON.stringify(relationship)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate relationship type
|
||||
const validTypes = ['association', 'include', 'extend'];
|
||||
if (!validTypes.includes(relationship.type)) {
|
||||
throw new Error(
|
||||
`Invalid relationship type: ${relationship.type}. Valid types are: ${validTypes.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate arrow type if present
|
||||
if (relationship.arrowType !== undefined) {
|
||||
const validArrowTypes = [0, 1, 2]; // SOLID_ARROW, BACK_ARROW, LINE_SOLID
|
||||
if (!validArrowTypes.includes(relationship.arrowType)) {
|
||||
throw new Error(
|
||||
`Invalid arrow type: ${relationship.arrowType}. Valid arrow types are: ${validArrowTypes.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
relationships.push(relationship);
|
||||
log.debug(
|
||||
`Added relationship: ${relationship.from} -> ${relationship.to} (${relationship.type})`
|
||||
);
|
||||
};
|
||||
|
||||
const getRelationships = (): Relationship[] => relationships;
|
||||
|
||||
export const db: UsecaseDB = {
|
||||
getConfig,
|
||||
|
||||
clear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
|
||||
addActor,
|
||||
getActors,
|
||||
getActor,
|
||||
|
||||
addUseCase,
|
||||
getUseCases,
|
||||
getUseCase,
|
||||
|
||||
addSystemBoundary,
|
||||
getSystemBoundaries,
|
||||
getSystemBoundary,
|
||||
|
||||
addRelationship,
|
||||
getRelationships,
|
||||
};
|
22
packages/mermaid/src/diagrams/usecase/usecaseDetector.ts
Normal file
22
packages/mermaid/src/diagrams/usecase/usecaseDetector.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'usecase';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*usecase/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./usecaseDiagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
export const usecase: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
12
packages/mermaid/src/diagrams/usecase/usecaseDiagram.ts
Normal file
12
packages/mermaid/src/diagrams/usecase/usecaseDiagram.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { parser } from './usecaseParser.js';
|
||||
import { db } from './usecaseDb.js';
|
||||
import { renderer } from './usecaseRenderer.js';
|
||||
import styles from './styles.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
styles,
|
||||
};
|
150
packages/mermaid/src/diagrams/usecase/usecaseParser.ts
Normal file
150
packages/mermaid/src/diagrams/usecase/usecaseParser.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
// Import ANTLR parser from the parser package
|
||||
import { parse } from '@mermaid-js/parser';
|
||||
import { log } from '../../logger.js';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import type {
|
||||
UsecaseDB,
|
||||
Actor,
|
||||
UseCase,
|
||||
SystemBoundary,
|
||||
Relationship,
|
||||
ArrowType,
|
||||
} from './usecaseTypes.js';
|
||||
import { db } from './usecaseDb.js';
|
||||
|
||||
// ANTLR parser result interface
|
||||
interface UsecaseParseResult {
|
||||
actors: { id: string; name: string; metadata?: Record<string, string> }[];
|
||||
useCases: { id: string; name: string; nodeId?: string; systemBoundary?: string }[];
|
||||
systemBoundaries: { id: string; name: string; useCases: string[]; type?: 'package' | 'rect' }[];
|
||||
relationships: {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'association' | 'include' | 'extend';
|
||||
arrowType: number;
|
||||
label?: string;
|
||||
}[];
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse usecase diagram using ANTLR parser
|
||||
*/
|
||||
const parseUsecaseWithAntlr = async (input: string): Promise<UsecaseParseResult> => {
|
||||
// Use the ANTLR parser from @mermaid-js/parser
|
||||
const result = (await parse('usecase', input)) as UsecaseParseResult;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Populate the database with parsed ANTLR results
|
||||
*/
|
||||
const populateDb = (ast: UsecaseParseResult, db: UsecaseDB) => {
|
||||
// Clear existing data
|
||||
db.clear();
|
||||
|
||||
// Add actors (ANTLR result already has id, name, and metadata)
|
||||
ast.actors.forEach((actorData) => {
|
||||
const actor: Actor = {
|
||||
id: actorData.id,
|
||||
name: actorData.name,
|
||||
metadata: actorData.metadata,
|
||||
};
|
||||
db.addActor(actor);
|
||||
});
|
||||
|
||||
// Add use cases (ANTLR result already has id, name, nodeId, and systemBoundary)
|
||||
ast.useCases.forEach((useCaseData) => {
|
||||
const useCase: UseCase = {
|
||||
id: useCaseData.id,
|
||||
name: useCaseData.name,
|
||||
nodeId: useCaseData.nodeId,
|
||||
systemBoundary: useCaseData.systemBoundary,
|
||||
};
|
||||
db.addUseCase(useCase);
|
||||
});
|
||||
|
||||
// Add system boundaries
|
||||
if (ast.systemBoundaries) {
|
||||
ast.systemBoundaries.forEach((boundaryData) => {
|
||||
const systemBoundary: SystemBoundary = {
|
||||
id: boundaryData.id,
|
||||
name: boundaryData.name,
|
||||
useCases: boundaryData.useCases,
|
||||
type: boundaryData.type || 'rect', // default to 'rect' if not specified
|
||||
};
|
||||
db.addSystemBoundary(systemBoundary);
|
||||
});
|
||||
}
|
||||
|
||||
// Add relationships (ANTLR result already has proper structure)
|
||||
ast.relationships.forEach((relationshipData) => {
|
||||
const relationship: Relationship = {
|
||||
id: relationshipData.id,
|
||||
from: relationshipData.from,
|
||||
to: relationshipData.to,
|
||||
type: relationshipData.type,
|
||||
arrowType: relationshipData.arrowType as ArrowType,
|
||||
label: relationshipData.label,
|
||||
};
|
||||
db.addRelationship(relationship);
|
||||
});
|
||||
|
||||
log.debug('Populated usecase database:', {
|
||||
actors: ast.actors.length,
|
||||
useCases: ast.useCases.length,
|
||||
relationships: ast.relationships.length,
|
||||
});
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parse: async (input: string): Promise<void> => {
|
||||
log.debug('Parsing usecase diagram with ANTLR:', input);
|
||||
|
||||
try {
|
||||
// Use our ANTLR parser
|
||||
const ast: UsecaseParseResult = await parseUsecaseWithAntlr(input);
|
||||
log.debug('ANTLR parsing result:', ast);
|
||||
|
||||
// Populate common database fields
|
||||
populateCommonDb(ast as any, db);
|
||||
|
||||
// Populate the database with validation
|
||||
populateDb(ast, db);
|
||||
|
||||
log.debug('Usecase diagram parsing completed successfully');
|
||||
} catch (error) {
|
||||
log.error('Error parsing usecase diagram:', error);
|
||||
|
||||
// Check if it's a UsecaseParseError from our ANTLR parser
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'name' in error &&
|
||||
error.name === 'UsecaseParseError'
|
||||
) {
|
||||
// Re-throw the detailed error for better error reporting
|
||||
throw error;
|
||||
}
|
||||
|
||||
// For other errors, wrap them in a generic error
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
const wrappedError = new Error(`Failed to parse usecase diagram: ${errorMessage}`);
|
||||
|
||||
// Add hash property for consistency with other diagram types
|
||||
(wrappedError as any).hash = {
|
||||
text: input.split('\n')[0] || '',
|
||||
token: 'unknown',
|
||||
line: '1',
|
||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
||||
expected: ['valid usecase syntax'],
|
||||
};
|
||||
|
||||
throw wrappedError;
|
||||
}
|
||||
},
|
||||
};
|
545
packages/mermaid/src/diagrams/usecase/usecaseRenderer.ts
Normal file
545
packages/mermaid/src/diagrams/usecase/usecaseRenderer.ts
Normal file
@@ -0,0 +1,545 @@
|
||||
import type { DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import {
|
||||
insertEdgeLabel,
|
||||
positionEdgeLabel,
|
||||
} from '../../rendering-util/rendering-elements/edges.js';
|
||||
import { db } from './usecaseDb.js';
|
||||
import { ARROW_TYPE } from './usecaseTypes.js';
|
||||
import type { Actor, UseCase, SystemBoundary, Relationship } from './usecaseTypes.js';
|
||||
|
||||
// Layout constants
|
||||
const ACTOR_WIDTH = 80;
|
||||
const ACTOR_HEIGHT = 100;
|
||||
const USECASE_WIDTH = 120;
|
||||
const USECASE_HEIGHT = 60;
|
||||
const SYSTEM_BOUNDARY_PADDING = 30;
|
||||
const MARGIN = 50;
|
||||
const SPACING_X = 200;
|
||||
const SPACING_Y = 150;
|
||||
|
||||
/**
|
||||
* Get actor styling based on metadata
|
||||
*/
|
||||
const getActorStyling = (metadata?: Record<string, string>) => {
|
||||
const defaults = {
|
||||
fillColor: 'none',
|
||||
strokeColor: 'black',
|
||||
strokeWidth: 2,
|
||||
type: 'solid',
|
||||
};
|
||||
|
||||
if (!metadata) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
return {
|
||||
fillColor: metadata.type === 'hollow' ? 'none' : metadata.fillColor || defaults.fillColor,
|
||||
strokeColor: metadata.strokeColor || defaults.strokeColor,
|
||||
strokeWidth: parseInt(metadata.strokeWidth || '2', 10),
|
||||
type: metadata.type || defaults.type,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw an actor (stick figure) with metadata support
|
||||
*/
|
||||
const drawActor = (group: SVGGroup, actor: Actor, x: number, y: number): void => {
|
||||
const actorGroup = group.append('g').attr('class', 'actor').attr('id', actor.id);
|
||||
const styling = getActorStyling(actor.metadata);
|
||||
|
||||
// Add metadata as data attributes for CSS styling
|
||||
if (actor.metadata) {
|
||||
Object.entries(actor.metadata).forEach(([key, value]) => {
|
||||
actorGroup.attr(`data-${key}`, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we should render an icon instead of stick figure
|
||||
if (actor.metadata?.icon) {
|
||||
drawActorWithIcon(actorGroup, actor, x, y, styling);
|
||||
} else {
|
||||
drawStickFigure(actorGroup, actor, x, y, styling);
|
||||
}
|
||||
|
||||
// Actor name (always rendered)
|
||||
const displayName = actor.metadata?.name ?? actor.name;
|
||||
actorGroup
|
||||
.append('text')
|
||||
.attr('x', x)
|
||||
.attr('y', y + 50)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('class', 'actor-label')
|
||||
.text(displayName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw traditional stick figure
|
||||
*/
|
||||
const drawStickFigure = (
|
||||
actorGroup: SVGGroup,
|
||||
_actor: Actor,
|
||||
x: number,
|
||||
y: number,
|
||||
styling: ReturnType<typeof getActorStyling>
|
||||
): void => {
|
||||
// Head (circle)
|
||||
actorGroup
|
||||
.append('circle')
|
||||
.attr('cx', x)
|
||||
.attr('cy', y - 30)
|
||||
.attr('r', 8)
|
||||
.attr('fill', styling.fillColor)
|
||||
.attr('stroke', styling.strokeColor)
|
||||
.attr('stroke-width', styling.strokeWidth);
|
||||
|
||||
// Body (line)
|
||||
actorGroup
|
||||
.append('line')
|
||||
.attr('x1', x)
|
||||
.attr('y1', y - 22)
|
||||
.attr('x2', x)
|
||||
.attr('y2', y + 10)
|
||||
.attr('stroke', styling.strokeColor)
|
||||
.attr('stroke-width', styling.strokeWidth);
|
||||
|
||||
// Arms (line)
|
||||
actorGroup
|
||||
.append('line')
|
||||
.attr('x1', x - 15)
|
||||
.attr('y1', y - 10)
|
||||
.attr('x2', x + 15)
|
||||
.attr('y2', y - 10)
|
||||
.attr('stroke', styling.strokeColor)
|
||||
.attr('stroke-width', styling.strokeWidth);
|
||||
|
||||
// Left leg
|
||||
actorGroup
|
||||
.append('line')
|
||||
.attr('x1', x)
|
||||
.attr('y1', y + 10)
|
||||
.attr('x2', x - 15)
|
||||
.attr('y2', y + 30)
|
||||
.attr('stroke', styling.strokeColor)
|
||||
.attr('stroke-width', styling.strokeWidth);
|
||||
|
||||
// Right leg
|
||||
actorGroup
|
||||
.append('line')
|
||||
.attr('x1', x)
|
||||
.attr('y1', y + 10)
|
||||
.attr('x2', x + 15)
|
||||
.attr('y2', y + 30)
|
||||
.attr('stroke', styling.strokeColor)
|
||||
.attr('stroke-width', styling.strokeWidth);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw actor with icon representation
|
||||
*/
|
||||
const drawActorWithIcon = (
|
||||
actorGroup: SVGGroup,
|
||||
actor: Actor,
|
||||
x: number,
|
||||
y: number,
|
||||
styling: ReturnType<typeof getActorStyling>
|
||||
): void => {
|
||||
const iconName = actor.metadata?.icon ?? 'user';
|
||||
|
||||
// Create a rectangle background for the icon
|
||||
actorGroup
|
||||
.append('rect')
|
||||
.attr('x', x - 20)
|
||||
.attr('y', y - 35)
|
||||
.attr('width', 40)
|
||||
.attr('height', 40)
|
||||
.attr('rx', 5)
|
||||
.attr('fill', styling.fillColor === 'none' ? 'white' : styling.fillColor)
|
||||
.attr('stroke', styling.strokeColor)
|
||||
.attr('stroke-width', styling.strokeWidth);
|
||||
|
||||
// Add icon text (could be enhanced to use actual icons/SVG symbols)
|
||||
actorGroup
|
||||
.append('text')
|
||||
.attr('x', x)
|
||||
.attr('y', y - 10)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('class', 'actor-icon')
|
||||
.attr('font-size', '16px')
|
||||
.attr('font-weight', 'bold')
|
||||
.text(getIconSymbol(iconName));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get symbol representation for common icons
|
||||
*/
|
||||
const getIconSymbol = (iconName: string): string => {
|
||||
const iconMap: Record<string, string> = {
|
||||
user: '👤',
|
||||
admin: '👑',
|
||||
system: '⚙️',
|
||||
database: '🗄️',
|
||||
api: '🔌',
|
||||
service: '🔧',
|
||||
client: '💻',
|
||||
server: '🖥️',
|
||||
mobile: '📱',
|
||||
web: '🌐',
|
||||
default: '👤',
|
||||
};
|
||||
|
||||
return iconMap[iconName.toLowerCase()] || iconMap.default;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw a use case (oval)
|
||||
*/
|
||||
const drawUseCase = (group: SVGGroup, useCase: UseCase, x: number, y: number): void => {
|
||||
const useCaseGroup = group.append('g').attr('class', 'usecase').attr('id', useCase.id);
|
||||
|
||||
// Oval
|
||||
useCaseGroup
|
||||
.append('ellipse')
|
||||
.attr('cx', x)
|
||||
.attr('cy', y)
|
||||
.attr('rx', USECASE_WIDTH / 2)
|
||||
.attr('ry', USECASE_HEIGHT / 2)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Use case name
|
||||
useCaseGroup
|
||||
.append('text')
|
||||
.attr('x', x)
|
||||
.attr('y', y + 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('class', 'usecase-label')
|
||||
.text(useCase.name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw a system boundary (supports both 'rect' and 'package' types)
|
||||
*/
|
||||
const drawSystemBoundary = (
|
||||
group: SVGGroup,
|
||||
boundary: SystemBoundary,
|
||||
useCasePositions: Map<string, { x: number; y: number }>
|
||||
): void => {
|
||||
// Calculate boundary dimensions based on contained use cases
|
||||
const containedUseCases = boundary.useCases
|
||||
.map((ucId) => useCasePositions.get(ucId))
|
||||
.filter((pos) => pos !== undefined) as { x: number; y: number }[];
|
||||
|
||||
if (containedUseCases.length === 0) {
|
||||
return; // No use cases to bound
|
||||
}
|
||||
|
||||
// Find min/max coordinates of contained use cases
|
||||
const minX =
|
||||
Math.min(...containedUseCases.map((pos) => pos.x)) -
|
||||
USECASE_WIDTH / 2 -
|
||||
SYSTEM_BOUNDARY_PADDING;
|
||||
const maxX =
|
||||
Math.max(...containedUseCases.map((pos) => pos.x)) +
|
||||
USECASE_WIDTH / 2 +
|
||||
SYSTEM_BOUNDARY_PADDING;
|
||||
const minY =
|
||||
Math.min(...containedUseCases.map((pos) => pos.y)) -
|
||||
USECASE_HEIGHT / 2 -
|
||||
SYSTEM_BOUNDARY_PADDING;
|
||||
const maxY =
|
||||
Math.max(...containedUseCases.map((pos) => pos.y)) +
|
||||
USECASE_HEIGHT / 2 +
|
||||
SYSTEM_BOUNDARY_PADDING;
|
||||
|
||||
const boundaryType = boundary.type || 'rect'; // default to 'rect'
|
||||
const boundaryGroup = group.append('g').attr('class', 'system-boundary').attr('id', boundary.id);
|
||||
|
||||
if (boundaryType === 'package') {
|
||||
drawPackageBoundary(boundaryGroup, boundary, minX, minY, maxX, maxY);
|
||||
} else {
|
||||
drawRectBoundary(boundaryGroup, boundary, minX, minY, maxX, maxY);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw rect-type system boundary (simple dashed rectangle)
|
||||
*/
|
||||
const drawRectBoundary = (
|
||||
boundaryGroup: SVGGroup,
|
||||
boundary: SystemBoundary,
|
||||
minX: number,
|
||||
minY: number,
|
||||
maxX: number,
|
||||
maxY: number
|
||||
): void => {
|
||||
// Draw dashed rectangle
|
||||
boundaryGroup
|
||||
.append('rect')
|
||||
.attr('x', minX)
|
||||
.attr('y', minY)
|
||||
.attr('width', maxX - minX)
|
||||
.attr('height', maxY - minY)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,5');
|
||||
// .attr('rx', 10)
|
||||
// .attr('ry', 10);
|
||||
|
||||
// Draw boundary label at top-left
|
||||
boundaryGroup
|
||||
.append('text')
|
||||
.attr('x', minX + 10)
|
||||
.attr('y', minY + 20)
|
||||
.attr('class', 'system-boundary-label')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '14px')
|
||||
.text(boundary.name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw package-type system boundary (rectangle with separate name box)
|
||||
*/
|
||||
const drawPackageBoundary = (
|
||||
boundaryGroup: SVGGroup,
|
||||
boundary: SystemBoundary,
|
||||
minX: number,
|
||||
minY: number,
|
||||
maxX: number,
|
||||
maxY: number
|
||||
): void => {
|
||||
// Calculate name box dimensions
|
||||
const nameBoxWidth = Math.max(80, boundary.name.length * 8 + 20);
|
||||
const nameBoxHeight = 25;
|
||||
|
||||
// Draw main boundary rectangle
|
||||
boundaryGroup
|
||||
.append('rect')
|
||||
.attr('x', minX)
|
||||
.attr('y', minY)
|
||||
.attr('width', maxX - minX)
|
||||
.attr('height', maxY - minY)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw name box (package tab)
|
||||
boundaryGroup
|
||||
.append('rect')
|
||||
.attr('x', minX)
|
||||
.attr('y', minY - nameBoxHeight)
|
||||
.attr('width', nameBoxWidth)
|
||||
.attr('height', nameBoxHeight)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
// .attr('rx', 5)
|
||||
// .attr('ry', 5);
|
||||
|
||||
// Draw boundary label in the name box
|
||||
boundaryGroup
|
||||
.append('text')
|
||||
.attr('x', minX + nameBoxWidth / 2)
|
||||
.attr('y', minY - nameBoxHeight / 2 + 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('class', 'system-boundary-label')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '14px')
|
||||
.text(boundary.name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw a relationship (line with arrow)
|
||||
*/
|
||||
const drawRelationship = async (
|
||||
group: SVGGroup,
|
||||
relationship: Relationship,
|
||||
fromPos: { x: number; y: number },
|
||||
toPos: { x: number; y: number }
|
||||
): Promise<void> => {
|
||||
const relationshipGroup = group
|
||||
.append('g')
|
||||
.attr('class', 'relationship')
|
||||
.attr('id', relationship.id);
|
||||
|
||||
// Calculate arrow direction
|
||||
const dx = toPos.x - fromPos.x;
|
||||
const dy = toPos.y - fromPos.y;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
const unitX = dx / length;
|
||||
const unitY = dy / length;
|
||||
|
||||
// Adjust start and end points to avoid overlapping with shapes
|
||||
const startX = fromPos.x + unitX * 40;
|
||||
const startY = fromPos.y + unitY * 40;
|
||||
const endX = toPos.x - unitX * 60;
|
||||
const endY = toPos.y - unitY * 30;
|
||||
|
||||
// Main line
|
||||
relationshipGroup
|
||||
.append('line')
|
||||
.attr('x1', startX)
|
||||
.attr('y1', startY)
|
||||
.attr('x2', endX)
|
||||
.attr('y2', endY)
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw arrow based on arrow type
|
||||
const arrowSize = 10;
|
||||
|
||||
if (relationship.arrowType === ARROW_TYPE.SOLID_ARROW) {
|
||||
// Forward arrow (-->)
|
||||
const arrowX1 = endX - arrowSize * unitX - arrowSize * unitY * 0.5;
|
||||
const arrowY1 = endY - arrowSize * unitY + arrowSize * unitX * 0.5;
|
||||
const arrowX2 = endX - arrowSize * unitX + arrowSize * unitY * 0.5;
|
||||
const arrowY2 = endY - arrowSize * unitY - arrowSize * unitX * 0.5;
|
||||
|
||||
relationshipGroup
|
||||
.append('polygon')
|
||||
.attr('points', `${endX},${endY} ${arrowX1},${arrowY1} ${arrowX2},${arrowY2}`)
|
||||
.attr('fill', 'black');
|
||||
} else if (relationship.arrowType === ARROW_TYPE.BACK_ARROW) {
|
||||
// Backward arrow (<--)
|
||||
const arrowX1 = startX + arrowSize * unitX - arrowSize * unitY * 0.5;
|
||||
const arrowY1 = startY + arrowSize * unitY + arrowSize * unitX * 0.5;
|
||||
const arrowX2 = startX + arrowSize * unitX + arrowSize * unitY * 0.5;
|
||||
const arrowY2 = startY + arrowSize * unitY - arrowSize * unitX * 0.5;
|
||||
|
||||
relationshipGroup
|
||||
.append('polygon')
|
||||
.attr('points', `${startX},${startY} ${arrowX1},${arrowY1} ${arrowX2},${arrowY2}`)
|
||||
.attr('fill', 'black');
|
||||
}
|
||||
// For LINE_SOLID (--), no arrow head is drawn
|
||||
|
||||
// Enhanced edge label rendering (if present)
|
||||
if (relationship.label) {
|
||||
const midX = (startX + endX) / 2;
|
||||
const midY = (startY + endY) / 2;
|
||||
|
||||
// Create edge data structure compatible with the edge label system
|
||||
const edgeData = {
|
||||
id: relationship.id,
|
||||
label: relationship.label,
|
||||
labelStyle: 'stroke: #333; stroke-width: 1.5px; fill: none;',
|
||||
x: midX,
|
||||
y: midY,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
// Use the proper edge label rendering system
|
||||
await insertEdgeLabel(relationshipGroup, edgeData);
|
||||
|
||||
// Position the edge label at the midpoint
|
||||
const points = [
|
||||
{ x: startX, y: startY },
|
||||
{ x: midX, y: midY },
|
||||
{ x: endX, y: endY },
|
||||
];
|
||||
|
||||
positionEdgeLabel(edgeData, {
|
||||
originalPath: points,
|
||||
});
|
||||
} catch (error) {
|
||||
// Fallback to simple text if edge label system fails
|
||||
log.warn(
|
||||
'Failed to render edge label with advanced system, falling back to simple text:',
|
||||
error
|
||||
);
|
||||
relationshipGroup
|
||||
.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('class', 'relationship-label')
|
||||
.text(relationship.label);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main draw function
|
||||
*/
|
||||
const draw: DrawDefinition = async (text, id, _version) => {
|
||||
log.debug('Rendering usecase diagram\n' + text);
|
||||
|
||||
const svg: SVG = selectSvgElement(id);
|
||||
const group: SVGGroup = svg.append('g');
|
||||
|
||||
// Get data from database
|
||||
const actors = [...db.getActors().values()];
|
||||
const useCases = [...db.getUseCases().values()];
|
||||
const systemBoundaries = [...db.getSystemBoundaries().values()];
|
||||
const relationships = db.getRelationships();
|
||||
|
||||
log.debug('Rendering data:', {
|
||||
actors: actors.length,
|
||||
useCases: useCases.length,
|
||||
systemBoundaries: systemBoundaries.length,
|
||||
relationships: relationships.length,
|
||||
});
|
||||
|
||||
// Calculate layout
|
||||
const actorPositions = new Map<string, { x: number; y: number }>();
|
||||
const useCasePositions = new Map<string, { x: number; y: number }>();
|
||||
|
||||
// Position actors on the left
|
||||
actors.forEach((actor, index) => {
|
||||
const x = MARGIN + ACTOR_WIDTH / 2;
|
||||
const y = MARGIN + ACTOR_HEIGHT / 2 + index * SPACING_Y;
|
||||
actorPositions.set(actor.id, { x, y });
|
||||
});
|
||||
|
||||
// Position use cases on the right
|
||||
useCases.forEach((useCase, index) => {
|
||||
const x = MARGIN + SPACING_X + USECASE_WIDTH / 2;
|
||||
const y = MARGIN + USECASE_HEIGHT / 2 + index * SPACING_Y;
|
||||
useCasePositions.set(useCase.id, { x, y });
|
||||
});
|
||||
|
||||
// Draw actors
|
||||
actors.forEach((actor) => {
|
||||
const pos = actorPositions.get(actor.id)!;
|
||||
drawActor(group, actor, pos.x, pos.y);
|
||||
});
|
||||
|
||||
// Draw use cases
|
||||
useCases.forEach((useCase) => {
|
||||
const pos = useCasePositions.get(useCase.id)!;
|
||||
drawUseCase(group, useCase, pos.x, pos.y);
|
||||
});
|
||||
|
||||
// Draw system boundaries (after use cases, before relationships)
|
||||
systemBoundaries.forEach((boundary) => {
|
||||
drawSystemBoundary(group, boundary, useCasePositions);
|
||||
});
|
||||
|
||||
// Draw relationships (async to handle edge labels)
|
||||
const relationshipPromises = relationships.map(async (relationship) => {
|
||||
const fromPos =
|
||||
actorPositions.get(relationship.from) ?? useCasePositions.get(relationship.from);
|
||||
const toPos = actorPositions.get(relationship.to) ?? useCasePositions.get(relationship.to);
|
||||
|
||||
if (fromPos && toPos) {
|
||||
await drawRelationship(group, relationship, fromPos, toPos);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all relationships to be drawn
|
||||
await Promise.all(relationshipPromises);
|
||||
|
||||
// Setup viewBox and SVG dimensions properly
|
||||
const { usecase: conf } = getConfig();
|
||||
const padding = 8; // Standard padding used by other diagrams
|
||||
setupViewPortForSVG(svg, padding, 'usecaseDiagram', conf?.useMaxWidth ?? true);
|
||||
};
|
||||
|
||||
export const renderer = { draw };
|
40
packages/mermaid/src/diagrams/usecase/usecaseStyles.ts
Normal file
40
packages/mermaid/src/diagrams/usecase/usecaseStyles.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const getStyles = (options: any) =>
|
||||
`
|
||||
.actor {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.actor-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.usecase {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.usecase-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.relationship {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.relationship-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
80
packages/mermaid/src/diagrams/usecase/usecaseTypes.ts
Normal file
80
packages/mermaid/src/diagrams/usecase/usecaseTypes.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { UsecaseDiagramConfig } from '../../config.type.js';
|
||||
|
||||
export type ActorMetadata = Record<string, string>;
|
||||
|
||||
export interface Actor {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
metadata?: ActorMetadata;
|
||||
}
|
||||
|
||||
export interface UseCase {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
nodeId?: string; // Optional node ID (e.g., 'a' in 'a(Go through code)')
|
||||
systemBoundary?: string; // Optional reference to system boundary
|
||||
}
|
||||
|
||||
export type SystemBoundaryType = 'package' | 'rect';
|
||||
|
||||
export interface SystemBoundary {
|
||||
id: string;
|
||||
name: string;
|
||||
useCases: string[]; // Array of use case IDs within this boundary
|
||||
type?: SystemBoundaryType; // Type of boundary rendering (default: 'rect')
|
||||
}
|
||||
|
||||
// Arrow types for usecase diagrams (matching parser types)
|
||||
export const ARROW_TYPE = {
|
||||
SOLID_ARROW: 0, // -->
|
||||
BACK_ARROW: 1, // <--
|
||||
LINE_SOLID: 2, // --
|
||||
} as const;
|
||||
|
||||
export type ArrowType = (typeof ARROW_TYPE)[keyof typeof ARROW_TYPE];
|
||||
|
||||
export interface Relationship {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'association' | 'include' | 'extend';
|
||||
arrowType: ArrowType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface UsecaseFields {
|
||||
actors: Map<string, Actor>;
|
||||
useCases: Map<string, UseCase>;
|
||||
systemBoundaries: Map<string, SystemBoundary>;
|
||||
relationships: Relationship[];
|
||||
config: Required<UsecaseDiagramConfig>;
|
||||
}
|
||||
|
||||
export interface UsecaseDB extends DiagramDB {
|
||||
getConfig: () => Required<UsecaseDiagramConfig>;
|
||||
|
||||
// Actor management
|
||||
addActor: (actor: Actor) => void;
|
||||
getActors: () => Map<string, Actor>;
|
||||
getActor: (id: string) => Actor | undefined;
|
||||
|
||||
// UseCase management
|
||||
addUseCase: (useCase: UseCase) => void;
|
||||
getUseCases: () => Map<string, UseCase>;
|
||||
getUseCase: (id: string) => UseCase | undefined;
|
||||
|
||||
// SystemBoundary management
|
||||
addSystemBoundary: (systemBoundary: SystemBoundary) => void;
|
||||
getSystemBoundaries: () => Map<string, SystemBoundary>;
|
||||
getSystemBoundary: (id: string) => SystemBoundary | undefined;
|
||||
|
||||
// Relationship management
|
||||
addRelationship: (relationship: Relationship) => void;
|
||||
getRelationships: () => Relationship[];
|
||||
|
||||
// Utility methods
|
||||
clear: () => void;
|
||||
}
|
@@ -1,7 +1,13 @@
|
||||
import mermaid, { type MermaidConfig } from 'mermaid';
|
||||
import zenuml from '../../../../../mermaid-zenuml/dist/mermaid-zenuml.core.mjs';
|
||||
import tidyTreeLayout from '../../../../../mermaid-layout-tidy-tree/dist/mermaid-layout-tidy-tree.core.mjs';
|
||||
import layouts from '../../../../../mermaid-layout-elk/dist/mermaid-layout-elk.core.mjs';
|
||||
|
||||
const init = mermaid.registerExternalDiagrams([zenuml]);
|
||||
const init = Promise.all([
|
||||
mermaid.registerExternalDiagrams([zenuml]),
|
||||
mermaid.registerLayoutLoaders(layouts),
|
||||
mermaid.registerLayoutLoaders(tidyTreeLayout),
|
||||
]);
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: 'logos',
|
||||
|
@@ -33,7 +33,7 @@
|
||||
"pathe": "^2.0.3",
|
||||
"unocss": "^66.4.2",
|
||||
"unplugin-vue-components": "^28.4.0",
|
||||
"vite": "^6.1.1",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vitepress": "1.6.3",
|
||||
"workbox-window": "^7.3.0"
|
||||
|
@@ -20,3 +20,5 @@ Each user journey is split into sections, these describe the part of the task
|
||||
the user is trying to complete.
|
||||
|
||||
Tasks syntax is `Task name: <score>: <comma separated list of actors>`
|
||||
|
||||
Score is a number between 1 and 5, inclusive.
|
||||
|
@@ -13,6 +13,10 @@ const virtualModuleId = 'virtual:mermaid-config';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
// Vite v7 changes the default target and drops old browser support
|
||||
target: 'modules',
|
||||
},
|
||||
optimizeDeps: {
|
||||
// vitepress is aliased with replacement `join(DIST_CLIENT_PATH, '/index')`
|
||||
// This needs to be excluded from optimization
|
||||
|
@@ -4,6 +4,9 @@ import { internalHelpers } from '../internals.js';
|
||||
import { log } from '../logger.js';
|
||||
import type { LayoutData } from './types.js';
|
||||
|
||||
// console.log('MUST be removed, this only for keeping dev server working');
|
||||
// import tmp from './layout-algorithms/dagre/index.js';
|
||||
|
||||
export interface RenderOptions {
|
||||
algorithm?: string;
|
||||
}
|
||||
|
@@ -1,9 +1,13 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { evaluate, getUrl } from '../../diagrams/common/common.js';
|
||||
import { evaluate } from '../../diagrams/common/common.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { createText } from '../createText.js';
|
||||
import utils from '../../utils.js';
|
||||
import { getLineFunctionsWithOffset } from '../../utils/lineWithOffset.js';
|
||||
import {
|
||||
getLineFunctionsWithOffset,
|
||||
markerOffsets,
|
||||
markerOffsets2,
|
||||
} from '../../utils/lineWithOffset.js';
|
||||
import { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js';
|
||||
|
||||
import {
|
||||
@@ -25,10 +29,10 @@ import {
|
||||
import rough from 'roughjs';
|
||||
import createLabel from './createLabel.js';
|
||||
import { addEdgeMarkers } from './edgeMarker.ts';
|
||||
import { isLabelStyle } from './shapes/handDrawnShapeStyles.js';
|
||||
import { isLabelStyle, styles2String } from './shapes/handDrawnShapeStyles.js';
|
||||
|
||||
const edgeLabels = new Map();
|
||||
const terminalLabels = new Map();
|
||||
export const edgeLabels = new Map();
|
||||
export const terminalLabels = new Map();
|
||||
|
||||
export const clear = () => {
|
||||
edgeLabels.clear();
|
||||
@@ -43,8 +47,10 @@ export const getLabelStyles = (styleArray) => {
|
||||
export const insertEdgeLabel = async (elem, edge) => {
|
||||
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||
|
||||
const { labelStyles } = styles2String(edge);
|
||||
edge.labelStyle = labelStyles;
|
||||
const labelElement = await createText(elem, edge.label, {
|
||||
style: getLabelStyles(edge.labelStyle),
|
||||
style: edge.labelStyle,
|
||||
useHtmlLabels,
|
||||
addSvgBackground: true,
|
||||
isNode: false,
|
||||
@@ -55,7 +61,7 @@ export const insertEdgeLabel = async (elem, edge) => {
|
||||
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
|
||||
|
||||
// Create inner g, label, this will be positioned now for centering the text
|
||||
const label = edgeLabel.insert('g').attr('class', 'label');
|
||||
const label = edgeLabel.insert('g').attr('class', 'label').attr('data-id', edge.id);
|
||||
label.node().appendChild(labelElement);
|
||||
|
||||
// Center the label
|
||||
@@ -438,7 +444,33 @@ const fixCorners = function (lineData) {
|
||||
}
|
||||
return newLineData;
|
||||
};
|
||||
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
|
||||
const generateDashArray = (len, oValueS, oValueE) => {
|
||||
const middleLength = len - oValueS - oValueE;
|
||||
const dashLength = 2; // Length of each dash
|
||||
const gapLength = 2; // Length of each gap
|
||||
const dashGapPairLength = dashLength + gapLength;
|
||||
|
||||
// Calculate number of complete dash-gap pairs that can fit
|
||||
const numberOfPairs = Math.floor(middleLength / dashGapPairLength);
|
||||
|
||||
// Generate the middle pattern array
|
||||
const middlePattern = Array(numberOfPairs).fill(`${dashLength} ${gapLength}`).join(' ');
|
||||
|
||||
// Combine all parts
|
||||
const dashArray = `0 ${oValueS} ${middlePattern} ${oValueE}`;
|
||||
|
||||
return dashArray;
|
||||
};
|
||||
export const insertEdge = function (
|
||||
elem,
|
||||
edge,
|
||||
clusterDb,
|
||||
diagramType,
|
||||
startNode,
|
||||
endNode,
|
||||
id,
|
||||
skipIntersect = false
|
||||
) {
|
||||
const { handDrawnSeed } = getConfig();
|
||||
let points = edge.points;
|
||||
let pointsHasChanged = false;
|
||||
@@ -452,11 +484,12 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
edgeClassStyles.push(edge.cssCompiledStyles[key]);
|
||||
}
|
||||
|
||||
if (head.intersect && tail.intersect) {
|
||||
log.debug('UIO intersect check', edge.points, head.x, tail.x);
|
||||
if (head.intersect && tail.intersect && !skipIntersect) {
|
||||
points = points.slice(1, edge.points.length - 1);
|
||||
points.unshift(tail.intersect(points[0]));
|
||||
log.debug(
|
||||
'Last point APA12',
|
||||
'Last point UIO',
|
||||
edge.start,
|
||||
'-->',
|
||||
edge.end,
|
||||
@@ -466,6 +499,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
);
|
||||
points.push(head.intersect(points[points.length - 1]));
|
||||
}
|
||||
const pointsStr = btoa(JSON.stringify(points));
|
||||
if (edge.toCluster) {
|
||||
log.info('to cluster abc88', clusterDb.get(edge.toCluster));
|
||||
points = cutPathAtIntersect(edge.points, clusterDb.get(edge.toCluster).node);
|
||||
@@ -529,6 +563,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
curve = curveBasis;
|
||||
}
|
||||
|
||||
// if (edge.curve) {
|
||||
// curve = edge.curve;
|
||||
// }
|
||||
|
||||
const { x, y } = getLineFunctionsWithOffset(edge);
|
||||
const lineFunction = line().x(x).y(y).curve(curve);
|
||||
|
||||
@@ -560,10 +598,14 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
strokeClasses += ' edge-pattern-solid';
|
||||
}
|
||||
let svgPath;
|
||||
let linePath = lineFunction(lineData);
|
||||
const edgeStyles = Array.isArray(edge.style) ? edge.style : edge.style ? [edge.style] : [];
|
||||
let linePath =
|
||||
edge.curve === 'rounded'
|
||||
? generateRoundedPath(applyMarkerOffsetsToPoints(lineData, edge), 5)
|
||||
: lineFunction(lineData);
|
||||
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
|
||||
let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
|
||||
|
||||
let animatedEdge = false;
|
||||
if (edge.look === 'handDrawn') {
|
||||
const rc = rough.svg(elem);
|
||||
Object.assign([], lineData);
|
||||
@@ -594,7 +636,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
animationClass = ' edge-animation-' + edge.animation;
|
||||
}
|
||||
|
||||
const pathStyle = stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles;
|
||||
const pathStyle =
|
||||
(stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles) +
|
||||
';' +
|
||||
(edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
|
||||
svgPath = elem
|
||||
.append('path')
|
||||
.attr('d', linePath)
|
||||
@@ -604,11 +649,39 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
|
||||
)
|
||||
.attr('style', pathStyle);
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
||||
strokeColor = pathStyle.match(/stroke:([^;]+)/)?.[1];
|
||||
|
||||
// Possible fix to remove eslint-disable-next-line
|
||||
//strokeColor = /stroke:([^;]+)/.exec(pathStyle)?.[1];
|
||||
|
||||
animatedEdge =
|
||||
edge.animate === true || !!edge.animation || stylesFromClasses.includes('animation');
|
||||
const pathNode = svgPath.node();
|
||||
const len = typeof pathNode.getTotalLength === 'function' ? pathNode.getTotalLength() : 0;
|
||||
const oValueS = markerOffsets2[edge.arrowTypeStart] || 0;
|
||||
const oValueE = markerOffsets2[edge.arrowTypeEnd] || 0;
|
||||
|
||||
if (edge.look === 'neo' && !animatedEdge) {
|
||||
const dashArray =
|
||||
edge.pattern === 'dotted' || edge.pattern === 'dashed'
|
||||
? generateDashArray(len, oValueS, oValueE)
|
||||
: `0 ${oValueS} ${len - oValueS - oValueE} ${oValueE}`;
|
||||
|
||||
// No offset needed because we already start with a zero-length dash that effectively sets us up for a gap at the start.
|
||||
const mOffset = `stroke-dasharray: ${dashArray}; stroke-dashoffset: 0;`;
|
||||
svgPath.attr('style', mOffset + svgPath.attr('style'));
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG code, DO NOT REMOVE
|
||||
// adds a red circle at each edge coordinate
|
||||
// MC Special
|
||||
svgPath.attr('data-edge', true);
|
||||
svgPath.attr('data-et', 'edge');
|
||||
svgPath.attr('data-id', edge.id);
|
||||
svgPath.attr('data-points', pointsStr);
|
||||
|
||||
// DEBUG code, adds a red circle at each edge coordinate
|
||||
// cornerPoints.forEach((point) => {
|
||||
// elem
|
||||
// .append('circle')
|
||||
@@ -618,19 +691,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
// .attr('cx', point.x)
|
||||
// .attr('cy', point.y);
|
||||
// });
|
||||
// lineData.forEach((point) => {
|
||||
// elem
|
||||
// .append('circle')
|
||||
// .style('stroke', 'red')
|
||||
// .style('fill', 'red')
|
||||
// .attr('r', 1)
|
||||
// .attr('cx', point.x)
|
||||
// .attr('cy', point.y);
|
||||
// });
|
||||
if (edge.showPoints) {
|
||||
lineData.forEach((point) => {
|
||||
elem
|
||||
.append('circle')
|
||||
.style('stroke', 'red')
|
||||
.style('fill', 'red')
|
||||
.attr('r', 1)
|
||||
.attr('cx', point.x)
|
||||
.attr('cy', point.y);
|
||||
});
|
||||
}
|
||||
|
||||
let url = '';
|
||||
if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) {
|
||||
url = getUrl(true);
|
||||
url =
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
window.location.search;
|
||||
url = url.replace(/\(/g, '\\(').replace(/\)/g, '\\)');
|
||||
}
|
||||
log.info('arrowTypeStart', edge.arrowTypeStart);
|
||||
log.info('arrowTypeEnd', edge.arrowTypeEnd);
|
||||
@@ -649,3 +730,134 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
paths.originalPath = edge.points;
|
||||
return paths;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates SVG path data with rounded corners from an array of points.
|
||||
* @param {Array} points - Array of points in the format [{x: Number, y: Number}, ...]
|
||||
* @param {Number} radius - The radius of the rounded corners
|
||||
* @returns {String} - SVG path data string
|
||||
*/
|
||||
function generateRoundedPath(points, radius) {
|
||||
if (points.length < 2) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let path = '';
|
||||
const size = points.length;
|
||||
const epsilon = 1e-5;
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const currPoint = points[i];
|
||||
const prevPoint = points[i - 1];
|
||||
const nextPoint = points[i + 1];
|
||||
|
||||
if (i === 0) {
|
||||
// Move to the first point
|
||||
path += `M${currPoint.x},${currPoint.y}`;
|
||||
} else if (i === size - 1) {
|
||||
// Last point, draw a straight line to the final point
|
||||
path += `L${currPoint.x},${currPoint.y}`;
|
||||
} else {
|
||||
// Calculate vectors for incoming and outgoing segments
|
||||
const dx1 = currPoint.x - prevPoint.x;
|
||||
const dy1 = currPoint.y - prevPoint.y;
|
||||
const dx2 = nextPoint.x - currPoint.x;
|
||||
const dy2 = nextPoint.y - currPoint.y;
|
||||
|
||||
const len1 = Math.hypot(dx1, dy1);
|
||||
const len2 = Math.hypot(dx2, dy2);
|
||||
|
||||
// Prevent division by zero
|
||||
if (len1 < epsilon || len2 < epsilon) {
|
||||
path += `L${currPoint.x},${currPoint.y}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalize the vectors
|
||||
const nx1 = dx1 / len1;
|
||||
const ny1 = dy1 / len1;
|
||||
const nx2 = dx2 / len2;
|
||||
const ny2 = dy2 / len2;
|
||||
|
||||
// Calculate the angle between the vectors
|
||||
const dot = nx1 * nx2 + ny1 * ny2;
|
||||
// Clamp the dot product to avoid numerical issues with acos
|
||||
const clampedDot = Math.max(-1, Math.min(1, dot));
|
||||
const angle = Math.acos(clampedDot);
|
||||
|
||||
// Skip rounding if the angle is too small or too close to 180 degrees
|
||||
if (angle < epsilon || Math.abs(Math.PI - angle) < epsilon) {
|
||||
path += `L${currPoint.x},${currPoint.y}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the distance to offset the control point
|
||||
const cutLen = Math.min(radius / Math.sin(angle / 2), len1 / 2, len2 / 2);
|
||||
|
||||
// Calculate the start and end points of the curve
|
||||
const startX = currPoint.x - nx1 * cutLen;
|
||||
const startY = currPoint.y - ny1 * cutLen;
|
||||
const endX = currPoint.x + nx2 * cutLen;
|
||||
const endY = currPoint.y + ny2 * cutLen;
|
||||
|
||||
// Draw the line to the start of the curve
|
||||
path += `L${startX},${startY}`;
|
||||
|
||||
// Draw the quadratic Bezier curve
|
||||
path += `Q${currPoint.x},${currPoint.y} ${endX},${endY}`;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
// Helper function to calculate delta and angle between two points
|
||||
function calculateDeltaAndAngle(point1, point2) {
|
||||
if (!point1 || !point2) {
|
||||
return { angle: 0, deltaX: 0, deltaY: 0 };
|
||||
}
|
||||
const deltaX = point2.x - point1.x;
|
||||
const deltaY = point2.y - point1.y;
|
||||
const angle = Math.atan2(deltaY, deltaX);
|
||||
return { angle, deltaX, deltaY };
|
||||
}
|
||||
|
||||
// Function to adjust the first and last points of the points array
|
||||
function applyMarkerOffsetsToPoints(points, edge) {
|
||||
// Copy the points array to avoid mutating the original data
|
||||
const newPoints = points.map((point) => ({ ...point }));
|
||||
|
||||
// Handle the first point (start of the edge)
|
||||
if (points.length >= 2 && markerOffsets[edge.arrowTypeStart]) {
|
||||
const offsetValue = markerOffsets[edge.arrowTypeStart];
|
||||
|
||||
const point1 = points[0];
|
||||
const point2 = points[1];
|
||||
|
||||
const { angle } = calculateDeltaAndAngle(point1, point2);
|
||||
|
||||
const offsetX = offsetValue * Math.cos(angle);
|
||||
const offsetY = offsetValue * Math.sin(angle);
|
||||
|
||||
newPoints[0].x = point1.x + offsetX;
|
||||
newPoints[0].y = point1.y + offsetY;
|
||||
}
|
||||
|
||||
// Handle the last point (end of the edge)
|
||||
const n = points.length;
|
||||
if (n >= 2 && markerOffsets[edge.arrowTypeEnd]) {
|
||||
const offsetValue = markerOffsets[edge.arrowTypeEnd];
|
||||
|
||||
const point1 = points[n - 1];
|
||||
const point2 = points[n - 2];
|
||||
|
||||
const { angle } = calculateDeltaAndAngle(point2, point1);
|
||||
|
||||
const offsetX = offsetValue * Math.cos(angle);
|
||||
const offsetY = offsetValue * Math.sin(angle);
|
||||
|
||||
newPoints[n - 1].x = point1.x - offsetX;
|
||||
newPoints[n - 1].y = point1.y - offsetY;
|
||||
}
|
||||
|
||||
return newPoints;
|
||||
}
|
||||
|
@@ -20,7 +20,11 @@ export const compileStyles = (node: Node) => {
|
||||
// the array is the styles of node from the classes it is using
|
||||
// node.cssStyles is an array of styles directly set on the node
|
||||
// concat the arrays and remove duplicates such that the values from node.cssStyles are used if there are duplicates
|
||||
const stylesMap = styles2Map([...(node.cssCompiledStyles || []), ...(node.cssStyles || [])]);
|
||||
const stylesMap = styles2Map([
|
||||
...(node.cssCompiledStyles || []),
|
||||
...(node.cssStyles || []),
|
||||
...(node.labelStyle || []),
|
||||
]);
|
||||
return { stylesMap, stylesArray: [...stylesMap] };
|
||||
};
|
||||
|
||||
|
@@ -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: |
|
||||
|
@@ -4,12 +4,22 @@ import type { EdgeData, Point } from '../types.js';
|
||||
// under any transparent markers.
|
||||
// The offsets are calculated from the markers' dimensions.
|
||||
export const markerOffsets = {
|
||||
aggregation: 18,
|
||||
extension: 18,
|
||||
composition: 18,
|
||||
aggregation: 17.25,
|
||||
extension: 17.25,
|
||||
composition: 17.25,
|
||||
dependency: 6,
|
||||
lollipop: 13.5,
|
||||
arrow_point: 4,
|
||||
//arrow_cross: 24,
|
||||
} as const;
|
||||
|
||||
// We need to draw the lines a bit shorter to avoid drawing
|
||||
// under any transparent markers.
|
||||
// The offsets are calculated from the markers' dimensions.
|
||||
export const markerOffsets2 = {
|
||||
arrow_point: 9,
|
||||
arrow_cross: 12.5,
|
||||
arrow_circle: 12.5,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -104,6 +114,7 @@ export const getLineFunctionsWithOffset = (
|
||||
adjustment *= DIRECTION === 'right' ? -1 : 1;
|
||||
offset += adjustment;
|
||||
}
|
||||
|
||||
return pointTransformer(d).x + offset;
|
||||
},
|
||||
y: function (
|
||||
|
14
packages/parser/antlr-config.json
Normal file
14
packages/parser/antlr-config.json
Normal 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"
|
||||
}
|
@@ -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": [
|
||||
|
@@ -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';
|
||||
|
170
packages/parser/src/language/usecase/Usecase.g4
Normal file
170
packages/parser/src/language/usecase/Usecase.g4
Normal file
@@ -0,0 +1,170 @@
|
||||
grammar Usecase;
|
||||
|
||||
// Parser rules
|
||||
usecaseDiagram
|
||||
: 'usecase' NEWLINE* statement* EOF
|
||||
;
|
||||
|
||||
statement
|
||||
: actorStatement
|
||||
| relationshipStatement
|
||||
| systemBoundaryStatement
|
||||
| systemBoundaryTypeStatement
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
actorStatement
|
||||
: 'actor' actorList NEWLINE*
|
||||
;
|
||||
|
||||
actorList
|
||||
: actorName (',' actorName)*
|
||||
;
|
||||
|
||||
actorName
|
||||
: (IDENTIFIER | STRING) metadata?
|
||||
;
|
||||
|
||||
metadata
|
||||
: '@' '{' metadataContent '}'
|
||||
;
|
||||
|
||||
metadataContent
|
||||
: metadataProperty (',' metadataProperty)*
|
||||
;
|
||||
|
||||
metadataProperty
|
||||
: STRING ':' STRING
|
||||
;
|
||||
|
||||
|
||||
|
||||
relationshipStatement
|
||||
: entityName arrow entityName NEWLINE*
|
||||
| actorDeclaration arrow entityName NEWLINE*
|
||||
;
|
||||
|
||||
systemBoundaryStatement
|
||||
: 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE*
|
||||
;
|
||||
|
||||
systemBoundaryName
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
;
|
||||
|
||||
systemBoundaryContent
|
||||
: usecaseInBoundary NEWLINE*
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
usecaseInBoundary
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
;
|
||||
|
||||
systemBoundaryTypeStatement
|
||||
: systemBoundaryName '@' '{' systemBoundaryTypeContent '}' NEWLINE*
|
||||
;
|
||||
|
||||
systemBoundaryTypeContent
|
||||
: systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)*
|
||||
;
|
||||
|
||||
systemBoundaryTypeProperty
|
||||
: 'type' ':' systemBoundaryType
|
||||
;
|
||||
|
||||
systemBoundaryType
|
||||
: 'package'
|
||||
| 'rect'
|
||||
;
|
||||
|
||||
entityName
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
| nodeIdWithLabel
|
||||
;
|
||||
|
||||
actorDeclaration
|
||||
: 'actor' actorName
|
||||
;
|
||||
|
||||
nodeIdWithLabel
|
||||
: IDENTIFIER '(' nodeLabel ')'
|
||||
;
|
||||
|
||||
nodeLabel
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
| nodeLabel IDENTIFIER
|
||||
| nodeLabel STRING
|
||||
;
|
||||
|
||||
arrow
|
||||
: SOLID_ARROW
|
||||
| BACK_ARROW
|
||||
| LINE_SOLID
|
||||
| labeledArrow
|
||||
;
|
||||
|
||||
labeledArrow
|
||||
: LINE_SOLID edgeLabel SOLID_ARROW
|
||||
| BACK_ARROW edgeLabel LINE_SOLID
|
||||
| LINE_SOLID edgeLabel LINE_SOLID
|
||||
;
|
||||
|
||||
edgeLabel
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
;
|
||||
|
||||
// Lexer rules
|
||||
SOLID_ARROW
|
||||
: '-->'
|
||||
;
|
||||
|
||||
BACK_ARROW
|
||||
: '<--'
|
||||
;
|
||||
|
||||
LINE_SOLID
|
||||
: '--'
|
||||
;
|
||||
|
||||
COMMA
|
||||
: ','
|
||||
;
|
||||
|
||||
AT
|
||||
: '@'
|
||||
;
|
||||
|
||||
LBRACE
|
||||
: '{'
|
||||
;
|
||||
|
||||
RBRACE
|
||||
: '}'
|
||||
;
|
||||
|
||||
COLON
|
||||
: ':'
|
||||
;
|
||||
|
||||
IDENTIFIER
|
||||
: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
;
|
||||
|
||||
STRING
|
||||
: '"' (~["\r\n])* '"'
|
||||
| '\'' (~['\r\n])* '\''
|
||||
;
|
||||
|
||||
NEWLINE
|
||||
: [\r\n]+
|
||||
;
|
||||
|
||||
WS
|
||||
: [ \t]+ -> skip
|
||||
;
|
4
packages/parser/src/language/usecase/index.ts
Normal file
4
packages/parser/src/language/usecase/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './module.js';
|
||||
export * from './types.js';
|
||||
export * from './parser.js';
|
||||
export * from './visitor.js';
|
50
packages/parser/src/language/usecase/module.ts
Normal file
50
packages/parser/src/language/usecase/module.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* ANTLR UseCase Module
|
||||
*
|
||||
* This module provides dependency injection and service creation
|
||||
* for the ANTLR-based UseCase parser, following the Langium pattern.
|
||||
*/
|
||||
|
||||
import type { AntlrUsecaseServices } from './types.js';
|
||||
import { UsecaseAntlrParser } from './parser.js';
|
||||
import { UsecaseAntlrVisitor } from './visitor.js';
|
||||
|
||||
/**
|
||||
* ANTLR UseCase Module for dependency injection
|
||||
*/
|
||||
export const AntlrUsecaseModule = {
|
||||
parser: () => new UsecaseAntlrParser(),
|
||||
visitor: () => new UsecaseAntlrVisitor(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the full set of ANTLR UseCase services
|
||||
*
|
||||
* This follows the Langium pattern but for ANTLR services
|
||||
*
|
||||
* @returns An object with ANTLR UseCase services
|
||||
*/
|
||||
export function createAntlrUsecaseServices(): AntlrUsecaseServices {
|
||||
const parser = new UsecaseAntlrParser();
|
||||
const visitor = new UsecaseAntlrVisitor();
|
||||
|
||||
return {
|
||||
parser,
|
||||
visitor,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton instance of ANTLR UseCase services
|
||||
*/
|
||||
let antlrUsecaseServices: AntlrUsecaseServices | undefined;
|
||||
|
||||
/**
|
||||
* Get or create the singleton ANTLR UseCase services
|
||||
*/
|
||||
export function getAntlrUsecaseServices(): AntlrUsecaseServices {
|
||||
if (!antlrUsecaseServices) {
|
||||
antlrUsecaseServices = createAntlrUsecaseServices();
|
||||
}
|
||||
return antlrUsecaseServices;
|
||||
}
|
194
packages/parser/src/language/usecase/parser.ts
Normal file
194
packages/parser/src/language/usecase/parser.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* True ANTLR Parser Implementation for UseCase Diagrams
|
||||
*
|
||||
* This parser uses the actual ANTLR-generated files from Usecase.g4
|
||||
* and implements the visitor pattern to build the AST.
|
||||
*/
|
||||
|
||||
import { CharStream, CommonTokenStream, BaseErrorListener } from 'antlr4ng';
|
||||
import type { RecognitionException, Recognizer } from 'antlr4ng';
|
||||
import { UsecaseLexer } from './generated/UsecaseLexer.js';
|
||||
import { UsecaseParser } from './generated/UsecaseParser.js';
|
||||
import { UsecaseAntlrVisitor } from './visitor.js';
|
||||
import type { AntlrUsecaseParser, UsecaseParseResult } from './types.js';
|
||||
|
||||
/**
|
||||
* Custom error listener for ANTLR parser to capture syntax errors
|
||||
*/
|
||||
class UsecaseErrorListener extends BaseErrorListener {
|
||||
private errors: string[] = [];
|
||||
|
||||
syntaxError(
|
||||
_recognizer: Recognizer<any>,
|
||||
_offendingSymbol: any,
|
||||
line: number,
|
||||
charPositionInLine: number,
|
||||
message: string,
|
||||
_e: RecognitionException | null
|
||||
): void {
|
||||
const errorMsg = `Syntax error at line ${line}:${charPositionInLine} - ${message}`;
|
||||
this.errors.push(errorMsg);
|
||||
}
|
||||
|
||||
reportAmbiguity(): void {
|
||||
// Optional: handle ambiguity reports
|
||||
}
|
||||
|
||||
reportAttemptingFullContext(): void {
|
||||
// Optional: handle full context attempts
|
||||
}
|
||||
|
||||
reportContextSensitivity(): void {
|
||||
// Optional: handle context sensitivity reports
|
||||
}
|
||||
|
||||
getErrors(): string[] {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
return this.errors.length > 0;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.errors = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error class for usecase parsing errors
|
||||
*/
|
||||
export class UsecaseParseError extends Error {
|
||||
public line?: number;
|
||||
public column?: number;
|
||||
public token?: string;
|
||||
public expected?: string[];
|
||||
public hash?: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
details?: {
|
||||
line?: number;
|
||||
column?: number;
|
||||
token?: string;
|
||||
expected?: string[];
|
||||
}
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'UsecaseParseError';
|
||||
this.line = details?.line;
|
||||
this.column = details?.column;
|
||||
this.token = details?.token;
|
||||
this.expected = details?.expected;
|
||||
|
||||
// Create hash object similar to other diagram types
|
||||
this.hash = {
|
||||
text: details?.token ?? '',
|
||||
token: details?.token ?? '',
|
||||
line: details?.line?.toString() ?? '1',
|
||||
loc: {
|
||||
first_line: details?.line ?? 1,
|
||||
last_line: details?.line ?? 1,
|
||||
first_column: details?.column ?? 1,
|
||||
last_column: (details?.column ?? 1) + (details?.token?.length ?? 0),
|
||||
},
|
||||
expected: details?.expected ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ANTLR-based UseCase parser implementation
|
||||
*/
|
||||
export class UsecaseAntlrParser implements AntlrUsecaseParser {
|
||||
private visitor: UsecaseAntlrVisitor;
|
||||
private errorListener: UsecaseErrorListener;
|
||||
|
||||
constructor() {
|
||||
this.visitor = new UsecaseAntlrVisitor();
|
||||
this.errorListener = new UsecaseErrorListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse UseCase diagram input using true ANTLR parsing
|
||||
*
|
||||
* @param input - The UseCase diagram text to parse
|
||||
* @returns Parsed result with actors, use cases, and relationships
|
||||
* @throws UsecaseParseError when syntax errors are encountered
|
||||
*/
|
||||
parse(input: string): UsecaseParseResult {
|
||||
// Clear previous errors
|
||||
this.errorListener.clear();
|
||||
|
||||
try {
|
||||
// Step 1: Create ANTLR input stream
|
||||
const chars = CharStream.fromString(input);
|
||||
|
||||
// Step 2: Create lexer from generated ANTLR lexer
|
||||
const lexer = new UsecaseLexer(chars);
|
||||
|
||||
// Add error listener to lexer
|
||||
lexer.removeErrorListeners();
|
||||
lexer.addErrorListener(this.errorListener);
|
||||
|
||||
// Step 3: Create token stream
|
||||
const tokens = new CommonTokenStream(lexer);
|
||||
|
||||
// Step 4: Create parser from generated ANTLR parser
|
||||
const parser = new UsecaseParser(tokens);
|
||||
|
||||
// Add error listener to parser
|
||||
parser.removeErrorListeners();
|
||||
parser.addErrorListener(this.errorListener);
|
||||
|
||||
// Step 5: Parse using the grammar rule: usecaseDiagram
|
||||
const tree = parser.usecaseDiagram();
|
||||
|
||||
// Check for syntax errors before proceeding
|
||||
if (this.errorListener.hasErrors()) {
|
||||
const errors = this.errorListener.getErrors();
|
||||
throw new UsecaseParseError(`Syntax error in usecase diagram: ${errors.join('; ')}`, {
|
||||
token: 'unknown',
|
||||
expected: ['valid usecase syntax'],
|
||||
});
|
||||
}
|
||||
|
||||
// Step 6: Visit the parse tree using our visitor
|
||||
this.visitor.visitUsecaseDiagram!(tree);
|
||||
|
||||
// Step 7: Get the parse result
|
||||
return this.visitor.getParseResult();
|
||||
} catch (error) {
|
||||
if (error instanceof UsecaseParseError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle other types of errors
|
||||
throw new UsecaseParseError(
|
||||
`Failed to parse usecase diagram: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
{
|
||||
token: 'unknown',
|
||||
expected: ['valid usecase syntax'],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a new ANTLR UseCase parser
|
||||
*/
|
||||
export function createUsecaseAntlrParser(): AntlrUsecaseParser {
|
||||
return new UsecaseAntlrParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for parsing UseCase diagrams
|
||||
*
|
||||
* @param input - The UseCase diagram text to parse
|
||||
* @returns Parsed result with actors, use cases, and relationships
|
||||
*/
|
||||
export function parseUsecaseWithAntlr(input: string): UsecaseParseResult {
|
||||
const parser = createUsecaseAntlrParser();
|
||||
return parser.parse(input);
|
||||
}
|
70
packages/parser/src/language/usecase/types.ts
Normal file
70
packages/parser/src/language/usecase/types.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Type definitions for ANTLR UseCase parser
|
||||
*/
|
||||
|
||||
// Arrow types for usecase diagrams (similar to sequence diagram LINETYPE)
|
||||
export const ARROW_TYPE = {
|
||||
SOLID_ARROW: 0, // -->
|
||||
BACK_ARROW: 1, // <--
|
||||
LINE_SOLID: 2, // --
|
||||
} as const;
|
||||
|
||||
export type ArrowType = (typeof ARROW_TYPE)[keyof typeof ARROW_TYPE];
|
||||
|
||||
export type ActorMetadata = Record<string, string>;
|
||||
|
||||
export interface Actor {
|
||||
id: string;
|
||||
name: string;
|
||||
metadata?: ActorMetadata;
|
||||
}
|
||||
|
||||
export interface UseCase {
|
||||
id: string;
|
||||
name: string;
|
||||
nodeId?: string; // Optional node ID (e.g., 'a' in 'a(Go through code)')
|
||||
systemBoundary?: string; // Optional reference to system boundary
|
||||
}
|
||||
|
||||
export type SystemBoundaryType = 'package' | 'rect';
|
||||
|
||||
export interface SystemBoundary {
|
||||
id: string;
|
||||
name: string;
|
||||
useCases: string[]; // Array of use case IDs within this boundary
|
||||
type?: SystemBoundaryType; // Type of boundary rendering (default: 'rect')
|
||||
}
|
||||
|
||||
export interface Relationship {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'association' | 'include' | 'extend';
|
||||
arrowType: ArrowType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface UsecaseParseResult {
|
||||
actors: Actor[];
|
||||
useCases: UseCase[];
|
||||
systemBoundaries: SystemBoundary[];
|
||||
relationships: Relationship[];
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ANTLR Parser Services interface
|
||||
*/
|
||||
export interface AntlrUsecaseServices {
|
||||
parser: AntlrUsecaseParser;
|
||||
visitor: any; // UsecaseAntlrVisitor - using any to avoid circular dependency
|
||||
}
|
||||
|
||||
/**
|
||||
* ANTLR Parser interface
|
||||
*/
|
||||
export interface AntlrUsecaseParser {
|
||||
parse(input: string): UsecaseParseResult;
|
||||
}
|
605
packages/parser/src/language/usecase/visitor.ts
Normal file
605
packages/parser/src/language/usecase/visitor.ts
Normal file
@@ -0,0 +1,605 @@
|
||||
/**
|
||||
* ANTLR Visitor Implementation for UseCase Diagrams
|
||||
*
|
||||
* This visitor traverses the ANTLR parse tree and builds the AST
|
||||
* according to the grammar rules defined in Usecase.g4
|
||||
*/
|
||||
|
||||
import { UsecaseVisitor } from './generated/UsecaseVisitor.js';
|
||||
import type {
|
||||
UsecaseDiagramContext,
|
||||
StatementContext,
|
||||
ActorStatementContext,
|
||||
ActorListContext,
|
||||
RelationshipStatementContext,
|
||||
SystemBoundaryStatementContext,
|
||||
SystemBoundaryTypeStatementContext,
|
||||
SystemBoundaryNameContext,
|
||||
SystemBoundaryTypeContentContext,
|
||||
SystemBoundaryTypePropertyContext,
|
||||
SystemBoundaryTypeContext,
|
||||
UsecaseInBoundaryContext,
|
||||
ActorNameContext,
|
||||
ActorDeclarationContext,
|
||||
NodeIdWithLabelContext,
|
||||
NodeLabelContext,
|
||||
MetadataContext,
|
||||
MetadataContentContext,
|
||||
MetadataPropertyContext,
|
||||
EntityNameContext,
|
||||
ArrowContext,
|
||||
LabeledArrowContext,
|
||||
EdgeLabelContext,
|
||||
} from './generated/UsecaseParser.js';
|
||||
import { ARROW_TYPE } from './types.js';
|
||||
import type {
|
||||
Actor,
|
||||
UseCase,
|
||||
SystemBoundary,
|
||||
Relationship,
|
||||
UsecaseParseResult,
|
||||
ArrowType,
|
||||
} from './types.js';
|
||||
|
||||
export class UsecaseAntlrVisitor extends UsecaseVisitor<void> {
|
||||
private actors: Actor[] = [];
|
||||
private useCases: UseCase[] = [];
|
||||
private systemBoundaries: SystemBoundary[] = [];
|
||||
private relationships: Relationship[] = [];
|
||||
private relationshipCounter = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Assign visitor functions as properties
|
||||
this.visitUsecaseDiagram = this.visitUsecaseDiagramImpl.bind(this);
|
||||
this.visitStatement = this.visitStatementImpl.bind(this);
|
||||
this.visitActorStatement = this.visitActorStatementImpl.bind(this);
|
||||
this.visitRelationshipStatement = this.visitRelationshipStatementImpl.bind(this);
|
||||
this.visitSystemBoundaryStatement = this.visitSystemBoundaryStatementImpl.bind(this);
|
||||
this.visitSystemBoundaryTypeStatement = this.visitSystemBoundaryTypeStatementImpl.bind(this);
|
||||
this.visitActorName = this.visitActorNameImpl.bind(this);
|
||||
this.visitArrow = this.visitArrowImpl.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit the root usecaseDiagram rule
|
||||
* Grammar: usecaseDiagram : 'usecase' statement* EOF ;
|
||||
*/
|
||||
visitUsecaseDiagramImpl(ctx: UsecaseDiagramContext): void {
|
||||
// Reset state
|
||||
this.actors = [];
|
||||
this.useCases = [];
|
||||
this.relationships = [];
|
||||
this.relationshipCounter = 0;
|
||||
|
||||
// Visit all statement children
|
||||
if (ctx.statement) {
|
||||
const statements = Array.isArray(ctx.statement()) ? ctx.statement() : [ctx.statement()];
|
||||
for (const statementCtx of statements) {
|
||||
if (Array.isArray(statementCtx)) {
|
||||
for (const stmt of statementCtx) {
|
||||
this.visitStatementImpl(stmt);
|
||||
}
|
||||
} else {
|
||||
this.visitStatementImpl(statementCtx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit statement rule
|
||||
* Grammar: statement : actorStatement | relationshipStatement | systemBoundaryStatement | systemBoundaryTypeStatement | NEWLINE ;
|
||||
*/
|
||||
private visitStatementImpl(ctx: StatementContext): void {
|
||||
if (ctx.actorStatement?.()) {
|
||||
this.visitActorStatementImpl(ctx.actorStatement()!);
|
||||
} else if (ctx.relationshipStatement?.()) {
|
||||
this.visitRelationshipStatementImpl(ctx.relationshipStatement()!);
|
||||
} else if (ctx.systemBoundaryStatement?.()) {
|
||||
this.visitSystemBoundaryStatementImpl(ctx.systemBoundaryStatement()!);
|
||||
} else if (ctx.systemBoundaryTypeStatement?.()) {
|
||||
this.visitSystemBoundaryTypeStatementImpl(ctx.systemBoundaryTypeStatement()!);
|
||||
}
|
||||
// NEWLINE is ignored
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit actorStatement rule
|
||||
* Grammar: actorStatement : 'actor' actorList NEWLINE* ;
|
||||
*/
|
||||
visitActorStatementImpl(ctx: ActorStatementContext): void {
|
||||
if (ctx.actorList?.()) {
|
||||
this.visitActorListImpl(ctx.actorList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit actorList rule
|
||||
* Grammar: actorList : actorName (',' actorName)* ;
|
||||
*/
|
||||
visitActorListImpl(ctx: ActorListContext): void {
|
||||
// Get all actorName contexts from the list
|
||||
const actorNameContexts = ctx.actorName();
|
||||
|
||||
for (const actorNameCtx of actorNameContexts) {
|
||||
const actorResult = this.visitActorNameImpl(actorNameCtx);
|
||||
this.actors.push({
|
||||
id: actorResult.name,
|
||||
name: actorResult.name,
|
||||
metadata: actorResult.metadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit relationshipStatement rule
|
||||
* Grammar: relationshipStatement : entityName arrow entityName NEWLINE* | actorDeclaration arrow entityName NEWLINE* ;
|
||||
*/
|
||||
visitRelationshipStatementImpl(ctx: RelationshipStatementContext): void {
|
||||
let from = '';
|
||||
let to = '';
|
||||
|
||||
// Handle different relationship patterns
|
||||
if (ctx.actorDeclaration?.()) {
|
||||
// Pattern: actor ActorName --> entityName
|
||||
from = this.visitActorDeclarationImpl(ctx.actorDeclaration()!);
|
||||
to = this.visitEntityNameImpl(ctx.entityName(0)!);
|
||||
} else if (ctx.entityName && ctx.entityName().length >= 2) {
|
||||
// Pattern: entityName --> entityName
|
||||
from = this.visitEntityNameImpl(ctx.entityName(0)!);
|
||||
to = this.visitEntityNameImpl(ctx.entityName(1)!);
|
||||
}
|
||||
|
||||
// Get arrow information (type and optional label)
|
||||
const arrowInfo = this.visitArrowImpl(ctx.arrow());
|
||||
|
||||
// Auto-create use cases for entities that are not actors
|
||||
this.ensureUseCaseExists(from);
|
||||
this.ensureUseCaseExists(to);
|
||||
|
||||
const relationship: Relationship = {
|
||||
id: `rel_${this.relationshipCounter++}`,
|
||||
from,
|
||||
to,
|
||||
type: 'association',
|
||||
arrowType: arrowInfo.arrowType,
|
||||
};
|
||||
|
||||
// Add label if present
|
||||
if (arrowInfo.label) {
|
||||
relationship.label = arrowInfo.label;
|
||||
}
|
||||
|
||||
this.relationships.push(relationship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a use case exists for the given entity name if it's not an actor
|
||||
*/
|
||||
private ensureUseCaseExists(entityName: string): void {
|
||||
// Check if it's already an actor
|
||||
const isActor = this.actors.some((actor) => actor.id === entityName);
|
||||
|
||||
// If it's not an actor, create it as a use case (if not already exists)
|
||||
if (!isActor) {
|
||||
const existingUseCase = this.useCases.some((useCase) => useCase.id === entityName);
|
||||
if (!existingUseCase) {
|
||||
this.useCases.push({
|
||||
id: entityName,
|
||||
name: entityName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryStatement rule
|
||||
* Grammar: systemBoundaryStatement : 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE* ;
|
||||
*/
|
||||
visitSystemBoundaryStatementImpl(ctx: SystemBoundaryStatementContext): void {
|
||||
let boundaryName = '';
|
||||
|
||||
// Get the system boundary name
|
||||
if (ctx.systemBoundaryName?.()) {
|
||||
boundaryName = this.visitSystemBoundaryNameImpl(ctx.systemBoundaryName());
|
||||
}
|
||||
|
||||
// Collect use cases within this boundary
|
||||
const useCasesInBoundary: string[] = [];
|
||||
|
||||
if (ctx.systemBoundaryContent?.()?.length > 0) {
|
||||
for (const contentCtx of ctx.systemBoundaryContent()) {
|
||||
const usecaseInBoundary = contentCtx.usecaseInBoundary?.();
|
||||
if (usecaseInBoundary) {
|
||||
const useCaseName = this.visitUsecaseInBoundaryImpl(usecaseInBoundary);
|
||||
useCasesInBoundary.push(useCaseName);
|
||||
|
||||
// Create the use case and mark it as being in this boundary
|
||||
const existingUseCase = this.useCases.find((uc) => uc.id === useCaseName);
|
||||
if (existingUseCase) {
|
||||
existingUseCase.systemBoundary = boundaryName;
|
||||
} else {
|
||||
this.useCases.push({
|
||||
id: useCaseName,
|
||||
name: useCaseName,
|
||||
systemBoundary: boundaryName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the system boundary with default type
|
||||
this.systemBoundaries.push({
|
||||
id: boundaryName,
|
||||
name: boundaryName,
|
||||
useCases: useCasesInBoundary,
|
||||
type: 'rect', // default type
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryName rule
|
||||
* Grammar: systemBoundaryName : IDENTIFIER | STRING ;
|
||||
*/
|
||||
private visitSystemBoundaryNameImpl(ctx: SystemBoundaryNameContext): string {
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
return identifier.getText();
|
||||
}
|
||||
|
||||
const string = ctx.STRING?.();
|
||||
if (string) {
|
||||
const text = string.getText();
|
||||
// Remove quotes from string
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit usecaseInBoundary rule
|
||||
* Grammar: usecaseInBoundary : IDENTIFIER | STRING ;
|
||||
*/
|
||||
private visitUsecaseInBoundaryImpl(ctx: UsecaseInBoundaryContext): string {
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
return identifier.getText();
|
||||
}
|
||||
|
||||
const string = ctx.STRING?.();
|
||||
if (string) {
|
||||
const text = string.getText();
|
||||
// Remove quotes from string
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryTypeStatement rule
|
||||
* Grammar: systemBoundaryTypeStatement : systemBoundaryName '\@' '\{' systemBoundaryTypeContent '\}' NEWLINE* ;
|
||||
*/
|
||||
visitSystemBoundaryTypeStatementImpl(ctx: SystemBoundaryTypeStatementContext): void {
|
||||
let boundaryName = '';
|
||||
|
||||
// Get the system boundary name
|
||||
const systemBoundaryName = ctx.systemBoundaryName?.();
|
||||
if (systemBoundaryName) {
|
||||
boundaryName = this.visitSystemBoundaryNameImpl(systemBoundaryName);
|
||||
}
|
||||
|
||||
// Get the type configuration
|
||||
let boundaryType: 'package' | 'rect' = 'rect'; // default
|
||||
const systemBoundaryTypeContent = ctx.systemBoundaryTypeContent?.();
|
||||
if (systemBoundaryTypeContent) {
|
||||
boundaryType = this.visitSystemBoundaryTypeContentImpl(systemBoundaryTypeContent);
|
||||
}
|
||||
|
||||
// Find the existing system boundary and update its type
|
||||
const existingBoundary = this.systemBoundaries.find((b) => b.id === boundaryName);
|
||||
if (existingBoundary) {
|
||||
existingBoundary.type = boundaryType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryTypeContent rule
|
||||
* Grammar: systemBoundaryTypeContent : systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)* ;
|
||||
*/
|
||||
private visitSystemBoundaryTypeContentImpl(
|
||||
ctx: SystemBoundaryTypeContentContext
|
||||
): 'package' | 'rect' {
|
||||
// Get all type properties
|
||||
const typeProperties = ctx.systemBoundaryTypeProperty();
|
||||
|
||||
for (const propCtx of typeProperties) {
|
||||
const type = this.visitSystemBoundaryTypePropertyImpl(propCtx);
|
||||
if (type) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return 'rect'; // default
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryTypeProperty rule
|
||||
* Grammar: systemBoundaryTypeProperty : 'type' ':' systemBoundaryType ;
|
||||
*/
|
||||
private visitSystemBoundaryTypePropertyImpl(
|
||||
ctx: SystemBoundaryTypePropertyContext
|
||||
): 'package' | 'rect' | null {
|
||||
const systemBoundaryType = ctx.systemBoundaryType?.();
|
||||
if (systemBoundaryType) {
|
||||
return this.visitSystemBoundaryTypeImpl(systemBoundaryType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryType rule
|
||||
* Grammar: systemBoundaryType : 'package' | 'rect' ;
|
||||
*/
|
||||
private visitSystemBoundaryTypeImpl(ctx: SystemBoundaryTypeContext): 'package' | 'rect' {
|
||||
const text = ctx.getText();
|
||||
if (text === 'package') {
|
||||
return 'package';
|
||||
} else if (text === 'rect') {
|
||||
return 'rect';
|
||||
}
|
||||
return 'rect'; // default
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit actorName rule
|
||||
* Grammar: actorName : (IDENTIFIER | STRING) metadata? ;
|
||||
*/
|
||||
private visitActorNameImpl(ctx: ActorNameContext): {
|
||||
name: string;
|
||||
metadata?: Record<string, string>;
|
||||
} {
|
||||
let name = '';
|
||||
|
||||
if (ctx.IDENTIFIER?.()) {
|
||||
name = ctx.IDENTIFIER()!.getText();
|
||||
} else if (ctx.STRING?.()) {
|
||||
const text = ctx.STRING()!.getText();
|
||||
// Remove quotes from string
|
||||
name = text.slice(1, -1);
|
||||
}
|
||||
|
||||
let metadata = undefined;
|
||||
if (ctx.metadata?.()) {
|
||||
metadata = this.visitMetadataImpl(ctx.metadata()!);
|
||||
}
|
||||
|
||||
return { name, metadata };
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit metadata rule
|
||||
* Grammar: metadata : '\@' '\{' metadataContent '\}' ;
|
||||
*/
|
||||
private visitMetadataImpl(ctx: MetadataContext): Record<string, string> {
|
||||
const metadataContent = ctx.metadataContent?.();
|
||||
if (metadataContent) {
|
||||
return this.visitMetadataContentImpl(metadataContent);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit metadataContent rule
|
||||
* Grammar: metadataContent : metadataProperty (',' metadataProperty)* ;
|
||||
*/
|
||||
private visitMetadataContentImpl(ctx: MetadataContentContext): Record<string, string> {
|
||||
const metadata: Record<string, string> = {};
|
||||
const properties = ctx.metadataProperty();
|
||||
|
||||
for (const property of properties) {
|
||||
const { key, value } = this.visitMetadataPropertyImpl(property);
|
||||
metadata[key] = value;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit metadataProperty rule
|
||||
* Grammar: metadataProperty : STRING ':' STRING ;
|
||||
*/
|
||||
private visitMetadataPropertyImpl(ctx: MetadataPropertyContext): { key: string; value: string } {
|
||||
const strings = ctx.STRING();
|
||||
if (strings.length >= 2) {
|
||||
const key = strings[0].getText().slice(1, -1); // Remove quotes
|
||||
const value = strings[1].getText().slice(1, -1); // Remove quotes
|
||||
return { key, value };
|
||||
}
|
||||
return { key: '', value: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit entityName rule
|
||||
* Grammar: entityName : IDENTIFIER | STRING | nodeIdWithLabel ;
|
||||
*/
|
||||
private visitEntityNameImpl(ctx: EntityNameContext): string {
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
return identifier.getText();
|
||||
}
|
||||
|
||||
const string = ctx.STRING?.();
|
||||
if (string) {
|
||||
const text = string.getText();
|
||||
// Remove quotes from string
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
const nodeIdWithLabel = ctx.nodeIdWithLabel?.();
|
||||
if (nodeIdWithLabel) {
|
||||
return this.visitNodeIdWithLabelImpl(nodeIdWithLabel);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit actorDeclaration rule
|
||||
* Grammar: actorDeclaration : 'actor' actorName ;
|
||||
*/
|
||||
private visitActorDeclarationImpl(ctx: ActorDeclarationContext): string {
|
||||
const actorName = ctx.actorName?.();
|
||||
if (actorName) {
|
||||
const actorResult = this.visitActorNameImpl(actorName);
|
||||
|
||||
// Add the actor if it doesn't already exist
|
||||
const existingActor = this.actors.find((actor) => actor.id === actorResult.name);
|
||||
if (!existingActor) {
|
||||
this.actors.push({
|
||||
id: actorResult.name,
|
||||
name: actorResult.name,
|
||||
metadata: actorResult.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
return actorResult.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit nodeIdWithLabel rule
|
||||
* Grammar: nodeIdWithLabel : IDENTIFIER '(' nodeLabel ')' ;
|
||||
*/
|
||||
private visitNodeIdWithLabelImpl(ctx: NodeIdWithLabelContext): string {
|
||||
let nodeId = '';
|
||||
let nodeLabel = '';
|
||||
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
nodeId = identifier.getText();
|
||||
}
|
||||
|
||||
const nodeLabelCtx = ctx.nodeLabel?.();
|
||||
if (nodeLabelCtx) {
|
||||
nodeLabel = this.visitNodeLabelImpl(nodeLabelCtx);
|
||||
}
|
||||
|
||||
// Create or update the use case with nodeId and label
|
||||
const existingUseCase = this.useCases.find((uc) => uc.id === nodeLabel || uc.nodeId === nodeId);
|
||||
if (existingUseCase) {
|
||||
// Update existing use case with nodeId if not already set
|
||||
existingUseCase.nodeId ??= nodeId;
|
||||
} else {
|
||||
// Create new use case with nodeId and label
|
||||
this.useCases.push({
|
||||
id: nodeLabel,
|
||||
name: nodeLabel,
|
||||
nodeId: nodeId,
|
||||
});
|
||||
}
|
||||
|
||||
return nodeLabel; // Return the label as the entity name for relationships
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit nodeLabel rule
|
||||
* Grammar: nodeLabel : IDENTIFIER | STRING | nodeLabel IDENTIFIER | nodeLabel STRING ;
|
||||
*/
|
||||
private visitNodeLabelImpl(ctx: NodeLabelContext): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Handle recursive nodeLabel structure
|
||||
const nodeLabel = ctx.nodeLabel?.();
|
||||
if (nodeLabel) {
|
||||
parts.push(this.visitNodeLabelImpl(nodeLabel));
|
||||
}
|
||||
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
parts.push(identifier.getText());
|
||||
} else {
|
||||
const string = ctx.STRING?.();
|
||||
if (string) {
|
||||
const text = string.getText();
|
||||
// Remove quotes from string
|
||||
parts.push(text.slice(1, -1));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit arrow rule
|
||||
* Grammar: arrow : SOLID_ARROW | BACK_ARROW | LINE_SOLID | labeledArrow ;
|
||||
*/
|
||||
private visitArrowImpl(ctx: ArrowContext): { arrowType: ArrowType; label?: string } {
|
||||
// Check if this is a labeled arrow
|
||||
if (ctx.labeledArrow()) {
|
||||
return this.visitLabeledArrowImpl(ctx.labeledArrow()!);
|
||||
}
|
||||
|
||||
// Regular arrow without label - determine type from token
|
||||
if (ctx.SOLID_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.SOLID_ARROW };
|
||||
} else if (ctx.BACK_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.BACK_ARROW };
|
||||
} else if (ctx.LINE_SOLID()) {
|
||||
return { arrowType: ARROW_TYPE.LINE_SOLID };
|
||||
}
|
||||
|
||||
// Fallback (should not happen with proper grammar)
|
||||
return { arrowType: ARROW_TYPE.SOLID_ARROW };
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit labeled arrow rule
|
||||
* Grammar: labeledArrow : LINE_SOLID edgeLabel SOLID_ARROW | BACK_ARROW edgeLabel LINE_SOLID | LINE_SOLID edgeLabel LINE_SOLID ;
|
||||
*/
|
||||
private visitLabeledArrowImpl(ctx: LabeledArrowContext): { arrowType: ArrowType; label: string } {
|
||||
const label = this.visitEdgeLabelImpl(ctx.edgeLabel());
|
||||
|
||||
// Determine arrow type based on the tokens present
|
||||
if (ctx.SOLID_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.SOLID_ARROW, label };
|
||||
} else if (ctx.BACK_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.BACK_ARROW, label };
|
||||
} else {
|
||||
return { arrowType: ARROW_TYPE.LINE_SOLID, label };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit edge label rule
|
||||
* Grammar: edgeLabel : IDENTIFIER | STRING ;
|
||||
*/
|
||||
private visitEdgeLabelImpl(ctx: EdgeLabelContext): string {
|
||||
const text = ctx.getText();
|
||||
// Remove quotes if it's a string
|
||||
if (
|
||||
(text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))
|
||||
) {
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parse result after visiting the diagram
|
||||
*/
|
||||
getParseResult(): UsecaseParseResult {
|
||||
return {
|
||||
actors: this.actors,
|
||||
useCases: this.useCases,
|
||||
systemBoundaries: this.systemBoundaries,
|
||||
relationships: this.relationships,
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,8 +1,10 @@
|
||||
import type { LangiumParser, ParseResult } from 'langium';
|
||||
|
||||
import type { 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);
|
||||
}
|
||||
|
1612
packages/parser/tests/usecase.test.ts
Normal file
1612
packages/parser/tests/usecase.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
2183
pnpm-lock.yaml
generated
2183
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ pushd packages/mermaid
|
||||
# Append commit hash to version
|
||||
jq ".version = .version + \"+${COMMIT_REF:0:7}\"" package.json > package.tmp.json
|
||||
mv package.tmp.json package.json
|
||||
yarn link
|
||||
popd
|
||||
|
||||
pnpm run -r clean
|
||||
@@ -26,13 +25,14 @@ cd mermaid-live-editor
|
||||
git clean -xdf
|
||||
rm -rf docs/
|
||||
|
||||
# We have to use npm instead of yarn because it causes trouble in netlify
|
||||
# Tells PNPM that mermaid-live-editor is not part of this workspace
|
||||
touch pnpm-workspace.yaml
|
||||
|
||||
# Install dependencies
|
||||
yarn install
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Link local mermaid to live editor
|
||||
yarn link mermaid
|
||||
pnpm link ../packages/mermaid
|
||||
|
||||
# Force Build the site
|
||||
yarn run build
|
||||
|
||||
pnpm run build
|
||||
|
Reference in New Issue
Block a user