mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-25 05:36:23 +01:00
Compare commits
75 Commits
sidv/iconi
...
feat/gitgr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
048eb4ba91 | ||
|
|
efebe4dd35 | ||
|
|
b1fe4ffe97 | ||
|
|
61c18b99a0 | ||
|
|
04ac594f5b | ||
|
|
a6c2b1d85e | ||
|
|
3c752421a2 | ||
|
|
8add133bbe | ||
|
|
8cf0d3373d | ||
|
|
ff3b194925 | ||
|
|
608d623641 | ||
|
|
ecf9ea1134 | ||
|
|
b33ce14932 | ||
|
|
283aef54e4 | ||
|
|
96f87fd597 | ||
|
|
03e8589818 | ||
|
|
6b9f26dac8 | ||
|
|
ea590cdafe | ||
|
|
f3769c70bc | ||
|
|
4cf4d15197 | ||
|
|
c02cf92656 | ||
|
|
3a1266892d | ||
|
|
67e81de557 | ||
|
|
847b3aa24e | ||
|
|
85a13da40f | ||
|
|
9ec0e8f932 | ||
|
|
9585ee7533 | ||
|
|
1269486124 | ||
|
|
f45ea0c60e | ||
|
|
d20955a56a | ||
|
|
fb66b3fbe3 | ||
|
|
82ea5d63bb | ||
|
|
881e74087a | ||
|
|
09920c0497 | ||
|
|
8065d65cd7 | ||
|
|
6bc6617ca6 | ||
|
|
29ed57ffec | ||
|
|
9fdc4b8005 | ||
|
|
09b841f781 | ||
|
|
d0f9dc0c9b | ||
|
|
15e2824d53 | ||
|
|
7eb582e860 | ||
|
|
6ca928f31f | ||
|
|
983120d945 | ||
|
|
61f74ffc5e | ||
|
|
74318f9337 | ||
|
|
dfd59470dc | ||
|
|
4aac6fa448 | ||
|
|
5f96f80efb | ||
|
|
545801e144 | ||
|
|
0bd74759cc | ||
|
|
e4cf266c1d | ||
|
|
c0e1662e50 | ||
|
|
6546aed482 | ||
|
|
b76ccae065 | ||
|
|
287a9a3fcb | ||
|
|
7f5160fa4d | ||
|
|
7b0763f262 | ||
|
|
38c289818c | ||
|
|
57530076aa | ||
|
|
f2d7877c7a | ||
|
|
62d2c6505e | ||
|
|
974236bbb8 | ||
|
|
cf0d1248a4 | ||
|
|
ebefbd87a8 | ||
|
|
1e7b71a085 | ||
|
|
c7f8a11ded | ||
|
|
b136acdc67 | ||
|
|
bba5e5938e | ||
|
|
835de0012d | ||
|
|
96a766dcdb | ||
|
|
39d7ebd32e | ||
|
|
34f40f0794 | ||
|
|
32ac2c689d | ||
|
|
dbcadc1d0b |
@@ -28,7 +28,6 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
||||
'packet',
|
||||
'architecture',
|
||||
'radar',
|
||||
'icons',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
5
.changeset/brave-baths-behave.md
Normal file
5
.changeset/brave-baths-behave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Prevent HTML tags from being escaped in sandbox label rendering
|
||||
5
.changeset/lazy-brooms-battle.md
Normal file
5
.changeset/lazy-brooms-battle.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
feat: add alias support for new participant syntax of sequence diagrams
|
||||
5
.changeset/ten-plums-bet.md
Normal file
5
.changeset/ten-plums-bet.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Support ComponentQueue_Ext to prevent parsing error
|
||||
5
.changeset/wide-lines-trade.md
Normal file
5
.changeset/wide-lines-trade.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Mindmap rendering issue when the number of Level 2 nodes exceeds 11
|
||||
5
.changeset/yellow-waves-type.md
Normal file
5
.changeset/yellow-waves-type.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add showCommitHashLabel config to hide auto-generated commit hash labels
|
||||
@@ -1,3 +1,5 @@
|
||||
!viewbox
|
||||
# It should be viewBox
|
||||
# This file contains coding related terms
|
||||
ALPHANUM
|
||||
antiscript
|
||||
|
||||
@@ -64,6 +64,7 @@ rscratch
|
||||
shiki
|
||||
Slidev
|
||||
sparkline
|
||||
speccharts
|
||||
sphinxcontrib
|
||||
ssim
|
||||
stylis
|
||||
|
||||
@@ -22,7 +22,6 @@ mermaidchart
|
||||
mermaidjs
|
||||
mindmap
|
||||
mindmaps
|
||||
mmdc
|
||||
mrtree
|
||||
multigraph
|
||||
nodesep
|
||||
|
||||
@@ -71,6 +71,9 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
|
||||
|
||||
const external: string[] = ['require', 'fs', 'path'];
|
||||
const outFileName = getFileName(name, options);
|
||||
const { dependencies, version } = JSON.parse(
|
||||
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
|
||||
);
|
||||
const output: BuildOptions = buildOptions({
|
||||
...rest,
|
||||
absWorkingDir: resolve(__dirname, `../packages/${packageName}`),
|
||||
@@ -82,15 +85,13 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
|
||||
chunkNames: `chunks/${outFileName}/[name]-[hash]`,
|
||||
define: {
|
||||
// This needs to be stringified for esbuild
|
||||
includeLargeFeatures: `${includeLargeFeatures}`,
|
||||
'injected.includeLargeFeatures': `${includeLargeFeatures}`,
|
||||
'injected.version': `'${version}'`,
|
||||
'import.meta.vitest': 'undefined',
|
||||
},
|
||||
});
|
||||
|
||||
if (core) {
|
||||
const { dependencies } = JSON.parse(
|
||||
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
|
||||
);
|
||||
// Core build is used to generate file without bundled dependencies.
|
||||
// This is used by downstream projects to bundle dependencies themselves.
|
||||
// Ignore dependencies and any dependencies of dependencies
|
||||
|
||||
2
.github/workflows/e2e-timings.yml
vendored
2
.github/workflows/e2e-timings.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and create pull request
|
||||
uses: peter-evans/create-pull-request@915d841dae6a4f191bb78faf61a257411d7be4d2
|
||||
uses: peter-evans/create-pull-request@b4733b9419fd47bbfa1807b15627e17cd70b5b22
|
||||
with:
|
||||
add-paths: |
|
||||
cypress/timings.json
|
||||
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -42,5 +42,4 @@ jobs:
|
||||
publish: pnpm changeset:publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
2
.github/workflows/scorecard.yml
vendored
2
.github/workflows/scorecard.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
|
||||
7
.github/workflows/validate-lockfile.yml
vendored
7
.github/workflows/validate-lockfile.yml
vendored
@@ -18,13 +18,6 @@ jobs:
|
||||
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
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Validate pnpm-lock.yaml entries
|
||||
id: validate # give this step an ID so we can reference its outputs
|
||||
run: |
|
||||
|
||||
@@ -78,6 +78,8 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
|
||||
},
|
||||
define: {
|
||||
'import.meta.vitest': 'undefined',
|
||||
'injected.includeLargeFeatures': 'true',
|
||||
'injected.version': `'0.0.0'`,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [],
|
||||
@@ -94,10 +96,6 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
|
||||
}),
|
||||
...visualizerOptions(packageName, core),
|
||||
],
|
||||
define: {
|
||||
// Needs to be string
|
||||
includeLargeFeatures: 'true',
|
||||
},
|
||||
};
|
||||
|
||||
if (watch && config.build) {
|
||||
|
||||
@@ -98,11 +98,21 @@ 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);
|
||||
// Handle sandbox mode where SVG is inside an iframe
|
||||
if (options.securityLevel === 'sandbox') {
|
||||
cy.get('iframe').should('be.visible');
|
||||
if (validation) {
|
||||
cy.get('iframe').should(validation);
|
||||
}
|
||||
} else {
|
||||
cy.get('svg').should('be.visible');
|
||||
// cspell:ignore viewbox
|
||||
cy.get('svg').should('not.have.attr', 'viewbox');
|
||||
|
||||
if (validation) {
|
||||
cy.get('svg').should(validation);
|
||||
}
|
||||
}
|
||||
|
||||
if (screenshot) {
|
||||
|
||||
@@ -114,4 +114,28 @@ describe('C4 diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('C4.6 should render C4Context diagram with ComponentQueue_Ext', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
C4Context
|
||||
title System Context diagram with ComponentQueue_Ext
|
||||
|
||||
Enterprise_Boundary(b0, "BankBoundary0") {
|
||||
Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.")
|
||||
|
||||
System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.")
|
||||
|
||||
Enterprise_Boundary(b1, "BankBoundary") {
|
||||
ComponentQueue_Ext(msgQueue, "Message Queue", "RabbitMQ", "External message queue system for processing banking transactions")
|
||||
System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.")
|
||||
}
|
||||
}
|
||||
|
||||
BiRel(customerA, SystemAA, "Uses")
|
||||
Rel(SystemAA, msgQueue, "Sends messages to")
|
||||
Rel(SystemAA, SystemC, "Sends e-mails", "SMTP")
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -445,7 +445,7 @@ ORDER ||--|{ LINE-ITEM : contains
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
it('should render ER diagram with numeric entity names and attributes', () => {
|
||||
it('should render ER diagram with standalone numeric entities', () => {
|
||||
imgSnapshotTest(
|
||||
`erDiagram
|
||||
PRODUCT ||--o{ ORDER-ITEM : has
|
||||
|
||||
@@ -79,6 +79,18 @@ describe('Flowchart v2', () => {
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('6a: should render complex HTML in labels with sandbox security', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
{ securityLevel: 'sandbox', flowchart: { htmlLabels: true } }
|
||||
);
|
||||
});
|
||||
it('7: should render a flowchart when useMaxWidth is true (default)', () => {
|
||||
renderGraph(
|
||||
`flowchart TD
|
||||
|
||||
@@ -1569,4 +1569,93 @@ gitGraph TB:
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('77: should hide commit hash labels when showCommitHashLabel is false', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'gitGraph': { 'showCommitHashLabel': false } } }%%
|
||||
gitGraph
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
commit
|
||||
merge develop id: "Delta"
|
||||
commit
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('78: should show all commit labels when showCommitHashLabel is true', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'gitGraph': { 'showCommitHashLabel': true } } }%%
|
||||
gitGraph
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
commit
|
||||
merge develop id: "Delta"
|
||||
commit
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('79: should hide commit hash labels with mixed custom and auto-generated IDs', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'gitGraph': { 'showCommitHashLabel': false } } }%%
|
||||
gitGraph
|
||||
commit id: "1-abcdefg"
|
||||
commit
|
||||
branch feature
|
||||
commit id: "Custom Feature"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "2-abcdefg"
|
||||
merge feature id: "Merge Feature"
|
||||
commit
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('80: should hide commit hash labels in vertical orientation (TB)', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'gitGraph': { 'showCommitHashLabel': false } } }%%
|
||||
gitGraph TB:
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
merge develop
|
||||
commit
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('81: should hide commit hash labels in bottom-to-top orientation (BT)', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'gitGraph': { 'showCommitHashLabel': false } } }%%
|
||||
gitGraph BT:
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
merge develop
|
||||
commit
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
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
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -247,5 +247,31 @@ root
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Level 2 nodes exceeding 11', () => {
|
||||
it('should render all Level 2 nodes correctly when there are more than 11', () => {
|
||||
imgSnapshotTest(
|
||||
`mindmap
|
||||
root
|
||||
Node1
|
||||
Node2
|
||||
Node3
|
||||
Node4
|
||||
Node5
|
||||
Node6
|
||||
Node7
|
||||
Node8
|
||||
Node9
|
||||
Node10
|
||||
Node11
|
||||
Node12
|
||||
Node13
|
||||
Node14
|
||||
Node15`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
);
|
||||
});
|
||||
});
|
||||
/* The end */
|
||||
});
|
||||
|
||||
@@ -776,5 +776,194 @@ describe('Sequence Diagram Special Cases', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Participant Stereotypes with Aliases', () => {
|
||||
it('should render participants with stereotypes and aliases', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant API@{ "type" : "boundary" } as Public API
|
||||
participant Auth@{ "type" : "control" } as Auth Controller
|
||||
participant DB@{ "type" : "database" } as User Database
|
||||
participant Cache@{ "type" : "entity" } as Cache Layer
|
||||
API ->> Auth: Authenticate request
|
||||
Auth ->> DB: Query user
|
||||
DB -->> Auth: User data
|
||||
Auth ->> Cache: Store session
|
||||
Cache -->> Auth: Confirmed
|
||||
Auth -->> API: Token`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render actors with stereotypes and aliases', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
actor U@{ "type" : "actor" } as End User
|
||||
actor A@{ "type" : "boundary" } as API Gateway
|
||||
actor S@{ "type" : "control" } as Service Layer
|
||||
actor D@{ "type" : "database" } as Data Store
|
||||
U ->> A: Send request
|
||||
A ->> S: Process
|
||||
S ->> D: Persist
|
||||
D -->> S: Success
|
||||
S -->> A: Response
|
||||
A -->> U: Result`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render mixed participants and actors with stereotypes and aliases', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
actor Client@{ "type" : "actor" } AS Mobile Client
|
||||
participant Gateway@{ "type" : "boundary" } as API Gateway
|
||||
participant OrderSvc@{ "type" : "control" } as Order Service
|
||||
participant Queue@{ "type" : "queue" } as Message Queue
|
||||
participant DB@{ "type" : "database" } as Order Database
|
||||
participant Logs@{ "type" : "collections" } as Audit Logs
|
||||
Client ->> Gateway: Place order
|
||||
Gateway ->> OrderSvc: Validate order
|
||||
OrderSvc ->> Queue: Queue for processing as well
|
||||
OrderSvc ->> DB: Save order
|
||||
OrderSvc ->> Logs: Log transaction
|
||||
Queue -->> OrderSvc: Processing started AS Well
|
||||
DB -->> OrderSvc: Order saved
|
||||
Logs -->> OrderSvc: Logged
|
||||
OrderSvc -->> Gateway: Order confirmed
|
||||
Gateway -->> Client: Confirmation`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render stereotypes with aliases in boxes', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
box rgb(200,220,255) Frontend Layer
|
||||
actor User@{ "type" : "actor" } as End User
|
||||
participant UI@{ "type" : "boundary" } as User Interface
|
||||
end
|
||||
box rgb(255,220,200) Backend Layer
|
||||
participant API@{ "type" : "boundary" } as REST API
|
||||
participant Svc@{ "type" : "control" } as Business Logic
|
||||
end
|
||||
box rgb(220,255,200) Data Layer
|
||||
participant DB@{ "type" : "database" } as Primary DB
|
||||
participant Cache@{ "type" : "entity" } as Cache Store
|
||||
end
|
||||
User ->> UI: Click button
|
||||
UI ->> API: HTTP request
|
||||
API ->> Svc: Process
|
||||
Svc ->> Cache: Check cache
|
||||
Cache -->> Svc: Cache miss
|
||||
Svc ->> DB: Query data
|
||||
DB -->> Svc: Data
|
||||
Svc ->> Cache: Update cache
|
||||
Svc -->> API: Response
|
||||
API -->> UI: Data
|
||||
UI -->> User: Display`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render stereotypes with aliases and complex interactions', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant Web@{ "type" : "boundary" } as Web Portal
|
||||
participant Auth@{ "type" : "control" } as Auth Service
|
||||
participant UserDB@{ "type" : "database" } as User DB
|
||||
participant Queue@{ "type" : "queue" } as Event Queue
|
||||
participant Audit@{ "type" : "collections" } as Audit Trail
|
||||
Web ->> Auth: Login request
|
||||
activate Auth
|
||||
Auth ->> UserDB: Verify credentials
|
||||
activate UserDB
|
||||
UserDB -->> Auth: User found
|
||||
deactivate UserDB
|
||||
alt Valid credentials
|
||||
Auth ->> Queue: Publish login event
|
||||
Auth ->> Audit: Log success
|
||||
par Parallel processing
|
||||
Queue -->> Auth: Event queued
|
||||
and
|
||||
Audit -->> Auth: Logged
|
||||
end
|
||||
Auth -->> Web: Success token
|
||||
else Invalid credentials
|
||||
Auth ->> Audit: Log failure
|
||||
Audit -->> Auth: Logged
|
||||
Auth --x Web: Access denied
|
||||
end
|
||||
deactivate Auth
|
||||
Note over Web,Audit: All interactions logged`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Participant Inline Alias in Config', () => {
|
||||
it('should render participants with inline alias in config object', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant API@{ "type" : "boundary", "alias": "Public API" }
|
||||
participant Auth@{ "type" : "control", "alias": "Auth Service" }
|
||||
participant DB@{ "type" : "database", "alias": "User DB" }
|
||||
API ->> Auth: Login request
|
||||
Auth ->> DB: Query user
|
||||
DB -->> Auth: User data
|
||||
Auth -->> API: Token`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render actors with inline alias in config object', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
actor U@{ "type" : "actor", "alias": "End User" }
|
||||
actor G@{ "type" : "boundary", "alias": "Gateway" }
|
||||
actor S@{ "type" : "control", "alias": "Service" }
|
||||
U ->> G: Request
|
||||
G ->> S: Process
|
||||
S -->> G: Response
|
||||
G -->> U: Result`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle mixed inline and external alias syntax', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant A@{ "type" : "boundary", "alias": "Service A" }
|
||||
participant B@{ "type" : "control" } as Service B
|
||||
participant C@{ "type" : "database" }
|
||||
A ->> B: Request
|
||||
B ->> C: Query
|
||||
C -->> B: Data
|
||||
B -->> A: Response`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should prioritize external alias over inline alias', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant API@{ "type" : "boundary", "alias": "Internal Name" } as External Name
|
||||
participant DB@{ "type" : "database", "alias": "Internal DB" } AS External DB
|
||||
API ->> DB: Query
|
||||
DB -->> API: Result`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render inline alias with only alias field (no type)', () => {
|
||||
imgSnapshotTest(
|
||||
`sequenceDiagram
|
||||
participant API@{ "alias": "Public API" }
|
||||
participant Auth@{ "alias": "Auth Service" }
|
||||
API ->> Auth: Request
|
||||
Auth -->> API: Response`,
|
||||
{ look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,227 +2,227 @@
|
||||
"durations": [
|
||||
{
|
||||
"spec": "cypress/integration/other/configuration.spec.js",
|
||||
"duration": 5841
|
||||
"duration": 6099
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/external-diagrams.spec.js",
|
||||
"duration": 2138
|
||||
"duration": 2236
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/ghsa.spec.js",
|
||||
"duration": 3370
|
||||
"duration": 3405
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/iife.spec.js",
|
||||
"duration": 2052
|
||||
"duration": 2176
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/interaction.spec.js",
|
||||
"duration": 12243
|
||||
"duration": 12300
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/rerender.spec.js",
|
||||
"duration": 2065
|
||||
"duration": 2089
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/xss.spec.js",
|
||||
"duration": 31288
|
||||
"duration": 32033
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/appli.spec.js",
|
||||
"duration": 3421
|
||||
"duration": 3672
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/architecture.spec.ts",
|
||||
"duration": 97
|
||||
"duration": 103
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/block.spec.js",
|
||||
"duration": 18500
|
||||
"duration": 18135
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/c4.spec.js",
|
||||
"duration": 5793
|
||||
"duration": 5661
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
|
||||
"duration": 40966
|
||||
"duration": 41456
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
|
||||
"duration": 39176
|
||||
"duration": 38910
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
|
||||
"duration": 23468
|
||||
"duration": 24120
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
|
||||
"duration": 38291
|
||||
"duration": 38454
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram.spec.js",
|
||||
"duration": 16949
|
||||
"duration": 17099
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
|
||||
"duration": 9480
|
||||
"duration": 9844
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/current.spec.js",
|
||||
"duration": 2753
|
||||
"duration": 2951
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
|
||||
"duration": 88028
|
||||
"duration": 90081
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram.spec.js",
|
||||
"duration": 15615
|
||||
"duration": 19496
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
|
||||
"duration": 3706
|
||||
"duration": 3829
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
|
||||
"duration": 43905
|
||||
"duration": 42517
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
|
||||
"duration": 31217
|
||||
"duration": 31541
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
|
||||
"duration": 7531
|
||||
"duration": 7749
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
|
||||
"duration": 25423
|
||||
"duration": 25230
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
|
||||
"duration": 49664
|
||||
"duration": 49359
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart.spec.js",
|
||||
"duration": 32525
|
||||
"duration": 33028
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gantt.spec.js",
|
||||
"duration": 20915
|
||||
"duration": 22271
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gitGraph.spec.js",
|
||||
"duration": 53556
|
||||
"duration": 51837
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/iconShape.spec.ts",
|
||||
"duration": 283038
|
||||
"duration": 285060
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/imageShape.spec.ts",
|
||||
"duration": 59434
|
||||
"duration": 59517
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/info.spec.ts",
|
||||
"duration": 3101
|
||||
"duration": 3501
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/journey.spec.js",
|
||||
"duration": 7099
|
||||
"duration": 7405
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/kanban.spec.ts",
|
||||
"duration": 7567
|
||||
"duration": 7975
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/katex.spec.js",
|
||||
"duration": 3817
|
||||
"duration": 4312
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
|
||||
"duration": 2624
|
||||
"duration": 2630
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/mindmap-tidy-tree.spec.js",
|
||||
"duration": 4246
|
||||
"duration": 4541
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/mindmap.spec.ts",
|
||||
"duration": 11967
|
||||
"duration": 12134
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/newShapes.spec.ts",
|
||||
"duration": 151914
|
||||
"duration": 151160
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
|
||||
"duration": 116698
|
||||
"duration": 118044
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/packet.spec.ts",
|
||||
"duration": 4967
|
||||
"duration": 5166
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/pie.spec.ts",
|
||||
"duration": 6700
|
||||
"duration": 7074
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
|
||||
"duration": 8963
|
||||
"duration": 9518
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/radar.spec.js",
|
||||
"duration": 5540
|
||||
"duration": 5846
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirement.spec.js",
|
||||
"duration": 2782
|
||||
"duration": 3089
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
|
||||
"duration": 54797
|
||||
"duration": 55361
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sankey.spec.ts",
|
||||
"duration": 6914
|
||||
"duration": 7236
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
|
||||
"duration": 20481
|
||||
"duration": 26057
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
|
||||
"duration": 38490
|
||||
"duration": 48401
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
|
||||
"duration": 30766
|
||||
"duration": 30364
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
|
||||
"duration": 16705
|
||||
"duration": 16862
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/theme.spec.js",
|
||||
"duration": 30928
|
||||
"duration": 30553
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/timeline.spec.ts",
|
||||
"duration": 8424
|
||||
"duration": 8962
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/treemap.spec.ts",
|
||||
"duration": 12533
|
||||
"duration": 12486
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/xyChart.spec.js",
|
||||
"duration": 21197
|
||||
"duration": 21718
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/zenuml.spec.js",
|
||||
"duration": 3455
|
||||
"duration": 3882
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,150 +4,15 @@
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/icons.md](../../packages/mermaid/src/docs/config/icons.md).
|
||||
|
||||
# Icon Pack Configuration
|
||||
# Registering icon pack in mermaid
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
## 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
|
||||
Using JSON file directly from CDN:
|
||||
|
||||
```js
|
||||
import mermaid from 'CDN/mermaid.esm.mjs';
|
||||
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: 'logos',
|
||||
@@ -157,15 +22,13 @@ mermaid.registerIconPacks([
|
||||
]);
|
||||
```
|
||||
|
||||
### Using Packages with Bundler
|
||||
|
||||
Install the icon pack:
|
||||
Using packages and a bundler:
|
||||
|
||||
```bash
|
||||
npm install @iconify-json/logos@1
|
||||
```
|
||||
|
||||
#### With Lazy Loading
|
||||
With lazy loading
|
||||
|
||||
```js
|
||||
import mermaid from 'mermaid';
|
||||
@@ -178,39 +41,15 @@ mermaid.registerIconPacks([
|
||||
]);
|
||||
```
|
||||
|
||||
#### Without Lazy Loading
|
||||
Without lazy loading
|
||||
|
||||
```js
|
||||
import mermaid from 'mermaid';
|
||||
import { icons } from '@iconify-json/logos';
|
||||
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: icons.prefix, // Use the prefix defined in the icon pack
|
||||
name: icons.prefix, // To 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.
|
||||
|
||||
@@ -229,14 +229,6 @@ 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`
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseOptions
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
|
||||
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **suppressErrors**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
|
||||
Defined in: [packages/mermaid/src/types.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L94)
|
||||
|
||||
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
||||
The `parseError` function will not be called.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
||||
Defined in: [packages/mermaid/src/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L97)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mer
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L104)
|
||||
Defined in: [packages/mermaid/src/types.ts:105](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L105)
|
||||
|
||||
The config passed as YAML frontmatter or directives
|
||||
|
||||
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
|
||||
Defined in: [packages/mermaid/src/types.ts:101](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L101)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: RenderResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
||||
Defined in: [packages/mermaid/src/types.ts:115](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L115)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/me
|
||||
|
||||
> `optional` **bindFunctions**: (`element`) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L132)
|
||||
Defined in: [packages/mermaid/src/types.ts:133](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L133)
|
||||
|
||||
Bind function to be called after the svg has been inserted into the DOM.
|
||||
This is necessary for adding event listeners to the elements in the svg.
|
||||
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L122)
|
||||
Defined in: [packages/mermaid/src/types.ts:123](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L123)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
> **svg**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
||||
Defined in: [packages/mermaid/src/types.ts:119](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L119)
|
||||
|
||||
The svg code for the rendered graph.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
> **ParseErrorFunction** = (`err`, `hash?`) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/Diagram.ts:11](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L11)
|
||||
Defined in: [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10)
|
||||
|
||||
## Parameters
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
- [GitHub Writer](https://github.com/ckeditor/github-writer)
|
||||
- [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator)
|
||||
- [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅
|
||||
- [GNU Octave](https://octave.org/) ✅
|
||||
- [octave_mermaid_js](https://github.com/CNOCTAVE/octave_mermaid_js) ✅
|
||||
- [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid)
|
||||
- [MonsterWriter](https://www.monsterwriter.com/) ✅
|
||||
- [Joplin](https://joplinapp.org) ✅
|
||||
@@ -272,6 +274,7 @@ Communication tools and platforms
|
||||
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
|
||||
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
||||
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
||||
- [speccharts: Turn your test suites into specification diagrams](https://github.com/arnaudrenaud/speccharts)
|
||||
- [Vitepress Plugin](https://github.com/sametcn99/vitepress-mermaid-renderer)
|
||||
|
||||
<!--- cspell:ignore Blazorade HueHive --->
|
||||
|
||||
@@ -651,6 +651,8 @@ gitGraph
|
||||
|
||||
Sometimes you may want to hide the commit labels from the diagram. You can do this by using the `showCommitLabel` keyword. By default its value is `true`. You can set it to `false` using directives.
|
||||
|
||||
### Hiding all commit labels
|
||||
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
@@ -759,6 +761,106 @@ config:
|
||||
merge release
|
||||
```
|
||||
|
||||
### Hiding only auto-generated commit hash labels
|
||||
|
||||
In many cases, you may want to show labels only for commits with custom IDs (like "Alpha", "v1.0", "Feature-X") while hiding the auto-generated hash labels (like "0-a1b2c3d", "1-x9y8z7w"). This is useful when you want to emphasize important commits while keeping the diagram clean.
|
||||
|
||||
You can achieve this by using the `showCommitHashLabel` keyword. By default its value is `true`. When set to `false`, only commits with custom IDs will show their labels, while commits with auto-generated hash IDs will be hidden.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- **Auto-generated IDs**: Commits without a custom `id` are automatically assigned IDs in the format `seq-hash` (e.g., `0-a1b2c3d`, `1-x9y8z7w`). These follow the pattern of a number, a dash, and a random hash.
|
||||
- **Custom IDs**: Commits with explicitly defined IDs using `commit id: "YourID"` are considered custom IDs.
|
||||
- When `showCommitHashLabel: false`, only custom IDs are displayed.
|
||||
|
||||
Usage example - Hiding auto-generated hash labels:
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
gitGraph:
|
||||
showCommitHashLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
commit
|
||||
merge develop id: "Delta"
|
||||
commit
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
gitGraph:
|
||||
showCommitHashLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
commit
|
||||
merge develop id: "Delta"
|
||||
commit
|
||||
```
|
||||
|
||||
In this example, only the commits with custom IDs ("Alpha", "Beta", "Gamma", "Delta") will show their labels. The commits without custom IDs (created with just `commit`) will not display their auto-generated hash labels.
|
||||
|
||||
Usage example - Showing all labels (default behavior):
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
gitGraph:
|
||||
showCommitHashLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
commit
|
||||
merge develop id: "Delta"
|
||||
commit
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
gitGraph:
|
||||
showCommitHashLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
commit
|
||||
merge develop id: "Delta"
|
||||
commit
|
||||
```
|
||||
|
||||
In this example, all commits will show their labels, including both custom IDs and auto-generated hash IDs.
|
||||
|
||||
## Customizing main branch name
|
||||
|
||||
Sometimes you may want to customize the name of the main/default branch. You can do this by using the `mainBranchName` keyword. By default its value is `main`. You can set it to any string using directives.
|
||||
|
||||
@@ -196,7 +196,11 @@ sequenceDiagram
|
||||
|
||||
### Aliases
|
||||
|
||||
The actor can have a convenient identifier and a descriptive label.
|
||||
The actor can have a convenient identifier and a descriptive label. Aliases can be defined in two ways: using external syntax with the `as` keyword, or inline within the configuration object.
|
||||
|
||||
#### External Alias Syntax
|
||||
|
||||
You can define an alias using the `as` keyword after the participant declaration:
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
@@ -214,6 +218,78 @@ sequenceDiagram
|
||||
J->>A: Great!
|
||||
```
|
||||
|
||||
The external alias syntax also works with participant stereotype configurations, allowing you to combine type specification with aliases:
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary" } as Public API
|
||||
actor DB@{ "type": "database" } as User Database
|
||||
participant Svc@{ "type": "control" } as Auth Service
|
||||
API->>Svc: Authenticate
|
||||
Svc->>DB: Query user
|
||||
DB-->>Svc: User data
|
||||
Svc-->>API: Token
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary" } as Public API
|
||||
actor DB@{ "type": "database" } as User Database
|
||||
participant Svc@{ "type": "control" } as Auth Service
|
||||
API->>Svc: Authenticate
|
||||
Svc->>DB: Query user
|
||||
DB-->>Svc: User data
|
||||
Svc-->>API: Token
|
||||
```
|
||||
|
||||
#### Inline Alias Syntax
|
||||
|
||||
Alternatively, you can define an alias directly inside the configuration object using the `"alias"` field. This works with both `participant` and `actor` keywords:
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary", "alias": "Public API" }
|
||||
participant Auth@{ "type": "control", "alias": "Auth Service" }
|
||||
participant DB@{ "type": "database", "alias": "User Database" }
|
||||
API->>Auth: Login request
|
||||
Auth->>DB: Query user
|
||||
DB-->>Auth: User data
|
||||
Auth-->>API: Access token
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary", "alias": "Public API" }
|
||||
participant Auth@{ "type": "control", "alias": "Auth Service" }
|
||||
participant DB@{ "type": "database", "alias": "User Database" }
|
||||
API->>Auth: Login request
|
||||
Auth->>DB: Query user
|
||||
DB-->>Auth: User data
|
||||
Auth-->>API: Access token
|
||||
```
|
||||
|
||||
#### Alias Precedence
|
||||
|
||||
When both inline alias (in the configuration object) and external alias (using `as` keyword) are provided, the **external alias takes precedence**:
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary", "alias": "Internal Name" } as External Name
|
||||
participant DB@{ "type": "database", "alias": "Internal DB" } as External DB
|
||||
API->>DB: Query
|
||||
DB-->>API: Result
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary", "alias": "Internal Name" } as External Name
|
||||
participant DB@{ "type": "database", "alias": "Internal DB" } as External DB
|
||||
API->>DB: Query
|
||||
DB-->>API: Result
|
||||
```
|
||||
|
||||
In the example above, "External Name" and "External DB" will be displayed, not "Internal Name" and "Internal DB".
|
||||
|
||||
### Actor Creation and Destruction (v10.3.0+)
|
||||
|
||||
It is possible to create and destroy actors by messages. To do so, add a create or destroy directive before the message.
|
||||
|
||||
34
package.json
34
package.json
@@ -63,21 +63,21 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-cypress": "^3.55.2",
|
||||
"@argos-ci/cypress": "^6.1.3",
|
||||
"@applitools/eyes-cypress": "^3.56.4",
|
||||
"@argos-ci/cypress": "^6.2.2",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"@cspell/eslint-plugin": "^8.19.4",
|
||||
"@cypress/code-coverage": "^3.14.6",
|
||||
"@cspell/eslint-plugin": "^9.3.0",
|
||||
"@cypress/code-coverage": "^3.14.7",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@rollup/plugin-typescript": "^12.1.4",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/express": "^5.0.5",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^22.18.6",
|
||||
"@types/node": "^22.19.1",
|
||||
"@types/rollup-plugin-visualizer": "^5.0.3",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/spy": "^3.2.4",
|
||||
@@ -88,23 +88,23 @@
|
||||
"cors": "^2.8.5",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cspell": "^9.2.1",
|
||||
"cspell": "^9.3.2",
|
||||
"cypress": "^14.5.4",
|
||||
"cypress-image-snapshot": "^4.0.1",
|
||||
"cypress-split": "^1.24.23",
|
||||
"esbuild": "^0.25.10",
|
||||
"cypress-split": "^1.24.25",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-cypress": "^4.3.0",
|
||||
"eslint-plugin-cypress": "^5.2.0",
|
||||
"eslint-plugin-html": "^8.1.3",
|
||||
"eslint-plugin-jest": "^28.14.0",
|
||||
"eslint-plugin-jsdoc": "^50.8.0",
|
||||
"eslint-plugin-jest": "^29.0.1",
|
||||
"eslint-plugin-jsdoc": "^61.1.12",
|
||||
"eslint-plugin-json": "^4.0.1",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"eslint-plugin-markdown": "^5.1.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-unicorn": "^59.0.1",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"express": "^5.1.0",
|
||||
"globals": "^16.4.0",
|
||||
"globby": "^14.1.0",
|
||||
@@ -121,13 +121,13 @@
|
||||
"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.1.2",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"start-server-and-test": "^2.1.3",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.20.5",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "~5.7.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.0.7",
|
||||
"vite": "^7.0.8",
|
||||
"vite-plugin-istanbul": "^7.0.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"elk",
|
||||
"mermaid"
|
||||
],
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
"mermaid",
|
||||
"layout"
|
||||
],
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zenuml/core": "^3.41.4"
|
||||
"@zenuml/core": "^3.41.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
|
||||
@@ -77,9 +77,9 @@
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.22",
|
||||
"dayjs": "^1.11.19",
|
||||
"dompurify": "^3.3.0",
|
||||
"katex": "^0.16.25",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^16.3.0",
|
||||
@@ -89,11 +89,11 @@
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adobe/jsonschema2md": "^8.0.5",
|
||||
"@adobe/jsonschema2md": "^8.0.8",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/cytoscape-fcose": "^2.2.4",
|
||||
"@types/d3-sankey": "^0.12.4",
|
||||
"@types/cytoscape-fcose": "^2.2.5",
|
||||
"@types/d3-sankey": "^0.12.5",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/d3-scale-chromatic": "^3.1.0",
|
||||
"@types/d3-selection": "^3.0.11",
|
||||
@@ -101,7 +101,7 @@
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/micromatch": "^4.0.10",
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -121,9 +121,9 @@
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"start-server-and-test": "^2.1.2",
|
||||
"start-server-and-test": "^2.1.3",
|
||||
"type-fest": "^4.41.0",
|
||||
"typedoc": "^0.28.13",
|
||||
"typedoc": "^0.28.14",
|
||||
"typedoc-plugin-markdown": "^4.8.1",
|
||||
"typescript": "~5.7.3",
|
||||
"unist-util-flatmap": "^1.0.0",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import * as configApi from './config.js';
|
||||
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
|
||||
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
|
||||
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
|
||||
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
|
||||
import { UnknownDiagramError } from './errors.js';
|
||||
import { registerDiagramIconPacks } from './rendering-util/icons.js';
|
||||
import type { DetailedError } from './utils.js';
|
||||
import { encodeEntities } from './utils.js';
|
||||
import type { DetailedError } from './utils.js';
|
||||
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void;
|
||||
@@ -42,7 +41,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -220,7 +220,6 @@ export interface MermaidConfig {
|
||||
*
|
||||
*/
|
||||
suppressErrorRendering?: boolean;
|
||||
icons?: IconsConfig;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for flowcharts
|
||||
@@ -1109,6 +1108,7 @@ export interface GitGraphDiagramConfig extends BaseDiagramConfig {
|
||||
showBranches?: boolean;
|
||||
rotateCommitLabel?: boolean;
|
||||
parallelCommits?: boolean;
|
||||
showCommitHashLabel?: boolean;
|
||||
/**
|
||||
* Controls whether or arrow markers in html code are absolute paths or anchors.
|
||||
* This matters if you are using base tag settings.
|
||||
@@ -1624,46 +1624,6 @@ 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".
|
||||
|
||||
@@ -72,7 +72,7 @@ export const addDiagrams = () => {
|
||||
}
|
||||
);
|
||||
|
||||
if (includeLargeFeatures) {
|
||||
if (injected.includeLargeFeatures) {
|
||||
registerLazyLoadedDiagrams(flowchartElk, mindmap, architecture);
|
||||
}
|
||||
|
||||
|
||||
58
packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js
Normal file
58
packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import c4Db from '../c4Db.js';
|
||||
import c4 from './c4Diagram.jison';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['Component', 'component'],
|
||||
['ComponentDb', 'component_db'],
|
||||
['ComponentQueue', 'component_queue'],
|
||||
['Component_Ext', 'external_component'],
|
||||
['ComponentDb_Ext', 'external_component_db'],
|
||||
['ComponentQueue_Ext', 'external_component_queue'],
|
||||
])('parsing a C4 %s', function (macroName, elementName) {
|
||||
beforeEach(function () {
|
||||
c4.parser.yy = c4Db;
|
||||
c4.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should parse a C4 diagram with one Component correctly', function () {
|
||||
c4.parser.parse(`C4Component
|
||||
title Component diagram for Internet Banking Component
|
||||
${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")`);
|
||||
|
||||
const yy = c4.parser.yy;
|
||||
|
||||
const shapes = yy.getC4ShapeArray();
|
||||
expect(shapes.length).toBe(1);
|
||||
const onlyShape = shapes[0];
|
||||
|
||||
expect(onlyShape).toMatchObject({
|
||||
alias: 'ComponentAA',
|
||||
descr: {
|
||||
text: 'Allows customers to view information about their bank accounts, and make payments.',
|
||||
},
|
||||
label: {
|
||||
text: 'Internet Banking Component',
|
||||
},
|
||||
techn: {
|
||||
text: 'Technology',
|
||||
},
|
||||
typeC4Shape: {
|
||||
text: elementName,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a trailing whitespaces after Component', function () {
|
||||
const whitespace = ' ';
|
||||
const rendered = c4.parser.parse(`C4Component${whitespace}
|
||||
title Component diagram for Internet Banking Component${whitespace}
|
||||
${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")${whitespace}`);
|
||||
|
||||
expect(rendered).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -158,10 +158,10 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
"UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';}
|
||||
"UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';}
|
||||
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config><<EOF>> return "EOF_IN_STRUCT";
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";}
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(] { this.begin("attribute"); }
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config,attribute>[)] { this.popState();this.popState();}
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config><<EOF>> return "EOF_IN_STRUCT";
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";}
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(] { this.begin("attribute"); }
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config,attribute>[)] { this.popState();this.popState();}
|
||||
|
||||
<attribute>",," { return 'ATTRIBUTE_EMPTY';}
|
||||
<attribute>"," { }
|
||||
|
||||
@@ -70,6 +70,31 @@ describe('Sanitize text', () => {
|
||||
});
|
||||
expect(result).not.toContain('javascript:alert(1)');
|
||||
});
|
||||
|
||||
it('should allow HTML tags in sandbox mode', () => {
|
||||
const htmlStr = '<p>This is a <strong>bold</strong> text</p>';
|
||||
const result = sanitizeText(htmlStr, {
|
||||
securityLevel: 'sandbox',
|
||||
flowchart: { htmlLabels: true },
|
||||
});
|
||||
expect(result).toContain('<p>');
|
||||
expect(result).toContain('<strong>');
|
||||
expect(result).toContain('</strong>');
|
||||
expect(result).toContain('</p>');
|
||||
});
|
||||
|
||||
it('should remove script tags in sandbox mode', () => {
|
||||
const maliciousStr = '<p>Hello <script>alert(1)</script> world</p>';
|
||||
const result = sanitizeText(maliciousStr, {
|
||||
securityLevel: 'sandbox',
|
||||
flowchart: { htmlLabels: true },
|
||||
});
|
||||
expect(result).not.toContain('<script>');
|
||||
expect(result).not.toContain('alert(1)');
|
||||
expect(result).toContain('<p>');
|
||||
expect(result).toContain('Hello');
|
||||
expect(result).toContain('world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic parser', () => {
|
||||
|
||||
@@ -66,7 +66,7 @@ export const removeScript = (txt: string): string => {
|
||||
const sanitizeMore = (text: string, config: MermaidConfig) => {
|
||||
if (config.flowchart?.htmlLabels !== false) {
|
||||
const level = config.securityLevel;
|
||||
if (level === 'antiscript' || level === 'strict') {
|
||||
if (level === 'antiscript' || level === 'strict' || level === 'sandbox') {
|
||||
text = removeScript(text);
|
||||
} else if (level !== 'loose') {
|
||||
text = breakToPlaceholder(text);
|
||||
@@ -333,7 +333,7 @@ const renderKatexUnsanitized = async (text: string, config: MermaidConfig): Prom
|
||||
return text.replace(katexRegex, 'MathML is unsupported in this environment.');
|
||||
}
|
||||
|
||||
if (includeLargeFeatures) {
|
||||
if (injected.includeLargeFeatures) {
|
||||
const { default: katex } = await import('katex');
|
||||
const outputMode =
|
||||
config.forceLegacyMathML || (!isMathMLSupported() && config.legacyMathML)
|
||||
|
||||
@@ -289,11 +289,15 @@ const drawCommitLabel = (
|
||||
commitPosition: CommitPositionOffset,
|
||||
pos: number
|
||||
) => {
|
||||
if (
|
||||
const hasCustomId = commit.customId || !/^\d+-[\dA-Za-z]+$/.exec(commit.id);
|
||||
|
||||
const shouldShowLabel =
|
||||
commit.type !== commitType.CHERRY_PICK &&
|
||||
((commit.customId && commit.type === commitType.MERGE) || commit.type !== commitType.MERGE) &&
|
||||
DEFAULT_GITGRAPH_CONFIG?.showCommitLabel
|
||||
) {
|
||||
DEFAULT_GITGRAPH_CONFIG?.showCommitLabel &&
|
||||
(DEFAULT_GITGRAPH_CONFIG?.showCommitHashLabel || hasCustomId);
|
||||
|
||||
if (shouldShowLabel) {
|
||||
const wrapper = gLabels.append('g');
|
||||
const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg');
|
||||
const text = wrapper
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { InfoFields, InfoDB } from './infoTypes.js';
|
||||
import packageJson from '../../../package.json' assert { type: 'json' };
|
||||
|
||||
export const DEFAULT_INFO_DB: InfoFields = {
|
||||
version: packageJson.version + (includeLargeFeatures ? '' : '-tiny'),
|
||||
version: injected.version + (injected.includeLargeFeatures ? '' : '-tiny'),
|
||||
} as const;
|
||||
|
||||
export const getVersion = (): string => DEFAULT_INFO_DB.version;
|
||||
|
||||
@@ -293,5 +293,37 @@ describe('MindmapDb getData function', () => {
|
||||
expect(edgeA1_aaa.section).toBe(1);
|
||||
expect(edgeA_a2.section).toBe(2);
|
||||
});
|
||||
|
||||
it('should wrap section numbers when there are more than 11 level 2 nodes', () => {
|
||||
db.addNode(0, 'root', 'Example', 0);
|
||||
|
||||
for (let i = 1; i <= 15; i++) {
|
||||
db.addNode(1, `child${i}`, `${i}`, 0);
|
||||
}
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toHaveLength(16);
|
||||
|
||||
const child1 = result.nodes.find((n) => n.label === '1') as MindmapLayoutNode;
|
||||
const child11 = result.nodes.find((n) => n.label === '11') as MindmapLayoutNode;
|
||||
const child12 = result.nodes.find((n) => n.label === '12') as MindmapLayoutNode;
|
||||
const child13 = result.nodes.find((n) => n.label === '13') as MindmapLayoutNode;
|
||||
const child14 = result.nodes.find((n) => n.label === '14') as MindmapLayoutNode;
|
||||
const child15 = result.nodes.find((n) => n.label === '15') as MindmapLayoutNode;
|
||||
|
||||
expect(child1.section).toBe(0);
|
||||
expect(child11.section).toBe(10);
|
||||
|
||||
expect(child12.section).toBe(0);
|
||||
expect(child13.section).toBe(1);
|
||||
expect(child14.section).toBe(2);
|
||||
expect(child15.section).toBe(3);
|
||||
|
||||
expect(child12.cssClasses).toBe('mindmap-node section-0');
|
||||
expect(child13.cssClasses).toBe('mindmap-node section-1');
|
||||
expect(child14.cssClasses).toBe('mindmap-node section-2');
|
||||
expect(child15.cssClasses).toBe('mindmap-node section-3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { MindmapNode } from './mindmapTypes.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
|
||||
import { getUserDefinedConfig } from '../../config.js';
|
||||
import { MAX_SECTIONS } from './svgDraw.js';
|
||||
|
||||
// Extend Node type for mindmap-specific properties
|
||||
export type MindmapLayoutNode = Node & {
|
||||
@@ -203,7 +204,7 @@ export class MindmapDB {
|
||||
// For other nodes, inherit parent's section number
|
||||
if (node.children) {
|
||||
for (const [index, child] of node.children.entries()) {
|
||||
const childSectionNumber = node.level === 0 ? index : sectionNumber;
|
||||
const childSectionNumber = node.level === 0 ? index % (MAX_SECTIONS - 1) : sectionNumber;
|
||||
this.assignSections(child, childSectionNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { parseFontSize } from '../../utils.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import type { MindmapDB } from './mindmapDb.js';
|
||||
|
||||
const MAX_SECTIONS = 12;
|
||||
export const MAX_SECTIONS = 12;
|
||||
|
||||
type ShapeFunction = (
|
||||
db: MindmapDB,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
[0-9]+(?=[ \n]+) return 'NUM';
|
||||
<ID>\@\{ { this.begin('CONFIG'); return 'CONFIG_START'; }
|
||||
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
|
||||
<CONFIG>\}(?=\s+as\s) { this.popState(); this.begin('ALIAS'); return 'CONFIG_END'; }
|
||||
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
|
||||
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
|
||||
<ID>[^<>:\n,;@\s]+(?=\s+as\s) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
@@ -264,7 +265,10 @@ participant_statement
|
||||
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
|
||||
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
|
||||
| 'participant' actor_with_config 'AS' restOfLine 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant' actor_with_config 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $$=$2;}
|
||||
| 'participant_actor' actor_with_config 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant_actor' actor_with_config 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
|
||||
|
||||
;
|
||||
|
||||
|
||||
@@ -172,6 +172,12 @@ export class SequenceDB implements DiagramDB {
|
||||
doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as ParticipantMetaData;
|
||||
}
|
||||
type = doc?.type ?? type;
|
||||
|
||||
// If alias is provided in metadata and description is not already set, use the alias
|
||||
if (doc?.alias && (!description || description.text === name)) {
|
||||
description = { text: doc.alias, wrap: description?.wrap, type };
|
||||
}
|
||||
|
||||
const old = this.state.records.actors.get(id);
|
||||
if (old) {
|
||||
// If already set and trying to set to a new one throw error
|
||||
|
||||
@@ -2621,5 +2621,114 @@ Bob->>Alice:Got it!
|
||||
}
|
||||
expect(error).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse participant with stereotype and alias', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" } as Public API
|
||||
participant Bob@{ "type" : "control" } as Controller
|
||||
Alice->>Bob: Request
|
||||
Bob-->>Alice: Response
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Alice').type).toBe('boundary');
|
||||
expect(actors.get('Alice').description).toBe('Public API');
|
||||
expect(actors.get('Bob').type).toBe('control');
|
||||
expect(actors.get('Bob').description).toBe('Controller');
|
||||
});
|
||||
|
||||
it('should parse actor with stereotype and alias', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
actor A@{ "type" : "database" } AS Database Server
|
||||
actor B@{ "type" : "queue" } as Message Queue
|
||||
A->>B: Send message
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('A').type).toBe('database');
|
||||
expect(actors.get('A').description).toBe('Database Server');
|
||||
expect(actors.get('B').type).toBe('queue');
|
||||
expect(actors.get('B').description).toBe('Message Queue');
|
||||
});
|
||||
|
||||
it('should parse participant with stereotype and simple alias', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant API@{ "type" : "boundary" } AS Public API
|
||||
API->>API: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('API').type).toBe('boundary');
|
||||
expect(actors.get('API').description).toBe('Public API');
|
||||
});
|
||||
|
||||
it('should parse participant with inline alias in config object', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant API@{ "type" : "boundary", "alias": "Public API" }
|
||||
participant Auth@{ "type" : "control", "alias": "Auth Controller" }
|
||||
API->>Auth: Request
|
||||
Auth-->>API: Response
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('API').type).toBe('boundary');
|
||||
expect(actors.get('API').description).toBe('Public API');
|
||||
expect(actors.get('Auth').type).toBe('control');
|
||||
expect(actors.get('Auth').description).toBe('Auth Controller');
|
||||
});
|
||||
|
||||
it('should parse actor with inline alias in config object', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
actor U@{ "type" : "actor", "alias": "End User" }
|
||||
actor DB@{ "type" : "database", "alias": "User Database" }
|
||||
U->>DB: Query
|
||||
DB-->>U: Result
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('U').type).toBe('actor');
|
||||
expect(actors.get('U').description).toBe('End User');
|
||||
expect(actors.get('DB').type).toBe('database');
|
||||
expect(actors.get('DB').description).toBe('User Database');
|
||||
});
|
||||
|
||||
it('should prioritize external alias over inline alias', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant API@{ "type" : "boundary", "alias": "Internal Name" } as External Name
|
||||
API->>API: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('API').type).toBe('boundary');
|
||||
expect(actors.get('API').description).toBe('External Name');
|
||||
});
|
||||
|
||||
it('should handle participant with only inline alias (no type)', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant API@{ "alias": "Public API" }
|
||||
API->>API: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('API').description).toBe('Public API');
|
||||
});
|
||||
|
||||
it('should handle mixed inline and external alias syntax', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant A@{ "type" : "boundary", "alias": "Service A" }
|
||||
participant B@{ "type" : "control" } as Service B
|
||||
participant C@{ "type" : "database" }
|
||||
A->>B: Request
|
||||
B->>C: Query
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('A').type).toBe('boundary');
|
||||
expect(actors.get('A').description).toBe('Service A');
|
||||
expect(actors.get('B').type).toBe('control');
|
||||
expect(actors.get('B').description).toBe('Service B');
|
||||
expect(actors.get('C').type).toBe('database');
|
||||
expect(actors.get('C').description).toBe('C');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkdownOptions } from 'vitepress';
|
||||
import { defineConfig } from 'vitepress';
|
||||
import packageJson from '../../../package.json' assert { type: 'json' };
|
||||
import packageJson from '../../../package.json' with { type: 'json' };
|
||||
import { addCanonicalUrls } from './canonical-urls.js';
|
||||
import MermaidExample from './mermaid-markdown-all.js';
|
||||
|
||||
|
||||
@@ -1,147 +1,12 @@
|
||||
# Icon Pack Configuration
|
||||
# Registering icon pack in mermaid
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
## 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
|
||||
Using JSON file directly from CDN:
|
||||
|
||||
```js
|
||||
import mermaid from 'CDN/mermaid.esm.mjs';
|
||||
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: 'logos',
|
||||
@@ -151,15 +16,13 @@ mermaid.registerIconPacks([
|
||||
]);
|
||||
```
|
||||
|
||||
### Using Packages with Bundler
|
||||
|
||||
Install the icon pack:
|
||||
Using packages and a bundler:
|
||||
|
||||
```bash
|
||||
npm install @iconify-json/logos@1
|
||||
```
|
||||
|
||||
#### With Lazy Loading
|
||||
With lazy loading
|
||||
|
||||
```js
|
||||
import mermaid from 'mermaid';
|
||||
@@ -172,39 +35,15 @@ mermaid.registerIconPacks([
|
||||
]);
|
||||
```
|
||||
|
||||
#### Without Lazy Loading
|
||||
Without lazy loading
|
||||
|
||||
```js
|
||||
import mermaid from 'mermaid';
|
||||
import { icons } from '@iconify-json/logos';
|
||||
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: icons.prefix, // Use the prefix defined in the icon pack
|
||||
name: icons.prefix, // To 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.
|
||||
|
||||
@@ -52,6 +52,8 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
- [GitHub Writer](https://github.com/ckeditor/github-writer)
|
||||
- [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator)
|
||||
- [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅
|
||||
- [GNU Octave](https://octave.org/) ✅
|
||||
- [octave_mermaid_js](https://github.com/CNOCTAVE/octave_mermaid_js) ✅
|
||||
- [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid)
|
||||
- [MonsterWriter](https://www.monsterwriter.com/) ✅
|
||||
- [Joplin](https://joplinapp.org) ✅
|
||||
@@ -267,6 +269,7 @@ Communication tools and platforms
|
||||
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
|
||||
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
||||
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
||||
- [speccharts: Turn your test suites into specification diagrams](https://github.com/arnaudrenaud/speccharts)
|
||||
- [Vitepress Plugin](https://github.com/sametcn99/vitepress-mermaid-renderer)
|
||||
|
||||
<!--- cspell:ignore Blazorade HueHive --->
|
||||
|
||||
@@ -21,19 +21,19 @@
|
||||
"font-awesome": "^4.7.0",
|
||||
"jiti": "^2.4.2",
|
||||
"mermaid": "workspace:^",
|
||||
"vue": "^3.5.21"
|
||||
"vue": "^3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/carbon": "^1.2.13",
|
||||
"@unocss/reset": "^66.5.1",
|
||||
"@vite-pwa/vitepress": "^1.0.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@iconify-json/carbon": "^1.2.14",
|
||||
"@unocss/reset": "^66.5.7",
|
||||
"@vite-pwa/vitepress": "^1.0.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"fast-glob": "^3.3.3",
|
||||
"https-localhost": "^4.7.1",
|
||||
"pathe": "^2.0.3",
|
||||
"unocss": "^66.5.1",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^7.0.7",
|
||||
"unocss": "^66.5.7",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^7.0.8",
|
||||
"vite-plugin-pwa": "^1.0.3",
|
||||
"vitepress": "1.6.4",
|
||||
"workbox-window": "^7.3.0"
|
||||
|
||||
@@ -407,6 +407,8 @@ gitGraph
|
||||
|
||||
Sometimes you may want to hide the commit labels from the diagram. You can do this by using the `showCommitLabel` keyword. By default its value is `true`. You can set it to `false` using directives.
|
||||
|
||||
### Hiding all commit labels
|
||||
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
@@ -462,6 +464,66 @@ config:
|
||||
merge release
|
||||
```
|
||||
|
||||
### Hiding only auto-generated commit hash labels
|
||||
|
||||
In many cases, you may want to show labels only for commits with custom IDs (like "Alpha", "v1.0", "Feature-X") while hiding the auto-generated hash labels (like "0-a1b2c3d", "1-x9y8z7w"). This is useful when you want to emphasize important commits while keeping the diagram clean.
|
||||
|
||||
You can achieve this by using the `showCommitHashLabel` keyword. By default its value is `true`. When set to `false`, only commits with custom IDs will show their labels, while commits with auto-generated hash IDs will be hidden.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- **Auto-generated IDs**: Commits without a custom `id` are automatically assigned IDs in the format `seq-hash` (e.g., `0-a1b2c3d`, `1-x9y8z7w`). These follow the pattern of a number, a dash, and a random hash.
|
||||
- **Custom IDs**: Commits with explicitly defined IDs using `commit id: "YourID"` are considered custom IDs.
|
||||
- When `showCommitHashLabel: false`, only custom IDs are displayed.
|
||||
|
||||
Usage example - Hiding auto-generated hash labels:
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
gitGraph:
|
||||
showCommitHashLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
commit
|
||||
merge develop id: "Delta"
|
||||
commit
|
||||
```
|
||||
|
||||
In this example, only the commits with custom IDs ("Alpha", "Beta", "Gamma", "Delta") will show their labels. The commits without custom IDs (created with just `commit`) will not display their auto-generated hash labels.
|
||||
|
||||
Usage example - Showing all labels (default behavior):
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
gitGraph:
|
||||
showCommitHashLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit id: "Alpha"
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit id: "Beta"
|
||||
commit
|
||||
checkout main
|
||||
commit id: "Gamma"
|
||||
commit
|
||||
merge develop id: "Delta"
|
||||
commit
|
||||
```
|
||||
|
||||
In this example, all commits will show their labels, including both custom IDs and auto-generated hash IDs.
|
||||
|
||||
## Customizing main branch name
|
||||
|
||||
Sometimes you may want to customize the name of the main/default branch. You can do this by using the `mainBranchName` keyword. By default its value is `main`. You can set it to any string using directives.
|
||||
|
||||
@@ -120,7 +120,11 @@ sequenceDiagram
|
||||
|
||||
### Aliases
|
||||
|
||||
The actor can have a convenient identifier and a descriptive label.
|
||||
The actor can have a convenient identifier and a descriptive label. Aliases can be defined in two ways: using external syntax with the `as` keyword, or inline within the configuration object.
|
||||
|
||||
#### External Alias Syntax
|
||||
|
||||
You can define an alias using the `as` keyword after the participant declaration:
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
@@ -130,6 +134,48 @@ sequenceDiagram
|
||||
J->>A: Great!
|
||||
```
|
||||
|
||||
The external alias syntax also works with participant stereotype configurations, allowing you to combine type specification with aliases:
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary" } as Public API
|
||||
actor DB@{ "type": "database" } as User Database
|
||||
participant Svc@{ "type": "control" } as Auth Service
|
||||
API->>Svc: Authenticate
|
||||
Svc->>DB: Query user
|
||||
DB-->>Svc: User data
|
||||
Svc-->>API: Token
|
||||
```
|
||||
|
||||
#### Inline Alias Syntax
|
||||
|
||||
Alternatively, you can define an alias directly inside the configuration object using the `"alias"` field. This works with both `participant` and `actor` keywords:
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary", "alias": "Public API" }
|
||||
participant Auth@{ "type": "control", "alias": "Auth Service" }
|
||||
participant DB@{ "type": "database", "alias": "User Database" }
|
||||
API->>Auth: Login request
|
||||
Auth->>DB: Query user
|
||||
DB-->>Auth: User data
|
||||
Auth-->>API: Access token
|
||||
```
|
||||
|
||||
#### Alias Precedence
|
||||
|
||||
When both inline alias (in the configuration object) and external alias (using `as` keyword) are provided, the **external alias takes precedence**:
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant API@{ "type": "boundary", "alias": "Internal Name" } as External Name
|
||||
participant DB@{ "type": "database", "alias": "Internal DB" } as External DB
|
||||
API->>DB: Query
|
||||
DB-->>API: Result
|
||||
```
|
||||
|
||||
In the example above, "External Name" and "External DB" will be displayed, not "Internal Name" and "Internal DB".
|
||||
|
||||
### Actor Creation and Destruction (v10.3.0+)
|
||||
|
||||
It is possible to create and destroy actors by messages. To do so, add a create or destroy directive before the message.
|
||||
|
||||
@@ -7,7 +7,6 @@ import { select } from 'd3';
|
||||
import { compile, serialize, stringify } from 'stylis';
|
||||
import DOMPurify from 'dompurify';
|
||||
import isEmpty from 'lodash-es/isEmpty.js';
|
||||
import packageJson from '../package.json' assert { type: 'json' };
|
||||
import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js';
|
||||
import assignWithDepth from './assignWithDepth.js';
|
||||
import * as configApi from './config.js';
|
||||
@@ -421,12 +420,12 @@ const render = async function (
|
||||
// -------------------------------------------------------------------------------
|
||||
// Draw the diagram with the renderer
|
||||
try {
|
||||
await diag.renderer.draw(text, id, packageJson.version, diag);
|
||||
await diag.renderer.draw(text, id, injected.version, diag);
|
||||
} catch (e) {
|
||||
if (config.suppressErrorRendering) {
|
||||
removeTempElements();
|
||||
} else {
|
||||
errorRenderer.draw(text, id, packageJson.version);
|
||||
errorRenderer.draw(text, id, injected.version);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -1,560 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { ExtendedIconifyIcon, IconifyIcon, IconifyJSON } from '@iconify/types';
|
||||
import type { IconifyIconCustomisations } from '@iconify/utils';
|
||||
import { getIconData, iconToHTML, iconToSVG, replaceIDs, stringToIcon } from '@iconify/utils';
|
||||
import { defaultConfig, getConfig } from '../config.js';
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
import { getConfig } from '../config.js';
|
||||
import { sanitizeText } from '../diagrams/common/common.js';
|
||||
import { log } from '../logger.js';
|
||||
|
||||
@@ -24,114 +23,66 @@ export const unknownIcon: IconifyIcon = {
|
||||
width: 80,
|
||||
};
|
||||
|
||||
class IconManager {
|
||||
private iconsStore = new Map<string, IconifyJSON>();
|
||||
private loaderStore = new Map<string, AsyncIconLoader['loader']>();
|
||||
const iconsStore = new Map<string, IconifyJSON>();
|
||||
const loaderStore = new Map<string, AsyncIconLoader['loader']>();
|
||||
|
||||
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.');
|
||||
}
|
||||
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.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
clear(): void {
|
||||
this.iconsStore.clear();
|
||||
this.loaderStore.clear();
|
||||
const getRegisteredIconData = async (iconName: string, fallbackPrefix?: string) => {
|
||||
const data = stringToIcon(iconName, true, fallbackPrefix !== undefined);
|
||||
if (!data) {
|
||||
throw new Error(`Invalid icon name: ${iconName}`);
|
||||
}
|
||||
|
||||
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;
|
||||
const prefix = data.prefix || fallbackPrefix;
|
||||
if (!prefix) {
|
||||
throw new Error(`Icon name must contain a prefix: ${iconName}`);
|
||||
}
|
||||
|
||||
async isIconAvailable(iconName: string): Promise<boolean> {
|
||||
let icons = iconsStore.get(prefix);
|
||||
if (!icons) {
|
||||
const loader = loaderStore.get(prefix);
|
||||
if (!loader) {
|
||||
throw new Error(`Icon set not found: ${data.prefix}`);
|
||||
}
|
||||
try {
|
||||
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);
|
||||
const loaded = await loader();
|
||||
icons = { ...loaded, prefix };
|
||||
iconsStore.set(prefix, icons);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
iconData = unknownIcon;
|
||||
throw new Error(`Failed to load icon set: ${data.prefix}`);
|
||||
}
|
||||
const renderData = iconToSVG(iconData, customisations);
|
||||
const svg = iconToHTML(replaceIDs(renderData.body), {
|
||||
...renderData.attributes,
|
||||
...extraAttributes,
|
||||
});
|
||||
return sanitizeText(svg, getConfig());
|
||||
}
|
||||
}
|
||||
|
||||
const globalIconManager = new IconManager();
|
||||
const ephemeralIconManager = new IconManager();
|
||||
|
||||
export const registerIconPacks = (iconLoaders: IconLoader[]) =>
|
||||
globalIconManager.registerIconPacks(iconLoaders);
|
||||
|
||||
export const clearIconPacks = () => {
|
||||
globalIconManager.clear();
|
||||
ephemeralIconManager.clear();
|
||||
const iconData = getIconData(icons, data.name);
|
||||
if (!iconData) {
|
||||
throw new Error(`Icon not found: ${iconName}`);
|
||||
}
|
||||
return iconData;
|
||||
};
|
||||
|
||||
export const isIconAvailable = async (iconName: string) => {
|
||||
if (await ephemeralIconManager?.isIconAvailable(iconName)) {
|
||||
try {
|
||||
await getRegisteredIconData(iconName);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return await globalIconManager.isIconAvailable(iconName);
|
||||
};
|
||||
|
||||
export const getIconSVG = async (
|
||||
@@ -139,180 +90,17 @@ export const getIconSVG = async (
|
||||
customisations?: IconifyIconCustomisations & { fallbackPrefix?: string },
|
||||
extraAttributes?: Record<string, string>
|
||||
) => {
|
||||
if (ephemeralIconManager && (await ephemeralIconManager.isIconAvailable(iconName))) {
|
||||
return await ephemeralIconManager.getIconSVG(iconName, customisations, extraAttributes);
|
||||
let iconData: ExtendedIconifyIcon;
|
||||
try {
|
||||
iconData = await getRegisteredIconData(iconName, customisations?.fallbackPrefix);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
iconData = unknownIcon;
|
||||
}
|
||||
return await globalIconManager.getIconSVG(iconName, customisations, extraAttributes);
|
||||
const renderData = iconToSVG(iconData, customisations);
|
||||
const svg = iconToHTML(replaceIDs(renderData.body), {
|
||||
...renderData.attributes,
|
||||
...extraAttributes,
|
||||
});
|
||||
return sanitizeText(svg, getConfig());
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const registerDefaultLayoutLoaders = () => {
|
||||
name: 'dagre',
|
||||
loader: async () => await import('./layout-algorithms/dagre/index.js'),
|
||||
},
|
||||
...(includeLargeFeatures
|
||||
...(injected.includeLargeFeatures
|
||||
? [
|
||||
{
|
||||
name: 'cose-bilkent',
|
||||
|
||||
@@ -329,8 +329,6 @@ 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:
|
||||
@@ -894,6 +892,9 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
parallelCommits:
|
||||
type: boolean
|
||||
default: false
|
||||
showCommitHashLabel:
|
||||
type: boolean
|
||||
default: true
|
||||
# YAML anchor reference, don't use $ref since ajv doesn't load defaults
|
||||
arrowMarkerAbsolute: *arrowMarkerAbsolute
|
||||
|
||||
@@ -2370,49 +2371,3 @@ $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'
|
||||
|
||||
5
packages/mermaid/src/type.d.ts
vendored
5
packages/mermaid/src/type.d.ts
vendored
@@ -1,2 +1,5 @@
|
||||
// eslint-disable-next-line no-var
|
||||
declare var includeLargeFeatures: boolean;
|
||||
declare var injected: {
|
||||
version: string;
|
||||
includeLargeFeatures: boolean;
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ParticipantMetaData {
|
||||
| 'database'
|
||||
| 'collections'
|
||||
| 'queue';
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
export interface EdgeMetaData {
|
||||
|
||||
@@ -35,11 +35,6 @@ 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);
|
||||
|
||||
3884
pnpm-lock.yaml
generated
3884
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,8 @@ export default defineConfig({
|
||||
},
|
||||
define: {
|
||||
// Needs to be string
|
||||
includeLargeFeatures: 'true',
|
||||
'injected.includeLargeFeatures': 'true',
|
||||
'import.meta.vitest': 'undefined',
|
||||
packageVersion: "'0.0.0'",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user