Compare commits

..

94 Commits

Author SHA1 Message Date
omkarht
cfed700a58 chore: modified the file structure to implement the new way of antlr parsing
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-14 18:24:37 +05:30
omkarht
b715d82458 feat: add support for new arrow types and enhance use case diagram features
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 19:07:11 +05:30
omkarht
5b2b3b8ae9 feat: enhance use case diagram support with arrow types, class definitions and styles 2025-10-13 18:46:47 +05:30
omkarht
b7ff1920a9 fix: add ellipse shape to the shapes table in documentation
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-09 19:32:10 +05:30
omkarht
58c06ed770 fix: exclude generated files from circular dependency check
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-09 19:19:55 +05:30
omkarht
5e05f91b7d fix: remove TypeScript error suppression for broken ellipse rendering 2025-10-09 18:55:01 +05:30
omkarht
5fd06ccbac 6806: Adding support for the new use-case diagram type
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-09 18:48:11 +05:30
Shubham P
c728d864c8 Merge pull request #7054 from mermaid-js/fix/update-argos-ci-version
fix: update @argos-ci/cypress to version 6.1.3
2025-10-07 08:34:58 +00:00
shubhamparikh2704
99f17bea3a fix: update @argos-ci/cypress to version 6.1.3 2025-10-07 13:39:28 +05:30
Ashish Jain
c1c14e401a Merge pull request #7019 from mermaid-js/tmp-mindmap-elk
mindmap breaking issue in ELK layout
2025-09-30 09:02:40 +00:00
darshanr0107
8b3057f27c fix: guard nodeDb[node.id] against undefined
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-30 12:27:53 +05:30
Shubham P
717d3b3bb2 Merge pull request #6984 from mermaid-js/fix/er-diagram-syntax-error-special-chars
fix(er-diagram): handle syntax errors for special characters in node names
2025-09-29 08:37:03 +00:00
Shubham P
2f8d9ba958 Merge pull request #6789 from mermaid-js/6638-sequence-diagram-additional-messages
6638:sequence diagram additional messages
2025-09-29 08:08:33 +00:00
darshanr0107
ace0367afd chore: add changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-29 13:37:10 +05:30
Knut Sveidqvist
b983626587 Fix for reference issue affecting mindmaps 2025-09-26 15:24:20 +02:00
Shubham P
7effdc147b Merge pull request #6997 from mermaid-js/ci/enable-codeql-for-github-actions
ci(codeql): enable CodeQL for GitHub Actions
2025-09-25 08:47:48 +00:00
Alois Klink
6e67515f41 ci(codeql): enable CodeQL for GitHub Actions
Support for scanning GitHub Actions was added in 2024-12-17, see
https://github.blog/changelog/2024-12-17-find-and-fix-actions-workflows-vulnerabilities-with-codeql-public-preview/
2025-09-25 17:17:12 +09:00
omkarht
09b74f1c29 chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-23 13:39:45 +05:30
omkarht
880da21908 test: add tests for handling special characters and numeric entity names
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-23 13:29:19 +05:30
omkarht
38191243be Merge branch 'develop' into fix/er-diagram-syntax-error-special-chars 2025-09-23 12:41:42 +05:30
omkarht
b75dcb8a82 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-23 12:40:39 +05:30
omkarht
4c1e170f4a fix(er-diagram): handle syntax errors for special characters in node names 2025-09-23 12:39:29 +05:30
Shubham P
d5c4eff251 Merge pull request #6972 from mermaid-js/renovate/patch-all-patch
fix(deps): update all patch dependencies (patch)
2025-09-22 13:49:30 +00:00
renovate[bot]
5324fd8dfd fix(deps): update all patch dependencies 2025-09-22 13:35:45 +00:00
omkarht
bd25b88a01 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-22 13:36:01 +05:30
Sidharth Vinod
37e3a6951b Merge pull request #6956 from mermaid-js/develop
Pre Release
2025-09-17 23:15:32 +05:30
Sidharth Vinod
0de0b063e4 Merge pull request #6958 from dijitali/fix/arch-icon-docs
fix(docs) correct phrasing for architecture icon installation
2025-09-17 23:06:48 +05:30
Shubham P
619515e5a9 Merge pull request #6925 from mermaid-js/renovate/patch-all-patch
fix(deps): update all patch dependencies (patch)
2025-09-17 14:16:47 +00:00
shubhamparikh2704
59c8b07509 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into renovate/patch-all-patch 2025-09-17 19:34:56 +05:30
Sidharth Vinod
9e72bbf62d Merge pull request #6955 from mermaid-js/sidv/updateArgos
chore: Update argos to 6.1.0
2025-09-17 18:48:40 +05:30
Sidharth Vinod
2a2c46f1e2 chore: Update argos to 6.1.1 2025-09-17 18:14:03 +05:30
Shubham P
f25df353d4 Merge pull request #6959 from mermaid-js/remove-changeset
chore: remove duplicate changeset file
2025-09-17 10:50:09 +00:00
shubhamparikh2704
398345a8bc chore: remove duplicate changeset file 2025-09-17 16:07:45 +05:30
autofix-ci[bot]
7fd2d94ef7 [autofix.ci] apply automated fixes 2025-09-17 08:47:15 +00:00
Ieuan Jenkins
bcc1472b9d fix(docs) correct phrasing for architecture icon installation
docs gave the incorrect impression that iconify.design icons came preinstalled
2025-09-17 09:21:42 +01:00
shubhamparikh2704
ddc1cfe6c8 Revert "fix: downgrade @argos-ci/cypress to version 5.0.2"
This reverts commit 18e9c1174d.
2025-09-17 13:07:43 +05:30
omkarht
d3de3ecbbb Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-17 12:41:51 +05:30
shubhamparikh2704
18e9c1174d fix: downgrade @argos-ci/cypress to version 5.0.2 2025-09-17 12:14:29 +05:30
Sidharth Vinod
789018abf6 chore: Update argos to 6.1.0 2025-09-17 11:38:57 +05:30
Shubham P
16569b295b Merge pull request #6942 from mermaid-js/renovate/peter-evans-create-pull-request-digest
chore(deps): update peter-evans/create-pull-request digest to 915d841
2025-09-16 17:28:34 +00:00
renovate[bot]
11a35c11ee chore(deps): update peter-evans/create-pull-request digest to 915d841 2025-09-16 16:02:14 +00:00
Shubham P
216be22801 Merge pull request #6945 from mermaid-js/fix/gantt-crash-obsidian-browser
6920: Fix Gantt chart crashes in Obsidian and browser
2025-09-16 15:49:49 +00:00
Ashish Jain
e87f77a865 Merge pull request #6950 from mermaid-js/patch/mindmap-fix
Pre Release
2025-09-16 12:48:55 +02:00
shubhamparikh2704
a9579083bf chore: add changeset for fixing mindmap rendering and applying tidytree layout 2025-09-16 13:33:31 +05:30
Knut Sveidqvist
6fd78d0856 Merge pull request #6944 from mermaid-js/docs/fix-mindmap-rendering
6932:Fix mindmap rendering in docs and apply tidytree layout
2025-09-16 13:27:23 +05:30
Sidharth Vinod
531f5e9380 Merge pull request #6948 from mermaid-js/add-newest-blog-posts
DOCS: add newest blog posts
2025-09-16 11:45:06 +05:30
Steph
ad4c227477 add newest blog posts 2025-09-15 13:24:39 -07:00
omkarht
3964ce0a0f Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-15 18:15:59 +05:30
shubhamparikh2704
a44e3e992c refactor: remove unused dependencies from pnpm-lock.yaml 2025-09-15 14:37:50 +05:30
shubhamparikh2704
ca5b370ffb Merge branch 'develop' of https://github.com/mermaid-js/mermaid into renovate/patch-all-patch 2025-09-15 14:32:02 +05:30
omkarht
4dbabba8e8 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-15 12:50:11 +05:30
darshanr0107
d318f1a13c chore: add changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-15 12:42:25 +05:30
darshanr0107
525a7de8ae fix:gant chart crashing in browser
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-15 12:42:24 +05:30
Sidharth Vinod
450754221e Merge pull request #6930 from mermaid-js/rajat-ht/chore-editor-selection-update
Chore: Simplify editor selection to single version
2025-09-11 19:57:08 +05:30
rajat-ht
7a4f5b62c9 chore: update variable name 2025-09-11 19:54:14 +05:30
omkarht
e3ef5e4208 fix: fixed central connection for bidirectional dotted arrow
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-11 13:03:06 +05:30
omkarht
daeb85bac2 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-11 12:40:55 +05:30
Shubham P
cf08ba0ef8 Merge branch 'develop' into renovate/patch-all-patch 2025-09-10 21:03:45 +05:30
rajat-ht
4829dfa8c5 Chore: Simplify editor selection to single version 2025-09-09 22:16:52 +05:30
autofix-ci[bot]
e7811886c3 [autofix.ci] apply automated fixes 2025-09-08 01:00:25 +00:00
renovate[bot]
32eda8565c fix(deps): update all patch dependencies 2025-09-08 00:54:41 +00:00
omkarht
2cdaf03ada Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-05 12:12:01 +05:30
Sidharth Vinod
9c85521e16 Merge pull request #6914 from mermaid-js/changeset-release/master
Version Packages
2025-09-04 20:36:52 +05:30
github-actions[bot]
8a565bce92 Version Packages 2025-09-04 14:12:57 +00:00
Sidharth Vinod
baf510b935 Merge pull request #6912 from mermaid-js/develop
Pre Release
2025-09-04 19:39:17 +05:30
omkarht
f6fa0260e7 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-04 12:56:49 +05:30
omkarht
29aad6d23c Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-04 12:12:03 +05:30
omkarht
82ef7b5fdb docs: add version placeholders for new features 2025-09-02 19:02:02 +05:30
omkarht
11cd3f1262 feat: add central connection rendering and parsing tests 2025-09-02 18:52:18 +05:30
omkarht
ac4aa94e78 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-02 15:13:07 +05:30
omkarht
c40faac80d Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-08-29 13:26:48 +05:30
omkarht
c530baed3f Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-08-28 13:07:57 +05:30
omkarht
045699de10 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into 6638-sequence-diagram-additional-messages 2025-08-25 15:14:50 +05:30
omkarht
1988d24227 fix: fixed reverse arrows placing for autonumber
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-22 16:46:49 +05:30
omkarht
39f90debe7 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-08-22 13:03:01 +05:30
omkarht
73e9849f99 chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-21 16:12:25 +05:30
omkarht
5a05540a5f Merge branch 'develop' of https://github.com/mermaid-js/mermaid into 6638-sequence-diagram-additional-messages 2025-08-21 16:09:18 +05:30
omkarht
2b58df9665 fix: refactored documentation
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-19 14:53:08 +05:30
omkarht
0b42bdba07 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-08-11 15:28:48 +05:30
omkarht
74c96db3e2 docs: document new syntax for half-arrow message and central connection
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 12:47:31 +05:30
omkarht
bd47c57eaf chore: refactored code
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-06 18:56:58 +05:30
omkarht
3e5d2db514 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-08-06 18:56:18 +05:30
omkarht
40990bb096 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-07-29 18:17:34 +05:30
omkarht
7ca0665764 fix: fixed failing test case
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-28 19:13:51 +05:30
omkarht
81a6a361ab Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-07-28 16:51:38 +05:30
omkarht
62faacdeeb fix: fixed failing test cases
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-28 16:01:43 +05:30
omkarht
0e40d8e8a8 fix: fixed failing test cases
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-28 15:55:07 +05:30
omkarht
e8d6daf4f6 add support for central connection circle after arrow in lifeline direction
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-28 15:37:33 +05:30
omkarht
cb4ed605b2 chore: added rendering test cases for arrow head type
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-23 16:59:04 +05:30
omkarht
ba9db26bfa Lexing : Added Support for new arrow types through lexing
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-22 19:25:27 +05:30
omkarht
252b1837f7 added arrowhead types for dotted line type
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-22 19:20:06 +05:30
omkarht
6b9c15d7f0 added reverse arrow head types
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-22 16:30:21 +05:30
omkarht
fda640c90c fix: adjusted arrowhead design
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-21 19:03:29 +05:30
omkarht
584a789183 6638: add support for additional message types for sequence diagram
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-21 13:33:52 +05:30
122 changed files with 10821 additions and 6694 deletions

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

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

5
.build/generateAntlr.ts Normal file
View File

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

View File

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

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
feat: Add custom images and icons support for sequence diagram actors

View File

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

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Handle arrows correctly when auto number is enabled

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
chore: Fix mindmap rendering in docs and apply tidytree layout

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
Add IDs in architecture diagrams

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.

View File

@@ -1,7 +0,0 @@
---
'mermaid': minor
'@mermaid-js/layout-tidy-tree': minor
'@mermaid-js/layout-elk': minor
---
feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Add half-arrowheads (solid & stick) and central connection support

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Resolve gantt chart crash due to invalid array length

View File

@@ -0,0 +1,5 @@
---
'@mermaid': patch
---
fix: Mindmap breaking in ELK layout

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix(er-diagram): prevent syntax error when using 'u', numbers, and decimals in node names

View File

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

View File

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

View File

@@ -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();
});

View File

@@ -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:
@@ -36,7 +36,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
uses: github/codeql-action/init@5378192d256ef1302a6980fffe5ca04426d43091 # v3.28.21
with:
config-file: ./.github/codeql/codeql-config.yml
languages: ${{ matrix.language }}
@@ -48,7 +48,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
uses: github/codeql-action/autobuild@5378192d256ef1302a6980fffe5ca04426d43091 # v3.28.21
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -62,4 +62,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
uses: github/codeql-action/analyze@5378192d256ef1302a6980fffe5ca04426d43091 # v3.28.21

View File

@@ -53,7 +53,7 @@ jobs:
args: -X POST "$APPLITOOLS_SERVER_URL/api/externals/github/push?apiKey=$APPLITOOLS_API_KEY&CommitSha=$GITHUB_SHA&BranchName=${APPLITOOLS_BRANCH}$&ParentBranchName=$APPLITOOLS_PARENT_BRANCH"
- name: Cypress run
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
id: cypress
with:
start: pnpm run dev

View File

@@ -27,12 +27,12 @@ jobs:
with:
node-version-file: '.node-version'
- name: Install dependencies
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
with:
runTests: false
- name: Cypress run
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
id: cypress
with:
install: false
@@ -58,7 +58,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Commit and create pull request
uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a
uses: peter-evans/create-pull-request@915d841dae6a4f191bb78faf61a257411d7be4d2
with:
add-paths: |
cypress/timings.json

View File

@@ -45,7 +45,7 @@ jobs:
node-version-file: '.node-version'
- name: Cache snapshots
id: cache-snapshot
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ./cypress/snapshots
key: ${{ runner.os }}-snapshots-${{ env.targetHash }}
@@ -59,7 +59,7 @@ jobs:
- name: Install dependencies
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
with:
# just perform install
runTests: false
@@ -95,13 +95,13 @@ jobs:
# These cached snapshots are downloaded, providing the reference snapshots.
- name: Cache snapshots
id: cache-snapshot
uses: actions/cache/restore@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ./cypress/snapshots
key: ${{ runner.os }}-snapshots-${{ env.targetHash }}
- name: Install dependencies
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
with:
runTests: false
@@ -117,7 +117,7 @@ jobs:
# Install NPM dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
id: cypress
with:
install: false

View File

@@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Restore lychee cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: .lycheecache
key: cache-lychee-${{ github.sha }}

View File

@@ -36,7 +36,7 @@ jobs:
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9
uses: changesets/action@06245a4e0a36c064a573d4150030f5ec548e4fcc # v1.4.10
with:
version: pnpm changeset:version
publish: pnpm changeset:publish

View File

@@ -20,18 +20,18 @@ jobs:
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
uses: github/codeql-action/upload-sarif@5378192d256ef1302a6980fffe5ca04426d43091 # v3.28.21
with:
sarif_file: results.sarif

View File

@@ -19,7 +19,7 @@ jobs:
message: 'chore: update browsers list'
push: false
- name: Create Pull Request
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
branch: update-browserslist
title: Update Browserslist

5
.gitignore vendored
View File

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

View File

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

View File

@@ -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 }
);
});
});
});

View File

@@ -803,4 +803,34 @@ describe('Gantt diagram', () => {
{}
);
});
it('should handle numeric timestamps with dateFormat x', () => {
imgSnapshotTest(
`
gantt
title Process time profile (ms)
dateFormat x
axisFormat %L
tickInterval 250millisecond
section Pipeline
Parse JSON p1: 000, 120
`,
{}
);
});
it('should handle numeric timestamps with dateFormat X', () => {
imgSnapshotTest(
`
gantt
title Process time profile (ms)
dateFormat X
axisFormat %L
tickInterval 250millisecond
section Pipeline
Parse JSON p1: 000, 120
`,
{}
);
});
});

View File

@@ -1,118 +0,0 @@
import { imgSnapshotTest } from '../../helpers/util';
const looks = ['classic'] as const;
looks.forEach((look) => {
describe(`SequenceDiagram icon participants in ${look} look`, () => {
it(`single participant with icon`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
Note over Bob: Icon participant`;
imgSnapshotTest(diagram, { look });
});
it(`two participants, one icon and one normal`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: Hello`;
imgSnapshotTest(diagram, { look });
});
it(`two icon participants`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice@{ type: "icon", icon: "fa:user" }
Bob->>Alice: Hello
Alice-->>Bob: Hi`;
imgSnapshotTest(diagram, { look });
});
it(`with markdown htmlLabels:true content`, () => {
// html/markdown in messages/notes (participants themselves don't support label/form/w/h)
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
imgSnapshotTest(diagram, { look });
});
it(`with markdown htmlLabels:false content`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
imgSnapshotTest(diagram, {
look,
htmlLabels: false,
flowchart: { htmlLabels: false },
});
});
it(`with styles applied to participant`, () => {
// style by participant id
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: Styled participant
`;
imgSnapshotTest(diagram, { look });
});
it(`with classDef and class application`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "fa:bell" }
participant Alice
Bob->>Alice: Classed participant
`;
imgSnapshotTest(diagram, { look });
});
});
});
// Colored emoji icon tests (analogous to the flowchart colored icon tests), no direction line.
describe('SequenceDiagram colored icon participant', () => {
it('colored emoji icon without styles', () => {
const icon = 'fluent-emoji:tropical-fish';
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "${icon}" }
Note over Bob: colored emoji icon
`;
imgSnapshotTest(diagram);
});
it('colored emoji icon with styles', () => {
const icon = 'fluent-emoji:tropical-fish';
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "${icon}" }
`;
imgSnapshotTest(diagram);
});
});
// Mixed scenario: multiple interactions, still no direction line.
describe('SequenceDiagram icon participant with multiple interactions', () => {
const icon = 'fa:bell-slash';
it('icon participant interacts with two normal participants', () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "icon", icon: "${icon}" }
participant Alice
participant Carol
Bob->>Alice: Ping
Alice-->>Bob: Pong
Bob->>Carol: Notify
Note right of Bob: Icon side note`;
imgSnapshotTest(diagram);
});
});

View File

@@ -1,94 +0,0 @@
import { imgSnapshotTest } from '../../helpers/util';
const looks = ['classic'] as const;
looks.forEach((look) => {
describe(`SequenceDiagram image participants in ${look} look`, () => {
it(`single participant with image`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
Note over Bob: Image participant`;
imgSnapshotTest(diagram, { look });
});
it(`two participants, one image and one normal`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: Hello`;
imgSnapshotTest(diagram, { look });
});
it(`two image participants`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
Bob->>Alice: Hello
Alice-->>Bob: Hi`;
imgSnapshotTest(diagram, { look });
});
it(`with markdown htmlLabels:true content`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
imgSnapshotTest(diagram, { look });
});
it(`with markdown htmlLabels:false content`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
imgSnapshotTest(diagram, {
look,
htmlLabels: false,
flowchart: { htmlLabels: false },
});
});
it(`with styles applied to participant`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: Styled participant
`;
imgSnapshotTest(diagram, { look });
});
it(`with classDef and class application`, () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: Classed participant
`;
imgSnapshotTest(diagram, { look });
});
});
});
// Mixed scenario: multiple interactions, still no direction line.
describe('SequenceDiagram image participant with multiple interactions', () => {
const imageUrl = 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg';
it('image participant interacts with two normal participants', () => {
const diagram = `
sequenceDiagram
participant Bob@{ type: "image", "image": "${imageUrl}" }
participant Alice
participant Carol
Bob->>Alice: Ping
Alice-->>Bob: Pong
Bob->>Carol: Notify
Note right of Bob: Image side note`;
imgSnapshotTest(diagram);
});
});

View File

@@ -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 } }
);
});
});
});
});

View File

@@ -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`
);
});
});
});

View File

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

View File

@@ -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&lt;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

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2020",
"lib": ["es2020", "dom"],
"types": ["cypress", "node", "@argos-ci/cypress/dist/support.d.ts"],
"types": ["cypress", "node", "@argos-ci/cypress/support"],
"allowImportingTsExtensions": true,
"noEmit": true
},

View File

@@ -11,7 +11,7 @@
rel="stylesheet"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
rel="stylesheet"
/>
<link

234
demos/usecase.html Normal file
View File

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

View File

@@ -29,7 +29,8 @@ In GitHub, you first [**fork a mermaid repository**](https://github.com/mermaid-
Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with.
> **💡 Tip** > [Here is a GitHub document that gives an overview of the process](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
> **💡 Tip**
> [Here is a GitHub document that gives an overview of the process](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
```bash
git clone git@github.com/your-fork/mermaid

View File

@@ -33,7 +33,8 @@ mindmap
## Join the Development
> **💡 Tip** > **Check out our** [**detailed contribution guide**](./contributing.md).
> **💡 Tip**
> **Check out our** [**detailed contribution guide**](./contributing.md).
Where to start:
@@ -47,7 +48,8 @@ Where to start:
## A Question Or a Suggestion?
> **💡 Tip** > **Have a look at** [**how to open an issue**](./questions-and-suggestions.md).
> **💡 Tip**
> **Have a look at** [**how to open an issue**](./questions-and-suggestions.md).
If you have faced a vulnerability [report it to us](./security.md).

View File

@@ -22,7 +22,6 @@ While directives allow you to change most of the default configuration settings,
Mermaid basically supports two types of configuration options to be overridden by directives.
1. _General/Top Level configurations_ : These are the configurations that are available and applied to all the diagram. **Some of the most important top-level** configurations are:
- theme
- fontFamily
- logLevel

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
# Interface: ParseOptions
Defined in: [packages/mermaid/src/types.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L76)
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:76](https://github.com/mermaid-js/mer
> `optional` **suppressErrors**: `boolean`
Defined in: [packages/mermaid/src/types.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L81)
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
The `parseError` function will not be called.

View File

@@ -10,7 +10,7 @@
# Interface: ParseResult
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mer
> **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
Defined in: [packages/mermaid/src/types.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L104)
The config passed as YAML frontmatter or directives
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
The diagram type, e.g. 'flowchart', 'sequence', etc.

View File

@@ -10,7 +10,7 @@
# Interface: RenderResult
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/me
> `optional` **bindFunctions**: (`element`) => `void`
Defined in: [packages/mermaid/src/types.ts:120](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L120)
Defined in: [packages/mermaid/src/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L132)
Bind function to be called after the svg has been inserted into the DOM.
This is necessary for adding event listeners to the elements in the svg.
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
Defined in: [packages/mermaid/src/types.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L122)
The diagram type, e.g. 'flowchart', 'sequence', etc.
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
> **svg**: `string`
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
The svg code for the rendered graph.

View File

@@ -29,7 +29,6 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
- **Plugins** - A plugin system for extending the functionality of Mermaid.
Official Mermaid Chart plugins:
- [Mermaid Chart GPT](https://chatgpt.com/g/g-684cc36f30208191b21383b88650a45d-mermaid-chart-diagrams-and-charts)
- [Confluence](https://marketplace.atlassian.com/apps/1234056/mermaid-chart-for-confluence?hosting=cloud&tab=overview)
- [Jira](https://marketplace.atlassian.com/apps/1234810/mermaid-chart-for-jira?tab=overview&hosting=cloud)

View File

@@ -35,13 +35,11 @@ The Mermaid Chart team is excited to introduce a new Visual Editor for Flowchart
Learn more:
- Visual Editor For Flowcharts
- [Blog post](https://www.mermaidchart.com/blog/posts/mermaid-chart-releases-new-visual-editor-for-flowcharts)
- [Demo video](https://www.youtube.com/watch?v=5aja0gijoO0)
- Visual Editor For Sequence diagrams
- [Blog post](https://www.mermaidchart.com/blog/posts/mermaid-chart-unveils-visual-editor-for-sequence-diagrams)
- [Demo video](https://youtu.be/imc2u5_N6Dc)

View File

@@ -6,6 +6,18 @@
# Blog
## [The Essential Guide to Mermaid Chart Plugin for VS Code \[08/2025\]](https://docs.mermaidchart.com/blog/posts/the-essential-guide-to-mermaid-chart-plugin-for-vs-code-08-2025)
9/9/2025 • 5 mins
Creating diagrams in VS Code has never been easier—the Mermaid VS Code plugin transforms text-based syntax into clear, professional visuals right inside your editor. With live previews, smart editing, and AI-powered diagram generation, it removes the friction from visualization so you can focus on coding.
## [How to Create Perfect Flowcharts Using AI in 2025 Step-by-Step Guide](https://docs.mermaidchart.com/blog/posts/how-to-create-perfect-flowcharts-using-ai-in-2025-step-by-step-guide)
7/22/2025 • 8 mins
In 2025, AI tools make creating flowcharts faster and easier than ever. With Mermaids AI flowchart generator, you can turn plain text into clear, accurate diagrams in seconds. This guide walks through how it works and how to get the most out of it for technical and business workflows alike.
## [Mermaid introduces the Visual Editor for Entity Relationship diagrams](https://docs.mermaidchart.com/blog/posts/mermaid-introduces-the-visual-editor-for-entity-relationship-diagrams)
7/15/2025 • 7 mins

View File

@@ -194,7 +194,7 @@ architecture-beta
## Icons
By default, architecture diagram supports the following icons: `cloud`, `database`, `disk`, `internet`, `server`.
Users can use any of the 200,000+ icons available in iconify.design, or [add custom icons](../config/icons.md).
Users can use any of the 200,000+ icons available in iconify.design, or add other custom icons, by [registering an icon pack](../config/icons.md).
After the icons are installed, they can be used in the architecture diagram by using the format "name:icon-name", where name is the value used when registering the icon pack.

View File

@@ -139,7 +139,6 @@ The following unfinished features are not supported in the short term.
- [ ] Legend
- [x] System Context
- [x] Person(alias, label, ?descr, ?sprite, ?tags, $link)
- [x] Person_Ext
- [x] System(alias, label, ?descr, ?sprite, ?tags, $link)
@@ -153,7 +152,6 @@ The following unfinished features are not supported in the short term.
- [x] System_Boundary
- [x] Container diagram
- [x] Container(alias, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] ContainerDb
- [x] ContainerQueue
@@ -163,7 +161,6 @@ The following unfinished features are not supported in the short term.
- [x] Container_Boundary(alias, label, ?tags, $link)
- [x] Component diagram
- [x] Component(alias, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] ComponentDb
- [x] ComponentQueue
@@ -172,18 +169,15 @@ The following unfinished features are not supported in the short term.
- [x] ComponentQueue_Ext
- [x] Dynamic diagram
- [x] RelIndex(index, from, to, label, ?tags, $link)
- [x] Deployment diagram
- [x] Deployment_Node(alias, label, ?type, ?descr, ?sprite, ?tags, $link)
- [x] Node(alias, label, ?type, ?descr, ?sprite, ?tags, $link): short name of Deployment_Node()
- [x] Node_L(alias, label, ?type, ?descr, ?sprite, ?tags, $link): left aligned Node()
- [x] Node_R(alias, label, ?type, ?descr, ?sprite, ?tags, $link): right aligned Node()
- [x] Relationship Types
- [x] Rel(from, to, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] BiRel (bidirectional relationship)
- [x] Rel_U, Rel_Up

View File

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

View File

@@ -360,7 +360,8 @@ gantt
weekday monday
```
> **Warning** > `millisecond` and `second` support was added in v10.3.0
> **Warning**
> `millisecond` and `second` support was added in v10.3.0
## Output in compact mode

View File

@@ -194,46 +194,6 @@ sequenceDiagram
Bob->>Alice: Queue response
```
### Icon
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
participant Bob
Alice->>Bob: Icon participant
Bob->>Alice: Response to icon
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
participant Bob
Alice->>Bob: Icon participant
Bob->>Alice: Response to icon
```
### Image
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Bob
Alice->>Bob: Image participant
Bob->>Alice: Response to image
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Bob
Alice->>Bob: Image participant
Bob->>Alice: Response to image
```
### Aliases
The actor can have a convenient identifier and a descriptive label.
@@ -369,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 |
| -------- | ---------------------------------------------------- |
@@ -384,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:

View File

@@ -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",
@@ -63,36 +65,36 @@
]
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.44.9",
"@argos-ci/cypress": "^5.0.2",
"@applitools/eyes-cypress": "^3.55.2",
"@argos-ci/cypress": "^6.1.3",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.27.12",
"@changesets/cli": "^2.29.7",
"@cspell/eslint-plugin": "^8.19.4",
"@cypress/code-coverage": "^3.12.49",
"@cypress/code-coverage": "^3.14.6",
"@eslint/js": "^9.26.0",
"@rollup/plugin-typescript": "^12.1.2",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@rollup/plugin-typescript": "^12.1.4",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/js-yaml": "^4.0.9",
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.15",
"@types/lodash": "^4.17.20",
"@types/mdast": "^4.0.4",
"@types/node": "^22.13.5",
"@types/node": "^22.18.6",
"@types/rollup-plugin-visualizer": "^5.0.3",
"@vitest/coverage-v8": "^3.0.6",
"@vitest/spy": "^3.0.6",
"@vitest/ui": "^3.0.6",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/spy": "^3.2.4",
"@vitest/ui": "^3.2.4",
"ajv": "^8.17.1",
"chokidar": "3.6.0",
"concurrently": "^9.1.2",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"cpy-cli": "^5.0.0",
"cross-env": "^7.0.3",
"cspell": "^9.1.3",
"cypress": "^14.5.1",
"cspell": "^9.2.1",
"cypress": "^14.5.4",
"cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.14",
"esbuild": "^0.25.0",
"cypress-split": "^1.24.23",
"esbuild": "^0.25.10",
"eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-cypress": "^4.3.0",
@@ -106,30 +108,30 @@
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-unicorn": "^59.0.1",
"express": "^5.1.0",
"globals": "^16.0.0",
"globby": "^14.0.2",
"globals": "^16.4.0",
"globby": "^14.1.0",
"husky": "^9.1.7",
"jest": "^30.0.4",
"jest": "^30.1.3",
"jison": "^0.4.18",
"js-yaml": "^4.1.0",
"jsdom": "^26.1.0",
"langium-cli": "3.3.0",
"lint-staged": "^16.1.2",
"lint-staged": "^16.1.6",
"markdown-table": "^3.0.4",
"nyc": "^17.1.0",
"path-browserify": "^1.0.1",
"prettier": "^3.5.2",
"prettier-plugin-jsdoc": "^1.3.2",
"prettier": "^3.6.2",
"prettier-plugin-jsdoc": "^1.3.3",
"rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^6.0.3",
"start-server-and-test": "^2.0.10",
"start-server-and-test": "^2.1.2",
"tslib": "^2.8.1",
"tsx": "^4.7.3",
"tsx": "^4.20.5",
"typescript": "~5.7.3",
"typescript-eslint": "^8.38.0",
"vite": "^7.0.3",
"vite": "^7.0.7",
"vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.0.6"
"vitest": "^3.2.4"
},
"nyc": {
"report-dir": "coverage/cypress"

View File

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

View File

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

View File

@@ -37,12 +37,12 @@
]
},
"dependencies": {
"@braintree/sanitize-url": "^7.0.4",
"@braintree/sanitize-url": "^7.1.1",
"d3": "^7.9.0",
"khroma": "^2.1.0"
},
"devDependencies": {
"concurrently": "^9.1.2",
"concurrently": "^9.2.1",
"mermaid": "workspace:*",
"rimraf": "^6.0.1"
},

View File

@@ -1,5 +1,16 @@
# @mermaid-js/layout-elk
## 0.2.0
### Minor Changes
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- Updated dependencies [[`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800), [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20), [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05), [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8)]:
- mermaid@11.11.0
## 0.1.9
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/layout-elk",
"version": "0.1.9",
"version": "0.2.0",
"description": "ELK layout engine for mermaid",
"module": "dist/mermaid-layout-elk.core.mjs",
"types": "dist/layouts.d.ts",

View File

@@ -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)[];

View File

@@ -0,0 +1,12 @@
# @mermaid-js/layout-tidy-tree
## 0.2.0
### Minor Changes
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- Updated dependencies [[`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800), [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20), [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05), [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8)]:
- mermaid@11.11.0

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/layout-tidy-tree",
"version": "0.1.0",
"version": "0.2.0",
"description": "Tidy-tree layout engine for mermaid",
"module": "dist/mermaid-layout-tidy-tree.core.mjs",
"types": "dist/layouts.d.ts",

View File

@@ -33,7 +33,7 @@
],
"license": "MIT",
"dependencies": {
"@zenuml/core": "^3.35.2"
"@zenuml/core": "^3.41.4"
},
"devDependencies": {
"mermaid": "workspace:^"

View File

@@ -1,5 +1,19 @@
# mermaid
## 11.11.0
### Minor Changes
- [#6704](https://github.com/mermaid-js/mermaid/pull/6704) [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05) Thanks [@omkarht](https://github.com/omkarht)! - feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- [#6905](https://github.com/mermaid-js/mermaid/pull/6905) [`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Render newlines as spaces in class diagrams
- [#6886](https://github.com/mermaid-js/mermaid/pull/6886) [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Handle arrows correctly when auto number is enabled
## 11.10.0
### Minor Changes
@@ -154,7 +168,6 @@
### Minor Changes
- [#6408](https://github.com/mermaid-js/mermaid/pull/6408) [`ad65313`](https://github.com/mermaid-js/mermaid/commit/ad653138e16765d095613a6e5de86dc5e52ac8f0) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix: restore curve type configuration functionality for flowcharts. This fixes the issue where curve type settings were not being applied when configured through any of the following methods:
- Config
- Init directive (%%{ init: { 'flowchart': { 'curve': '...' } } }%%)
- LinkStyle command (linkStyle default interpolate ...)
@@ -173,14 +186,12 @@
### Minor Changes
- [#6187](https://github.com/mermaid-js/mermaid/pull/6187) [`7809b5a`](https://github.com/mermaid-js/mermaid/commit/7809b5a93fae127f45727071f5ff14325222c518) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Flowchart new syntax for node metadata bugs
- Incorrect label mapping for nodes when using `&`
- Syntax error when `}` with trailing spaces before new line
- [#6136](https://github.com/mermaid-js/mermaid/pull/6136) [`ec0d9c3`](https://github.com/mermaid-js/mermaid/commit/ec0d9c389aa6018043187654044c1e0b5aa4f600) Thanks [@knsv](https://github.com/knsv)! - Adding support for animation of flowchart edges
- [#6373](https://github.com/mermaid-js/mermaid/pull/6373) [`05bdf0e`](https://github.com/mermaid-js/mermaid/commit/05bdf0e20e2629fe77513218fbd4e28e65f75882) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Upgrade Requirement and ER diagram to use the common renderer flow
- Added support for directions
- Added support for hand drawn look

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "11.10.0",
"version": "11.11.0",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -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": {
@@ -67,29 +69,30 @@
]
},
"dependencies": {
"@braintree/sanitize-url": "^7.0.4",
"@iconify/utils": "^3.0.1",
"@braintree/sanitize-url": "^7.1.1",
"@iconify/utils": "^3.0.2",
"@mermaid-js/parser": "workspace:^",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.3",
"antlr4ng": "^3.0.7",
"cytoscape": "^3.33.1",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.11",
"dayjs": "^1.11.13",
"dayjs": "^1.11.18",
"dompurify": "^3.2.5",
"katex": "^0.16.22",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^16.0.0",
"marked": "^16.3.0",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@adobe/jsonschema2md": "^8.0.2",
"@adobe/jsonschema2md": "^8.0.5",
"@iconify/types": "^2.0.0",
"@types/cytoscape": "^3.21.9",
"@types/cytoscape-fcose": "^2.2.4",
@@ -105,30 +108,30 @@
"@types/stylis": "^4.2.7",
"@types/uuid": "^10.0.0",
"ajv": "^8.17.1",
"canvas": "^3.1.0",
"canvas": "^3.2.0",
"chokidar": "3.6.0",
"concurrently": "^9.1.2",
"concurrently": "^9.2.1",
"csstree-validator": "^4.0.1",
"globby": "^14.0.2",
"globby": "^14.1.0",
"jison": "^0.4.18",
"js-base64": "^3.7.7",
"js-base64": "^3.7.8",
"jsdom": "^26.1.0",
"json-schema-to-typescript": "^15.0.4",
"micromatch": "^4.0.8",
"path-browserify": "^1.0.1",
"prettier": "^3.5.2",
"prettier": "^3.6.2",
"remark": "^15.0.1",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"rimraf": "^6.0.1",
"start-server-and-test": "^2.0.10",
"type-fest": "^4.35.0",
"typedoc": "^0.28.9",
"typedoc-plugin-markdown": "^4.8.0",
"start-server-and-test": "^2.1.2",
"type-fest": "^4.41.0",
"typedoc": "^0.28.13",
"typedoc-plugin-markdown": "^4.8.1",
"typescript": "~5.7.3",
"unist-util-flatmap": "^1.0.0",
"unist-util-visit": "^5.0.0",
"vitepress": "^1.0.2",
"vitepress": "^1.6.4",
"vitepress-plugin-search": "1.0.4-alpha.22"
},
"files": [

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
import { treemap } from '../diagrams/treemap/detector.js';
import { usecase } from '../diagrams/usecase/usecaseDetector.js';
import '../type.d.ts';
let hasLoadedDiagrams = false;
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
xychart,
block,
radar,
treemap
treemap,
usecase
);
};

View File

@@ -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

View File

@@ -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
}
`
);
});
});
});
});

View File

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

View File

@@ -268,7 +268,9 @@ const fixTaskDates = function (startTime, endTime, dateFormat, excludes, include
const getStartDate = function (prevTime, dateFormat, str) {
str = str.trim();
if ((dateFormat.trim() === 'x' || dateFormat.trim() === 'X') && /^\d+$/.test(str)) {
return new Date(Number(str));
}
// Test for after
const afterRePattern = /^after\s+(?<ids>[\d\w- ]+)/;
const afterStatement = afterRePattern.exec(str);

View File

@@ -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; }

View File

@@ -13,7 +13,8 @@ import {
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import type { Actor, AddMessageParams, Box, Message, Note, ParticipantMetaData } from './types.js';
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
import type { ParticipantMetaData } from '../../types.js';
interface SequenceState {
prevActor?: string;
@@ -63,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 = {
@@ -85,8 +110,6 @@ export const PARTICIPANT_TYPE = {
ENTITY: 'entity',
PARTICIPANT: 'participant',
QUEUE: 'queue',
ICON: 'icon',
IMAGE: 'image',
} as const;
export class SequenceDB implements DiagramDB {
@@ -187,7 +210,6 @@ export class SequenceDB implements DiagramDB {
actorCnt: null,
rectData: null,
type: type ?? 'participant',
doc: doc,
});
if (this.state.records.prevActor) {
const prevActorInRecords = this.state.records.actors.get(this.state.records.prevActor);
@@ -246,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 ?? '');
@@ -273,6 +296,7 @@ export class SequenceDB implements DiagramDB {
wrap: message?.wrap ?? this.autoWrap(),
type: messageType,
activate,
centralConnection: centralConnection ?? 0,
});
return true;
}
@@ -565,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;
@@ -608,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);

View File

@@ -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(`
@@ -2326,73 +2610,4 @@ Bob->>Alice:Got it!
expect(actors.get('E').description).toBe('E');
});
});
describe('image and icon participant parsing', () => {
it('should parse image participant with image URL', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
Bob->>Bob: test
`);
const actors = diagram.db.getActors();
expect(actors.get('Bob').type).toBe('image');
expect(actors.get('Bob').doc.image).toBe(
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
);
});
it('should parse icon participant with icon name', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Alice@{ "type": "icon", "icon": "fa:bell" }
Alice->>Alice: test
`);
const actors = diagram.db.getActors();
expect(actors.get('Alice').type).toBe('icon');
expect(actors.get('Alice').doc.icon).toBe('fa:bell');
});
it('should parse two image participants', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg" }
Bob->>Alice: Hello
Alice-->>Bob: Hi
`);
const actors = diagram.db.getActors();
expect(actors.get('Bob').type).toBe('image');
expect(actors.get('Bob').doc.image).toBe(
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
);
expect(actors.get('Alice').type).toBe('image');
expect(actors.get('Alice').doc.image).toBe(
'https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg'
);
});
it('should parse image participant with normal participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
participant Alice
Bob->>Alice: Hello
`);
const actors = diagram.db.getActors();
expect(actors.get('Bob').type).toBe('image');
expect(actors.get('Alice').type).toBe('participant');
});
it('should parse icon participant with normal participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Bob@{ "type": "icon", "icon": "fa:bell" }
participant Alice
Bob->>Alice: Hello
`);
const actors = diagram.db.getActors();
expect(actors.get('Bob').type).toBe('icon');
expect(actors.get('Alice').type).toBe('participant');
});
});
});

View File

@@ -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)`);
}
};
@@ -752,8 +865,6 @@ function adjustCreatedDestroyedData(
PARTICIPANT_TYPE.CONTROL,
PARTICIPANT_TYPE.ENTITY,
PARTICIPANT_TYPE.DATABASE,
PARTICIPANT_TYPE.ICON,
PARTICIPANT_TYPE.IMAGE,
];
// if it is a create message
@@ -859,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.
@@ -899,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;
@@ -1057,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);
@@ -1070,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,
@@ -1089,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);
@@ -1463,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,
@@ -1486,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;
@@ -1519,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);
}
@@ -1527,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);
}

View File

@@ -1,6 +1,5 @@
import { sanitizeUrl } from '@braintree/sanitize-url';
import * as configApi from '../../config.js';
import { getIconSVG } from '../../rendering-util/icons.js';
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
import common, {
calculateMathMLDimensions,
@@ -323,89 +322,6 @@ export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => {
});
};
const drawActorTypeIcon = async function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + actor.height / 2;
const line = elem.append('g').lower();
if (!isFooter) {
actorCnt++;
line
.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY + 25)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
actor.actorCnt = actorCnt;
}
const actElem = elem.append('g');
let cssClass = 'actor-icon';
if (isFooter) {
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssClass += ` ${TOP_ACTOR_CLASS}`;
}
actElem.attr('class', cssClass);
actElem.attr('name', actor.name);
// Define the size of the square and icon
const iconSize = actor.width / 5;
const squareX = center - iconSize / 2;
const squareY = !isFooter ? actorY + 10 : actorY;
// Draw a square rectangle for the actor icon background
actElem
.append('rect')
.attr('x', squareX)
.attr('y', squareY)
.attr('width', iconSize)
.attr('height', iconSize)
.attr('rx', 3) // rounded corners, optional
.attr('ry', 3)
.attr('fill', 'none'); // light gray background or customize as needed
// Render icon SVG inside the rectangle
const iconGroup = actElem.append('g').attr('transform', `translate(${squareX}, ${squareY})`);
iconGroup
.append('svg')
.attr('width', iconSize)
.attr('height', iconSize)
.html(
`<g>${await getIconSVG(actor.doc.icon, {
height: iconSize,
width: iconSize,
fallbackPrefix: '',
})}</g>`
);
// Add text label below icon
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
actElem,
actor.x,
actorY + (!isFooter ? 40 : 30), // positioning below the square icon
actor.width,
20,
{ class: 'actor-icon-text' },
conf
);
const bounds = actElem.node().getBBox();
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
return actor.height;
};
/**
* Draws an actor in the diagram with the attached line
*
@@ -498,174 +414,6 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
return height;
};
const drawActorTypeImage = async function (elem, actor, conf, isFooter) {
const img = new Image();
img.src = actor.doc.image ?? '';
await img.decode();
const imageNaturalWidth = Number(img.naturalWidth.toString().replace('px', ''));
const imageNaturalHeight = Number(img.naturalHeight.toString().replace('px', ''));
actor.doc.imageAspectRatio = imageNaturalWidth / imageNaturalHeight;
// Calculate image dimensions with proper sizing logic
let imageWidth, imageHeight;
// Check if custom dimensions are provided and valid
const hasValidCustomDimensions =
actor.doc.height && actor.doc.height > 10 && actor.doc.width && actor.doc.width > 10;
if (hasValidCustomDimensions) {
if (actor.doc.constraint === 'on') {
// Maintain aspect ratio with constraint
const customAspectRatio = imageNaturalWidth / imageNaturalHeight;
if (customAspectRatio > actor.doc.imageAspectRatio) {
// Width is the limiting factor
imageHeight = actor.doc.height;
imageWidth = actor.doc.height * actor.doc.imageAspectRatio;
} else {
// Height is the limiting factor
imageWidth = actor.doc.width;
imageHeight = actor.doc.width / actor.doc.imageAspectRatio;
}
} else {
// Use custom dimensions without maintaining aspect ratio
imageWidth = actor.doc.width;
imageHeight = actor.doc.height;
}
} else {
// Use default sizing based on actor width
const defaultImageSize = actor.width / 3.5;
// Ensure minimum and maximum sizes
const minSize = 30;
const maxSize = actor.width * 0.8;
if (actor.doc.imageAspectRatio >= 1) {
// Landscape or square image
imageWidth = Math.min(Math.max(defaultImageSize, minSize), maxSize);
imageHeight = imageWidth / actor.doc.imageAspectRatio;
} else {
// Portrait image
imageHeight = Math.min(Math.max(defaultImageSize, minSize), maxSize);
imageWidth = imageHeight * actor.doc.imageAspectRatio;
}
// Ensure the image doesn't exceed actor bounds
if (imageWidth > actor.width * 0.9) {
imageWidth = actor.width * 0.9;
imageHeight = imageWidth / actor.doc.imageAspectRatio;
}
}
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + imageHeight;
// Calculate positioning
const squareX = center - imageWidth / 2;
let squareY;
if (isFooter) {
squareY = actorY + (imageHeight - imageHeight * 1);
} else {
squareY = actorY + 5;
}
// Calculate text position based on image position and size
const textY = !isFooter ? squareY + imageHeight : actorY + imageHeight; // Place text below image for header
// Draw actor line for non-footer elements
const x = center;
const y = centerY + (isFooter ? 0 : imageHeight / 2); // Adjust line start based on image height
const line = elem.append('g').lower();
if (!isFooter) {
actorCnt++;
line
.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', x)
.attr('y1', y) // Adjust line start based on image height
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
actor.actorCnt = actorCnt;
}
const actElem = elem.append('g');
let cssClass = 'actor-image';
if (isFooter) {
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssClass += ` ${TOP_ACTOR_CLASS}`;
}
actElem.attr('class', cssClass);
actElem.attr('name', actor.name);
// Draw background rectangle for the actor image
actElem
.append('rect')
.attr('x', squareX)
.attr('y', squareY)
.attr('width', imageWidth)
.attr('height', imageHeight)
.attr('rx', 3)
.attr('ry', 3)
.attr('fill', 'white')
.attr('stroke', '#ddd')
.attr('stroke-width', '1px');
// Create clipping path for the image
const clipId = `clip-actor-${actorCnt || 'footer'}-${Math.random().toString(36).substr(2, 9)}`;
actElem
.append('defs')
.append('clipPath')
.attr('id', clipId)
.append('rect')
.attr('x', squareX)
.attr('y', squareY)
.attr('width', imageWidth)
.attr('height', imageHeight)
.attr('rx', 3)
.attr('ry', 3);
// Render image inside the rectangle
const imageGroup = actElem.append('g');
if (actor.doc.image) {
imageGroup
.append('image')
.attr('x', squareX)
.attr('y', squareY)
.attr('width', imageWidth)
.attr('height', imageHeight)
.attr('href', img.src)
.attr('preserveAspectRatio', actor.doc.constraint === 'on' ? 'xMidYMid meet' : 'none');
}
// Add text label
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
actElem,
actor.x,
textY,
actor.width,
20,
{ class: 'actor-image-text' },
conf
);
// Calculate final bounds and update actor height
const bounds = actElem.node().getBBox();
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
return actor.height;
};
/**
* Draws an actor in the diagram with the attached line
@@ -1374,10 +1122,6 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
return await drawActorTypeCollections(elem, actor, conf, isFooter);
case 'queue':
return await drawActorTypeQueue(elem, actor, conf, isFooter);
case 'icon':
return await drawActorTypeIcon(elem, actor, conf, isFooter);
case 'image':
return await drawActorTypeImage(elem, actor, conf, isFooter);
}
};
@@ -1965,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,
@@ -1987,4 +1802,8 @@ export default {
getNoteRect,
fixLifeLineHeights,
sanitizeUrl,
insertSolidTopArrowHead,
insertSolidBottomArrowHead,
insertStickTopArrowHead,
insertStickBottomArrowHead,
};

View File

@@ -17,9 +17,6 @@ export interface Actor {
actorCnt: number | null;
rectData: unknown;
type: string;
doc?: ParticipantMetaData; // For documentation
iconName?: string; // For icon type
imgSrc?: string; // For img type
}
export interface Message {
@@ -38,6 +35,7 @@ export interface Message {
type?: number;
activate?: boolean;
placement?: string;
centralConnection?: number;
}
export interface AddMessageParams {
@@ -53,6 +51,8 @@ export interface AddMessageParams {
| 'destroyParticipant'
| 'activeStart'
| 'activeEnd'
| 'centralConnection'
| 'centralConnectionReverse'
| 'addNote'
| 'addLinks'
| 'addALink'
@@ -93,20 +93,3 @@ export interface Note {
message: string;
wrap: boolean;
}
export interface ParticipantMetaData {
type?:
| 'actor'
| 'participant'
| 'boundary'
| 'control'
| 'entity'
| 'database'
| 'collections'
| 'queue'
| 'icon'
| 'img';
icon?: string;
img?: string;
form?: string;
}

View File

@@ -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');
}
}

View File

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

View File

@@ -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);
}
}
}
}

View File

@@ -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)*
;

View File

@@ -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, '_');
}
}

View File

@@ -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);
}
}
}
}

View File

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

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

View 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');
});
});
});

View 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,
};

View File

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

View File

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

View File

@@ -0,0 +1,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;
}
},
};

View File

@@ -0,0 +1,48 @@
import type { DrawDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import utils from '../../utils.js';
import type { UsecaseDB } from './usecaseTypes.js';
/**
* Main draw function using unified rendering system
*/
const draw: DrawDefinition = async (_text, id, _version, diag) => {
log.info('Drawing usecase diagram (unified)', id);
const { securityLevel, usecase: conf, layout } = getConfig();
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
// into the Layout data format
const usecaseDb = diag.db as UsecaseDB;
const data4Layout = usecaseDb.getData();
// Create the root SVG - the element is the div containing the SVG element
const svg = getDiagramElement(id, securityLevel);
data4Layout.type = diag.type;
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
data4Layout.nodeSpacing = 50; // Default node spacing
data4Layout.rankSpacing = 50; // Default rank spacing
data4Layout.markers = ['point', '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 };

View File

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

View File

@@ -0,0 +1,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;
}

View File

@@ -40,105 +40,26 @@ const openSourceFeatures: Feature[] = [
{ iconUrl: '/icons/version-history.svg', featureName: 'Version history' },
];
const playgroundFeatures: Feature[] = [
{ iconUrl: '/icons/public.svg', featureName: 'Diagram stored in URL' },
{ iconUrl: '/icons/terminal.svg', featureName: 'Code editor' },
{ iconUrl: '/icons/whiteboard.svg', featureName: 'Whiteboard' },
const editorColumns: EditorColumn[] = [
{
title: 'Mermaid Pro',
description: 'Unlock AI and real-time collaboration',
highlighted: true,
redBarText: 'Recommended',
proTrialButtonText: 'Start free trial',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=2_editor_selection&utm_campaign=start_pro&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout%3FisFromMermaid%3Dtrue',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
buttonText: 'Start free',
redirectUrl: 'https://mermaid.live/edit',
features: openSourceFeatures,
},
];
const editorColumnVariants: EditorColumn[][] = [
[
{
title: 'Playground',
description: 'Basic features, no login',
redirectUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=3_editor_selection_A&utm_campaign=start_playground',
buttonText: 'Start free',
features: playgroundFeatures,
},
{
title: 'Free or Pro',
description: 'Advanced features, Free or Pro account',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=3_editor_selection_A&utm_campaign=start_free',
proTrialButtonText: 'Start free',
highlighted: true,
redBarText: 'Best for collaboration',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
redirectUrl: 'https://mermaid.live/edit',
buttonText: 'Start free',
features: openSourceFeatures,
},
],
[
{
title: 'Mermaid Pro',
description: 'Unlock AI and real-time collaboration',
redirectUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection_B&utm_campaign=start_free',
buttonText: 'Start Free',
highlighted: true,
redBarText: 'Recommended',
proTrialButtonText: 'Start Pro trial',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection_B&utm_campaign=start_trial&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout%3FisFromMermaid%3Dtrue',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
buttonText: 'Start free',
redirectUrl: 'https://mermaid.live/edit',
isButtonMargined: true,
features: openSourceFeatures,
},
],
[
{
title: 'Mermaid Pro',
description: 'Unlock AI and real-time collaboration',
highlighted: true,
redBarText: 'Recommended',
proTrialButtonText: 'Start free trial',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection_C&utm_campaign=start_trial&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout%3FisFromMermaid%3Dtrue',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
buttonText: 'Start free',
redirectUrl: 'https://mermaid.live/edit',
features: openSourceFeatures,
},
],
[
{
title: 'Mermaid Pro',
description: 'Unlock AI and real-time collaboration',
highlighted: true,
redBarText: 'Recommended',
proTrialButtonText: 'Start free',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection_D&utm_campaign=start_free',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
redirectUrl: 'https://mermaid.live/edit',
buttonText: 'Start free',
features: openSourceFeatures,
},
],
];
const editorColumns = editorColumnVariants[Math.floor(Math.random() * editorColumnVariants.length)];
const isVisible = ref(false);
const handleMouseDown = (e: MouseEvent) => {

View File

@@ -17,7 +17,6 @@ While directives allow you to change most of the default configuration settings,
Mermaid basically supports two types of configuration options to be overridden by directives.
1. _General/Top Level configurations_ : These are the configurations that are available and applied to all the diagram. **Some of the most important top-level** configurations are:
- theme
- fontFamily
- logLevel

View File

@@ -23,7 +23,6 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
- **Plugins** - A plugin system for extending the functionality of Mermaid.
Official Mermaid Chart plugins:
- [Mermaid Chart GPT](https://chatgpt.com/g/g-684cc36f30208191b21383b88650a45d-mermaid-chart-diagrams-and-charts)
- [Confluence](https://marketplace.atlassian.com/apps/1234056/mermaid-chart-for-confluence?hosting=cloud&tab=overview)
- [Jira](https://marketplace.atlassian.com/apps/1234810/mermaid-chart-for-jira?tab=overview&hosting=cloud)

View File

@@ -33,13 +33,11 @@ The Mermaid Chart team is excited to introduce a new Visual Editor for Flowchart
Learn more:
- Visual Editor For Flowcharts
- [Blog post](https://www.mermaidchart.com/blog/posts/mermaid-chart-releases-new-visual-editor-for-flowcharts)
- [Demo video](https://www.youtube.com/watch?v=5aja0gijoO0)
- Visual Editor For Sequence diagrams
- [Blog post](https://www.mermaidchart.com/blog/posts/mermaid-chart-unveils-visual-editor-for-sequence-diagrams)
- [Demo video](https://youtu.be/imc2u5_N6Dc)

Some files were not shown because too many files have changed in this diff Show More