mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-05 21:34:14 +01:00
Compare commits
97 Commits
feat/useca
...
renovate/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
569e7e5427 | ||
|
|
dfd59470dc | ||
|
|
4aac6fa448 | ||
|
|
5f96f80efb | ||
|
|
545801e144 | ||
|
|
0bd74759cc | ||
|
|
e4cf266c1d | ||
|
|
c0e1662e50 | ||
|
|
6546aed482 | ||
|
|
b76ccae065 | ||
|
|
287a9a3fcb | ||
|
|
7f5160fa4d | ||
|
|
7b0763f262 | ||
|
|
38c289818c | ||
|
|
57530076aa | ||
|
|
f2d7877c7a | ||
|
|
62d2c6505e | ||
|
|
974236bbb8 | ||
|
|
cf0d1248a4 | ||
|
|
ebefbd87a8 | ||
|
|
1e7b71a085 | ||
|
|
f28f3c25aa | ||
|
|
58137aa631 | ||
|
|
e7719f14c5 | ||
|
|
35d9cead8a | ||
|
|
13baf51b35 | ||
|
|
9af985ba9b | ||
|
|
c7f8a11ded | ||
|
|
762b44cf33 | ||
|
|
02c0091106 | ||
|
|
16359adc33 | ||
|
|
061632c580 | ||
|
|
cbf89462ac | ||
|
|
700aa100f2 | ||
|
|
49103ea654 | ||
|
|
3f46c94ab2 | ||
|
|
fed8a523a4 | ||
|
|
33b4946e21 | ||
|
|
3d768f3adf | ||
|
|
76e17ffd20 | ||
|
|
60f633101c | ||
|
|
18f51eb14e | ||
|
|
2bb57bf7d2 | ||
|
|
a6276daffd | ||
|
|
7def6eecbf | ||
|
|
34f40f0794 | ||
|
|
32ac2c689d | ||
|
|
dbcadc1d0b | ||
|
|
ac411a7d7e | ||
|
|
d80a638e55 | ||
|
|
7a869c08a2 | ||
|
|
44e8cbb1de | ||
|
|
efe38b8425 | ||
|
|
6fecb985e8 | ||
|
|
69b338d8af | ||
|
|
fa15ce8502 | ||
|
|
6d0650918f | ||
|
|
ad82448084 | ||
|
|
9498619d3c | ||
|
|
7a8557a1a2 | ||
|
|
74863c94fb | ||
|
|
63df702146 | ||
|
|
421f8d4633 | ||
|
|
bf6e1a594c | ||
|
|
1a9d45abf0 | ||
|
|
0116b272b4 | ||
|
|
634f3367da | ||
|
|
bbb93b263d | ||
|
|
4240340a18 | ||
|
|
ca10a259fa | ||
|
|
0ed9c65572 | ||
|
|
56cc12690f | ||
|
|
e6fb4a84da | ||
|
|
32723b2de1 | ||
|
|
18703782ee | ||
|
|
47297f7c26 | ||
|
|
967aa0629e | ||
|
|
04b20a79b9 | ||
|
|
d60b09cafc | ||
|
|
875827f59b | ||
|
|
c4e08261b5 | ||
|
|
70f679d2fa | ||
|
|
25c43fa439 | ||
|
|
ec1c6325d4 | ||
|
|
309ff6be38 | ||
|
|
02d368df05 | ||
|
|
4ee1fe2ca4 | ||
|
|
4ff2ae9f4e | ||
|
|
7a729e8f16 | ||
|
|
3c7fd95617 | ||
|
|
2dd29bee25 | ||
|
|
259a508d8a | ||
|
|
1963064369 | ||
|
|
a101ce803c | ||
|
|
fc0c7936d1 | ||
|
|
2d2add5b44 | ||
|
|
58fd5ddbaf |
@@ -1,92 +0,0 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { generateFromConfig } from './antlr-cli.js';
|
||||
|
||||
export async function generateAntlr() {
|
||||
await generateFromConfig('./packages/parser/antlr-config.json');
|
||||
}
|
||||
@@ -28,7 +28,6 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
||||
'packet',
|
||||
'architecture',
|
||||
'radar',
|
||||
'usecase',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
5
.changeset/brave-memes-flash.md
Normal file
5
.changeset/brave-memes-flash.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Support edge animation in hand drawn look
|
||||
5
.changeset/busy-mirrors-try.md
Normal file
5
.changeset/busy-mirrors-try.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Resolved parsing error where direction TD was not recognized within subgraphs
|
||||
5
.changeset/chilly-words-march.md
Normal file
5
.changeset/chilly-words-march.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Correct viewBox casing and make SVGs responsive
|
||||
5
.changeset/curly-apes-prove.md
Normal file
5
.changeset/curly-apes-prove.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Improve participant parsing and prevent recursive loops on invalid syntax
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: Fix mindmap rendering in docs and apply tidytree layout
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Ensure edge label color is applied when using classDef with edge IDs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Resolve gantt chart crash due to invalid array length
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add IDs in architecture diagrams
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: revert marked dependency from ^15.0.7 to ^16.0.0
|
||||
|
||||
- Reverted marked package version to ^16.0.0 for better compatibility
|
||||
- This is a dependency update that maintains API compatibility
|
||||
- All tests pass with the updated version
|
||||
5
.changeset/short-seals-sort.md
Normal file
5
.changeset/short-seals-sort.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: allow to put notes in namespaces on classDiagram
|
||||
@@ -1,3 +1,5 @@
|
||||
!viewbox
|
||||
# It should be viewBox
|
||||
# This file contains coding related terms
|
||||
ALPHANUM
|
||||
antiscript
|
||||
@@ -143,9 +145,6 @@ typeof
|
||||
typestr
|
||||
unshift
|
||||
urlsafe
|
||||
usecase
|
||||
Usecase
|
||||
USECASE
|
||||
verifymethod
|
||||
VERIFYMTHD
|
||||
WARN_DOCSDIR_DOESNT_MATCH
|
||||
|
||||
@@ -64,6 +64,7 @@ rscratch
|
||||
shiki
|
||||
Slidev
|
||||
sparkline
|
||||
speccharts
|
||||
sphinxcontrib
|
||||
ssim
|
||||
stylis
|
||||
|
||||
@@ -4,7 +4,6 @@ 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');
|
||||
|
||||
@@ -96,7 +95,6 @@ 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
|
||||
|
||||
@@ -71,6 +71,9 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
|
||||
|
||||
const external: string[] = ['require', 'fs', 'path'];
|
||||
const outFileName = getFileName(name, options);
|
||||
const { dependencies, version } = JSON.parse(
|
||||
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
|
||||
);
|
||||
const output: BuildOptions = buildOptions({
|
||||
...rest,
|
||||
absWorkingDir: resolve(__dirname, `../packages/${packageName}`),
|
||||
@@ -82,15 +85,13 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
|
||||
chunkNames: `chunks/${outFileName}/[name]-[hash]`,
|
||||
define: {
|
||||
// This needs to be stringified for esbuild
|
||||
includeLargeFeatures: `${includeLargeFeatures}`,
|
||||
'injected.includeLargeFeatures': `${includeLargeFeatures}`,
|
||||
'injected.version': `'${version}'`,
|
||||
'import.meta.vitest': 'undefined',
|
||||
},
|
||||
});
|
||||
|
||||
if (core) {
|
||||
const { dependencies } = JSON.parse(
|
||||
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
|
||||
);
|
||||
// Core build is used to generate file without bundled dependencies.
|
||||
// This is used by downstream projects to bundle dependencies themselves.
|
||||
// Ignore dependencies and any dependencies of dependencies
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -89,7 +89,7 @@ jobs:
|
||||
continue-on-error: ${{ github.event_name == 'push' }}
|
||||
run: pnpm run docs:verify
|
||||
|
||||
- uses: testomatio/check-tests@0ea638fcec1820cf2e7b9854fdbdd04128a55bd4 # stable
|
||||
- uses: testomatio/check-tests@cb0a0a628a9ade49b9f32360c812e15b9ef96d39 # stable
|
||||
with:
|
||||
framework: cypress
|
||||
tests: './cypress/e2e/**/**.spec.js'
|
||||
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -42,5 +42,4 @@ jobs:
|
||||
publish: pnpm changeset:publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
38
.github/workflows/validate-lockfile.yml
vendored
38
.github/workflows/validate-lockfile.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Validate pnpm-lock.yaml
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'pnpm-lock.yaml'
|
||||
- '**/package.json'
|
||||
@@ -15,13 +15,8 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Validate pnpm-lock.yaml entries
|
||||
id: validate # give this step an ID so we can reference its outputs
|
||||
@@ -55,16 +50,41 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Find existing lockfile validation comment
|
||||
if: always()
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: 'Lockfile Validation Failed'
|
||||
|
||||
- name: Comment on PR if validation failed
|
||||
if: failure()
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
❌ **Lockfile Validation Failed**
|
||||
|
||||
The following issue(s) were detected:
|
||||
${{ steps.validate.outputs.errors }}
|
||||
|
||||
Please address these and push an update.
|
||||
|
||||
_Posted automatically by GitHub Actions_
|
||||
|
||||
- name: Delete comment if validation passed
|
||||
if: success() && steps.find-comment.outputs.comment-id != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: ${{ steps.find-comment.outputs.comment-id }},
|
||||
});
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -51,8 +51,5 @@ demos/dev/**
|
||||
tsx-0/**
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# autogenereated by langium-cli and antlr-cli
|
||||
# autogenereated by langium-cli
|
||||
generated/
|
||||
|
||||
# autogenereated by antlr-cli
|
||||
.antlr/
|
||||
|
||||
@@ -10,7 +10,6 @@ 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');
|
||||
@@ -79,6 +78,8 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
|
||||
},
|
||||
define: {
|
||||
'import.meta.vitest': 'undefined',
|
||||
'injected.includeLargeFeatures': 'true',
|
||||
'injected.version': `'0.0.0'`,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [],
|
||||
@@ -95,10 +96,6 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
|
||||
}),
|
||||
...visualizerOptions(packageName, core),
|
||||
],
|
||||
define: {
|
||||
// Needs to be string
|
||||
includeLargeFeatures: 'true',
|
||||
},
|
||||
};
|
||||
|
||||
if (watch && config.build) {
|
||||
@@ -124,7 +121,6 @@ const main = async () => {
|
||||
};
|
||||
|
||||
await generateLangium();
|
||||
await generateAntlr();
|
||||
|
||||
if (watch) {
|
||||
await build(getBuildConfig({ minify: false, watch, core: false, entryName: 'parser' }));
|
||||
|
||||
@@ -5,7 +5,7 @@ USER 0:0
|
||||
RUN corepack enable \
|
||||
&& corepack enable pnpm
|
||||
|
||||
RUN apk add --no-cache git~=2.43.4 \
|
||||
RUN apk add --no-cache git~=2.43 \
|
||||
&& git config --add --system safe.directory /mermaid
|
||||
|
||||
ENV NODE_OPTIONS="--max_old_space_size=8192"
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CypressConfig {
|
||||
listUrl?: boolean;
|
||||
listId?: string;
|
||||
name?: string;
|
||||
screenshot?: boolean;
|
||||
}
|
||||
type CypressMermaidConfig = MermaidConfig & CypressConfig;
|
||||
|
||||
@@ -90,7 +91,7 @@ export const renderGraph = (
|
||||
|
||||
export const openURLAndVerifyRendering = (
|
||||
url: string,
|
||||
options: CypressMermaidConfig,
|
||||
{ screenshot = true, ...options }: CypressMermaidConfig,
|
||||
validation?: any
|
||||
): void => {
|
||||
const name: string = (options.name ?? cy.state('runnable').fullTitle()).replace(/\s+/g, '-');
|
||||
@@ -98,12 +99,16 @@ export const openURLAndVerifyRendering = (
|
||||
cy.visit(url);
|
||||
cy.window().should('have.property', 'rendered', true);
|
||||
cy.get('svg').should('be.visible');
|
||||
// cspell:ignore viewbox
|
||||
cy.get('svg').should('not.have.attr', 'viewbox');
|
||||
|
||||
if (validation) {
|
||||
cy.get('svg').should(validation);
|
||||
}
|
||||
|
||||
verifyScreenshot(name);
|
||||
if (screenshot) {
|
||||
verifyScreenshot(name);
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyScreenshot = (name: string): void => {
|
||||
|
||||
@@ -562,6 +562,20 @@ class C13["With Città foreign language"]
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should add notes in namespaces', function () {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
note "This is a outer note"
|
||||
note for C1 "This is a outer note for C1"
|
||||
namespace Namespace1 {
|
||||
note "This is a inner note"
|
||||
note for C1 "This is a inner note for C1"
|
||||
class C1
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should render a simple class diagram with no members', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
|
||||
@@ -709,6 +709,20 @@ class C13["With Città foreign language"]
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should add notes in namespaces', function () {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
note "This is a outer note"
|
||||
note for C1 "This is a outer note for C1"
|
||||
namespace Namespace1 {
|
||||
note "This is a inner note"
|
||||
note for C1 "This is a inner note for C1"
|
||||
class C1
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should render a simple class diagram with no members', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
|
||||
@@ -1029,4 +1029,19 @@ graph TD
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('FDH49: should add edge animation', () => {
|
||||
renderGraph(
|
||||
`
|
||||
flowchart TD
|
||||
A(["Start"]) L_A_B_0@--> B{"Decision"}
|
||||
B --> C["Option A"] & D["Option B"]
|
||||
style C stroke-width:4px,stroke-dasharray: 5
|
||||
L_A_B_0@{ animation: slow }
|
||||
L_B_D_0@{ animation: fast }`,
|
||||
{ look: 'handDrawn', screenshot: false }
|
||||
);
|
||||
cy.get('path#L_A_B_0').should('have.class', 'edge-animation-slow');
|
||||
cy.get('path#L_B_D_0').should('have.class', 'edge-animation-fast');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -774,6 +774,21 @@ describe('Graph', () => {
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
it('40: should add edge animation', () => {
|
||||
renderGraph(
|
||||
`
|
||||
flowchart TD
|
||||
A(["Start"]) L_A_B_0@--> B{"Decision"}
|
||||
B --> C["Option A"] & D["Option B"]
|
||||
style C stroke-width:4px,stroke-dasharray: 5
|
||||
L_A_B_0@{ animation: slow }
|
||||
L_B_D_0@{ animation: fast }`,
|
||||
{ screenshot: false }
|
||||
);
|
||||
// Verify animation classes are applied to both edges
|
||||
cy.get('path#L_A_B_0').should('have.class', 'edge-animation-slow');
|
||||
cy.get('path#L_B_D_0').should('have.class', 'edge-animation-fast');
|
||||
});
|
||||
it('58: handle styling with style expressions', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
@@ -973,4 +988,19 @@ graph TD
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('70: should render a subgraph with direction TD', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart LR
|
||||
subgraph A
|
||||
direction TD
|
||||
a --> b
|
||||
end
|
||||
`,
|
||||
{
|
||||
fontFamily: 'courier',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
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
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -603,6 +603,10 @@
|
||||
</div>
|
||||
<div class="test">
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
theme: dark
|
||||
---
|
||||
classDiagram
|
||||
test ()--() test2
|
||||
</pre>
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
}
|
||||
Admin --> Report : generates
|
||||
</pre>
|
||||
<hr />
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
namespace Company.Project.Module {
|
||||
@@ -240,6 +241,20 @@
|
||||
Bike --> Square : "Logo Shape"
|
||||
|
||||
</pre>
|
||||
<hr />
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
note "This is a outer note"
|
||||
note for Class1 "This is a outer note for Class1"
|
||||
namespace ns {
|
||||
note "This is a inner note"
|
||||
note for Class1 "This is a inner note for Class1"
|
||||
class Class1
|
||||
class Class2
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
mermaid.initialize({
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Recursive:wght@300..1000&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<style>
|
||||
.recursive-500 {
|
||||
font-family: 'Recursive', serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
'slnt' 0,
|
||||
'CASL' 0,
|
||||
'CRSV' 0.5,
|
||||
'MONO' 0;
|
||||
}
|
||||
body {
|
||||
/* background: rgb(221, 208, 208); */
|
||||
/* background: #333; */
|
||||
/* font-family: 'Arial'; */
|
||||
font-family: 'Recursive', serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
'slnt' 0,
|
||||
'CASL' 0,
|
||||
'CRSV' 0.5,
|
||||
'MONO' 0;
|
||||
/* color: white; */
|
||||
/* font-size: 18px !important; */
|
||||
}
|
||||
.gridify.tiny {
|
||||
background-image:
|
||||
linear-gradient(transparent 11px, rgba(220, 220, 200, 0.8) 12px, transparent 12px),
|
||||
linear-gradient(90deg, transparent 11px, rgba(220, 220, 200, 0.8) 12px, transparent 12px);
|
||||
background-size:
|
||||
100% 12px,
|
||||
12px 100%;
|
||||
}
|
||||
|
||||
.gridify.dots {
|
||||
background-image: radial-gradient(
|
||||
circle at center,
|
||||
rgba(220, 220, 200, 0.8) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mermaid svg {
|
||||
font-size: 16px !important;
|
||||
font-family: 'Recursive', serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
'slnt' 0,
|
||||
'CASL' 0,
|
||||
'CRSV' 0.5,
|
||||
'MONO' 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
/*box-shadow: 4px 4px 0px 0px #0000000F;*/
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="gridify dots">
|
||||
<p class="mb-20">Test Diagram</p>
|
||||
<div class="w-full h-64">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
usecase
|
||||
direction LR
|
||||
actor User1, User2, User3, User4
|
||||
actor Admin1, Admin2
|
||||
actor System1@{ "icon": "bell" }
|
||||
actor System2@{ "icon": "database" }
|
||||
|
||||
systemBoundary "Module A"
|
||||
"Feature A1"
|
||||
"Feature A2"
|
||||
"Admin A1"
|
||||
|
||||
end
|
||||
"Module A"@{ type: package }
|
||||
|
||||
systemBoundary "Module B"
|
||||
"Feature B1"
|
||||
"Feature B2"
|
||||
"Admin B1"
|
||||
end
|
||||
|
||||
User1 --important--> "Feature A1"
|
||||
User2 --> "Feature A2"
|
||||
Admin1 --> "Admin A1"
|
||||
User3 --> "Feature B1"
|
||||
User4 --> "Feature B2"
|
||||
Admin2 --> "Admin B1"
|
||||
|
||||
System1 <-- "Feature A1"
|
||||
System1 <-- "Feature B1"
|
||||
System2 <-- "Admin A1"
|
||||
System2 <-- "Admin B1"
|
||||
|
||||
User1 --"collaborates"--> User2
|
||||
Admin1 --"supervises"--> Admin2
|
||||
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||
|
||||
const staticBellIconPack = {
|
||||
prefix: 'fa6-regular',
|
||||
icons: {
|
||||
bell: {
|
||||
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
|
||||
width: 448,
|
||||
},
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
};
|
||||
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: 'logos',
|
||||
loader: () =>
|
||||
fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()),
|
||||
},
|
||||
{
|
||||
name: 'fa',
|
||||
loader: () =>
|
||||
fetch('https://unpkg.com/@iconify-json/fa6-solid/icons.json').then((res) => res.json()),
|
||||
},
|
||||
]);
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('Mermaid error: ', err);
|
||||
};
|
||||
window.callback = function () {
|
||||
alert('A callback was triggered');
|
||||
};
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
}
|
||||
await mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
// theme: 'forest',
|
||||
// handDrawnSeed: 12,
|
||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||
layout: 'dagre',
|
||||
// layout: 'elk',
|
||||
// layout: 'fixed',
|
||||
// htmlLabels: false,
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
|
||||
// fontFamily: 'Caveat',
|
||||
// fontFamily: 'Kalam',
|
||||
// fontFamily: 'courier',
|
||||
fontFamily: 'Recursive',
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
messageFontFamily: 'courier',
|
||||
},
|
||||
kanban: {
|
||||
htmlLabels: false,
|
||||
},
|
||||
fontSize: 16,
|
||||
logLevel: 0,
|
||||
securityLevel: 'loose',
|
||||
callback,
|
||||
});
|
||||
// setTimeout(() => {
|
||||
mermaid.init(undefined, document.querySelectorAll('.mermaid'));
|
||||
// }, 1000);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
> `const` **configKeys**: `Set`<`string`>
|
||||
|
||||
Defined in: [packages/mermaid/src/defaultConfig.ts:295](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L295)
|
||||
Defined in: [packages/mermaid/src/defaultConfig.ts:292](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L292)
|
||||
|
||||
@@ -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:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
|
||||
|
||||
---
|
||||
|
||||
@@ -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:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
|
||||
|
||||
---
|
||||
|
||||
@@ -292,7 +292,7 @@ Defines which main look to use for the diagram.
|
||||
|
||||
> `optional` **markdownAutoWrap**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/config.type.ts:217](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L217)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
|
||||
|
||||
---
|
||||
|
||||
@@ -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:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L222)
|
||||
|
||||
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,19 +466,11 @@ 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:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
- [GitHub Writer](https://github.com/ckeditor/github-writer)
|
||||
- [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator)
|
||||
- [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅
|
||||
- [GNU Octave](https://octave.org/) ✅
|
||||
- [octave_mermaid_js](https://github.com/CNOCTAVE/octave_mermaid_js) ✅
|
||||
- [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid)
|
||||
- [MonsterWriter](https://www.monsterwriter.com/) ✅
|
||||
- [Joplin](https://joplinapp.org) ✅
|
||||
@@ -272,6 +274,7 @@ Communication tools and platforms
|
||||
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
|
||||
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
||||
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
||||
- [speccharts: Turn your test suites into specification diagrams](https://github.com/arnaudrenaud/speccharts)
|
||||
- [Vitepress Plugin](https://github.com/sametcn99/vitepress-mermaid-renderer)
|
||||
|
||||
<!--- cspell:ignore Blazorade HueHive --->
|
||||
|
||||
@@ -47,7 +47,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
|
||||
|
||||
## Plans
|
||||
|
||||
- **Free** - A free plan that includes three diagrams.
|
||||
- **Free** - A free plan that includes six diagrams.
|
||||
|
||||
- **Pro** - A paid plan that includes unlimited diagrams, access to the collaboration feature, and more.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ title: Animal example
|
||||
classDiagram
|
||||
note "From Duck till Zebra"
|
||||
Animal <|-- Duck
|
||||
note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging"
|
||||
note for Duck "can fly<br>can swim<br>can dive<br>can help in debugging"
|
||||
Animal <|-- Fish
|
||||
Animal <|-- Zebra
|
||||
Animal : +int age
|
||||
@@ -50,7 +50,7 @@ title: Animal example
|
||||
classDiagram
|
||||
note "From Duck till Zebra"
|
||||
Animal <|-- Duck
|
||||
note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging"
|
||||
note for Duck "can fly<br>can swim<br>can dive<br>can help in debugging"
|
||||
Animal <|-- Fish
|
||||
Animal <|-- Zebra
|
||||
Animal : +int age
|
||||
|
||||
@@ -344,7 +344,6 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
|
||||
| Display | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` |
|
||||
| Divided Process | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-process`, `divided-rectangle` |
|
||||
| Document | Document | `doc` | Represents a document | `doc`, `document` |
|
||||
| Ellipse | Ellipse | `ellipse` | Ellipse shape | `oval` |
|
||||
| Event | Rounded Rectangle | `rounded` | Represents an event | `event` |
|
||||
| Extract | Triangle | `tri` | Extraction process | `extract`, `triangle` |
|
||||
| Fork/Join | Filled Rectangle | `fork` | Fork or join in process flow | `join` |
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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,7 +21,6 @@ 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,
|
||||
@@ -46,5 +45,4 @@ export const diagramData: DiagramMetadata[] = [
|
||||
packetDiagram,
|
||||
blockDiagram,
|
||||
treemapDiagram,
|
||||
usecaseDiagram,
|
||||
];
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"elk",
|
||||
"mermaid"
|
||||
],
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
"mermaid",
|
||||
"layout"
|
||||
],
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# mermaid
|
||||
|
||||
## 11.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7107](https://github.com/mermaid-js/mermaid/pull/7107) [`cbf8946`](https://github.com/mermaid-js/mermaid/commit/cbf89462acecac7a06f19843e8d48cb137df0753) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - fix: Updated the dependency dagre-d3-es to 7.0.13 to fix GHSA-cc8p-78qf-8p7q
|
||||
|
||||
## 11.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#6921](https://github.com/mermaid-js/mermaid/pull/6921) [`764b315`](https://github.com/mermaid-js/mermaid/commit/764b315dc16d0359add7c6b8e3ef7592e9bdc09c) Thanks [@quilicicf](https://github.com/quilicicf)! - feat: Add IDs in architecture diagrams
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6950](https://github.com/mermaid-js/mermaid/pull/6950) [`a957908`](https://github.com/mermaid-js/mermaid/commit/a9579083bfba367a4f4678547ec37ed7b61b9f5b) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - chore: Fix mindmap rendering in docs and apply tidytree layout
|
||||
|
||||
- [#6826](https://github.com/mermaid-js/mermaid/pull/6826) [`1d36810`](https://github.com/mermaid-js/mermaid/commit/1d3681053b9168354e48e5763023b6305cd1ca72) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Ensure edge label color is applied when using classDef with edge IDs
|
||||
|
||||
- [#6945](https://github.com/mermaid-js/mermaid/pull/6945) [`d318f1a`](https://github.com/mermaid-js/mermaid/commit/d318f1a13cd7429334a29c70e449074ec1cb9f68) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Resolve gantt chart crash due to invalid array length
|
||||
|
||||
- [#6918](https://github.com/mermaid-js/mermaid/pull/6918) [`cfe9238`](https://github.com/mermaid-js/mermaid/commit/cfe9238882cbe95416db1feea3112456a71b6aaf) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - chore: revert marked dependency from ^15.0.7 to ^16.0.0
|
||||
- Reverted marked package version to ^16.0.0 for better compatibility
|
||||
- This is a dependency update that maintains API compatibility
|
||||
- All tests pass with the updated version
|
||||
|
||||
## 11.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mermaid",
|
||||
"version": "11.11.0",
|
||||
"version": "11.12.1",
|
||||
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
|
||||
"type": "module",
|
||||
"module": "./dist/mermaid.core.mjs",
|
||||
@@ -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 --exclude '.*generated.*'",
|
||||
"checkCircle": "npx madge --circular ./src",
|
||||
"prepublishOnly": "pnpm docs:verify-version"
|
||||
},
|
||||
"repository": {
|
||||
@@ -76,7 +76,7 @@
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.11",
|
||||
"dagre-d3-es": "7.0.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.22",
|
||||
|
||||
@@ -189,7 +189,6 @@ This Markdown should be kept.
|
||||
| Display | Curved Trapezoid | \`curv-trap\` | Represents a display | \`curved-trapezoid\`, \`display\` |
|
||||
| Divided Process | Divided Rectangle | \`div-rect\` | Divided process shape | \`div-proc\`, \`divided-process\`, \`divided-rectangle\` |
|
||||
| Document | Document | \`doc\` | Represents a document | \`doc\`, \`document\` |
|
||||
| Ellipse | Ellipse | \`ellipse\` | Ellipse shape | \`oval\` |
|
||||
| Event | Rounded Rectangle | \`rounded\` | Represents an event | \`event\` |
|
||||
| Extract | Triangle | \`tri\` | Extraction process | \`extract\`, \`triangle\` |
|
||||
| Fork/Join | Filled Rectangle | \`fork\` | Fork or join in process flow | \`join\` |
|
||||
|
||||
@@ -210,7 +210,6 @@ export interface MermaidConfig {
|
||||
packet?: PacketDiagramConfig;
|
||||
block?: BlockDiagramConfig;
|
||||
radar?: RadarDiagramConfig;
|
||||
usecase?: UsecaseDiagramConfig;
|
||||
dompurifyConfig?: DOMPurifyConfiguration;
|
||||
wrap?: boolean;
|
||||
fontSize?: number;
|
||||
@@ -1624,50 +1623,6 @@ 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,9 +264,6 @@ const config: RequiredDeep<MermaidConfig> = {
|
||||
radar: {
|
||||
...defaultConfigJson.radar,
|
||||
},
|
||||
usecase: {
|
||||
...defaultConfigJson.usecase,
|
||||
},
|
||||
treemap: {
|
||||
useMaxWidth: true,
|
||||
padding: 10,
|
||||
|
||||
@@ -28,7 +28,6 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
import { treemap } from '../diagrams/treemap/detector.js';
|
||||
import { usecase } from '../diagrams/usecase/usecaseDetector.js';
|
||||
import '../type.d.ts';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
@@ -73,7 +72,7 @@ export const addDiagrams = () => {
|
||||
}
|
||||
);
|
||||
|
||||
if (includeLargeFeatures) {
|
||||
if (injected.includeLargeFeatures) {
|
||||
registerLazyLoadedDiagrams(flowchartElk, mindmap, architecture);
|
||||
}
|
||||
|
||||
@@ -102,7 +101,6 @@ export const addDiagrams = () => {
|
||||
xychart,
|
||||
block,
|
||||
radar,
|
||||
treemap,
|
||||
usecase
|
||||
treemap
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
ClassRelation,
|
||||
ClassNode,
|
||||
ClassNote,
|
||||
ClassNoteMap,
|
||||
ClassMap,
|
||||
NamespaceMap,
|
||||
NamespaceNode,
|
||||
@@ -33,15 +34,16 @@ const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
|
||||
|
||||
export class ClassDB implements DiagramDB {
|
||||
private relations: ClassRelation[] = [];
|
||||
private classes = new Map<string, ClassNode>();
|
||||
private classes: ClassMap = new Map<string, ClassNode>();
|
||||
private readonly styleClasses = new Map<string, StyleClass>();
|
||||
private notes: ClassNote[] = [];
|
||||
private notes: ClassNoteMap = new Map<string, ClassNote>();
|
||||
private interfaces: Interface[] = [];
|
||||
// private static classCounter = 0;
|
||||
private namespaces = new Map<string, NamespaceNode>();
|
||||
private namespaceCounter = 0;
|
||||
|
||||
private functions: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
private functions: Function[] = [];
|
||||
|
||||
constructor() {
|
||||
this.functions.push(this.setupToolTips.bind(this));
|
||||
@@ -124,7 +126,7 @@ export class ClassDB implements DiagramDB {
|
||||
annotations: [],
|
||||
styles: [],
|
||||
domId: MERMAID_DOM_ID_PREFIX + name + '-' + classCounter,
|
||||
} as ClassNode);
|
||||
});
|
||||
|
||||
classCounter++;
|
||||
}
|
||||
@@ -155,12 +157,12 @@ export class ClassDB implements DiagramDB {
|
||||
|
||||
public clear() {
|
||||
this.relations = [];
|
||||
this.classes = new Map();
|
||||
this.notes = [];
|
||||
this.classes = new Map<string, ClassNode>();
|
||||
this.notes = new Map<string, ClassNote>();
|
||||
this.interfaces = [];
|
||||
this.functions = [];
|
||||
this.functions.push(this.setupToolTips.bind(this));
|
||||
this.namespaces = new Map();
|
||||
this.namespaces = new Map<string, NamespaceNode>();
|
||||
this.namespaceCounter = 0;
|
||||
this.direction = 'TB';
|
||||
commonClear();
|
||||
@@ -178,7 +180,12 @@ export class ClassDB implements DiagramDB {
|
||||
return this.relations;
|
||||
}
|
||||
|
||||
public getNotes() {
|
||||
public getNote(id: string | number): ClassNote {
|
||||
const key = typeof id === 'number' ? `note${id}` : id;
|
||||
return this.notes.get(key)!;
|
||||
}
|
||||
|
||||
public getNotes(): ClassNoteMap {
|
||||
return this.notes;
|
||||
}
|
||||
|
||||
@@ -279,16 +286,19 @@ export class ClassDB implements DiagramDB {
|
||||
}
|
||||
}
|
||||
|
||||
public addNote(text: string, className: string) {
|
||||
public addNote(text: string, className: string): string {
|
||||
const index = this.notes.size;
|
||||
const note = {
|
||||
id: `note${this.notes.length}`,
|
||||
id: `note${index}`,
|
||||
class: className,
|
||||
text: text,
|
||||
index: index,
|
||||
};
|
||||
this.notes.push(note);
|
||||
this.notes.set(note.id, note);
|
||||
return note.id;
|
||||
}
|
||||
|
||||
public cleanupLabel(label: string) {
|
||||
public cleanupLabel(label: string): string {
|
||||
if (label.startsWith(':')) {
|
||||
label = label.substring(1);
|
||||
}
|
||||
@@ -354,7 +364,7 @@ export class ClassDB implements DiagramDB {
|
||||
});
|
||||
}
|
||||
|
||||
public getTooltip(id: string, namespace?: string) {
|
||||
public getTooltip(id: string, namespace?: string): string | undefined {
|
||||
if (namespace && this.namespaces.has(namespace)) {
|
||||
return this.namespaces.get(namespace)!.classes.get(id)!.tooltip;
|
||||
}
|
||||
@@ -534,10 +544,11 @@ export class ClassDB implements DiagramDB {
|
||||
|
||||
this.namespaces.set(id, {
|
||||
id: id,
|
||||
classes: new Map(),
|
||||
children: {},
|
||||
classes: new Map<string, ClassNode>(),
|
||||
notes: new Map<string, ClassNote>(),
|
||||
children: new Map<string, NamespaceNode>(),
|
||||
domId: MERMAID_DOM_ID_PREFIX + id + '-' + this.namespaceCounter,
|
||||
} as NamespaceNode);
|
||||
});
|
||||
|
||||
this.namespaceCounter++;
|
||||
}
|
||||
@@ -555,16 +566,23 @@ export class ClassDB implements DiagramDB {
|
||||
*
|
||||
* @param id - ID of the namespace to add
|
||||
* @param classNames - IDs of the class to add
|
||||
* @param noteNames - IDs of the notes to add
|
||||
* @public
|
||||
*/
|
||||
public addClassesToNamespace(id: string, classNames: string[]) {
|
||||
public addClassesToNamespace(id: string, classNames: string[], noteNames: string[]) {
|
||||
if (!this.namespaces.has(id)) {
|
||||
return;
|
||||
}
|
||||
for (const name of classNames) {
|
||||
const { className } = this.splitClassNameAndType(name);
|
||||
this.classes.get(className)!.parent = id;
|
||||
this.namespaces.get(id)!.classes.set(className, this.classes.get(className)!);
|
||||
const classNode = this.getClass(className);
|
||||
classNode.parent = id;
|
||||
this.namespaces.get(id)!.classes.set(className, classNode);
|
||||
}
|
||||
for (const noteName of noteNames) {
|
||||
const noteNode = this.getNote(noteName);
|
||||
noteNode.parent = id;
|
||||
this.namespaces.get(id)!.notes.set(noteName, noteNode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,36 +635,32 @@ export class ClassDB implements DiagramDB {
|
||||
const edges: Edge[] = [];
|
||||
const config = getConfig();
|
||||
|
||||
for (const namespaceKey of this.namespaces.keys()) {
|
||||
const namespace = this.namespaces.get(namespaceKey);
|
||||
if (namespace) {
|
||||
const node: Node = {
|
||||
id: namespace.id,
|
||||
label: namespace.id,
|
||||
isGroup: true,
|
||||
padding: config.class!.padding ?? 16,
|
||||
// parent node must be one of [rect, roundedWithTitle, noteGroup, divider]
|
||||
shape: 'rect',
|
||||
cssStyles: ['fill: none', 'stroke: black'],
|
||||
look: config.look,
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
for (const namespace of this.namespaces.values()) {
|
||||
const node: Node = {
|
||||
id: namespace.id,
|
||||
label: namespace.id,
|
||||
isGroup: true,
|
||||
padding: config.class!.padding ?? 16,
|
||||
// parent node must be one of [rect, roundedWithTitle, noteGroup, divider]
|
||||
shape: 'rect',
|
||||
cssStyles: [],
|
||||
look: config.look,
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
for (const classKey of this.classes.keys()) {
|
||||
const classNode = this.classes.get(classKey);
|
||||
if (classNode) {
|
||||
const node = classNode as unknown as Node;
|
||||
node.parentId = classNode.parent;
|
||||
node.look = config.look;
|
||||
nodes.push(node);
|
||||
}
|
||||
for (const classNode of this.classes.values()) {
|
||||
const node: Node = {
|
||||
...classNode,
|
||||
type: undefined,
|
||||
isGroup: false,
|
||||
parentId: classNode.parent,
|
||||
look: config.look,
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
let cnt = 0;
|
||||
for (const note of this.notes) {
|
||||
cnt++;
|
||||
for (const note of this.notes.values()) {
|
||||
const noteNode: Node = {
|
||||
id: note.id,
|
||||
label: note.text,
|
||||
@@ -660,14 +674,15 @@ export class ClassDB implements DiagramDB {
|
||||
`stroke: ${config.themeVariables.noteBorderColor}`,
|
||||
],
|
||||
look: config.look,
|
||||
parentId: note.parent,
|
||||
};
|
||||
nodes.push(noteNode);
|
||||
|
||||
const noteClassId = this.classes.get(note.class)?.id ?? '';
|
||||
const noteClassId = this.classes.get(note.class)?.id;
|
||||
|
||||
if (noteClassId) {
|
||||
const edge: Edge = {
|
||||
id: `edgeNote${cnt}`,
|
||||
id: `edgeNote${note.index}`,
|
||||
start: note.id,
|
||||
end: noteClassId,
|
||||
type: 'normal',
|
||||
@@ -697,7 +712,7 @@ export class ClassDB implements DiagramDB {
|
||||
nodes.push(interfaceNode);
|
||||
}
|
||||
|
||||
cnt = 0;
|
||||
let cnt = 0;
|
||||
for (const classRelation of this.relations) {
|
||||
cnt++;
|
||||
const edge: Edge = {
|
||||
|
||||
@@ -417,7 +417,7 @@ class C13["With Città foreign language"]
|
||||
note "This is a keyword: ${keyword}. It truly is."
|
||||
`;
|
||||
parser.parse(str);
|
||||
expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
|
||||
expect(classDb.getNote(0).text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
|
||||
});
|
||||
|
||||
it.each(keywords)(
|
||||
@@ -427,7 +427,7 @@ class C13["With Città foreign language"]
|
||||
note "${keyword}"`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(classDb.getNotes()[0].text).toEqual(`${keyword}`);
|
||||
expect(classDb.getNote(0).text).toEqual(`${keyword}`);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -441,7 +441,7 @@ class C13["With Città foreign language"]
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
|
||||
expect(classDb.getNote(0).text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
|
||||
});
|
||||
|
||||
it.each(keywords)(
|
||||
@@ -456,7 +456,7 @@ class C13["With Città foreign language"]
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(classDb.getNotes()[0].text).toEqual(`${keyword}`);
|
||||
expect(classDb.getNote(0).text).toEqual(`${keyword}`);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import utils, { getEdgeId } from '../../utils.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import common from '../common/common.js';
|
||||
import type { ClassRelation, ClassNote, ClassMap, NamespaceMap } from './classTypes.js';
|
||||
import type { ClassRelation, ClassMap, ClassNoteMap, NamespaceMap } from './classTypes.js';
|
||||
import type { EdgeData } from '../../types.js';
|
||||
|
||||
const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
|
||||
@@ -65,6 +65,9 @@ export const addNamespaces = function (
|
||||
|
||||
g.setNode(vertex.id, node);
|
||||
addClasses(vertex.classes, g, _id, diagObj, vertex.id);
|
||||
const classes: ClassMap = diagObj.db.getClasses();
|
||||
const relations: ClassRelation[] = diagObj.db.getRelations();
|
||||
addNotes(vertex.notes, g, relations.length + 1, classes, vertex.id);
|
||||
|
||||
log.info('setNode', node);
|
||||
});
|
||||
@@ -144,69 +147,74 @@ export const addClasses = function (
|
||||
* @param classes - Classes
|
||||
*/
|
||||
export const addNotes = function (
|
||||
notes: ClassNote[],
|
||||
notes: ClassNoteMap,
|
||||
g: graphlib.Graph,
|
||||
startEdgeId: number,
|
||||
classes: ClassMap
|
||||
classes: ClassMap,
|
||||
parent?: string
|
||||
) {
|
||||
log.info(notes);
|
||||
|
||||
notes.forEach(function (note, i) {
|
||||
const vertex = note;
|
||||
[...notes.values()]
|
||||
.filter((note) => note.parent === parent)
|
||||
.forEach(function (vertex) {
|
||||
const cssNoteStr = '';
|
||||
|
||||
const cssNoteStr = '';
|
||||
const styles = { labelStyle: '', style: '' };
|
||||
|
||||
const styles = { labelStyle: '', style: '' };
|
||||
const vertexText = vertex.text;
|
||||
|
||||
const vertexText = vertex.text;
|
||||
const radius = 0;
|
||||
const shape = 'note';
|
||||
const node = {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: shape,
|
||||
labelText: sanitizeText(vertexText),
|
||||
noteData: vertex,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: cssNoteStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
domId: vertex.id,
|
||||
tooltip: '',
|
||||
type: 'note',
|
||||
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
|
||||
};
|
||||
g.setNode(vertex.id, node);
|
||||
log.info('setNode', node);
|
||||
|
||||
const radius = 0;
|
||||
const shape = 'note';
|
||||
const node = {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: shape,
|
||||
labelText: sanitizeText(vertexText),
|
||||
noteData: vertex,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: cssNoteStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
domId: vertex.id,
|
||||
tooltip: '',
|
||||
type: 'note',
|
||||
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
|
||||
};
|
||||
g.setNode(vertex.id, node);
|
||||
log.info('setNode', node);
|
||||
if (parent) {
|
||||
g.setParent(vertex.id, parent);
|
||||
}
|
||||
|
||||
if (!vertex.class || !classes.has(vertex.class)) {
|
||||
return;
|
||||
}
|
||||
const edgeId = startEdgeId + i;
|
||||
if (!vertex.class || !classes.has(vertex.class)) {
|
||||
return;
|
||||
}
|
||||
const edgeId = startEdgeId + vertex.index;
|
||||
|
||||
const edgeData: EdgeData = {
|
||||
id: `edgeNote${edgeId}`,
|
||||
//Set relationship style and line type
|
||||
classes: 'relation',
|
||||
pattern: 'dotted',
|
||||
// Set link type for rendering
|
||||
arrowhead: 'none',
|
||||
//Set edge extra labels
|
||||
startLabelRight: '',
|
||||
endLabelLeft: '',
|
||||
//Set relation arrow types
|
||||
arrowTypeStart: 'none',
|
||||
arrowTypeEnd: 'none',
|
||||
style: 'fill:none',
|
||||
labelStyle: '',
|
||||
curve: interpolateToCurve(conf.curve, curveLinear),
|
||||
};
|
||||
const edgeData: EdgeData = {
|
||||
id: `edgeNote${edgeId}`,
|
||||
//Set relationship style and line type
|
||||
classes: 'relation',
|
||||
pattern: 'dotted',
|
||||
// Set link type for rendering
|
||||
arrowhead: 'none',
|
||||
//Set edge extra labels
|
||||
startLabelRight: '',
|
||||
endLabelLeft: '',
|
||||
//Set relation arrow types
|
||||
arrowTypeStart: 'none',
|
||||
arrowTypeEnd: 'none',
|
||||
style: 'fill:none',
|
||||
labelStyle: '',
|
||||
curve: interpolateToCurve(conf.curve, curveLinear),
|
||||
};
|
||||
|
||||
// Add the edge to the graph
|
||||
g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
|
||||
});
|
||||
// Add the edge to the graph
|
||||
g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -329,7 +337,7 @@ export const draw = async function (text: string, id: string, _version: string,
|
||||
const namespaces: NamespaceMap = diagObj.db.getNamespaces();
|
||||
const classes: ClassMap = diagObj.db.getClasses();
|
||||
const relations: ClassRelation[] = diagObj.db.getRelations();
|
||||
const notes: ClassNote[] = diagObj.db.getNotes();
|
||||
const notes: ClassNoteMap = diagObj.db.getNotes();
|
||||
log.info(relations);
|
||||
addNamespaces(namespaces, g, id, diagObj);
|
||||
addClasses(classes, g, id, diagObj);
|
||||
|
||||
@@ -206,7 +206,7 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
);
|
||||
});
|
||||
|
||||
const notes = diagObj.db.getNotes();
|
||||
const notes = diagObj.db.getNotes().values();
|
||||
notes.forEach(function (note) {
|
||||
log.debug(`Adding note: ${JSON.stringify(note)}`);
|
||||
const node = svgDraw.drawNote(diagram, note, conf, diagObj);
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface ClassNode {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
shape: string;
|
||||
shape: 'classBox';
|
||||
text: string;
|
||||
cssClasses: string;
|
||||
methods: ClassMember[];
|
||||
@@ -149,6 +149,8 @@ export interface ClassNote {
|
||||
id: string;
|
||||
class: string;
|
||||
text: string;
|
||||
index: number;
|
||||
parent?: string;
|
||||
}
|
||||
|
||||
export interface ClassRelation {
|
||||
@@ -177,6 +179,7 @@ export interface NamespaceNode {
|
||||
id: string;
|
||||
domId: string;
|
||||
classes: ClassMap;
|
||||
notes: ClassNoteMap;
|
||||
children: NamespaceMap;
|
||||
}
|
||||
|
||||
@@ -187,4 +190,5 @@ export interface StyleClass {
|
||||
}
|
||||
|
||||
export type ClassMap = Map<string, ClassNode>;
|
||||
export type ClassNoteMap = Map<string, ClassNote>;
|
||||
export type NamespaceMap = Map<string, NamespaceNode>;
|
||||
|
||||
@@ -275,8 +275,8 @@ statement
|
||||
;
|
||||
|
||||
namespaceStatement
|
||||
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $3); }
|
||||
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $4); }
|
||||
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $3[0], $3[1]); }
|
||||
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $4[0], $4[1]); }
|
||||
;
|
||||
|
||||
namespaceIdentifier
|
||||
@@ -284,9 +284,12 @@ namespaceIdentifier
|
||||
;
|
||||
|
||||
classStatements
|
||||
: classStatement {$$=[$1]}
|
||||
| classStatement NEWLINE {$$=[$1]}
|
||||
| classStatement NEWLINE classStatements {$3.unshift($1); $$=$3}
|
||||
: classStatement {$$=[[$1], []]}
|
||||
| classStatement NEWLINE {$$=[[$1], []]}
|
||||
| classStatement NEWLINE classStatements {$3[0].unshift($1); $$=$3}
|
||||
| noteStatement {$$=[[], [$1]]}
|
||||
| noteStatement NEWLINE {$$=[[], [$1]]}
|
||||
| noteStatement NEWLINE classStatements {$3[1].unshift($1); $$=$3}
|
||||
;
|
||||
|
||||
classStatement
|
||||
@@ -333,8 +336,8 @@ relationStatement
|
||||
;
|
||||
|
||||
noteStatement
|
||||
: NOTE_FOR className noteText { yy.addNote($3, $2); }
|
||||
| NOTE noteText { yy.addNote($2); }
|
||||
: NOTE_FOR className noteText { $$ = yy.addNote($3, $2); }
|
||||
| NOTE noteText { $$ = yy.addNote($2); }
|
||||
;
|
||||
|
||||
classDefStatement
|
||||
|
||||
@@ -13,6 +13,30 @@ const getStyles = (options) =>
|
||||
|
||||
}
|
||||
|
||||
.cluster-label text {
|
||||
fill: ${options.titleColor};
|
||||
}
|
||||
.cluster-label span {
|
||||
color: ${options.titleColor};
|
||||
}
|
||||
.cluster-label span p {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.cluster rect {
|
||||
fill: ${options.clusterBkg};
|
||||
stroke: ${options.clusterBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.cluster text {
|
||||
fill: ${options.titleColor};
|
||||
}
|
||||
|
||||
.cluster span {
|
||||
color: ${options.titleColor};
|
||||
}
|
||||
|
||||
.nodeLabel, .edgeLabel {
|
||||
color: ${options.classText};
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ const renderKatexUnsanitized = async (text: string, config: MermaidConfig): Prom
|
||||
return text.replace(katexRegex, 'MathML is unsupported in this environment.');
|
||||
}
|
||||
|
||||
if (includeLargeFeatures) {
|
||||
if (injected.includeLargeFeatures) {
|
||||
const { default: katex } = await import('katex');
|
||||
const outputMode =
|
||||
config.forceLegacyMathML || (!isMathMLSupported() && config.legacyMathML)
|
||||
|
||||
@@ -962,6 +962,7 @@ You have to call mermaid.initialize.`
|
||||
case 'round':
|
||||
return 'roundedRect';
|
||||
case 'ellipse':
|
||||
// @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976
|
||||
return 'ellipse';
|
||||
default:
|
||||
return vertex.type;
|
||||
|
||||
@@ -140,6 +140,7 @@ that id.
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
.*direction\s+TD[^\n]* return 'direction_td';
|
||||
|
||||
[^\s\"]+\@(?=[^\{\"]) { return 'LINK_ID'; }
|
||||
[0-9]+ return 'NUM';
|
||||
@@ -626,6 +627,8 @@ direction
|
||||
{ $$={stmt:'dir', value:'RL'};}
|
||||
| direction_lr
|
||||
{ $$={stmt:'dir', value:'LR'};}
|
||||
| direction_td
|
||||
{ $$={stmt:'dir', value:'TD'};}
|
||||
;
|
||||
|
||||
%%
|
||||
|
||||
@@ -309,4 +309,21 @@ describe('when parsing subgraphs', function () {
|
||||
expect(subgraphA.nodes).toContain('a');
|
||||
expect(subgraphA.nodes).not.toContain('c');
|
||||
});
|
||||
it('should correctly parse direction TD inside a subgraph', function () {
|
||||
const res = flow.parser.parse(`
|
||||
graph LR
|
||||
subgraph WithTD
|
||||
direction TD
|
||||
A1 --> A2
|
||||
end
|
||||
`);
|
||||
|
||||
const subgraphs = flow.parser.yy.getSubGraphs();
|
||||
expect(subgraphs.length).toBe(1);
|
||||
const subgraph = subgraphs[0];
|
||||
|
||||
expect(subgraph.dir).toBe('TD');
|
||||
expect(subgraph.nodes).toContain('A1');
|
||||
expect(subgraph.nodes).toContain('A2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { InfoFields, InfoDB } from './infoTypes.js';
|
||||
import packageJson from '../../../package.json' assert { type: 'json' };
|
||||
|
||||
export const DEFAULT_INFO_DB: InfoFields = {
|
||||
version: packageJson.version + (includeLargeFeatures ? '' : '-tiny'),
|
||||
version: injected.version + (injected.includeLargeFeatures ? '' : '-tiny'),
|
||||
} as const;
|
||||
|
||||
export const getVersion = (): string => DEFAULT_INFO_DB.version;
|
||||
|
||||
@@ -16,7 +16,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
|
||||
const svgWidth = bitWidth * bitsPerRow + 2;
|
||||
const svg: SVG = selectSvgElement(id);
|
||||
|
||||
svg.attr('viewbox', `0 0 ${svgWidth} ${svgHeight}`);
|
||||
svg.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
||||
configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth);
|
||||
|
||||
for (const [word, packet] of words.entries()) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Diagram } from '../../Diagram.js';
|
||||
import type { RadarDiagramConfig } from '../../config.type.js';
|
||||
import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import type { RadarDB, RadarAxis, RadarCurve } from './types.js';
|
||||
|
||||
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
|
||||
@@ -53,11 +54,9 @@ const drawFrame = (svg: SVG, config: Required<RadarDiagramConfig>): SVGGroup =>
|
||||
x: config.marginLeft + config.width / 2,
|
||||
y: config.marginTop + config.height / 2,
|
||||
};
|
||||
// Initialize the SVG
|
||||
svg
|
||||
.attr('viewbox', `0 0 ${totalWidth} ${totalHeight}`)
|
||||
.attr('width', totalWidth)
|
||||
.attr('height', totalHeight);
|
||||
configureSvgSize(svg, totalHeight, totalWidth, config.useMaxWidth ?? true);
|
||||
|
||||
svg.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
|
||||
// g element to center the radar chart
|
||||
return svg.append('g').attr('transform', `translate(${center.x}, ${center.y})`);
|
||||
};
|
||||
|
||||
@@ -32,13 +32,14 @@
|
||||
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
|
||||
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
|
||||
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
|
||||
<ID>[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
<ID>[^<>:\n,;@\s]+(?=\s+as\s) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
<ID>[^<>:\n,;@]+(?=\s*[\n;#]|$) { yytext = yytext.trim(); this.popState(); return 'ACTOR'; }
|
||||
<ID>[^<>:\n,;@]*\<[^\n]* { this.popState(); return 'INVALID'; }
|
||||
"box" { this.begin('LINE'); return 'box'; }
|
||||
"participant" { this.begin('ID'); return 'participant'; }
|
||||
"actor" { this.begin('ID'); return 'participant_actor'; }
|
||||
"create" return 'create';
|
||||
"destroy" { this.begin('ID'); return 'destroy'; }
|
||||
<ID>[^<\->\->:\n,;]+?([\-]*[^<\->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
|
||||
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
|
||||
"loop" { this.begin('LINE'); return 'loop'; }
|
||||
@@ -145,6 +146,7 @@ line
|
||||
: SPACE statement { $$ = $2 }
|
||||
| statement { $$ = $1 }
|
||||
| NEWLINE { $$=[]; }
|
||||
| INVALID { $$=[]; }
|
||||
;
|
||||
|
||||
box_section
|
||||
@@ -411,4 +413,4 @@ text2
|
||||
: TXT {$$ = yy.parseMessage($1.trim().substring(1)) }
|
||||
;
|
||||
|
||||
%%
|
||||
%%
|
||||
@@ -2609,5 +2609,17 @@ Bob->>Alice:Got it!
|
||||
expect(actors.get('E').type).toBe('entity');
|
||||
expect(actors.get('E').description).toBe('E');
|
||||
});
|
||||
it('should handle fail parsing when alias token causes conflicts in participant definition', async () => {
|
||||
let error = false;
|
||||
try {
|
||||
await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant SAS MyServiceWithMoreThan20Chars <br> service decription
|
||||
`);
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
expect(error).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
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-actor-shape line {
|
||||
stroke: ${options.actorBorder};
|
||||
fill: ${options.actorBkg};
|
||||
}
|
||||
.usecase-actor-shape circle, line {
|
||||
stroke: ${options.actorBorder};
|
||||
fill: ${options.actorBkg};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.usecase {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.usecase-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Ellipse shape styling for use cases */
|
||||
.usecase-element ellipse {
|
||||
fill: ${options.mainBkg ?? '#ffffff'};
|
||||
stroke: ${options.primaryColor};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.usecase-element .label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* General ellipse styling */
|
||||
.node ellipse {
|
||||
fill: ${options.mainBkg ?? '#ffffff'};
|
||||
stroke: ${options.nodeBorder ?? options.primaryColor};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.relationship {
|
||||
stroke: ${options.lineColor};
|
||||
fill: none;
|
||||
}
|
||||
|
||||
& .marker {
|
||||
fill: ${options.lineColor};
|
||||
stroke: ${options.lineColor};
|
||||
}
|
||||
|
||||
.relationship-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nodeLabel, .edgeLabel {
|
||||
color: ${options.classText};
|
||||
}
|
||||
.system-boundary {
|
||||
fill: ${options.clusterBkg};
|
||||
stroke: ${options.clusterBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
@@ -1,98 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -1,397 +0,0 @@
|
||||
import { log } from '../../logger.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
} from '../common/commonDb.js';
|
||||
import type {
|
||||
UsecaseFields,
|
||||
UsecaseDB,
|
||||
Actor,
|
||||
UseCase,
|
||||
SystemBoundary,
|
||||
Relationship,
|
||||
ActorMetadata,
|
||||
Direction,
|
||||
ClassDef,
|
||||
} from './usecaseTypes.js';
|
||||
import { DEFAULT_DIRECTION, ARROW_TYPE } from './usecaseTypes.js';
|
||||
import type { RequiredDeep } from 'type-fest';
|
||||
import type { UsecaseDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { getConfig as getGlobalConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { LayoutData, Node, ClusterNode, Edge } from '../../rendering-util/types.js';
|
||||
|
||||
export const DEFAULT_USECASE_CONFIG: Required<UsecaseDiagramConfig> = DEFAULT_CONFIG.usecase;
|
||||
|
||||
export const DEFAULT_USECASE_DB: RequiredDeep<UsecaseFields> = {
|
||||
actors: new Map(),
|
||||
useCases: new Map(),
|
||||
systemBoundaries: new Map(),
|
||||
relationships: [],
|
||||
classDefs: new Map(),
|
||||
direction: DEFAULT_DIRECTION,
|
||||
config: DEFAULT_USECASE_CONFIG,
|
||||
} as const;
|
||||
|
||||
let actors = new Map<string, Actor>();
|
||||
let useCases = new Map<string, UseCase>();
|
||||
let systemBoundaries = new Map<string, SystemBoundary>();
|
||||
let relationships: Relationship[] = [];
|
||||
let classDefs = new Map<string, ClassDef>();
|
||||
let direction: Direction = DEFAULT_DIRECTION;
|
||||
const config: Required<UsecaseDiagramConfig> = structuredClone(DEFAULT_USECASE_CONFIG);
|
||||
|
||||
const getConfig = (): Required<UsecaseDiagramConfig> => structuredClone(config);
|
||||
|
||||
const clear = (): void => {
|
||||
actors = new Map();
|
||||
useCases = new Map();
|
||||
systemBoundaries = new Map();
|
||||
relationships = [];
|
||||
classDefs = new Map();
|
||||
direction = DEFAULT_DIRECTION;
|
||||
commonClear();
|
||||
};
|
||||
|
||||
// Actor management
|
||||
const addActor = (actor: Actor): void => {
|
||||
if (!actor.id || !actor.name) {
|
||||
throw new Error(
|
||||
`Invalid actor: Actor must have both id and name. Received: ${JSON.stringify(actor)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!actors.has(actor.id)) {
|
||||
actors.set(actor.id, actor);
|
||||
log.debug(`Added actor: ${actor.id} (${actor.name})`);
|
||||
} else {
|
||||
log.debug(`Actor ${actor.id} already exists`);
|
||||
}
|
||||
};
|
||||
|
||||
const getActors = (): Map<string, Actor> => actors;
|
||||
|
||||
const getActor = (id: string): Actor | undefined => actors.get(id);
|
||||
|
||||
// UseCase management
|
||||
const addUseCase = (useCase: UseCase): void => {
|
||||
if (!useCase.id || !useCase.name) {
|
||||
throw new Error(
|
||||
`Invalid use case: Use case must have both id and name. Received: ${JSON.stringify(useCase)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!useCases.has(useCase.id)) {
|
||||
useCases.set(useCase.id, useCase);
|
||||
log.debug(`Added use case: ${useCase.id} (${useCase.name})`);
|
||||
} else {
|
||||
log.debug(`Use case ${useCase.id} already exists`);
|
||||
}
|
||||
};
|
||||
|
||||
const getUseCases = (): Map<string, UseCase> => useCases;
|
||||
|
||||
const getUseCase = (id: string): UseCase | undefined => useCases.get(id);
|
||||
|
||||
// SystemBoundary management
|
||||
const addSystemBoundary = (systemBoundary: SystemBoundary): void => {
|
||||
if (!systemBoundary.id || !systemBoundary.name) {
|
||||
throw new Error(
|
||||
`Invalid system boundary: System boundary must have both id and name. Received: ${JSON.stringify(systemBoundary)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!systemBoundaries.has(systemBoundary.id)) {
|
||||
systemBoundaries.set(systemBoundary.id, systemBoundary);
|
||||
log.debug(`Added system boundary: ${systemBoundary.name}`);
|
||||
} else {
|
||||
log.debug(`System boundary ${systemBoundary.id} already exists`);
|
||||
}
|
||||
};
|
||||
|
||||
const getSystemBoundaries = (): Map<string, SystemBoundary> => systemBoundaries;
|
||||
|
||||
const getSystemBoundary = (id: string): SystemBoundary | undefined => systemBoundaries.get(id);
|
||||
|
||||
// Relationship management
|
||||
const addRelationship = (relationship: Relationship): void => {
|
||||
// Validate relationship structure
|
||||
if (!relationship.id || !relationship.from || !relationship.to) {
|
||||
throw new Error(
|
||||
`Invalid relationship: Relationship must have id, from, and to fields. Received: ${JSON.stringify(relationship)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!relationship.type) {
|
||||
throw new Error(
|
||||
`Invalid relationship: Relationship must have a type. Received: ${JSON.stringify(relationship)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate relationship type
|
||||
const validTypes = ['association', 'include', 'extend'];
|
||||
if (!validTypes.includes(relationship.type)) {
|
||||
throw new Error(
|
||||
`Invalid relationship type: ${relationship.type}. Valid types are: ${validTypes.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate arrow type if present
|
||||
if (relationship.arrowType !== undefined) {
|
||||
const validArrowTypes = [0, 1, 2, 3, 4, 5, 6]; // SOLID_ARROW, BACK_ARROW, LINE_SOLID, CIRCLE_ARROW, CROSS_ARROW
|
||||
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;
|
||||
|
||||
// ClassDef management
|
||||
const addClassDef = (classDef: ClassDef): void => {
|
||||
if (!classDef.id) {
|
||||
throw new Error(
|
||||
`Invalid classDef: ClassDef must have an id. Received: ${JSON.stringify(classDef)}`
|
||||
);
|
||||
}
|
||||
|
||||
classDefs.set(classDef.id, classDef);
|
||||
log.debug(`Added classDef: ${classDef.id}`);
|
||||
};
|
||||
|
||||
const getClassDefs = (): Map<string, ClassDef> => classDefs;
|
||||
|
||||
const getClassDef = (id: string): ClassDef | undefined => classDefs.get(id);
|
||||
|
||||
/**
|
||||
* Get compiled styles from class definitions
|
||||
* Similar to flowchart's getCompiledStyles method
|
||||
*/
|
||||
const getCompiledStyles = (classNames: string[]): string[] => {
|
||||
let compiledStyles: string[] = [];
|
||||
for (const className of classNames) {
|
||||
const cssClass = classDefs.get(className);
|
||||
if (cssClass?.styles) {
|
||||
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim());
|
||||
}
|
||||
}
|
||||
return compiledStyles;
|
||||
};
|
||||
|
||||
// Direction management
|
||||
const setDirection = (dir: Direction): void => {
|
||||
// Normalize TD to TB (same as flowchart)
|
||||
if (dir === 'TD') {
|
||||
direction = 'TB';
|
||||
} else {
|
||||
direction = dir;
|
||||
}
|
||||
log.debug('Direction set to:', direction);
|
||||
};
|
||||
|
||||
const getDirection = (): Direction => direction;
|
||||
|
||||
// Convert usecase diagram data to LayoutData format for unified rendering
|
||||
const getData = (): LayoutData => {
|
||||
const globalConfig = getGlobalConfig();
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
// Convert actors to nodes
|
||||
for (const actor of actors.values()) {
|
||||
const classesArray = ['default', 'usecase-actor'];
|
||||
const cssCompiledStyles = getCompiledStyles(classesArray);
|
||||
|
||||
// Determine which shape to use based on whether actor has an icon
|
||||
const actorShape = actor.metadata?.icon ? 'usecaseActorIcon' : 'usecaseActor';
|
||||
|
||||
const node: Node = {
|
||||
id: actor.id,
|
||||
label: actor.name,
|
||||
description: actor.description ? [actor.description] : undefined,
|
||||
shape: actorShape, // Use icon shape if icon is present, otherwise stick figure
|
||||
isGroup: false,
|
||||
padding: 10,
|
||||
look: globalConfig.look,
|
||||
// Add metadata as data attributes for styling
|
||||
cssClasses: `usecase-actor ${
|
||||
actor.metadata && Object.keys(actor.metadata).length > 0
|
||||
? Object.entries(actor.metadata)
|
||||
.map(([key, value]) => `actor-${key}-${value}`)
|
||||
.join(' ')
|
||||
: ''
|
||||
}`.trim(),
|
||||
cssStyles: actor.styles ?? [], // Direct styles
|
||||
cssCompiledStyles, // Compiled styles from class definitions
|
||||
// Pass actor metadata to the shape handler
|
||||
metadata: actor.metadata,
|
||||
} as Node & { metadata?: ActorMetadata };
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
// Convert use cases to nodes
|
||||
for (const useCase of useCases.values()) {
|
||||
// Build CSS classes string
|
||||
let cssClasses = 'usecase-element';
|
||||
const classesArray = ['default', 'usecase-element'];
|
||||
if (useCase.classes && useCase.classes.length > 0) {
|
||||
cssClasses += ' ' + useCase.classes.join(' ');
|
||||
classesArray.push(...useCase.classes);
|
||||
}
|
||||
|
||||
// Get compiled styles from class definitions
|
||||
const cssCompiledStyles = getCompiledStyles(classesArray);
|
||||
const node: Node = {
|
||||
id: useCase.id,
|
||||
label: useCase.name,
|
||||
description: useCase.description ? [useCase.description] : undefined,
|
||||
shape: 'ellipse', // Use ellipse shape for use cases
|
||||
isGroup: false,
|
||||
padding: 10,
|
||||
look: globalConfig.look,
|
||||
cssClasses,
|
||||
cssStyles: useCase.styles ?? [], // Direct styles
|
||||
cssCompiledStyles, // Compiled styles from class definitions
|
||||
// If use case belongs to a system boundary, set parentId
|
||||
...(useCase.systemBoundary && { parentId: useCase.systemBoundary }),
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
// Convert system boundaries to group nodes
|
||||
for (const boundary of systemBoundaries.values()) {
|
||||
const classesArray = [
|
||||
'default',
|
||||
'system-boundary',
|
||||
`system-boundary-${boundary.type ?? 'rect'}`,
|
||||
];
|
||||
const cssCompiledStyles = getCompiledStyles(classesArray);
|
||||
|
||||
const node: ClusterNode & { boundaryType?: string } = {
|
||||
id: boundary.id,
|
||||
label: boundary.name,
|
||||
shape: 'usecaseSystemBoundary', // Use custom usecase system boundary cluster shape
|
||||
isGroup: true, // System boundaries are clusters (containers for other nodes)
|
||||
padding: 20,
|
||||
look: globalConfig.look,
|
||||
cssClasses: `system-boundary system-boundary-${boundary.type ?? 'rect'}`,
|
||||
cssStyles: boundary.styles ?? [], // Direct styles
|
||||
cssCompiledStyles, // Compiled styles from class definitions
|
||||
// Pass boundary type to the shape handler
|
||||
boundaryType: boundary.type,
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
// Convert relationships to edges
|
||||
relationships.forEach((relationship, index) => {
|
||||
// Determine arrow types based on relationship.arrowType
|
||||
let arrowTypeEnd = 'none';
|
||||
let arrowTypeStart = 'none';
|
||||
|
||||
switch (relationship.arrowType) {
|
||||
case ARROW_TYPE.SOLID_ARROW: // -->
|
||||
arrowTypeEnd = 'arrow_point';
|
||||
break;
|
||||
case ARROW_TYPE.BACK_ARROW: // <--
|
||||
arrowTypeStart = 'arrow_point';
|
||||
break;
|
||||
case ARROW_TYPE.CIRCLE_ARROW: // --o
|
||||
arrowTypeEnd = 'arrow_circle';
|
||||
break;
|
||||
case ARROW_TYPE.CROSS_ARROW: // --x
|
||||
arrowTypeEnd = 'arrow_cross';
|
||||
break;
|
||||
case ARROW_TYPE.CIRCLE_ARROW_REVERSED: // o--
|
||||
arrowTypeStart = 'arrow_circle';
|
||||
break;
|
||||
case ARROW_TYPE.CROSS_ARROW_REVERSED: // x--
|
||||
arrowTypeStart = 'arrow_cross';
|
||||
break;
|
||||
case ARROW_TYPE.LINE_SOLID: // --
|
||||
// Both remain 'none'
|
||||
break;
|
||||
}
|
||||
|
||||
const edge: Edge = {
|
||||
id: relationship.id || `edge-${index}`,
|
||||
start: relationship.from,
|
||||
end: relationship.to,
|
||||
source: relationship.from,
|
||||
target: relationship.to,
|
||||
label: relationship.label,
|
||||
labelpos: 'c', // Center label position for proper dagre layout
|
||||
type: relationship.type,
|
||||
arrowTypeEnd,
|
||||
arrowTypeStart,
|
||||
classes: `relationship relationship-${relationship.type}`,
|
||||
look: globalConfig.look,
|
||||
thickness: 'normal',
|
||||
pattern: 'solid',
|
||||
};
|
||||
edges.push(edge);
|
||||
});
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
config: globalConfig,
|
||||
// Additional properties that layout algorithms might expect
|
||||
type: 'usecase',
|
||||
layoutAlgorithm: 'dagre', // Default layout algorithm
|
||||
direction: getDirection(), // Use the current direction setting
|
||||
nodeSpacing: 50, // Default node spacing
|
||||
rankSpacing: 50, // Default rank spacing
|
||||
markers: ['arrow_point'], // Arrow point markers used in usecase diagrams
|
||||
};
|
||||
};
|
||||
|
||||
export const db: UsecaseDB = {
|
||||
getConfig,
|
||||
|
||||
clear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
|
||||
addActor,
|
||||
getActors,
|
||||
getActor,
|
||||
|
||||
addUseCase,
|
||||
getUseCases,
|
||||
getUseCase,
|
||||
|
||||
addSystemBoundary,
|
||||
getSystemBoundaries,
|
||||
getSystemBoundary,
|
||||
|
||||
addRelationship,
|
||||
getRelationships,
|
||||
|
||||
addClassDef,
|
||||
getClassDefs,
|
||||
getClassDef,
|
||||
|
||||
// Direction management
|
||||
setDirection,
|
||||
getDirection,
|
||||
|
||||
// Add getData method for unified rendering
|
||||
getData,
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'usecase';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*usecase/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./usecaseDiagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
export const usecase: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,188 +0,0 @@
|
||||
// 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,
|
||||
ClassDef,
|
||||
} from './usecaseTypes.js';
|
||||
import { db } from './usecaseDb.js';
|
||||
|
||||
// ANTLR parser result interface
|
||||
interface UsecaseParseResult {
|
||||
actors: { id: string; name: string; metadata?: Record<string, string>; styles?: string[] }[];
|
||||
useCases: {
|
||||
id: string;
|
||||
name: string;
|
||||
nodeId?: string;
|
||||
systemBoundary?: string;
|
||||
classes?: string[];
|
||||
styles?: string[];
|
||||
}[];
|
||||
systemBoundaries: {
|
||||
id: string;
|
||||
name: string;
|
||||
useCases: string[];
|
||||
type?: 'package' | 'rect';
|
||||
styles?: string[];
|
||||
}[];
|
||||
relationships: {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'association' | 'include' | 'extend';
|
||||
arrowType: number;
|
||||
label?: string;
|
||||
}[];
|
||||
classDefs?: Map<string, { id: string; styles: string[] }>;
|
||||
direction?: string;
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse usecase diagram using ANTLR parser
|
||||
*/
|
||||
const parseUsecaseWithAntlr = async (input: string): Promise<UsecaseParseResult> => {
|
||||
// Use the ANTLR parser from @mermaid-js/parser
|
||||
const result = (await parse('usecase', input)) as UsecaseParseResult;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Populate the database with parsed ANTLR results
|
||||
*/
|
||||
const populateDb = (ast: UsecaseParseResult, db: UsecaseDB) => {
|
||||
// Clear existing data
|
||||
db.clear();
|
||||
|
||||
// Add actors (ANTLR result already has id, name, and metadata)
|
||||
ast.actors.forEach((actorData) => {
|
||||
const actor: Actor = {
|
||||
id: actorData.id,
|
||||
name: actorData.name,
|
||||
metadata: actorData.metadata,
|
||||
styles: actorData.styles,
|
||||
};
|
||||
db.addActor(actor);
|
||||
});
|
||||
|
||||
// Add use cases (ANTLR result already has id, name, nodeId, systemBoundary, and classes)
|
||||
ast.useCases.forEach((useCaseData) => {
|
||||
const useCase: UseCase = {
|
||||
id: useCaseData.id,
|
||||
name: useCaseData.name,
|
||||
nodeId: useCaseData.nodeId,
|
||||
systemBoundary: useCaseData.systemBoundary,
|
||||
classes: useCaseData.classes,
|
||||
styles: useCaseData.styles,
|
||||
};
|
||||
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
|
||||
styles: boundaryData.styles,
|
||||
};
|
||||
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);
|
||||
});
|
||||
|
||||
// Add class definitions
|
||||
if (ast.classDefs) {
|
||||
ast.classDefs.forEach((classDefData) => {
|
||||
const classDef: ClassDef = {
|
||||
id: classDefData.id,
|
||||
styles: classDefData.styles,
|
||||
};
|
||||
db.addClassDef(classDef);
|
||||
});
|
||||
}
|
||||
|
||||
// Set direction if provided
|
||||
if (ast.direction) {
|
||||
db.setDirection(ast.direction as any);
|
||||
}
|
||||
|
||||
log.debug('Populated usecase database:', {
|
||||
actors: ast.actors.length,
|
||||
useCases: ast.useCases.length,
|
||||
relationships: ast.relationships.length,
|
||||
classDefs: ast.classDefs?.size ?? 0,
|
||||
direction: ast.direction,
|
||||
});
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parse: async (input: string): Promise<void> => {
|
||||
log.debug('Parsing usecase diagram with ANTLR:', input);
|
||||
|
||||
try {
|
||||
// Use our ANTLR parser
|
||||
const ast: UsecaseParseResult = await parseUsecaseWithAntlr(input);
|
||||
log.debug('ANTLR parsing result:', ast);
|
||||
|
||||
// Populate common database fields
|
||||
populateCommonDb(ast as any, db);
|
||||
|
||||
// Populate the database with validation
|
||||
populateDb(ast, db);
|
||||
|
||||
log.debug('Usecase diagram parsing completed successfully');
|
||||
} catch (error) {
|
||||
log.error('Error parsing usecase diagram:', error);
|
||||
|
||||
// Check if it's a UsecaseParseError from our ANTLR parser
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'name' in error &&
|
||||
error.name === 'UsecaseParseError'
|
||||
) {
|
||||
// Re-throw the detailed error for better error reporting
|
||||
throw error;
|
||||
}
|
||||
|
||||
// For other errors, wrap them in a generic error
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
const wrappedError = new Error(`Failed to parse usecase diagram: ${errorMessage}`);
|
||||
|
||||
// Add hash property for consistency with other diagram types
|
||||
(wrappedError as any).hash = {
|
||||
text: input.split('\n')[0] || '',
|
||||
token: 'unknown',
|
||||
line: '1',
|
||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
||||
expected: ['valid usecase syntax'],
|
||||
};
|
||||
|
||||
throw wrappedError;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { DrawDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import utils from '../../utils.js';
|
||||
import type { UsecaseDB } from './usecaseTypes.js';
|
||||
|
||||
/**
|
||||
* Main draw function using unified rendering system
|
||||
*/
|
||||
const draw: DrawDefinition = async (_text, id, _version, diag) => {
|
||||
log.info('Drawing usecase diagram (unified)', id);
|
||||
const { securityLevel, usecase: conf, layout } = getConfig();
|
||||
|
||||
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
|
||||
// into the Layout data format
|
||||
const usecaseDb = diag.db as UsecaseDB;
|
||||
const data4Layout = usecaseDb.getData();
|
||||
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
|
||||
data4Layout.nodeSpacing = 50; // Default node spacing
|
||||
data4Layout.rankSpacing = 50; // Default rank spacing
|
||||
data4Layout.markers = ['point', 'circle', 'cross']; // Support point, circle, and cross markers
|
||||
data4Layout.diagramId = id;
|
||||
|
||||
log.debug('Usecase layout data:', data4Layout);
|
||||
|
||||
// Use the unified rendering system
|
||||
await render(data4Layout, svg);
|
||||
|
||||
const padding = 8;
|
||||
utils.insertTitle(
|
||||
svg,
|
||||
'usecaseDiagramTitleText',
|
||||
0, // Default title top margin
|
||||
usecaseDb.getDiagramTitle?.() ?? ''
|
||||
);
|
||||
setupViewPortForSVG(svg, padding, 'usecaseDiagram', conf?.useMaxWidth ?? false);
|
||||
};
|
||||
|
||||
export const renderer = { draw };
|
||||
@@ -1,40 +0,0 @@
|
||||
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;
|
||||
@@ -1,113 +0,0 @@
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { UsecaseDiagramConfig } from '../../config.type.js';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
|
||||
export type ActorMetadata = Record<string, string>;
|
||||
|
||||
export interface Actor {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
metadata?: ActorMetadata;
|
||||
styles?: string[]; // Direct CSS styles applied to this actor
|
||||
}
|
||||
|
||||
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
|
||||
classes?: string[]; // CSS classes applied to this use case
|
||||
styles?: string[]; // Direct CSS styles applied to this use case
|
||||
}
|
||||
|
||||
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')
|
||||
styles?: string[]; // Direct CSS styles applied to this system boundary
|
||||
}
|
||||
|
||||
// Arrow types for usecase diagrams (matching parser types)
|
||||
export const ARROW_TYPE = {
|
||||
SOLID_ARROW: 0, // -->
|
||||
BACK_ARROW: 1, // <--
|
||||
LINE_SOLID: 2, // --
|
||||
CIRCLE_ARROW: 3, // --o
|
||||
CROSS_ARROW: 4, // --x
|
||||
CIRCLE_ARROW_REVERSED: 5, // o--
|
||||
CROSS_ARROW_REVERSED: 6, // x--
|
||||
} as const;
|
||||
|
||||
export type ArrowType = (typeof ARROW_TYPE)[keyof typeof ARROW_TYPE];
|
||||
|
||||
export interface Relationship {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'association' | 'include' | 'extend';
|
||||
arrowType: ArrowType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
// Direction types for usecase diagrams
|
||||
export type Direction = 'TB' | 'TD' | 'BT' | 'RL' | 'LR';
|
||||
|
||||
export const DEFAULT_DIRECTION: Direction = 'LR';
|
||||
|
||||
export interface ClassDef {
|
||||
id: string;
|
||||
styles: string[];
|
||||
}
|
||||
|
||||
export interface UsecaseFields {
|
||||
actors: Map<string, Actor>;
|
||||
useCases: Map<string, UseCase>;
|
||||
systemBoundaries: Map<string, SystemBoundary>;
|
||||
relationships: Relationship[];
|
||||
classDefs: Map<string, ClassDef>;
|
||||
direction: Direction;
|
||||
config: Required<UsecaseDiagramConfig>;
|
||||
}
|
||||
|
||||
export interface UsecaseDB extends DiagramDB {
|
||||
getConfig: () => Required<UsecaseDiagramConfig>;
|
||||
|
||||
// Actor management
|
||||
addActor: (actor: Actor) => void;
|
||||
getActors: () => Map<string, Actor>;
|
||||
getActor: (id: string) => Actor | undefined;
|
||||
|
||||
// UseCase management
|
||||
addUseCase: (useCase: UseCase) => void;
|
||||
getUseCases: () => Map<string, UseCase>;
|
||||
getUseCase: (id: string) => UseCase | undefined;
|
||||
|
||||
// SystemBoundary management
|
||||
addSystemBoundary: (systemBoundary: SystemBoundary) => void;
|
||||
getSystemBoundaries: () => Map<string, SystemBoundary>;
|
||||
getSystemBoundary: (id: string) => SystemBoundary | undefined;
|
||||
|
||||
// Relationship management
|
||||
addRelationship: (relationship: Relationship) => void;
|
||||
getRelationships: () => Relationship[];
|
||||
|
||||
// ClassDef management
|
||||
addClassDef: (classDef: ClassDef) => void;
|
||||
getClassDefs: () => Map<string, ClassDef>;
|
||||
getClassDef: (id: string) => ClassDef | undefined;
|
||||
|
||||
// Direction management
|
||||
setDirection: (direction: Direction) => void;
|
||||
getDirection: () => Direction;
|
||||
|
||||
// Unified rendering support
|
||||
getData: () => LayoutData;
|
||||
|
||||
// Utility methods
|
||||
clear: () => void;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkdownOptions } from 'vitepress';
|
||||
import { defineConfig } from 'vitepress';
|
||||
import packageJson from '../../../package.json' assert { type: 'json' };
|
||||
import packageJson from '../../../package.json' with { type: 'json' };
|
||||
import { addCanonicalUrls } from './canonical-urls.js';
|
||||
import MermaidExample from './mermaid-markdown-all.js';
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
- [GitHub Writer](https://github.com/ckeditor/github-writer)
|
||||
- [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator)
|
||||
- [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅
|
||||
- [GNU Octave](https://octave.org/) ✅
|
||||
- [octave_mermaid_js](https://github.com/CNOCTAVE/octave_mermaid_js) ✅
|
||||
- [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid)
|
||||
- [MonsterWriter](https://www.monsterwriter.com/) ✅
|
||||
- [Joplin](https://joplinapp.org) ✅
|
||||
@@ -267,6 +269,7 @@ Communication tools and platforms
|
||||
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
|
||||
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
||||
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
||||
- [speccharts: Turn your test suites into specification diagrams](https://github.com/arnaudrenaud/speccharts)
|
||||
- [Vitepress Plugin](https://github.com/sametcn99/vitepress-mermaid-renderer)
|
||||
|
||||
<!--- cspell:ignore Blazorade HueHive --->
|
||||
|
||||
@@ -41,7 +41,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
|
||||
|
||||
## Plans
|
||||
|
||||
- **Free** - A free plan that includes three diagrams.
|
||||
- **Free** - A free plan that includes six diagrams.
|
||||
|
||||
- **Pro** - A paid plan that includes unlimited diagrams, access to the collaboration feature, and more.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ title: Animal example
|
||||
classDiagram
|
||||
note "From Duck till Zebra"
|
||||
Animal <|-- Duck
|
||||
note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging"
|
||||
note for Duck "can fly<br>can swim<br>can dive<br>can help in debugging"
|
||||
Animal <|-- Fish
|
||||
Animal <|-- Zebra
|
||||
Animal : +int age
|
||||
|
||||
@@ -7,7 +7,6 @@ import { select } from 'd3';
|
||||
import { compile, serialize, stringify } from 'stylis';
|
||||
import DOMPurify from 'dompurify';
|
||||
import isEmpty from 'lodash-es/isEmpty.js';
|
||||
import packageJson from '../package.json' assert { type: 'json' };
|
||||
import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js';
|
||||
import assignWithDepth from './assignWithDepth.js';
|
||||
import * as configApi from './config.js';
|
||||
@@ -421,12 +420,12 @@ const render = async function (
|
||||
// -------------------------------------------------------------------------------
|
||||
// Draw the diagram with the renderer
|
||||
try {
|
||||
await diag.renderer.draw(text, id, packageJson.version, diag);
|
||||
await diag.renderer.draw(text, id, injected.version, diag);
|
||||
} catch (e) {
|
||||
if (config.suppressErrorRendering) {
|
||||
removeTempElements();
|
||||
} else {
|
||||
errorRenderer.draw(text, id, packageJson.version);
|
||||
errorRenderer.draw(text, id, injected.version);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const registerDefaultLayoutLoaders = () => {
|
||||
name: 'dagre',
|
||||
loader: async () => await import('./layout-algorithms/dagre/index.js'),
|
||||
},
|
||||
...(includeLargeFeatures
|
||||
...(injected.includeLargeFeatures
|
||||
? [
|
||||
{
|
||||
name: 'cose-bilkent',
|
||||
|
||||
@@ -459,173 +459,6 @@ const divider = (parent, node) => {
|
||||
return { cluster: shapeSvg, labelBBox: {} };
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom cluster shape for usecase system boundaries
|
||||
* Supports two types: 'rect' (dashed rectangle) and 'package' (UML package notation)
|
||||
* @param {any} parent
|
||||
* @param {any} node
|
||||
* @returns {any} ShapeSvg
|
||||
*/
|
||||
const usecaseSystemBoundary = async (parent, node) => {
|
||||
log.info('Creating usecase system boundary for ', node.id, node);
|
||||
const siteConfig = getConfig();
|
||||
const { handDrawnSeed } = siteConfig;
|
||||
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', 'cluster usecase-system-boundary ' + node.cssClasses)
|
||||
.attr('id', node.id)
|
||||
.attr('data-look', node.look);
|
||||
|
||||
// Get boundary type from node metadata (default to 'rect')
|
||||
const boundaryType = node.boundaryType || 'rect';
|
||||
shapeSvg.attr('data-boundary-type', boundaryType);
|
||||
|
||||
const useHtmlLabels = evaluate(siteConfig.flowchart?.htmlLabels);
|
||||
|
||||
// Create the label
|
||||
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label');
|
||||
const text = await createText(labelEl, node.label, {
|
||||
style: node.labelStyle,
|
||||
useHtmlLabels,
|
||||
isNode: true,
|
||||
});
|
||||
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
if (evaluate(siteConfig.flowchart?.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
dv.attr('width', bbox.width);
|
||||
dv.attr('height', bbox.height);
|
||||
}
|
||||
|
||||
// Calculate width with padding (similar to rect cluster)
|
||||
const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;
|
||||
if (node.width <= bbox.width + node.padding) {
|
||||
node.diff = (width - node.width) / 2 - node.padding;
|
||||
} else {
|
||||
node.diff = -node.padding;
|
||||
}
|
||||
|
||||
const height = node.height;
|
||||
// Use absolute coordinates from layout engine (like rect cluster does)
|
||||
const x = node.x - width / 2;
|
||||
const y = node.y - height / 2;
|
||||
|
||||
let boundaryRect;
|
||||
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
|
||||
|
||||
if (boundaryType === 'package') {
|
||||
// Draw package-type boundary (rectangle with separate name box at top)
|
||||
const nameBoxWidth = Math.max(80, bbox.width + 20);
|
||||
const nameBoxHeight = 25;
|
||||
|
||||
if (node.look === 'handDrawn') {
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {
|
||||
stroke: 'black',
|
||||
strokeWidth: 2,
|
||||
fill: 'none',
|
||||
seed: handDrawnSeed,
|
||||
});
|
||||
|
||||
// Draw main boundary rectangle
|
||||
const roughRect = rc.rectangle(x, y, width, height, options);
|
||||
boundaryRect = shapeSvg.insert(() => roughRect, ':first-child');
|
||||
|
||||
// Draw name box at top-left
|
||||
const roughNameBox = rc.rectangle(x, y - nameBoxHeight, nameBoxWidth, nameBoxHeight, options);
|
||||
shapeSvg.insert(() => roughNameBox, ':first-child');
|
||||
} else {
|
||||
// Draw main boundary rectangle
|
||||
boundaryRect = shapeSvg
|
||||
.insert('rect', ':first-child')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw name box at top-left
|
||||
shapeSvg
|
||||
.insert('rect', ':first-child')
|
||||
.attr('x', x)
|
||||
.attr('y', y - nameBoxHeight)
|
||||
.attr('width', nameBoxWidth)
|
||||
.attr('height', nameBoxHeight)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
}
|
||||
|
||||
// Position label in the center of the name box (using absolute coordinates)
|
||||
// The name box is at (x, y - nameBoxHeight), so center the label there
|
||||
labelEl.attr(
|
||||
'transform',
|
||||
`translate(${x + nameBoxWidth / 2 - bbox.width / 2}, ${y - nameBoxHeight})`
|
||||
);
|
||||
} else {
|
||||
// Draw rect-type boundary (simple dashed rectangle)
|
||||
if (node.look === 'handDrawn') {
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {
|
||||
stroke: 'black',
|
||||
strokeWidth: 2,
|
||||
fill: 'none',
|
||||
strokeLineDash: [5, 5],
|
||||
seed: handDrawnSeed,
|
||||
});
|
||||
|
||||
const roughRect = rc.rectangle(x, y, width, height, options);
|
||||
boundaryRect = shapeSvg.insert(() => roughRect, ':first-child');
|
||||
} else {
|
||||
// Draw dashed rectangle
|
||||
boundaryRect = shapeSvg
|
||||
.insert('rect', ':first-child')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,5');
|
||||
}
|
||||
|
||||
// Position label at top-left (using absolute coordinates, same as rect cluster)
|
||||
labelEl.attr(
|
||||
'transform',
|
||||
`translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
|
||||
);
|
||||
}
|
||||
|
||||
// Get the bounding box of the boundary rectangle
|
||||
const rectBox = boundaryRect.node().getBBox();
|
||||
|
||||
// Set node properties required by layout engine (similar to rect cluster)
|
||||
node.offsetX = 0;
|
||||
node.width = rectBox.width;
|
||||
node.height = rectBox.height;
|
||||
// Used by layout engine to position subgraph in parent
|
||||
node.offsetY = bbox.height - node.padding / 2;
|
||||
|
||||
// Set intersection function for edge routing
|
||||
node.intersect = function (point) {
|
||||
return intersectRect(node, point);
|
||||
};
|
||||
|
||||
// Return cluster object
|
||||
return {
|
||||
cluster: shapeSvg,
|
||||
labelBBox: bbox,
|
||||
};
|
||||
};
|
||||
|
||||
const squareRect = rect;
|
||||
const shapes = {
|
||||
rect,
|
||||
@@ -634,7 +467,6 @@ const shapes = {
|
||||
noteGroup,
|
||||
divider,
|
||||
kanbanSection,
|
||||
usecaseSystemBoundary,
|
||||
};
|
||||
|
||||
let clusterElems = new Map();
|
||||
|
||||
@@ -605,6 +605,14 @@ export const insertEdge = function (
|
||||
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
|
||||
let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
|
||||
|
||||
let animationClass = '';
|
||||
if (edge.animate) {
|
||||
animationClass = 'edge-animation-fast';
|
||||
}
|
||||
if (edge.animation) {
|
||||
animationClass = 'edge-animation-' + edge.animation;
|
||||
}
|
||||
|
||||
let animatedEdge = false;
|
||||
if (edge.look === 'handDrawn') {
|
||||
const rc = rough.svg(elem);
|
||||
@@ -620,7 +628,13 @@ export const insertEdge = function (
|
||||
svgPath = select(svgPathNode)
|
||||
.select('path')
|
||||
.attr('id', edge.id)
|
||||
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
|
||||
.attr(
|
||||
'class',
|
||||
' ' +
|
||||
strokeClasses +
|
||||
(edge.classes ? ' ' + edge.classes : '') +
|
||||
(animationClass ? ' ' + animationClass : '')
|
||||
)
|
||||
.attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
|
||||
let d = svgPath.attr('d');
|
||||
svgPath.attr('d', d);
|
||||
@@ -628,13 +642,6 @@ export const insertEdge = function (
|
||||
} else {
|
||||
const stylesFromClasses = edgeClassStyles.join(';');
|
||||
const styles = edgeStyles ? edgeStyles.reduce((acc, style) => acc + style + ';', '') : '';
|
||||
let animationClass = '';
|
||||
if (edge.animate) {
|
||||
animationClass = ' edge-animation-fast';
|
||||
}
|
||||
if (edge.animation) {
|
||||
animationClass = ' edge-animation-' + edge.animation;
|
||||
}
|
||||
|
||||
const pathStyle =
|
||||
(stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles) +
|
||||
@@ -646,7 +653,10 @@ export const insertEdge = function (
|
||||
.attr('id', edge.id)
|
||||
.attr(
|
||||
'class',
|
||||
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
|
||||
' ' +
|
||||
strokeClasses +
|
||||
(edge.classes ? ' ' + edge.classes : '') +
|
||||
(animationClass ? ' ' + animationClass : '')
|
||||
)
|
||||
.attr('style', pathStyle);
|
||||
|
||||
|
||||
@@ -130,7 +130,6 @@ const lollipop = (elem, type, id) => {
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('circle')
|
||||
.attr('stroke', 'black')
|
||||
.attr('fill', 'transparent')
|
||||
.attr('cx', 7)
|
||||
.attr('cy', 7)
|
||||
@@ -147,7 +146,6 @@ const lollipop = (elem, type, id) => {
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('circle')
|
||||
.attr('stroke', 'black')
|
||||
.attr('fill', 'transparent')
|
||||
.attr('cx', 7)
|
||||
.attr('cy', 7)
|
||||
|
||||
@@ -14,7 +14,6 @@ import { curvedTrapezoid } from './shapes/curvedTrapezoid.js';
|
||||
import { cylinder } from './shapes/cylinder.js';
|
||||
import { dividedRectangle } from './shapes/dividedRect.js';
|
||||
import { doublecircle } from './shapes/doubleCircle.js';
|
||||
import { ellipse } from './shapes/ellipse.js';
|
||||
import { filledCircle } from './shapes/filledCircle.js';
|
||||
import { flippedTriangle } from './shapes/flippedTriangle.js';
|
||||
import { forkJoin } from './shapes/forkJoin.js';
|
||||
@@ -33,8 +32,6 @@ import { lean_right } from './shapes/leanRight.js';
|
||||
import { lightningBolt } from './shapes/lightningBolt.js';
|
||||
import { linedCylinder } from './shapes/linedCylinder.js';
|
||||
import { linedWaveEdgedRect } from './shapes/linedWaveEdgedRect.js';
|
||||
import { usecaseActor } from './shapes/usecaseActor.js';
|
||||
import { usecaseActorIcon } from './shapes/usecaseActorIcon.js';
|
||||
import { multiRect } from './shapes/multiRect.js';
|
||||
import { multiWaveEdgedRectangle } from './shapes/multiWaveEdgedRectangle.js';
|
||||
import { note } from './shapes/note.js';
|
||||
@@ -118,14 +115,6 @@ export const shapesDefs = [
|
||||
aliases: ['terminal', 'pill'],
|
||||
handler: stadium,
|
||||
},
|
||||
{
|
||||
semanticName: 'Ellipse',
|
||||
name: 'Ellipse',
|
||||
shortName: 'ellipse',
|
||||
description: 'Ellipse shape',
|
||||
aliases: ['oval'],
|
||||
handler: ellipse,
|
||||
},
|
||||
{
|
||||
semanticName: 'Subprocess',
|
||||
name: 'Framed Rectangle',
|
||||
@@ -518,10 +507,6 @@ const generateShapeMap = () => {
|
||||
|
||||
// Requirement diagram
|
||||
requirementBox,
|
||||
|
||||
// Usecase diagram
|
||||
usecaseActor,
|
||||
usecaseActorIcon,
|
||||
} as const;
|
||||
|
||||
const entries = [
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import rough from 'roughjs';
|
||||
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||
|
||||
export async function ellipse<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node));
|
||||
|
||||
// Calculate ellipse dimensions with padding
|
||||
const padding = halfPadding ?? 10;
|
||||
const radiusX = bbox.width / 2 + padding * 2;
|
||||
const radiusY = bbox.height / 2 + padding;
|
||||
|
||||
let ellipseElem;
|
||||
const { cssStyles } = node;
|
||||
|
||||
if (node.look === 'handDrawn') {
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
const roughNode = rc.ellipse(0, 0, radiusX * 2, radiusY * 2, options);
|
||||
|
||||
ellipseElem = shapeSvg.insert(() => roughNode, ':first-child');
|
||||
ellipseElem.attr('class', 'basic label-container');
|
||||
|
||||
if (cssStyles) {
|
||||
ellipseElem.attr('style', cssStyles);
|
||||
}
|
||||
} else {
|
||||
ellipseElem = shapeSvg
|
||||
.insert('ellipse', ':first-child')
|
||||
.attr('class', 'basic label-container')
|
||||
.attr('style', nodeStyles)
|
||||
.attr('rx', radiusX)
|
||||
.attr('ry', radiusY)
|
||||
.attr('cx', 0)
|
||||
.attr('cy', 0);
|
||||
}
|
||||
|
||||
node.width = radiusX * 2;
|
||||
node.height = radiusY * 2;
|
||||
|
||||
updateNodeBounds(node, ellipseElem);
|
||||
|
||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||
const rx = bounds.width / 2;
|
||||
const ry = bounds.height / 2;
|
||||
return intersect.ellipse(bounds, rx, ry, point);
|
||||
};
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.ellipse(node, radiusX, radiusY, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import rough from 'roughjs';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
|
||||
/**
|
||||
* Get actor styling based on metadata
|
||||
*/
|
||||
const getActorStyling = (metadata?: Record<string, string>) => {
|
||||
const defaults = {
|
||||
fillColor: 'none',
|
||||
strokeColor: 'black',
|
||||
strokeWidth: 2,
|
||||
type: 'solid',
|
||||
};
|
||||
|
||||
if (!metadata) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
return {
|
||||
fillColor: metadata.type === 'hollow' ? 'none' : metadata.fillColor || defaults.fillColor,
|
||||
strokeColor: metadata.strokeColor || defaults.strokeColor,
|
||||
strokeWidth: parseInt(metadata.strokeWidth || '2', 10),
|
||||
type: metadata.type || defaults.type,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create stick figure path data
|
||||
* This generates the SVG path for a stick figure centered at (x, y)
|
||||
*/
|
||||
const createStickFigurePathD = (x: number, y: number, scale = 1.5): string => {
|
||||
// Base path template (centered at origin):
|
||||
// M 0 -4 C 4.4183 -4 8 -7.5817 8 -12 C 8 -16.4183 4.4183 -20 0 -20 C -4.4183 -20 -8 -16.4183 -8 -12 C -8 -7.5817 -4.4183 -4 0 -4 Z M 0 -4 V 5 M -10 14.5 L 0 5 M 10 14.5 L 0 5 M -11 0 H 11
|
||||
|
||||
// Scale all coordinates
|
||||
const s = (val: number) => val * scale;
|
||||
|
||||
// Translate the path to the desired position
|
||||
return [
|
||||
// Head (circle using cubic bezier curves)
|
||||
`M ${x + s(0)} ${y + s(-4)}`,
|
||||
`C ${x + s(4.4183)} ${y + s(-4)} ${x + s(8)} ${y + s(-7.5817)} ${x + s(8)} ${y + s(-12)}`,
|
||||
`C ${x + s(8)} ${y + s(-16.4183)} ${x + s(4.4183)} ${y + s(-20)} ${x + s(0)} ${y + s(-20)}`,
|
||||
`C ${x + s(-4.4183)} ${y + s(-20)} ${x + s(-8)} ${y + s(-16.4183)} ${x + s(-8)} ${y + s(-12)}`,
|
||||
`C ${x + s(-8)} ${y + s(-7.5817)} ${x + s(-4.4183)} ${y + s(-4)} ${x + s(0)} ${y + s(-4)}`,
|
||||
'Z',
|
||||
// Body (vertical line from head to torso)
|
||||
`M ${x + s(0)} ${y + s(-4)}`,
|
||||
`V ${y + s(5)}`,
|
||||
// Left leg
|
||||
`M ${x + s(-10)} ${y + s(14.5)}`,
|
||||
`L ${x + s(0)} ${y + s(5)}`,
|
||||
// Right leg
|
||||
`M ${x + s(10)} ${y + s(14.5)}`,
|
||||
`L ${x + s(0)} ${y + s(5)}`,
|
||||
// Arms (horizontal line)
|
||||
`M ${x + s(-11)} ${y + s(0)}`,
|
||||
`H ${x + s(11)}`,
|
||||
].join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw traditional stick figure
|
||||
*/
|
||||
const drawStickFigure = (
|
||||
actorGroup: D3Selection<SVGGElement>,
|
||||
styling: ReturnType<typeof getActorStyling>,
|
||||
node: Node
|
||||
): void => {
|
||||
const x = 0; // Center at origin
|
||||
const y = -10; // Adjust vertical position
|
||||
actorGroup.attr('class', 'usecase-actor-shape');
|
||||
|
||||
const pathData = createStickFigurePathD(x, y);
|
||||
|
||||
if (node.look === 'handDrawn') {
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(actorGroup);
|
||||
const options = userNodeOverrides(node, {
|
||||
stroke: styling.strokeColor,
|
||||
strokeWidth: styling.strokeWidth,
|
||||
fill: styling.fillColor,
|
||||
});
|
||||
|
||||
// Draw the stick figure using the path
|
||||
const stickFigure = rc.path(pathData, options);
|
||||
actorGroup.insert(() => stickFigure, ':first-child');
|
||||
} else {
|
||||
// Draw the stick figure using standard SVG path
|
||||
actorGroup
|
||||
.append('path')
|
||||
.attr('d', pathData)
|
||||
.attr('fill', styling.fillColor)
|
||||
.attr('stroke', styling.strokeColor)
|
||||
.attr('stroke-width', styling.strokeWidth);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom shape handler for usecase actors (stick figure)
|
||||
*/
|
||||
export async function usecaseActor<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: Node
|
||||
) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
|
||||
node.labelStyle = labelStyles;
|
||||
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
|
||||
|
||||
// Get actor metadata from node
|
||||
const metadata = (node as Node & { metadata?: Record<string, string> }).metadata;
|
||||
const styling = getActorStyling(metadata);
|
||||
|
||||
// Create actor group
|
||||
const actorGroup = shapeSvg.append('g');
|
||||
|
||||
// Add metadata as data attributes for CSS styling
|
||||
if (metadata) {
|
||||
Object.entries(metadata).forEach(([key, value]) => {
|
||||
actorGroup.attr(`data-${key}`, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Draw stick figure
|
||||
drawStickFigure(actorGroup, styling, node);
|
||||
|
||||
// Get the actual bounding box of the rendered actor
|
||||
const actorBBox = actorGroup.node()?.getBBox();
|
||||
const actorHeight = actorBBox?.height ?? 70;
|
||||
|
||||
// Actor name (always rendered below the figure)
|
||||
const labelY = actorHeight / 2 + 15; // Position label below the figure
|
||||
|
||||
// Calculate label height from the actual text element
|
||||
const labelBBox = label.node()?.getBBox() ?? { height: 20 };
|
||||
const labelHeight = labelBBox.height + 10; // Space for label below
|
||||
const totalHeight = actorHeight + labelHeight;
|
||||
|
||||
actorGroup.attr('transform', `translate(${0}, ${-totalHeight / 2 + 35})`);
|
||||
label.attr(
|
||||
'transform',
|
||||
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${labelY / 2 - 15} )`
|
||||
);
|
||||
|
||||
if (nodeStyles && node.look !== 'handDrawn') {
|
||||
actorGroup.selectChildren('path').attr('style', nodeStyles);
|
||||
}
|
||||
|
||||
// Update node bounds for layout - this will set node.width and node.height from the bounding box
|
||||
updateNodeBounds(node, actorGroup);
|
||||
|
||||
// Override height to include label space
|
||||
// Width is kept from updateNodeBounds as it correctly reflects the actor's visual width
|
||||
node.height = totalHeight;
|
||||
|
||||
// Add intersect function for edge connection points
|
||||
// Use rectangular intersection since the actor has a rectangular bounding box
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import { getIconSVG } from '../../icons.js';
|
||||
import rough from 'roughjs';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
|
||||
/**
|
||||
* Get actor styling based on metadata
|
||||
*/
|
||||
const getActorStyling = (metadata?: Record<string, string>) => {
|
||||
const defaults = {
|
||||
fillColor: 'none',
|
||||
strokeColor: 'black',
|
||||
strokeWidth: 2,
|
||||
type: 'solid',
|
||||
};
|
||||
|
||||
if (!metadata) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
return {
|
||||
fillColor: metadata.type === 'hollow' ? 'none' : metadata.fillColor || defaults.fillColor,
|
||||
strokeColor: metadata.strokeColor || defaults.strokeColor,
|
||||
strokeWidth: parseInt(metadata.strokeWidth || '2', 10),
|
||||
type: metadata.type || defaults.type,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw actor with icon representation
|
||||
*/
|
||||
const drawActorWithIcon = async (
|
||||
actorGroup: D3Selection<SVGGElement>,
|
||||
iconName: string,
|
||||
styling: ReturnType<typeof getActorStyling>,
|
||||
node: Node
|
||||
): Promise<void> => {
|
||||
const x = 0; // Center at origin
|
||||
const y = -10; // Adjust vertical position
|
||||
const iconSize = 50; // Icon size
|
||||
|
||||
if (node.look === 'handDrawn') {
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(actorGroup);
|
||||
const options = userNodeOverrides(node, {
|
||||
stroke: styling.strokeColor,
|
||||
strokeWidth: styling.strokeWidth,
|
||||
fill: styling.fillColor === 'none' ? 'white' : styling.fillColor,
|
||||
});
|
||||
actorGroup.attr('class', 'usecase-icon');
|
||||
// Create a rectangle background for the icon
|
||||
const iconBg = rc.rectangle(x - 35, y - 40, 50, 50, options);
|
||||
actorGroup.insert(() => iconBg, ':first-child');
|
||||
} else {
|
||||
// Create a rectangle background for the icon
|
||||
actorGroup
|
||||
.append('rect')
|
||||
.attr('x', x - 27.5)
|
||||
.attr('y', y - 42)
|
||||
.attr('width', 55)
|
||||
.attr('height', 55)
|
||||
.attr('rx', 5)
|
||||
.attr('fill', styling.fillColor === 'none' ? 'white' : styling.fillColor)
|
||||
.attr('stroke', styling.strokeColor)
|
||||
.attr('stroke-width', styling.strokeWidth);
|
||||
}
|
||||
|
||||
// Add icon using getIconSVG (like iconCircle.ts does)
|
||||
const iconElem = actorGroup.append('g').attr('class', 'actor-icon');
|
||||
iconElem.html(
|
||||
`<g>${await getIconSVG(iconName, {
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
fallbackPrefix: 'fa',
|
||||
})}</g>`
|
||||
);
|
||||
|
||||
// Get icon bounding box for positioning
|
||||
const iconBBox = iconElem.node()?.getBBox();
|
||||
if (iconBBox) {
|
||||
const iconWidth = iconBBox.width;
|
||||
const iconHeight = iconBBox.height;
|
||||
const iconX = iconBBox.x;
|
||||
const iconY = iconBBox.y;
|
||||
|
||||
// Center the icon in the rectangle
|
||||
iconElem.attr(
|
||||
'transform',
|
||||
`translate(${-iconWidth / 2 - iconX}, ${y - 15 - iconHeight / 2 - iconY})`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom shape handler for usecase actors with icons
|
||||
*/
|
||||
export async function usecaseActorIcon<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: Node
|
||||
) {
|
||||
const { labelStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
|
||||
|
||||
// Get actor metadata from node
|
||||
const metadata = (node as Node & { metadata?: Record<string, string> }).metadata;
|
||||
const styling = getActorStyling(metadata);
|
||||
|
||||
// Create actor group
|
||||
const actorGroup = shapeSvg.append('g');
|
||||
|
||||
// Add metadata as data attributes for CSS styling
|
||||
if (metadata) {
|
||||
Object.entries(metadata).forEach(([key, value]) => {
|
||||
actorGroup.attr(`data-${key}`, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Get icon name from metadata
|
||||
const iconName = metadata?.icon ?? 'user';
|
||||
await drawActorWithIcon(actorGroup, iconName, styling, node);
|
||||
|
||||
// Get the actual bounding box of the rendered actor icon
|
||||
const actorBBox = actorGroup.node()?.getBBox();
|
||||
const actorHeight = actorBBox?.height ?? 70;
|
||||
|
||||
// Actor name (always rendered below the figure)
|
||||
const labelY = actorHeight / 2 + 15; // Position label below the figure
|
||||
|
||||
// Calculate label height from the actual text element
|
||||
const labelBBox = label.node()?.getBBox() ?? { height: 20 };
|
||||
const labelHeight = labelBBox.height + 10; // Space for label below
|
||||
const totalHeight = actorHeight + labelHeight;
|
||||
label.attr(
|
||||
'transform',
|
||||
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${labelY / 2 - 15})`
|
||||
);
|
||||
|
||||
// Update node bounds for layout - this will set node.width and node.height from the bounding box
|
||||
updateNodeBounds(node, actorGroup);
|
||||
|
||||
// Override height to include label space
|
||||
// Width is kept from updateNodeBounds as it correctly reflects the actor's visual width
|
||||
node.height = totalHeight;
|
||||
|
||||
// Add intersect function for edge connection points
|
||||
// Use rectangular intersection for icon actors
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
@@ -56,7 +56,6 @@ required:
|
||||
- block
|
||||
- look
|
||||
- radar
|
||||
- usecase
|
||||
properties:
|
||||
theme:
|
||||
description: |
|
||||
@@ -311,8 +310,6 @@ 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.
|
||||
@@ -2332,57 +2329,6 @@ $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: |
|
||||
|
||||
5
packages/mermaid/src/type.d.ts
vendored
5
packages/mermaid/src/type.d.ts
vendored
@@ -1,2 +1,5 @@
|
||||
// eslint-disable-next-line no-var
|
||||
declare var includeLargeFeatures: boolean;
|
||||
declare var injected: {
|
||||
version: string;
|
||||
includeLargeFeatures: boolean;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @mermaid-js/parser
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7051](https://github.com/mermaid-js/mermaid/pull/7051) [`63df702`](https://github.com/mermaid-js/mermaid/commit/63df7021462e8dc1f2aaecb9c5febbbbde4c38e3) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - Add validation for negative values in pie charts:
|
||||
|
||||
Prevents crashes during parsing by validating values post-parsing.
|
||||
|
||||
Provides clearer, user-friendly error messages for invalid negative inputs.
|
||||
|
||||
## 0.6.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"projectName": "Mermaid",
|
||||
"grammars": [
|
||||
{
|
||||
"id": "usecase",
|
||||
"grammar": "src/language/usecase/Usecase.g4",
|
||||
"outputDir": "src/language/usecase/generated",
|
||||
"language": "TypeScript",
|
||||
"generateVisitor": true,
|
||||
"generateListener": true
|
||||
}
|
||||
],
|
||||
"mode": "production"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mermaid-js/parser",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.3",
|
||||
"description": "MermaidJS parser",
|
||||
"author": "Yokozuna59",
|
||||
"contributors": [
|
||||
@@ -19,8 +19,7 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist src/language/generated",
|
||||
"langium:generate": "langium generate",
|
||||
"langium:watch": "langium generate --watch",
|
||||
"antlr:generate": "tsx ../../.build/antlr-cli.ts antlr-config.json"
|
||||
"langium:watch": "langium generate --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -34,11 +33,9 @@
|
||||
"ast"
|
||||
],
|
||||
"dependencies": {
|
||||
"antlr4ng": "^3.0.7",
|
||||
"langium": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"antlr-ng": "^1.0.10",
|
||||
"chevrotain": "^11.0.3"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -45,4 +45,3 @@ export * from './pie/index.js';
|
||||
export * from './architecture/index.js';
|
||||
export * from './radar/index.js';
|
||||
export * from './treemap/index.js';
|
||||
export * from './usecase/index.js';
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
grammar Usecase;
|
||||
|
||||
// Parser rules
|
||||
usecaseDiagram
|
||||
: 'usecase' NEWLINE* statement* EOF
|
||||
;
|
||||
|
||||
statement
|
||||
: actorStatement
|
||||
| relationshipStatement
|
||||
| systemBoundaryStatement
|
||||
| systemBoundaryTypeStatement
|
||||
| directionStatement
|
||||
| classDefStatement
|
||||
| classStatement
|
||||
| styleStatement
|
||||
| usecaseStatement
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
usecaseStatement
|
||||
: entityName 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
|
||||
: usecaseWithClass
|
||||
| IDENTIFIER
|
||||
| STRING
|
||||
;
|
||||
|
||||
usecaseWithClass
|
||||
: IDENTIFIER CLASS_SEPARATOR IDENTIFIER
|
||||
| STRING CLASS_SEPARATOR IDENTIFIER
|
||||
;
|
||||
|
||||
systemBoundaryTypeStatement
|
||||
: systemBoundaryName '@' '{' systemBoundaryTypeContent '}' NEWLINE*
|
||||
;
|
||||
|
||||
systemBoundaryTypeContent
|
||||
: systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)*
|
||||
;
|
||||
|
||||
systemBoundaryTypeProperty
|
||||
: 'type' ':' systemBoundaryType
|
||||
;
|
||||
|
||||
systemBoundaryType
|
||||
: 'package'
|
||||
| 'rect'
|
||||
;
|
||||
|
||||
entityName
|
||||
: IDENTIFIER CLASS_SEPARATOR IDENTIFIER
|
||||
| STRING CLASS_SEPARATOR IDENTIFIER
|
||||
| IDENTIFIER
|
||||
| STRING
|
||||
| nodeIdWithLabel
|
||||
;
|
||||
|
||||
actorDeclaration
|
||||
: 'actor' actorName
|
||||
;
|
||||
|
||||
nodeIdWithLabel
|
||||
: IDENTIFIER '(' nodeLabel ')'
|
||||
;
|
||||
|
||||
nodeLabel
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
| nodeLabel IDENTIFIER
|
||||
| nodeLabel STRING
|
||||
;
|
||||
|
||||
arrow
|
||||
: SOLID_ARROW
|
||||
| BACK_ARROW
|
||||
| LINE_SOLID
|
||||
| CIRCLE_ARROW
|
||||
| CROSS_ARROW
|
||||
| CIRCLE_ARROW_REVERSED
|
||||
| CROSS_ARROW_REVERSED
|
||||
| labeledArrow
|
||||
;
|
||||
|
||||
labeledArrow
|
||||
: LINE_SOLID edgeLabel SOLID_ARROW
|
||||
| BACK_ARROW edgeLabel LINE_SOLID
|
||||
| LINE_SOLID edgeLabel LINE_SOLID
|
||||
| LINE_SOLID edgeLabel CIRCLE_ARROW
|
||||
| LINE_SOLID edgeLabel CROSS_ARROW
|
||||
| CIRCLE_ARROW_REVERSED edgeLabel LINE_SOLID
|
||||
| CROSS_ARROW_REVERSED edgeLabel LINE_SOLID
|
||||
;
|
||||
|
||||
edgeLabel
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
;
|
||||
|
||||
directionStatement
|
||||
: 'direction' direction NEWLINE*
|
||||
;
|
||||
|
||||
direction
|
||||
: 'TB'
|
||||
| 'TD'
|
||||
| 'BT'
|
||||
| 'RL'
|
||||
| 'LR'
|
||||
;
|
||||
|
||||
classDefStatement
|
||||
: 'classDef' IDENTIFIER stylesOpt NEWLINE*
|
||||
;
|
||||
|
||||
stylesOpt
|
||||
: style
|
||||
| stylesOpt COMMA style
|
||||
;
|
||||
|
||||
style
|
||||
: styleComponent
|
||||
| style styleComponent
|
||||
;
|
||||
|
||||
styleComponent
|
||||
: IDENTIFIER
|
||||
| NUMBER
|
||||
| HASH_COLOR
|
||||
| COLON
|
||||
| STRING
|
||||
| DASH
|
||||
| DOT
|
||||
| PERCENT
|
||||
;
|
||||
|
||||
classStatement
|
||||
: 'class' nodeList IDENTIFIER NEWLINE*
|
||||
;
|
||||
|
||||
styleStatement
|
||||
: 'style' IDENTIFIER stylesOpt NEWLINE*
|
||||
;
|
||||
|
||||
nodeList
|
||||
: IDENTIFIER (',' IDENTIFIER)*
|
||||
;
|
||||
|
||||
// Lexer rules
|
||||
SOLID_ARROW
|
||||
: '-->'
|
||||
;
|
||||
|
||||
BACK_ARROW
|
||||
: '<--'
|
||||
;
|
||||
|
||||
CIRCLE_ARROW
|
||||
: '--o'
|
||||
;
|
||||
CIRCLE_ARROW_REVERSED
|
||||
: 'o--'
|
||||
;
|
||||
|
||||
CROSS_ARROW
|
||||
: '--x'
|
||||
;
|
||||
|
||||
CROSS_ARROW_REVERSED
|
||||
: 'x--'
|
||||
;
|
||||
|
||||
LINE_SOLID
|
||||
: '--'
|
||||
;
|
||||
|
||||
COMMA
|
||||
: ','
|
||||
;
|
||||
|
||||
AT
|
||||
: '@'
|
||||
;
|
||||
|
||||
LBRACE
|
||||
: '{'
|
||||
;
|
||||
|
||||
RBRACE
|
||||
: '}'
|
||||
;
|
||||
|
||||
COLON
|
||||
: ':'
|
||||
;
|
||||
|
||||
CLASS_SEPARATOR
|
||||
: ':::'
|
||||
;
|
||||
|
||||
IDENTIFIER
|
||||
: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
;
|
||||
|
||||
STRING
|
||||
: '"' (~["\r\n])* '"'
|
||||
| '\'' (~['\r\n])* '\''
|
||||
;
|
||||
|
||||
HASH_COLOR
|
||||
: '#' [a-fA-F0-9]+
|
||||
;
|
||||
|
||||
NUMBER
|
||||
: [0-9]+ ('.' [0-9]+)? ([a-zA-Z]+)?
|
||||
;
|
||||
|
||||
// These tokens are defined last so they have lowest priority
|
||||
// This ensures arrow tokens like '-->' are matched before DASH
|
||||
DASH
|
||||
: '-'
|
||||
;
|
||||
|
||||
DOT
|
||||
: '.'
|
||||
;
|
||||
|
||||
PERCENT
|
||||
: '%'
|
||||
;
|
||||
|
||||
NEWLINE
|
||||
: [\r\n]+
|
||||
;
|
||||
|
||||
WS
|
||||
: [ \t]+ -> skip
|
||||
;
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './module.js';
|
||||
export * from './types.js';
|
||||
export * from './parser.js';
|
||||
export * from './visitor.js';
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* 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, // --
|
||||
CIRCLE_ARROW: 3, // --o
|
||||
CROSS_ARROW: 4, // --x
|
||||
CIRCLE_ARROW_REVERSED: 5, // o--
|
||||
CROSS_ARROW_REVERSED: 6, // x--
|
||||
} 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;
|
||||
styles?: string[]; // Direct CSS styles applied to this actor
|
||||
}
|
||||
|
||||
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
|
||||
classes?: string[]; // CSS classes applied to this use case
|
||||
styles?: string[]; // Direct CSS styles applied to this use case
|
||||
}
|
||||
|
||||
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')
|
||||
styles?: string[]; // Direct CSS styles applied to this system boundary
|
||||
}
|
||||
|
||||
export interface Relationship {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'association' | 'include' | 'extend';
|
||||
arrowType: ArrowType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface ClassDef {
|
||||
id: string;
|
||||
styles: string[];
|
||||
}
|
||||
|
||||
export interface UsecaseParseResult {
|
||||
actors: Actor[];
|
||||
useCases: UseCase[];
|
||||
systemBoundaries: SystemBoundary[];
|
||||
relationships: Relationship[];
|
||||
classDefs?: Map<string, ClassDef>;
|
||||
direction?: string;
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ANTLR Parser Services interface
|
||||
*/
|
||||
export interface AntlrUsecaseServices {
|
||||
parser: AntlrUsecaseParser;
|
||||
visitor: any; // UsecaseAntlrVisitor - using any to avoid circular dependency
|
||||
}
|
||||
|
||||
/**
|
||||
* ANTLR Parser interface
|
||||
*/
|
||||
export interface AntlrUsecaseParser {
|
||||
parse(input: string): UsecaseParseResult;
|
||||
}
|
||||
@@ -1,894 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
UsecaseWithClassContext,
|
||||
UsecaseStatementContext,
|
||||
ActorNameContext,
|
||||
ActorDeclarationContext,
|
||||
NodeIdWithLabelContext,
|
||||
NodeLabelContext,
|
||||
MetadataContext,
|
||||
MetadataContentContext,
|
||||
MetadataPropertyContext,
|
||||
EntityNameContext,
|
||||
ArrowContext,
|
||||
LabeledArrowContext,
|
||||
EdgeLabelContext,
|
||||
DirectionStatementContext,
|
||||
DirectionContext,
|
||||
ClassDefStatementContext,
|
||||
ClassStatementContext,
|
||||
NodeListContext,
|
||||
} from './generated/UsecaseParser.js';
|
||||
import { ARROW_TYPE } from './types.js';
|
||||
import type {
|
||||
Actor,
|
||||
UseCase,
|
||||
SystemBoundary,
|
||||
Relationship,
|
||||
UsecaseParseResult,
|
||||
ArrowType,
|
||||
ClassDef,
|
||||
} from './types.js';
|
||||
|
||||
export class UsecaseAntlrVisitor extends UsecaseVisitor<void> {
|
||||
private actors: Actor[] = [];
|
||||
private useCases: UseCase[] = [];
|
||||
private systemBoundaries: SystemBoundary[] = [];
|
||||
private relationships: Relationship[] = [];
|
||||
private relationshipCounter = 0;
|
||||
private direction = 'TB'; // Default direction
|
||||
private classDefs = new Map<string, ClassDef>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Assign visitor functions as properties
|
||||
this.visitUsecaseDiagram = this.visitUsecaseDiagramImpl.bind(this);
|
||||
this.visitStatement = this.visitStatementImpl.bind(this);
|
||||
this.visitActorStatement = this.visitActorStatementImpl.bind(this);
|
||||
this.visitRelationshipStatement = this.visitRelationshipStatementImpl.bind(this);
|
||||
this.visitSystemBoundaryStatement = this.visitSystemBoundaryStatementImpl.bind(this);
|
||||
this.visitSystemBoundaryTypeStatement = this.visitSystemBoundaryTypeStatementImpl.bind(this);
|
||||
this.visitDirectionStatement = this.visitDirectionStatementImpl.bind(this);
|
||||
this.visitClassDefStatement = this.visitClassDefStatementImpl.bind(this);
|
||||
this.visitClassStatement = this.visitClassStatementImpl.bind(this);
|
||||
this.visitStyleStatement = this.visitStyleStatementImpl.bind(this);
|
||||
this.visitUsecaseStatement = this.visitUsecaseStatementImpl.bind(this);
|
||||
this.visitActorName = this.visitActorNameImpl.bind(this);
|
||||
this.visitArrow = this.visitArrowImpl.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit the root usecaseDiagram rule
|
||||
* Grammar: usecaseDiagram : 'usecase' statement* EOF ;
|
||||
*/
|
||||
visitUsecaseDiagramImpl(ctx: UsecaseDiagramContext): void {
|
||||
// Reset state
|
||||
this.actors = [];
|
||||
this.useCases = [];
|
||||
this.relationships = [];
|
||||
this.relationshipCounter = 0;
|
||||
this.direction = 'TB'; // Reset direction to default
|
||||
|
||||
// Visit all statement children
|
||||
if (ctx.statement) {
|
||||
const statements = Array.isArray(ctx.statement()) ? ctx.statement() : [ctx.statement()];
|
||||
for (const statementCtx of statements) {
|
||||
if (Array.isArray(statementCtx)) {
|
||||
for (const stmt of statementCtx) {
|
||||
this.visitStatementImpl(stmt);
|
||||
}
|
||||
} else {
|
||||
this.visitStatementImpl(statementCtx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit statement rule
|
||||
* Grammar: statement : actorStatement | relationshipStatement | systemBoundaryStatement | systemBoundaryTypeStatement | directionStatement | classDefStatement | classStatement | usecaseStatement | NEWLINE ;
|
||||
*/
|
||||
private visitStatementImpl(ctx: StatementContext): void {
|
||||
if (ctx.actorStatement?.()) {
|
||||
this.visitActorStatementImpl(ctx.actorStatement()!);
|
||||
} else if (ctx.relationshipStatement?.()) {
|
||||
this.visitRelationshipStatementImpl(ctx.relationshipStatement()!);
|
||||
} else if (ctx.systemBoundaryStatement?.()) {
|
||||
this.visitSystemBoundaryStatementImpl(ctx.systemBoundaryStatement()!);
|
||||
} else if (ctx.systemBoundaryTypeStatement?.()) {
|
||||
this.visitSystemBoundaryTypeStatementImpl(ctx.systemBoundaryTypeStatement()!);
|
||||
} else if (ctx.directionStatement?.()) {
|
||||
this.visitDirectionStatementImpl(ctx.directionStatement()!);
|
||||
} else if (ctx.classDefStatement?.()) {
|
||||
const classDefStmt = ctx.classDefStatement();
|
||||
if (classDefStmt) {
|
||||
this.visitClassDefStatementImpl(classDefStmt);
|
||||
}
|
||||
} else if (ctx.classStatement?.()) {
|
||||
const classStmt = ctx.classStatement();
|
||||
if (classStmt) {
|
||||
this.visitClassStatementImpl(classStmt);
|
||||
}
|
||||
} else if (ctx.styleStatement?.()) {
|
||||
this.visitStyleStatementImpl(ctx.styleStatement());
|
||||
} else if (ctx.usecaseStatement?.()) {
|
||||
const usecaseStmt = ctx.usecaseStatement();
|
||||
if (usecaseStmt) {
|
||||
this.visitUsecaseStatementImpl(usecaseStmt);
|
||||
}
|
||||
}
|
||||
// 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 : usecaseWithClass | IDENTIFIER | STRING ;
|
||||
*/
|
||||
private visitUsecaseInBoundaryImpl(ctx: UsecaseInBoundaryContext): string {
|
||||
// Check for usecaseWithClass (e.g., "debugging:::case1")
|
||||
const usecaseWithClass = ctx.usecaseWithClass?.();
|
||||
if (usecaseWithClass) {
|
||||
return this.visitUsecaseWithClassImpl(usecaseWithClass);
|
||||
}
|
||||
|
||||
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 usecaseWithClass rule
|
||||
* Grammar: usecaseWithClass : IDENTIFIER CLASS_SEPARATOR IDENTIFIER | STRING CLASS_SEPARATOR IDENTIFIER ;
|
||||
*/
|
||||
private visitUsecaseWithClassImpl(ctx: UsecaseWithClassContext): string {
|
||||
let usecaseName = '';
|
||||
let className = '';
|
||||
|
||||
const identifier0 = ctx.IDENTIFIER(0);
|
||||
const identifier1 = ctx.IDENTIFIER(1);
|
||||
const string = ctx.STRING();
|
||||
|
||||
if (identifier0 && identifier1) {
|
||||
// IDENTIFIER:::IDENTIFIER
|
||||
usecaseName = identifier0.getText();
|
||||
className = identifier1.getText();
|
||||
} else if (string && identifier0) {
|
||||
// STRING:::IDENTIFIER
|
||||
const text = string.getText();
|
||||
usecaseName = text.slice(1, -1); // Remove quotes
|
||||
className = identifier0.getText();
|
||||
}
|
||||
|
||||
// Apply class to the use case
|
||||
if (usecaseName && className) {
|
||||
this.applyClassToEntity(usecaseName, className);
|
||||
}
|
||||
|
||||
return usecaseName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 CLASS_SEPARATOR IDENTIFIER | STRING CLASS_SEPARATOR IDENTIFIER | IDENTIFIER | STRING | nodeIdWithLabel ;
|
||||
*/
|
||||
private visitEntityNameImpl(ctx: EntityNameContext): string {
|
||||
const classSeparator = ctx.CLASS_SEPARATOR?.();
|
||||
|
||||
// Check for class application syntax (e.g., "debugging:::case1")
|
||||
if (classSeparator) {
|
||||
let entityName = '';
|
||||
let className = '';
|
||||
|
||||
const identifier0 = ctx.IDENTIFIER(0);
|
||||
const identifier1 = ctx.IDENTIFIER(1);
|
||||
const string0 = ctx.STRING();
|
||||
|
||||
if (identifier0 && identifier1) {
|
||||
// IDENTIFIER:::IDENTIFIER
|
||||
entityName = identifier0.getText();
|
||||
className = identifier1.getText();
|
||||
} else if (string0 && identifier0) {
|
||||
// STRING:::IDENTIFIER
|
||||
const text = string0.getText();
|
||||
entityName = text.slice(1, -1); // Remove quotes
|
||||
className = identifier0.getText();
|
||||
}
|
||||
|
||||
// Apply class to the entity
|
||||
if (entityName && className) {
|
||||
this.applyClassToEntity(entityName, className);
|
||||
}
|
||||
|
||||
return entityName;
|
||||
}
|
||||
|
||||
// Regular entity name without class
|
||||
const identifier = ctx.IDENTIFIER(0);
|
||||
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 '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a class to an entity (use case)
|
||||
*/
|
||||
private applyClassToEntity(entityName: string, className: string): void {
|
||||
// Find or create the use case
|
||||
let useCase = this.useCases.find((uc) => uc.id === entityName);
|
||||
if (!useCase) {
|
||||
useCase = {
|
||||
id: entityName,
|
||||
name: entityName,
|
||||
classes: [],
|
||||
};
|
||||
this.useCases.push(useCase);
|
||||
}
|
||||
|
||||
// Add the class if not already present
|
||||
if (!useCase.classes) {
|
||||
useCase.classes = [];
|
||||
}
|
||||
if (!useCase.classes.includes(className)) {
|
||||
useCase.classes.push(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 | CIRCLE_ARROW | CROSS_ARROW | CIRCLE_ARROW_REVERSED | CROSS_ARROW_REVERSED | 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 };
|
||||
} else if (ctx.CIRCLE_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.CIRCLE_ARROW };
|
||||
} else if (ctx.CROSS_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.CROSS_ARROW };
|
||||
} else if (ctx.CIRCLE_ARROW_REVERSED()) {
|
||||
return { arrowType: ARROW_TYPE.CIRCLE_ARROW_REVERSED };
|
||||
} else if (ctx.CROSS_ARROW_REVERSED()) {
|
||||
return { arrowType: ARROW_TYPE.CROSS_ARROW_REVERSED };
|
||||
}
|
||||
|
||||
// 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 | LINE_SOLID edgeLabel CIRCLE_ARROW | LINE_SOLID edgeLabel CROSS_ARROW | CIRCLE_ARROW_REVERSED edgeLabel LINE_SOLID | CROSS_ARROW_REVERSED 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 if (ctx.CIRCLE_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.CIRCLE_ARROW, label };
|
||||
} else if (ctx.CROSS_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.CROSS_ARROW, label };
|
||||
} else if (ctx.CIRCLE_ARROW_REVERSED()) {
|
||||
return { arrowType: ARROW_TYPE.CIRCLE_ARROW_REVERSED, label };
|
||||
} else if (ctx.CROSS_ARROW_REVERSED()) {
|
||||
return { arrowType: ARROW_TYPE.CROSS_ARROW_REVERSED, label };
|
||||
} else {
|
||||
return { arrowType: ARROW_TYPE.LINE_SOLID, label };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit edge label rule
|
||||
* Grammar: edgeLabel : IDENTIFIER | STRING ;
|
||||
*/
|
||||
private visitEdgeLabelImpl(ctx: EdgeLabelContext): string {
|
||||
const text = ctx.getText();
|
||||
// Remove quotes if it's a string
|
||||
if (
|
||||
(text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))
|
||||
) {
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit directionStatement rule
|
||||
* Grammar: directionStatement : 'direction' direction NEWLINE* ;
|
||||
*/
|
||||
visitDirectionStatementImpl(ctx: DirectionStatementContext): void {
|
||||
const directionCtx = ctx.direction?.();
|
||||
if (directionCtx) {
|
||||
this.direction = this.visitDirectionImpl(directionCtx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit direction rule
|
||||
* Grammar: direction : 'TB' | 'TD' | 'BT' | 'RL' | 'LR' ;
|
||||
*/
|
||||
private visitDirectionImpl(ctx: DirectionContext): string {
|
||||
const text = ctx.getText();
|
||||
// Normalize TD to TB (same as flowchart)
|
||||
if (text === 'TD') {
|
||||
return 'TB';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit classDefStatement rule
|
||||
* Grammar: classDefStatement : 'classDef' IDENTIFIER stylesOpt NEWLINE* ;
|
||||
*/
|
||||
visitClassDefStatementImpl(ctx: ClassDefStatementContext): void {
|
||||
const className = ctx.IDENTIFIER().getText();
|
||||
const stylesOptCtx = ctx.stylesOpt();
|
||||
|
||||
// Get all style properties as an array of strings
|
||||
const styles = this.visitStylesOptImpl(stylesOptCtx);
|
||||
|
||||
this.classDefs.set(className, {
|
||||
id: className,
|
||||
styles,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit stylesOpt rule
|
||||
* Grammar: stylesOpt : style | stylesOpt COMMA style ;
|
||||
* Returns an array of style strings like ['stroke:#f00', 'fill:#ff0']
|
||||
*/
|
||||
private visitStylesOptImpl(ctx: any): string[] {
|
||||
const styles: string[] = [];
|
||||
|
||||
// Check if this is a recursive stylesOpt (stylesOpt COMMA style)
|
||||
const stylesOptCtx = ctx.stylesOpt?.();
|
||||
if (stylesOptCtx) {
|
||||
styles.push(...this.visitStylesOptImpl(stylesOptCtx));
|
||||
}
|
||||
|
||||
// Get the style context
|
||||
const styleCtx = ctx.style();
|
||||
if (styleCtx) {
|
||||
const styleText = this.visitStyleImpl(styleCtx);
|
||||
styles.push(styleText);
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit style rule
|
||||
* Grammar: style : styleComponent | style styleComponent ;
|
||||
* Returns a single style string like 'stroke:#f00'
|
||||
*/
|
||||
private visitStyleImpl(ctx: any): string {
|
||||
// Get all text from the style context
|
||||
return ctx.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit classStatement rule
|
||||
* Grammar: classStatement : 'class' nodeList IDENTIFIER NEWLINE* ;
|
||||
*/
|
||||
visitClassStatementImpl(ctx: ClassStatementContext): void {
|
||||
const nodeIds = this.visitNodeListImpl(ctx.nodeList());
|
||||
const className = ctx.IDENTIFIER().getText();
|
||||
|
||||
// Apply class to each node
|
||||
nodeIds.forEach((nodeId) => {
|
||||
this.applyClassToEntity(nodeId, className);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit styleStatement rule
|
||||
* Grammar: styleStatement : 'style' IDENTIFIER stylesOpt NEWLINE* ;
|
||||
*/
|
||||
visitStyleStatementImpl(ctx: any): void {
|
||||
const nodeId = ctx.IDENTIFIER().getText();
|
||||
const stylesOptCtx = ctx.stylesOpt();
|
||||
|
||||
// Get all style properties as an array of strings
|
||||
const styles = this.visitStylesOptImpl(stylesOptCtx);
|
||||
|
||||
// Apply styles directly to the entity
|
||||
let entity = this.useCases.find((uc) => uc.id === nodeId);
|
||||
if (!entity) {
|
||||
entity = this.actors.find((a) => a.id === nodeId);
|
||||
}
|
||||
if (!entity) {
|
||||
entity = this.systemBoundaries.find((sb) => sb.id === nodeId);
|
||||
}
|
||||
|
||||
if (entity) {
|
||||
// Initialize styles array if it doesn't exist
|
||||
if (!entity.styles) {
|
||||
entity.styles = [];
|
||||
}
|
||||
// Add the new styles
|
||||
entity.styles.push(...styles);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit nodeList rule
|
||||
* Grammar: nodeList : IDENTIFIER (',' IDENTIFIER)* ;
|
||||
*/
|
||||
private visitNodeListImpl(ctx: NodeListContext): string[] {
|
||||
const identifiers = ctx.IDENTIFIER();
|
||||
return identifiers.map((id) => id.getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit usecaseStatement rule
|
||||
* Grammar: usecaseStatement : entityName NEWLINE* ;
|
||||
*/
|
||||
visitUsecaseStatementImpl(ctx: UsecaseStatementContext): void {
|
||||
const entityName = this.visitEntityNameImpl(ctx.entityName());
|
||||
|
||||
// Create a standalone use case if it doesn't already exist
|
||||
if (!this.useCases.some((uc) => uc.id === entityName)) {
|
||||
this.useCases.push({
|
||||
id: entityName,
|
||||
name: entityName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parse result after visiting the diagram
|
||||
*/
|
||||
getParseResult(): UsecaseParseResult {
|
||||
return {
|
||||
actors: this.actors,
|
||||
useCases: this.useCases,
|
||||
systemBoundaries: this.systemBoundaries,
|
||||
relationships: this.relationships,
|
||||
classDefs: this.classDefs,
|
||||
direction: this.direction,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
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 | UsecaseParseResult;
|
||||
export type LangiumDiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
|
||||
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
|
||||
|
||||
const parsers: Record<string, LangiumParser> = {};
|
||||
const initializers = {
|
||||
@@ -43,9 +41,6 @@ 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>;
|
||||
@@ -55,12 +50,7 @@ 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
|
||||
@@ -69,19 +59,11 @@ 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 extends LangiumDiagramAST ? T : never> =
|
||||
parser.parse<T extends LangiumDiagramAST ? T : never>(text);
|
||||
const result: ParseResult<T> = parser.parse<T>(text);
|
||||
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
|
||||
throw new MermaidParseError(result);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user