mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-11 06:54:07 +01:00
Compare commits
58 Commits
deprecate-
...
feat/useca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfed700a58 | ||
|
|
b715d82458 | ||
|
|
5b2b3b8ae9 | ||
|
|
b7ff1920a9 | ||
|
|
58c06ed770 | ||
|
|
5e05f91b7d | ||
|
|
5fd06ccbac | ||
|
|
c728d864c8 | ||
|
|
99f17bea3a | ||
|
|
c1c14e401a | ||
|
|
8b3057f27c | ||
|
|
717d3b3bb2 | ||
|
|
2f8d9ba958 | ||
|
|
ace0367afd | ||
|
|
b983626587 | ||
|
|
7effdc147b | ||
|
|
6e67515f41 | ||
|
|
09b74f1c29 | ||
|
|
880da21908 | ||
|
|
38191243be | ||
|
|
b75dcb8a82 | ||
|
|
4c1e170f4a | ||
|
|
bd25b88a01 | ||
|
|
d3de3ecbbb | ||
|
|
3964ce0a0f | ||
|
|
4dbabba8e8 | ||
|
|
e3ef5e4208 | ||
|
|
daeb85bac2 | ||
|
|
2cdaf03ada | ||
|
|
f6fa0260e7 | ||
|
|
29aad6d23c | ||
|
|
82ef7b5fdb | ||
|
|
11cd3f1262 | ||
|
|
ac4aa94e78 | ||
|
|
c40faac80d | ||
|
|
c530baed3f | ||
|
|
045699de10 | ||
|
|
1988d24227 | ||
|
|
39f90debe7 | ||
|
|
73e9849f99 | ||
|
|
5a05540a5f | ||
|
|
2b58df9665 | ||
|
|
0b42bdba07 | ||
|
|
74c96db3e2 | ||
|
|
bd47c57eaf | ||
|
|
3e5d2db514 | ||
|
|
40990bb096 | ||
|
|
7ca0665764 | ||
|
|
81a6a361ab | ||
|
|
62faacdeeb | ||
|
|
0e40d8e8a8 | ||
|
|
e8d6daf4f6 | ||
|
|
cb4ed605b2 | ||
|
|
ba9db26bfa | ||
|
|
252b1837f7 | ||
|
|
6b9c15d7f0 | ||
|
|
fda640c90c | ||
|
|
584a789183 |
92
.build/antlr-cli.ts
Normal file
92
.build/antlr-cli.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { readFile, mkdir, access } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
interface AntlrGrammarConfig {
|
||||
id: string;
|
||||
grammar: string;
|
||||
outputDir: string;
|
||||
language: string;
|
||||
generateVisitor?: boolean;
|
||||
generateListener?: boolean;
|
||||
}
|
||||
|
||||
interface AntlrConfig {
|
||||
projectName: string;
|
||||
grammars: AntlrGrammarConfig[];
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export async function generateFromConfig(configFile: string): Promise<void> {
|
||||
const configPath = resolve(configFile);
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
throw new Error(`ANTLR config file not found: ${configPath}`);
|
||||
}
|
||||
|
||||
const configContent = await readFile(configPath, 'utf-8');
|
||||
const config: AntlrConfig = JSON.parse(configContent);
|
||||
|
||||
const configDir = dirname(configPath);
|
||||
|
||||
for (const grammarConfig of config.grammars) {
|
||||
await generateGrammar(grammarConfig, configDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateGrammar(grammarConfig: AntlrGrammarConfig, baseDir: string): Promise<void> {
|
||||
const grammarFile = resolve(baseDir, grammarConfig.grammar);
|
||||
const outputDir = resolve(baseDir, grammarConfig.outputDir);
|
||||
|
||||
// Check if grammar file exists
|
||||
try {
|
||||
await access(grammarFile);
|
||||
} catch {
|
||||
throw new Error(`Grammar file not found: ${grammarFile}`);
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
|
||||
// Build ANTLR command arguments
|
||||
|
||||
// eslint-disable-next-line @cspell/spellchecker
|
||||
const args = ['antlr-ng', `-Dlanguage=${grammarConfig.language}`];
|
||||
|
||||
if (grammarConfig.generateVisitor) {
|
||||
args.push('--generate-visitor');
|
||||
}
|
||||
|
||||
if (grammarConfig.generateListener) {
|
||||
args.push('--generate-listener');
|
||||
}
|
||||
|
||||
args.push('-o', `"${outputDir}"`, `"${grammarFile}"`);
|
||||
|
||||
const command = `npx ${args.join(' ')}`;
|
||||
|
||||
try {
|
||||
await execAsync(command);
|
||||
console.log(`Generated ANTLR files for ${grammarConfig.id}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to generate ANTLR files for ${grammarConfig.id}:`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const configFile = process.argv[2] || './packages/parser/antlr-config.json';
|
||||
try {
|
||||
await generateFromConfig(configFile);
|
||||
console.log('ANTLR generation completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('ANTLR generation failed:', error.message);
|
||||
}
|
||||
}
|
||||
5
.build/generateAntlr.ts
Normal file
5
.build/generateAntlr.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { generateFromConfig } from './antlr-cli.js';
|
||||
|
||||
export async function generateAntlr() {
|
||||
await generateFromConfig('./packages/parser/antlr-config.json');
|
||||
}
|
||||
@@ -28,6 +28,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
||||
'packet',
|
||||
'architecture',
|
||||
'radar',
|
||||
'usecase',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
5
.changeset/loud-results-melt.md
Normal file
5
.changeset/loud-results-melt.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add half-arrowheads (solid & stick) and central connection support
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Deprecate flowchart.htmlLabels in favor of root-level htmlLabels
|
||||
5
.changeset/slow-lemons-know.md
Normal file
5
.changeset/slow-lemons-know.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@mermaid': patch
|
||||
---
|
||||
|
||||
fix: Mindmap breaking in ELK layout
|
||||
5
.changeset/sweet-games-build.md
Normal file
5
.changeset/sweet-games-build.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix(er-diagram): prevent syntax error when using 'u', numbers, and decimals in node names
|
||||
@@ -143,6 +143,9 @@ typeof
|
||||
typestr
|
||||
unshift
|
||||
urlsafe
|
||||
usecase
|
||||
Usecase
|
||||
USECASE
|
||||
verifymethod
|
||||
VERIFYMTHD
|
||||
WARN_DOCSDIR_DOESNT_MATCH
|
||||
|
||||
@@ -4,6 +4,7 @@ import { packageOptions } from '../.build/common.js';
|
||||
import { generateLangium } from '../.build/generateLangium.js';
|
||||
import type { MermaidBuildOptions } from './util.js';
|
||||
import { defaultOptions, getBuildConfig } from './util.js';
|
||||
import { generateAntlr } from '../.build/generateAntlr.js';
|
||||
|
||||
const shouldVisualize = process.argv.includes('--visualize');
|
||||
|
||||
@@ -95,6 +96,7 @@ const buildTinyMermaid = async () => {
|
||||
|
||||
const main = async () => {
|
||||
await generateLangium();
|
||||
await generateAntlr();
|
||||
await mkdir('stats', { recursive: true });
|
||||
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
|
||||
// it should build `parser` before `mermaid` because it's a dependency
|
||||
|
||||
@@ -4,6 +4,7 @@ import cors from 'cors';
|
||||
import { context } from 'esbuild';
|
||||
import type { Request, Response } from 'express';
|
||||
import express from 'express';
|
||||
import { execSync } from 'child_process';
|
||||
import { packageOptions } from '../.build/common.js';
|
||||
import { generateLangium } from '../.build/generateLangium.js';
|
||||
import { defaultOptions, getBuildConfig } from './util.js';
|
||||
@@ -64,6 +65,28 @@ function eventsHandler(request: Request, response: Response) {
|
||||
}
|
||||
|
||||
let timeoutID: NodeJS.Timeout | undefined = undefined;
|
||||
let isGeneratingAntlr = false;
|
||||
|
||||
/**
|
||||
* Generate ANTLR parser files from grammar files
|
||||
*/
|
||||
function generateAntlr(): void {
|
||||
if (isGeneratingAntlr) {
|
||||
console.log('⏳ ANTLR generation already in progress, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isGeneratingAntlr = true;
|
||||
console.log('🎯 ANTLR: Generating parser files...');
|
||||
execSync('tsx scripts/antlr-generate.mts', { stdio: 'inherit' });
|
||||
console.log('✅ ANTLR: Parser files generated successfully\n');
|
||||
} catch (error) {
|
||||
console.error('❌ ANTLR: Failed to generate parser files:', error);
|
||||
} finally {
|
||||
isGeneratingAntlr = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce file change events to avoid rebuilding multiple times.
|
||||
@@ -89,7 +112,7 @@ async function createServer() {
|
||||
handleFileChange();
|
||||
const app = express();
|
||||
chokidar
|
||||
.watch('**/src/**/*.{js,ts,langium,yaml,json}', {
|
||||
.watch('**/src/**/*.{js,ts,g4,langium,yaml,json}', {
|
||||
ignoreInitial: true,
|
||||
ignored: [/node_modules/, /dist/, /docs/, /coverage/],
|
||||
})
|
||||
@@ -103,6 +126,9 @@ async function createServer() {
|
||||
if (path.endsWith('.langium')) {
|
||||
await generateLangium();
|
||||
}
|
||||
if (path.endsWith('.g4')) {
|
||||
generateAntlr();
|
||||
}
|
||||
handleFileChange();
|
||||
});
|
||||
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -26,8 +26,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript']
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
language: ['javascript', 'actions']
|
||||
# CodeQL supports [ 'actions', 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -51,5 +51,8 @@ demos/dev/**
|
||||
tsx-0/**
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# autogenereated by langium-cli
|
||||
# autogenereated by langium-cli and antlr-cli
|
||||
generated/
|
||||
|
||||
# autogenereated by antlr-cli
|
||||
.antlr/
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { TemplateType } from 'rollup-plugin-visualizer/dist/plugin/template
|
||||
import istanbul from 'vite-plugin-istanbul';
|
||||
import { packageOptions } from '../.build/common.js';
|
||||
import { generateLangium } from '../.build/generateLangium.js';
|
||||
import { generateAntlr } from '../.build/generateAntlr.js';
|
||||
|
||||
const visualize = process.argv.includes('--visualize');
|
||||
const watch = process.argv.includes('--watch');
|
||||
@@ -123,6 +124,7 @@ const main = async () => {
|
||||
};
|
||||
|
||||
await generateLangium();
|
||||
await generateAntlr();
|
||||
|
||||
if (watch) {
|
||||
await build(getBuildConfig({ minify: false, watch, core: false, entryName: 'parser' }));
|
||||
|
||||
@@ -369,4 +369,92 @@ ORDER ||--|{ LINE-ITEM : contains
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Special characters and numbers syntax', () => {
|
||||
it('should render ER diagram with numeric entity names', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
1 ||--|| ORDER : places
|
||||
ORDER ||--|{ 2 : contains
|
||||
2 ||--o{ 3.5 : references
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render ER diagram with "u" character in entity names and cardinality', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--|| u : has
|
||||
u ||--|| ORDER : places
|
||||
PROJECT u--o{ TEAM_MEMBER : "parent"
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render ER diagram with decimal numbers in relationships', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
2.5 ||--|| 1.5 : has
|
||||
CUSTOMER ||--o{ 3.14 : references
|
||||
1.0 ||--|{ ORDER : contains
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render ER diagram with numeric entity names and attributes', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
1 {
|
||||
string name
|
||||
int value
|
||||
}
|
||||
1 ||--|| ORDER : places
|
||||
ORDER {
|
||||
float price
|
||||
string description
|
||||
}
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render complex ER diagram with mixed special entity names', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ 1 : places
|
||||
1 ||--|{ u : contains
|
||||
1.5
|
||||
u ||--|| 2.5 : processes
|
||||
2.5 {
|
||||
string id
|
||||
float value
|
||||
}
|
||||
u {
|
||||
varchar(50) name
|
||||
int count
|
||||
}
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
it('should render ER diagram with numeric entity names and attributes', () => {
|
||||
imgSnapshotTest(
|
||||
`erDiagram
|
||||
PRODUCT ||--o{ ORDER-ITEM : has
|
||||
1.5
|
||||
u
|
||||
1
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -655,5 +655,126 @@ describe('Sequence Diagram Special Cases', () => {
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Central Connection Rendering Tests', () => {
|
||||
it('should render central connection circles on actor vertical lines', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
participant Charlie
|
||||
Alice ()->>() Bob: Central connection
|
||||
Bob ()-->> Charlie: Reverse central connection
|
||||
Charlie ()<<-->>() Alice: Dual central connection`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render central connections with different arrow types', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()->>() Bob: Solid open arrow
|
||||
Alice ()-->>() Bob: Dotted open arrow
|
||||
Alice ()-x() Bob: Solid cross
|
||||
Alice ()--x() Bob: Dotted cross
|
||||
Alice ()->() Bob: Solid arrow`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render central connections with bidirectional arrows', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()<<->>() Bob: Bidirectional solid
|
||||
Alice ()<<-->>() Bob: Bidirectional dotted`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render central connections with activations', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
participant Charlie
|
||||
Alice ()->>() Bob: Activate Bob
|
||||
activate Bob
|
||||
Bob ()-->> Charlie: Message to Charlie
|
||||
Bob ()->>() Alice: Response to Alice
|
||||
deactivate Bob`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render central connections mixed with normal messages', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
participant Charlie
|
||||
Alice ->> Bob: Normal message
|
||||
Bob ()->>() Charlie: Central connection
|
||||
Charlie -->> Alice: Normal dotted message
|
||||
Alice ()<<-->>() Bob: Dual central connection
|
||||
Bob -x Charlie: Normal cross message`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render central connections with notes', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
participant Charlie
|
||||
Alice ()->>() Bob: Central connection
|
||||
Note over Alice,Bob: Central connection note
|
||||
Bob ()-->> Charlie: Reverse central connection
|
||||
Note right of Charlie: Response note
|
||||
Charlie ()<<-->>() Alice: Dual central connection`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render central connections with loops and alternatives', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
participant Charlie
|
||||
loop Every minute
|
||||
Alice ()->>() Bob: Central heartbeat
|
||||
Bob ()-->> Charlie: Forward heartbeat
|
||||
end
|
||||
alt Success
|
||||
Charlie ()<<-->>() Alice: Success response
|
||||
else Failure
|
||||
Charlie ()-x() Alice: Failure response
|
||||
end`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render central connections with different participant types', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Alice
|
||||
actor Bob
|
||||
participant Charlie@{"type":"boundary"}
|
||||
participant David@{"type":"control"}
|
||||
participant Eve@{"type":"entity"}
|
||||
Alice ()->>() Bob: To actor
|
||||
Bob ()-->> Charlie: To boundary
|
||||
Charlie ()->>() David: To control
|
||||
David ()<<-->>() Eve: To entity
|
||||
Eve ()-x() Alice: Back to participant`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1053,4 +1053,167 @@ describe('Sequence diagram', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('render new arrow type', () => {
|
||||
it('should render Solid half arrow top', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice -|\\ John: Hello John, how are you?
|
||||
Alice-|\\ John: Hi Alice, I can hear you!
|
||||
Alice -|\\ John: Test
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should render Solid half arrow bottom', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice-|/John: Hello John, how are you?
|
||||
Alice-|/John: Hi Alice, I can hear you!
|
||||
Alice-|/John: Test
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Stick half arrow top ', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice-\\\\John: Hello John, how are you?
|
||||
Alice-\\\\John: Hi Alice, I can hear you!
|
||||
Alice-\\\\John: Test
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should render Stick half arrow bottom ', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice-//John: Hello John, how are you?
|
||||
Alice-//John: Hi Alice, I can hear you!
|
||||
Alice-//John: Test
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should render Solid half arrow top reverse ', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice/|-John: Hello Alice, how are you?
|
||||
Alice/|-John: Hi Alice, I can hear you!
|
||||
Alice/|-John: Test
|
||||
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Solid half arrow bottom reverse ', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
Alice \\|- John: Hello Alice, how are you?
|
||||
Alice \\|- John: Hi Alice, I can hear you!
|
||||
Alice \\|- John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Stick half arrow top reverse ', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice //-John: Hello Alice, how are you?
|
||||
Alice //-John: Hi Alice, I can hear you!
|
||||
Alice //-John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Stick half arrow bottom reverse ', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice \\\\-John: Hello Alice, how are you?
|
||||
Alice \\\\-John: Hi Alice, I can hear you!
|
||||
Alice \\\\-John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Solid half arrow top dotted', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice --|\\John: Hello John, how are you?
|
||||
Alice --|\\John: Hi Alice, I can hear you!
|
||||
Alice --|\\John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Solid half arrow bottom dotted', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice --|/John: Hello John, how are you?
|
||||
Alice --|/John: Hi Alice, I can hear you!
|
||||
Alice --|/John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Stick half arrow top dotted', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice--\\\\John: Hello John, how are you?
|
||||
Alice--\\\\John: Hi Alice, I can hear you!
|
||||
Alice--\\\\John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Stick half arrow bottom dotted', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice--//John: Hello John, how are you?
|
||||
Alice--//John: Hi Alice, I can hear you!
|
||||
Alice--//John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Solid half arrow top reverse dotted', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice/|--John: Hello Alice, how are you?
|
||||
Alice/|--John: Hi Alice, I can hear you!
|
||||
Alice/|--John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Solid half arrow bottom reverse dotted', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice\\|--John: Hello Alice, how are you?
|
||||
Alice\\|--John: Hi Alice, I can hear you!
|
||||
Alice\\|--John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Stick half arrow top reverse dotted ', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice//--John: Hello Alice, how are you?
|
||||
Alice//--John: Hi Alice, I can hear you!
|
||||
Alice//--John: Test`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Stick half arrow bottom reverse dotted ', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
Alice\\\\--John: Hello Alice, how are you?
|
||||
Alice\\\\--John: Hi Alice, I can hear you!
|
||||
Alice\\\\--John: Test`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
425
cypress/integration/rendering/usecase.spec.ts
Normal file
425
cypress/integration/rendering/usecase.spec.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
|
||||
|
||||
describe('Usecase diagram', () => {
|
||||
it('should render a simple usecase diagram with actors and use cases', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin --> "Manage Users"
|
||||
User --> "View Profile"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with quoted actor names', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Customer Service"
|
||||
actor "System Administrator"
|
||||
"Customer Service" --> "Handle Tickets"
|
||||
"System Administrator" --> "Manage System"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with different arrow types', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin <-- Logout
|
||||
User -- "View Data"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with edge labels', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Developer
|
||||
actor Manager
|
||||
Developer --important--> "Write Code"
|
||||
Manager --review--> "Code Review"
|
||||
Developer --urgent--> Manager
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with node ID syntax', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
User --> a(Login)
|
||||
User --> b("View Profile")
|
||||
User --> c("Update Settings")
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with comma-separated actors', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Customer Service", "Technical Support", "Sales Team"
|
||||
actor SystemAdmin
|
||||
"Customer Service" --> "Handle Tickets"
|
||||
"Technical Support" --> "Resolve Issues"
|
||||
"Sales Team" --> "Process Orders"
|
||||
SystemAdmin --> "Manage System"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with actor metadata', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User@{ "type" : "primary", "icon" : "user" }
|
||||
actor Admin@{ "type" : "secondary", "icon" : "admin" }
|
||||
actor System@{ "type" : "hollow", "icon" : "system" }
|
||||
User --> Login
|
||||
Admin --> "Manage Users"
|
||||
System --> "Process Data"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with system boundaries (rect type)', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Admin, User
|
||||
systemBoundary "Authentication"
|
||||
Login
|
||||
Logout
|
||||
"Reset Password"
|
||||
end
|
||||
"Authentication"@{ type: rect }
|
||||
Admin --> Login
|
||||
User --> Login
|
||||
User --> "Reset Password"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with system boundaries (package type)', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Admin, User
|
||||
systemBoundary "Authentication"
|
||||
Login
|
||||
Logout
|
||||
"Reset Password"
|
||||
end
|
||||
"Authentication"@{ type: package }
|
||||
Admin --> Login
|
||||
User --> Login
|
||||
User --> "Reset Password"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render complex usecase diagram with all features', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Customer Service"@{ "type" : "primary", "icon" : "user" }
|
||||
actor "System Admin"@{ "type" : "secondary", "icon" : "admin" }
|
||||
actor "Database"@{ "type" : "hollow", "icon" : "database" }
|
||||
|
||||
systemBoundary "Customer Support System"
|
||||
"Handle Tickets"
|
||||
"View Customer Info"
|
||||
end
|
||||
"Customer Support System"@{ type: package }
|
||||
|
||||
systemBoundary "Administration"
|
||||
"User Management"
|
||||
"System Config"
|
||||
end
|
||||
|
||||
"Customer Service" --priority--> "Handle Tickets"
|
||||
"Customer Service" --> "View Customer Info"
|
||||
"System Admin" --manage--> "User Management"
|
||||
"System Admin" --> "System Config"
|
||||
"Database" <-- "Handle Tickets"
|
||||
"Database" <-- "View Customer Info"
|
||||
"Database" <-- "User Management"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with actor-to-actor relationships', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Manager
|
||||
actor Developer
|
||||
actor Tester
|
||||
|
||||
Manager --supervises--> Developer
|
||||
Manager --coordinates--> Tester
|
||||
Developer --collaborates--> Tester
|
||||
|
||||
Developer --> "Write Code"
|
||||
Tester --> "Test Code"
|
||||
Manager --> "Review Progress"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with mixed relationship types', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
|
||||
User --> "Basic Login"
|
||||
Admin --> "Advanced Login"
|
||||
User --includes--> "View Profile"
|
||||
Admin --extends--> "Manage Profiles"
|
||||
|
||||
"Basic Login" <-- "Advanced Login"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with long labels and text wrapping', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Customer Service Representative"
|
||||
actor "System Administrator with Extended Privileges"
|
||||
|
||||
"Customer Service Representative" --Process--> "Handle Complex Customer Support Tickets"
|
||||
"System Administrator with Extended Privileges" --> "Manage System Configuration and User Permissions"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with special characters in names', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "User@Company.com"
|
||||
actor "Admin (Level-1)"
|
||||
"User@Company.com" --> a("Login & Authenticate")
|
||||
"Admin (Level-1)" --> b("Manage Users & Permissions")
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram when useMaxWidth is true (default)', () => {
|
||||
renderGraph(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin --> "Manage System"
|
||||
User --> "View Profile"
|
||||
`,
|
||||
{ usecase: { useMaxWidth: true } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
expect(svg).to.have.attr('width', '100%');
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render usecase diagram when useMaxWidth is false', () => {
|
||||
renderGraph(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin --> "Manage System"
|
||||
`,
|
||||
{ usecase: { useMaxWidth: false } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
expect(width).to.be.greaterThan(200);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render empty usecase diagram', () => {
|
||||
imgSnapshotTest(`usecase`);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with only actors', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
actor Guest
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with implicit use case creation', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login
|
||||
User --> Register
|
||||
User --> "Forgot Password"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with nested system boundaries', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
|
||||
systemBoundary "Main System"
|
||||
Login
|
||||
Logout
|
||||
"Create User"
|
||||
"Delete User"
|
||||
end
|
||||
|
||||
User --> Login
|
||||
User --> Logout
|
||||
Admin --> "Create User"
|
||||
Admin --> "Delete User"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with multiple edge labels on same relationship', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor Developer
|
||||
actor Manager
|
||||
|
||||
Developer --"code review"--> Manager
|
||||
Developer --"status update"--> Manager
|
||||
Manager --"feedback"--> Developer
|
||||
Manager --"approval"--> Developer
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with various actor icon types', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User@{ "icon": "user" }
|
||||
actor Admin@{ "icon": "admin" }
|
||||
actor Database@{ "icon": "database" }
|
||||
actor API@{ "icon": "api" }
|
||||
actor Mobile@{ "icon": "mobile" }
|
||||
actor Web@{ "icon": "web" }
|
||||
|
||||
User --> "Access System"
|
||||
Admin --> "Manage System"
|
||||
Database --> "Store Data"
|
||||
API --> "Provide Services"
|
||||
Mobile --> "Mobile Access"
|
||||
Web --> "Web Access"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with mixed arrow directions and labels', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor System
|
||||
actor Admin
|
||||
|
||||
User --request--> System
|
||||
System --response--> User
|
||||
System <--monitor-- Admin
|
||||
Admin --configure--> System
|
||||
User -- "direct access" -- Admin
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with boundary-less use cases', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
|
||||
systemBoundary "Secure Area"
|
||||
"Admin Panel"
|
||||
"User Management"
|
||||
end
|
||||
|
||||
User --> "Public Login"
|
||||
User --> "Guest Access"
|
||||
Admin --> "Public Login"
|
||||
Admin --> "Admin Panel"
|
||||
Admin --> "User Management"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with complex metadata combinations', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "Primary User"@{ "type": "primary", "icon": "user", "fillColor": "lightblue" }
|
||||
actor "Secondary User"@{ "type": "secondary", "icon": "client", "strokeColor": "red" }
|
||||
actor "System Service"@{ "type": "hollow", "icon": "service", "strokeWidth": "3" }
|
||||
|
||||
"Primary User" --"high priority"--> a("Critical Process")
|
||||
"Secondary User" --"low priority"--> b("Background Task")
|
||||
"System Service" --"automated"--> c("System Maintenance")
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render usecase diagram with Unicode characters', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor "用户"@{ "icon": "user" }
|
||||
actor "管理员"@{ "icon": "admin" }
|
||||
|
||||
"用户" --"登录"--> "系统访问"
|
||||
"管理员" --"管理"--> "用户管理"
|
||||
"用户" --> "数据查看"
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render large usecase diagram with many elements', () => {
|
||||
imgSnapshotTest(
|
||||
`usecase
|
||||
actor User1, User2, User3, User4
|
||||
actor Admin1, Admin2
|
||||
actor System1@{ "icon": "system" }
|
||||
actor System2@{ "icon": "database" }
|
||||
|
||||
systemBoundary "Module A"
|
||||
"Feature A1"
|
||||
"Feature A2"
|
||||
"Admin A1"
|
||||
end
|
||||
"Module A"@{ type: package }
|
||||
|
||||
systemBoundary "Module B"
|
||||
"Feature B1"
|
||||
"Feature B2"
|
||||
"Admin B1"
|
||||
end
|
||||
|
||||
User1 --> "Feature A1"
|
||||
User2 --> "Feature A2"
|
||||
Admin1 --> "Admin A1"
|
||||
User3 --> "Feature B1"
|
||||
User4 --> "Feature B2"
|
||||
Admin2 --> "Admin B1"
|
||||
|
||||
System1 <-- "Feature A1"
|
||||
System1 <-- "Feature B1"
|
||||
System2 <-- "Admin A1"
|
||||
System2 <-- "Admin B1"
|
||||
|
||||
User1 --"collaborates"--> User2
|
||||
Admin1 --"supervises"--> Admin2
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -110,6 +110,48 @@
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
id)I am a cloud(
|
||||
id))I am a bang((
|
||||
Tools
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart
|
||||
aid0
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
aid0
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: ogdc
|
||||
---
|
||||
flowchart-elk TB
|
||||
c1-->a2
|
||||
subgraph one
|
||||
|
||||
234
demos/usecase.html
Normal file
234
demos/usecase.html
Normal file
@@ -0,0 +1,234 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Recursive:wght@300..1000&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<style>
|
||||
.recursive-500 {
|
||||
font-family: 'Recursive', serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
'slnt' 0,
|
||||
'CASL' 0,
|
||||
'CRSV' 0.5,
|
||||
'MONO' 0;
|
||||
}
|
||||
body {
|
||||
/* background: rgb(221, 208, 208); */
|
||||
/* background: #333; */
|
||||
/* font-family: 'Arial'; */
|
||||
font-family: 'Recursive', serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
'slnt' 0,
|
||||
'CASL' 0,
|
||||
'CRSV' 0.5,
|
||||
'MONO' 0;
|
||||
/* color: white; */
|
||||
/* font-size: 18px !important; */
|
||||
}
|
||||
.gridify.tiny {
|
||||
background-image:
|
||||
linear-gradient(transparent 11px, rgba(220, 220, 200, 0.8) 12px, transparent 12px),
|
||||
linear-gradient(90deg, transparent 11px, rgba(220, 220, 200, 0.8) 12px, transparent 12px);
|
||||
background-size:
|
||||
100% 12px,
|
||||
12px 100%;
|
||||
}
|
||||
|
||||
.gridify.dots {
|
||||
background-image: radial-gradient(
|
||||
circle at center,
|
||||
rgba(220, 220, 200, 0.8) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mermaid svg {
|
||||
font-size: 16px !important;
|
||||
font-family: 'Recursive', serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
'slnt' 0,
|
||||
'CASL' 0,
|
||||
'CRSV' 0.5,
|
||||
'MONO' 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
/*box-shadow: 4px 4px 0px 0px #0000000F;*/
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="gridify dots">
|
||||
<p class="mb-20">Test Diagram</p>
|
||||
<div class="w-full h-64">
|
||||
<pre id="diagram4" class="mermaid">
|
||||
usecase
|
||||
direction LR
|
||||
actor User1, User2, User3, User4
|
||||
actor Admin1, Admin2
|
||||
actor System1@{ "icon": "bell" }
|
||||
actor System2@{ "icon": "database" }
|
||||
|
||||
systemBoundary "Module A"
|
||||
"Feature A1"
|
||||
"Feature A2"
|
||||
"Admin A1"
|
||||
|
||||
end
|
||||
"Module A"@{ type: package }
|
||||
|
||||
systemBoundary "Module B"
|
||||
"Feature B1"
|
||||
"Feature B2"
|
||||
"Admin B1"
|
||||
end
|
||||
|
||||
User1 --important--> "Feature A1"
|
||||
User2 --> "Feature A2"
|
||||
Admin1 --> "Admin A1"
|
||||
User3 --> "Feature B1"
|
||||
User4 --> "Feature B2"
|
||||
Admin2 --> "Admin B1"
|
||||
|
||||
System1 <-- "Feature A1"
|
||||
System1 <-- "Feature B1"
|
||||
System2 <-- "Admin A1"
|
||||
System2 <-- "Admin B1"
|
||||
|
||||
User1 --"collaborates"--> User2
|
||||
Admin1 --"supervises"--> Admin2
|
||||
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||
|
||||
const staticBellIconPack = {
|
||||
prefix: 'fa6-regular',
|
||||
icons: {
|
||||
bell: {
|
||||
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
|
||||
width: 448,
|
||||
},
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
};
|
||||
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: 'logos',
|
||||
loader: () =>
|
||||
fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()),
|
||||
},
|
||||
{
|
||||
name: 'fa',
|
||||
loader: () =>
|
||||
fetch('https://unpkg.com/@iconify-json/fa6-solid/icons.json').then((res) => res.json()),
|
||||
},
|
||||
]);
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('Mermaid error: ', err);
|
||||
};
|
||||
window.callback = function () {
|
||||
alert('A callback was triggered');
|
||||
};
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
}
|
||||
await mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
// theme: 'forest',
|
||||
// handDrawnSeed: 12,
|
||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||
layout: 'dagre',
|
||||
// layout: 'elk',
|
||||
// layout: 'fixed',
|
||||
// htmlLabels: false,
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
|
||||
// fontFamily: 'Caveat',
|
||||
// fontFamily: 'Kalam',
|
||||
// fontFamily: 'courier',
|
||||
fontFamily: 'Recursive',
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
messageFontFamily: 'courier',
|
||||
},
|
||||
kanban: {
|
||||
htmlLabels: false,
|
||||
},
|
||||
fontSize: 16,
|
||||
logLevel: 0,
|
||||
securityLevel: 'loose',
|
||||
callback,
|
||||
});
|
||||
// setTimeout(() => {
|
||||
mermaid.init(undefined, document.querySelectorAll('.mermaid'));
|
||||
// }, 1000);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -236,27 +236,22 @@ A --> C[End]
|
||||
|
||||
Some common flowchart configurations are:
|
||||
|
||||
- _htmlLabels_: true/false
|
||||
- _curve_: linear/curve
|
||||
- _diagramPadding_: number
|
||||
- _useMaxWidth_: number
|
||||
|
||||
**Deprecated configurations:**
|
||||
|
||||
- ~~_htmlLabels_~~: Use global `htmlLabels` instead
|
||||
|
||||
For a complete list of flowchart configurations, see [defaultConfig.ts](https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/defaultConfig.ts) in the source code.
|
||||
_Soon we plan to publish a complete list of all diagram-specific configurations updated in the docs._
|
||||
|
||||
The following code snippet changes flowchart config:
|
||||
|
||||
```
|
||||
%%{init: { "htmlLabels": true, "flowchart": { "curve": "linear" } } }%%
|
||||
```
|
||||
`%%{init: { "flowchart": { "htmlLabels": true, "curve": "linear" } } }%%`
|
||||
|
||||
**Note:** `flowchart.htmlLabels` has been deprecated. Use the global `htmlLabels` configuration instead.
|
||||
Here we are overriding only the flowchart config, and not the general config, setting `htmlLabels` to `true` and `curve` to `linear`.
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { "htmlLabels": true, "flowchart": { "curve": "linear" } } }%%
|
||||
%%{init: { "flowchart": { "htmlLabels": true, "curve": "linear" } } }%%
|
||||
graph TD
|
||||
A(Forest) --> B[/Another/]
|
||||
A --> C[End]
|
||||
@@ -267,7 +262,7 @@ A --> C[End]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { "htmlLabels": true, "flowchart": { "curve": "linear" } } }%%
|
||||
%%{init: { "flowchart": { "htmlLabels": true, "curve": "linear" } } }%%
|
||||
graph TD
|
||||
A(Forest) --> B[/Another/]
|
||||
A --> C[End]
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
- [addDirective](functions/addDirective.md)
|
||||
- [getConfig](functions/getConfig.md)
|
||||
- [getEffectiveHtmlLabels](functions/getEffectiveHtmlLabels.md)
|
||||
- [getSiteConfig](functions/getSiteConfig.md)
|
||||
- [getUserDefinedConfig](functions/getUserDefinedConfig.md)
|
||||
- [reset](functions/reset.md)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/getEffectiveHtmlLabels.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/getEffectiveHtmlLabels.md).
|
||||
|
||||
[**mermaid**](../../README.md)
|
||||
|
||||
---
|
||||
|
||||
# Function: getEffectiveHtmlLabels()
|
||||
|
||||
> **getEffectiveHtmlLabels**(`config`): `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/config.ts:272](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L272)
|
||||
|
||||
Helper function to handle deprecated flowchart.htmlLabels
|
||||
|
||||
## Parameters
|
||||
|
||||
### config
|
||||
|
||||
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
|
||||
|
||||
The configuration object
|
||||
|
||||
## Returns
|
||||
|
||||
`boolean`
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/config.ts:254](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L254)
|
||||
Defined in: [packages/mermaid/src/config.ts:252](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L252)
|
||||
|
||||
## Returns
|
||||
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
> `const` **configKeys**: `Set`<`string`>
|
||||
|
||||
Defined in: [packages/mermaid/src/defaultConfig.ts:292](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L292)
|
||||
Defined in: [packages/mermaid/src/defaultConfig.ts:295](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L295)
|
||||
|
||||
@@ -105,7 +105,7 @@ You can set this attribute to base the seed on a static string.
|
||||
|
||||
> `optional` **dompurifyConfig**: `Config`
|
||||
|
||||
Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
|
||||
|
||||
---
|
||||
|
||||
@@ -179,7 +179,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
|
||||
|
||||
> `optional` **fontSize**: `number`
|
||||
|
||||
Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
|
||||
|
||||
---
|
||||
|
||||
@@ -292,7 +292,7 @@ Defines which main look to use for the diagram.
|
||||
|
||||
> `optional` **markdownAutoWrap**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:217](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L217)
|
||||
|
||||
---
|
||||
|
||||
@@ -424,7 +424,7 @@ Defined in: [packages/mermaid/src/config.type.ts:198](https://github.com/mermaid
|
||||
|
||||
> `optional` **suppressErrorRendering**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/config.type.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L222)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223)
|
||||
|
||||
Suppresses inserting 'Syntax error' diagram in the DOM.
|
||||
This is useful when you want to control how to handle syntax errors in your application.
|
||||
@@ -466,11 +466,19 @@ Defined in: [packages/mermaid/src/config.type.ts:196](https://github.com/mermaid
|
||||
|
||||
---
|
||||
|
||||
### usecase?
|
||||
|
||||
> `optional` **usecase**: `UsecaseDiagramConfig`
|
||||
|
||||
Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
|
||||
|
||||
---
|
||||
|
||||
### wrap?
|
||||
|
||||
> `optional` **wrap**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
|
||||
Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -372,7 +372,7 @@ The list of configuration objects are described [in the mermaidAPI documentation
|
||||
```html
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
let config = { startOnLoad: true, htmlLabels: true, flowchart: { useMaxWidth: false } };
|
||||
let config = { startOnLoad: true, flowchart: { useMaxWidth: false, htmlLabels: true } };
|
||||
mermaid.initialize(config);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -344,6 +344,7 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
|
||||
| Display | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` |
|
||||
| Divided Process | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-process`, `divided-rectangle` |
|
||||
| Document | Document | `doc` | Represents a document | `doc`, `document` |
|
||||
| Ellipse | Ellipse | `ellipse` | Ellipse shape | `oval` |
|
||||
| Event | Rounded Rectangle | `rounded` | Represents an event | `event` |
|
||||
| Extract | Triangle | `tri` | Extraction process | `extract`, `triangle` |
|
||||
| Fork/Join | Filled Rectangle | `fork` | Fork or join in process flow | `join` |
|
||||
|
||||
@@ -329,7 +329,11 @@ Messages can be of two displayed either solid or with a dotted line.
|
||||
[Actor][Arrow][Actor]:Message text
|
||||
```
|
||||
|
||||
There are ten types of arrows currently supported:
|
||||
Lines can be solid or dotted, and can end with various types of arrowheads, crosses, or open arrows.
|
||||
|
||||
#### Supported Arrow Types
|
||||
|
||||
**Standard Arrow Types**
|
||||
|
||||
| Type | Description |
|
||||
| -------- | ---------------------------------------------------- |
|
||||
@@ -344,6 +348,58 @@ There are ten types of arrows currently supported:
|
||||
| `-)` | Solid line with an open arrow at the end (async) |
|
||||
| `--)` | Dotted line with a open arrow at the end (async) |
|
||||
|
||||
**Half-Arrows (v\<MERMAID_RELEASE_VERSION>+)**
|
||||
|
||||
The following half-arrow types are supported for more expressive sequence diagrams. Both solid and dotted variants are available by increasing the number of dashes (`-` → `--`).
|
||||
|
||||
---
|
||||
|
||||
| Type | Description |
|
||||
| ------- | ---------------------------------------------------- |
|
||||
| `-\|\` | Solid line with top half arrowhead |
|
||||
| `--\|\` | Dotted line with top half arrowhead |
|
||||
| `-\|/` | Solid line with bottom half arrowhead |
|
||||
| `--\|/` | Dotted line with bottom half arrowhead |
|
||||
| `/\|-` | Solid line with reverse top half arrowhead |
|
||||
| `/\|--` | Dotted line with reverse top half arrowhead |
|
||||
| `\\-` | Solid line with reverse bottom half arrowhead |
|
||||
| `\\--` | Dotted line with reverse bottom half arrowhead |
|
||||
| `-\\` | Solid line with top stick half arrowhead |
|
||||
| `--\\` | Dotted line with top stick half arrowhead |
|
||||
| `-//` | Solid line with bottom stick half arrowhead |
|
||||
| `--//` | Dotted line with bottom stick half arrowhead |
|
||||
| `//-` | Solid line with reverse top stick half arrowhead |
|
||||
| `//--` | Dotted line with reverse top stick half arrowhead |
|
||||
| `\\-` | Solid line with reverse bottom stick half arrowhead |
|
||||
| `\\--` | Dotted line with reverse bottom stick half arrowhead |
|
||||
|
||||
## Central Connections (v\<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
Mermaid sequence diagrams support **central lifeline connections** using a `()`.
|
||||
This is useful to represent messages or signals that connect to a central point, rather than from one actor directly to another.
|
||||
|
||||
To indicate a central connection, append `()` to the arrow syntax.
|
||||
|
||||
#### Basic Syntax
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant John
|
||||
Alice->>()John: Hello John
|
||||
Alice()->>John: How are you?
|
||||
John()->>()Alice: Great!
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant John
|
||||
Alice->>()John: Hello John
|
||||
Alice()->>John: How are you?
|
||||
John()->>()Alice: Great!
|
||||
```
|
||||
|
||||
## Activations
|
||||
|
||||
It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations:
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
"git graph"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm build:esbuild && pnpm build:types",
|
||||
"build": "pnpm antlr:generate && pnpm build:esbuild && pnpm build:types",
|
||||
"build:esbuild": "pnpm run -r clean && tsx .esbuild/build.ts",
|
||||
"antlr:generate": "tsx scripts/antlr-generate.mts",
|
||||
"antlr:watch": "tsx scripts/antlr-watch.mts",
|
||||
"build:mermaid": "pnpm build:esbuild --mermaid",
|
||||
"build:viz": "pnpm build:esbuild --visualize",
|
||||
"build:types": "pnpm --filter mermaid types:build-config && tsx .build/types.ts",
|
||||
@@ -64,7 +66,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-cypress": "^3.55.2",
|
||||
"@argos-ci/cypress": "^6.1.1",
|
||||
"@argos-ci/cypress": "^6.1.3",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"@cspell/eslint-plugin": "^8.19.4",
|
||||
|
||||
51
packages/examples/src/examples/usecase.ts
Normal file
51
packages/examples/src/examples/usecase.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { DiagramMetadata } from '../types.js';
|
||||
|
||||
export default {
|
||||
id: 'usecase',
|
||||
name: 'Use Case Diagram',
|
||||
description: 'Visualize system functionality and user interactions',
|
||||
examples: [
|
||||
{
|
||||
title: 'Basic Use Case',
|
||||
isDefault: true,
|
||||
code: `usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> (Login)
|
||||
User --> (View Profile)
|
||||
Admin --> (Manage Users)
|
||||
Admin --> (View Reports)`,
|
||||
},
|
||||
{
|
||||
title: 'System Boundary',
|
||||
code: `usecase
|
||||
actor Customer
|
||||
actor Support
|
||||
|
||||
SystemBoundary@{ type: rect } "E-commerce System" {
|
||||
Customer --> (Browse Products)
|
||||
Customer --> (Place Order)
|
||||
Customer --> (Track Order)
|
||||
}
|
||||
|
||||
SystemBoundary@{ type: package } "Admin Panel" {
|
||||
Support --> (Process Orders)
|
||||
Support --> (Handle Returns)
|
||||
}`,
|
||||
},
|
||||
{
|
||||
title: 'Actor Relationships',
|
||||
code: `usecase
|
||||
actor Developer1
|
||||
actor Developer2
|
||||
actor Manager
|
||||
|
||||
Developer1 --> (Write Code)
|
||||
Developer2 --> (Review Code)
|
||||
Manager --> (Approve Release)
|
||||
|
||||
Developer1 --> Developer2
|
||||
Manager --> Developer1`,
|
||||
},
|
||||
],
|
||||
} satisfies DiagramMetadata;
|
||||
@@ -21,6 +21,7 @@ import quadrantChart from './examples/quadrant-chart.js';
|
||||
import packetDiagram from './examples/packet.js';
|
||||
import blockDiagram from './examples/block.js';
|
||||
import treemapDiagram from './examples/treemap.js';
|
||||
import usecaseDiagram from './examples/usecase.js';
|
||||
|
||||
export const diagramData: DiagramMetadata[] = [
|
||||
flowChart,
|
||||
@@ -45,4 +46,5 @@ export const diagramData: DiagramMetadata[] = [
|
||||
packetDiagram,
|
||||
blockDiagram,
|
||||
treemapDiagram,
|
||||
usecaseDiagram,
|
||||
];
|
||||
|
||||
@@ -67,7 +67,22 @@ export const render = async (
|
||||
|
||||
// Add the element to the DOM
|
||||
if (!node.isGroup) {
|
||||
const child = node as NodeWithVertex;
|
||||
// const child = node as NodeWithVertex;
|
||||
const child: NodeWithVertex = {
|
||||
id: node.id,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
// Store the original node data for later use
|
||||
label: node.label,
|
||||
isGroup: node.isGroup,
|
||||
shape: node.shape,
|
||||
padding: node.padding,
|
||||
cssClasses: node.cssClasses,
|
||||
cssStyles: node.cssStyles,
|
||||
look: node.look,
|
||||
// Include parentId for subgraph processing
|
||||
parentId: node.parentId,
|
||||
};
|
||||
graph.children.push(child);
|
||||
nodeDb[node.id] = node;
|
||||
|
||||
@@ -150,7 +165,7 @@ export const render = async (
|
||||
domId: { node: () => any; attr: (arg0: string, arg1: string) => void };
|
||||
}) {
|
||||
if (node) {
|
||||
nodeDb[node.id] = node;
|
||||
nodeDb[node.id] ??= {};
|
||||
nodeDb[node.id].offset = {
|
||||
posX: node.x + relX,
|
||||
posY: node.y + relY,
|
||||
@@ -860,11 +875,13 @@ export const render = async (
|
||||
log.info('APA01 layout result:', JSON.stringify(g, null, 2));
|
||||
} catch (error) {
|
||||
log.error('APA01 ELK layout error:', error);
|
||||
log.error('APA01 elkGraph that caused error:', JSON.stringify(elkGraph, null, 2));
|
||||
throw error;
|
||||
}
|
||||
|
||||
// debugger;
|
||||
await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
|
||||
|
||||
g.edges?.map(
|
||||
(edge: {
|
||||
sources: (string | number)[];
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"dev": "pnpm -w dev",
|
||||
"antlr:generate": "tsx ../../scripts/antlr-generate.mts",
|
||||
"antlr:watch": "tsx ../../scripts/antlr-watch.mts",
|
||||
"docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaid.ts && prettier --write ./src/docs/config/setup",
|
||||
"docs:build": "rimraf ../../docs && pnpm docs:code && pnpm docs:spellcheck && tsx scripts/docs.cli.mts",
|
||||
"docs:verify": "pnpm docs:code && pnpm docs:spellcheck && tsx scripts/docs.cli.mts --verify",
|
||||
@@ -47,7 +49,7 @@
|
||||
"docs:verify-version": "tsx scripts/update-release-version.mts --verify",
|
||||
"types:build-config": "tsx scripts/create-types-from-json-schema.mts",
|
||||
"types:verify-config": "tsx scripts/create-types-from-json-schema.mts --verify",
|
||||
"checkCircle": "npx madge --circular ./src",
|
||||
"checkCircle": "npx madge --circular ./src --exclude '.*generated.*'",
|
||||
"prepublishOnly": "pnpm docs:verify-version"
|
||||
},
|
||||
"repository": {
|
||||
@@ -71,6 +73,7 @@
|
||||
"@iconify/utils": "^3.0.2",
|
||||
"@mermaid-js/parser": "workspace:^",
|
||||
"@types/d3": "^7.4.3",
|
||||
"antlr4ng": "^3.0.7",
|
||||
"cytoscape": "^3.33.1",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
|
||||
@@ -189,6 +189,7 @@ This Markdown should be kept.
|
||||
| Display | Curved Trapezoid | \`curv-trap\` | Represents a display | \`curved-trapezoid\`, \`display\` |
|
||||
| Divided Process | Divided Rectangle | \`div-rect\` | Divided process shape | \`div-proc\`, \`divided-process\`, \`divided-rectangle\` |
|
||||
| Document | Document | \`doc\` | Represents a document | \`doc\`, \`document\` |
|
||||
| Ellipse | Ellipse | \`ellipse\` | Ellipse shape | \`oval\` |
|
||||
| Event | Rounded Rectangle | \`rounded\` | Represents an event | \`event\` |
|
||||
| Extract | Triangle | \`tri\` | Extraction process | \`extract\`, \`triangle\` |
|
||||
| Fork/Join | Filled Rectangle | \`fork\` | Fork or join in process flow | \`join\` |
|
||||
|
||||
@@ -227,8 +227,6 @@ export const reset = (config = siteConfig): void => {
|
||||
const ConfigWarning = {
|
||||
LAZY_LOAD_DEPRECATED:
|
||||
'The configuration options lazyLoadedDiagrams and loadExternalDiagramsAtStartup are deprecated. Please use registerExternalDiagrams instead.',
|
||||
FLOWCHART_HTML_LABELS_DEPRECATED:
|
||||
'flowchart.htmlLabels is deprecated. Please use global htmlLabels instead.',
|
||||
} as const;
|
||||
|
||||
type ConfigWarningStrings = keyof typeof ConfigWarning;
|
||||
@@ -264,15 +262,3 @@ export const getUserDefinedConfig = (): MermaidConfig => {
|
||||
|
||||
return userConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to handle deprecated flowchart.htmlLabels
|
||||
* @param config - The configuration object
|
||||
*/
|
||||
export const getEffectiveHtmlLabels = (config: MermaidConfig): boolean => {
|
||||
if (config.flowchart?.htmlLabels !== undefined) {
|
||||
issueWarning('FLOWCHART_HTML_LABELS_DEPRECATED');
|
||||
}
|
||||
|
||||
return config.htmlLabels ?? config.flowchart?.htmlLabels ?? true;
|
||||
};
|
||||
|
||||
@@ -210,6 +210,7 @@ export interface MermaidConfig {
|
||||
packet?: PacketDiagramConfig;
|
||||
block?: BlockDiagramConfig;
|
||||
radar?: RadarDiagramConfig;
|
||||
usecase?: UsecaseDiagramConfig;
|
||||
dompurifyConfig?: DOMPurifyConfiguration;
|
||||
wrap?: boolean;
|
||||
fontSize?: number;
|
||||
@@ -248,12 +249,7 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
|
||||
*/
|
||||
diagramPadding?: number;
|
||||
/**
|
||||
* @deprecated
|
||||
* **DEPRECATED: Use global `htmlLabels` instead.**
|
||||
*
|
||||
* Flag for setting whether or not a html tag should be used for rendering labels on nodes and edges.
|
||||
* This property is deprecated.
|
||||
* Please use the global `htmlLabels` configuration instead.
|
||||
* Flag for setting whether or not a html tag should be used for rendering labels on the edges.
|
||||
*
|
||||
*/
|
||||
htmlLabels?: boolean;
|
||||
@@ -1628,6 +1624,50 @@ export interface RadarDiagramConfig extends BaseDiagramConfig {
|
||||
*/
|
||||
curveTension?: number;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for usecase diagrams.
|
||||
*
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "UsecaseDiagramConfig".
|
||||
*/
|
||||
export interface UsecaseDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Font size for actor labels
|
||||
*/
|
||||
actorFontSize?: number;
|
||||
/**
|
||||
* Font family for actor labels
|
||||
*/
|
||||
actorFontFamily?: string;
|
||||
/**
|
||||
* Font weight for actor labels
|
||||
*/
|
||||
actorFontWeight?: string;
|
||||
/**
|
||||
* Font size for usecase labels
|
||||
*/
|
||||
usecaseFontSize?: number;
|
||||
/**
|
||||
* Font family for usecase labels
|
||||
*/
|
||||
usecaseFontFamily?: string;
|
||||
/**
|
||||
* Font weight for usecase labels
|
||||
*/
|
||||
usecaseFontWeight?: string;
|
||||
/**
|
||||
* Margin around actors
|
||||
*/
|
||||
actorMargin?: number;
|
||||
/**
|
||||
* Margin around use cases
|
||||
*/
|
||||
usecaseMargin?: number;
|
||||
/**
|
||||
* Padding around the entire diagram
|
||||
*/
|
||||
diagramPadding?: number;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "FontConfig".
|
||||
|
||||
@@ -4,8 +4,7 @@ import createLabel from './createLabel.js';
|
||||
import { createText } from '../rendering-util/createText.js';
|
||||
import { select } from 'd3';
|
||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../config.js';
|
||||
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
|
||||
|
||||
const rect = async (parent, node) => {
|
||||
@@ -21,7 +20,7 @@ const rect = async (parent, node) => {
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
const useHtmlLabels = getEffectiveHtmlLabels(siteConfig);
|
||||
const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
|
||||
|
||||
// Create the label and insert it after the rect
|
||||
const label = shapeSvg.insert('g').attr('class', 'cluster-label');
|
||||
@@ -39,7 +38,7 @@ const rect = async (parent, node) => {
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
|
||||
if (getEffectiveHtmlLabels(siteConfig)) {
|
||||
if (evaluate(siteConfig.flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -151,7 +150,7 @@ const roundedWithTitle = async (parent, node) => {
|
||||
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
if (getEffectiveHtmlLabels(siteConfig)) {
|
||||
if (evaluate(siteConfig.flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -191,7 +190,7 @@ const roundedWithTitle = async (parent, node) => {
|
||||
node.y -
|
||||
node.height / 2 -
|
||||
node.padding / 3 +
|
||||
(getEffectiveHtmlLabels(siteConfig) ? 5 : 3) +
|
||||
(evaluate(siteConfig.flowchart.htmlLabels) ? 5 : 3) +
|
||||
subGraphTitleTopMargin
|
||||
})`
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { select } from 'd3';
|
||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../config.js';
|
||||
import { sanitizeText } from '../diagrams/common/common.js';
|
||||
import { evaluate, sanitizeText } from '../diagrams/common/common.js';
|
||||
import { log } from '../logger.js';
|
||||
import { replaceIconSubstring } from '../rendering-util/createText.js';
|
||||
import { decodeEntities } from '../utils.js';
|
||||
@@ -51,7 +50,7 @@ const createLabel = async (_vertexText, style, isTitle, isNode) => {
|
||||
vertexText = vertexText[0];
|
||||
}
|
||||
const config = getConfig();
|
||||
if (getEffectiveHtmlLabels(config)) {
|
||||
if (evaluate(config.flowchart.htmlLabels)) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
vertexText = vertexText.replace(/\\n|\n/g, '<br />');
|
||||
log.debug('vertexText' + vertexText);
|
||||
|
||||
@@ -3,9 +3,8 @@ import createLabel from './createLabel.js';
|
||||
import { createText } from '../rendering-util/createText.js';
|
||||
import { line, curveBasis, select } from 'd3';
|
||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../config.js';
|
||||
import utils from '../utils.js';
|
||||
import { getUrl } from '../diagrams/common/common.js';
|
||||
import { evaluate, getUrl } from '../diagrams/common/common.js';
|
||||
import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js';
|
||||
import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
|
||||
import { addEdgeMarkers } from './edgeMarker.js';
|
||||
@@ -20,7 +19,7 @@ export const clear = () => {
|
||||
|
||||
export const insertEdgeLabel = async (elem, edge) => {
|
||||
const config = getConfig();
|
||||
const useHtmlLabels = getEffectiveHtmlLabels(config);
|
||||
const useHtmlLabels = evaluate(config.flowchart.htmlLabels);
|
||||
// Create the actual text element
|
||||
const labelElement =
|
||||
edge.labelType === 'markdown'
|
||||
@@ -134,7 +133,7 @@ export const insertEdgeLabel = async (elem, edge) => {
|
||||
* @param {any} value
|
||||
*/
|
||||
function setTerminalWidth(fo, value) {
|
||||
if (getEffectiveHtmlLabels(getConfig()) && fo) {
|
||||
if (getConfig().flowchart.htmlLabels && fo) {
|
||||
fo.style.width = value.length * 9 + 'px';
|
||||
fo.style.height = '12px';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { select } from 'd3';
|
||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../config.js';
|
||||
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
import { log } from '../logger.js';
|
||||
import { getArrowPoints } from './blockArrowHelper.js';
|
||||
import createLabel from './createLabel.js';
|
||||
@@ -589,7 +588,7 @@ const rectWithTitle = async (parent, node) => {
|
||||
|
||||
const text = label.node().appendChild(await createLabel(title, node.labelStyle, true, true));
|
||||
let bbox = { width: 0, height: 0 };
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -610,7 +609,7 @@ const rectWithTitle = async (parent, node) => {
|
||||
)
|
||||
);
|
||||
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
const div = descr.children[0];
|
||||
const dv = select(descr);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -918,7 +917,7 @@ const class_box = async (parent, node) => {
|
||||
.node()
|
||||
.appendChild(await createLabel(interfaceLabelText, node.labelStyle, true, true));
|
||||
let interfaceBBox = interfaceLabel.getBBox();
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
const div = interfaceLabel.children[0];
|
||||
const dv = select(interfaceLabel);
|
||||
interfaceBBox = div.getBoundingClientRect();
|
||||
@@ -933,7 +932,7 @@ const class_box = async (parent, node) => {
|
||||
let classTitleString = node.classData.label;
|
||||
|
||||
if (node.classData.type !== undefined && node.classData.type !== '') {
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
classTitleString += '<' + node.classData.type + '>';
|
||||
} else {
|
||||
classTitleString += '<' + node.classData.type + '>';
|
||||
@@ -944,7 +943,7 @@ const class_box = async (parent, node) => {
|
||||
.appendChild(await createLabel(classTitleString, node.labelStyle, true, true));
|
||||
select(classTitleLabel).attr('class', 'classTitle');
|
||||
let classTitleBBox = classTitleLabel.getBBox();
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
const div = classTitleLabel.children[0];
|
||||
const dv = select(classTitleLabel);
|
||||
classTitleBBox = div.getBoundingClientRect();
|
||||
@@ -959,7 +958,7 @@ const class_box = async (parent, node) => {
|
||||
node.classData.members.forEach(async (member) => {
|
||||
const parsedInfo = member.getDisplayDetails();
|
||||
let parsedText = parsedInfo.displayText;
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
parsedText = parsedText.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
const lbl = labelContainer
|
||||
@@ -973,7 +972,7 @@ const class_box = async (parent, node) => {
|
||||
)
|
||||
);
|
||||
let bbox = lbl.getBBox();
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
const div = lbl.children[0];
|
||||
const dv = select(lbl);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -993,7 +992,7 @@ const class_box = async (parent, node) => {
|
||||
node.classData.methods.forEach(async (member) => {
|
||||
const parsedInfo = member.getDisplayDetails();
|
||||
let displayText = parsedInfo.displayText;
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
displayText = displayText.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
const lbl = labelContainer
|
||||
@@ -1007,7 +1006,7 @@ const class_box = async (parent, node) => {
|
||||
)
|
||||
);
|
||||
let bbox = lbl.getBBox();
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
const div = lbl.children[0];
|
||||
const dv = select(lbl);
|
||||
bbox = div.getBoundingClientRect();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { updateNodeBounds, labelHelper } from './util.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../../config.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
|
||||
const note = async (parent, node) => {
|
||||
const useHtmlLabels = getEffectiveHtmlLabels(getConfig()) || node.useHtmlLabels;
|
||||
const useHtmlLabels = node.useHtmlLabels || getConfig().flowchart.htmlLabels;
|
||||
if (!useHtmlLabels) {
|
||||
node.centerLabel = true;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import createLabel from '../createLabel.js';
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../../config.js';
|
||||
import { select } from 'd3';
|
||||
import { sanitizeText } from '../../diagrams/common/common.js';
|
||||
import { evaluate, sanitizeText } from '../../diagrams/common/common.js';
|
||||
import { decodeEntities } from '../../utils.js';
|
||||
|
||||
export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
const config = getConfig();
|
||||
let classes;
|
||||
const useHtmlLabels = node.useHtmlLabels || getEffectiveHtmlLabels(config);
|
||||
const useHtmlLabels = node.useHtmlLabels || evaluate(config.flowchart.htmlLabels);
|
||||
if (!_classes) {
|
||||
classes = 'node default';
|
||||
} else {
|
||||
@@ -61,7 +60,7 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
let bbox = text.getBBox();
|
||||
const halfPadding = node.padding / 2;
|
||||
|
||||
if (getEffectiveHtmlLabels(config)) {
|
||||
if (evaluate(config.flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
|
||||
|
||||
@@ -264,6 +264,9 @@ const config: RequiredDeep<MermaidConfig> = {
|
||||
radar: {
|
||||
...defaultConfigJson.radar,
|
||||
},
|
||||
usecase: {
|
||||
...defaultConfigJson.usecase,
|
||||
},
|
||||
treemap: {
|
||||
useMaxWidth: true,
|
||||
padding: 10,
|
||||
|
||||
@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
import { treemap } from '../diagrams/treemap/detector.js';
|
||||
import { usecase } from '../diagrams/usecase/usecaseDetector.js';
|
||||
import '../type.d.ts';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
|
||||
xychart,
|
||||
block,
|
||||
radar,
|
||||
treemap
|
||||
treemap,
|
||||
usecase
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { select, curveLinear } from 'd3';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../../config.js';
|
||||
import { render } from '../../dagre-wrapper/index.js';
|
||||
import utils, { getEdgeId } from '../../utils.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
|
||||
@@ -261,7 +260,7 @@ export const addRelations = function (relations: ClassRelation[], g: graphlib.Gr
|
||||
edgeData.labelpos = 'c';
|
||||
|
||||
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (getConfig().flowchart?.htmlLabels ?? getConfig().htmlLabels) {
|
||||
edgeData.labelType = 'html';
|
||||
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { getEffectiveHtmlLabels } from '../../config.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
|
||||
// Remove and ignore br:s
|
||||
@@ -65,7 +64,7 @@ export const removeScript = (txt: string): string => {
|
||||
};
|
||||
|
||||
const sanitizeMore = (text: string, config: MermaidConfig) => {
|
||||
if (getEffectiveHtmlLabels(config)) {
|
||||
if (config.flowchart?.htmlLabels !== false) {
|
||||
const level = config.securityLevel;
|
||||
if (level === 'antiscript' || level === 'strict') {
|
||||
text = removeScript(text);
|
||||
|
||||
@@ -66,12 +66,15 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
\}\| return 'ONE_OR_MORE';
|
||||
"one" return 'ONLY_ONE';
|
||||
"only one" return 'ONLY_ONE';
|
||||
"1" return 'ONLY_ONE';
|
||||
[0-9]+\.[0-9]+ return 'DECIMAL_NUM';
|
||||
"1"(?=\s+[A-Za-z_"']) return 'ONLY_ONE';
|
||||
"1" return 'ENTITY_ONE';
|
||||
[0-9]+ return 'NUM';
|
||||
\|\| return 'ONLY_ONE';
|
||||
o\| return 'ZERO_OR_ONE';
|
||||
o\{ return 'ZERO_OR_MORE';
|
||||
\|\{ return 'ONE_OR_MORE';
|
||||
\s*u return 'MD_PARENT';
|
||||
u(?=[\.\-\|]) return 'MD_PARENT';
|
||||
\.\. return 'NON_IDENTIFYING';
|
||||
\-\- return 'IDENTIFYING';
|
||||
"to" return 'IDENTIFYING';
|
||||
@@ -80,13 +83,15 @@ o\{ return 'ZERO_OR_MORE';
|
||||
\-\. return 'NON_IDENTIFYING';
|
||||
<style>([^\x00-\x7F]|\w|\-|\*)+ return 'STYLE_TEXT';
|
||||
<style>';' return 'SEMI';
|
||||
([^\x00-\x7F]|\w|\-|\*)+ return 'UNICODE_TEXT';
|
||||
[0-9] return 'NUM';
|
||||
([^\x00-\x7F]|\w|\-|\*|\.)+ return 'UNICODE_TEXT';
|
||||
. return yytext[0];
|
||||
<<EOF>> return 'EOF';
|
||||
|
||||
/lex
|
||||
|
||||
%left 'ONLY_ONE'
|
||||
%left 'ZERO_OR_ONE' 'ZERO_OR_MORE' 'ONE_OR_MORE' 'MD_PARENT'
|
||||
|
||||
%start start
|
||||
%% /* language grammar */
|
||||
|
||||
@@ -228,6 +233,9 @@ styleComponent: STYLE_TEXT | NUM | COLON | BRKT;
|
||||
entityName
|
||||
: 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
|
||||
| 'UNICODE_TEXT' { $$ = $1; }
|
||||
| 'NUM' { $$ = $1; }
|
||||
| 'DECIMAL_NUM' { $$ = $1; }
|
||||
| 'ENTITY_ONE' { $$ = $1; }
|
||||
;
|
||||
|
||||
attributes
|
||||
|
||||
@@ -1001,4 +1001,90 @@ describe('when parsing ER diagram it...', function () {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('syntax fixes for special characters and numbers', function () {
|
||||
describe('standalone entity names', function () {
|
||||
it('should allow number "1" as standalone entity', function () {
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n1`);
|
||||
});
|
||||
|
||||
it('should allow character "u" as standalone entity', function () {
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\nu`);
|
||||
});
|
||||
|
||||
it('should allow decimal numbers as standalone entities', function () {
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n2.5`);
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n1.5`);
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n0.1`);
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n99.99`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entity names with attributes', function () {
|
||||
it('should allow "u" as entity name with attributes', function () {
|
||||
erDiagram.parser.parse(`erDiagram\nu {\nstring name\nint id\n}`);
|
||||
});
|
||||
|
||||
it('should allow number "1" as entity name with attributes', function () {
|
||||
erDiagram.parser.parse(`erDiagram\n1 {\nstring name\nint id\n}`);
|
||||
});
|
||||
|
||||
it('should allow decimal numbers as entity names with attributes', function () {
|
||||
erDiagram.parser.parse(`erDiagram\n2.5 {\nstring name\nint id\n}`);
|
||||
erDiagram.parser.parse(`erDiagram\n1.5 {\nstring value\n}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entity names in relationships', function () {
|
||||
it('should allow "u" in relationships', function () {
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER ||--|| u : has`);
|
||||
erDiagram.parser.parse(`erDiagram\nu ||--|| ORDER : places`);
|
||||
erDiagram.parser.parse(`erDiagram\nu ||--|| v : connects`);
|
||||
});
|
||||
|
||||
it('should allow numbers in relationships', function () {
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER ||--|| 1 : has`);
|
||||
erDiagram.parser.parse(`erDiagram\n1 ||--|| ORDER : places`);
|
||||
erDiagram.parser.parse(`erDiagram\n1 ||--|| 2 : connects`);
|
||||
});
|
||||
|
||||
it('should allow decimal numbers in relationships', function () {
|
||||
erDiagram.parser.parse(`erDiagram\nCUSTOMER ||--|| 2.5 : has`);
|
||||
erDiagram.parser.parse(`erDiagram\n1.5 ||--|| ORDER : places`);
|
||||
erDiagram.parser.parse(`erDiagram\n2.5 ||--|| 5.5 : connects`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed scenarios', function () {
|
||||
it('should handle complex diagram with special entity names', function () {
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
CUSTOMER ||--o{ 1 : places
|
||||
1 ||--|{ u : contains
|
||||
u {
|
||||
string name
|
||||
int quantity
|
||||
}
|
||||
"2.5" ||--|| ORDER : processes
|
||||
ORDER {
|
||||
int id
|
||||
date created
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle attributes with numbers in names (but not starting)', function () {
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
ENTITY {
|
||||
string name1
|
||||
int value2
|
||||
float point3_5
|
||||
}
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -962,7 +962,6 @@ You have to call mermaid.initialize.`
|
||||
case 'round':
|
||||
return 'roundedRect';
|
||||
case 'ellipse':
|
||||
// @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976
|
||||
return 'ellipse';
|
||||
default:
|
||||
return vertex.type;
|
||||
|
||||
@@ -321,7 +321,7 @@ export class RequirementDB implements DiagramDB {
|
||||
id: `${relation.src}-${relation.dst}-${counter}`,
|
||||
start: this.requirements.get(relation.src)?.name ?? this.elements.get(relation.src)?.name,
|
||||
end: this.requirements.get(relation.dst)?.name ?? this.elements.get(relation.dst)?.name,
|
||||
label: `«${relation.type}»`,
|
||||
label: `<<${relation.type}>>`,
|
||||
classes: 'relationshipLine',
|
||||
style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'],
|
||||
labelpos: 'c',
|
||||
|
||||
@@ -40,15 +40,6 @@ const getStyles = (options) => `
|
||||
.relationshipLabel {
|
||||
fill: ${options.relationLabelColor};
|
||||
}
|
||||
.edgeLabel {
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
}
|
||||
.edgeLabel .label rect {
|
||||
fill: ${options.edgeLabelBackground};
|
||||
}
|
||||
.edgeLabel .label text {
|
||||
fill: ${options.relationLabelColor};
|
||||
}
|
||||
.divider {
|
||||
stroke: ${options.nodeBorder};
|
||||
stroke-width: 1;
|
||||
|
||||
@@ -78,7 +78,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
"off" return 'off';
|
||||
"," return ',';
|
||||
";" return 'NEWLINE';
|
||||
[^+<\->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+<\->\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; }
|
||||
[^\/\\\+\()\+<\->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)|\-\|\\|\-\\|\-\/|\-\/\/|\-\|\/|\/\|\-|\\\|\-|\/\/\-|\\\\\-|\/\|\-|\-\-\|\\|\-\-|\(\)))[\-]*[^\+<\->\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; } //final_4.11
|
||||
"->>" return 'SOLID_ARROW';
|
||||
"<<->>" return 'BIDIRECTIONAL_SOLID_ARROW';
|
||||
"-->>" return 'DOTTED_ARROW';
|
||||
@@ -89,10 +89,36 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
\-\-[x] return 'DOTTED_CROSS';
|
||||
\-[\)] return 'SOLID_POINT';
|
||||
\-\-[\)] return 'DOTTED_POINT';
|
||||
|
||||
//normal-dotted
|
||||
\-\-\|\\ return 'SOLID_ARROW_TOP_DOTTED';
|
||||
\-\-\|\/ return 'SOLID_ARROW_BOTTOM_DOTTED';
|
||||
\-\-\\\\ return 'STICK_ARROW_TOP_DOTTED';
|
||||
\-\-\/\/ return 'STICK_ARROW_BOTTOM_DOTTED';
|
||||
|
||||
//reverse-dotted
|
||||
\/\|\-\- return 'SOLID_ARROW_TOP_REVERSE_DOTTED';
|
||||
\\\|\-\- return 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED';
|
||||
\/\/\-\- return 'STICK_ARROW_TOP_REVERSE_DOTTED';
|
||||
\\\\\-\- return 'STICK_ARROW_BOTTOM_REVERSE_DOTTED';
|
||||
|
||||
//normal
|
||||
\-\|\\ return 'SOLID_ARROW_TOP';
|
||||
\-\|\/ return 'SOLID_ARROW_BOTTOM';
|
||||
\-\\\\ return 'STICK_ARROW_TOP';
|
||||
\-\/\/ return 'STICK_ARROW_BOTTOM';
|
||||
|
||||
//reverse
|
||||
\/\|\- return 'SOLID_ARROW_TOP_REVERSE';
|
||||
\\\|\- return 'SOLID_ARROW_BOTTOM_REVERSE';
|
||||
\/\/\- return 'STICK_ARROW_TOP_REVERSE';
|
||||
\\\\\- return 'STICK_ARROW_BOTTOM_REVERSE';
|
||||
|
||||
":"(?:(?:no)?wrap:)?[^#\n;]* return 'TXT';
|
||||
":" return 'TXT';
|
||||
"+" return '+';
|
||||
"-" return '-';
|
||||
"()" return '()';
|
||||
<<EOF>> return 'NEWLINE';
|
||||
. return 'INVALID';
|
||||
|
||||
@@ -304,6 +330,20 @@ signal
|
||||
{ $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5},
|
||||
{type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $1.actor}
|
||||
]}
|
||||
| actor signaltype '()' actor text2
|
||||
{ $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5, activate: true, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION},
|
||||
{type: 'centralConnection', signalType: yy.LINETYPE.CENTRAL_CONNECTION, actor: $4.actor, }
|
||||
]}
|
||||
|
||||
| actor '()' signaltype actor text2
|
||||
{ $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$3, msg:$5, activate: false, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE},
|
||||
{type: 'centralConnectionReverse', signalType: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE, actor: $1.actor}
|
||||
]}
|
||||
| actor '()' signaltype '()' actor text2
|
||||
{ $$ = [$1,$5,{type: 'addMessage', from:$1.actor, to:$5.actor, signalType:$3, msg:$6, activate: true, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION_DUAL},
|
||||
{type: 'centralConnection', signalType: yy.LINETYPE.CENTRAL_CONNECTION, actor: $5.actor, },
|
||||
{type: 'centralConnectionReverse', signalType: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE, actor: $1.actor}
|
||||
]}
|
||||
| actor signaltype actor text2
|
||||
{ $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]}
|
||||
;
|
||||
@@ -337,7 +377,28 @@ signaltype
|
||||
: SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
|
||||
| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
|
||||
| SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
|
||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||
|
||||
| SOLID_ARROW_TOP { $$ = yy.LINETYPE.SOLID_TOP; }
|
||||
| SOLID_ARROW_BOTTOM { $$ = yy.LINETYPE.SOLID_BOTTOM; }
|
||||
| STICK_ARROW_TOP { $$ = yy.LINETYPE.STICK_TOP; }
|
||||
| STICK_ARROW_BOTTOM { $$ = yy.LINETYPE.STICK_BOTTOM; }
|
||||
|
||||
| SOLID_ARROW_TOP_DOTTED { $$ = yy.LINETYPE.SOLID_TOP_DOTTED; }
|
||||
| SOLID_ARROW_BOTTOM_DOTTED { $$ = yy.LINETYPE.SOLID_BOTTOM_DOTTED; }
|
||||
| STICK_ARROW_TOP_DOTTED { $$ = yy.LINETYPE.STICK_TOP_DOTTED; }
|
||||
| STICK_ARROW_BOTTOM_DOTTED { $$ = yy.LINETYPE.STICK_BOTTOM_DOTTED; }
|
||||
|
||||
| SOLID_ARROW_TOP_REVERSE { $$ = yy.LINETYPE.SOLID_ARROW_TOP_REVERSE; }
|
||||
| SOLID_ARROW_BOTTOM_REVERSE { $$ = yy.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE; }
|
||||
| STICK_ARROW_TOP_REVERSE { $$ = yy.LINETYPE.STICK_ARROW_TOP_REVERSE; }
|
||||
| STICK_ARROW_BOTTOM_REVERSE { $$ = yy.LINETYPE.STICK_ARROW_BOTTOM_REVERSE; }
|
||||
|
||||
| SOLID_ARROW_TOP_REVERSE_DOTTED { $$ = yy.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED; }
|
||||
| SOLID_ARROW_BOTTOM_REVERSE_DOTTED { $$ = yy.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED; }
|
||||
| STICK_ARROW_TOP_REVERSE_DOTTED { $$ = yy.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED; }
|
||||
| STICK_ARROW_BOTTOM_REVERSE_DOTTED { $$ = yy.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED; }
|
||||
|
||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||
| DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
|
||||
| BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; }
|
||||
| SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }
|
||||
|
||||
@@ -64,6 +64,30 @@ const LINETYPE = {
|
||||
PAR_OVER_START: 32,
|
||||
BIDIRECTIONAL_SOLID: 33,
|
||||
BIDIRECTIONAL_DOTTED: 34,
|
||||
|
||||
SOLID_TOP: 41,
|
||||
SOLID_BOTTOM: 42,
|
||||
STICK_TOP: 43,
|
||||
STICK_BOTTOM: 44,
|
||||
|
||||
SOLID_ARROW_TOP_REVERSE: 45,
|
||||
SOLID_ARROW_BOTTOM_REVERSE: 46,
|
||||
STICK_ARROW_TOP_REVERSE: 47,
|
||||
STICK_ARROW_BOTTOM_REVERSE: 48,
|
||||
|
||||
SOLID_TOP_DOTTED: 51,
|
||||
SOLID_BOTTOM_DOTTED: 52,
|
||||
STICK_TOP_DOTTED: 53,
|
||||
STICK_BOTTOM_DOTTED: 54,
|
||||
|
||||
SOLID_ARROW_TOP_REVERSE_DOTTED: 55,
|
||||
SOLID_ARROW_BOTTOM_REVERSE_DOTTED: 56,
|
||||
STICK_ARROW_TOP_REVERSE_DOTTED: 57,
|
||||
STICK_ARROW_BOTTOM_REVERSE_DOTTED: 58,
|
||||
|
||||
CENTRAL_CONNECTION: 59,
|
||||
CENTRAL_CONNECTION_REVERSE: 60,
|
||||
CENTRAL_CONNECTION_DUAL: 61,
|
||||
} as const;
|
||||
|
||||
const ARROWTYPE = {
|
||||
@@ -244,7 +268,8 @@ export class SequenceDB implements DiagramDB {
|
||||
idTo?: Message['to'],
|
||||
message?: { text: string; wrap: boolean },
|
||||
messageType?: number,
|
||||
activate = false
|
||||
activate = false,
|
||||
centralConnection?: number
|
||||
) {
|
||||
if (messageType === this.LINETYPE.ACTIVE_END) {
|
||||
const cnt = this.activationCount(idFrom ?? '');
|
||||
@@ -271,6 +296,7 @@ export class SequenceDB implements DiagramDB {
|
||||
wrap: message?.wrap ?? this.autoWrap(),
|
||||
type: messageType,
|
||||
activate,
|
||||
centralConnection: centralConnection ?? 0,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -563,6 +589,12 @@ export class SequenceDB implements DiagramDB {
|
||||
case 'activeStart':
|
||||
this.addSignal(param.actor, undefined, undefined, param.signalType);
|
||||
break;
|
||||
case 'centralConnection':
|
||||
this.addSignal(param.actor, undefined, undefined, param.signalType);
|
||||
break;
|
||||
case 'centralConnectionReverse':
|
||||
this.addSignal(param.actor, undefined, undefined, param.signalType);
|
||||
break;
|
||||
case 'activeEnd':
|
||||
this.addSignal(param.actor, undefined, undefined, param.signalType);
|
||||
break;
|
||||
@@ -606,7 +638,14 @@ export class SequenceDB implements DiagramDB {
|
||||
this.state.records.lastDestroyed = undefined;
|
||||
}
|
||||
}
|
||||
this.addSignal(param.from, param.to, param.msg, param.signalType, param.activate);
|
||||
this.addSignal(
|
||||
param.from,
|
||||
param.to,
|
||||
param.msg,
|
||||
param.signalType,
|
||||
param.activate,
|
||||
param.centralConnection
|
||||
);
|
||||
break;
|
||||
case 'boxStart':
|
||||
this.addBox(param.boxData);
|
||||
|
||||
@@ -104,6 +104,7 @@ describe('more than one sequence diagram', () => {
|
||||
[
|
||||
{
|
||||
"activate": false,
|
||||
"centralConnection": 0,
|
||||
"from": "Alice",
|
||||
"id": "0",
|
||||
"message": "Hello Bob, how are you?",
|
||||
@@ -113,6 +114,7 @@ describe('more than one sequence diagram', () => {
|
||||
},
|
||||
{
|
||||
"activate": false,
|
||||
"centralConnection": 0,
|
||||
"from": "Bob",
|
||||
"id": "1",
|
||||
"message": "I am good thanks!",
|
||||
@@ -131,6 +133,7 @@ describe('more than one sequence diagram', () => {
|
||||
[
|
||||
{
|
||||
"activate": false,
|
||||
"centralConnection": 0,
|
||||
"from": "Alice",
|
||||
"id": "0",
|
||||
"message": "Hello Bob, how are you?",
|
||||
@@ -140,6 +143,7 @@ describe('more than one sequence diagram', () => {
|
||||
},
|
||||
{
|
||||
"activate": false,
|
||||
"centralConnection": 0,
|
||||
"from": "Bob",
|
||||
"id": "1",
|
||||
"message": "I am good thanks!",
|
||||
@@ -160,6 +164,7 @@ describe('more than one sequence diagram', () => {
|
||||
[
|
||||
{
|
||||
"activate": false,
|
||||
"centralConnection": 0,
|
||||
"from": "Alice",
|
||||
"id": "0",
|
||||
"message": "Hello John, how are you?",
|
||||
@@ -169,6 +174,7 @@ describe('more than one sequence diagram', () => {
|
||||
},
|
||||
{
|
||||
"activate": false,
|
||||
"centralConnection": 0,
|
||||
"from": "John",
|
||||
"id": "1",
|
||||
"message": "I am good thanks!",
|
||||
@@ -181,6 +187,254 @@ describe('more than one sequence diagram', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Central Connection Parsing', () => {
|
||||
describe('when parsing central connection syntax', () => {
|
||||
it('should parse actor ()->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()->>() Bob: Hello Bob, how are you?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse
|
||||
|
||||
// Find the actual message (type: 'addMessage')
|
||||
const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessage).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
message: 'Hello Bob, how are you?',
|
||||
centralConnection: 61, // CENTRAL_CONNECTION_DUAL
|
||||
activate: true,
|
||||
type: 0, // SOLID (based on test output)
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse actor ()-->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()-->>() Bob: Hello Bob, how are you?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse
|
||||
|
||||
const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessage).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
message: 'Hello Bob, how are you?',
|
||||
centralConnection: 61, // CENTRAL_CONNECTION_DUAL
|
||||
activate: true,
|
||||
type: 1, // DOTTED (based on test output)
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse actor ->>() actor syntax as CENTRAL_CONNECTION', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ->>() Bob: Hello Bob, how are you?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(2); // addMessage + centralConnection (no activation for this pattern)
|
||||
|
||||
const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessage).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
message: 'Hello Bob, how are you?',
|
||||
centralConnection: 59, // CENTRAL_CONNECTION
|
||||
activate: true,
|
||||
type: 0, // SOLID (based on actual parsing)
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse actor ()-->> actor syntax as CENTRAL_CONNECTION_REVERSE', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()-->> Bob: Hello Bob, how are you?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(2); // addMessage + centralConnectionReverse
|
||||
|
||||
const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessage).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
message: 'Hello Bob, how are you?',
|
||||
centralConnection: 60, // CENTRAL_CONNECTION_REVERSE
|
||||
activate: false,
|
||||
type: 1, // DOTTED (based on test output)
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse actor ()->> actor syntax as CENTRAL_CONNECTION_REVERSE', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()->> Bob: Hello Bob, how are you?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(2); // addMessage + centralConnectionReverse
|
||||
|
||||
const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessage).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
message: 'Hello Bob, how are you?',
|
||||
centralConnection: 60, // CENTRAL_CONNECTION_REVERSE
|
||||
activate: false,
|
||||
type: 0, // SOLID (based on test output)
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse actor ()<<-->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()<<-->>() Bob: Hello Bob, how are you?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse
|
||||
|
||||
const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessage).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
message: 'Hello Bob, how are you?',
|
||||
centralConnection: 61, // CENTRAL_CONNECTION_DUAL
|
||||
activate: true,
|
||||
type: 34, // BIDIRECTIONAL_DOTTED
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse actor ()<<->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()<<->>() Bob: Hello Bob, how are you?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse
|
||||
|
||||
const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessage).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
message: 'Hello Bob, how are you?',
|
||||
centralConnection: 61, // CENTRAL_CONNECTION_DUAL
|
||||
activate: true,
|
||||
type: 33, // BIDIRECTIONAL_SOLID
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple central connection types in one diagram', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
participant Charlie
|
||||
Alice ()->>() Bob: Message 1
|
||||
Bob ()-->> Charlie: Message 2
|
||||
Charlie ()<<-->>() Alice: Message 3
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(8); // 3 addMessages + 5 central connection markers
|
||||
|
||||
// Filter to get only the actual messages
|
||||
const actualMessages = messages.filter((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessages).toHaveLength(3);
|
||||
|
||||
expect(actualMessages[0]).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()->>())
|
||||
});
|
||||
|
||||
expect(actualMessages[1]).toMatchObject({
|
||||
from: 'Bob',
|
||||
to: 'Charlie',
|
||||
centralConnection: 60, // CENTRAL_CONNECTION_REVERSE (()-->>)
|
||||
});
|
||||
|
||||
expect(actualMessages[2]).toMatchObject({
|
||||
from: 'Charlie',
|
||||
to: 'Alice',
|
||||
centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()<<-->>())
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle central connections with different arrow types', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ()-x() Bob: Cross message
|
||||
Alice ()--x() Bob: Dotted cross message
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(6); // 2 addMessages + 4 central connection markers
|
||||
|
||||
const actualMessages = messages.filter((msg) => msg.type !== undefined && msg.from && msg.to);
|
||||
expect(actualMessages).toHaveLength(2);
|
||||
|
||||
expect(actualMessages[0]).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()-x())
|
||||
type: 3, // SOLID_CROSS
|
||||
});
|
||||
|
||||
expect(actualMessages[1]).toMatchObject({
|
||||
from: 'Alice',
|
||||
to: 'Bob',
|
||||
centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()--x())
|
||||
type: 4, // DOTTED_CROSS
|
||||
});
|
||||
});
|
||||
|
||||
it('should not break existing parsing without central connections', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice ->> Bob: Normal message
|
||||
Bob -->> Alice: Normal dotted message
|
||||
Alice -x Bob: Normal cross message
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages).toHaveLength(3);
|
||||
|
||||
messages.forEach((msg) => {
|
||||
expect(msg.centralConnection).toBe(0); // No central connection
|
||||
});
|
||||
|
||||
expect(messages[0].type).toBe(0); // SOLID (based on actual parsing)
|
||||
expect(messages[1].type).toBe(1); // DOTTED (based on actual parsing)
|
||||
expect(messages[2].type).toBe(3); // SOLID_CROSS
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing a sequenceDiagram', function () {
|
||||
let diagram;
|
||||
beforeEach(async function () {
|
||||
@@ -2058,6 +2312,36 @@ Bob->>Alice:Got it!
|
||||
expect(messages[0].from).toBe('Alice');
|
||||
expect(messages[0].to).toBe('Bob');
|
||||
});
|
||||
|
||||
it('1 should parse ', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
actor Bob
|
||||
actor Alice
|
||||
Bob -|\\ Alice: Hello Alice, how are you?
|
||||
Bob -|/ Alice: Hello Alice, how are you?
|
||||
Bob -// Alice: Hello Alice, how are you?
|
||||
Bob -\\\\ Alice: Hello Alice, how are you?
|
||||
|
||||
Bob \\|- Alice: Hello Alice, how are you?
|
||||
Bob /|- Alice: Hello Alice, how are you?
|
||||
Bob //- Alice: Hello Alice, how are you?
|
||||
Bob \\\\- Alice: Hello Alice, how are you?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
});
|
||||
|
||||
it('2 should parse ', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
actor Bob
|
||||
actor Alice
|
||||
Alice ()<<->>() Bob: hey?
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
});
|
||||
describe('when parsing extended participant syntax', () => {
|
||||
it('should parse participants with different quote styles and whitespace', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
|
||||
@@ -282,6 +282,49 @@ const drawNote = async function (elem: any, noteModel: NoteModel) {
|
||||
bounds.models.addNote(noteModel);
|
||||
};
|
||||
|
||||
const drawCentralConnection = function (
|
||||
elem: any,
|
||||
msg: any,
|
||||
msgModel: any,
|
||||
diagObj: Diagram,
|
||||
startx: number,
|
||||
stopx: number,
|
||||
lineStartY: number
|
||||
) {
|
||||
const actors = diagObj.db.getActors();
|
||||
const fromActor = actors.get(msg.from);
|
||||
const toActor = actors.get(msg.to);
|
||||
const fromCenter = fromActor.x + fromActor.width / 2;
|
||||
const toCenter = toActor.x + toActor.width / 2;
|
||||
|
||||
const g = elem.append('g');
|
||||
|
||||
const drawCircle = (cx: number) => {
|
||||
g.append('circle')
|
||||
.attr('cx', cx)
|
||||
.attr('cy', lineStartY)
|
||||
.attr('r', 5)
|
||||
.attr('width', 10)
|
||||
.attr('height', 10);
|
||||
};
|
||||
|
||||
const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } =
|
||||
diagObj.db.LINETYPE;
|
||||
|
||||
switch (msg.centralConnection) {
|
||||
case CENTRAL_CONNECTION:
|
||||
drawCircle(toCenter);
|
||||
break;
|
||||
case CENTRAL_CONNECTION_REVERSE:
|
||||
drawCircle(fromCenter);
|
||||
break;
|
||||
case CENTRAL_CONNECTION_DUAL:
|
||||
drawCircle(fromCenter);
|
||||
drawCircle(toCenter);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const messageFont = (cnf) => {
|
||||
return {
|
||||
fontFamily: cnf.messageFontFamily,
|
||||
@@ -367,7 +410,7 @@ async function boundMessage(_diagram, msgModel): Promise<number> {
|
||||
* @param lineStartY - The Y coordinate at which the message line starts
|
||||
* @param diagObj - The diagram object.
|
||||
*/
|
||||
const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram) {
|
||||
const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram, msg) {
|
||||
const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel;
|
||||
const textDims = utils.calculateTextDimensions(message, messageFont(conf));
|
||||
const textObj = svgDrawCommon.getTextObj();
|
||||
@@ -433,6 +476,9 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
line.attr('y1', lineStartY);
|
||||
line.attr('x2', stopx);
|
||||
line.attr('y2', lineStartY);
|
||||
if (hasCentralConnection(msg, diagObj)) {
|
||||
drawCentralConnection(diagram, msg, msgModel, diagObj, startx, stopx, lineStartY);
|
||||
}
|
||||
}
|
||||
// Make an SVG Container
|
||||
// Draw the line
|
||||
@@ -441,7 +487,15 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
type === diagObj.db.LINETYPE.DOTTED_CROSS ||
|
||||
type === diagObj.db.LINETYPE.DOTTED_POINT ||
|
||||
type === diagObj.db.LINETYPE.DOTTED_OPEN ||
|
||||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED
|
||||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.SOLID_TOP_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.STICK_TOP_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED
|
||||
) {
|
||||
line.style('stroke-dasharray', '3, 3');
|
||||
line.attr('class', 'messageLine1');
|
||||
@@ -457,6 +511,51 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
line.attr('stroke-width', 2);
|
||||
line.attr('stroke', 'none'); // handled by theme/css anyway
|
||||
line.style('fill', 'none'); // remove any fill colour
|
||||
|
||||
if (type === diagObj.db.LINETYPE.SOLID_TOP || type === diagObj.db.LINETYPE.SOLID_TOP_DOTTED) {
|
||||
line.attr('marker-end', 'url(' + url + '#solidTopArrowHead)');
|
||||
}
|
||||
if (
|
||||
type === diagObj.db.LINETYPE.SOLID_BOTTOM ||
|
||||
type === diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED
|
||||
) {
|
||||
line.attr('marker-end', 'url(' + url + '#solidBottomArrowHead)');
|
||||
}
|
||||
if (type === diagObj.db.LINETYPE.STICK_TOP || type === diagObj.db.LINETYPE.STICK_TOP_DOTTED) {
|
||||
line.attr('marker-end', 'url(' + url + '#stickTopArrowHead)');
|
||||
}
|
||||
if (
|
||||
type === diagObj.db.LINETYPE.STICK_BOTTOM ||
|
||||
type === diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED
|
||||
) {
|
||||
line.attr('marker-end', 'url(' + url + '#stickBottomArrowHead)');
|
||||
}
|
||||
|
||||
if (
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE ||
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED
|
||||
) {
|
||||
line.attr('marker-start', 'url(' + url + '#solidBottomArrowHead)');
|
||||
}
|
||||
if (
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE ||
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED
|
||||
) {
|
||||
line.attr('marker-start', 'url(' + url + '#solidTopArrowHead)');
|
||||
}
|
||||
if (
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE ||
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED
|
||||
) {
|
||||
line.attr('marker-start', 'url(' + url + '#stickBottomArrowHead)');
|
||||
}
|
||||
if (
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE ||
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED
|
||||
) {
|
||||
line.attr('marker-start', 'url(' + url + '#stickTopArrowHead)');
|
||||
}
|
||||
|
||||
if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) {
|
||||
line.attr('marker-end', 'url(' + url + '#arrowhead)');
|
||||
}
|
||||
@@ -481,7 +580,18 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID ||
|
||||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED;
|
||||
|
||||
if (isBidirectional) {
|
||||
const isReverseArrowType =
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE ||
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE ||
|
||||
type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE ||
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED ||
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE ||
|
||||
type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED;
|
||||
|
||||
let x = 0;
|
||||
if (isBidirectional || isReverseArrowType) {
|
||||
const SEQUENCE_NUMBER_RADIUS = 6;
|
||||
|
||||
if (startx < stopx) {
|
||||
@@ -489,6 +599,7 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
} else {
|
||||
line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS);
|
||||
}
|
||||
x = 3.5;
|
||||
}
|
||||
|
||||
diagram
|
||||
@@ -498,7 +609,8 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
.attr('x2', startx)
|
||||
.attr('y2', lineStartY)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('marker-start', 'url(' + url + '#sequencenumber)');
|
||||
.attr('marker-start', 'url(' + url + '#sequencenumber)')
|
||||
.attr('transform', `translate(-${x}, 0)`);
|
||||
|
||||
diagram
|
||||
.append('text')
|
||||
@@ -508,7 +620,8 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
.attr('font-size', '12px')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('class', 'sequenceNumber')
|
||||
.text(sequenceIndex);
|
||||
.text(sequenceIndex)
|
||||
.attr('transform', `translate(-${x}, 0)`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -857,6 +970,10 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
svgDraw.insertArrowCrossHead(diagram);
|
||||
svgDraw.insertArrowFilledHead(diagram);
|
||||
svgDraw.insertSequenceNumber(diagram);
|
||||
svgDraw.insertSolidTopArrowHead(diagram);
|
||||
svgDraw.insertSolidBottomArrowHead(diagram);
|
||||
svgDraw.insertStickTopArrowHead(diagram);
|
||||
svgDraw.insertStickBottomArrowHead(diagram);
|
||||
|
||||
/**
|
||||
* @param msg - The message to draw.
|
||||
@@ -897,6 +1014,12 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
case diagObj.db.LINETYPE.ACTIVE_START:
|
||||
bounds.newActivation(msg, diagram, actors);
|
||||
break;
|
||||
case diagObj.db.LINETYPE.CENTRAL_CONNECTION:
|
||||
bounds.newActivation(msg, diagram, actors);
|
||||
break;
|
||||
case diagObj.db.LINETYPE.CENTRAL_CONNECTION_REVERSE:
|
||||
bounds.newActivation(msg, diagram, actors);
|
||||
break;
|
||||
case diagObj.db.LINETYPE.ACTIVE_END:
|
||||
activeEnd(msg, bounds.getVerticalPos());
|
||||
break;
|
||||
@@ -1055,7 +1178,7 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
createdActors,
|
||||
destroyedActors
|
||||
);
|
||||
messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY });
|
||||
messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY, msg });
|
||||
bounds.models.addMessage(msgModel);
|
||||
} catch (e) {
|
||||
log.error('error while drawing message', e);
|
||||
@@ -1068,6 +1191,27 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
diagObj.db.LINETYPE.SOLID_OPEN,
|
||||
diagObj.db.LINETYPE.DOTTED_OPEN,
|
||||
diagObj.db.LINETYPE.SOLID,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_TOP,
|
||||
diagObj.db.LINETYPE.SOLID_BOTTOM,
|
||||
diagObj.db.LINETYPE.STICK_TOP,
|
||||
diagObj.db.LINETYPE.STICK_BOTTOM,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_TOP_DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_TOP_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,
|
||||
|
||||
diagObj.db.LINETYPE.DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_CROSS,
|
||||
diagObj.db.LINETYPE.DOTTED_CROSS,
|
||||
@@ -1087,7 +1231,7 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
await drawActors(diagram, actors, actorKeys, false);
|
||||
|
||||
for (const e of messagesToDraw) {
|
||||
await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj);
|
||||
await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj, e.msg);
|
||||
}
|
||||
if (conf.mirrorActors) {
|
||||
await drawActors(diagram, actors, actorKeys, true);
|
||||
@@ -1461,12 +1605,85 @@ const buildNoteModel = async function (msg, actors, diagObj) {
|
||||
return noteModel;
|
||||
};
|
||||
|
||||
// Central connection positioning constants
|
||||
const CENTRAL_CONNECTION_BASE_OFFSET = 4;
|
||||
const CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET = 6;
|
||||
|
||||
/**
|
||||
* Check if a message has central connection
|
||||
* @param msg - The message object
|
||||
* @param diagObj - The diagram object containing LINETYPE constants
|
||||
* @returns True if the message has any type of central connection
|
||||
*/
|
||||
const hasCentralConnection = function (msg, diagObj) {
|
||||
const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } =
|
||||
diagObj.db.LINETYPE;
|
||||
return [CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL].includes(
|
||||
msg.centralConnection
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the positioning offset for central connection arrows
|
||||
* @param msg - The message object
|
||||
* @param diagObj - The diagram object containing LINETYPE constants
|
||||
* @param isArrowToRight - Whether the arrow is pointing to the right
|
||||
* @returns The offset to apply to startx position
|
||||
*/
|
||||
const calculateCentralConnectionOffset = function (msg, diagObj, isArrowToRight) {
|
||||
const {
|
||||
CENTRAL_CONNECTION_REVERSE,
|
||||
CENTRAL_CONNECTION_DUAL,
|
||||
BIDIRECTIONAL_SOLID,
|
||||
BIDIRECTIONAL_DOTTED,
|
||||
} = diagObj.db.LINETYPE;
|
||||
|
||||
let offset = 0;
|
||||
|
||||
if (
|
||||
msg.centralConnection === CENTRAL_CONNECTION_REVERSE ||
|
||||
msg.centralConnection === CENTRAL_CONNECTION_DUAL
|
||||
) {
|
||||
offset += CENTRAL_CONNECTION_BASE_OFFSET;
|
||||
}
|
||||
|
||||
if (
|
||||
msg.centralConnection === CENTRAL_CONNECTION_DUAL &&
|
||||
(msg.type === BIDIRECTIONAL_SOLID || msg.type === BIDIRECTIONAL_DOTTED)
|
||||
) {
|
||||
offset += isArrowToRight ? 0 : -CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET;
|
||||
}
|
||||
|
||||
return offset;
|
||||
};
|
||||
|
||||
const buildMessageModel = function (msg, actors, diagObj) {
|
||||
if (
|
||||
![
|
||||
diagObj.db.LINETYPE.SOLID_OPEN,
|
||||
diagObj.db.LINETYPE.DOTTED_OPEN,
|
||||
diagObj.db.LINETYPE.SOLID,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_TOP,
|
||||
diagObj.db.LINETYPE.SOLID_BOTTOM,
|
||||
diagObj.db.LINETYPE.STICK_TOP,
|
||||
diagObj.db.LINETYPE.STICK_BOTTOM,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_TOP_DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_TOP_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,
|
||||
|
||||
diagObj.db.LINETYPE.DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_CROSS,
|
||||
diagObj.db.LINETYPE.DOTTED_CROSS,
|
||||
@@ -1484,6 +1701,8 @@ const buildMessageModel = function (msg, actors, diagObj) {
|
||||
let startx = isArrowToRight ? fromRight : fromLeft;
|
||||
let stopx = isArrowToRight ? toLeft : toRight;
|
||||
|
||||
// Apply central connection positioning adjustments
|
||||
startx += calculateCentralConnectionOffset(msg, diagObj, isArrowToRight);
|
||||
// As the line width is considered, the left and right values will be off by 2.
|
||||
const isArrowToActivation = Math.abs(toLeft - toRight) > 2;
|
||||
|
||||
@@ -1517,7 +1736,30 @@ const buildMessageModel = function (msg, actors, diagObj) {
|
||||
* Shorten the length of arrow at the end and move the marker forward (using refX) to have a clean arrowhead
|
||||
* This is not required for open arrows that don't have arrowheads
|
||||
*/
|
||||
if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) {
|
||||
if (
|
||||
![
|
||||
diagObj.db.LINETYPE.SOLID_OPEN,
|
||||
diagObj.db.LINETYPE.DOTTED_OPEN,
|
||||
|
||||
diagObj.db.LINETYPE.STICK_TOP,
|
||||
diagObj.db.LINETYPE.STICK_BOTTOM,
|
||||
|
||||
diagObj.db.LINETYPE.STICK_TOP_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
|
||||
|
||||
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,
|
||||
|
||||
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,
|
||||
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,
|
||||
].includes(msg.type)
|
||||
) {
|
||||
stopx += adjustValue(3);
|
||||
}
|
||||
|
||||
@@ -1525,9 +1767,14 @@ const buildMessageModel = function (msg, actors, diagObj) {
|
||||
* Shorten start position of bidirectional arrow to accommodate for second arrowhead
|
||||
*/
|
||||
if (
|
||||
[diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes(
|
||||
msg.type
|
||||
)
|
||||
[
|
||||
diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID,
|
||||
diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,
|
||||
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,
|
||||
].includes(msg.type)
|
||||
) {
|
||||
startx -= adjustValue(3);
|
||||
}
|
||||
|
||||
@@ -1709,6 +1709,77 @@ const _drawMenuItemTextCandidateFunc = (function () {
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Setup arrow head and define the marker. The result is appended to the svg.
|
||||
*
|
||||
* @param elem
|
||||
*/
|
||||
export const insertSolidTopArrowHead = function (elem) {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'solidTopArrowHead')
|
||||
.attr('refX', 7.9)
|
||||
.attr('refY', 7.25)
|
||||
.attr('markerUnits', 'userSpaceOnUse')
|
||||
.attr('markerWidth', 12)
|
||||
.attr('markerHeight', 12)
|
||||
.attr('orient', 'auto-start-reverse')
|
||||
.append('path')
|
||||
.attr('d', 'M 0 0 L 10 8 L 0 8 z'); // this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
export const insertSolidBottomArrowHead = function (elem) {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'solidBottomArrowHead')
|
||||
.attr('refX', 7.9)
|
||||
.attr('refY', 0.75)
|
||||
.attr('markerUnits', 'userSpaceOnUse')
|
||||
.attr('markerWidth', 12)
|
||||
.attr('markerHeight', 12)
|
||||
.attr('orient', 'auto-start-reverse')
|
||||
.append('path')
|
||||
.attr('d', 'M 0 0 L 10 0 L 0 8 z');
|
||||
};
|
||||
|
||||
export const insertStickTopArrowHead = function (elem) {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'stickTopArrowHead')
|
||||
.attr('refX', 7.5)
|
||||
.attr('refY', 7)
|
||||
.attr('markerUnits', 'userSpaceOnUse')
|
||||
.attr('markerWidth', 12)
|
||||
.attr('markerHeight', 12)
|
||||
.attr('orient', 'auto-start-reverse')
|
||||
.append('path')
|
||||
.attr('d', 'M 0 0 L 7 7')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('fill', 'none');
|
||||
};
|
||||
|
||||
export const insertStickBottomArrowHead = function (elem) {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'stickBottomArrowHead')
|
||||
.attr('refX', 7.5)
|
||||
.attr('refY', 0)
|
||||
.attr('markerUnits', 'userSpaceOnUse')
|
||||
.attr('markerWidth', 12)
|
||||
.attr('markerHeight', 12)
|
||||
.attr('orient', 'auto-start-reverse')
|
||||
.append('path')
|
||||
.attr('d', 'M 0 7 L 7 0')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('fill', 'none');
|
||||
};
|
||||
|
||||
export default {
|
||||
drawRect,
|
||||
drawText,
|
||||
@@ -1731,4 +1802,8 @@ export default {
|
||||
getNoteRect,
|
||||
fixLifeLineHeights,
|
||||
sanitizeUrl,
|
||||
insertSolidTopArrowHead,
|
||||
insertSolidBottomArrowHead,
|
||||
insertStickTopArrowHead,
|
||||
insertStickBottomArrowHead,
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface Message {
|
||||
type?: number;
|
||||
activate?: boolean;
|
||||
placement?: string;
|
||||
centralConnection?: number;
|
||||
}
|
||||
|
||||
export interface AddMessageParams {
|
||||
@@ -50,6 +51,8 @@ export interface AddMessageParams {
|
||||
| 'destroyParticipant'
|
||||
| 'activeStart'
|
||||
| 'activeEnd'
|
||||
| 'centralConnection'
|
||||
| 'centralConnectionReverse'
|
||||
| 'addNote'
|
||||
| 'addLinks'
|
||||
| 'addALink'
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { BaseErrorListener } from 'antlr4ng';
|
||||
import type { RecognitionException, Recognizer } from 'antlr4ng';
|
||||
|
||||
/**
|
||||
* Custom error listener for ANTLR usecase parser
|
||||
* Captures syntax errors and provides detailed error messages
|
||||
*/
|
||||
export class UsecaseErrorListener extends BaseErrorListener {
|
||||
private errors: { line: number; column: number; message: string; offendingSymbol?: any }[] = [];
|
||||
|
||||
syntaxError(
|
||||
_recognizer: Recognizer<any>,
|
||||
offendingSymbol: any,
|
||||
line: number,
|
||||
charPositionInLine: number,
|
||||
message: string,
|
||||
_e: RecognitionException | null
|
||||
): void {
|
||||
this.errors.push({
|
||||
line,
|
||||
column: charPositionInLine,
|
||||
message,
|
||||
offendingSymbol,
|
||||
});
|
||||
}
|
||||
|
||||
reportAmbiguity(): void {
|
||||
// Optional: handle ambiguity reports
|
||||
}
|
||||
|
||||
reportAttemptingFullContext(): void {
|
||||
// Optional: handle full context attempts
|
||||
}
|
||||
|
||||
reportContextSensitivity(): void {
|
||||
// Optional: handle context sensitivity reports
|
||||
}
|
||||
|
||||
getErrors(): { line: number; column: number; message: string; offendingSymbol?: any }[] {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
return this.errors.length > 0;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.errors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detailed error with JISON-compatible hash property
|
||||
*/
|
||||
createDetailedError(): Error {
|
||||
if (this.errors.length === 0) {
|
||||
return new Error('Unknown parsing error');
|
||||
}
|
||||
|
||||
const firstError = this.errors[0];
|
||||
const message = `Parse error on line ${firstError.line}: ${firstError.message}`;
|
||||
const error = new Error(message);
|
||||
|
||||
// Add hash property for JISON compatibility
|
||||
Object.assign(error, {
|
||||
hash: {
|
||||
line: firstError.line,
|
||||
loc: {
|
||||
first_line: firstError.line,
|
||||
last_line: firstError.line,
|
||||
first_column: firstError.column,
|
||||
last_column: firstError.column,
|
||||
},
|
||||
text: firstError.offendingSymbol?.text ?? '',
|
||||
token: firstError.offendingSymbol?.text ?? '',
|
||||
expected: [],
|
||||
},
|
||||
});
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all error messages as a single string
|
||||
*/
|
||||
getErrorMessages(): string {
|
||||
return this.errors
|
||||
.map((error) => `Line ${error.line}:${error.column} - ${error.message}`)
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
lexer grammar UsecaseLexer;
|
||||
|
||||
// Keywords
|
||||
ACTOR: 'actor';
|
||||
SYSTEM_BOUNDARY: 'systemBoundary';
|
||||
END: 'end';
|
||||
DIRECTION: 'direction';
|
||||
CLASS_DEF: 'classDef';
|
||||
CLASS: 'class';
|
||||
STYLE: 'style';
|
||||
USECASE: 'usecase';
|
||||
|
||||
// Direction keywords
|
||||
TB: 'TB';
|
||||
TD: 'TD';
|
||||
BT: 'BT';
|
||||
RL: 'RL';
|
||||
LR: 'LR';
|
||||
|
||||
// System boundary types
|
||||
PACKAGE: 'package';
|
||||
RECT: 'rect';
|
||||
TYPE: 'type';
|
||||
|
||||
// Arrow types (order matters - longer patterns first)
|
||||
SOLID_ARROW: '-->';
|
||||
BACK_ARROW: '<--';
|
||||
CIRCLE_ARROW: '--o';
|
||||
CIRCLE_ARROW_REVERSED: 'o--';
|
||||
CROSS_ARROW: '--x';
|
||||
CROSS_ARROW_REVERSED: 'x--';
|
||||
LINE_SOLID: '--';
|
||||
|
||||
// Symbols
|
||||
COMMA: ',';
|
||||
AT: '@';
|
||||
LBRACE: '{';
|
||||
RBRACE: '}';
|
||||
COLON: ':';
|
||||
LPAREN: '(';
|
||||
RPAREN: ')';
|
||||
CLASS_SEPARATOR: ':::';
|
||||
|
||||
// Hash color (must come before HASH to avoid conflicts)
|
||||
HASH_COLOR: '#' [a-fA-F0-9]+;
|
||||
|
||||
// Number with optional unit
|
||||
NUMBER: [0-9]+ ('.' [0-9]+)? ([a-zA-Z]+)?;
|
||||
|
||||
// Identifier
|
||||
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*;
|
||||
|
||||
// String literals
|
||||
STRING: '"' (~["\r\n])* '"' | '\'' (~['\r\n])* '\'';
|
||||
|
||||
// These tokens are defined last so they have lowest priority
|
||||
// This ensures arrow tokens like '-->' are matched before DASH
|
||||
DASH: '-';
|
||||
DOT: '.';
|
||||
PERCENT: '%';
|
||||
|
||||
// Whitespace and newlines
|
||||
NEWLINE: [\r\n]+;
|
||||
WS: [ \t]+ -> skip;
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
import type { ParseTreeListener } from 'antlr4ng';
|
||||
import { UsecaseParserCore } from './UsecaseParserCore.js';
|
||||
import { log } from '../../../../logger.js';
|
||||
import type { UsecaseDB } from '../../usecaseTypes.js';
|
||||
|
||||
/**
|
||||
* Listener implementation that builds the usecase model
|
||||
* Extends the core logic to ensure consistency with Visitor pattern
|
||||
*/
|
||||
export class UsecaseListener extends UsecaseParserCore implements ParseTreeListener {
|
||||
constructor(db: UsecaseDB) {
|
||||
super(db);
|
||||
log.debug('👂 UsecaseListener: Constructor called');
|
||||
}
|
||||
|
||||
// Standard ParseTreeListener methods
|
||||
enterEveryRule = (ctx: any) => {
|
||||
if (this.getEnvVar('ANTLR_DEBUG') === 'true') {
|
||||
const ruleName = ctx.constructor.name;
|
||||
log.debug('🔍 UsecaseListener: Entering rule:', ruleName);
|
||||
}
|
||||
};
|
||||
|
||||
exitEveryRule = (ctx: any) => {
|
||||
if (this.getEnvVar('ANTLR_DEBUG') === 'true') {
|
||||
const ruleName = ctx.constructor.name;
|
||||
log.debug('🔍 UsecaseListener: Exiting rule:', ruleName);
|
||||
}
|
||||
};
|
||||
|
||||
visitTerminal = (_node: any) => {
|
||||
// Optional: Handle terminal nodes
|
||||
};
|
||||
|
||||
visitErrorNode = (_node: any) => {
|
||||
log.debug('❌ UsecaseListener: Error node encountered');
|
||||
};
|
||||
|
||||
// Actor statement
|
||||
exitActorName = (ctx: any) => {
|
||||
let actorName = '';
|
||||
|
||||
if (ctx.IDENTIFIER()) {
|
||||
actorName = ctx.IDENTIFIER().getText();
|
||||
} else if (ctx.STRING()) {
|
||||
actorName = this.extractString(ctx.STRING().getText());
|
||||
}
|
||||
|
||||
const actorId = this.generateId(actorName);
|
||||
|
||||
// Process metadata if present
|
||||
let metadata: Record<string, string> | undefined;
|
||||
if (ctx.metadata()) {
|
||||
metadata = this.extractMetadata(ctx.metadata());
|
||||
}
|
||||
|
||||
this.processActorStatement(actorId, actorName, metadata);
|
||||
};
|
||||
|
||||
// Relationship statement
|
||||
exitRelationshipStatement = (ctx: any) => {
|
||||
let from = '';
|
||||
let to = '';
|
||||
let arrowType = 0;
|
||||
let label: string | undefined;
|
||||
|
||||
// Get entity names
|
||||
const entityNames = ctx.entityName();
|
||||
if (entityNames && entityNames.length >= 2) {
|
||||
from = this.extractEntityName(entityNames[0]);
|
||||
to = this.extractEntityName(entityNames[1]);
|
||||
} else if (ctx.actorDeclaration()) {
|
||||
from = this.extractActorDeclaration(ctx.actorDeclaration());
|
||||
if (entityNames && entityNames.length >= 1) {
|
||||
to = this.extractEntityName(entityNames[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get arrow type
|
||||
const arrow = ctx.arrow();
|
||||
if (arrow) {
|
||||
const arrowResult = this.extractArrow(arrow);
|
||||
arrowType = arrowResult.type;
|
||||
label = arrowResult.label;
|
||||
}
|
||||
|
||||
this.processRelationship(from, to, arrowType, label);
|
||||
};
|
||||
|
||||
// System boundary statement
|
||||
enterSystemBoundaryStatement = (ctx: any) => {
|
||||
const boundaryName = ctx.systemBoundaryName();
|
||||
let boundaryId = '';
|
||||
let boundaryNameText = '';
|
||||
|
||||
if (boundaryName) {
|
||||
if (boundaryName.IDENTIFIER()) {
|
||||
boundaryNameText = boundaryName.IDENTIFIER().getText();
|
||||
} else if (boundaryName.STRING()) {
|
||||
boundaryNameText = this.extractString(boundaryName.STRING().getText());
|
||||
}
|
||||
boundaryId = this.generateId(boundaryNameText);
|
||||
}
|
||||
|
||||
this.processSystemBoundaryStart(boundaryId, boundaryNameText);
|
||||
};
|
||||
|
||||
exitSystemBoundaryStatement = (_ctx: any) => {
|
||||
this.processSystemBoundaryEnd();
|
||||
};
|
||||
|
||||
exitUsecaseInBoundary = (ctx: any) => {
|
||||
let useCaseId = '';
|
||||
let useCaseName = '';
|
||||
let classes: string[] | undefined;
|
||||
|
||||
if (ctx.usecaseWithClass()) {
|
||||
const withClass = ctx.usecaseWithClass();
|
||||
if (withClass.IDENTIFIER()) {
|
||||
const identifiers = withClass.IDENTIFIER();
|
||||
if (Array.isArray(identifiers) && identifiers.length >= 2) {
|
||||
useCaseId = identifiers[0].getText();
|
||||
useCaseName = useCaseId;
|
||||
classes = [identifiers[1].getText()];
|
||||
}
|
||||
} else if (withClass.STRING()) {
|
||||
useCaseName = this.extractString(withClass.STRING().getText());
|
||||
useCaseId = this.generateId(useCaseName);
|
||||
const identifiers = withClass.IDENTIFIER();
|
||||
if (identifiers) {
|
||||
classes = [identifiers.getText()];
|
||||
}
|
||||
}
|
||||
} else if (ctx.IDENTIFIER()) {
|
||||
useCaseId = ctx.IDENTIFIER().getText();
|
||||
useCaseName = useCaseId;
|
||||
} else if (ctx.STRING()) {
|
||||
useCaseName = this.extractString(ctx.STRING().getText());
|
||||
useCaseId = this.generateId(useCaseName);
|
||||
}
|
||||
|
||||
if (useCaseId && useCaseName) {
|
||||
this.processUseCaseStatement(useCaseId, useCaseName, undefined, classes);
|
||||
}
|
||||
};
|
||||
|
||||
// System boundary type statement
|
||||
exitSystemBoundaryTypeStatement = (ctx: any) => {
|
||||
const boundaryName = ctx.systemBoundaryName();
|
||||
let boundaryId = '';
|
||||
|
||||
if (boundaryName) {
|
||||
if (boundaryName.IDENTIFIER()) {
|
||||
boundaryId = boundaryName.IDENTIFIER().getText();
|
||||
} else if (boundaryName.STRING()) {
|
||||
boundaryId = this.generateId(this.extractString(boundaryName.STRING().getText()));
|
||||
}
|
||||
}
|
||||
|
||||
const typeContent = ctx.systemBoundaryTypeContent();
|
||||
if (typeContent) {
|
||||
const properties = typeContent.systemBoundaryTypeProperty();
|
||||
const props = Array.isArray(properties) ? properties : [properties];
|
||||
|
||||
for (const prop of props) {
|
||||
const type = prop.systemBoundaryType();
|
||||
if (type) {
|
||||
let typeValue: 'package' | 'rect' = 'rect';
|
||||
if (type.PACKAGE()) {
|
||||
typeValue = 'package';
|
||||
} else if (type.RECT()) {
|
||||
typeValue = 'rect';
|
||||
}
|
||||
this.processSystemBoundaryType(boundaryId, typeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Direction statement
|
||||
exitDirectionStatement = (ctx: any) => {
|
||||
const direction = ctx.direction();
|
||||
if (direction) {
|
||||
let directionText = '';
|
||||
if (direction.TB()) {
|
||||
directionText = 'TB';
|
||||
} else if (direction.TD()) {
|
||||
directionText = 'TD';
|
||||
} else if (direction.BT()) {
|
||||
directionText = 'BT';
|
||||
} else if (direction.RL()) {
|
||||
directionText = 'RL';
|
||||
} else if (direction.LR()) {
|
||||
directionText = 'LR';
|
||||
}
|
||||
this.processDirectionStatement(directionText);
|
||||
}
|
||||
};
|
||||
|
||||
// Class definition statement
|
||||
exitClassDefStatement = (ctx: any) => {
|
||||
let classId = '';
|
||||
if (ctx.IDENTIFIER()) {
|
||||
classId = ctx.IDENTIFIER().getText();
|
||||
}
|
||||
|
||||
const styles: string[] = [];
|
||||
const stylesOpt = ctx.stylesOpt();
|
||||
if (stylesOpt) {
|
||||
this.collectStyles(stylesOpt, styles);
|
||||
}
|
||||
|
||||
this.processClassDefStatement(classId, styles);
|
||||
};
|
||||
|
||||
// Class statement
|
||||
exitClassStatement = (ctx: any) => {
|
||||
const nodeList = ctx.nodeList();
|
||||
const nodeIds: string[] = [];
|
||||
|
||||
if (nodeList) {
|
||||
const identifiers = nodeList.IDENTIFIER();
|
||||
const ids = Array.isArray(identifiers) ? identifiers : [identifiers];
|
||||
for (const id of ids) {
|
||||
nodeIds.push(id.getText());
|
||||
}
|
||||
}
|
||||
|
||||
let classId = '';
|
||||
const identifiers = ctx.IDENTIFIER();
|
||||
if (identifiers) {
|
||||
const ids = Array.isArray(identifiers) ? identifiers : [identifiers];
|
||||
if (ids.length > 0) {
|
||||
classId = ids[ids.length - 1].getText();
|
||||
}
|
||||
}
|
||||
|
||||
this.processClassStatement(nodeIds, classId);
|
||||
};
|
||||
|
||||
// Style statement
|
||||
exitStyleStatement = (ctx: any) => {
|
||||
let nodeId = '';
|
||||
if (ctx.IDENTIFIER()) {
|
||||
nodeId = ctx.IDENTIFIER().getText();
|
||||
}
|
||||
|
||||
const styles: string[] = [];
|
||||
const stylesOpt = ctx.stylesOpt();
|
||||
if (stylesOpt) {
|
||||
this.collectStyles(stylesOpt, styles);
|
||||
}
|
||||
|
||||
this.processStyleStatement(nodeId, styles);
|
||||
};
|
||||
|
||||
// Usecase statement
|
||||
exitUsecaseStatement = (ctx: any) => {
|
||||
const entityName = ctx.entityName();
|
||||
if (entityName) {
|
||||
const useCaseId = this.extractEntityName(entityName);
|
||||
this.processUseCaseStatement(useCaseId, useCaseId);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper methods
|
||||
private extractMetadata(ctx: any): Record<string, string> {
|
||||
const metadata: Record<string, string> = {};
|
||||
const content = ctx.metadataContent();
|
||||
if (content) {
|
||||
const properties = content.metadataProperty();
|
||||
const props = Array.isArray(properties) ? properties : [properties];
|
||||
|
||||
for (const prop of props) {
|
||||
const strings = prop.STRING();
|
||||
if (strings && strings.length >= 2) {
|
||||
const key = this.extractString(strings[0].getText());
|
||||
const value = this.extractString(strings[1].getText());
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private extractEntityName(ctx: any): string {
|
||||
if (ctx.nodeIdWithLabel()) {
|
||||
const nodeId = ctx.nodeIdWithLabel();
|
||||
if (nodeId.IDENTIFIER()) {
|
||||
return nodeId.IDENTIFIER().getText();
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.IDENTIFIER()) {
|
||||
const identifiers = ctx.IDENTIFIER();
|
||||
if (Array.isArray(identifiers) && identifiers.length >= 2) {
|
||||
return identifiers[0].getText();
|
||||
}
|
||||
return identifiers.getText ? identifiers.getText() : identifiers[0].getText();
|
||||
}
|
||||
|
||||
if (ctx.STRING()) {
|
||||
const strings = ctx.STRING();
|
||||
const text = strings.getText ? strings.getText() : strings[0].getText();
|
||||
return this.extractString(text);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private extractActorDeclaration(ctx: any): string {
|
||||
const actorName = ctx.actorName();
|
||||
if (actorName) {
|
||||
if (actorName.IDENTIFIER()) {
|
||||
return actorName.IDENTIFIER().getText();
|
||||
} else if (actorName.STRING()) {
|
||||
return this.extractString(actorName.STRING().getText());
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private extractArrow(ctx: any): { type: number; label?: string } {
|
||||
let arrowText = '';
|
||||
let label: string | undefined;
|
||||
|
||||
if (ctx.labeledArrow()) {
|
||||
const labeledArrow = ctx.labeledArrow();
|
||||
const edgeLabel = labeledArrow.edgeLabel();
|
||||
if (edgeLabel) {
|
||||
if (edgeLabel.IDENTIFIER()) {
|
||||
label = edgeLabel.IDENTIFIER().getText();
|
||||
} else if (edgeLabel.STRING()) {
|
||||
label = this.extractString(edgeLabel.STRING().getText());
|
||||
}
|
||||
}
|
||||
|
||||
if (labeledArrow.SOLID_ARROW()) {
|
||||
arrowText = '-->';
|
||||
} else if (labeledArrow.BACK_ARROW()) {
|
||||
arrowText = '<--';
|
||||
} else if (labeledArrow.CIRCLE_ARROW()) {
|
||||
arrowText = '--o';
|
||||
} else if (labeledArrow.CROSS_ARROW()) {
|
||||
arrowText = '--x';
|
||||
} else if (labeledArrow.CIRCLE_ARROW_REVERSED()) {
|
||||
arrowText = 'o--';
|
||||
} else if (labeledArrow.CROSS_ARROW_REVERSED()) {
|
||||
arrowText = 'x--';
|
||||
} else {
|
||||
arrowText = '--';
|
||||
}
|
||||
} else {
|
||||
if (ctx.SOLID_ARROW()) {
|
||||
arrowText = '-->';
|
||||
} else if (ctx.BACK_ARROW()) {
|
||||
arrowText = '<--';
|
||||
} else if (ctx.LINE_SOLID()) {
|
||||
arrowText = '--';
|
||||
} else if (ctx.CIRCLE_ARROW()) {
|
||||
arrowText = '--o';
|
||||
} else if (ctx.CROSS_ARROW()) {
|
||||
arrowText = '--x';
|
||||
} else if (ctx.CIRCLE_ARROW_REVERSED()) {
|
||||
arrowText = 'o--';
|
||||
} else if (ctx.CROSS_ARROW_REVERSED()) {
|
||||
arrowText = 'x--';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: this.parseArrowType(arrowText),
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
private collectStyles(ctx: any, styles: string[]): void {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styleComponents = this.getAllStyleComponents(ctx);
|
||||
for (const component of styleComponents) {
|
||||
styles.push(component.getText());
|
||||
}
|
||||
}
|
||||
|
||||
private getAllStyleComponents(ctx: any): any[] {
|
||||
const components: any[] = [];
|
||||
|
||||
if (ctx.style) {
|
||||
const styleCtx = ctx.style();
|
||||
if (styleCtx) {
|
||||
this.collectStyleComponents(styleCtx, components);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.stylesOpt) {
|
||||
const stylesOptList = Array.isArray(ctx.stylesOpt()) ? ctx.stylesOpt() : [ctx.stylesOpt()];
|
||||
for (const opt of stylesOptList) {
|
||||
if (opt) {
|
||||
this.collectStyleComponents(opt, components);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
private collectStyleComponents(ctx: any, components: any[]): void {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.styleComponent) {
|
||||
const comp = ctx.styleComponent();
|
||||
if (comp) {
|
||||
components.push(comp);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.style) {
|
||||
const styleCtx = ctx.style();
|
||||
if (styleCtx) {
|
||||
this.collectStyleComponents(styleCtx, components);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
parser grammar UsecaseParser;
|
||||
|
||||
options {
|
||||
tokenVocab = UsecaseLexer;
|
||||
}
|
||||
|
||||
// Entry point
|
||||
start: USECASE NEWLINE* statement* EOF;
|
||||
|
||||
// Statement types
|
||||
statement
|
||||
: actorStatement
|
||||
| relationshipStatement
|
||||
| systemBoundaryStatement
|
||||
| systemBoundaryTypeStatement
|
||||
| directionStatement
|
||||
| classDefStatement
|
||||
| classStatement
|
||||
| styleStatement
|
||||
| usecaseStatement
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
// Usecase statement (standalone entity)
|
||||
usecaseStatement
|
||||
: entityName NEWLINE*
|
||||
;
|
||||
|
||||
// Actor statement
|
||||
actorStatement
|
||||
: ACTOR actorList NEWLINE*
|
||||
;
|
||||
|
||||
actorList
|
||||
: actorName (COMMA actorName)*
|
||||
;
|
||||
|
||||
actorName
|
||||
: (IDENTIFIER | STRING) metadata?
|
||||
;
|
||||
|
||||
metadata
|
||||
: AT LBRACE metadataContent RBRACE
|
||||
;
|
||||
|
||||
metadataContent
|
||||
: metadataProperty (COMMA metadataProperty)*
|
||||
;
|
||||
|
||||
metadataProperty
|
||||
: STRING COLON STRING
|
||||
;
|
||||
|
||||
// Relationship statement
|
||||
relationshipStatement
|
||||
: entityName arrow entityName NEWLINE*
|
||||
| actorDeclaration arrow entityName NEWLINE*
|
||||
;
|
||||
|
||||
// System boundary statement
|
||||
systemBoundaryStatement
|
||||
: SYSTEM_BOUNDARY 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
|
||||
;
|
||||
|
||||
// System boundary type statement
|
||||
systemBoundaryTypeStatement
|
||||
: systemBoundaryName AT LBRACE systemBoundaryTypeContent RBRACE NEWLINE*
|
||||
;
|
||||
|
||||
systemBoundaryTypeContent
|
||||
: systemBoundaryTypeProperty (COMMA systemBoundaryTypeProperty)*
|
||||
;
|
||||
|
||||
systemBoundaryTypeProperty
|
||||
: TYPE COLON systemBoundaryType
|
||||
;
|
||||
|
||||
systemBoundaryType
|
||||
: PACKAGE
|
||||
| RECT
|
||||
;
|
||||
|
||||
// Entity name (node reference)
|
||||
entityName
|
||||
: IDENTIFIER CLASS_SEPARATOR IDENTIFIER
|
||||
| STRING CLASS_SEPARATOR IDENTIFIER
|
||||
| IDENTIFIER
|
||||
| STRING
|
||||
| nodeIdWithLabel
|
||||
;
|
||||
|
||||
// Actor declaration (inline)
|
||||
actorDeclaration
|
||||
: ACTOR actorName
|
||||
;
|
||||
|
||||
// Node with label
|
||||
nodeIdWithLabel
|
||||
: IDENTIFIER LPAREN nodeLabel RPAREN
|
||||
;
|
||||
|
||||
nodeLabel
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
| nodeLabel IDENTIFIER
|
||||
| nodeLabel STRING
|
||||
;
|
||||
|
||||
// Arrow types
|
||||
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
|
||||
;
|
||||
|
||||
// Direction statement
|
||||
directionStatement
|
||||
: DIRECTION direction NEWLINE*
|
||||
;
|
||||
|
||||
direction
|
||||
: TB
|
||||
| TD
|
||||
| BT
|
||||
| RL
|
||||
| LR
|
||||
;
|
||||
|
||||
// Class definition statement
|
||||
classDefStatement
|
||||
: CLASS_DEF IDENTIFIER stylesOpt NEWLINE*
|
||||
;
|
||||
|
||||
stylesOpt
|
||||
: style
|
||||
| stylesOpt COMMA style
|
||||
;
|
||||
|
||||
style
|
||||
: styleComponent
|
||||
| style styleComponent
|
||||
;
|
||||
|
||||
styleComponent
|
||||
: IDENTIFIER
|
||||
| NUMBER
|
||||
| HASH_COLOR
|
||||
| COLON
|
||||
| STRING
|
||||
| DASH
|
||||
| DOT
|
||||
| PERCENT
|
||||
;
|
||||
|
||||
// Class statement
|
||||
classStatement
|
||||
: CLASS nodeList IDENTIFIER NEWLINE*
|
||||
;
|
||||
|
||||
// Style statement
|
||||
styleStatement
|
||||
: STYLE IDENTIFIER stylesOpt NEWLINE*
|
||||
;
|
||||
|
||||
// Node list
|
||||
nodeList
|
||||
: IDENTIFIER (COMMA IDENTIFIER)*
|
||||
;
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import type {
|
||||
UsecaseDB,
|
||||
Actor,
|
||||
UseCase,
|
||||
SystemBoundary,
|
||||
Relationship,
|
||||
ClassDef,
|
||||
ArrowType,
|
||||
} from '../../usecaseTypes.js';
|
||||
import { ARROW_TYPE } from '../../usecaseTypes.js';
|
||||
import { log } from '../../../../logger.js';
|
||||
|
||||
/**
|
||||
* Core shared logic for both Listener and Visitor patterns
|
||||
* Contains all the proven parsing logic for usecase diagrams
|
||||
*/
|
||||
export class UsecaseParserCore {
|
||||
protected db: UsecaseDB;
|
||||
protected relationshipCounter = 0;
|
||||
protected currentSystemBoundary: string | null = null;
|
||||
protected currentSystemBoundaryUseCases: string[] = [];
|
||||
|
||||
constructor(db: UsecaseDB) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser-safe environment variable access
|
||||
*/
|
||||
protected getEnvVar(name: string): string | undefined {
|
||||
try {
|
||||
if (typeof process !== 'undefined' && process.env) {
|
||||
return process.env[name];
|
||||
}
|
||||
} catch (_e) {
|
||||
// process is not defined in browser
|
||||
}
|
||||
|
||||
// Browser fallback
|
||||
if (typeof window !== 'undefined' && (window as any).MERMAID_CONFIG) {
|
||||
return (window as any).MERMAID_CONFIG[name];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process actor statement
|
||||
*/
|
||||
protected processActorStatement(
|
||||
actorId: string,
|
||||
actorName: string,
|
||||
metadata?: Record<string, string>
|
||||
): void {
|
||||
const actor: Actor = {
|
||||
id: actorId,
|
||||
name: actorName,
|
||||
metadata,
|
||||
};
|
||||
|
||||
this.db.addActor(actor);
|
||||
log.debug(`Processed actor: ${actorId} (${actorName})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process use case statement
|
||||
*/
|
||||
protected processUseCaseStatement(
|
||||
useCaseId: string,
|
||||
useCaseName: string,
|
||||
nodeId?: string,
|
||||
classes?: string[]
|
||||
): void {
|
||||
const useCase: UseCase = {
|
||||
id: useCaseId,
|
||||
name: useCaseName,
|
||||
nodeId,
|
||||
classes,
|
||||
systemBoundary: this.currentSystemBoundary ?? undefined,
|
||||
};
|
||||
|
||||
this.db.addUseCase(useCase);
|
||||
|
||||
// Add to current system boundary if we're inside one
|
||||
if (this.currentSystemBoundary) {
|
||||
this.currentSystemBoundaryUseCases.push(useCaseId);
|
||||
}
|
||||
|
||||
log.debug(`Processed use case: ${useCaseId} (${useCaseName})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process relationship statement
|
||||
*/
|
||||
protected processRelationship(from: string, to: string, arrowType: number, label?: string): void {
|
||||
// Generate IDs for checking if entities exist
|
||||
const fromId = this.generateId(from);
|
||||
const toId = this.generateId(to);
|
||||
|
||||
// Ensure entities exist - if they're not actors, create them as use cases
|
||||
if (!this.db.getActor(fromId) && !this.db.getUseCase(fromId)) {
|
||||
this.db.addUseCase({ id: fromId, name: from });
|
||||
log.debug(`Auto-created use case: ${fromId} (${from})`);
|
||||
}
|
||||
if (!this.db.getActor(toId) && !this.db.getUseCase(toId)) {
|
||||
this.db.addUseCase({ id: toId, name: to });
|
||||
log.debug(`Auto-created use case: ${toId} (${to})`);
|
||||
}
|
||||
|
||||
const relationshipId = `rel_${this.relationshipCounter++}`;
|
||||
|
||||
// Determine relationship type based on arrow type and label
|
||||
let type: 'association' | 'include' | 'extend' = 'association';
|
||||
if (label) {
|
||||
const lowerLabel = label.toLowerCase();
|
||||
if (lowerLabel.includes('include')) {
|
||||
type = 'include';
|
||||
} else if (lowerLabel.includes('extend')) {
|
||||
type = 'extend';
|
||||
}
|
||||
}
|
||||
|
||||
const relationship: Relationship = {
|
||||
id: relationshipId,
|
||||
from: fromId,
|
||||
to: toId,
|
||||
type,
|
||||
arrowType: arrowType as ArrowType,
|
||||
label,
|
||||
};
|
||||
|
||||
this.db.addRelationship(relationship);
|
||||
log.debug(`Processed relationship: ${fromId} -> ${toId} (${type})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process system boundary start
|
||||
*/
|
||||
protected processSystemBoundaryStart(boundaryId: string, boundaryName: string): void {
|
||||
this.currentSystemBoundary = boundaryId;
|
||||
this.currentSystemBoundaryUseCases = [];
|
||||
log.debug(`Started system boundary: ${boundaryId} (${boundaryName})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process system boundary end
|
||||
*/
|
||||
protected processSystemBoundaryEnd(): void {
|
||||
if (this.currentSystemBoundary) {
|
||||
const systemBoundary: SystemBoundary = {
|
||||
id: this.currentSystemBoundary,
|
||||
name: this.currentSystemBoundary,
|
||||
useCases: [...this.currentSystemBoundaryUseCases],
|
||||
};
|
||||
|
||||
this.db.addSystemBoundary(systemBoundary);
|
||||
log.debug(`Ended system boundary: ${this.currentSystemBoundary}`);
|
||||
|
||||
this.currentSystemBoundary = null;
|
||||
this.currentSystemBoundaryUseCases = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process system boundary type
|
||||
*/
|
||||
protected processSystemBoundaryType(boundaryId: string, type: 'package' | 'rect'): void {
|
||||
const boundary = this.db.getSystemBoundary(boundaryId);
|
||||
if (boundary) {
|
||||
boundary.type = type;
|
||||
log.debug(`Set system boundary type: ${boundaryId} -> ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process direction statement
|
||||
*/
|
||||
protected processDirectionStatement(direction: string): void {
|
||||
const normalizedDirection = this.normalizeDirection(direction);
|
||||
this.db.setDirection(normalizedDirection as any);
|
||||
log.debug(`Set direction: ${normalizedDirection}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize direction
|
||||
*/
|
||||
protected normalizeDirection(dir: string): string {
|
||||
switch (dir) {
|
||||
case 'TD':
|
||||
return 'TB';
|
||||
default:
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process class definition statement
|
||||
*/
|
||||
protected processClassDefStatement(classId: string, styles: string[]): void {
|
||||
const classDef: ClassDef = {
|
||||
id: classId,
|
||||
styles,
|
||||
};
|
||||
|
||||
this.db.addClassDef(classDef);
|
||||
log.debug(`Processed class definition: ${classId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process class statement (apply class to nodes)
|
||||
*/
|
||||
protected processClassStatement(nodeIds: string[], classId: string): void {
|
||||
for (const nodeId of nodeIds) {
|
||||
const useCase = this.db.getUseCase(nodeId);
|
||||
if (useCase) {
|
||||
if (!useCase.classes) {
|
||||
useCase.classes = [];
|
||||
}
|
||||
if (!useCase.classes.includes(classId)) {
|
||||
useCase.classes.push(classId);
|
||||
}
|
||||
log.debug(`Applied class ${classId} to use case ${nodeId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process style statement (apply styles directly to node)
|
||||
*/
|
||||
protected processStyleStatement(nodeId: string, styles: string[]): void {
|
||||
const useCase = this.db.getUseCase(nodeId);
|
||||
if (useCase) {
|
||||
useCase.styles = styles;
|
||||
log.debug(`Applied styles to use case ${nodeId}`);
|
||||
}
|
||||
|
||||
const actor = this.db.getActor(nodeId);
|
||||
if (actor) {
|
||||
actor.styles = styles;
|
||||
log.debug(`Applied styles to actor ${nodeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text from string (remove quotes)
|
||||
*/
|
||||
protected extractString(text: string): string {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove surrounding quotes
|
||||
if (
|
||||
(text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))
|
||||
) {
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse arrow type from token
|
||||
*/
|
||||
protected parseArrowType(arrowText: string): number {
|
||||
switch (arrowText) {
|
||||
case '-->':
|
||||
return ARROW_TYPE.SOLID_ARROW;
|
||||
case '<--':
|
||||
return ARROW_TYPE.BACK_ARROW;
|
||||
case '--':
|
||||
return ARROW_TYPE.LINE_SOLID;
|
||||
case '--o':
|
||||
return ARROW_TYPE.CIRCLE_ARROW;
|
||||
case '--x':
|
||||
return ARROW_TYPE.CROSS_ARROW;
|
||||
case 'o--':
|
||||
return ARROW_TYPE.CIRCLE_ARROW_REVERSED;
|
||||
case 'x--':
|
||||
return ARROW_TYPE.CROSS_ARROW_REVERSED;
|
||||
default:
|
||||
return ARROW_TYPE.SOLID_ARROW;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique ID from name
|
||||
*/
|
||||
protected generateId(name: string): string {
|
||||
return name.replace(/\W/g, '_');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
import { UsecaseParserCore } from './UsecaseParserCore.js';
|
||||
import { log } from '../../../../logger.js';
|
||||
import type { UsecaseDB } from '../../usecaseTypes.js';
|
||||
|
||||
/**
|
||||
* Visitor implementation that builds the usecase model
|
||||
* Uses the same core logic as the Listener for consistency
|
||||
*/
|
||||
export class UsecaseVisitor extends UsecaseParserCore {
|
||||
private visitCount = 0;
|
||||
|
||||
constructor(db: UsecaseDB) {
|
||||
super(db);
|
||||
|
||||
if (this.getEnvVar('ANTLR_DEBUG') === 'true') {
|
||||
log.debug('🎯 UsecaseVisitor: Constructor called');
|
||||
}
|
||||
}
|
||||
|
||||
// Default visitor methods
|
||||
visit(tree: any): any {
|
||||
const shouldLog = this.getEnvVar('ANTLR_DEBUG') === 'true';
|
||||
|
||||
this.visitCount++;
|
||||
|
||||
if (shouldLog) {
|
||||
log.debug(`🔍 UsecaseVisitor: Visiting node type: ${tree.constructor.name}`);
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = tree.accept(this);
|
||||
if (shouldLog) {
|
||||
log.debug(`✅ UsecaseVisitor: Successfully visited ${tree.constructor.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`❌ UsecaseVisitor: Error visiting ${tree.constructor.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
visitChildren(node: any): any {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result = null;
|
||||
const n = node.getChildCount();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const child = node.getChild(i);
|
||||
if (child) {
|
||||
const childResult = child.accept(this);
|
||||
if (childResult !== null) {
|
||||
result = childResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
visitTerminal(_node: any): any {
|
||||
return null;
|
||||
}
|
||||
|
||||
visitErrorNode(_node: any): any {
|
||||
log.error('❌ UsecaseVisitor: Error node encountered');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Start rule
|
||||
visitStart(ctx: any): any {
|
||||
return this.visitChildren(ctx);
|
||||
}
|
||||
|
||||
// Statement rule
|
||||
visitStatement(ctx: any): any {
|
||||
return this.visitChildren(ctx);
|
||||
}
|
||||
|
||||
// Actor statement
|
||||
visitActorStatement(ctx: any): any {
|
||||
const actorList = ctx.actorList();
|
||||
if (actorList) {
|
||||
this.visitActorList(actorList);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
visitActorList(ctx: any): any {
|
||||
const actorNames = ctx.actorName();
|
||||
if (actorNames) {
|
||||
const names = Array.isArray(actorNames) ? actorNames : [actorNames];
|
||||
for (const actorName of names) {
|
||||
this.visitActorName(actorName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
visitActorName(ctx: any): any {
|
||||
let actorName = '';
|
||||
|
||||
const identifier = ctx.IDENTIFIER();
|
||||
if (identifier) {
|
||||
actorName = identifier.getText();
|
||||
} else {
|
||||
const stringToken = ctx.STRING();
|
||||
if (stringToken) {
|
||||
actorName = this.extractString(stringToken.getText());
|
||||
}
|
||||
}
|
||||
|
||||
const actorId = this.generateId(actorName);
|
||||
|
||||
// Process metadata if present
|
||||
let metadata: Record<string, string> | undefined;
|
||||
const metadataCtx = ctx.metadata();
|
||||
if (metadataCtx) {
|
||||
metadata = this.visitMetadata(metadataCtx);
|
||||
}
|
||||
|
||||
this.processActorStatement(actorId, actorName, metadata);
|
||||
return null;
|
||||
}
|
||||
|
||||
visitMetadata(ctx: any): Record<string, string> {
|
||||
const metadata: Record<string, string> = {};
|
||||
const content = ctx.metadataContent();
|
||||
if (content) {
|
||||
const properties = content.metadataProperty();
|
||||
const props = Array.isArray(properties) ? properties : [properties];
|
||||
|
||||
for (const prop of props) {
|
||||
const strings = prop.STRING();
|
||||
if (strings && strings.length >= 2) {
|
||||
const key = this.extractString(strings[0].getText());
|
||||
const value = this.extractString(strings[1].getText());
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// Relationship statement
|
||||
visitRelationshipStatement(ctx: any): any {
|
||||
let from = '';
|
||||
let to = '';
|
||||
let arrowType = 0;
|
||||
let label: string | undefined;
|
||||
|
||||
// Get entity names
|
||||
const entityNames = ctx.entityName();
|
||||
if (entityNames && entityNames.length >= 2) {
|
||||
from = this.visitEntityName(entityNames[0]);
|
||||
to = this.visitEntityName(entityNames[1]);
|
||||
} else if (ctx.actorDeclaration()) {
|
||||
from = this.visitActorDeclaration(ctx.actorDeclaration());
|
||||
if (entityNames && entityNames.length >= 1) {
|
||||
to = this.visitEntityName(entityNames[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get arrow type
|
||||
const arrow = ctx.arrow();
|
||||
if (arrow) {
|
||||
const arrowResult = this.visitArrow(arrow);
|
||||
arrowType = arrowResult.type;
|
||||
label = arrowResult.label;
|
||||
}
|
||||
|
||||
this.processRelationship(from, to, arrowType, label);
|
||||
return null;
|
||||
}
|
||||
|
||||
visitEntityName(ctx: any): string {
|
||||
if (!ctx) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const nodeIdWithLabel = ctx.nodeIdWithLabel();
|
||||
if (nodeIdWithLabel) {
|
||||
return this.visitNodeIdWithLabel(nodeIdWithLabel);
|
||||
}
|
||||
|
||||
const identifiers = ctx.IDENTIFIER();
|
||||
if (identifiers) {
|
||||
if (Array.isArray(identifiers) && identifiers.length >= 2) {
|
||||
// Has class separator (:::)
|
||||
return identifiers[0].getText();
|
||||
} else if (Array.isArray(identifiers) && identifiers.length === 1) {
|
||||
return identifiers[0].getText();
|
||||
}
|
||||
}
|
||||
|
||||
const stringToken = ctx.STRING();
|
||||
if (stringToken) {
|
||||
return this.extractString(stringToken.getText());
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
visitActorDeclaration(ctx: any): string {
|
||||
const actorName = ctx.actorName();
|
||||
if (actorName) {
|
||||
return this.visitActorName(actorName);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
visitNodeIdWithLabel(ctx: any): string {
|
||||
if (ctx.IDENTIFIER()) {
|
||||
return ctx.IDENTIFIER().getText();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
visitArrow(ctx: any): { type: number; label?: string } {
|
||||
let arrowText = '';
|
||||
let label: string | undefined;
|
||||
|
||||
if (ctx.labeledArrow()) {
|
||||
const labeledArrow = ctx.labeledArrow();
|
||||
const edgeLabel = labeledArrow.edgeLabel();
|
||||
if (edgeLabel) {
|
||||
if (edgeLabel.IDENTIFIER()) {
|
||||
label = edgeLabel.IDENTIFIER().getText();
|
||||
} else if (edgeLabel.STRING()) {
|
||||
label = this.extractString(edgeLabel.STRING().getText());
|
||||
}
|
||||
}
|
||||
|
||||
// Determine arrow type from labeled arrow structure
|
||||
if (labeledArrow.SOLID_ARROW()) {
|
||||
arrowText = '-->';
|
||||
} else if (labeledArrow.BACK_ARROW()) {
|
||||
arrowText = '<--';
|
||||
} else if (labeledArrow.CIRCLE_ARROW()) {
|
||||
arrowText = '--o';
|
||||
} else if (labeledArrow.CROSS_ARROW()) {
|
||||
arrowText = '--x';
|
||||
} else if (labeledArrow.CIRCLE_ARROW_REVERSED()) {
|
||||
arrowText = 'o--';
|
||||
} else if (labeledArrow.CROSS_ARROW_REVERSED()) {
|
||||
arrowText = 'x--';
|
||||
} else {
|
||||
arrowText = '--';
|
||||
}
|
||||
} else {
|
||||
// Simple arrow
|
||||
if (ctx.SOLID_ARROW()) {
|
||||
arrowText = '-->';
|
||||
} else if (ctx.BACK_ARROW()) {
|
||||
arrowText = '<--';
|
||||
} else if (ctx.LINE_SOLID()) {
|
||||
arrowText = '--';
|
||||
} else if (ctx.CIRCLE_ARROW()) {
|
||||
arrowText = '--o';
|
||||
} else if (ctx.CROSS_ARROW()) {
|
||||
arrowText = '--x';
|
||||
} else if (ctx.CIRCLE_ARROW_REVERSED()) {
|
||||
arrowText = 'o--';
|
||||
} else if (ctx.CROSS_ARROW_REVERSED()) {
|
||||
arrowText = 'x--';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: this.parseArrowType(arrowText),
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
// System boundary statement
|
||||
visitSystemBoundaryStatement(ctx: any): any {
|
||||
const boundaryName = ctx.systemBoundaryName();
|
||||
let boundaryId = '';
|
||||
let boundaryNameText = '';
|
||||
|
||||
if (boundaryName) {
|
||||
if (boundaryName.IDENTIFIER()) {
|
||||
boundaryNameText = boundaryName.IDENTIFIER().getText();
|
||||
} else if (boundaryName.STRING()) {
|
||||
boundaryNameText = this.extractString(boundaryName.STRING().getText());
|
||||
}
|
||||
boundaryId = this.generateId(boundaryNameText);
|
||||
}
|
||||
|
||||
this.processSystemBoundaryStart(boundaryId, boundaryNameText);
|
||||
|
||||
// Visit boundary content
|
||||
const contents = ctx.systemBoundaryContent();
|
||||
if (contents) {
|
||||
const contentList = Array.isArray(contents) ? contents : [contents];
|
||||
for (const content of contentList) {
|
||||
this.visitSystemBoundaryContent(content);
|
||||
}
|
||||
}
|
||||
|
||||
this.processSystemBoundaryEnd();
|
||||
return null;
|
||||
}
|
||||
|
||||
visitSystemBoundaryContent(ctx: any): any {
|
||||
const usecaseInBoundary = ctx.usecaseInBoundary();
|
||||
if (usecaseInBoundary) {
|
||||
this.visitUsecaseInBoundary(usecaseInBoundary);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
visitUsecaseInBoundary(ctx: any): any {
|
||||
let useCaseId = '';
|
||||
let useCaseName = '';
|
||||
let classes: string[] | undefined;
|
||||
|
||||
if (ctx.usecaseWithClass()) {
|
||||
const withClass = ctx.usecaseWithClass();
|
||||
if (withClass.IDENTIFIER()) {
|
||||
const identifiers = withClass.IDENTIFIER();
|
||||
if (Array.isArray(identifiers) && identifiers.length >= 2) {
|
||||
useCaseId = identifiers[0].getText();
|
||||
useCaseName = useCaseId;
|
||||
classes = [identifiers[1].getText()];
|
||||
}
|
||||
} else if (withClass.STRING()) {
|
||||
useCaseName = this.extractString(withClass.STRING().getText());
|
||||
useCaseId = this.generateId(useCaseName);
|
||||
const identifiers = withClass.IDENTIFIER();
|
||||
if (identifiers) {
|
||||
classes = [identifiers.getText()];
|
||||
}
|
||||
}
|
||||
} else if (ctx.IDENTIFIER()) {
|
||||
useCaseId = ctx.IDENTIFIER().getText();
|
||||
useCaseName = useCaseId;
|
||||
} else if (ctx.STRING()) {
|
||||
useCaseName = this.extractString(ctx.STRING().getText());
|
||||
useCaseId = this.generateId(useCaseName);
|
||||
}
|
||||
|
||||
if (useCaseId && useCaseName) {
|
||||
this.processUseCaseStatement(useCaseId, useCaseName, undefined, classes);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// System boundary type statement
|
||||
visitSystemBoundaryTypeStatement(ctx: any): any {
|
||||
const boundaryName = ctx.systemBoundaryName();
|
||||
let boundaryId = '';
|
||||
|
||||
if (boundaryName) {
|
||||
if (boundaryName.IDENTIFIER()) {
|
||||
boundaryId = boundaryName.IDENTIFIER().getText();
|
||||
} else if (boundaryName.STRING()) {
|
||||
boundaryId = this.generateId(this.extractString(boundaryName.STRING().getText()));
|
||||
}
|
||||
}
|
||||
|
||||
const typeContent = ctx.systemBoundaryTypeContent();
|
||||
if (typeContent) {
|
||||
const properties = typeContent.systemBoundaryTypeProperty();
|
||||
const props = Array.isArray(properties) ? properties : [properties];
|
||||
|
||||
for (const prop of props) {
|
||||
const type = prop.systemBoundaryType();
|
||||
if (type) {
|
||||
let typeValue: 'package' | 'rect' = 'rect';
|
||||
if (type.PACKAGE()) {
|
||||
typeValue = 'package';
|
||||
} else if (type.RECT()) {
|
||||
typeValue = 'rect';
|
||||
}
|
||||
this.processSystemBoundaryType(boundaryId, typeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Direction statement
|
||||
visitDirectionStatement(ctx: any): any {
|
||||
const direction = ctx.direction();
|
||||
if (direction) {
|
||||
let directionText = '';
|
||||
if (direction.TB()) {
|
||||
directionText = 'TB';
|
||||
} else if (direction.TD()) {
|
||||
directionText = 'TD';
|
||||
} else if (direction.BT()) {
|
||||
directionText = 'BT';
|
||||
} else if (direction.RL()) {
|
||||
directionText = 'RL';
|
||||
} else if (direction.LR()) {
|
||||
directionText = 'LR';
|
||||
}
|
||||
this.processDirectionStatement(directionText);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Class definition statement
|
||||
visitClassDefStatement(ctx: any): any {
|
||||
let classId = '';
|
||||
if (ctx.IDENTIFIER()) {
|
||||
classId = ctx.IDENTIFIER().getText();
|
||||
}
|
||||
|
||||
const styles: string[] = [];
|
||||
const stylesOpt = ctx.stylesOpt();
|
||||
if (stylesOpt) {
|
||||
this.collectStyles(stylesOpt, styles);
|
||||
}
|
||||
|
||||
this.processClassDefStatement(classId, styles);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Class statement
|
||||
visitClassStatement(ctx: any): any {
|
||||
const nodeList = ctx.nodeList();
|
||||
const nodeIds: string[] = [];
|
||||
|
||||
if (nodeList) {
|
||||
const identifiers = nodeList.IDENTIFIER();
|
||||
const ids = Array.isArray(identifiers) ? identifiers : [identifiers];
|
||||
for (const id of ids) {
|
||||
nodeIds.push(id.getText());
|
||||
}
|
||||
}
|
||||
|
||||
let classId = '';
|
||||
const identifiers = ctx.IDENTIFIER();
|
||||
if (identifiers) {
|
||||
const ids = Array.isArray(identifiers) ? identifiers : [identifiers];
|
||||
if (ids.length > 0) {
|
||||
classId = ids[ids.length - 1].getText();
|
||||
}
|
||||
}
|
||||
|
||||
this.processClassStatement(nodeIds, classId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Style statement
|
||||
visitStyleStatement(ctx: any): any {
|
||||
let nodeId = '';
|
||||
if (ctx.IDENTIFIER()) {
|
||||
nodeId = ctx.IDENTIFIER().getText();
|
||||
}
|
||||
|
||||
const styles: string[] = [];
|
||||
const stylesOpt = ctx.stylesOpt();
|
||||
if (stylesOpt) {
|
||||
this.collectStyles(stylesOpt, styles);
|
||||
}
|
||||
|
||||
this.processStyleStatement(nodeId, styles);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Usecase statement
|
||||
visitUsecaseStatement(ctx: any): any {
|
||||
const entityName = ctx.entityName();
|
||||
if (entityName) {
|
||||
const useCaseId = this.visitEntityName(entityName);
|
||||
this.processUseCaseStatement(useCaseId, useCaseId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper method to collect styles
|
||||
private collectStyles(ctx: any, styles: string[]): void {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Visit all style components
|
||||
const styleComponents = this.getAllStyleComponents(ctx);
|
||||
for (const component of styleComponents) {
|
||||
styles.push(component.getText());
|
||||
}
|
||||
}
|
||||
|
||||
private getAllStyleComponents(ctx: any): any[] {
|
||||
const components: any[] = [];
|
||||
|
||||
if (ctx.style) {
|
||||
const styleCtx = ctx.style();
|
||||
if (styleCtx) {
|
||||
this.collectStyleComponents(styleCtx, components);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.stylesOpt) {
|
||||
const stylesOptList = Array.isArray(ctx.stylesOpt()) ? ctx.stylesOpt() : [ctx.stylesOpt()];
|
||||
for (const opt of stylesOptList) {
|
||||
if (opt) {
|
||||
this.collectStyleComponents(opt, components);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
private collectStyleComponents(ctx: any, components: any[]): void {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.styleComponent) {
|
||||
const comp = ctx.styleComponent();
|
||||
if (comp) {
|
||||
components.push(comp);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.style) {
|
||||
const styleCtx = ctx.style();
|
||||
if (styleCtx) {
|
||||
this.collectStyleComponents(styleCtx, components);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* ANTLR-based Usecase Diagram Parser
|
||||
*
|
||||
* This is a proper ANTLR implementation using antlr-ng generated parser code.
|
||||
* It provides the same interface as the existing parser for 100% compatibility.
|
||||
*
|
||||
* Follows the same structure as flowchart and sequence ANTLR parsers with both
|
||||
* listener and visitor pattern support.
|
||||
*/
|
||||
|
||||
import { CharStream, CommonTokenStream, ParseTreeWalker } from 'antlr4ng';
|
||||
import { UsecaseLexer } from './generated/UsecaseLexer.js';
|
||||
import { UsecaseParser } from './generated/UsecaseParser.js';
|
||||
import { UsecaseListener } from './UsecaseListener.js';
|
||||
import { UsecaseVisitor } from './UsecaseVisitor.js';
|
||||
import { UsecaseErrorListener } from './UsecaseErrorListener.js';
|
||||
import type { UsecaseDB } from '../../usecaseTypes.js';
|
||||
import { log } from '../../../../logger.js';
|
||||
|
||||
/**
|
||||
* Main ANTLR parser class that provides the same interface as the existing parser
|
||||
*/
|
||||
export class ANTLRUsecaseParser {
|
||||
yy: UsecaseDB | null;
|
||||
|
||||
constructor() {
|
||||
this.yy = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse usecase diagram input using ANTLR
|
||||
*
|
||||
* @param input - The usecase diagram text to parse
|
||||
* @returns The database instance populated with parsed data
|
||||
*/
|
||||
parse(input: string): UsecaseDB {
|
||||
if (!this.yy) {
|
||||
throw new Error('Usecase ANTLR parser missing yy (database).');
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Get environment variable helper
|
||||
const getEnvVar = (name: string): string | undefined => {
|
||||
try {
|
||||
if (typeof process !== 'undefined' && process.env) {
|
||||
return process.env[name];
|
||||
}
|
||||
} catch (_e) {
|
||||
// process is not defined in browser
|
||||
}
|
||||
|
||||
// Browser fallback
|
||||
if (typeof window !== 'undefined' && (window as any).MERMAID_CONFIG) {
|
||||
return (window as any).MERMAID_CONFIG[name];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const shouldLog = getEnvVar('ANTLR_DEBUG') === 'true';
|
||||
|
||||
if (shouldLog) {
|
||||
log.debug('🎯 ANTLR Usecase Parser: Starting parse');
|
||||
log.debug(`📝 Input length: ${input.length} characters`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Reset database state
|
||||
if (this.yy.clear) {
|
||||
this.yy.clear();
|
||||
}
|
||||
|
||||
// Create input stream and lexer
|
||||
const inputStream = CharStream.fromString(input);
|
||||
const lexer = new UsecaseLexer(inputStream);
|
||||
|
||||
// Add custom error listener to lexer
|
||||
const lexerErrorListener = new UsecaseErrorListener();
|
||||
lexer.removeErrorListeners();
|
||||
lexer.addErrorListener(lexerErrorListener);
|
||||
|
||||
const tokenStream = new CommonTokenStream(lexer);
|
||||
|
||||
// Create parser
|
||||
const parser = new UsecaseParser(tokenStream);
|
||||
|
||||
// Add custom error listener to parser
|
||||
const parserErrorListener = new UsecaseErrorListener();
|
||||
parser.removeErrorListeners();
|
||||
parser.addErrorListener(parserErrorListener);
|
||||
|
||||
// Generate parse tree
|
||||
if (shouldLog) {
|
||||
log.debug('🌳 ANTLR Usecase Parser: Starting parse tree generation');
|
||||
}
|
||||
const tree = parser.start();
|
||||
|
||||
// Check for syntax errors
|
||||
if (lexerErrorListener.hasErrors()) {
|
||||
throw lexerErrorListener.createDetailedError();
|
||||
}
|
||||
if (parserErrorListener.hasErrors()) {
|
||||
throw parserErrorListener.createDetailedError();
|
||||
}
|
||||
|
||||
if (shouldLog) {
|
||||
log.debug('✅ ANTLR Usecase Parser: Parse tree generated successfully');
|
||||
}
|
||||
|
||||
// Check if we should use Visitor or Listener pattern
|
||||
// Default to Visitor pattern (true) unless explicitly set to false
|
||||
const useVisitorPattern = getEnvVar('USE_ANTLR_VISITOR') !== 'false';
|
||||
|
||||
if (shouldLog) {
|
||||
log.debug('🔧 Usecase Parser: Pattern =', useVisitorPattern ? 'Visitor' : 'Listener');
|
||||
}
|
||||
|
||||
if (useVisitorPattern) {
|
||||
const visitor = new UsecaseVisitor(this.yy);
|
||||
try {
|
||||
visitor.visit(tree);
|
||||
if (shouldLog) {
|
||||
log.debug('✅ ANTLR Usecase Parser: Visitor completed successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'❌ ANTLR Usecase Parser: Visitor failed:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
log.error(
|
||||
'❌ ANTLR Usecase Parser: Visitor stack:',
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
const listener = new UsecaseListener(this.yy);
|
||||
try {
|
||||
ParseTreeWalker.DEFAULT.walk(listener, tree);
|
||||
if (shouldLog) {
|
||||
log.debug('✅ ANTLR Usecase Parser: Listener completed successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'❌ ANTLR Usecase Parser: Listener failed:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
log.error(
|
||||
'❌ ANTLR Usecase Parser: Listener stack:',
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
|
||||
if (shouldLog) {
|
||||
log.debug(`⏱️ Total parse time: ${totalTime.toFixed(2)}ms`);
|
||||
log.debug('✅ ANTLR Usecase Parser: Parse completed successfully');
|
||||
}
|
||||
|
||||
return this.yy;
|
||||
} catch (error) {
|
||||
const totalTime = performance.now() - startTime;
|
||||
log.error(`❌ ANTLR usecase parsing error after ${totalTime.toFixed(2)}ms:`, error);
|
||||
log.error('📝 Input that caused error (first 500 chars):', input.substring(0, 500));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Provide the same interface as existing parser
|
||||
setYY(yy: UsecaseDB) {
|
||||
this.yy = yy;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for compatibility with existing code
|
||||
export const parser = new ANTLRUsecaseParser();
|
||||
|
||||
export default parser;
|
||||
87
packages/mermaid/src/diagrams/usecase/styles.ts
Normal file
87
packages/mermaid/src/diagrams/usecase/styles.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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;
|
||||
501
packages/mermaid/src/diagrams/usecase/usecase.spec.ts
Normal file
501
packages/mermaid/src/diagrams/usecase/usecase.spec.ts
Normal file
@@ -0,0 +1,501 @@
|
||||
import { vi, describe, it, expect, beforeEach, beforeAll } from 'vitest';
|
||||
import { Diagram } from '../../Diagram.js';
|
||||
import { addDiagrams } from '../../diagram-api/diagram-orchestration.js';
|
||||
import { db } from './usecaseDb.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();
|
||||
|
||||
describe('UseCase diagram with ANTLR parser', () => {
|
||||
beforeEach(() => {
|
||||
db.clear();
|
||||
});
|
||||
|
||||
describe('when parsing basic actors', () => {
|
||||
it('should parse a single actor', async () => {
|
||||
const diagram = await Diagram.fromText(
|
||||
`usecase
|
||||
actor User`
|
||||
);
|
||||
|
||||
expect(diagram).toBeDefined();
|
||||
expect(diagram.type).toBe('usecase');
|
||||
|
||||
const actors = db.getActors();
|
||||
expect(actors.size).toBe(1);
|
||||
expect(actors.has('User')).toBe(true);
|
||||
expect(actors.get('User')?.name).toBe('User');
|
||||
});
|
||||
|
||||
it('should parse multiple actors', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
actor Guest`
|
||||
);
|
||||
|
||||
const actors = db.getActors();
|
||||
expect(actors.size).toBe(3);
|
||||
expect(actors.has('User')).toBe(true);
|
||||
expect(actors.has('Admin')).toBe(true);
|
||||
expect(actors.has('Guest')).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse actor with simple name', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor SystemUser`
|
||||
);
|
||||
|
||||
const actors = db.getActors();
|
||||
expect(actors.size).toBe(1);
|
||||
expect(actors.has('SystemUser')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing use cases', () => {
|
||||
it('should parse use cases from relationships', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login`
|
||||
);
|
||||
|
||||
const useCases = db.getUseCases();
|
||||
expect(useCases.size).toBe(1);
|
||||
expect(useCases.has('Login')).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse multiple use cases from relationships', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login
|
||||
User --> Logout
|
||||
User --> Register`
|
||||
);
|
||||
|
||||
const useCases = db.getUseCases();
|
||||
expect(useCases.size).toBe(3);
|
||||
expect(useCases.has('Login')).toBe(true);
|
||||
expect(useCases.has('Logout')).toBe(true);
|
||||
expect(useCases.has('Register')).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse use case from relationship', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> UserLoginProcess`
|
||||
);
|
||||
|
||||
const useCases = db.getUseCases();
|
||||
expect(useCases.size).toBe(1);
|
||||
expect(useCases.has('UserLoginProcess')).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse use cases with quoted names', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor "Customer Service"
|
||||
actor "System Administrator"
|
||||
"Customer Service" --> "Handle Tickets"
|
||||
"System Administrator" --> "Manage System"`
|
||||
);
|
||||
|
||||
const actors = db.getActors();
|
||||
expect(actors.size).toBe(2);
|
||||
// IDs are generated with underscores replacing spaces
|
||||
expect(actors.has('Customer_Service')).toBe(true);
|
||||
expect(actors.has('System_Administrator')).toBe(true);
|
||||
// But names should preserve the original text
|
||||
expect(actors.get('Customer_Service')?.name).toBe('Customer Service');
|
||||
expect(actors.get('System_Administrator')?.name).toBe('System Administrator');
|
||||
|
||||
const useCases = db.getUseCases();
|
||||
expect(useCases.size).toBe(2);
|
||||
expect(useCases.has('Handle_Tickets')).toBe(true);
|
||||
expect(useCases.has('Manage_System')).toBe(true);
|
||||
expect(useCases.get('Handle_Tickets')?.name).toBe('Handle Tickets');
|
||||
expect(useCases.get('Manage_System')?.name).toBe('Manage System');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing relationships', () => {
|
||||
it('should parse actor to use case relationship', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login`
|
||||
);
|
||||
|
||||
const relationships = db.getRelationships();
|
||||
expect(relationships.length).toBe(1);
|
||||
expect(relationships[0].from).toBe('User');
|
||||
expect(relationships[0].to).toBe('Login');
|
||||
expect(relationships[0].type).toBe('association');
|
||||
});
|
||||
|
||||
it('should parse multiple relationships', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login
|
||||
User --> Logout`
|
||||
);
|
||||
|
||||
const relationships = db.getRelationships();
|
||||
expect(relationships.length).toBe(2);
|
||||
expect(relationships[0].from).toBe('User');
|
||||
expect(relationships[0].to).toBe('Login');
|
||||
expect(relationships[1].from).toBe('User');
|
||||
expect(relationships[1].to).toBe('Logout');
|
||||
});
|
||||
|
||||
it('should parse relationship with label', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor Developer
|
||||
Developer --important--> WriteCode`
|
||||
);
|
||||
|
||||
const relationships = db.getRelationships();
|
||||
expect(relationships.length).toBe(1);
|
||||
expect(relationships[0].label).toBe('important');
|
||||
});
|
||||
|
||||
it('should parse different arrow types', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin <-- Logout
|
||||
User -- ViewData`
|
||||
);
|
||||
|
||||
const relationships = db.getRelationships();
|
||||
expect(relationships.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing system boundaries', () => {
|
||||
it('should parse a system boundary', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor Admin, User
|
||||
systemBoundary "Authentication"
|
||||
Login
|
||||
Logout
|
||||
end
|
||||
Admin --> Login
|
||||
User --> Login`
|
||||
);
|
||||
|
||||
const boundaries = db.getSystemBoundaries();
|
||||
expect(boundaries.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should parse use cases within system boundary', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
systemBoundary "Authentication System"
|
||||
Login
|
||||
Logout
|
||||
end
|
||||
User --> Login`
|
||||
);
|
||||
|
||||
const useCases = db.getUseCases();
|
||||
expect(useCases.size).toBe(2);
|
||||
expect(useCases.has('Login')).toBe(true);
|
||||
expect(useCases.has('Logout')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing direction', () => {
|
||||
it('should handle TB direction', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
direction TB
|
||||
actor User`
|
||||
);
|
||||
|
||||
expect(db.getDirection()).toBe('TB');
|
||||
});
|
||||
|
||||
it('should handle LR direction', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
direction LR
|
||||
actor User`
|
||||
);
|
||||
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
});
|
||||
|
||||
it('should normalize TD to TB', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
direction TD
|
||||
actor User`
|
||||
);
|
||||
|
||||
expect(db.getDirection()).toBe('TB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing actor metadata', () => {
|
||||
it('should parse actor with metadata', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User@{ "type" : "primary", "icon" : "user" }
|
||||
User --> Login`
|
||||
);
|
||||
|
||||
const actors = db.getActors();
|
||||
expect(actors.size).toBe(1);
|
||||
const user = actors.get('User');
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.metadata).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse multiple actors with different metadata', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User@{ "type" : "primary", "icon" : "user" }
|
||||
actor Admin@{ "type" : "secondary", "icon" : "admin" }
|
||||
User --> Login
|
||||
Admin --> ManageUsers`
|
||||
);
|
||||
|
||||
const actors = db.getActors();
|
||||
expect(actors.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing complex diagrams', () => {
|
||||
it('should parse a complete authentication system', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
|
||||
systemBoundary "Authentication System"
|
||||
Login
|
||||
Logout
|
||||
Register
|
||||
ResetPassword
|
||||
end
|
||||
|
||||
User --> Login
|
||||
User --> Register
|
||||
User --> Logout
|
||||
Admin --> Login`
|
||||
);
|
||||
|
||||
const actors = db.getActors();
|
||||
const useCases = db.getUseCases();
|
||||
const relationships = db.getRelationships();
|
||||
|
||||
expect(actors.size).toBe(2);
|
||||
expect(useCases.size).toBe(4);
|
||||
expect(relationships.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should parse diagram with multiple arrow types', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin
|
||||
User --> Login
|
||||
Admin <-- Logout
|
||||
User -- ViewData`
|
||||
);
|
||||
|
||||
const relationships = db.getRelationships();
|
||||
expect(relationships.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle use case creation from relationships', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor Developer
|
||||
Developer --> LoginSystem
|
||||
Developer --> Authentication`
|
||||
);
|
||||
|
||||
const useCases = db.getUseCases();
|
||||
expect(useCases.size).toBe(2);
|
||||
expect(useCases.has('LoginSystem')).toBe(true);
|
||||
expect(useCases.has('Authentication')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing class definitions', () => {
|
||||
it('should handle classDef', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login
|
||||
classDef important fill:#f96,stroke:#333,stroke-width:4px
|
||||
class Login important`
|
||||
);
|
||||
|
||||
const classDefs = db.getClassDefs();
|
||||
expect(classDefs.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('database methods', () => {
|
||||
it('should clear all data', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login`
|
||||
);
|
||||
|
||||
expect(db.getActors().size).toBe(1);
|
||||
expect(db.getUseCases().size).toBe(1);
|
||||
expect(db.getRelationships().length).toBe(1);
|
||||
|
||||
db.clear();
|
||||
|
||||
expect(db.getActors().size).toBe(0);
|
||||
expect(db.getUseCases().size).toBe(0);
|
||||
expect(db.getRelationships().length).toBe(0);
|
||||
});
|
||||
|
||||
it('should get specific actor by id', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
actor Admin`
|
||||
);
|
||||
|
||||
const user = db.getActor('User');
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toBe('User');
|
||||
expect(user?.name).toBe('User');
|
||||
});
|
||||
|
||||
it('should get specific use case by id', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login
|
||||
User --> Logout`
|
||||
);
|
||||
|
||||
const login = db.getUseCase('Login');
|
||||
expect(login).toBeDefined();
|
||||
expect(login?.id).toBe('Login');
|
||||
expect(login?.name).toBe('Login');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent actor', () => {
|
||||
const actor = db.getActor('NonExistent');
|
||||
expect(actor).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent use case', () => {
|
||||
const useCase = db.getUseCase('NonExistent');
|
||||
expect(useCase).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getData method', () => {
|
||||
it('should convert diagram data to LayoutData format', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
actor User
|
||||
User --> Login`
|
||||
);
|
||||
|
||||
const data = db.getData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.nodes).toBeDefined();
|
||||
expect(data.edges).toBeDefined();
|
||||
expect(data.nodes.length).toBe(2);
|
||||
expect(data.edges.length).toBe(1);
|
||||
expect(data.type).toBe('usecase');
|
||||
});
|
||||
|
||||
it('should include direction in layout data', async () => {
|
||||
await Diagram.fromText(
|
||||
`usecase
|
||||
direction LR
|
||||
actor User`
|
||||
);
|
||||
|
||||
const data = db.getData();
|
||||
expect(data.direction).toBe('LR');
|
||||
});
|
||||
});
|
||||
});
|
||||
397
packages/mermaid/src/diagrams/usecase/usecaseDb.ts
Normal file
397
packages/mermaid/src/diagrams/usecase/usecaseDb.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
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,
|
||||
};
|
||||
22
packages/mermaid/src/diagrams/usecase/usecaseDetector.ts
Normal file
22
packages/mermaid/src/diagrams/usecase/usecaseDetector.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'usecase';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*usecase/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./usecaseDiagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
export const usecase: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
12
packages/mermaid/src/diagrams/usecase/usecaseDiagram.ts
Normal file
12
packages/mermaid/src/diagrams/usecase/usecaseDiagram.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { parser } from './usecaseParser.js';
|
||||
import { db } from './usecaseDb.js';
|
||||
import { renderer } from './usecaseRenderer.js';
|
||||
import styles from './styles.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
58
packages/mermaid/src/diagrams/usecase/usecaseParser.ts
Normal file
58
packages/mermaid/src/diagrams/usecase/usecaseParser.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// Import local ANTLR parser
|
||||
import { log } from '../../logger.js';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { db } from './usecaseDb.js';
|
||||
|
||||
// Import local ANTLR parser implementation
|
||||
import antlrParser from './parser/antlr/antlr-parser.js';
|
||||
|
||||
/**
|
||||
* Parse usecase diagram using local ANTLR parser
|
||||
*/
|
||||
const parseUsecaseWithLocalAntlr = (input: string) => {
|
||||
// Set the database instance
|
||||
antlrParser.yy = db;
|
||||
|
||||
// Parse and return the populated database
|
||||
return antlrParser.parse(input);
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parse: (input: string): void => {
|
||||
log.debug('Parsing usecase diagram with local ANTLR parser:', input);
|
||||
|
||||
try {
|
||||
// Use local ANTLR parser
|
||||
parseUsecaseWithLocalAntlr(input);
|
||||
log.debug('ANTLR 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
48
packages/mermaid/src/diagrams/usecase/usecaseRenderer.ts
Normal file
48
packages/mermaid/src/diagrams/usecase/usecaseRenderer.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { DrawDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import utils from '../../utils.js';
|
||||
import type { UsecaseDB } from './usecaseTypes.js';
|
||||
|
||||
/**
|
||||
* Main draw function using unified rendering system
|
||||
*/
|
||||
const draw: DrawDefinition = async (_text, id, _version, diag) => {
|
||||
log.info('Drawing usecase diagram (unified)', id);
|
||||
const { securityLevel, usecase: conf, layout } = getConfig();
|
||||
|
||||
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
|
||||
// into the Layout data format
|
||||
const usecaseDb = diag.db as UsecaseDB;
|
||||
const data4Layout = usecaseDb.getData();
|
||||
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
|
||||
data4Layout.nodeSpacing = 50; // Default node spacing
|
||||
data4Layout.rankSpacing = 50; // Default rank spacing
|
||||
data4Layout.markers = ['point', '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 };
|
||||
40
packages/mermaid/src/diagrams/usecase/usecaseStyles.ts
Normal file
40
packages/mermaid/src/diagrams/usecase/usecaseStyles.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const getStyles = (options: any) =>
|
||||
`
|
||||
.actor {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.actor-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.usecase {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.usecase-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.relationship {
|
||||
stroke: ${options.primaryColor};
|
||||
fill: ${options.primaryColor};
|
||||
}
|
||||
|
||||
.relationship-label {
|
||||
fill: ${options.primaryTextColor};
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
113
packages/mermaid/src/diagrams/usecase/usecaseTypes.ts
Normal file
113
packages/mermaid/src/diagrams/usecase/usecaseTypes.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
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;
|
||||
}
|
||||
@@ -185,27 +185,22 @@ A --> C[End]
|
||||
|
||||
Some common flowchart configurations are:
|
||||
|
||||
- _htmlLabels_: true/false
|
||||
- _curve_: linear/curve
|
||||
- _diagramPadding_: number
|
||||
- _useMaxWidth_: number
|
||||
|
||||
**Deprecated configurations:**
|
||||
|
||||
- ~~_htmlLabels_~~: Use global `htmlLabels` instead
|
||||
|
||||
For a complete list of flowchart configurations, see [defaultConfig.ts](https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/defaultConfig.ts) in the source code.
|
||||
_Soon we plan to publish a complete list of all diagram-specific configurations updated in the docs._
|
||||
|
||||
The following code snippet changes flowchart config:
|
||||
|
||||
```
|
||||
%%{init: { "htmlLabels": true, "flowchart": { "curve": "linear" } } }%%
|
||||
```
|
||||
`%%{init: { "flowchart": { "htmlLabels": true, "curve": "linear" } } }%%`
|
||||
|
||||
**Note:** `flowchart.htmlLabels` has been deprecated. Use the global `htmlLabels` configuration instead.
|
||||
Here we are overriding only the flowchart config, and not the general config, setting `htmlLabels` to `true` and `curve` to `linear`.
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { "htmlLabels": true, "flowchart": { "curve": "linear" } } }%%
|
||||
%%{init: { "flowchart": { "htmlLabels": true, "curve": "linear" } } }%%
|
||||
graph TD
|
||||
A(Forest) --> B[/Another/]
|
||||
A --> C[End]
|
||||
|
||||
@@ -368,7 +368,7 @@ The list of configuration objects are described [in the mermaidAPI documentation
|
||||
```html
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
let config = { startOnLoad: true, htmlLabels: true, flowchart: { useMaxWidth: false } };
|
||||
let config = { startOnLoad: true, flowchart: { useMaxWidth: false, htmlLabels: true } };
|
||||
mermaid.initialize(config);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -216,7 +216,11 @@ Messages can be of two displayed either solid or with a dotted line.
|
||||
[Actor][Arrow][Actor]:Message text
|
||||
```
|
||||
|
||||
There are ten types of arrows currently supported:
|
||||
Lines can be solid or dotted, and can end with various types of arrowheads, crosses, or open arrows.
|
||||
|
||||
#### Supported Arrow Types
|
||||
|
||||
**Standard Arrow Types**
|
||||
|
||||
| Type | Description |
|
||||
| -------- | ---------------------------------------------------- |
|
||||
@@ -231,6 +235,49 @@ There are ten types of arrows currently supported:
|
||||
| `-)` | Solid line with an open arrow at the end (async) |
|
||||
| `--)` | Dotted line with a open arrow at the end (async) |
|
||||
|
||||
**Half-Arrows (v<MERMAID_RELEASE_VERSION>+)**
|
||||
|
||||
The following half-arrow types are supported for more expressive sequence diagrams. Both solid and dotted variants are available by increasing the number of dashes (`-` → `--`).
|
||||
|
||||
---
|
||||
|
||||
| Type | Description |
|
||||
| ------- | ---------------------------------------------------- |
|
||||
| `-\|\` | Solid line with top half arrowhead |
|
||||
| `--\|\` | Dotted line with top half arrowhead |
|
||||
| `-\|/` | Solid line with bottom half arrowhead |
|
||||
| `--\|/` | Dotted line with bottom half arrowhead |
|
||||
| `/\|-` | Solid line with reverse top half arrowhead |
|
||||
| `/\|--` | Dotted line with reverse top half arrowhead |
|
||||
| `\\-` | Solid line with reverse bottom half arrowhead |
|
||||
| `\\--` | Dotted line with reverse bottom half arrowhead |
|
||||
| `-\\` | Solid line with top stick half arrowhead |
|
||||
| `--\\` | Dotted line with top stick half arrowhead |
|
||||
| `-//` | Solid line with bottom stick half arrowhead |
|
||||
| `--//` | Dotted line with bottom stick half arrowhead |
|
||||
| `//-` | Solid line with reverse top stick half arrowhead |
|
||||
| `//--` | Dotted line with reverse top stick half arrowhead |
|
||||
| `\\-` | Solid line with reverse bottom stick half arrowhead |
|
||||
| `\\--` | Dotted line with reverse bottom stick half arrowhead |
|
||||
|
||||
## Central Connections (v<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
Mermaid sequence diagrams support **central lifeline connections** using a `()`.
|
||||
This is useful to represent messages or signals that connect to a central point, rather than from one actor directly to another.
|
||||
|
||||
To indicate a central connection, append `()` to the arrow syntax.
|
||||
|
||||
#### Basic Syntax
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant John
|
||||
Alice->>()John: Hello John
|
||||
Alice()->>John: How are you?
|
||||
John()->>()Alice: Great!
|
||||
```
|
||||
|
||||
## Activations
|
||||
|
||||
It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations:
|
||||
|
||||
@@ -207,7 +207,7 @@ describe('when using mermaid and ', () => {
|
||||
[Error: Parse error on line 2:
|
||||
...equenceDiagramAlice:->Bob: Hello Bob, h...
|
||||
----------------------^
|
||||
Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT']
|
||||
Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT']
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import assignWithDepth from './assignWithDepth.js';
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
import { getEffectiveHtmlLabels } from './config.js';
|
||||
import mermaid from './mermaid.js';
|
||||
import mermaidAPI, {
|
||||
appendDivSvgG,
|
||||
@@ -359,11 +358,10 @@ describe('mermaidAPI', () => {
|
||||
});
|
||||
|
||||
describe('no htmlLabels in the configuration', () => {
|
||||
const mocked_config_no_htmlLabels: MermaidConfig = {
|
||||
const mocked_config_no_htmlLabels = {
|
||||
themeCSS: 'default',
|
||||
fontFamily: 'serif',
|
||||
altFontFamily: 'sans-serif',
|
||||
htmlLabels: false, // Explicitly set to false
|
||||
};
|
||||
|
||||
describe('creates styles for shape elements "rect", "polygon", "ellipse", and "circle"', () => {
|
||||
@@ -1150,63 +1148,4 @@ flowchart TD
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('flowchart.htmlLabels deprecation behavior', () => {
|
||||
beforeEach(() => {
|
||||
mermaidAPI.globalReset();
|
||||
});
|
||||
|
||||
it('should use root-level htmlLabels when only root-level is set', () => {
|
||||
const config: MermaidConfig = { htmlLabels: true };
|
||||
expect(config.htmlLabels).toBe(true);
|
||||
|
||||
const config2: MermaidConfig = { htmlLabels: false };
|
||||
expect(config2.htmlLabels).toBe(false);
|
||||
});
|
||||
|
||||
it('should check config.htmlLabels value directly when set', () => {
|
||||
const config1: MermaidConfig = { htmlLabels: true };
|
||||
expect(config1.htmlLabels).toBe(true);
|
||||
|
||||
const config2: MermaidConfig = { htmlLabels: false };
|
||||
expect(config2.htmlLabels).toBe(false);
|
||||
|
||||
const config3: MermaidConfig = { htmlLabels: undefined };
|
||||
expect(config3.htmlLabels).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should use getEffectiveHtmlLabels only when fallback logic is needed', () => {
|
||||
// Only call getEffectiveHtmlLabels when we need the fallback behavior
|
||||
const configWithDeprecated: MermaidConfig = { flowchart: { htmlLabels: true } };
|
||||
expect(getEffectiveHtmlLabels(configWithDeprecated)).toBe(true);
|
||||
|
||||
const configWithBoth: MermaidConfig = {
|
||||
htmlLabels: false,
|
||||
flowchart: { htmlLabels: true },
|
||||
};
|
||||
expect(getEffectiveHtmlLabels(configWithBoth)).toBe(false); // Root takes precedence
|
||||
|
||||
const configEmpty: MermaidConfig = {};
|
||||
expect(getEffectiveHtmlLabels(configEmpty)).toBe(true); // Default to true
|
||||
});
|
||||
|
||||
it('should verify the precedence logic: config.htmlLabels ?? config.flowchart?.htmlLabels ?? true', () => {
|
||||
// Test the exact precedence chain
|
||||
const config1: MermaidConfig = { htmlLabels: true };
|
||||
const result1 = config1.htmlLabels ?? config1.flowchart?.htmlLabels ?? true;
|
||||
expect(result1).toBe(true);
|
||||
|
||||
const config2: MermaidConfig = { htmlLabels: false };
|
||||
const result2 = config2.htmlLabels ?? config2.flowchart?.htmlLabels ?? true;
|
||||
expect(result2).toBe(false);
|
||||
|
||||
const config3: MermaidConfig = { flowchart: { htmlLabels: true } };
|
||||
const result3 = config3.htmlLabels ?? config3.flowchart?.htmlLabels ?? true;
|
||||
expect(result3).toBe(true);
|
||||
|
||||
const config4: MermaidConfig = {};
|
||||
const result4 = config4.htmlLabels ?? config4.flowchart?.htmlLabels ?? true;
|
||||
expect(result4).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ 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';
|
||||
import { getEffectiveHtmlLabels } from './config.js';
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
|
||||
import type { DiagramMetadata, DiagramStyleClassDef } from './diagram-api/types.js';
|
||||
@@ -129,7 +128,7 @@ export const createCssStyles = (
|
||||
|
||||
// classDefs defined in the diagram text
|
||||
if (classDefs instanceof Map) {
|
||||
const htmlLabels = getEffectiveHtmlLabels(config);
|
||||
const htmlLabels = config.htmlLabels ?? config.flowchart?.htmlLabels; // TODO why specifically check the Flowchart diagram config?
|
||||
|
||||
const cssHtmlElements = ['> *', 'span']; // TODO make a constant
|
||||
const cssShapeElements = ['rect', 'polygon', 'ellipse', 'circle', 'path']; // TODO make a constant
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../../config.js';
|
||||
|
||||
import { evaluate } from '../../diagrams/common/common.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js';
|
||||
import { select } from 'd3';
|
||||
@@ -26,7 +25,7 @@ const rect = async (parent, node) => {
|
||||
.attr('id', node.id)
|
||||
.attr('data-look', node.look);
|
||||
|
||||
const useHtmlLabels = getEffectiveHtmlLabels(siteConfig);
|
||||
const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
|
||||
|
||||
// Create the label and insert it after the rect
|
||||
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');
|
||||
@@ -40,7 +39,7 @@ const rect = async (parent, node) => {
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
|
||||
if (getEffectiveHtmlLabels(siteConfig)) {
|
||||
if (evaluate(siteConfig.flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -189,7 +188,7 @@ const roundedWithTitle = async (parent, node) => {
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
|
||||
if (getEffectiveHtmlLabels(siteConfig)) {
|
||||
if (evaluate(siteConfig.flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -265,7 +264,7 @@ const roundedWithTitle = async (parent, node) => {
|
||||
|
||||
label.attr(
|
||||
'transform',
|
||||
`translate(${node.x - bbox.width / 2}, ${y + 1 - (getEffectiveHtmlLabels(siteConfig) ? 0 : 3)})`
|
||||
`translate(${node.x - bbox.width / 2}, ${y + 1 - (evaluate(siteConfig.flowchart.htmlLabels) ? 0 : 3)})`
|
||||
);
|
||||
|
||||
const rectBox = rect.node().getBBox();
|
||||
@@ -296,7 +295,7 @@ const kanbanSection = async (parent, node) => {
|
||||
.attr('id', node.id)
|
||||
.attr('data-look', node.look);
|
||||
|
||||
const useHtmlLabels = getEffectiveHtmlLabels(siteConfig);
|
||||
const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
|
||||
|
||||
// Create the label and insert it after the rect
|
||||
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');
|
||||
@@ -311,7 +310,7 @@ const kanbanSection = async (parent, node) => {
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
|
||||
if (getEffectiveHtmlLabels(siteConfig)) {
|
||||
if (evaluate(siteConfig.flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -460,6 +459,173 @@ const divider = (parent, node) => {
|
||||
return { cluster: shapeSvg, labelBBox: {} };
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom cluster shape for usecase system boundaries
|
||||
* Supports two types: 'rect' (dashed rectangle) and 'package' (UML package notation)
|
||||
* @param {any} parent
|
||||
* @param {any} node
|
||||
* @returns {any} ShapeSvg
|
||||
*/
|
||||
const usecaseSystemBoundary = async (parent, node) => {
|
||||
log.info('Creating usecase system boundary for ', node.id, node);
|
||||
const siteConfig = getConfig();
|
||||
const { handDrawnSeed } = siteConfig;
|
||||
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', 'cluster usecase-system-boundary ' + node.cssClasses)
|
||||
.attr('id', node.id)
|
||||
.attr('data-look', node.look);
|
||||
|
||||
// Get boundary type from node metadata (default to 'rect')
|
||||
const boundaryType = node.boundaryType || 'rect';
|
||||
shapeSvg.attr('data-boundary-type', boundaryType);
|
||||
|
||||
const useHtmlLabels = evaluate(siteConfig.flowchart?.htmlLabels);
|
||||
|
||||
// Create the label
|
||||
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label');
|
||||
const text = await createText(labelEl, node.label, {
|
||||
style: node.labelStyle,
|
||||
useHtmlLabels,
|
||||
isNode: true,
|
||||
});
|
||||
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
if (evaluate(siteConfig.flowchart?.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
dv.attr('width', bbox.width);
|
||||
dv.attr('height', bbox.height);
|
||||
}
|
||||
|
||||
// Calculate width with padding (similar to rect cluster)
|
||||
const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;
|
||||
if (node.width <= bbox.width + node.padding) {
|
||||
node.diff = (width - node.width) / 2 - node.padding;
|
||||
} else {
|
||||
node.diff = -node.padding;
|
||||
}
|
||||
|
||||
const height = node.height;
|
||||
// Use absolute coordinates from layout engine (like rect cluster does)
|
||||
const x = node.x - width / 2;
|
||||
const y = node.y - height / 2;
|
||||
|
||||
let boundaryRect;
|
||||
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
|
||||
|
||||
if (boundaryType === 'package') {
|
||||
// Draw package-type boundary (rectangle with separate name box at top)
|
||||
const nameBoxWidth = Math.max(80, bbox.width + 20);
|
||||
const nameBoxHeight = 25;
|
||||
|
||||
if (node.look === 'handDrawn') {
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {
|
||||
stroke: 'black',
|
||||
strokeWidth: 2,
|
||||
fill: 'none',
|
||||
seed: handDrawnSeed,
|
||||
});
|
||||
|
||||
// Draw main boundary rectangle
|
||||
const roughRect = rc.rectangle(x, y, width, height, options);
|
||||
boundaryRect = shapeSvg.insert(() => roughRect, ':first-child');
|
||||
|
||||
// Draw name box at top-left
|
||||
const roughNameBox = rc.rectangle(x, y - nameBoxHeight, nameBoxWidth, nameBoxHeight, options);
|
||||
shapeSvg.insert(() => roughNameBox, ':first-child');
|
||||
} else {
|
||||
// Draw main boundary rectangle
|
||||
boundaryRect = shapeSvg
|
||||
.insert('rect', ':first-child')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw name box at top-left
|
||||
shapeSvg
|
||||
.insert('rect', ':first-child')
|
||||
.attr('x', x)
|
||||
.attr('y', y - nameBoxHeight)
|
||||
.attr('width', nameBoxWidth)
|
||||
.attr('height', nameBoxHeight)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2);
|
||||
}
|
||||
|
||||
// Position label in the center of the name box (using absolute coordinates)
|
||||
// The name box is at (x, y - nameBoxHeight), so center the label there
|
||||
labelEl.attr(
|
||||
'transform',
|
||||
`translate(${x + nameBoxWidth / 2 - bbox.width / 2}, ${y - nameBoxHeight})`
|
||||
);
|
||||
} else {
|
||||
// Draw rect-type boundary (simple dashed rectangle)
|
||||
if (node.look === 'handDrawn') {
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {
|
||||
stroke: 'black',
|
||||
strokeWidth: 2,
|
||||
fill: 'none',
|
||||
strokeLineDash: [5, 5],
|
||||
seed: handDrawnSeed,
|
||||
});
|
||||
|
||||
const roughRect = rc.rectangle(x, y, width, height, options);
|
||||
boundaryRect = shapeSvg.insert(() => roughRect, ':first-child');
|
||||
} else {
|
||||
// Draw dashed rectangle
|
||||
boundaryRect = shapeSvg
|
||||
.insert('rect', ':first-child')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'black')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,5');
|
||||
}
|
||||
|
||||
// Position label at top-left (using absolute coordinates, same as rect cluster)
|
||||
labelEl.attr(
|
||||
'transform',
|
||||
`translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
|
||||
);
|
||||
}
|
||||
|
||||
// Get the bounding box of the boundary rectangle
|
||||
const rectBox = boundaryRect.node().getBBox();
|
||||
|
||||
// Set node properties required by layout engine (similar to rect cluster)
|
||||
node.offsetX = 0;
|
||||
node.width = rectBox.width;
|
||||
node.height = rectBox.height;
|
||||
// Used by layout engine to position subgraph in parent
|
||||
node.offsetY = bbox.height - node.padding / 2;
|
||||
|
||||
// Set intersection function for edge routing
|
||||
node.intersect = function (point) {
|
||||
return intersectRect(node, point);
|
||||
};
|
||||
|
||||
// Return cluster object
|
||||
return {
|
||||
cluster: shapeSvg,
|
||||
labelBBox: bbox,
|
||||
};
|
||||
};
|
||||
|
||||
const squareRect = rect;
|
||||
const shapes = {
|
||||
rect,
|
||||
@@ -468,6 +634,7 @@ const shapes = {
|
||||
noteGroup,
|
||||
divider,
|
||||
kanbanSection,
|
||||
usecaseSystemBoundary,
|
||||
};
|
||||
|
||||
let clusterElems = new Map();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { select } from 'd3';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../../config.js';
|
||||
import common, {
|
||||
evaluate,
|
||||
hasKatex,
|
||||
renderKatexSanitized,
|
||||
sanitizeText,
|
||||
@@ -64,7 +64,7 @@ const createLabel = async (_vertexText, style, isTitle, isNode) => {
|
||||
vertexText = vertexText[0];
|
||||
}
|
||||
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
vertexText = vertexText.replace(/\\n|\n/g, '<br />');
|
||||
log.info('vertexText' + vertexText);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../../config.js';
|
||||
import { evaluate } from '../../diagrams/common/common.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { createText } from '../createText.js';
|
||||
import utils from '../../utils.js';
|
||||
@@ -45,7 +45,7 @@ export const getLabelStyles = (styleArray) => {
|
||||
};
|
||||
|
||||
export const insertEdgeLabel = async (elem, edge) => {
|
||||
let useHtmlLabels = getEffectiveHtmlLabels(getConfig());
|
||||
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||
|
||||
const { labelStyles } = styles2String(edge);
|
||||
edge.labelStyle = labelStyles;
|
||||
@@ -161,7 +161,7 @@ export const insertEdgeLabel = async (elem, edge) => {
|
||||
* @param {any} value
|
||||
*/
|
||||
function setTerminalWidth(fo, value) {
|
||||
if (getEffectiveHtmlLabels(getConfig()) && fo) {
|
||||
if (getConfig().flowchart.htmlLabels && fo) {
|
||||
fo.style.width = value.length * 9 + 'px';
|
||||
fo.style.height = '12px';
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { curvedTrapezoid } from './shapes/curvedTrapezoid.js';
|
||||
import { cylinder } from './shapes/cylinder.js';
|
||||
import { dividedRectangle } from './shapes/dividedRect.js';
|
||||
import { doublecircle } from './shapes/doubleCircle.js';
|
||||
import { ellipse } from './shapes/ellipse.js';
|
||||
import { filledCircle } from './shapes/filledCircle.js';
|
||||
import { flippedTriangle } from './shapes/flippedTriangle.js';
|
||||
import { forkJoin } from './shapes/forkJoin.js';
|
||||
@@ -32,6 +33,8 @@ 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';
|
||||
@@ -115,6 +118,14 @@ export const shapesDefs = [
|
||||
aliases: ['terminal', 'pill'],
|
||||
handler: stadium,
|
||||
},
|
||||
{
|
||||
semanticName: 'Ellipse',
|
||||
name: 'Ellipse',
|
||||
shortName: 'ellipse',
|
||||
description: 'Ellipse shape',
|
||||
aliases: ['oval'],
|
||||
handler: ellipse,
|
||||
},
|
||||
{
|
||||
semanticName: 'Subprocess',
|
||||
name: 'Framed Rectangle',
|
||||
@@ -507,6 +518,10 @@ const generateShapeMap = () => {
|
||||
|
||||
// Requirement diagram
|
||||
requirementBox,
|
||||
|
||||
// Usecase diagram
|
||||
usecaseActor,
|
||||
usecaseActorIcon,
|
||||
} as const;
|
||||
|
||||
const entries = [
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import rough from 'roughjs';
|
||||
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||
|
||||
export async function ellipse<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node));
|
||||
|
||||
// Calculate ellipse dimensions with padding
|
||||
const padding = halfPadding ?? 10;
|
||||
const radiusX = bbox.width / 2 + padding * 2;
|
||||
const radiusY = bbox.height / 2 + padding;
|
||||
|
||||
let ellipseElem;
|
||||
const { cssStyles } = node;
|
||||
|
||||
if (node.look === 'handDrawn') {
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
const roughNode = rc.ellipse(0, 0, radiusX * 2, radiusY * 2, options);
|
||||
|
||||
ellipseElem = shapeSvg.insert(() => roughNode, ':first-child');
|
||||
ellipseElem.attr('class', 'basic label-container');
|
||||
|
||||
if (cssStyles) {
|
||||
ellipseElem.attr('style', cssStyles);
|
||||
}
|
||||
} else {
|
||||
ellipseElem = shapeSvg
|
||||
.insert('ellipse', ':first-child')
|
||||
.attr('class', 'basic label-container')
|
||||
.attr('style', nodeStyles)
|
||||
.attr('rx', radiusX)
|
||||
.attr('ry', radiusY)
|
||||
.attr('cx', 0)
|
||||
.attr('cy', 0);
|
||||
}
|
||||
|
||||
node.width = radiusX * 2;
|
||||
node.height = radiusY * 2;
|
||||
|
||||
updateNodeBounds(node, ellipseElem);
|
||||
|
||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||
const rx = bounds.width / 2;
|
||||
const ry = bounds.height / 2;
|
||||
return intersect.ellipse(bounds, rx, ry, point);
|
||||
};
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.ellipse(node, radiusX, radiusY, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import { getConfig } from '../../../config.js';
|
||||
import { getEffectiveHtmlLabels } from '../../../config.js';
|
||||
|
||||
export async function note<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
@@ -14,7 +13,7 @@ export async function note<T extends SVGGraphicsElement>(
|
||||
) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
const useHtmlLabels = getEffectiveHtmlLabels(getConfig()) || node.useHtmlLabels;
|
||||
const useHtmlLabels = node.useHtmlLabels || getConfig().flowchart?.htmlLabels !== false;
|
||||
if (!useHtmlLabels) {
|
||||
node.centerLabel = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createText } from '../../createText.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import { getConfig } from '../../../diagram-api/diagramAPI.js';
|
||||
import { getEffectiveHtmlLabels } from '../../../config.js';
|
||||
import { select } from 'd3';
|
||||
import defaultConfig from '../../../defaultConfig.js';
|
||||
import { evaluate, sanitizeText } from '../../../diagrams/common/common.js';
|
||||
@@ -131,7 +130,7 @@ export const insertLabel = async <T extends SVGGraphicsElement>(
|
||||
addSvgBackground?: boolean | undefined;
|
||||
}
|
||||
) => {
|
||||
const useHtmlLabels = getEffectiveHtmlLabels(getConfig()) || options.useHtmlLabels;
|
||||
const useHtmlLabels = options.useHtmlLabels || evaluate(getConfig()?.flowchart?.htmlLabels);
|
||||
|
||||
// Create the label and insert it after the rect
|
||||
const labelEl = parent
|
||||
@@ -149,7 +148,7 @@ export const insertLabel = async <T extends SVGGraphicsElement>(
|
||||
let bbox = text.getBBox();
|
||||
const halfPadding = options.padding / 2;
|
||||
|
||||
if (getEffectiveHtmlLabels(getConfig())) {
|
||||
if (evaluate(getConfig()?.flowchart?.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ required:
|
||||
- block
|
||||
- look
|
||||
- radar
|
||||
- usecase
|
||||
properties:
|
||||
theme:
|
||||
description: |
|
||||
@@ -153,8 +154,6 @@ properties:
|
||||
default: false
|
||||
htmlLabels:
|
||||
type: boolean # maybe unused, seems to be copied in each diagram config
|
||||
default: true
|
||||
|
||||
fontFamily:
|
||||
description: |
|
||||
Specifies the font to be used in the rendered diagrams.
|
||||
@@ -312,6 +311,8 @@ properties:
|
||||
$ref: '#/$defs/BlockDiagramConfig'
|
||||
radar:
|
||||
$ref: '#/$defs/RadarDiagramConfig'
|
||||
usecase:
|
||||
$ref: '#/$defs/UsecaseDiagramConfig'
|
||||
dompurifyConfig:
|
||||
title: DOM Purify Configuration
|
||||
description: Configuration options to pass to the `dompurify` library.
|
||||
@@ -2054,6 +2055,7 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
- titleTopMargin
|
||||
- subGraphTitleMargin
|
||||
- diagramPadding
|
||||
- htmlLabels
|
||||
- nodeSpacing
|
||||
- rankSpacing
|
||||
- curve
|
||||
@@ -2085,13 +2087,9 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
default: 8
|
||||
htmlLabels:
|
||||
description: |
|
||||
**DEPRECATED: Use global `htmlLabels` instead.**
|
||||
|
||||
Flag for setting whether or not a html tag should be used for rendering labels on nodes and edges.
|
||||
This property is deprecated.
|
||||
Please use the global `htmlLabels` configuration instead.
|
||||
Flag for setting whether or not a html tag should be used for rendering labels on the edges.
|
||||
type: boolean
|
||||
deprecated: true
|
||||
default: true
|
||||
nodeSpacing:
|
||||
description: |
|
||||
Defines the spacing between nodes on the same level
|
||||
@@ -2334,6 +2332,57 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
maximum: 1
|
||||
default: 0.17
|
||||
|
||||
UsecaseDiagramConfig:
|
||||
title: Usecase Diagram Config
|
||||
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
|
||||
description: The object containing configurations specific for usecase diagrams.
|
||||
type: object
|
||||
unevaluatedProperties: false
|
||||
required:
|
||||
- useMaxWidth
|
||||
properties:
|
||||
actorFontSize:
|
||||
description: Font size for actor labels
|
||||
type: number
|
||||
minimum: 1
|
||||
default: 14
|
||||
actorFontFamily:
|
||||
description: Font family for actor labels
|
||||
type: string
|
||||
default: '"Open Sans", sans-serif'
|
||||
actorFontWeight:
|
||||
description: Font weight for actor labels
|
||||
type: string
|
||||
default: 'normal'
|
||||
usecaseFontSize:
|
||||
description: Font size for usecase labels
|
||||
type: number
|
||||
minimum: 1
|
||||
default: 12
|
||||
usecaseFontFamily:
|
||||
description: Font family for usecase labels
|
||||
type: string
|
||||
default: '"Open Sans", sans-serif'
|
||||
usecaseFontWeight:
|
||||
description: Font weight for usecase labels
|
||||
type: string
|
||||
default: 'normal'
|
||||
actorMargin:
|
||||
description: Margin around actors
|
||||
type: number
|
||||
minimum: 0
|
||||
default: 50
|
||||
usecaseMargin:
|
||||
description: Margin around use cases
|
||||
type: number
|
||||
minimum: 0
|
||||
default: 50
|
||||
diagramPadding:
|
||||
description: Padding around the entire diagram
|
||||
type: number
|
||||
minimum: 0
|
||||
default: 20
|
||||
|
||||
FontCalculator:
|
||||
title: Font Calculator
|
||||
description: |
|
||||
|
||||
5
packages/parser/antlr-config.json
Normal file
5
packages/parser/antlr-config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"projectName": "Mermaid",
|
||||
"grammars": [],
|
||||
"mode": "production"
|
||||
}
|
||||
@@ -19,7 +19,8 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist src/language/generated",
|
||||
"langium:generate": "langium generate",
|
||||
"langium:watch": "langium generate --watch"
|
||||
"langium:watch": "langium generate --watch",
|
||||
"antlr:generate": "tsx ../../.build/antlr-cli.ts antlr-config.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,9 +34,11 @@
|
||||
"ast"
|
||||
],
|
||||
"dependencies": {
|
||||
"antlr4ng": "^3.0.7",
|
||||
"langium": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"antlr-ng": "^1.0.10",
|
||||
"chevrotain": "^11.0.3"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { LangiumParser, ParseResult } from 'langium';
|
||||
|
||||
import type { Info, Packet, Pie, Architecture, GitGraph, Radar, Treemap } from './index.js';
|
||||
|
||||
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
|
||||
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar | Treemap;
|
||||
export type LangiumDiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar | Treemap;
|
||||
|
||||
const parsers: Record<string, LangiumParser> = {};
|
||||
const initializers = {
|
||||
@@ -51,7 +52,7 @@ export async function parse(diagramType: 'gitGraph', text: string): Promise<GitG
|
||||
export async function parse(diagramType: 'radar', text: string): Promise<Radar>;
|
||||
export async function parse(diagramType: 'treemap', text: string): Promise<Treemap>;
|
||||
|
||||
export async function parse<T extends DiagramAST>(
|
||||
export async function parse<T extends LangiumDiagramAST>(
|
||||
diagramType: keyof typeof initializers,
|
||||
text: string
|
||||
): Promise<T> {
|
||||
@@ -59,6 +60,7 @@ export async function parse<T extends DiagramAST>(
|
||||
if (!initializer) {
|
||||
throw new Error(`Unknown diagram type: ${diagramType}`);
|
||||
}
|
||||
|
||||
if (!parsers[diagramType]) {
|
||||
await initializer();
|
||||
}
|
||||
|
||||
134
pnpm-lock.yaml
generated
134
pnpm-lock.yaml
generated
@@ -17,8 +17,8 @@ importers:
|
||||
specifier: ^3.55.2
|
||||
version: 3.55.2(encoding@0.1.13)(typescript@5.7.3)
|
||||
'@argos-ci/cypress':
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1(cypress@14.5.4)
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3(cypress@14.5.4)
|
||||
'@changesets/changelog-github':
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1(encoding@0.1.13)
|
||||
@@ -235,6 +235,9 @@ importers:
|
||||
'@types/d3':
|
||||
specifier: ^7.4.3
|
||||
version: 7.4.3
|
||||
antlr4ng:
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.16
|
||||
cytoscape:
|
||||
specifier: ^3.33.1
|
||||
version: 3.33.1
|
||||
@@ -535,10 +538,16 @@ importers:
|
||||
|
||||
packages/parser:
|
||||
dependencies:
|
||||
antlr4ng:
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.16
|
||||
langium:
|
||||
specifier: 3.3.1
|
||||
version: 3.3.1
|
||||
devDependencies:
|
||||
antlr-ng:
|
||||
specifier: ^1.0.10
|
||||
version: 1.0.10
|
||||
chevrotain:
|
||||
specifier: ^11.0.3
|
||||
version: 11.0.3
|
||||
@@ -793,26 +802,26 @@ packages:
|
||||
resolution: {integrity: sha512-8mBaNNJ0zUBlb09ycc8aFTKajoqEu+E7M7kdV1IENIwuVOI3ecM6x9vr4ptWQz0LTnel7M+L3NPqAGJqoQ3AKA==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
|
||||
'@argos-ci/api-client@0.11.0':
|
||||
resolution: {integrity: sha512-mv7LWrJfEDjjs+CmAJaM1GIexpb3A8TwuyTUCTKgDp/SHdbU0uF8uC6lV4P/mfeGIvBYZzIRKq/frd+IETlC2g==}
|
||||
'@argos-ci/api-client@0.12.0':
|
||||
resolution: {integrity: sha512-WfhI+StLJKIKERWQaIm7Kv1/k+YO/CYIp3djDVhZIU6mv/8yalyNXHnkRC6ofq1kPpmRvoag1KW79/C2WsB4Ag==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@argos-ci/browser@5.0.0':
|
||||
resolution: {integrity: sha512-SKAD7EXoLX4u50dzTIT/ABnpD284+DnBfoJM0ZrTIav2eiiVJyknNKSznF5w118lYGnYvugTXbKMnukGPzJeOA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@argos-ci/core@4.1.5':
|
||||
resolution: {integrity: sha512-tPsbnSuHEClkdGLUU/qHTNsMe3kAPBvz0DK0nkv6Z18N0imEbzVg+ggmcTmc2x2yEm7i1V456Z2MLhFvTqXnlw==}
|
||||
'@argos-ci/core@4.2.0':
|
||||
resolution: {integrity: sha512-3RNyBZ84pYfQ8dn/Ivv5ls2x2rgqFuh8wA8e4ugggA5lx2dE7a6yghJw8cPzud+zbHrpOntl/HBM3akh2SXLkw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@argos-ci/cypress@6.1.1':
|
||||
resolution: {integrity: sha512-fs6K2o7vEiAjBtQhrB6cp7YG6beYBRI9WyVbAHRVYyhdEic36agAqQ7/q3tx8d+uf7nXjjtZuW7KGUxjBmC9MA==}
|
||||
'@argos-ci/cypress@6.1.3':
|
||||
resolution: {integrity: sha512-JlBabUsksKXH7QT2M47dhBNHRxNwW+GQ1lvBT/mgGaFJX8P/GqLkEEmKolf1YBn28MFemQmjuK4G+z5Pjs3rLg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
cypress: ^12.0.0 || ^13.0.0 || ^14.0.0
|
||||
|
||||
'@argos-ci/util@3.1.0':
|
||||
resolution: {integrity: sha512-QM0IwJGm9YsRdsvTAskQab9iXpQOTOOLb+h9Yev76L2TzoLZ2tM9QO+pYNNlX9YLK5dYr/H/pBNQ1lWr130Jjw==}
|
||||
'@argos-ci/util@3.1.1':
|
||||
resolution: {integrity: sha512-sGb9PS7yqdVVtxpxRD1Nfter3kaioC4nPPTknVmMSqo2GQKO1gdmjMJtwHY+Nf9FgiMfwpTCnk8Rrf0pjS3Sug==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
@@ -3908,10 +3917,20 @@ packages:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
antlr-ng@1.0.10:
|
||||
resolution: {integrity: sha512-fw3NdsQP3dabuZrDhKAMewrBsY5KSAcMrvhWBVDmHYegv5D51pypzCYK1PpjaRVKcVeP/5xKfqJY31TvXACOdA==}
|
||||
hasBin: true
|
||||
|
||||
antlr4@4.11.0:
|
||||
resolution: {integrity: sha512-GUGlpE2JUjAN+G8G5vY+nOoeyNhHsXoIJwP1XF1oRw89vifA1K46T6SEkwLwr7drihN7I/lf0DIjKc4OZvBX8w==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
antlr4ng@3.0.15:
|
||||
resolution: {integrity: sha512-VELFqTfcpGI2bj6ScMWuxM3FI6HOsojrgmnw3cCbUtsQ1DNOq32wJsjOt7vLvfIniyyuE1DIYegGcuFmn+jgyw==}
|
||||
|
||||
antlr4ng@3.0.16:
|
||||
resolution: {integrity: sha512-DQuJkC7kX3xunfF4K2KsWTSvoxxslv+FQp/WHQZTJSsH2Ec3QfFmrxC3Nky2ok9yglXn6nHM4zUaVDxcN5f6kA==}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
@@ -4475,6 +4494,10 @@ packages:
|
||||
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@13.1.0:
|
||||
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@14.0.1:
|
||||
resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -5608,6 +5631,10 @@ packages:
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-printf@1.6.10:
|
||||
resolution: {integrity: sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==}
|
||||
engines: {node: '>=10.0'}
|
||||
|
||||
fast-querystring@1.1.2:
|
||||
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
|
||||
|
||||
@@ -6076,6 +6103,10 @@ packages:
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
highlight.js@10.7.3:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
|
||||
@@ -7054,6 +7085,10 @@ packages:
|
||||
lunr@2.3.9:
|
||||
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
|
||||
|
||||
luxon@3.5.0:
|
||||
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
magic-string@0.25.9:
|
||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||
|
||||
@@ -7603,8 +7638,8 @@ packages:
|
||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
openapi-fetch@0.14.0:
|
||||
resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==}
|
||||
openapi-fetch@0.14.1:
|
||||
resolution: {integrity: sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==}
|
||||
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
@@ -7712,6 +7747,9 @@ packages:
|
||||
package-manager-detector@1.3.0:
|
||||
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
|
||||
|
||||
pako@0.2.9:
|
||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
@@ -8773,6 +8811,9 @@ packages:
|
||||
resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
stringtemplate4ts@1.0.9:
|
||||
resolution: {integrity: sha512-KYZm2bJlSjynG5Y+L46fkaKBQG6mhV6hb2RBA8dpx3/Vj6G4u7gwXNKYvaN9+QD5sj68/1srtSNDvqEso7MwsQ==}
|
||||
|
||||
strip-ansi@3.0.1:
|
||||
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -8960,6 +9001,9 @@ packages:
|
||||
thunky@1.1.0:
|
||||
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -9208,10 +9252,16 @@ packages:
|
||||
resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
unicode-properties@1.4.1:
|
||||
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
|
||||
|
||||
unicode-property-aliases-ecmascript@2.2.0:
|
||||
resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
unicode-trie@2.0.0:
|
||||
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
|
||||
|
||||
unicorn-magic@0.3.0:
|
||||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10298,19 +10348,19 @@ snapshots:
|
||||
|
||||
'@applitools/utils@1.12.0': {}
|
||||
|
||||
'@argos-ci/api-client@0.11.0':
|
||||
'@argos-ci/api-client@0.12.0':
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
openapi-fetch: 0.14.0
|
||||
openapi-fetch: 0.14.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@argos-ci/browser@5.0.0': {}
|
||||
|
||||
'@argos-ci/core@4.1.5':
|
||||
'@argos-ci/core@4.2.0':
|
||||
dependencies:
|
||||
'@argos-ci/api-client': 0.11.0
|
||||
'@argos-ci/util': 3.1.0
|
||||
'@argos-ci/api-client': 0.12.0
|
||||
'@argos-ci/util': 3.1.1
|
||||
convict: 6.2.4
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
fast-glob: 3.3.3
|
||||
@@ -10319,17 +10369,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@argos-ci/cypress@6.1.1(cypress@14.5.4)':
|
||||
'@argos-ci/cypress@6.1.3(cypress@14.5.4)':
|
||||
dependencies:
|
||||
'@argos-ci/browser': 5.0.0
|
||||
'@argos-ci/core': 4.1.5
|
||||
'@argos-ci/util': 3.1.0
|
||||
'@argos-ci/core': 4.2.0
|
||||
'@argos-ci/util': 3.1.1
|
||||
cypress: 14.5.4
|
||||
cypress-wait-until: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@argos-ci/util@3.1.0': {}
|
||||
'@argos-ci/util@3.1.1': {}
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
dependencies:
|
||||
@@ -13966,8 +14016,19 @@ snapshots:
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
antlr-ng@1.0.10:
|
||||
dependencies:
|
||||
antlr4ng: 3.0.16
|
||||
commander: 13.1.0
|
||||
stringtemplate4ts: 1.0.9
|
||||
unicode-properties: 1.4.1
|
||||
|
||||
antlr4@4.11.0: {}
|
||||
|
||||
antlr4ng@3.0.15: {}
|
||||
|
||||
antlr4ng@3.0.16: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
@@ -14576,6 +14637,8 @@ snapshots:
|
||||
|
||||
commander@12.1.0: {}
|
||||
|
||||
commander@13.1.0: {}
|
||||
|
||||
commander@14.0.1: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
@@ -16056,6 +16119,8 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-printf@1.6.10: {}
|
||||
|
||||
fast-querystring@1.1.2:
|
||||
dependencies:
|
||||
fast-decode-uri-component: 1.0.1
|
||||
@@ -16611,6 +16676,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
highlight.js@10.7.3: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
@@ -17785,6 +17852,8 @@ snapshots:
|
||||
|
||||
lunr@2.3.9: {}
|
||||
|
||||
luxon@3.5.0: {}
|
||||
|
||||
magic-string@0.25.9:
|
||||
dependencies:
|
||||
sourcemap-codec: 1.4.8
|
||||
@@ -18528,7 +18597,7 @@ snapshots:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
openapi-fetch@0.14.0:
|
||||
openapi-fetch@0.14.1:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.15
|
||||
|
||||
@@ -18631,6 +18700,8 @@ snapshots:
|
||||
|
||||
package-manager-detector@1.3.0: {}
|
||||
|
||||
pako@0.2.9: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
pako@2.1.0: {}
|
||||
@@ -19887,6 +19958,13 @@ snapshots:
|
||||
is-obj: 1.0.1
|
||||
is-regexp: 1.0.0
|
||||
|
||||
stringtemplate4ts@1.0.9:
|
||||
dependencies:
|
||||
antlr4ng: 3.0.15
|
||||
fast-printf: 1.6.10
|
||||
he: 1.2.0
|
||||
luxon: 3.5.0
|
||||
|
||||
strip-ansi@3.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 2.1.1
|
||||
@@ -20099,6 +20177,8 @@ snapshots:
|
||||
|
||||
thunky@1.1.0: {}
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
@@ -20322,8 +20402,18 @@ snapshots:
|
||||
|
||||
unicode-match-property-value-ecmascript@2.2.1: {}
|
||||
|
||||
unicode-properties@1.4.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
unicode-trie: 2.0.0
|
||||
|
||||
unicode-property-aliases-ecmascript@2.2.0: {}
|
||||
|
||||
unicode-trie@2.0.0:
|
||||
dependencies:
|
||||
pako: 0.2.9
|
||||
tiny-inflate: 1.0.3
|
||||
|
||||
unicorn-magic@0.3.0: {}
|
||||
|
||||
unified@11.0.5:
|
||||
|
||||
230
scripts/antlr-generate.mts
Normal file
230
scripts/antlr-generate.mts
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console */
|
||||
/* cspell:disable */
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
||||
import { join, dirname, basename } from 'path';
|
||||
|
||||
/**
|
||||
* Generic ANTLR generator script that finds all .g4 files and generates parsers
|
||||
* Automatically creates generated folders and runs antlr4ng for each diagram type
|
||||
*/
|
||||
|
||||
interface GrammarInfo {
|
||||
lexerFile: string;
|
||||
parserFile: string;
|
||||
outputDir: string;
|
||||
diagramType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all .g4 files in a directory
|
||||
*/
|
||||
function findG4Files(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
files.push(...findG4Files(fullPath));
|
||||
} else if (entry.endsWith('.g4')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all ANTLR grammar files in the diagrams directory
|
||||
*/
|
||||
function findGrammarFiles(): GrammarInfo[] {
|
||||
const grammarFiles: GrammarInfo[] = [];
|
||||
|
||||
// Determine the correct path based on current working directory
|
||||
const cwd = process.cwd();
|
||||
let diagramsPath: string;
|
||||
|
||||
if (cwd.endsWith('/packages/mermaid')) {
|
||||
// Running from mermaid package directory
|
||||
diagramsPath = 'src/diagrams';
|
||||
} else {
|
||||
// Running from project root
|
||||
diagramsPath = 'packages/mermaid/src/diagrams';
|
||||
}
|
||||
|
||||
// Find all .g4 files
|
||||
const g4Files = findG4Files(diagramsPath);
|
||||
|
||||
// Group by directory (each diagram should have a Lexer and Parser pair)
|
||||
const grammarDirs = new Map<string, string[]>();
|
||||
|
||||
for (const file of g4Files) {
|
||||
const dir = dirname(file);
|
||||
if (!grammarDirs.has(dir)) {
|
||||
grammarDirs.set(dir, []);
|
||||
}
|
||||
grammarDirs.get(dir)!.push(file);
|
||||
}
|
||||
|
||||
// Process each directory
|
||||
for (const [dir, files] of grammarDirs) {
|
||||
const lexerFile = files.find((f) => f.includes('Lexer.g4'));
|
||||
const parserFile = files.find((f) => f.includes('Parser.g4'));
|
||||
|
||||
if (lexerFile && parserFile) {
|
||||
// Extract diagram type from path
|
||||
const pathParts = dir.split('/');
|
||||
const diagramIndex = pathParts.indexOf('diagrams');
|
||||
const diagramType = diagramIndex >= 0 ? pathParts[diagramIndex + 1] : 'unknown';
|
||||
|
||||
grammarFiles.push({
|
||||
lexerFile,
|
||||
parserFile,
|
||||
outputDir: join(dir, 'generated'),
|
||||
diagramType,
|
||||
});
|
||||
} else {
|
||||
console.warn(`⚠️ Incomplete grammar pair in ${dir}:`);
|
||||
console.warn(` Lexer: ${lexerFile ?? 'MISSING'}`);
|
||||
console.warn(` Parser: ${parserFile ?? 'MISSING'}`);
|
||||
}
|
||||
}
|
||||
|
||||
return grammarFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the generated directory
|
||||
*/
|
||||
function cleanGeneratedDir(outputDir: string): void {
|
||||
try {
|
||||
execSync(`rimraf "${outputDir}"`, { stdio: 'inherit' });
|
||||
console.log(`🧹 Cleaned: ${outputDir}`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to clean ${outputDir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the generated directory if it doesn't exist
|
||||
*/
|
||||
function ensureGeneratedDir(outputDir: string): void {
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
console.log(`📁 Created: ${outputDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ANTLR files for a grammar pair
|
||||
*/
|
||||
function generateAntlrFiles(grammar: GrammarInfo): void {
|
||||
const { lexerFile, parserFile, outputDir, diagramType } = grammar;
|
||||
|
||||
console.log(`\n🎯 Generating ANTLR files for ${diagramType} diagram...`);
|
||||
console.log(` Lexer: ${basename(lexerFile)}`);
|
||||
console.log(` Parser: ${basename(parserFile)}`);
|
||||
console.log(` Output: ${outputDir}`);
|
||||
|
||||
try {
|
||||
// Clean and create output directory
|
||||
cleanGeneratedDir(outputDir);
|
||||
ensureGeneratedDir(outputDir);
|
||||
|
||||
// Determine common header lib path for imported grammars
|
||||
const cwd = process.cwd();
|
||||
const commonLibPath = cwd.endsWith('/packages/mermaid')
|
||||
? 'src/diagrams/common/parser/antlr'
|
||||
: 'packages/mermaid/src/diagrams/common/parser/antlr';
|
||||
|
||||
// TODO: Use JS api instead of CLI
|
||||
// Generate ANTLR files
|
||||
const command = [
|
||||
'pnpm',
|
||||
'dlx',
|
||||
'antlr-ng',
|
||||
'-Dlanguage=TypeScript',
|
||||
'-l',
|
||||
'-v',
|
||||
`--lib "${commonLibPath}"`,
|
||||
`-o "${outputDir}"`,
|
||||
`"${lexerFile}"`,
|
||||
`"${parserFile}"`,
|
||||
].join(' ');
|
||||
|
||||
console.log(` Command: ${command}`);
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
|
||||
console.log(`✅ Successfully generated ANTLR files for ${diagramType}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to generate ANTLR files for ${diagramType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
export function generateAntlr(): void {
|
||||
console.log('🚀 ANTLR Generator - Finding and generating all grammar files...\n');
|
||||
|
||||
try {
|
||||
// Find all grammar files
|
||||
const grammarFiles = findGrammarFiles();
|
||||
|
||||
if (grammarFiles.length === 0) {
|
||||
console.log('ℹ️ No ANTLR grammar files found.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📋 Found ${grammarFiles.length} diagram(s) with ANTLR grammars:`);
|
||||
for (const grammar of grammarFiles) {
|
||||
console.log(` • ${grammar.diagramType}`);
|
||||
}
|
||||
|
||||
// Generate files for each grammar
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
for (const grammar of grammarFiles) {
|
||||
try {
|
||||
generateAntlrFiles(grammar);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
failureCount++;
|
||||
console.error(`Failed to process ${grammar.diagramType}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n📊 Generation Summary:');
|
||||
console.log(` ✅ Successful: ${successCount}`);
|
||||
console.log(` ❌ Failed: ${failureCount}`);
|
||||
console.log(` 📁 Total: ${grammarFiles.length}`);
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.error('\n❌ Some ANTLR generations failed. Check the errors above.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\n🎉 All ANTLR files generated successfully!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Fatal error during ANTLR generation:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
generateAntlr();
|
||||
}
|
||||
119
scripts/antlr-watch.mts
Executable file
119
scripts/antlr-watch.mts
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import chokidar from 'chokidar';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* ANTLR Watch Script
|
||||
*
|
||||
* This script generates ANTLR files and then watches for changes to .g4 grammar files,
|
||||
* automatically regenerating the corresponding parsers when changes are detected.
|
||||
*
|
||||
* Features:
|
||||
* - Initial generation of all ANTLR files
|
||||
* - Watch .g4 files for changes
|
||||
* - Debounced regeneration to avoid multiple builds
|
||||
* - Clear logging and progress reporting
|
||||
* - Graceful shutdown handling
|
||||
*/
|
||||
|
||||
let isGenerating = false;
|
||||
let timeoutID: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Generate ANTLR parser files from grammar files
|
||||
*/
|
||||
function generateAntlr(): void {
|
||||
if (isGenerating) {
|
||||
console.log('⏳ ANTLR generation already in progress, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isGenerating = true;
|
||||
console.log('🎯 ANTLR: Generating parser files...');
|
||||
execSync('tsx scripts/antlr-generate.mts', { stdio: 'inherit' });
|
||||
console.log('✅ ANTLR: Parser files generated successfully\n');
|
||||
} catch (error) {
|
||||
console.error('❌ ANTLR: Failed to generate parser files:', error);
|
||||
} finally {
|
||||
isGenerating = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file change events with debouncing
|
||||
*/
|
||||
function handleFileChange(path: string): void {
|
||||
if (timeoutID !== undefined) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
|
||||
console.log(`🎯 Grammar file changed: ${path}`);
|
||||
|
||||
// Debounce file changes to avoid multiple regenerations
|
||||
timeoutID = setTimeout(() => {
|
||||
console.log('🔄 Regenerating ANTLR files...\n');
|
||||
generateAntlr();
|
||||
timeoutID = undefined;
|
||||
}, 500); // 500ms debounce
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup graceful shutdown
|
||||
*/
|
||||
function setupGracefulShutdown(): void {
|
||||
const shutdown = () => {
|
||||
console.log('\n🛑 Shutting down ANTLR watch...');
|
||||
if (timeoutID) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main(): void {
|
||||
console.log('🚀 ANTLR Watch - Generate and watch grammar files for changes\n');
|
||||
|
||||
// Setup graceful shutdown
|
||||
setupGracefulShutdown();
|
||||
|
||||
// Initial generation
|
||||
generateAntlr();
|
||||
|
||||
// Setup file watcher
|
||||
console.log('👀 Watching for .g4 file changes...');
|
||||
console.log('📁 Pattern: **/src/**/parser/antlr/*.g4');
|
||||
console.log('🛑 Press Ctrl+C to stop watching\n');
|
||||
|
||||
const watcher = chokidar.watch('**/src/**/parser/antlr/*.g4', {
|
||||
ignoreInitial: true,
|
||||
ignored: [/node_modules/, /dist/, /docs/, /coverage/],
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
watcher
|
||||
.on('change', handleFileChange)
|
||||
.on('add', handleFileChange)
|
||||
.on('error', (error) => {
|
||||
console.error('❌ Watcher error:', error);
|
||||
})
|
||||
.on('ready', () => {
|
||||
console.log('✅ Watcher ready - monitoring grammar files for changes...\n');
|
||||
});
|
||||
|
||||
// Keep the process alive
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
// Run the script
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user