Compare commits

..

120 Commits

Author SHA1 Message Date
Sidharth Vinod
f8a0a61b5e Merge branch 'develop' into sidv/iconifyNative 2025-10-30 02:30:15 +09:00
Sidharth Vinod
f28f3c25aa Merge pull request #7116 from mermaid-js/sidv/lockfileValidation
fix: Lockfile validation
2025-10-30 02:29:50 +09:00
Sidharth Vinod
525e630ebc fix: indentation 2025-10-30 02:01:45 +09:00
Sidharth Vinod
58137aa631 feat: Allow validation workflow to run on forks 2025-10-30 01:56:18 +09:00
Sidharth Vinod
e7719f14c5 fix: Prevent duplicate comments by validation workflow 2025-10-30 01:55:58 +09:00
Sidharth Vinod
35d9cead8a chore: Cleanup lockfile 2025-10-30 01:22:17 +09:00
Sidharth Vinod
928ae32063 docs: Update icons documentation 2025-10-30 01:20:53 +09:00
Sidharth Vinod
25d96a90de Merge branch 'sidv/iconifyNative' of https://github.com/mermaid-js/mermaid into sidv/iconifyNative
* 'sidv/iconifyNative' of https://github.com/mermaid-js/mermaid:
  [autofix.ci] apply automated fixes
2025-10-29 22:08:39 +09:00
Sidharth Vinod
c24a1fb1b9 test: Add test for timeout 2025-10-29 22:08:18 +09:00
autofix-ci[bot]
c607163999 [autofix.ci] apply automated fixes 2025-10-29 08:50:25 +00:00
Sidharth Vinod
df21885a27 test: Visual tests for icons 2025-10-29 17:43:19 +09:00
Sidharth Vinod
172030377f feat: Add support for icons packs via config 2025-10-29 17:11:50 +09:00
Sidharth Vinod
a23c2baed8 refactor: Convert icon manager into class 2025-10-29 01:38:42 +09:00
Ashish Jain
13baf51b35 Merge 11.12.1 back to develop 2025-10-28 11:23:19 +01:00
Justin Greywolf
9af985ba9b Merge pull request #5814 from kairi003/feature/4706_allow_notes_in_namespace
feat: allow to put notes in namespaces on classDiagram
2025-10-27 19:21:00 +00:00
kairi003
700aa100f2 fix: style broken caused by a mistake in merge 3f46c94a 2025-10-26 04:18:19 +09:00
kairi003
49103ea654 fix: update ClassDB and ClassTypes for improved type safety and consistency 2025-10-26 03:01:00 +09:00
kairi003
3f46c94ab2 Merge branch 'develop' into tmp/feature/4706_allow_notes_in_namespace 2025-10-26 02:39:38 +09:00
Knut Sveidqvist
fed8a523a4 Merge pull request #7076 from mermaid-js/6919-fix-incorrect-viewBox-casing
6919: correct viewBox casing in Radar & Packet
2025-10-21 12:20:04 +00:00
Knut Sveidqvist
33b4946e21 Merge branch 'develop' into 6919-fix-incorrect-viewBox-casing 2025-10-21 10:33:21 +02:00
Justin Greywolf
3d768f3adf Merge pull request #6975 from ilovelinux/fix/6974-ilovelinux-fix-classdiagram-example
Fix classDiagram example
2025-10-17 02:56:34 +00:00
darshanr0107
76e17ffd20 fix: add validation to ensure correct casing of 'viewBox' in all rendering tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-15 18:56:49 +05:30
darshanr0107
60f633101c chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 19:14:22 +05:30
Shubham P
18f51eb14e Merge pull request #6843 from saurabhg772244/saurabh/fix-edge-animation-for-hand-dranw-shapes
Fixed edge animation for hand drawn shapes
2025-10-13 13:14:24 +00:00
Shubham P
2bb57bf7d2 Merge pull request #6989 from mermaid-js/subgraph-td-direction
6338: allow direction TD inside subgraphs
2025-10-13 13:13:46 +00:00
Shubham P
a6276daffd Merge pull request #7055 from mermaid-js/sequence-diagram-participant-name-parsing
6853:  prevent browser freeze caused by invalid participant name in sequenceDiagram
2025-10-13 13:12:08 +00:00
darshanr0107
7def6eecbf fix: correct viewBox casing in radar and packet diagram
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 18:15:38 +05:30
Antonio Spadaro
ac411a7d7e Fix class diagram example 2025-10-11 22:56:37 +02:00
Justin Greywolf
d80a638e55 Merge pull request #6023 from yari-dewalt/fix_lollipop_stroke
Fix: Delete 'stroke: black' from lollipop marker so it matches themes and edge paths
2025-10-11 20:43:08 +00:00
Justin Greywolf
7a869c08a2 Merge pull request #6026 from yari-dewalt/bug/5669_class-diagram-fix-namespace-themes
Fix: Class diagram namespaces black lines, not responding to theme variables
2025-10-11 20:42:53 +00:00
Justin Greywolf
44e8cbb1de Merge branch 'develop' into bug/5669_class-diagram-fix-namespace-themes 2025-10-11 13:27:47 -07:00
Justin Greywolf
efe38b8425 Merge branch 'develop' into fix_lollipop_stroke 2025-10-11 13:26:59 -07:00
darshanr0107
6fecb985e8 fix: failing argos
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-09 11:50:44 +05:30
darshanr0107
69b338d8af fix: formatting issues
Co-authored-by: omkarht <omkar@mermaidchart.com>
2025-10-09 11:37:05 +05:30
darshanr0107
fa15ce8502 chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-08 13:23:28 +05:30
darshanr0107
6d0650918f fix: handle participant names with spaces correctly in sequenceDiagram
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-08 13:09:30 +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
darshanr0107
1a9d45abf0 fix: allow direction TD inside subgraphs
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-24 12:22:31 +05:30
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
omkarht
d3de3ecbbb Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-17 12:41:51 +05:30
omkarht
3964ce0a0f Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-15 18:15:59 +05:30
omkarht
4dbabba8e8 Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-15 12:50:11 +05:30
Saurabh Gore
bbb93b263d Merge branch 'develop' into saurabh/fix-edge-animation-for-hand-dranw-shapes 2025-09-13 11:37:21 +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
saurabhg772244
4240340a18 Merge branch 'develop' of github.com:mermaid-js/mermaid into saurabh/fix-edge-animation-for-hand-dranw-shapes 2025-09-11 11:36:26 +05:30
omkarht
ca10a259fa Merge branch 'develop' into saurabh/fix-edge-animation-for-hand-dranw-shapes 2025-09-05 16:28:39 +05:30
Saurabh Gore
0ed9c65572 Merge branch 'develop' into saurabh/fix-edge-animation-for-hand-dranw-shapes 2025-09-05 12:56:27 +05:30
saurabhg772244
56cc12690f Added option to skip screenshot for cypress tests 2025-09-05 12:47:07 +05:30
omkarht
2cdaf03ada Merge branch 'develop' into 6638-sequence-diagram-additional-messages 2025-09-05 12:12:01 +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
saurabhg772244
e6fb4a84da code refactor 2025-08-11 16:32:03 +05:30
saurabhg772244
32723b2de1 Added change set 2025-08-11 15:59:07 +05:30
saurabhg772244
18703782ee Fixed edge animation for hand drawn shapes 2025-08-11 15:52:49 +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
autofix-ci[bot]
47297f7c26 [autofix.ci] apply automated fixes 2025-02-26 17:49:51 +00:00
Yari DeWalt
967aa0629e Merge branch 'develop' into bug/5669_class-diagram-fix-namespace-themes 2025-02-26 09:45:14 -08:00
Yari DeWalt
04b20a79b9 Merge branch 'develop' into fix_lollipop_stroke 2025-02-26 09:20:16 -08:00
kairi
d60b09cafc Merge branch 'develop' into feature/4706_allow_notes_in_namespace 2024-12-01 09:20:39 +09:00
kairi003
875827f59b Merge branch 'develop' into feature/4706_allow_notes_in_namespace 2024-11-21 07:16:47 +09:00
kairi003
c4e08261b5 Merge branch 'develop' into feature/4706_allow_notes_in_namespace 2024-11-10 23:56:48 +09:00
kairi003
70f679d2fa add: e2e test for classDiagram-v3 2024-11-10 20:27:35 +09:00
kairi003
25c43fa439 add: horizontal rules to demos/classchart.html 2024-11-10 02:34:31 +09:00
kairi003
ec1c6325d4 refactor: Add type annotations and optimize redundant set loop 2024-11-10 02:34:31 +09:00
kairi003
309ff6be38 fix: Reimplement notes in namespaces for classRenderer-v3 (#5880) 2024-11-10 02:32:20 +09:00
kairi003
02d368df05 chore: relax git version constraint in Dockerfile to allow patch updates 2024-11-10 01:58:29 +09:00
kairi003
4ee1fe2ca4 Merge branch 'master' into feature/4706_allow_notes_in_namespace 2024-11-10 01:52:46 +09:00
yari-dewalt
4ff2ae9f4e Add styles for clusters and remove hard-coded styling for namespace nodes 2024-11-06 09:00:21 -08:00
Yari DeWalt
7a729e8f16 Merge branch 'develop' into fix_lollipop_stroke 2024-11-04 10:21:00 -08:00
yari-dewalt
3c7fd95617 Remove black stroke from lollipops so it matches edge paths 2024-11-04 10:15:51 -08:00
kairi003
2dd29bee25 add changelogs 2024-09-08 02:13:33 +09:00
kairi003
259a508d8a add e2e test 2024-09-08 01:31:26 +09:00
kairi
1963064369 Merge branch 'develop' into feature/4706_allow_notes_in_namespace 2024-09-08 00:27:14 +09:00
kairi
a101ce803c Merge branch 'develop' into feature/4706_allow_notes_in_namespace 2024-09-04 14:14:19 +09:00
kairi003
fc0c7936d1 feat: Add support for adding notes to namespaces in class diagrams 2024-09-03 15:27:31 +09:00
kairi003
2d2add5b44 Change: use Map for notes 2024-09-03 15:22:59 +09:00
kairi003
58fd5ddbaf Add: demo for notes in namespace on classDialog 2024-09-03 15:22:41 +09:00
68 changed files with 3845 additions and 813 deletions

View File

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

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Support edge animation in hand drawn look

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Resolved parsing error where direction TD was not recognized within subgraphs

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Correct viewBox casing and make SVGs responsive

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Improve participant parsing and prevent recursive loops on invalid syntax

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': minor
---
feat: allow to put notes in namespaces on classDiagram

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

@@ -22,6 +22,7 @@ mermaidchart
mermaidjs
mindmap
mindmaps
mmdc
mrtree
multigraph
nodesep

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:

View File

@@ -1,7 +1,7 @@
name: Validate pnpm-lock.yaml
on:
pull_request:
pull_request_target:
paths:
- 'pnpm-lock.yaml'
- '**/package.json'
@@ -15,6 +15,8 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Set up Node.js
uses: actions/setup-node@v4
@@ -55,16 +57,41 @@ jobs:
exit 1
fi
- name: Find existing lockfile validation comment
if: always()
uses: peter-evans/find-comment@v3
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: 'Lockfile Validation Failed'
- name: Comment on PR if validation failed
if: failure()
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
❌ **Lockfile Validation Failed**
The following issue(s) were detected:
${{ steps.validate.outputs.errors }}
Please address these and push an update.
_Posted automatically by GitHub Actions_
- name: Delete comment if validation passed
if: success() && steps.find-comment.outputs.comment-id != ''
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ steps.find-comment.outputs.comment-id }},
});

View File

@@ -5,7 +5,7 @@ USER 0:0
RUN corepack enable \
&& corepack enable pnpm
RUN apk add --no-cache git~=2.43.4 \
RUN apk add --no-cache git~=2.43 \
&& git config --add --system safe.directory /mermaid
ENV NODE_OPTIONS="--max_old_space_size=8192"

View File

@@ -6,6 +6,7 @@ interface CypressConfig {
listUrl?: boolean;
listId?: string;
name?: string;
screenshot?: boolean;
}
type CypressMermaidConfig = MermaidConfig & CypressConfig;
@@ -90,7 +91,7 @@ export const renderGraph = (
export const openURLAndVerifyRendering = (
url: string,
options: CypressMermaidConfig,
{ screenshot = true, ...options }: CypressMermaidConfig,
validation?: any
): void => {
const name: string = (options.name ?? cy.state('runnable').fullTitle()).replace(/\s+/g, '-');
@@ -98,12 +99,15 @@ export const openURLAndVerifyRendering = (
cy.visit(url);
cy.window().should('have.property', 'rendered', true);
cy.get('svg').should('be.visible');
cy.get('svg').should('not.have.attr', 'viewbox');
if (validation) {
cy.get('svg').should(validation);
}
verifyScreenshot(name);
if (screenshot) {
verifyScreenshot(name);
}
};
export const verifyScreenshot = (name: string): void => {

View File

@@ -562,6 +562,20 @@ class C13["With Città foreign language"]
`
);
});
it('should add notes in namespaces', function () {
imgSnapshotTest(
`
classDiagram
note "This is a outer note"
note for C1 "This is a outer note for C1"
namespace Namespace1 {
note "This is a inner note"
note for C1 "This is a inner note for C1"
class C1
}
`
);
});
it('should render a simple class diagram with no members', () => {
imgSnapshotTest(
`

View File

@@ -709,6 +709,20 @@ class C13["With Città foreign language"]
`
);
});
it('should add notes in namespaces', function () {
imgSnapshotTest(
`
classDiagram
note "This is a outer note"
note for C1 "This is a outer note for C1"
namespace Namespace1 {
note "This is a inner note"
note for C1 "This is a inner note for C1"
class C1
}
`
);
});
it('should render a simple class diagram with no members', () => {
imgSnapshotTest(
`

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

@@ -1029,4 +1029,19 @@ graph TD
}
);
});
it('FDH49: should add edge animation', () => {
renderGraph(
`
flowchart TD
A(["Start"]) L_A_B_0@--> B{"Decision"}
B --> C["Option A"] & D["Option B"]
style C stroke-width:4px,stroke-dasharray: 5
L_A_B_0@{ animation: slow }
L_B_D_0@{ animation: fast }`,
{ look: 'handDrawn', screenshot: false }
);
cy.get('path#L_A_B_0').should('have.class', 'edge-animation-slow');
cy.get('path#L_B_D_0').should('have.class', 'edge-animation-fast');
});
});

View File

@@ -774,6 +774,21 @@ describe('Graph', () => {
expect(svg).to.not.have.attr('style');
});
});
it('40: should add edge animation', () => {
renderGraph(
`
flowchart TD
A(["Start"]) L_A_B_0@--> B{"Decision"}
B --> C["Option A"] & D["Option B"]
style C stroke-width:4px,stroke-dasharray: 5
L_A_B_0@{ animation: slow }
L_B_D_0@{ animation: fast }`,
{ screenshot: false }
);
// Verify animation classes are applied to both edges
cy.get('path#L_A_B_0').should('have.class', 'edge-animation-slow');
cy.get('path#L_B_D_0').should('have.class', 'edge-animation-fast');
});
it('58: handle styling with style expressions', () => {
imgSnapshotTest(
`
@@ -973,4 +988,19 @@ graph TD
}
);
});
it('70: should render a subgraph with direction TD', () => {
imgSnapshotTest(
`
flowchart LR
subgraph A
direction TD
a --> b
end
`,
{
fontFamily: 'courier',
}
);
});
});

View File

@@ -0,0 +1,240 @@
import { imgSnapshotTest } from '../../helpers/util';
describe('Icons rendering tests', () => {
it('should render icon from config pack', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Docker' }
B --> C[End]
`);
});
it('should render icons from different packs', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
simple-icons: "@iconify-json/simple-icons@1"
---
flowchart TB
A@{ icon: 'logos:aws', label: 'AWS' } --> B@{ icon: 'logos:docker', label: 'Docker' }
B --> C@{ icon: 'logos:kubernetes', label: 'K8s' }
C --> D@{ icon: 'simple-icons:github', label: 'GitHub' }
`);
});
it('should use custom CDN template', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
cdnTemplate: "https://cdn.jsdelivr.net/npm/\${packageSpec}/icons.json"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Docker' }
B --> C[End]
`);
});
it('should use different allowed hosts', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
allowedHosts:
- cdn.jsdelivr.net
- unpkg.com
---
flowchart TB
A[Start] --> B@{ icon: 'logos:aws', label: 'AWS' }
`);
});
it('should render icon with label at top', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Docker Container', pos: 't' }
`);
});
it('should render icon with label at bottom', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:kubernetes', label: 'Kubernetes', pos: 'b' }
`);
});
it('should render icon with long label', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'This is a very long label for Docker container orchestration', h: 64 }
`);
});
it('should render large icon', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Large', h: 80, w: 80 }
`);
});
it('should render small icon', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Small', h: 32, w: 32 }
`);
});
it('should apply custom styles to icon shape', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Styled', form: 'square' }
B --> C[End]
style B fill:#0db7ed,stroke:#333,stroke-width:4px
`);
});
it('should use classDef with icons', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
classDef dockerIcon fill:#0db7ed,stroke:#fff,stroke-width:2px
classDef awsIcon fill:#FF9900,stroke:#fff,stroke-width:2px
A[Start] --> B@{ icon: 'logos:docker', label: 'Docker' }
B --> C@{ icon: 'logos:aws', label: 'AWS' }
B:::dockerIcon
C:::awsIcon
`);
});
it('should render in TB layout', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Docker' }
B --> C[End]
`);
});
it('should render in LR layout', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart LR
A[Start] --> B@{ icon: 'logos:kubernetes', label: 'K8s' }
B --> C[End]
`);
});
it('should handle unknown icon gracefully', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'unknown:invalid', label: 'Unknown Icon' }
B --> C[End]
`);
});
it('should handle timeouts gracefully', () => {
imgSnapshotTest(`---
config:
icons:
timeout: 1
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:aws', label: 'Timeout' }
B --> C[End]
`);
});
it('should handle missing pack gracefully', () => {
imgSnapshotTest(`flowchart TB
A[Start] --> B@{ icon: 'missing:icon', label: 'Missing Pack Icon' }
`);
});
it('should render multiple icons in sequence', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:aws', label: 'AWS' }
B --> C@{ icon: 'logos:docker', label: 'Docker' }
C --> D@{ icon: 'logos:kubernetes', label: 'K8s' }
D --> E[End]
`);
});
it('should render icons in parallel branches', () => {
imgSnapshotTest(`---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Docker' }
A --> C@{ icon: 'logos:kubernetes', label: 'K8s' }
B --> D[End]
C --> D
`);
});
});

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

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

@@ -603,6 +603,10 @@
</div>
<div class="test">
<pre class="mermaid">
---
config:
theme: dark
---
classDiagram
test ()--() test2
</pre>

View File

@@ -184,6 +184,7 @@
}
Admin --> Report : generates
</pre>
<hr />
<pre class="mermaid">
classDiagram
namespace Company.Project.Module {
@@ -240,6 +241,20 @@
Bike --> Square : "Logo Shape"
</pre>
<hr />
<pre class="mermaid">
classDiagram
note "This is a outer note"
note for Class1 "This is a outer note for Class1"
namespace ns {
note "This is a inner note"
note for Class1 "This is a inner note for Class1"
class Class1
class Class2
}
</pre>
<hr />
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({

View File

@@ -4,15 +4,150 @@
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/icons.md](../../packages/mermaid/src/docs/config/icons.md).
# Registering icon pack in mermaid
# Icon Pack Configuration
The icon packs available can be found at [icones.js.org](https://icones.js.org/).
We use the name defined when registering the icon pack, to override the prefix field of the iconify pack. This allows the user to use shorter names for the icons. It also allows us to load a particular pack only when it is used in a diagram.
Mermaid supports icons through Iconify-compatible icon packs. You can register icon packs either **declaratively via configuration** (recommended for most use cases) or **programmatically via JavaScript API** (for advanced/offline scenarios).
Using JSON file directly from CDN:
## Declarative Configuration (v\<MERMAID_RELEASE_VERSION>+) (Recommended)
The easiest way to use icons in Mermaid is through declarative configuration. This works in browsers, CLI, Live Editor, and headless renders without requiring custom JavaScript.
### Basic Usage
Configure icon packs in your Mermaid config:
```yaml
---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Docker' }
B --> C[End]
```
Or in JavaScript:
```js
mermaid.initialize({
icons: {
packs: {
logos: '@iconify-json/logos@1',
'simple-icons': '@iconify-json/simple-icons@1',
},
},
});
```
### Package Spec Format
Icon packs are specified using **package specs** with version pinning:
- Format: `@scope/package@<version>` or `package@<version>`
- **Must include at least a major version** (e.g., `@iconify-json/logos@1`)
- Minor and patch versions are optional (e.g., `@iconify-json/logos@1.2.3`)
**Important**: Only package specs are supported. Direct URLs are not allowed for security reasons.
### Configuration Options
```yaml
icons:
packs:
# Icon pack configuration
# Key: local name to use in diagrams
# Value: package spec with version
logos: '@iconify-json/logos@1'
'simple-icons': '@iconify-json/simple-icons@1'
# CDN template for resolving package specs
# Must contain ${packageSpec} placeholder
cdnTemplate: 'https://cdn.jsdelivr.net/npm/${packageSpec}/icons.json'
# Maximum file size in MB for icon pack JSON files
# Range: 1-10 MB, default: 5 MB
maxFileSizeMB: 5
# Network timeout in milliseconds for icon pack fetches
# Range: 1000-30000 ms, default: 5000 ms
timeout: 5000
# List of allowed hosts to fetch icons from
allowedHosts:
- 'unpkg.com'
- 'cdn.jsdelivr.net'
- 'npmjs.com'
```
### Security Features
- **Host allowlisting**: Only fetch from hosts in `allowedHosts`
- **Size limits**: Maximum file size enforced via `maxFileSizeMB`
- **Timeouts**: Network requests timeout after specified milliseconds
- **HTTPS only**: All remote fetches occur over HTTPS
- **Version pinning**: Package specs must include version for reproducibility
### Examples
#### Using Custom CDN Template
```yaml
---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
cdnTemplate: "https://unpkg.com/${packageSpec}/icons.json"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:aws', label: 'AWS' }
```
#### Multiple Icon Packs
```yaml
---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
"simple-icons": "@iconify-json/simple-icons@1"
---
flowchart TB
A@{ icon: 'logos:docker', label: 'Docker' } --> B@{ icon: 'simple-icons:github', label: 'GitHub' }
```
#### CLI Usage
Create a `mermaid.json` config file:
```json
{
"icons": {
"packs": {
"logos": "@iconify-json/logos@1"
}
}
}
```
Then use it with the CLI:
```bash
mmdc -i diagram.mmd -o diagram.svg --configFile mermaid.json
```
## Programmatic API (v11.1.0+) (Advanced)
For advanced scenarios or offline use, you can register icon packs programmatically:
### Using JSON File from CDN
```js
import mermaid from 'CDN/mermaid.esm.mjs';
mermaid.registerIconPacks([
{
name: 'logos',
@@ -22,13 +157,15 @@ mermaid.registerIconPacks([
]);
```
Using packages and a bundler:
### Using Packages with Bundler
Install the icon pack:
```bash
npm install @iconify-json/logos@1
```
With lazy loading
#### With Lazy Loading
```js
import mermaid from 'mermaid';
@@ -41,15 +178,39 @@ mermaid.registerIconPacks([
]);
```
Without lazy loading
#### Without Lazy Loading
```js
import mermaid from 'mermaid';
import { icons } from '@iconify-json/logos';
mermaid.registerIconPacks([
{
name: icons.prefix, // To use the prefix defined in the icon pack
name: icons.prefix, // Use the prefix defined in the icon pack
icons,
},
]);
```
## Finding Icon Packs
Icon packs available for use can be found at [iconify.design](https://icon-sets.iconify.design/) or [icones.js.org](https://icones.js.org/).
The pack name you register is the **local name** used in diagrams. It can differ from the pack's prefix, allowing you to:
- Use shorter names (e.g., register `@iconify-json/material-design-icons` as `mdi`)
- Load specific packs only when used in diagrams (lazy loading)
## Error Handling
If an icon cannot be found:
- The diagram still renders (non-fatal)
- A fallback icon is displayed
- A warning is logged (visible in CLI stderr or browser console)
## Licensing
Iconify JSON format is MIT licensed, but **individual icons may have varying licenses**. Please verify the licenses of the icon packs you configure before use in production.
Mermaid does **not** redistribute third-party icon assets in the core bundle.

View File

@@ -229,6 +229,14 @@ Defined in: [packages/mermaid/src/config.type.ts:124](https://github.com/mermaid
---
### icons?
> `optional` **icons**: `IconsConfig`
Defined in: [packages/mermaid/src/config.type.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223)
---
### journey?
> `optional` **journey**: `JourneyDiagramConfig`

View File

@@ -12,7 +12,7 @@
> **ParseErrorFunction** = (`err`, `hash?`) => `void`
Defined in: [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10)
Defined in: [packages/mermaid/src/Diagram.ts:11](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L11)
## Parameters

View File

@@ -21,7 +21,7 @@ title: Animal example
classDiagram
note "From Duck till Zebra"
Animal <|-- Duck
note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging"
note for Duck "can fly<br>can swim<br>can dive<br>can help in debugging"
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
@@ -50,7 +50,7 @@ title: Animal example
classDiagram
note "From Duck till Zebra"
Animal <|-- Duck
note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging"
note for Duck "can fly<br>can swim<br>can dive<br>can help in debugging"
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age

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,12 +63,12 @@
]
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.44.9",
"@argos-ci/cypress": "^6.1.1",
"@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.4",
"@types/cors": "^2.8.19",
@@ -77,22 +77,22 @@
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.20",
"@types/mdast": "^4.0.4",
"@types/node": "^22.13.17",
"@types/node": "^22.18.6",
"@types/rollup-plugin-visualizer": "^5.0.3",
"@vitest/coverage-v8": "^3.0.9",
"@vitest/spy": "^3.0.9",
"@vitest/ui": "^3.0.9",
"@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.5",
"cspell": "^9.2.1",
"cypress": "^14.5.4",
"cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.21",
"esbuild": "^0.25.9",
"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,10 +106,10 @@
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-unicorn": "^59.0.1",
"express": "^5.1.0",
"globals": "^16.0.0",
"globals": "^16.4.0",
"globby": "^14.1.0",
"husky": "^9.1.7",
"jest": "^30.0.5",
"jest": "^30.1.3",
"jison": "^0.4.18",
"js-yaml": "^4.1.0",
"jsdom": "^26.1.0",
@@ -118,18 +118,18 @@
"markdown-table": "^3.0.4",
"nyc": "^17.1.0",
"path-browserify": "^1.0.1",
"prettier": "^3.5.3",
"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.13",
"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.6",
"vite": "^7.0.7",
"vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.0.9"
"vitest": "^3.2.4"
},
"nyc": {
"report-dir": "coverage/cypress"

View File

@@ -42,7 +42,7 @@
"khroma": "^2.1.0"
},
"devDependencies": {
"concurrently": "^9.1.2",
"concurrently": "^9.2.1",
"mermaid": "workspace:*",
"rimraf": "^6.0.1"
},

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

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

View File

@@ -68,10 +68,10 @@
},
"dependencies": {
"@braintree/sanitize-url": "^7.1.1",
"@iconify/utils": "^3.0.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",
@@ -82,7 +82,7 @@
"katex": "^0.16.22",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^16.2.1",
"marked": "^16.3.0",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
@@ -105,9 +105,9 @@
"@types/stylis": "^4.2.7",
"@types/uuid": "^10.0.0",
"ajv": "^8.17.1",
"canvas": "^3.1.2",
"canvas": "^3.2.0",
"chokidar": "3.6.0",
"concurrently": "^9.1.2",
"concurrently": "^9.2.1",
"csstree-validator": "^4.0.1",
"globby": "^14.1.0",
"jison": "^0.4.18",
@@ -116,14 +116,14 @@
"json-schema-to-typescript": "^15.0.4",
"micromatch": "^4.0.8",
"path-browserify": "^1.0.1",
"prettier": "^3.5.3",
"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.13",
"type-fest": "^4.35.0",
"typedoc": "^0.28.12",
"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",

View File

@@ -1,10 +1,11 @@
import * as configApi from './config.js';
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
import { UnknownDiagramError } from './errors.js';
import { encodeEntities } from './utils.js';
import type { DetailedError } from './utils.js';
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
import { UnknownDiagramError } from './errors.js';
import { registerDiagramIconPacks } from './rendering-util/icons.js';
import type { DetailedError } from './utils.js';
import { encodeEntities } from './utils.js';
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void;
@@ -41,6 +42,7 @@ export class Diagram {
if (metadata.title) {
db.setDiagramTitle?.(metadata.title);
}
registerDiagramIconPacks(config.icons);
await parser.parse(text);
return new Diagram(type, text, db, parser, renderer);
}

View File

@@ -220,6 +220,7 @@ export interface MermaidConfig {
*
*/
suppressErrorRendering?: boolean;
icons?: IconsConfig;
}
/**
* The object containing configurations specific for flowcharts
@@ -1623,6 +1624,46 @@ export interface RadarDiagramConfig extends BaseDiagramConfig {
*/
curveTension?: number;
}
/**
* Configuration for icon packs and CDN template.
* Enables icons in browsers and CLI/headless renders without custom JavaScript.
*
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "IconsConfig".
*/
export interface IconsConfig {
/**
* Icon pack configuration. Key is the local pack name.
* Value is a package spec with version that complies with Iconify standards.
* Package specs must include at least a major version (e.g., '@iconify-json/logos@1').
*
*/
packs?: {
[k: string]: string;
};
/**
* URL template for resolving package specs (must contain ${packageSpec}).
* Used to build URLs for package specs in icons.packs.
*
*/
cdnTemplate?: string;
/**
* Maximum file size in MB for icon pack JSON files.
*
*/
maxFileSizeMB?: number;
/**
* Network timeout in milliseconds for icon pack fetches.
*
*/
timeout?: number;
/**
* List of allowed hosts to fetch icons from
*
*/
allowedHosts?: string[];
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "FontConfig".

View File

@@ -17,6 +17,7 @@ import type {
ClassRelation,
ClassNode,
ClassNote,
ClassNoteMap,
ClassMap,
NamespaceMap,
NamespaceNode,
@@ -33,15 +34,16 @@ const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
export class ClassDB implements DiagramDB {
private relations: ClassRelation[] = [];
private classes = new Map<string, ClassNode>();
private classes: ClassMap = new Map<string, ClassNode>();
private readonly styleClasses = new Map<string, StyleClass>();
private notes: ClassNote[] = [];
private notes: ClassNoteMap = new Map<string, ClassNote>();
private interfaces: Interface[] = [];
// private static classCounter = 0;
private namespaces = new Map<string, NamespaceNode>();
private namespaceCounter = 0;
private functions: any[] = [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
private functions: Function[] = [];
constructor() {
this.functions.push(this.setupToolTips.bind(this));
@@ -124,7 +126,7 @@ export class ClassDB implements DiagramDB {
annotations: [],
styles: [],
domId: MERMAID_DOM_ID_PREFIX + name + '-' + classCounter,
} as ClassNode);
});
classCounter++;
}
@@ -155,12 +157,12 @@ export class ClassDB implements DiagramDB {
public clear() {
this.relations = [];
this.classes = new Map();
this.notes = [];
this.classes = new Map<string, ClassNode>();
this.notes = new Map<string, ClassNote>();
this.interfaces = [];
this.functions = [];
this.functions.push(this.setupToolTips.bind(this));
this.namespaces = new Map();
this.namespaces = new Map<string, NamespaceNode>();
this.namespaceCounter = 0;
this.direction = 'TB';
commonClear();
@@ -178,7 +180,12 @@ export class ClassDB implements DiagramDB {
return this.relations;
}
public getNotes() {
public getNote(id: string | number): ClassNote {
const key = typeof id === 'number' ? `note${id}` : id;
return this.notes.get(key)!;
}
public getNotes(): ClassNoteMap {
return this.notes;
}
@@ -279,16 +286,19 @@ export class ClassDB implements DiagramDB {
}
}
public addNote(text: string, className: string) {
public addNote(text: string, className: string): string {
const index = this.notes.size;
const note = {
id: `note${this.notes.length}`,
id: `note${index}`,
class: className,
text: text,
index: index,
};
this.notes.push(note);
this.notes.set(note.id, note);
return note.id;
}
public cleanupLabel(label: string) {
public cleanupLabel(label: string): string {
if (label.startsWith(':')) {
label = label.substring(1);
}
@@ -354,7 +364,7 @@ export class ClassDB implements DiagramDB {
});
}
public getTooltip(id: string, namespace?: string) {
public getTooltip(id: string, namespace?: string): string | undefined {
if (namespace && this.namespaces.has(namespace)) {
return this.namespaces.get(namespace)!.classes.get(id)!.tooltip;
}
@@ -534,10 +544,11 @@ export class ClassDB implements DiagramDB {
this.namespaces.set(id, {
id: id,
classes: new Map(),
children: {},
classes: new Map<string, ClassNode>(),
notes: new Map<string, ClassNote>(),
children: new Map<string, NamespaceNode>(),
domId: MERMAID_DOM_ID_PREFIX + id + '-' + this.namespaceCounter,
} as NamespaceNode);
});
this.namespaceCounter++;
}
@@ -555,16 +566,23 @@ export class ClassDB implements DiagramDB {
*
* @param id - ID of the namespace to add
* @param classNames - IDs of the class to add
* @param noteNames - IDs of the notes to add
* @public
*/
public addClassesToNamespace(id: string, classNames: string[]) {
public addClassesToNamespace(id: string, classNames: string[], noteNames: string[]) {
if (!this.namespaces.has(id)) {
return;
}
for (const name of classNames) {
const { className } = this.splitClassNameAndType(name);
this.classes.get(className)!.parent = id;
this.namespaces.get(id)!.classes.set(className, this.classes.get(className)!);
const classNode = this.getClass(className);
classNode.parent = id;
this.namespaces.get(id)!.classes.set(className, classNode);
}
for (const noteName of noteNames) {
const noteNode = this.getNote(noteName);
noteNode.parent = id;
this.namespaces.get(id)!.notes.set(noteName, noteNode);
}
}
@@ -617,36 +635,32 @@ export class ClassDB implements DiagramDB {
const edges: Edge[] = [];
const config = getConfig();
for (const namespaceKey of this.namespaces.keys()) {
const namespace = this.namespaces.get(namespaceKey);
if (namespace) {
const node: Node = {
id: namespace.id,
label: namespace.id,
isGroup: true,
padding: config.class!.padding ?? 16,
// parent node must be one of [rect, roundedWithTitle, noteGroup, divider]
shape: 'rect',
cssStyles: ['fill: none', 'stroke: black'],
look: config.look,
};
nodes.push(node);
}
for (const namespace of this.namespaces.values()) {
const node: Node = {
id: namespace.id,
label: namespace.id,
isGroup: true,
padding: config.class!.padding ?? 16,
// parent node must be one of [rect, roundedWithTitle, noteGroup, divider]
shape: 'rect',
cssStyles: [],
look: config.look,
};
nodes.push(node);
}
for (const classKey of this.classes.keys()) {
const classNode = this.classes.get(classKey);
if (classNode) {
const node = classNode as unknown as Node;
node.parentId = classNode.parent;
node.look = config.look;
nodes.push(node);
}
for (const classNode of this.classes.values()) {
const node: Node = {
...classNode,
type: undefined,
isGroup: false,
parentId: classNode.parent,
look: config.look,
};
nodes.push(node);
}
let cnt = 0;
for (const note of this.notes) {
cnt++;
for (const note of this.notes.values()) {
const noteNode: Node = {
id: note.id,
label: note.text,
@@ -660,14 +674,15 @@ export class ClassDB implements DiagramDB {
`stroke: ${config.themeVariables.noteBorderColor}`,
],
look: config.look,
parentId: note.parent,
};
nodes.push(noteNode);
const noteClassId = this.classes.get(note.class)?.id ?? '';
const noteClassId = this.classes.get(note.class)?.id;
if (noteClassId) {
const edge: Edge = {
id: `edgeNote${cnt}`,
id: `edgeNote${note.index}`,
start: note.id,
end: noteClassId,
type: 'normal',
@@ -697,7 +712,7 @@ export class ClassDB implements DiagramDB {
nodes.push(interfaceNode);
}
cnt = 0;
let cnt = 0;
for (const classRelation of this.relations) {
cnt++;
const edge: Edge = {

View File

@@ -417,7 +417,7 @@ class C13["With Città foreign language"]
note "This is a keyword: ${keyword}. It truly is."
`;
parser.parse(str);
expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
expect(classDb.getNote(0).text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
});
it.each(keywords)(
@@ -427,7 +427,7 @@ class C13["With Città foreign language"]
note "${keyword}"`;
parser.parse(str);
expect(classDb.getNotes()[0].text).toEqual(`${keyword}`);
expect(classDb.getNote(0).text).toEqual(`${keyword}`);
}
);
@@ -441,7 +441,7 @@ class C13["With Città foreign language"]
`;
parser.parse(str);
expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
expect(classDb.getNote(0).text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
});
it.each(keywords)(
@@ -456,7 +456,7 @@ class C13["With Città foreign language"]
`;
parser.parse(str);
expect(classDb.getNotes()[0].text).toEqual(`${keyword}`);
expect(classDb.getNote(0).text).toEqual(`${keyword}`);
}
);

View File

@@ -8,7 +8,7 @@ import utils, { getEdgeId } from '../../utils.js';
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import common from '../common/common.js';
import type { ClassRelation, ClassNote, ClassMap, NamespaceMap } from './classTypes.js';
import type { ClassRelation, ClassMap, ClassNoteMap, NamespaceMap } from './classTypes.js';
import type { EdgeData } from '../../types.js';
const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
@@ -65,6 +65,9 @@ export const addNamespaces = function (
g.setNode(vertex.id, node);
addClasses(vertex.classes, g, _id, diagObj, vertex.id);
const classes: ClassMap = diagObj.db.getClasses();
const relations: ClassRelation[] = diagObj.db.getRelations();
addNotes(vertex.notes, g, relations.length + 1, classes, vertex.id);
log.info('setNode', node);
});
@@ -144,69 +147,74 @@ export const addClasses = function (
* @param classes - Classes
*/
export const addNotes = function (
notes: ClassNote[],
notes: ClassNoteMap,
g: graphlib.Graph,
startEdgeId: number,
classes: ClassMap
classes: ClassMap,
parent?: string
) {
log.info(notes);
notes.forEach(function (note, i) {
const vertex = note;
[...notes.values()]
.filter((note) => note.parent === parent)
.forEach(function (vertex) {
const cssNoteStr = '';
const cssNoteStr = '';
const styles = { labelStyle: '', style: '' };
const styles = { labelStyle: '', style: '' };
const vertexText = vertex.text;
const vertexText = vertex.text;
const radius = 0;
const shape = 'note';
const node = {
labelStyle: styles.labelStyle,
shape: shape,
labelText: sanitizeText(vertexText),
noteData: vertex,
rx: radius,
ry: radius,
class: cssNoteStr,
style: styles.style,
id: vertex.id,
domId: vertex.id,
tooltip: '',
type: 'note',
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
};
g.setNode(vertex.id, node);
log.info('setNode', node);
const radius = 0;
const shape = 'note';
const node = {
labelStyle: styles.labelStyle,
shape: shape,
labelText: sanitizeText(vertexText),
noteData: vertex,
rx: radius,
ry: radius,
class: cssNoteStr,
style: styles.style,
id: vertex.id,
domId: vertex.id,
tooltip: '',
type: 'note',
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
};
g.setNode(vertex.id, node);
log.info('setNode', node);
if (parent) {
g.setParent(vertex.id, parent);
}
if (!vertex.class || !classes.has(vertex.class)) {
return;
}
const edgeId = startEdgeId + i;
if (!vertex.class || !classes.has(vertex.class)) {
return;
}
const edgeId = startEdgeId + vertex.index;
const edgeData: EdgeData = {
id: `edgeNote${edgeId}`,
//Set relationship style and line type
classes: 'relation',
pattern: 'dotted',
// Set link type for rendering
arrowhead: 'none',
//Set edge extra labels
startLabelRight: '',
endLabelLeft: '',
//Set relation arrow types
arrowTypeStart: 'none',
arrowTypeEnd: 'none',
style: 'fill:none',
labelStyle: '',
curve: interpolateToCurve(conf.curve, curveLinear),
};
const edgeData: EdgeData = {
id: `edgeNote${edgeId}`,
//Set relationship style and line type
classes: 'relation',
pattern: 'dotted',
// Set link type for rendering
arrowhead: 'none',
//Set edge extra labels
startLabelRight: '',
endLabelLeft: '',
//Set relation arrow types
arrowTypeStart: 'none',
arrowTypeEnd: 'none',
style: 'fill:none',
labelStyle: '',
curve: interpolateToCurve(conf.curve, curveLinear),
};
// Add the edge to the graph
g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
});
// Add the edge to the graph
g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
});
};
/**
@@ -329,7 +337,7 @@ export const draw = async function (text: string, id: string, _version: string,
const namespaces: NamespaceMap = diagObj.db.getNamespaces();
const classes: ClassMap = diagObj.db.getClasses();
const relations: ClassRelation[] = diagObj.db.getRelations();
const notes: ClassNote[] = diagObj.db.getNotes();
const notes: ClassNoteMap = diagObj.db.getNotes();
log.info(relations);
addNamespaces(namespaces, g, id, diagObj);
addClasses(classes, g, id, diagObj);

View File

@@ -206,7 +206,7 @@ export const draw = function (text, id, _version, diagObj) {
);
});
const notes = diagObj.db.getNotes();
const notes = diagObj.db.getNotes().values();
notes.forEach(function (note) {
log.debug(`Adding note: ${JSON.stringify(note)}`);
const node = svgDraw.drawNote(diagram, note, conf, diagObj);

View File

@@ -5,7 +5,7 @@ export interface ClassNode {
id: string;
type: string;
label: string;
shape: string;
shape: 'classBox';
text: string;
cssClasses: string;
methods: ClassMember[];
@@ -149,6 +149,8 @@ export interface ClassNote {
id: string;
class: string;
text: string;
index: number;
parent?: string;
}
export interface ClassRelation {
@@ -177,6 +179,7 @@ export interface NamespaceNode {
id: string;
domId: string;
classes: ClassMap;
notes: ClassNoteMap;
children: NamespaceMap;
}
@@ -187,4 +190,5 @@ export interface StyleClass {
}
export type ClassMap = Map<string, ClassNode>;
export type ClassNoteMap = Map<string, ClassNote>;
export type NamespaceMap = Map<string, NamespaceNode>;

View File

@@ -275,8 +275,8 @@ statement
;
namespaceStatement
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $3); }
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $4); }
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $3[0], $3[1]); }
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $4[0], $4[1]); }
;
namespaceIdentifier
@@ -284,9 +284,12 @@ namespaceIdentifier
;
classStatements
: classStatement {$$=[$1]}
| classStatement NEWLINE {$$=[$1]}
| classStatement NEWLINE classStatements {$3.unshift($1); $$=$3}
: classStatement {$$=[[$1], []]}
| classStatement NEWLINE {$$=[[$1], []]}
| classStatement NEWLINE classStatements {$3[0].unshift($1); $$=$3}
| noteStatement {$$=[[], [$1]]}
| noteStatement NEWLINE {$$=[[], [$1]]}
| noteStatement NEWLINE classStatements {$3[1].unshift($1); $$=$3}
;
classStatement
@@ -333,8 +336,8 @@ relationStatement
;
noteStatement
: NOTE_FOR className noteText { yy.addNote($3, $2); }
| NOTE noteText { yy.addNote($2); }
: NOTE_FOR className noteText { $$ = yy.addNote($3, $2); }
| NOTE noteText { $$ = yy.addNote($2); }
;
classDefStatement

View File

@@ -13,6 +13,30 @@ const getStyles = (options) =>
}
.cluster-label text {
fill: ${options.titleColor};
}
.cluster-label span {
color: ${options.titleColor};
}
.cluster-label span p {
background-color: transparent;
}
.cluster rect {
fill: ${options.clusterBkg};
stroke: ${options.clusterBorder};
stroke-width: 1px;
}
.cluster text {
fill: ${options.titleColor};
}
.cluster span {
color: ${options.titleColor};
}
.nodeLabel, .edgeLabel {
color: ${options.classText};
}

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

@@ -140,6 +140,7 @@ that id.
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
.*direction\s+TD[^\n]* return 'direction_td';
[^\s\"]+\@(?=[^\{\"]) { return 'LINK_ID'; }
[0-9]+ return 'NUM';
@@ -626,6 +627,8 @@ direction
{ $$={stmt:'dir', value:'RL'};}
| direction_lr
{ $$={stmt:'dir', value:'LR'};}
| direction_td
{ $$={stmt:'dir', value:'TD'};}
;
%%

View File

@@ -309,4 +309,21 @@ describe('when parsing subgraphs', function () {
expect(subgraphA.nodes).toContain('a');
expect(subgraphA.nodes).not.toContain('c');
});
it('should correctly parse direction TD inside a subgraph', function () {
const res = flow.parser.parse(`
graph LR
subgraph WithTD
direction TD
A1 --> A2
end
`);
const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0];
expect(subgraph.dir).toBe('TD');
expect(subgraph.nodes).toContain('A1');
expect(subgraph.nodes).toContain('A2');
});
});

View File

@@ -16,7 +16,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
const svgWidth = bitWidth * bitsPerRow + 2;
const svg: SVG = selectSvgElement(id);
svg.attr('viewbox', `0 0 ${svgWidth} ${svgHeight}`);
svg.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth);
for (const [word, packet] of words.entries()) {

View File

@@ -2,6 +2,7 @@ import type { Diagram } from '../../Diagram.js';
import type { RadarDiagramConfig } from '../../config.type.js';
import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { RadarDB, RadarAxis, RadarCurve } from './types.js';
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
@@ -53,11 +54,9 @@ const drawFrame = (svg: SVG, config: Required<RadarDiagramConfig>): SVGGroup =>
x: config.marginLeft + config.width / 2,
y: config.marginTop + config.height / 2,
};
// Initialize the SVG
svg
.attr('viewbox', `0 0 ${totalWidth} ${totalHeight}`)
.attr('width', totalWidth)
.attr('height', totalHeight);
configureSvgSize(svg, totalHeight, totalWidth, config.useMaxWidth ?? true);
svg.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
// g element to center the radar chart
return svg.append('g').attr('transform', `translate(${center.x}, ${center.y})`);
};

View File

@@ -32,13 +32,14 @@
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
<ID>[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ID>[^<>:\n,;@\s]+(?=\s+as\s) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ID>[^<>:\n,;@]+(?=\s*[\n;#]|$) { yytext = yytext.trim(); this.popState(); return 'ACTOR'; }
<ID>[^<>:\n,;@]*\<[^\n]* { this.popState(); return 'INVALID'; }
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
"create" return 'create';
"destroy" { this.begin('ID'); return 'destroy'; }
<ID>[^<\->\->:\n,;]+?([\-]*[^<\->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
"loop" { this.begin('LINE'); return 'loop'; }
@@ -78,7 +79,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 +90,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';
@@ -119,6 +146,7 @@ line
: SPACE statement { $$ = $2 }
| statement { $$ = $1 }
| NEWLINE { $$=[]; }
| INVALID { $$=[]; }
;
box_section
@@ -304,6 +332,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 +379,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; }
@@ -350,4 +413,4 @@ text2
: TXT {$$ = yy.parseMessage($1.trim().substring(1)) }
;
%%
%%

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(`
@@ -2325,5 +2609,17 @@ Bob->>Alice:Got it!
expect(actors.get('E').type).toBe('entity');
expect(actors.get('E').description).toBe('E');
});
it('should handle fail parsing when alias token causes conflicts in participant definition', async () => {
let error = false;
try {
await Diagram.fromText(`
sequenceDiagram
participant SAS MyServiceWithMoreThan20Chars <br> service decription
`);
} catch (e) {
error = true;
}
expect(error).toBe(true);
});
});
});

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

@@ -1,12 +1,147 @@
# Registering icon pack in mermaid
# Icon Pack Configuration
The icon packs available can be found at [icones.js.org](https://icones.js.org/).
We use the name defined when registering the icon pack, to override the prefix field of the iconify pack. This allows the user to use shorter names for the icons. It also allows us to load a particular pack only when it is used in a diagram.
Mermaid supports icons through Iconify-compatible icon packs. You can register icon packs either **declaratively via configuration** (recommended for most use cases) or **programmatically via JavaScript API** (for advanced/offline scenarios).
Using JSON file directly from CDN:
## Declarative Configuration (v<MERMAID_RELEASE_VERSION>+) (Recommended)
The easiest way to use icons in Mermaid is through declarative configuration. This works in browsers, CLI, Live Editor, and headless renders without requiring custom JavaScript.
### Basic Usage
Configure icon packs in your Mermaid config:
```yaml
---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:docker', label: 'Docker' }
B --> C[End]
```
Or in JavaScript:
```js
mermaid.initialize({
icons: {
packs: {
logos: '@iconify-json/logos@1',
'simple-icons': '@iconify-json/simple-icons@1',
},
},
});
```
### Package Spec Format
Icon packs are specified using **package specs** with version pinning:
- Format: `@scope/package@<version>` or `package@<version>`
- **Must include at least a major version** (e.g., `@iconify-json/logos@1`)
- Minor and patch versions are optional (e.g., `@iconify-json/logos@1.2.3`)
**Important**: Only package specs are supported. Direct URLs are not allowed for security reasons.
### Configuration Options
```yaml
icons:
packs:
# Icon pack configuration
# Key: local name to use in diagrams
# Value: package spec with version
logos: '@iconify-json/logos@1'
'simple-icons': '@iconify-json/simple-icons@1'
# CDN template for resolving package specs
# Must contain ${packageSpec} placeholder
cdnTemplate: 'https://cdn.jsdelivr.net/npm/${packageSpec}/icons.json'
# Maximum file size in MB for icon pack JSON files
# Range: 1-10 MB, default: 5 MB
maxFileSizeMB: 5
# Network timeout in milliseconds for icon pack fetches
# Range: 1000-30000 ms, default: 5000 ms
timeout: 5000
# List of allowed hosts to fetch icons from
allowedHosts:
- 'unpkg.com'
- 'cdn.jsdelivr.net'
- 'npmjs.com'
```
### Security Features
- **Host allowlisting**: Only fetch from hosts in `allowedHosts`
- **Size limits**: Maximum file size enforced via `maxFileSizeMB`
- **Timeouts**: Network requests timeout after specified milliseconds
- **HTTPS only**: All remote fetches occur over HTTPS
- **Version pinning**: Package specs must include version for reproducibility
### Examples
#### Using Custom CDN Template
```yaml
---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
cdnTemplate: "https://unpkg.com/${packageSpec}/icons.json"
---
flowchart TB
A[Start] --> B@{ icon: 'logos:aws', label: 'AWS' }
```
#### Multiple Icon Packs
```yaml
---
config:
icons:
packs:
logos: "@iconify-json/logos@1"
"simple-icons": "@iconify-json/simple-icons@1"
---
flowchart TB
A@{ icon: 'logos:docker', label: 'Docker' } --> B@{ icon: 'simple-icons:github', label: 'GitHub' }
```
#### CLI Usage
Create a `mermaid.json` config file:
```json
{
"icons": {
"packs": {
"logos": "@iconify-json/logos@1"
}
}
}
```
Then use it with the CLI:
```bash
mmdc -i diagram.mmd -o diagram.svg --configFile mermaid.json
```
## Programmatic API (v11.1.0+) (Advanced)
For advanced scenarios or offline use, you can register icon packs programmatically:
### Using JSON File from CDN
```js
import mermaid from 'CDN/mermaid.esm.mjs';
mermaid.registerIconPacks([
{
name: 'logos',
@@ -16,13 +151,15 @@ mermaid.registerIconPacks([
]);
```
Using packages and a bundler:
### Using Packages with Bundler
Install the icon pack:
```bash
npm install @iconify-json/logos@1
```
With lazy loading
#### With Lazy Loading
```js
import mermaid from 'mermaid';
@@ -35,15 +172,39 @@ mermaid.registerIconPacks([
]);
```
Without lazy loading
#### Without Lazy Loading
```js
import mermaid from 'mermaid';
import { icons } from '@iconify-json/logos';
mermaid.registerIconPacks([
{
name: icons.prefix, // To use the prefix defined in the icon pack
name: icons.prefix, // Use the prefix defined in the icon pack
icons,
},
]);
```
## Finding Icon Packs
Icon packs available for use can be found at [iconify.design](https://icon-sets.iconify.design/) or [icones.js.org](https://icones.js.org/).
The pack name you register is the **local name** used in diagrams. It can differ from the pack's prefix, allowing you to:
- Use shorter names (e.g., register `@iconify-json/material-design-icons` as `mdi`)
- Load specific packs only when used in diagrams (lazy loading)
## Error Handling
If an icon cannot be found:
- The diagram still renders (non-fatal)
- A fallback icon is displayed
- A warning is logged (visible in CLI stderr or browser console)
## Licensing
Iconify JSON format is MIT licensed, but **individual icons may have varying licenses**. Please verify the licenses of the icon packs you configure before use in production.
Mermaid does **not** redistribute third-party icon assets in the core bundle.

View File

@@ -17,7 +17,7 @@
},
"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:^",
@@ -25,17 +25,17 @@
},
"devDependencies": {
"@iconify-json/carbon": "^1.2.13",
"@unocss/reset": "^66.0.0",
"@unocss/reset": "^66.5.1",
"@vite-pwa/vitepress": "^1.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

@@ -15,7 +15,7 @@ title: Animal example
classDiagram
note "From Duck till Zebra"
Animal <|-- Duck
note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging"
note for Duck "can fly<br>can swim<br>can dive<br>can help in debugging"
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age

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

@@ -0,0 +1,560 @@
import type { IconifyJSON } from '@iconify/types';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { MermaidConfig } from '../config.type.js';
import {
clearIconPacks,
getIconSVG,
isIconAvailable,
registerDiagramIconPacks,
registerIconPacks,
validatePackageVersion,
} from './icons.js';
// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe('Icons Loading', () => {
// Mock objects for reuse
const mockIcons: IconifyJSON = {
prefix: 'test',
icons: {
'test-icon': {
body: '<path d="M0 0h24v24H0z"/>',
width: 24,
height: 24,
},
},
};
const mockIconsWithMultipleIcons: IconifyJSON = {
prefix: 'test',
icons: {
'test-icon': {
body: '<path d="M0 0h24v24H0z"/>',
width: 24,
height: 24,
},
'another-icon': {
body: '<path d="M12 12h12v12H12z"/>',
width: 24,
height: 24,
},
},
};
const mockFetchResponse = {
ok: true,
headers: {
get: (name: string) => {
if (name === 'content-type') {
return 'application/json';
}
if (name === 'content-length') {
return '1024';
}
return null;
},
},
json: () => Promise.resolve(mockIcons),
};
const mockFetchResponseLarge = {
ok: true,
headers: {
get: (name: string) => {
if (name === 'content-type') {
return 'application/json';
}
if (name === 'content-length') {
return '10485760'; // 10MB
}
return null;
},
},
json: () => Promise.resolve({}),
};
const mockFetchResponseWrongContentType = {
ok: true,
headers: {
get: (name: string) => {
if (name === 'content-type') {
return 'text/html';
}
return null;
},
},
json: () => Promise.resolve({}),
};
const mockFetchResponseInvalidJson = {
ok: true,
headers: {
get: (name: string) => {
if (name === 'content-type') {
return 'application/json';
}
return null;
},
},
json: () => Promise.resolve({}), // Missing prefix and icons
};
const mockFetchResponseHttpError = {
ok: false,
status: 404,
statusText: 'Not Found',
};
const mockGlobalIcons: IconifyJSON = {
prefix: 'global',
icons: {
'global-icon': {
body: '<path d="M0 0h24v24H0z"/>',
width: 24,
height: 24,
},
},
};
const mockEphemeralIcons: IconifyJSON = {
prefix: 'ephemeral',
icons: {
'ephemeral-icon': {
body: '<path d="M0 0h24v24H0z"/>',
width: 24,
height: 24,
},
},
};
beforeEach(() => {
vi.clearAllMocks();
// Clear icon manager state between tests
clearIconPacks();
});
describe('validatePackageVersion', () => {
const validPackages = [
'package@1',
'package@1.2.3',
'@scope/package@1',
'@scope/package@1.2.3',
'package@1.0.0-alpha.1',
'package@1.0.0+build.1',
'@iconify-json/my-icons-package@2.1.0',
'@scope@weird/package@1.0.0', // edge case: multiple @ symbols
];
const invalidPackages = [
'package', // no @
'@scope/package', // scoped without version
'package@', // empty version
'@scope/package@', // scoped empty version
'package@ ', // whitespace version
'', // empty string
'@', // just @
'@scope@weird/package@', // multiple @ with empty version
];
it.each(validPackages)('should accept "%s"', (packageName) => {
expect(() => validatePackageVersion(packageName)).not.toThrow();
});
it.each(invalidPackages)('should reject "%s"', (packageName) => {
expect(() => validatePackageVersion(packageName)).toThrow(
/must include at least a major version/
);
});
});
describe('registerIconPacks', () => {
it('should register sync icon packs', () => {
const iconLoaders = [
{
name: 'test',
icons: mockIcons,
},
];
expect(() => registerIconPacks(iconLoaders)).not.toThrow();
});
it('should register async icon packs', () => {
const iconLoaders = [
{
name: 'test',
loader: () => Promise.resolve(mockIcons),
},
];
expect(() => registerIconPacks(iconLoaders)).not.toThrow();
});
it('should throw error for invalid icon loaders', () => {
expect(() => registerIconPacks([{ name: '', icons: {} as IconifyJSON }])).toThrow(
'Invalid icon loader. Must have a "name" property with non-empty string value.'
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => registerIconPacks([{} as unknown as any])).toThrow(
'Invalid icon loader. Must have a "name" property with non-empty string value.'
);
});
});
describe('isIconAvailable', () => {
it('should return true for available icons', async () => {
registerIconPacks([
{
name: 'test',
icons: mockIcons,
},
]);
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(true);
});
it('should return false for unavailable icons', async () => {
const available = await isIconAvailable('nonexistent:icon');
expect(available).toBe(false);
});
});
describe('getIconSVG', () => {
it('should return SVG for available icons', async () => {
registerIconPacks([
{
name: 'test',
icons: mockIcons,
},
]);
const svg = await getIconSVG('test:test-icon');
expect(svg).toContain('<svg');
expect(svg).toContain('<path d="M0 0h24v24H0z"></path>');
});
it('should return unknown icon SVG for unavailable icons', async () => {
const svg = await getIconSVG('nonexistent:icon');
expect(svg).toContain('<svg');
expect(svg).toContain('?'); // unknown icon contains a question mark
});
it('should apply customisations', async () => {
registerIconPacks([
{
name: 'test',
icons: mockIcons,
},
]);
const svg = await getIconSVG('test:test-icon', { width: 32, height: 32 });
expect(svg).toContain('width="32"');
expect(svg).toContain('height="32"');
});
});
describe('registerDiagramIconPacks', () => {
beforeEach(() => {
// Reset fetch mock
mockFetch.mockClear();
});
it('should register icon packs from config', () => {
const config: MermaidConfig['icons'] = {
packs: {
logos: '@iconify-json/logos@1',
},
cdnTemplate: 'https://cdn.jsdelivr.net/npm/${packageSpec}/icons.json',
maxFileSizeMB: 5,
timeout: 5000,
allowedHosts: ['cdn.jsdelivr.net'],
};
expect(() => registerDiagramIconPacks(config)).not.toThrow();
});
it('should handle empty config', () => {
expect(() => registerDiagramIconPacks({})).not.toThrow();
expect(() => registerDiagramIconPacks(undefined)).not.toThrow();
});
it('should throw error for invalid package specs', () => {
const config: MermaidConfig['icons'] = {
packs: {
invalid: 'invalid-package-spec',
},
};
expect(() => registerDiagramIconPacks(config)).toThrow(
"Package name 'invalid-package-spec' must include at least a major version"
);
});
it('should throw error for direct URLs', () => {
const config: MermaidConfig['icons'] = {
packs: {
direct: 'https://example.com/icons.json',
},
};
expect(() => registerDiagramIconPacks(config)).toThrow(
"Invalid icon pack configuration for 'direct': Direct URLs are not allowed."
);
});
it('should throw error for invalid CDN template', () => {
const config: MermaidConfig['icons'] = {
packs: {
logos: '@iconify-json/logos@1',
},
cdnTemplate: 'https://example.com/package.json', // missing ${packageSpec}
};
expect(() => registerDiagramIconPacks(config)).toThrow(
'CDN template must contain ${packageSpec} placeholder'
);
});
it('should throw error for disallowed hosts', () => {
const config: MermaidConfig['icons'] = {
packs: {
logos: '@iconify-json/logos@1',
},
cdnTemplate: 'https://malicious.com/${packageSpec}/icons.json',
allowedHosts: ['cdn.jsdelivr.net'],
};
expect(() => registerDiagramIconPacks(config)).toThrow(
"Host 'malicious.com' is not in the allowed hosts list"
);
});
});
describe('Network fetching', () => {
it('should handle successful fetch', async () => {
mockFetch.mockResolvedValueOnce(mockFetchResponse);
const config: MermaidConfig['icons'] = {
packs: {
test: '@test/icons@1',
},
};
registerDiagramIconPacks(config);
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(true);
});
it('should handle fetch errors', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
const config: MermaidConfig['icons'] = {
packs: {
test: '@test/icons@1',
},
};
registerDiagramIconPacks(config);
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(false);
});
it('should handle HTTP errors', async () => {
mockFetch.mockResolvedValueOnce(mockFetchResponseHttpError);
const config: MermaidConfig['icons'] = {
packs: {
test: '@test/icons@1',
},
};
registerDiagramIconPacks(config);
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(false);
});
it('should handle invalid JSON', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
headers: {
get: (name: string) => {
if (name === 'content-type') {
return 'application/json';
}
return null;
},
},
json: () => Promise.reject(new SyntaxError('Invalid JSON')),
});
const config: MermaidConfig['icons'] = {
packs: {
test: '@test/icons@1',
},
};
registerDiagramIconPacks(config);
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(false);
});
it('should handle wrong content type', async () => {
mockFetch.mockResolvedValueOnce(mockFetchResponseWrongContentType);
const config: MermaidConfig['icons'] = {
packs: {
test: '@test/icons@1',
},
};
registerDiagramIconPacks(config);
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(false);
});
it('should handle file size limits', async () => {
mockFetch.mockResolvedValueOnce(mockFetchResponseLarge);
const config: MermaidConfig['icons'] = {
packs: {
test: '@test/icons@1',
},
maxFileSizeMB: 5,
};
registerDiagramIconPacks(config);
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(false);
});
it('should handle timeout', async () => {
mockFetch.mockImplementationOnce(
() => new Promise((_, reject) => setTimeout(() => reject(new Error('AbortError')), 100))
);
const config: MermaidConfig['icons'] = {
packs: {
test: '@test/icons@1',
},
timeout: 50,
};
registerDiagramIconPacks(config);
// Wait for async loading
await new Promise((resolve) => setTimeout(resolve, 200));
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(false);
});
it('should validate Iconify format', async () => {
mockFetch.mockResolvedValueOnce(mockFetchResponseInvalidJson);
const config: MermaidConfig['icons'] = {
packs: {
test: '@test/icons@1',
},
};
registerDiagramIconPacks(config);
const available = await isIconAvailable('test:test-icon');
expect(available).toBe(false);
});
});
describe('Configuration defaults', () => {
it('should use default CDN template', () => {
const config: MermaidConfig['icons'] = {
packs: {
logos: '@iconify-json/logos@1',
},
};
expect(() => registerDiagramIconPacks(config)).not.toThrow();
});
it('should use default allowed hosts', () => {
const config: MermaidConfig['icons'] = {
packs: {
logos: '@iconify-json/logos@1',
},
cdnTemplate: 'https://cdn.jsdelivr.net/npm/${packageSpec}/icons.json',
};
expect(() => registerDiagramIconPacks(config)).not.toThrow();
});
it('should use custom CDN template', async () => {
const config: MermaidConfig['icons'] = {
packs: {
logos: '@iconify-json/logos@1',
},
cdnTemplate: 'https://unpkg.com/${packageSpec}/icons.json',
allowedHosts: ['unpkg.com'],
};
mockFetch.mockResolvedValueOnce(mockFetchResponse);
expect(() => registerDiagramIconPacks(config)).not.toThrow();
// Trigger lazy loading by checking for an icon
await isIconAvailable('logos:some-icon');
// Verify that fetch was called with the correct unpkg URL
expect(mockFetch).toHaveBeenCalledWith(
'https://unpkg.com/@iconify-json/logos@1/icons.json',
expect.any(Object)
);
});
});
describe('Ephemeral vs Global icon managers', () => {
it('should prioritize ephemeral icon manager', async () => {
// Register global icons
registerIconPacks([
{
name: 'global',
icons: mockGlobalIcons,
},
]);
// Register ephemeral icons
registerDiagramIconPacks({
packs: {
ephemeral: '@ephemeral/icons@1',
},
});
// Mock fetch for ephemeral icons
mockFetch.mockResolvedValueOnce({
ok: true,
headers: {
get: (name: string) => {
if (name === 'content-type') {
return 'application/json';
}
return null;
},
},
json: () => Promise.resolve(mockEphemeralIcons),
});
// Both should be available
expect(await isIconAvailable('global:global-icon')).toBe(true);
expect(await isIconAvailable('ephemeral:ephemeral-icon')).toBe(true);
});
});
});

View File

@@ -1,7 +1,8 @@
import type { ExtendedIconifyIcon, IconifyIcon, IconifyJSON } from '@iconify/types';
import type { IconifyIconCustomisations } from '@iconify/utils';
import { getIconData, iconToHTML, iconToSVG, replaceIDs, stringToIcon } from '@iconify/utils';
import { getConfig } from '../config.js';
import { defaultConfig, getConfig } from '../config.js';
import type { MermaidConfig } from '../config.type.js';
import { sanitizeText } from '../diagrams/common/common.js';
import { log } from '../logger.js';
@@ -23,66 +24,114 @@ export const unknownIcon: IconifyIcon = {
width: 80,
};
const iconsStore = new Map<string, IconifyJSON>();
const loaderStore = new Map<string, AsyncIconLoader['loader']>();
class IconManager {
private iconsStore = new Map<string, IconifyJSON>();
private loaderStore = new Map<string, AsyncIconLoader['loader']>();
export const registerIconPacks = (iconLoaders: IconLoader[]) => {
for (const iconLoader of iconLoaders) {
if (!iconLoader.name) {
throw new Error(
'Invalid icon loader. Must have a "name" property with non-empty string value.'
);
}
log.debug('Registering icon pack:', iconLoader.name);
if ('loader' in iconLoader) {
loaderStore.set(iconLoader.name, iconLoader.loader);
} else if ('icons' in iconLoader) {
iconsStore.set(iconLoader.name, iconLoader.icons);
} else {
log.error('Invalid icon loader:', iconLoader);
throw new Error('Invalid icon loader. Must have either "icons" or "loader" property.');
registerIconPacks(iconLoaders: IconLoader[]): void {
for (const iconLoader of iconLoaders) {
if (!iconLoader.name) {
throw new Error(
'Invalid icon loader. Must have a "name" property with non-empty string value.'
);
}
log.debug('Registering icon pack:', iconLoader.name);
if ('loader' in iconLoader) {
this.loaderStore.set(iconLoader.name, iconLoader.loader);
} else if ('icons' in iconLoader) {
this.iconsStore.set(iconLoader.name, iconLoader.icons);
} else {
log.error('Invalid icon loader:', iconLoader);
throw new Error('Invalid icon loader. Must have either "icons" or "loader" property.');
}
}
}
};
const getRegisteredIconData = async (iconName: string, fallbackPrefix?: string) => {
const data = stringToIcon(iconName, true, fallbackPrefix !== undefined);
if (!data) {
throw new Error(`Invalid icon name: ${iconName}`);
clear(): void {
this.iconsStore.clear();
this.loaderStore.clear();
}
const prefix = data.prefix || fallbackPrefix;
if (!prefix) {
throw new Error(`Icon name must contain a prefix: ${iconName}`);
}
let icons = iconsStore.get(prefix);
if (!icons) {
const loader = loaderStore.get(prefix);
if (!loader) {
throw new Error(`Icon set not found: ${data.prefix}`);
private async getRegisteredIconData(
iconName: string,
fallbackPrefix?: string
): Promise<ExtendedIconifyIcon> {
const data = stringToIcon(iconName, true, fallbackPrefix !== undefined);
if (!data) {
throw new Error(`Invalid icon name: ${iconName}`);
}
const prefix = data.prefix || fallbackPrefix;
if (!prefix) {
throw new Error(`Icon name must contain a prefix: ${iconName}`);
}
let icons = this.iconsStore.get(prefix);
if (!icons) {
const loader = this.loaderStore.get(prefix);
if (!loader) {
throw new Error(`Icon set not found: ${data.prefix}`);
}
try {
const loaded = await loader();
icons = { ...loaded, prefix };
this.iconsStore.set(prefix, icons);
} catch (e) {
log.error(e);
throw new Error(`Failed to load icon set: ${data.prefix}`);
}
}
const iconData = getIconData(icons, data.name);
if (!iconData) {
throw new Error(`Icon not found: ${iconName}`);
}
return iconData;
}
async isIconAvailable(iconName: string): Promise<boolean> {
try {
const loaded = await loader();
icons = { ...loaded, prefix };
iconsStore.set(prefix, icons);
await this.getRegisteredIconData(iconName);
return true;
} catch {
return false;
}
}
async getIconSVG(
iconName: string,
customisations?: IconifyIconCustomisations & { fallbackPrefix?: string },
extraAttributes?: Record<string, string>
): Promise<string> {
let iconData: ExtendedIconifyIcon;
try {
iconData = await this.getRegisteredIconData(iconName, customisations?.fallbackPrefix);
} catch (e) {
log.error(e);
throw new Error(`Failed to load icon set: ${data.prefix}`);
iconData = unknownIcon;
}
const renderData = iconToSVG(iconData, customisations);
const svg = iconToHTML(replaceIDs(renderData.body), {
...renderData.attributes,
...extraAttributes,
});
return sanitizeText(svg, getConfig());
}
const iconData = getIconData(icons, data.name);
if (!iconData) {
throw new Error(`Icon not found: ${iconName}`);
}
return iconData;
}
const globalIconManager = new IconManager();
const ephemeralIconManager = new IconManager();
export const registerIconPacks = (iconLoaders: IconLoader[]) =>
globalIconManager.registerIconPacks(iconLoaders);
export const clearIconPacks = () => {
globalIconManager.clear();
ephemeralIconManager.clear();
};
export const isIconAvailable = async (iconName: string) => {
try {
await getRegisteredIconData(iconName);
if (await ephemeralIconManager?.isIconAvailable(iconName)) {
return true;
} catch {
return false;
}
return await globalIconManager.isIconAvailable(iconName);
};
export const getIconSVG = async (
@@ -90,17 +139,180 @@ export const getIconSVG = async (
customisations?: IconifyIconCustomisations & { fallbackPrefix?: string },
extraAttributes?: Record<string, string>
) => {
let iconData: ExtendedIconifyIcon;
try {
iconData = await getRegisteredIconData(iconName, customisations?.fallbackPrefix);
} catch (e) {
log.error(e);
iconData = unknownIcon;
if (ephemeralIconManager && (await ephemeralIconManager.isIconAvailable(iconName))) {
return await ephemeralIconManager.getIconSVG(iconName, customisations, extraAttributes);
}
const renderData = iconToSVG(iconData, customisations);
const svg = iconToHTML(replaceIDs(renderData.body), {
...renderData.attributes,
...extraAttributes,
});
return sanitizeText(svg, getConfig());
return await globalIconManager.getIconSVG(iconName, customisations, extraAttributes);
};
/**
* Validates that a package name includes at least a major version specification.
* @param packageName - The package name to validate (e.g., 'package\@1' or '\@scope/package\@1.0.0')
* @throws Error if the package name doesn't include a valid version
*/
export function validatePackageVersion(packageName: string): void {
// Accepts: package@1, @scope/package@1, package@1.2.3, @scope/package@1.2.3
// Rejects: package, @scope/package, package@, @scope/package@
const match = /^(?:@[^/]+\/)?[^@]+@\d/.exec(packageName);
if (!match) {
throw new Error(
`Package name '${packageName}' must include at least a major version (e.g., 'package@1' or '@scope/package@1.0.0')`
);
}
}
/**
* Fetches JSON data from a URL with proper error handling, size limits, and timeout
* @param url - The URL to fetch from
* @param maxFileSizeMB - Maximum file size in MB (default: 5)
* @param timeout - Network timeout in milliseconds (default: 5000)
* @returns Promise that resolves to the parsed JSON data
* @throws Error with descriptive message for various failure cases
*/
async function fetchIconsJson(
url: string,
maxFileSizeMB = 5,
timeout = 5000
): Promise<IconifyJSON> {
const controller = new AbortController();
const timeoutID = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(
`Failed to fetch icons from ${url}: ${response.status} ${response.statusText}`
);
}
const contentType = response.headers.get('content-type');
if (!contentType?.includes('application/json')) {
throw new Error(`Expected JSON response from ${url}, got: ${contentType ?? 'unknown'}`);
}
const contentLength = response.headers.get('content-length');
if (contentLength) {
const sizeMB = parseInt(contentLength, 10) / (1024 * 1024);
if (sizeMB > maxFileSizeMB) {
throw new Error(
`Icon pack size (${sizeMB.toFixed(2)}MB) exceeds limit (${maxFileSizeMB}MB)`
);
}
}
const data = await response.json();
// Validate Iconify format
if (!data.prefix || !data.icons) {
throw new Error(`Invalid Iconify format: missing 'prefix' or 'icons' field`);
}
return data;
} catch (error) {
if (error instanceof TypeError) {
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms while fetching icons from ${url}`);
}
throw new TypeError(`Network error while fetching icons from ${url}: ${error.message}`);
} else if (error instanceof SyntaxError) {
throw new SyntaxError(`Invalid JSON response from ${url}: ${error.message}`);
}
throw error;
} finally {
clearTimeout(timeoutID);
}
}
/**
* Validates that a URL is from an allowed host
* @param url - The URL to validate
* @param allowedHosts - Array of allowed hosts
* @throws Error if the host is not in the allowed list
*/
function validateAllowedHost(url: string, allowedHosts: string[]): void {
try {
const urlObj = new URL(url);
const hostname = urlObj.hostname;
// Check if the hostname or any parent domain is in the allowed list
const isAllowed = allowedHosts.some((allowedHost) => {
return hostname === allowedHost || hostname.endsWith(`.${allowedHost}`);
});
if (!isAllowed) {
throw new Error(
`Host '${hostname}' is not in the allowed hosts list: ${allowedHosts.join(', ')}`
);
}
} catch (error) {
if (error instanceof TypeError) {
throw new Error(`Invalid URL format: ${url}`);
}
throw error;
}
}
/**
* Creates an icon loader based on package spec or URL with security validation
* @param name - The local pack name
* @param packageSpec - Package spec (e.g., '\@iconify-json/logos\@1') or HTTPS URL
* @param config - Icons configuration from MermaidConfig
* @returns IconLoader instance
* @throws Error for invalid configurations or security violations
*/
function getIconLoader(
name: string,
packageSpec: string,
config: MermaidConfig['icons']
): IconLoader {
const isUrl = packageSpec.startsWith('https://');
const allowedHosts = config?.allowedHosts ?? defaultConfig.icons?.allowedHosts ?? [];
const cdnTemplate = config?.cdnTemplate ?? defaultConfig.icons?.cdnTemplate ?? '';
const maxFileSizeMB = config?.maxFileSizeMB ?? defaultConfig.icons?.maxFileSizeMB ?? 0;
const timeout = config?.timeout ?? defaultConfig.icons?.timeout ?? 0;
if (isUrl) {
throw new Error('Direct URLs are not allowed.');
}
// Validate package version for package specs
validatePackageVersion(packageSpec);
// Build URL using CDN template
if (!cdnTemplate.includes('${packageSpec}')) {
throw new Error('CDN template must contain ${packageSpec} placeholder');
}
const url = cdnTemplate.replace('${packageSpec}', packageSpec);
// Validate the generated URL host
validateAllowedHost(url, allowedHosts);
return {
name,
loader: () => fetchIconsJson(url, maxFileSizeMB, timeout),
};
}
export function registerDiagramIconPacks(config: MermaidConfig['icons']): void {
const iconPacks: IconLoader[] = [];
for (const [name, packageSpec] of Object.entries(config?.packs ?? {})) {
try {
const iconPack = getIconLoader(name, packageSpec, config);
iconPacks.push(iconPack);
} catch (error) {
log.error(`Failed to create icon loader for '${name}':`, error);
throw new Error(
`Invalid icon pack configuration for '${name}': ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
ephemeralIconManager.clear();
if (iconPacks.length > 0) {
ephemeralIconManager.registerIconPacks(iconPacks);
}
}

View File

@@ -605,6 +605,14 @@ export const insertEdge = function (
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
let animationClass = '';
if (edge.animate) {
animationClass = 'edge-animation-fast';
}
if (edge.animation) {
animationClass = 'edge-animation-' + edge.animation;
}
let animatedEdge = false;
if (edge.look === 'handDrawn') {
const rc = rough.svg(elem);
@@ -620,7 +628,13 @@ export const insertEdge = function (
svgPath = select(svgPathNode)
.select('path')
.attr('id', edge.id)
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
.attr(
'class',
' ' +
strokeClasses +
(edge.classes ? ' ' + edge.classes : '') +
(animationClass ? ' ' + animationClass : '')
)
.attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
let d = svgPath.attr('d');
svgPath.attr('d', d);
@@ -628,13 +642,6 @@ export const insertEdge = function (
} else {
const stylesFromClasses = edgeClassStyles.join(';');
const styles = edgeStyles ? edgeStyles.reduce((acc, style) => acc + style + ';', '') : '';
let animationClass = '';
if (edge.animate) {
animationClass = ' edge-animation-fast';
}
if (edge.animation) {
animationClass = ' edge-animation-' + edge.animation;
}
const pathStyle =
(stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles) +
@@ -646,7 +653,10 @@ export const insertEdge = function (
.attr('id', edge.id)
.attr(
'class',
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
' ' +
strokeClasses +
(edge.classes ? ' ' + edge.classes : '') +
(animationClass ? ' ' + animationClass : '')
)
.attr('style', pathStyle);

View File

@@ -130,7 +130,6 @@ const lollipop = (elem, type, id) => {
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('circle')
.attr('stroke', 'black')
.attr('fill', 'transparent')
.attr('cx', 7)
.attr('cy', 7)
@@ -147,7 +146,6 @@ const lollipop = (elem, type, id) => {
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('circle')
.attr('stroke', 'black')
.attr('fill', 'transparent')
.attr('cx', 7)
.attr('cy', 7)

View File

@@ -329,6 +329,8 @@ properties:
description: |
Suppresses inserting 'Syntax error' diagram in the DOM.
This is useful when you want to control how to handle syntax errors in your application.
icons:
$ref: '#/$defs/IconsConfig'
$defs: # JSON Schema definition (maybe we should move these to a separate file)
BaseDiagramConfig:
@@ -2368,3 +2370,49 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
description: The font weight to use.
type: ['string', 'number']
default: normal
IconsConfig:
title: Icons Config
description: |
Configuration for icon packs and CDN template.
Enables icons in browsers and CLI/headless renders without custom JavaScript.
type: object
properties:
packs:
description: |
Icon pack configuration. Key is the local pack name.
Value is a package spec with version that complies with Iconify standards.
Package specs must include at least a major version (e.g., '@iconify-json/logos@1').
type: object
additionalProperties:
type: string
cdnTemplate:
description: |
URL template for resolving package specs (must contain ${packageSpec}).
Used to build URLs for package specs in icons.packs.
type: string
pattern: '^https://.*\$\{packageSpec\}.*$'
default: 'https://cdn.jsdelivr.net/npm/${packageSpec}/icons.json'
maxFileSizeMB:
description: |
Maximum file size in MB for icon pack JSON files.
type: integer
default: 5
minimum: 1
maximum: 10
timeout:
description: |
Network timeout in milliseconds for icon pack fetches.
type: integer
default: 5000
minimum: 1000
maximum: 30000
allowedHosts:
description: |
List of allowed hosts to fetch icons from
type: array
items:
type: string
default:
- 'unpkg.com'
- 'cdn.jsdelivr.net'
- 'npmjs.com'

View File

@@ -35,6 +35,11 @@ export const sanitizeDirective = (args: any): void => {
continue;
}
if (key === 'icons') {
// Skip icons key as it is handled by the registerDiagramIconPacks function
continue;
}
// Recurse if an object
if (typeof args[key] === 'object') {
log.debug('sanitizing object', key);

1057
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff