Compare commits

..

93 Commits

Author SHA1 Message Date
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
118 changed files with 12020 additions and 6064 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': 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

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Ensure flowchart htmlLabels resolution respects both global and flowchart config

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

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

@@ -1199,61 +1199,4 @@ class link myClass
`
);
});
describe('htmlLabels rendering', () => {
it('should not render with htmlLabels when disabled via flowchart config', () => {
imgSnapshotTest(
`flowchart LR
A["HTML label <br> with breaks"] --> B["Another label"]
C --> D
`,
{ flowchart: { htmlLabels: false } }
);
});
it('should not render with htmlLabels when disabled via global config', () => {
imgSnapshotTest(
`flowchart LR
A["HTML label <br> with breaks"] --> B["Another label"]
C --> D
`,
{ htmlLabels: false }
);
});
it('should render with htmlLabels when enabled', () => {
imgSnapshotTest(
`flowchart LR
A["HTML label <br> with breaks"] --> B["Another label"]
C --> D
`,
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
);
});
it('should not render with htmlLabels when disabled via flowchart config, even when enabled in global config', () => {
imgSnapshotTest(
`flowchart LR
A["HTML label <br> with breaks"] --> B["Another label"]
C --> D
`,
{ htmlLabels: true, flowchart: { htmlLabels: false } },
undefined,
($svg) => {
expect($svg.find('foreignObject').length).to.equal(0);
}
);
});
it('should create foreignObject elements when htmlLabels enabled', () => {
renderGraph(
`flowchart TD
A["Node with <br> HTML"] -- "edge <br> label" --> B["Another node"]
C --> D
`,
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
);
cy.get('svg foreignObject').should('exist');
});
});
});

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

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

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

@@ -329,7 +329,11 @@ Messages can be of two displayed either solid or with a dotted line.
[Actor][Arrow][Actor]:Message text
```
There are ten types of arrows currently supported:
Lines can be solid or dotted, and can end with various types of arrowheads, crosses, or open arrows.
#### Supported Arrow Types
**Standard Arrow Types**
| Type | Description |
| -------- | ---------------------------------------------------- |
@@ -344,6 +348,58 @@ There are ten types of arrows currently supported:
| `-)` | Solid line with an open arrow at the end (async) |
| `--)` | Dotted line with a open arrow at the end (async) |
**Half-Arrows (v\<MERMAID_RELEASE_VERSION>+)**
The following half-arrow types are supported for more expressive sequence diagrams. Both solid and dotted variants are available by increasing the number of dashes (`-` → `--`).
---
| Type | Description |
| ------- | ---------------------------------------------------- |
| `-\|\` | Solid line with top half arrowhead |
| `--\|\` | Dotted line with top half arrowhead |
| `-\|/` | Solid line with bottom half arrowhead |
| `--\|/` | Dotted line with bottom half arrowhead |
| `/\|-` | Solid line with reverse top half arrowhead |
| `/\|--` | Dotted line with reverse top half arrowhead |
| `\\-` | Solid line with reverse bottom half arrowhead |
| `\\--` | Dotted line with reverse bottom half arrowhead |
| `-\\` | Solid line with top stick half arrowhead |
| `--\\` | Dotted line with top stick half arrowhead |
| `-//` | Solid line with bottom stick half arrowhead |
| `--//` | Dotted line with bottom stick half arrowhead |
| `//-` | Solid line with reverse top stick half arrowhead |
| `//--` | Dotted line with reverse top stick half arrowhead |
| `\\-` | Solid line with reverse bottom stick half arrowhead |
| `\\--` | Dotted line with reverse bottom stick half arrowhead |
## Central Connections (v\<MERMAID_RELEASE_VERSION>+)
Mermaid sequence diagrams support **central lifeline connections** using a `()`.
This is useful to represent messages or signals that connect to a central point, rather than from one actor directly to another.
To indicate a central connection, append `()` to the arrow syntax.
#### Basic Syntax
```mermaid-example
sequenceDiagram
participant Alice
participant John
Alice->>()John: Hello John
Alice()->>John: How are you?
John()->>()Alice: Great!
```
```mermaid
sequenceDiagram
participant Alice
participant John
Alice->>()John: Hello John
Alice()->>John: How are you?
John()->>()Alice: Great!
```
## Activations
It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations:

View File

@@ -63,36 +63,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 +106,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",
@@ -47,7 +47,7 @@
"docs:verify-version": "tsx scripts/update-release-version.mts --verify",
"types:build-config": "tsx scripts/create-types-from-json-schema.mts",
"types:verify-config": "tsx scripts/create-types-from-json-schema.mts --verify",
"checkCircle": "npx madge --circular ./src",
"checkCircle": "npx madge --circular ./src --exclude '.*generated.*'",
"prepublishOnly": "pnpm docs:verify-version"
},
"repository": {
@@ -67,29 +67,29 @@
]
},
"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",
"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 +105,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

@@ -21,13 +21,7 @@ export const diagram = {
if (cnf.layout) {
setConfig({ layout: cnf.layout });
}
cnf.flowchart.htmlLabels = cnf.flowchart?.htmlLabels ?? cnf?.htmlLabels;
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
setConfig({
flowchart: {
arrowMarkerAbsolute: cnf.arrowMarkerAbsolute,
htmlLabels: cnf.flowchart.htmlLabels,
},
});
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
},
};

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

@@ -64,6 +64,30 @@ const LINETYPE = {
PAR_OVER_START: 32,
BIDIRECTIONAL_SOLID: 33,
BIDIRECTIONAL_DOTTED: 34,
SOLID_TOP: 41,
SOLID_BOTTOM: 42,
STICK_TOP: 43,
STICK_BOTTOM: 44,
SOLID_ARROW_TOP_REVERSE: 45,
SOLID_ARROW_BOTTOM_REVERSE: 46,
STICK_ARROW_TOP_REVERSE: 47,
STICK_ARROW_BOTTOM_REVERSE: 48,
SOLID_TOP_DOTTED: 51,
SOLID_BOTTOM_DOTTED: 52,
STICK_TOP_DOTTED: 53,
STICK_BOTTOM_DOTTED: 54,
SOLID_ARROW_TOP_REVERSE_DOTTED: 55,
SOLID_ARROW_BOTTOM_REVERSE_DOTTED: 56,
STICK_ARROW_TOP_REVERSE_DOTTED: 57,
STICK_ARROW_BOTTOM_REVERSE_DOTTED: 58,
CENTRAL_CONNECTION: 59,
CENTRAL_CONNECTION_REVERSE: 60,
CENTRAL_CONNECTION_DUAL: 61,
} as const;
const ARROWTYPE = {
@@ -244,7 +268,8 @@ export class SequenceDB implements DiagramDB {
idTo?: Message['to'],
message?: { text: string; wrap: boolean },
messageType?: number,
activate = false
activate = false,
centralConnection?: number
) {
if (messageType === this.LINETYPE.ACTIVE_END) {
const cnt = this.activationCount(idFrom ?? '');
@@ -271,6 +296,7 @@ export class SequenceDB implements DiagramDB {
wrap: message?.wrap ?? this.autoWrap(),
type: messageType,
activate,
centralConnection: centralConnection ?? 0,
});
return true;
}
@@ -563,6 +589,12 @@ export class SequenceDB implements DiagramDB {
case 'activeStart':
this.addSignal(param.actor, undefined, undefined, param.signalType);
break;
case 'centralConnection':
this.addSignal(param.actor, undefined, undefined, param.signalType);
break;
case 'centralConnectionReverse':
this.addSignal(param.actor, undefined, undefined, param.signalType);
break;
case 'activeEnd':
this.addSignal(param.actor, undefined, undefined, param.signalType);
break;
@@ -606,7 +638,14 @@ export class SequenceDB implements DiagramDB {
this.state.records.lastDestroyed = undefined;
}
}
this.addSignal(param.from, param.to, param.msg, param.signalType, param.activate);
this.addSignal(
param.from,
param.to,
param.msg,
param.signalType,
param.activate,
param.centralConnection
);
break;
case 'boxStart':
this.addBox(param.boxData);

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(`

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)`);
}
};
@@ -857,6 +970,10 @@ export const draw = async function (_text: string, id: string, _version: string,
svgDraw.insertArrowCrossHead(diagram);
svgDraw.insertArrowFilledHead(diagram);
svgDraw.insertSequenceNumber(diagram);
svgDraw.insertSolidTopArrowHead(diagram);
svgDraw.insertSolidBottomArrowHead(diagram);
svgDraw.insertStickTopArrowHead(diagram);
svgDraw.insertStickBottomArrowHead(diagram);
/**
* @param msg - The message to draw.
@@ -897,6 +1014,12 @@ export const draw = async function (_text: string, id: string, _version: string,
case diagObj.db.LINETYPE.ACTIVE_START:
bounds.newActivation(msg, diagram, actors);
break;
case diagObj.db.LINETYPE.CENTRAL_CONNECTION:
bounds.newActivation(msg, diagram, actors);
break;
case diagObj.db.LINETYPE.CENTRAL_CONNECTION_REVERSE:
bounds.newActivation(msg, diagram, actors);
break;
case diagObj.db.LINETYPE.ACTIVE_END:
activeEnd(msg, bounds.getVerticalPos());
break;
@@ -1055,7 +1178,7 @@ export const draw = async function (_text: string, id: string, _version: string,
createdActors,
destroyedActors
);
messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY });
messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY, msg });
bounds.models.addMessage(msgModel);
} catch (e) {
log.error('error while drawing message', e);
@@ -1068,6 +1191,27 @@ export const draw = async function (_text: string, id: string, _version: string,
diagObj.db.LINETYPE.SOLID_OPEN,
diagObj.db.LINETYPE.DOTTED_OPEN,
diagObj.db.LINETYPE.SOLID,
diagObj.db.LINETYPE.SOLID_TOP,
diagObj.db.LINETYPE.SOLID_BOTTOM,
diagObj.db.LINETYPE.STICK_TOP,
diagObj.db.LINETYPE.STICK_BOTTOM,
diagObj.db.LINETYPE.SOLID_TOP_DOTTED,
diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED,
diagObj.db.LINETYPE.STICK_TOP_DOTTED,
diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE,
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,
diagObj.db.LINETYPE.DOTTED,
diagObj.db.LINETYPE.SOLID_CROSS,
diagObj.db.LINETYPE.DOTTED_CROSS,
@@ -1087,7 +1231,7 @@ export const draw = async function (_text: string, id: string, _version: string,
await drawActors(diagram, actors, actorKeys, false);
for (const e of messagesToDraw) {
await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj);
await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj, e.msg);
}
if (conf.mirrorActors) {
await drawActors(diagram, actors, actorKeys, true);
@@ -1461,12 +1605,85 @@ const buildNoteModel = async function (msg, actors, diagObj) {
return noteModel;
};
// Central connection positioning constants
const CENTRAL_CONNECTION_BASE_OFFSET = 4;
const CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET = 6;
/**
* Check if a message has central connection
* @param msg - The message object
* @param diagObj - The diagram object containing LINETYPE constants
* @returns True if the message has any type of central connection
*/
const hasCentralConnection = function (msg, diagObj) {
const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } =
diagObj.db.LINETYPE;
return [CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL].includes(
msg.centralConnection
);
};
/**
* Calculate the positioning offset for central connection arrows
* @param msg - The message object
* @param diagObj - The diagram object containing LINETYPE constants
* @param isArrowToRight - Whether the arrow is pointing to the right
* @returns The offset to apply to startx position
*/
const calculateCentralConnectionOffset = function (msg, diagObj, isArrowToRight) {
const {
CENTRAL_CONNECTION_REVERSE,
CENTRAL_CONNECTION_DUAL,
BIDIRECTIONAL_SOLID,
BIDIRECTIONAL_DOTTED,
} = diagObj.db.LINETYPE;
let offset = 0;
if (
msg.centralConnection === CENTRAL_CONNECTION_REVERSE ||
msg.centralConnection === CENTRAL_CONNECTION_DUAL
) {
offset += CENTRAL_CONNECTION_BASE_OFFSET;
}
if (
msg.centralConnection === CENTRAL_CONNECTION_DUAL &&
(msg.type === BIDIRECTIONAL_SOLID || msg.type === BIDIRECTIONAL_DOTTED)
) {
offset += isArrowToRight ? 0 : -CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET;
}
return offset;
};
const buildMessageModel = function (msg, actors, diagObj) {
if (
![
diagObj.db.LINETYPE.SOLID_OPEN,
diagObj.db.LINETYPE.DOTTED_OPEN,
diagObj.db.LINETYPE.SOLID,
diagObj.db.LINETYPE.SOLID_TOP,
diagObj.db.LINETYPE.SOLID_BOTTOM,
diagObj.db.LINETYPE.STICK_TOP,
diagObj.db.LINETYPE.STICK_BOTTOM,
diagObj.db.LINETYPE.SOLID_TOP_DOTTED,
diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED,
diagObj.db.LINETYPE.STICK_TOP_DOTTED,
diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE,
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,
diagObj.db.LINETYPE.DOTTED,
diagObj.db.LINETYPE.SOLID_CROSS,
diagObj.db.LINETYPE.DOTTED_CROSS,
@@ -1484,6 +1701,8 @@ const buildMessageModel = function (msg, actors, diagObj) {
let startx = isArrowToRight ? fromRight : fromLeft;
let stopx = isArrowToRight ? toLeft : toRight;
// Apply central connection positioning adjustments
startx += calculateCentralConnectionOffset(msg, diagObj, isArrowToRight);
// As the line width is considered, the left and right values will be off by 2.
const isArrowToActivation = Math.abs(toLeft - toRight) > 2;
@@ -1517,7 +1736,30 @@ const buildMessageModel = function (msg, actors, diagObj) {
* Shorten the length of arrow at the end and move the marker forward (using refX) to have a clean arrowhead
* This is not required for open arrows that don't have arrowheads
*/
if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) {
if (
![
diagObj.db.LINETYPE.SOLID_OPEN,
diagObj.db.LINETYPE.DOTTED_OPEN,
diagObj.db.LINETYPE.STICK_TOP,
diagObj.db.LINETYPE.STICK_BOTTOM,
diagObj.db.LINETYPE.STICK_TOP_DOTTED,
diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE,
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,
diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,
diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,
].includes(msg.type)
) {
stopx += adjustValue(3);
}
@@ -1525,9 +1767,14 @@ const buildMessageModel = function (msg, actors, diagObj) {
* Shorten start position of bidirectional arrow to accommodate for second arrowhead
*/
if (
[diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes(
msg.type
)
[
diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID,
diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,
diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,
].includes(msg.type)
) {
startx -= adjustValue(3);
}

View File

@@ -1709,6 +1709,77 @@ const _drawMenuItemTextCandidateFunc = (function () {
};
})();
/**
* Setup arrow head and define the marker. The result is appended to the svg.
*
* @param elem
*/
export const insertSolidTopArrowHead = function (elem) {
elem
.append('defs')
.append('marker')
.attr('id', 'solidTopArrowHead')
.attr('refX', 7.9)
.attr('refY', 7.25)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)
.attr('markerHeight', 12)
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d', 'M 0 0 L 10 8 L 0 8 z'); // this is actual shape for arrowhead
};
export const insertSolidBottomArrowHead = function (elem) {
elem
.append('defs')
.append('marker')
.attr('id', 'solidBottomArrowHead')
.attr('refX', 7.9)
.attr('refY', 0.75)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)
.attr('markerHeight', 12)
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d', 'M 0 0 L 10 0 L 0 8 z');
};
export const insertStickTopArrowHead = function (elem) {
elem
.append('defs')
.append('marker')
.attr('id', 'stickTopArrowHead')
.attr('refX', 7.5)
.attr('refY', 7)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)
.attr('markerHeight', 12)
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d', 'M 0 0 L 7 7')
.attr('stroke', 'black')
.attr('stroke-width', 1.5)
.attr('fill', 'none');
};
export const insertStickBottomArrowHead = function (elem) {
elem
.append('defs')
.append('marker')
.attr('id', 'stickBottomArrowHead')
.attr('refX', 7.5)
.attr('refY', 0)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)
.attr('markerHeight', 12)
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d', 'M 0 7 L 7 0')
.attr('stroke', 'black')
.attr('stroke-width', 1.5)
.attr('fill', 'none');
};
export default {
drawRect,
drawText,
@@ -1731,4 +1802,8 @@ export default {
getNoteRect,
fixLifeLineHeights,
sanitizeUrl,
insertSolidTopArrowHead,
insertSolidBottomArrowHead,
insertStickTopArrowHead,
insertStickBottomArrowHead,
};

View File

@@ -35,6 +35,7 @@ export interface Message {
type?: number;
activate?: boolean;
placement?: string;
centralConnection?: number;
}
export interface AddMessageParams {
@@ -50,6 +51,8 @@ export interface AddMessageParams {
| 'destroyParticipant'
| 'activeStart'
| 'activeEnd'
| 'centralConnection'
| 'centralConnectionReverse'
| 'addNote'
| 'addLinks'
| 'addALink'

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

View File

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

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)

View File

@@ -1,5 +1,17 @@
# 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

@@ -17,25 +17,25 @@
},
"dependencies": {
"@mdi/font": "^7.4.47",
"@vueuse/core": "^13.1.0",
"@vueuse/core": "^13.9.0",
"font-awesome": "^4.7.0",
"jiti": "^2.4.2",
"mermaid": "workspace:^",
"vue": "^3.4.38"
"vue": "^3.5.21"
},
"devDependencies": {
"@iconify-json/carbon": "^1.1.37",
"@unocss/reset": "^66.0.0",
"@iconify-json/carbon": "^1.2.13",
"@unocss/reset": "^66.5.1",
"@vite-pwa/vitepress": "^1.0.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue": "^6.0.1",
"fast-glob": "^3.3.3",
"https-localhost": "^4.7.1",
"pathe": "^2.0.3",
"unocss": "^66.4.2",
"unplugin-vue-components": "^28.4.0",
"vite": "^7.0.0",
"vite-plugin-pwa": "^1.0.0",
"vitepress": "1.6.3",
"unocss": "^66.5.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^7.0.7",
"vite-plugin-pwa": "^1.0.3",
"vitepress": "1.6.4",
"workbox-window": "^7.3.0"
}
}

View File

@@ -156,7 +156,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

@@ -83,7 +83,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)
@@ -97,7 +96,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
@@ -107,7 +105,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
@@ -116,18 +113,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

@@ -216,7 +216,11 @@ Messages can be of two displayed either solid or with a dotted line.
[Actor][Arrow][Actor]:Message text
```
There are ten types of arrows currently supported:
Lines can be solid or dotted, and can end with various types of arrowheads, crosses, or open arrows.
#### Supported Arrow Types
**Standard Arrow Types**
| Type | Description |
| -------- | ---------------------------------------------------- |
@@ -231,6 +235,49 @@ There are ten types of arrows currently supported:
| `-)` | Solid line with an open arrow at the end (async) |
| `--)` | Dotted line with a open arrow at the end (async) |
**Half-Arrows (v<MERMAID_RELEASE_VERSION>+)**
The following half-arrow types are supported for more expressive sequence diagrams. Both solid and dotted variants are available by increasing the number of dashes (`-` → `--`).
---
| Type | Description |
| ------- | ---------------------------------------------------- |
| `-\|\` | Solid line with top half arrowhead |
| `--\|\` | Dotted line with top half arrowhead |
| `-\|/` | Solid line with bottom half arrowhead |
| `--\|/` | Dotted line with bottom half arrowhead |
| `/\|-` | Solid line with reverse top half arrowhead |
| `/\|--` | Dotted line with reverse top half arrowhead |
| `\\-` | Solid line with reverse bottom half arrowhead |
| `\\--` | Dotted line with reverse bottom half arrowhead |
| `-\\` | Solid line with top stick half arrowhead |
| `--\\` | Dotted line with top stick half arrowhead |
| `-//` | Solid line with bottom stick half arrowhead |
| `--//` | Dotted line with bottom stick half arrowhead |
| `//-` | Solid line with reverse top stick half arrowhead |
| `//--` | Dotted line with reverse top stick half arrowhead |
| `\\-` | Solid line with reverse bottom stick half arrowhead |
| `\\--` | Dotted line with reverse bottom stick half arrowhead |
## Central Connections (v<MERMAID_RELEASE_VERSION>+)
Mermaid sequence diagrams support **central lifeline connections** using a `()`.
This is useful to represent messages or signals that connect to a central point, rather than from one actor directly to another.
To indicate a central connection, append `()` to the arrow syntax.
#### Basic Syntax
```mermaid-example
sequenceDiagram
participant Alice
participant John
Alice->>()John: Hello John
Alice()->>John: How are you?
John()->>()Alice: Great!
```
## Activations
It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations:

View File

@@ -207,7 +207,7 @@ describe('when using mermaid and ', () => {
[Error: Parse error on line 2:
...equenceDiagramAlice:->Bob: Hello Bob, h...
----------------------^
Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT']
Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT']
`);
});

View File

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

View File

@@ -1,4 +1,5 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { evaluate } from '../../diagrams/common/common.js';
import { log } from '../../logger.js';
import { createText } from '../createText.js';
import utils from '../../utils.js';
@@ -44,8 +45,8 @@ export const getLabelStyles = (styleArray) => {
};
export const insertEdgeLabel = async (elem, edge) => {
const config = getConfig();
let useHtmlLabels = config.flowchart.htmlLabels;
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
const { labelStyles } = styles2String(edge);
edge.labelStyle = labelStyles;
const labelElement = await createText(elem, edge.label, {

View File

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

View File

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

View File

@@ -0,0 +1,168 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import type { D3Selection } from '../../../types.js';
import intersect from '../intersect/index.js';
/**
* Get actor styling based on metadata
*/
const getActorStyling = (metadata?: Record<string, string>) => {
const defaults = {
fillColor: 'none',
strokeColor: 'black',
strokeWidth: 2,
type: 'solid',
};
if (!metadata) {
return defaults;
}
return {
fillColor: metadata.type === 'hollow' ? 'none' : metadata.fillColor || defaults.fillColor,
strokeColor: metadata.strokeColor || defaults.strokeColor,
strokeWidth: parseInt(metadata.strokeWidth || '2', 10),
type: metadata.type || defaults.type,
};
};
/**
* Create stick figure path data
* This generates the SVG path for a stick figure centered at (x, y)
*/
const createStickFigurePathD = (x: number, y: number, scale = 1.5): string => {
// Base path template (centered at origin):
// M 0 -4 C 4.4183 -4 8 -7.5817 8 -12 C 8 -16.4183 4.4183 -20 0 -20 C -4.4183 -20 -8 -16.4183 -8 -12 C -8 -7.5817 -4.4183 -4 0 -4 Z M 0 -4 V 5 M -10 14.5 L 0 5 M 10 14.5 L 0 5 M -11 0 H 11
// Scale all coordinates
const s = (val: number) => val * scale;
// Translate the path to the desired position
return [
// Head (circle using cubic bezier curves)
`M ${x + s(0)} ${y + s(-4)}`,
`C ${x + s(4.4183)} ${y + s(-4)} ${x + s(8)} ${y + s(-7.5817)} ${x + s(8)} ${y + s(-12)}`,
`C ${x + s(8)} ${y + s(-16.4183)} ${x + s(4.4183)} ${y + s(-20)} ${x + s(0)} ${y + s(-20)}`,
`C ${x + s(-4.4183)} ${y + s(-20)} ${x + s(-8)} ${y + s(-16.4183)} ${x + s(-8)} ${y + s(-12)}`,
`C ${x + s(-8)} ${y + s(-7.5817)} ${x + s(-4.4183)} ${y + s(-4)} ${x + s(0)} ${y + s(-4)}`,
'Z',
// Body (vertical line from head to torso)
`M ${x + s(0)} ${y + s(-4)}`,
`V ${y + s(5)}`,
// Left leg
`M ${x + s(-10)} ${y + s(14.5)}`,
`L ${x + s(0)} ${y + s(5)}`,
// Right leg
`M ${x + s(10)} ${y + s(14.5)}`,
`L ${x + s(0)} ${y + s(5)}`,
// Arms (horizontal line)
`M ${x + s(-11)} ${y + s(0)}`,
`H ${x + s(11)}`,
].join(' ');
};
/**
* Draw traditional stick figure
*/
const drawStickFigure = (
actorGroup: D3Selection<SVGGElement>,
styling: ReturnType<typeof getActorStyling>,
node: Node
): void => {
const x = 0; // Center at origin
const y = -10; // Adjust vertical position
actorGroup.attr('class', 'usecase-actor-shape');
const pathData = createStickFigurePathD(x, y);
if (node.look === 'handDrawn') {
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(actorGroup);
const options = userNodeOverrides(node, {
stroke: styling.strokeColor,
strokeWidth: styling.strokeWidth,
fill: styling.fillColor,
});
// Draw the stick figure using the path
const stickFigure = rc.path(pathData, options);
actorGroup.insert(() => stickFigure, ':first-child');
} else {
// Draw the stick figure using standard SVG path
actorGroup
.append('path')
.attr('d', pathData)
.attr('fill', styling.fillColor)
.attr('stroke', styling.strokeColor)
.attr('stroke-width', styling.strokeWidth);
}
};
/**
* Custom shape handler for usecase actors (stick figure)
*/
export async function usecaseActor<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node
) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
// Get actor metadata from node
const metadata = (node as Node & { metadata?: Record<string, string> }).metadata;
const styling = getActorStyling(metadata);
// Create actor group
const actorGroup = shapeSvg.append('g');
// Add metadata as data attributes for CSS styling
if (metadata) {
Object.entries(metadata).forEach(([key, value]) => {
actorGroup.attr(`data-${key}`, value);
});
}
// Draw stick figure
drawStickFigure(actorGroup, styling, node);
// Get the actual bounding box of the rendered actor
const actorBBox = actorGroup.node()?.getBBox();
const actorHeight = actorBBox?.height ?? 70;
// Actor name (always rendered below the figure)
const labelY = actorHeight / 2 + 15; // Position label below the figure
// Calculate label height from the actual text element
const labelBBox = label.node()?.getBBox() ?? { height: 20 };
const labelHeight = labelBBox.height + 10; // Space for label below
const totalHeight = actorHeight + labelHeight;
actorGroup.attr('transform', `translate(${0}, ${-totalHeight / 2 + 35})`);
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${labelY / 2 - 15} )`
);
if (nodeStyles && node.look !== 'handDrawn') {
actorGroup.selectChildren('path').attr('style', nodeStyles);
}
// Update node bounds for layout - this will set node.width and node.height from the bounding box
updateNodeBounds(node, actorGroup);
// Override height to include label space
// Width is kept from updateNodeBounds as it correctly reflects the actor's visual width
node.height = totalHeight;
// Add intersect function for edge connection points
// Use rectangular intersection since the actor has a rectangular bounding box
node.intersect = function (point) {
return intersect.rect(node, point);
};
return shapeSvg;
}

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