mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-19 16:26:43 +02:00
Compare commits
155 Commits
test-valid
...
ipsep-cola
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f06f1bacd3 | ||
![]() |
d93d9a521d | ||
![]() |
c99bce6bab | ||
![]() |
4ab98c2ec7 | ||
![]() |
aeb51e56e2 | ||
![]() |
e12036ed6c | ||
![]() |
ddcd8a5e73 | ||
![]() |
e464d080ef | ||
![]() |
1a9b94ca2d | ||
![]() |
e4b33a1d99 | ||
![]() |
e27a9da61d | ||
![]() |
03cf10003f | ||
![]() |
8e31fdb611 | ||
![]() |
5dd748148f | ||
![]() |
895f9d43ff | ||
![]() |
4ea4a7b3fe | ||
![]() |
fb890a2be8 | ||
![]() |
5986189a52 | ||
![]() |
1988dfc956 | ||
![]() |
e48b0ba61d | ||
![]() |
1a4b8662cf | ||
![]() |
6083463c8e | ||
![]() |
1a14e331ea | ||
![]() |
ebb6680eba | ||
![]() |
6d1d46f88a | ||
![]() |
435790f931 | ||
![]() |
ffe9c1090e | ||
![]() |
a476e99d4c | ||
![]() |
0cc0b63e52 | ||
![]() |
694844050a | ||
![]() |
1be1620000 | ||
![]() |
c36cd05c45 | ||
![]() |
b7a591b8d3 | ||
![]() |
8bb29fc879 | ||
![]() |
e073c80019 | ||
![]() |
01aaef39b4 | ||
![]() |
3d640fc620 | ||
![]() |
724197c910 | ||
![]() |
6180c5f2ff | ||
![]() |
a9f7a94ae3 | ||
![]() |
3ffe9618ae | ||
![]() |
da539c1fa1 | ||
![]() |
5e8aa2dccf | ||
![]() |
ac04172cf8 | ||
![]() |
cf5b4b89a8 | ||
![]() |
1c269e0432 | ||
![]() |
999b836508 | ||
![]() |
326e4e3693 | ||
![]() |
9c92da487f | ||
![]() |
10752f1357 | ||
![]() |
1a80854242 | ||
![]() |
fc9c600a31 | ||
![]() |
da8ce0b93e | ||
![]() |
00a79353fc | ||
![]() |
1ceeca1ef1 | ||
![]() |
94890390ef | ||
![]() |
adfeb093cb | ||
![]() |
366d217928 | ||
![]() |
b94f1336ab | ||
![]() |
020c6d66e0 | ||
![]() |
09c239fc25 | ||
![]() |
b01a0d7dcd | ||
![]() |
cfc2551bdc | ||
![]() |
000308c8f5 | ||
![]() |
6039a8b930 | ||
![]() |
cd282f2245 | ||
![]() |
a27d90fe9c | ||
![]() |
64bf34b9ab | ||
![]() |
9faf2f9fb2 | ||
![]() |
f683b03645 | ||
![]() |
9cef40d164 | ||
![]() |
04612e078a | ||
![]() |
af585bdcc7 | ||
![]() |
37bfa2aa75 | ||
![]() |
54640ce476 | ||
![]() |
47b4c56b2b | ||
![]() |
6b1b0bf151 | ||
![]() |
7ba332ad4a | ||
![]() |
412d2a09d3 | ||
![]() |
7886fed8b2 | ||
![]() |
af3d5b6528 | ||
![]() |
404286a90d | ||
![]() |
827a9af790 | ||
![]() |
95733b6295 | ||
![]() |
8b86d617e7 | ||
![]() |
cc2112c7aa | ||
![]() |
767754f4fb | ||
![]() |
cff59c58b4 | ||
![]() |
ac976245ad | ||
![]() |
5b241bbb97 | ||
![]() |
7e5e47843b | ||
![]() |
9e2cd1a926 | ||
![]() |
abf2227faf | ||
![]() |
7db942b0e1 | ||
![]() |
70041c806f | ||
![]() |
77e2703f72 | ||
![]() |
771801b366 | ||
![]() |
a9a0a9b2de | ||
![]() |
1e5e835c41 | ||
![]() |
1f3f8da0f7 | ||
![]() |
b11f40e8ce | ||
![]() |
1fe045e638 | ||
![]() |
1c750ffc70 | ||
![]() |
b0ec93f29c | ||
![]() |
6b071c135a | ||
![]() |
42a3c3487f | ||
![]() |
d7b8ed2c5a | ||
![]() |
96c21c7e54 | ||
![]() |
e95e4d155a | ||
![]() |
688170558c | ||
![]() |
832f012e10 | ||
![]() |
2e2e8c4152 | ||
![]() |
6ff6e08c4b | ||
![]() |
d3e2be35be | ||
![]() |
260a045da0 | ||
![]() |
34e91f8b65 | ||
![]() |
627ee1f34d | ||
![]() |
28840ebd84 | ||
![]() |
4145879003 | ||
![]() |
e097b480d5 | ||
![]() |
003d1c7a70 | ||
![]() |
daf8d8d3be | ||
![]() |
d7a55b422b | ||
![]() |
12e3d31437 | ||
![]() |
ad024b01d6 | ||
![]() |
c12aea588c | ||
![]() |
9dfbf1166d | ||
![]() |
98bf9b4cb4 | ||
![]() |
e9ce8cf4da | ||
![]() |
d90634bf2b | ||
![]() |
90707e8062 | ||
![]() |
7e23f984e6 | ||
![]() |
b3a12237c0 | ||
![]() |
12c94a177b | ||
![]() |
71b04f93b0 | ||
![]() |
27185f62e4 | ||
![]() |
939da082b2 | ||
![]() |
e70be4f155 | ||
![]() |
6621f6ddb2 | ||
![]() |
3e99158c04 | ||
![]() |
339927d7e5 | ||
![]() |
2785ca47a0 | ||
![]() |
b70abed096 | ||
![]() |
1a5a846d71 | ||
![]() |
20ddc12b42 | ||
![]() |
5e037ee315 | ||
![]() |
70eedd840d | ||
![]() |
bb0aa4009c | ||
![]() |
75f940bf9e | ||
![]() |
a46de01885 | ||
![]() |
a7e9f4f926 | ||
![]() |
99c8224dfa | ||
![]() |
13f8549f81 | ||
![]() |
cfd25ed33e | ||
![]() |
3a8952ebe0 |
5
.changeset/beige-peas-shave.md
Normal file
5
.changeset/beige-peas-shave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@mermaid-js/mermaid-zenuml': patch
|
||||
---
|
||||
|
||||
Fixed a critical bug that the ZenUML diagram is not rendered.
|
5
.changeset/cute-crews-shake.md
Normal file
5
.changeset/cute-crews-shake.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Added IpSepCoLa algorithm for layout
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add `getRegisteredDiagramsMetadata` to `mermaid`, which returns all the registered diagram IDs in mermaid
|
5
.changeset/large-mirrors-cheer.md
Normal file
5
.changeset/large-mirrors-cheer.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Update casing of ID in requirement diagram
|
5
.changeset/lemon-masks-unite.md
Normal file
5
.changeset/lemon-masks-unite.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Added support for per link curve styling in flowchart diagram using edge ids
|
5
.changeset/light-flowers-judge.md
Normal file
5
.changeset/light-flowers-judge.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Make flowchart elk detector regex match less greedy
|
8
.changeset/lovely-queens-own.md
Normal file
8
.changeset/lovely-queens-own.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix(block): overflowing blocks no longer affect later lines
|
||||
|
||||
This may change the layout of block diagrams that have overflowing lines
|
||||
(i.e. block diagrams that use up more columns that the `columns` specifier).
|
7
.changeset/ninety-roses-turn.md
Normal file
7
.changeset/ninety-roses-turn.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: log warning for blocks exceeding column width
|
||||
|
||||
This update adds a validation check that logs a warning message when a block's width exceeds the defined column layout.
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@mermaid-js/examples': minor
|
||||
---
|
||||
|
||||
feat: Add examples for diagrams in the `@mermaid-js/examples` package
|
5
.changeset/rare-women-fly.md
Normal file
5
.changeset/rare-women-fly.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Add escaped class literal name on namespace
|
5
.changeset/silver-eyes-build.md
Normal file
5
.changeset/silver-eyes-build.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: migrate to class-based ArchitectureDB implementation
|
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'@mermaid-js/examples': patch
|
||||
'mermaid': patch
|
||||
'@mermaid-js/parser': patch
|
||||
---
|
||||
|
||||
chore: Move packet diagram out of beta
|
5
.changeset/smart-humans-cover.md
Normal file
5
.changeset/smart-humans-cover.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Update flowchart direction TD's behavior to be the same as TB
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: adjust sequence diagram title positioning to prevent overlap with top border in Safari
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: downgrade chokidar to 3.6.0 and ignore updates for hot reload stability
|
5
.changeset/vast-buses-see.md
Normal file
5
.changeset/vast-buses-see.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: Update packet diagram to use new class-based database structure
|
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix(timeline): fix loading `leftMargin` from config
|
||||
|
||||
The `timeline.leftMargin` config value should now correctly control the size of the left margin, instead of being ignored.
|
@@ -12,6 +12,7 @@ gantt
|
||||
gitgraph
|
||||
gzipped
|
||||
handDrawn
|
||||
ipsepcola
|
||||
kanban
|
||||
marginx
|
||||
marginy
|
||||
|
3
.github/lychee.toml
vendored
3
.github/lychee.toml
vendored
@@ -52,6 +52,9 @@ exclude = [
|
||||
# Swimm returns 404, even though the link is valid
|
||||
"https://docs.swimm.io",
|
||||
|
||||
# Certificate Error
|
||||
"https://noteshub.app",
|
||||
|
||||
# Timeout
|
||||
"https://huehive.co",
|
||||
"https://foswiki.org",
|
||||
|
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@889dce9eaba7900ce30494f5e1ac7220b27e5c81
|
||||
uses: peter-evans/create-pull-request@07cbaebb4bfc9c5d7db426ea5a5f585df29dd0a0
|
||||
with:
|
||||
add-paths: |
|
||||
cypress/timings.json
|
||||
|
6
.github/workflows/validate-lockfile.yml
vendored
6
.github/workflows/validate-lockfile.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
# 2) No unwanted vitepress paths
|
||||
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then
|
||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run \`rm -rf packages/mermaid/src/vitepress && pnpm install\` to regenerate.")
|
||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run `rm -rf packages/mermaid/src/vitepress && pnpm install` to regenerate.")
|
||||
fi
|
||||
|
||||
# 3) Lockfile only changes when package.json changes
|
||||
@@ -46,11 +46,12 @@ jobs:
|
||||
|
||||
# If any issues, output them and fail
|
||||
if [ ${#issues[@]} -gt 0 ]; then
|
||||
# Use the new GITHUB_OUTPUT approach to set a multiline output
|
||||
{
|
||||
echo "errors<<EOF"
|
||||
printf '%s\n' "${issues[@]}"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
} >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -62,7 +63,6 @@ jobs:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
The following issue(s) were detected:
|
||||
|
||||
${{ steps.validate.outputs.errors }}
|
||||
|
||||
Please address these and push an update.
|
||||
|
1005
CHANGELOG.md
1005
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
1
CHANGELOG.md
Symbolic link
1
CHANGELOG.md
Symbolic link
@@ -0,0 +1 @@
|
||||
./packages/mermaid/CHANGELOG.md
|
@@ -1,13 +0,0 @@
|
||||
import { MockedD3 } from '../packages/mermaid/src/tests/MockedD3.js';
|
||||
|
||||
export const select = function () {
|
||||
return new MockedD3();
|
||||
};
|
||||
|
||||
export const selectAll = function () {
|
||||
return new MockedD3();
|
||||
};
|
||||
|
||||
export const curveBasis = 'basis';
|
||||
export const curveLinear = 'linear';
|
||||
export const curveCardinal = 'cardinal';
|
@@ -384,4 +384,17 @@ describe('Block diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('BL30: block should overflow if too wide for columns', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
columns 2
|
||||
fit:2
|
||||
overflow:3
|
||||
short:1
|
||||
also_overflow:2
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -495,4 +495,34 @@ describe('Class diagram', () => {
|
||||
cy.get('a').should('have.attr', 'target', '_blank').should('have.attr', 'rel', 'noopener');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Include char sequence "graph" in text (#6795)', () => {
|
||||
it('has a label with char sequence "graph"', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
class Person {
|
||||
+String name
|
||||
-Int id
|
||||
#double age
|
||||
+Text demographicProfile
|
||||
}
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle backticks for namespace and class names', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
namespace \`A::B\` {
|
||||
class \`IPC::Sender\`
|
||||
}
|
||||
RenderProcessHost --|> \`IPC::Sender\`
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -354,4 +354,19 @@ ORDER ||--|{ LINE-ITEM : contains
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
describe('Include char sequence "graph" in text (#6795)', () => {
|
||||
it('has a label with char sequence "graph"', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
p[Photograph] {
|
||||
varchar(12) jobId
|
||||
date dateCreated
|
||||
}
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1113,4 +1113,24 @@ end
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('6617: Per Link Curve Styling using edge Ids', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart TD
|
||||
A e1@-->B e5@--> E
|
||||
E e7@--> D
|
||||
B e3@-->D
|
||||
A e2@-->C e4@-->D
|
||||
C e6@--> F
|
||||
F e8@--> D
|
||||
e1@{ curve: natural }
|
||||
e2@{ curve: stepAfter }
|
||||
e3@{ curve: monotoneY }
|
||||
e4@{ curve: bumpY }
|
||||
e5@{ curve: linear }
|
||||
e6@{ curve: catmullRom }
|
||||
e7@{ curve: cardinal }
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
153
cypress/integration/rendering/ipsepCola.spec.ts
Normal file
153
cypress/integration/rendering/ipsepCola.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { imgSnapshotTest } from '../../helpers/util.ts';
|
||||
|
||||
describe('Flowchart IPSepCoLa', () => {
|
||||
it('1-ipsepCola: should render a simple flowchart', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart
|
||||
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]
|
||||
`
|
||||
);
|
||||
});
|
||||
it('2-ipsepCola: handle bidirectional edges', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
subgraph D
|
||||
A --> B
|
||||
A --> B
|
||||
B --> A
|
||||
B --> A
|
||||
end
|
||||
`
|
||||
);
|
||||
});
|
||||
it('3-ipsepCola: handle multiple self loops', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart
|
||||
a --> a
|
||||
a --> a
|
||||
a --> a
|
||||
a --> a
|
||||
`
|
||||
);
|
||||
});
|
||||
it('4-ipsepCola: handle state diagram example', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
`
|
||||
);
|
||||
});
|
||||
it('5-ipsepCola: handle multiple subgraphs with edges between them', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart LR
|
||||
c1-->a2
|
||||
subgraph one
|
||||
a1-->a2
|
||||
end
|
||||
subgraph two
|
||||
b1-->b2
|
||||
end
|
||||
subgraph three
|
||||
c1-->c2
|
||||
end
|
||||
one --> two
|
||||
three --> two
|
||||
two --> c2
|
||||
`
|
||||
);
|
||||
});
|
||||
it('6-ipsepCola: handle class diagram example', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
classDiagram
|
||||
class AuthService {
|
||||
+login(username: string, password: string): boolean
|
||||
+logout(): void
|
||||
+register(): void
|
||||
}
|
||||
|
||||
class User {
|
||||
-username: string
|
||||
-password: string
|
||||
-role: Role
|
||||
+changePassword(): void
|
||||
}
|
||||
|
||||
class Role {
|
||||
-name: string
|
||||
-permissions: string[]
|
||||
+hasPermission(): boolean
|
||||
}
|
||||
|
||||
AuthService --> User
|
||||
User --> Role
|
||||
`
|
||||
);
|
||||
});
|
||||
it('7-ipsepCola: should render a decision flowchart', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Start([Start]) --> Prep[Preparation Step]
|
||||
Prep --> Split{Ready to Process?}
|
||||
Split -->|Yes| T1[Task A]
|
||||
Split -->|Yes| T2[Task B]
|
||||
T1 --> Merge
|
||||
T2 --> Merge
|
||||
Merge((Join Results)) --> Finalize[Finalize Process]
|
||||
Finalize --> End([End])
|
||||
`
|
||||
);
|
||||
});
|
||||
it('8-ipsepCola: handle nested subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart LR
|
||||
subgraph main
|
||||
subgraph subcontainer
|
||||
subcontainer-child
|
||||
end
|
||||
subcontainer-child--> subcontainer-sibling
|
||||
end
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
@@ -246,5 +246,22 @@ Word!\`]
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Include char sequence "graph" in text (#6795)', () => {
|
||||
it('has a label with char sequence "graph"', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
root
|
||||
Photograph
|
||||
Waterfall
|
||||
Landscape
|
||||
Geography
|
||||
Mountains
|
||||
Rocks
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
/* The end */
|
||||
});
|
||||
|
601
cypress/platform/ipsepcola_sample.html
Normal file
601
cypress/platform/ipsepcola_sample.html
Normal file
@@ -0,0 +1,601 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/font-awesome.min.css"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<style>
|
||||
body {
|
||||
/* background: #333; */
|
||||
font-family: 'Arial';
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
border: 3px solid #300;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
A --> B
|
||||
subgraph hello
|
||||
C --> D
|
||||
end
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
A --> B
|
||||
A --> B
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
A[hello] --> A
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
subgraph C
|
||||
c
|
||||
end
|
||||
A --> C
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
subgraph C
|
||||
c
|
||||
end
|
||||
A --> c
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart TD
|
||||
subgraph D
|
||||
A --> B
|
||||
A --> B
|
||||
B --> A
|
||||
B --> A
|
||||
end
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
subgraph B2
|
||||
A --> B --> C
|
||||
B --> D
|
||||
end
|
||||
|
||||
B2 --> X
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
classDiagram
|
||||
Animal <|-- Duck
|
||||
Animal <|-- Fish
|
||||
Animal <|-- Zebra
|
||||
Animal : +int age
|
||||
Animal : +String gender
|
||||
Animal: +isMammal()
|
||||
Animal: +mate()
|
||||
class Duck{
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
class Fish{
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
class Zebra{
|
||||
+bool is_wild
|
||||
+run()
|
||||
}
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart TD
|
||||
P1
|
||||
P1 -->P1.5
|
||||
subgraph P1.5
|
||||
P2
|
||||
P2.5(( A ))
|
||||
P3
|
||||
end
|
||||
P2 --> P4
|
||||
P3 --> P6
|
||||
P1.5 --> P5
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% Length of edges
|
||||
flowchart TD
|
||||
L1 --- L2
|
||||
L2 --- C
|
||||
M1 ---> C
|
||||
R1 .-> R2
|
||||
R2 <.-> C
|
||||
C -->|Label 1| E1
|
||||
C <-- Label 2 ---> E2
|
||||
C ----> E3
|
||||
C <-...-> E4
|
||||
C ======> E5
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% Stadium shape
|
||||
flowchart TD
|
||||
A([stadium shape test])
|
||||
A -->|Get money| B([Go shopping])
|
||||
B --> C([Let me think...<br />Do I want something for work,<br />something to spend every free second with,<br />or something to get around?])
|
||||
C -->|One| D([Laptop])
|
||||
C -->|Two| E([iPhone])
|
||||
C -->|Three| F([Car<br/>wroom wroom])
|
||||
click A "index.html#link-clicked" "link test"
|
||||
click B testClick "click test"
|
||||
classDef someclass fill:#f96;
|
||||
class A someclass;
|
||||
class C someclass;
|
||||
</pre>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% should render escaped without html labels
|
||||
flowchart TD
|
||||
a["<strong>Haiya</strong>"]---->b
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs in reverse order
|
||||
flowchart LR
|
||||
a -->b
|
||||
subgraph A
|
||||
B
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs in several levels
|
||||
flowchart LR
|
||||
b-->B
|
||||
a-->c
|
||||
subgraph O
|
||||
A
|
||||
end
|
||||
subgraph B
|
||||
c
|
||||
end
|
||||
subgraph A
|
||||
a
|
||||
b
|
||||
B
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with edges in and out
|
||||
flowchart LR
|
||||
internet
|
||||
nat
|
||||
routeur
|
||||
lb1
|
||||
lb2
|
||||
compute1
|
||||
compute2
|
||||
subgraph project
|
||||
routeur
|
||||
nat
|
||||
subgraph subnet1
|
||||
compute1
|
||||
lb1
|
||||
end
|
||||
subgraph subnet2
|
||||
compute2
|
||||
lb2
|
||||
end
|
||||
end
|
||||
internet --> routeur
|
||||
routeur --> subnet1 & subnet2
|
||||
subnet1 & subnet2 --> nat --> internet
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links
|
||||
flowchart LR
|
||||
subgraph main
|
||||
subgraph subcontainer
|
||||
subcontainer-child
|
||||
end
|
||||
subcontainer-child--> subcontainer-sibling
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with ingoing links
|
||||
flowchart LR
|
||||
subgraph one[One]
|
||||
subgraph sub_one[Sub One]
|
||||
_sub_one
|
||||
end
|
||||
subgraph sub_two[Sub Two]
|
||||
_sub_two
|
||||
end
|
||||
_one
|
||||
end
|
||||
|
||||
%% here, either the first or the second one
|
||||
sub_one --> sub_two
|
||||
_one --> b
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 3
|
||||
flowchart LR
|
||||
subgraph container_Beta
|
||||
process_C-->Process_D
|
||||
end
|
||||
subgraph container_Alpha
|
||||
process_A-->process_B
|
||||
process_A-->|messages|process_C
|
||||
end
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 4
|
||||
flowchart LR
|
||||
subgraph A
|
||||
a -->b
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 2
|
||||
flowchart LR
|
||||
c1-->a2
|
||||
subgraph one
|
||||
a1-->a2
|
||||
end
|
||||
subgraph two
|
||||
b1-->b2
|
||||
end
|
||||
subgraph three
|
||||
c1-->c2
|
||||
end
|
||||
one --> two
|
||||
three --> two
|
||||
two --> c2
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 5
|
||||
flowchart LR
|
||||
subgraph container_Beta
|
||||
process_C-->Process_D
|
||||
end
|
||||
subgraph container_Alpha
|
||||
process_A-->process_B
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
process_A-->|messages|process_C
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% More subgraphs
|
||||
flowchart LR
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
|
||||
three --> two
|
||||
two --> c2
|
||||
note[There are two links in this diagram]
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 5
|
||||
flowchart LR
|
||||
A[red text] -->|default style| B(blue text)
|
||||
C([red text]) -->|default style| D[[blue text]]
|
||||
E[(red text)] -->|default style| F((blue text))
|
||||
G>red text] -->|default style| H{blue text}
|
||||
I{{red text}} -->|default style| J[/blue text/]
|
||||
K[\\ red text\\] -->|default style| L[/blue text\\]
|
||||
M[\\ red text/] -->|default style| N[blue text];
|
||||
O(((red text))) -->|default style| P(((blue text)));
|
||||
linkStyle default color:Sienna;
|
||||
style A stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style B stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style C stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style D stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style E stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style F stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style G stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style H stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style I stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style J stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style K stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style L stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style M stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style N stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style O stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style P stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% labels on edges in a cluster
|
||||
flowchart RL
|
||||
subgraph one
|
||||
a1 -- I am a long label --> a2
|
||||
a1 -- Another long label --> a2
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% labels on edges in a cluster
|
||||
flowchart RL
|
||||
subgraph one
|
||||
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
|
||||
a1 -- Another long label --> a2
|
||||
a3 --> a1 & a2 & a3 & a4
|
||||
a1 --> a4
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% labels on edges in a cluster
|
||||
flowchart RL
|
||||
subgraph one
|
||||
a1[Iam a node with a super long label]
|
||||
a2[I am another node with a mega long label]
|
||||
a3[I am a node with a super long label]
|
||||
a4[I am another node with a mega long label]
|
||||
a1 -- Another long label --> a2
|
||||
a3 --> a1 & a2 & a3 & a4
|
||||
a1 --> a4
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% labels on edges in a cluster
|
||||
flowchart RL
|
||||
a1[I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. ]
|
||||
a2[I am another node with a mega long label]
|
||||
a3[I am a node with a super long label]
|
||||
a4[I am another node with a mega long label]
|
||||
a1 & a2 & a3 & a4 --> a5 & a6 & a7 & a8 & a9 & a10
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
subgraph Z
|
||||
subgraph X
|
||||
a --> b
|
||||
end
|
||||
subgraph Y
|
||||
c --> d
|
||||
end
|
||||
end
|
||||
Y --> X
|
||||
X --> P
|
||||
P --> Y
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
|
||||
a --> b
|
||||
b --> c
|
||||
b --> d
|
||||
c --> a
|
||||
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram3" class="mermaid">
|
||||
flowchart TD
|
||||
Start([Start]) --> Prep[Preparation Step]
|
||||
Prep --> Split{Ready to Process?}
|
||||
Split -->|Yes| T1[Task A]
|
||||
Split -->|Yes| T2[Task B]
|
||||
T1 --> Merge
|
||||
T2 --> Merge
|
||||
Merge((Join Results)) --> Finalize[Finalize Process]
|
||||
Finalize --> End([End])
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart TD
|
||||
A[Start Build] --> B[Compile Source]
|
||||
B --> C[Test Suite]
|
||||
C --> D{Tests Passed?}
|
||||
D -->|No| E[Notify Developer]
|
||||
E --> A
|
||||
D -->|Yes| F[Build Docker Image]
|
||||
|
||||
subgraph Deploy Pipeline
|
||||
F --> G[Deploy to Staging]
|
||||
G --> H[Run Integration Tests]
|
||||
H --> I{Tests Passed?}
|
||||
I -->|No| J[Rollback & Alert]
|
||||
I -->|Yes| K[Deploy to Production]
|
||||
end
|
||||
|
||||
K --> L([Success])
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Controller {
|
||||
+handleRequest(): void
|
||||
}
|
||||
|
||||
class View {
|
||||
+render(): void
|
||||
}
|
||||
|
||||
class Model {
|
||||
+getData(): any
|
||||
+setData(data: any): void
|
||||
}
|
||||
|
||||
Controller --> Model
|
||||
Controller --> View
|
||||
Model --> View : notifyChange()
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class AuthService {
|
||||
+login(username: string, password: string): boolean
|
||||
+logout(): void
|
||||
+register(): void
|
||||
}
|
||||
|
||||
class User {
|
||||
-username: string
|
||||
-password: string
|
||||
-role: Role
|
||||
+changePassword(): void
|
||||
}
|
||||
|
||||
class Role {
|
||||
-name: string
|
||||
-permissions: string[]
|
||||
+hasPermission(): boolean
|
||||
}
|
||||
|
||||
AuthService --> User
|
||||
User --> Role
|
||||
</pre
|
||||
>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||
|
||||
const staticBellIconPack = {
|
||||
prefix: 'fa6-regular',
|
||||
icons: {
|
||||
bell: {
|
||||
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
|
||||
width: 448,
|
||||
},
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
};
|
||||
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: 'logos',
|
||||
loader: () =>
|
||||
fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()),
|
||||
},
|
||||
{
|
||||
name: 'fa',
|
||||
loader: () => staticBellIconPack,
|
||||
},
|
||||
]);
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('Mermaid error: ', err);
|
||||
};
|
||||
window.callback = function () {
|
||||
alert('A callback was triggered');
|
||||
};
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
}
|
||||
await mermaid.initialize({
|
||||
theme: 'redux-dark',
|
||||
// theme: 'default',
|
||||
// theme: 'forest',
|
||||
handDrawnSeed: 12,
|
||||
look: 'classic ',
|
||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||
// layout: 'dagre',
|
||||
layout: 'ipsepCola',
|
||||
// layout: 'elk',
|
||||
// layout: 'sugiyama',
|
||||
// htmlLabels: false,
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
|
||||
// fontFamily: 'Caveat',
|
||||
// fontFamily: 'Kalam',
|
||||
// fontFamily: 'courier',
|
||||
fontFamily: 'arial',
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
messageFontFamily: 'courier',
|
||||
},
|
||||
kanban: {
|
||||
htmlLabels: false,
|
||||
},
|
||||
fontSize: 12,
|
||||
logLevel: 0,
|
||||
securityLevel: 'loose',
|
||||
callback,
|
||||
});
|
||||
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -301,7 +301,7 @@ If you are adding a feature, you will definitely need to add tests. Depending on
|
||||
|
||||
Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run.
|
||||
|
||||
Unit tests are mandatory for all code except the renderers. (The renderers are tested with integration tests.)
|
||||
Unit tests are mandatory for all code except the layout tests. (The layouts are tested with integration tests.)
|
||||
|
||||
We use [Vitest](https://vitest.dev) to run unit tests.
|
||||
|
||||
@@ -327,6 +327,30 @@ When using Docker prepend your command with `./run`:
|
||||
./run pnpm test
|
||||
```
|
||||
|
||||
##### Testing the DOM
|
||||
|
||||
One can use `jsdomIt` to test any part of Mermaid that interacts with the DOM, as long as it is not related to the layout.
|
||||
|
||||
The function `jsdomIt` ([developed in utils.ts](../../tests/util.ts)) overrides `it` from `vitest`, and creates a pseudo-browser environment that works almost like the real deal for the duration of the test. It uses JSDOM to create a DOM, and adds objects `window` and `document` to `global` to mock the browser environment.
|
||||
|
||||
> \[!NOTE]
|
||||
> The layout cannot work in `jsdomIt` tests because JSDOM has no rendering engine, hence the pseudo-browser environment.
|
||||
|
||||
Example :
|
||||
|
||||
```typescript
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
|
||||
jsdomIt('should add element "thing" in the SVG', ({ svg }) => {
|
||||
// Code in this block runs in a pseudo-browser environment
|
||||
addThing(svg); // The svg item is the D3 selection of the SVG node
|
||||
const svgNode = ensureNodeFromSelector('svg'); // Retrieve the DOM node using the DOM API
|
||||
expect(svgNode.querySelector('thing')).not.toBeNull(); // Test the structure of the SVG
|
||||
});
|
||||
```
|
||||
|
||||
They can be used to test any method that interacts with the DOM, including for testing renderers. For renderers, additional integration testing is necessary to test the layout though.
|
||||
|
||||
#### Integration / End-to-End (E2E) Tests
|
||||
|
||||
These test the rendering and visual appearance of the diagrams.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: LayoutData
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:153](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L153)
|
||||
|
||||
## Indexable
|
||||
|
||||
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.co
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L148)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:156](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L156)
|
||||
|
||||
---
|
||||
|
||||
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.co
|
||||
|
||||
> **edges**: `Edge`\[]
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L155)
|
||||
|
||||
---
|
||||
|
||||
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.co
|
||||
|
||||
> **nodes**: `Node`\[]
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:154](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L154)
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseOptions
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L59)
|
||||
Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:59](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **suppressErrors**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L64)
|
||||
Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77)
|
||||
|
||||
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:67](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L67)
|
||||
Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:67](https://github.com/mermaid-js/mer
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:75](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L75)
|
||||
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
|
||||
|
||||
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:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L71)
|
||||
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: RenderResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:85](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L85)
|
||||
Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:85](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **bindFunctions**: (`element`) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:103](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L103)
|
||||
Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
|
||||
|
||||
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:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
|
||||
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
|
||||
|
||||
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:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
|
||||
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
|
||||
|
||||
The svg code for the rendered graph.
|
||||
|
189
docs/diagrams/flowchart-code-flow.mmd
Normal file
189
docs/diagrams/flowchart-code-flow.mmd
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDiagram.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDb.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/styles.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/types.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowChartShapes.js"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/elk/detector.ts"
|
||||
generationTime: 2025-07-23T10:31:53.266Z
|
||||
---
|
||||
flowchart TD
|
||||
%% Entry Points and Detection
|
||||
Input["User Input Text"] --> Detection{Detection Phase}
|
||||
|
||||
Detection --> flowDetector["flowDetector.ts<br/>detector(txt, config)"]
|
||||
Detection --> flowDetectorV2["flowDetector-v2.ts<br/>detector(txt, config)"]
|
||||
Detection --> elkDetector["elk/detector.ts<br/>detector(txt, config)"]
|
||||
|
||||
flowDetector --> |"Checks /^\s*graph/"| DetectLegacy{Legacy Flowchart?}
|
||||
flowDetectorV2 --> |"Checks /^\s*flowchart/"| DetectNew{New Flowchart?}
|
||||
elkDetector --> |"Checks /^\s*flowchart-elk/"| DetectElk{ELK Layout?}
|
||||
|
||||
DetectLegacy --> |Yes| LoadDiagram
|
||||
DetectNew --> |Yes| LoadDiagram
|
||||
DetectElk --> |Yes| LoadDiagram
|
||||
|
||||
%% Loading Phase
|
||||
LoadDiagram["loader() function"] --> flowDiagram["flowDiagram.ts<br/>diagram object"]
|
||||
|
||||
flowDiagram --> DiagramStructure{Diagram Components}
|
||||
DiagramStructure --> Parser["parser: flowParser"]
|
||||
DiagramStructure --> Database["db: new FlowDB()"]
|
||||
DiagramStructure --> Renderer["renderer: flowRenderer-v3-unified"]
|
||||
DiagramStructure --> Styles["styles: flowStyles"]
|
||||
DiagramStructure --> Init["init: (cnf: MermaidConfig)"]
|
||||
|
||||
%% Parser Phase
|
||||
Parser --> flowParser["parser/flowParser.ts<br/>newParser.parse(src)"]
|
||||
flowParser --> |"Preprocesses src"| RemoveWhitespace["Remove trailing whitespace<br/>src.replace(/}\s*\n/g, '}\n')"]
|
||||
RemoveWhitespace --> flowJison["parser/flow.jison<br/>flowJisonParser.parse(newSrc)"]
|
||||
|
||||
flowJison --> ParseGraph["Parse Graph Structure"]
|
||||
ParseGraph --> ParseVertices["Parse Vertices"]
|
||||
ParseGraph --> ParseEdges["Parse Edges"]
|
||||
ParseGraph --> ParseSubgraphs["Parse Subgraphs"]
|
||||
ParseGraph --> ParseClasses["Parse Classes"]
|
||||
ParseGraph --> ParseStyles["Parse Styles"]
|
||||
|
||||
%% Database Phase - FlowDB Class
|
||||
Database --> FlowDBClass["flowDb.ts<br/>FlowDB class"]
|
||||
|
||||
FlowDBClass --> DBInit["constructor()<br/>- Initialize counters<br/>- Bind methods<br/>- Setup toolTips<br/>- Call clear()"]
|
||||
|
||||
DBInit --> DBMethods{FlowDB Methods}
|
||||
|
||||
DBMethods --> addVertex["addVertex(id, textObj, type, style,<br/>classes, dir, props, metadata)"]
|
||||
DBMethods --> addLink["addLink(_start[], _end[], linkData)"]
|
||||
DBMethods --> addSingleLink["addSingleLink(_start, _end, type, id)"]
|
||||
DBMethods --> setDirection["setDirection(dir)"]
|
||||
DBMethods --> addSubGraph["addSubGraph(nodes[], id, title)"]
|
||||
DBMethods --> addClass["addClass(id, style)"]
|
||||
DBMethods --> setClass["setClass(ids, className)"]
|
||||
DBMethods --> setTooltip["setTooltip(ids, tooltip)"]
|
||||
DBMethods --> setClickEvent["setClickEvent(id, functionName, args)"]
|
||||
DBMethods --> setClickFun["setClickFun(id, functionName, args)"]
|
||||
|
||||
%% Vertex Processing
|
||||
addVertex --> VertexProcess{Vertex Processing}
|
||||
VertexProcess --> CreateVertex["Create FlowVertex object<br/>- id, labelType, domId<br/>- styles[], classes[]"]
|
||||
VertexProcess --> SanitizeText["sanitizeText(textObj.text)"]
|
||||
VertexProcess --> ParseMetadata["Parse YAML metadata<br/>yaml.load(yamlData)"]
|
||||
VertexProcess --> SetVertexProps["Set vertex properties<br/>- shape, label, icon, form<br/>- pos, img, constraint, w, h"]
|
||||
|
||||
%% Edge Processing
|
||||
addSingleLink --> EdgeProcess{Edge Processing}
|
||||
EdgeProcess --> CreateEdge["Create FlowEdge object<br/>- start, end, type, text<br/>- labelType, classes[]"]
|
||||
EdgeProcess --> ProcessLinkText["Process link text<br/>- sanitizeText()<br/>- strip quotes"]
|
||||
EdgeProcess --> SetEdgeProps["Set edge properties<br/>- type, stroke, length"]
|
||||
EdgeProcess --> GenerateEdgeId["Generate edge ID<br/>getEdgeId(start, end, counter)"]
|
||||
EdgeProcess --> ValidateEdgeLimit["Validate edge limit<br/>maxEdges check"]
|
||||
|
||||
%% Data Collection
|
||||
DBMethods --> GetData["getData()"]
|
||||
GetData --> CollectNodes["Collect nodes[] from vertices"]
|
||||
GetData --> CollectEdges["Collect edges[] from edges"]
|
||||
GetData --> ProcessSubGraphs["Process subgraphs<br/>- parentDB Map<br/>- subGraphDB Map"]
|
||||
GetData --> AddNodeFromVertex["addNodeFromVertex()<br/>for each vertex"]
|
||||
GetData --> ProcessEdgeTypes["destructEdgeType()<br/>arrowTypeStart, arrowTypeEnd"]
|
||||
|
||||
%% Node Creation
|
||||
AddNodeFromVertex --> NodeCreation{Node Creation}
|
||||
NodeCreation --> FindExistingNode["findNode(nodes, vertex.id)"]
|
||||
NodeCreation --> CreateBaseNode["Create base node<br/>- id, label, parentId<br/>- cssStyles, cssClasses<br/>- shape, domId, tooltip"]
|
||||
NodeCreation --> GetCompiledStyles["getCompiledStyles(classDefs)"]
|
||||
NodeCreation --> GetTypeFromVertex["getTypeFromVertex(vertex)"]
|
||||
|
||||
%% Rendering Phase
|
||||
Renderer --> flowRendererV3["flowRenderer-v3-unified.ts<br/>draw(text, id, version, diag)"]
|
||||
|
||||
flowRendererV3 --> RenderInit["Initialize rendering<br/>- getConfig()<br/>- handle securityLevel<br/>- getDiagramElement()"]
|
||||
|
||||
RenderInit --> GetLayoutData["diag.db.getData()<br/>as LayoutData"]
|
||||
GetLayoutData --> SetupLayoutData["Setup layout data<br/>- type, layoutAlgorithm<br/>- direction, spacing<br/>- markers, diagramId"]
|
||||
|
||||
SetupLayoutData --> CallRender["render(data4Layout, svg)"]
|
||||
CallRender --> SetupViewPort["setupViewPortForSVG(svg, padding)"]
|
||||
SetupViewPort --> ProcessLinks["Process vertex links<br/>- create anchor elements<br/>- handle click events"]
|
||||
|
||||
%% Shape Rendering
|
||||
CallRender --> ShapeSystem["flowChartShapes.js<br/>Shape Functions"]
|
||||
|
||||
ShapeSystem --> ShapeFunctions{Shape Functions}
|
||||
ShapeFunctions --> question["question(parent, bbox, node)"]
|
||||
ShapeFunctions --> hexagon["hexagon(parent, bbox, node)"]
|
||||
ShapeFunctions --> rect_left_inv_arrow["rect_left_inv_arrow(parent, bbox, node)"]
|
||||
ShapeFunctions --> lean_right["lean_right(parent, bbox, node)"]
|
||||
ShapeFunctions --> lean_left["lean_left(parent, bbox, node)"]
|
||||
|
||||
ShapeFunctions --> insertPolygonShape["insertPolygonShape(parent, w, h, points)"]
|
||||
ShapeFunctions --> intersectPolygon["intersectPolygon(node, points, point)"]
|
||||
ShapeFunctions --> intersectRect["intersectRect(node, point)"]
|
||||
|
||||
%% Styling System
|
||||
Styles --> stylesTS["styles.ts<br/>getStyles(options)"]
|
||||
stylesTS --> StyleOptions["FlowChartStyleOptions<br/>- arrowheadColor, border2<br/>- clusterBkg, mainBkg<br/>- fontFamily, textColor"]
|
||||
|
||||
StyleOptions --> GenerateCSS["Generate CSS styles<br/>- .label, .cluster-label<br/>- .node, .edgePath<br/>- .flowchart-link, .edgeLabel"]
|
||||
GenerateCSS --> GetIconStyles["getIconStyles()"]
|
||||
|
||||
%% Type System
|
||||
Parser --> TypeSystem["types.ts<br/>Type Definitions"]
|
||||
TypeSystem --> FlowVertex["FlowVertex interface"]
|
||||
TypeSystem --> FlowEdge["FlowEdge interface"]
|
||||
TypeSystem --> FlowClass["FlowClass interface"]
|
||||
TypeSystem --> FlowSubGraph["FlowSubGraph interface"]
|
||||
TypeSystem --> FlowVertexTypeParam["FlowVertexTypeParam<br/>Shape types"]
|
||||
|
||||
%% Utility Functions
|
||||
DBMethods --> UtilityFunctions{Utility Functions}
|
||||
UtilityFunctions --> lookUpDomId["lookUpDomId(id)"]
|
||||
UtilityFunctions --> getClasses["getClasses()"]
|
||||
UtilityFunctions --> getDirection["getDirection()"]
|
||||
UtilityFunctions --> getVertices["getVertices()"]
|
||||
UtilityFunctions --> getEdges["getEdges()"]
|
||||
UtilityFunctions --> getSubGraphs["getSubGraphs()"]
|
||||
UtilityFunctions --> clear["clear()"]
|
||||
UtilityFunctions --> defaultConfig["defaultConfig()"]
|
||||
|
||||
%% Event Handling
|
||||
ProcessLinks --> EventHandling{Event Handling}
|
||||
EventHandling --> setupToolTips["setupToolTips(element)"]
|
||||
EventHandling --> bindFunctions["bindFunctions(element)"]
|
||||
EventHandling --> runFunc["utils.runFunc(functionName, args)"]
|
||||
|
||||
%% Common Database Functions
|
||||
DBMethods --> CommonDB["commonDb.js functions"]
|
||||
CommonDB --> setAccTitle["setAccTitle()"]
|
||||
CommonDB --> getAccTitle["getAccTitle()"]
|
||||
CommonDB --> setAccDescription["setAccDescription()"]
|
||||
CommonDB --> getAccDescription["getAccDescription()"]
|
||||
CommonDB --> setDiagramTitle["setDiagramTitle()"]
|
||||
CommonDB --> getDiagramTitle["getDiagramTitle()"]
|
||||
CommonDB --> commonClear["clear()"]
|
||||
|
||||
%% Final Output
|
||||
ProcessLinks --> FinalSVG["Final SVG Output"]
|
||||
|
||||
%% Layout Algorithm Selection
|
||||
SetupLayoutData --> LayoutAlgorithm{Layout Algorithm}
|
||||
LayoutAlgorithm --> Dagre["dagre<br/>(default)"]
|
||||
LayoutAlgorithm --> DagreWrapper["dagre-wrapper<br/>(v2 renderer)"]
|
||||
LayoutAlgorithm --> ELK["elk<br/>(external package)"]
|
||||
|
||||
%% Testing Components
|
||||
FlowDBClass --> TestFiles["Test Files"]
|
||||
TestFiles --> flowDbSpec["flowDb.spec.ts"]
|
||||
TestFiles --> flowChartShapesSpec["flowChartShapes.spec.js"]
|
||||
TestFiles --> ParserTests["parser/*.spec.js files<br/>- flow-text.spec.js<br/>- flow-edges.spec.js<br/>- flow-style.spec.js<br/>- subgraph.spec.js"]
|
||||
|
||||
%% Configuration
|
||||
Init --> ConfigSetup["Configuration Setup"]
|
||||
ConfigSetup --> FlowchartConfig["cnf.flowchart config"]
|
||||
ConfigSetup --> ArrowMarkers["arrowMarkerAbsolute"]
|
||||
ConfigSetup --> LayoutConfig["layout config"]
|
||||
ConfigSetup --> SetConfig["setConfig() calls"]
|
307
docs/diagrams/mermaid-api-sequence.mmd
Normal file
307
docs/diagrams/mermaid-api-sequence.mmd
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/mermaidAPI.ts"
|
||||
- "File: /packages/mermaid/src/mermaid.ts"
|
||||
generationTime: 2025-01-28T16:30:00.000Z
|
||||
---
|
||||
sequenceDiagram
|
||||
participant User as User/Browser
|
||||
participant Mermaid as mermaid.ts
|
||||
participant Queue as executionQueue
|
||||
participant API as mermaidAPI.ts
|
||||
participant Config as configApi
|
||||
participant Preprocessor as preprocessDiagram
|
||||
participant DiagramAPI as diagram-api
|
||||
participant Diagram as Diagram.fromText
|
||||
participant Renderer as diagram.renderer
|
||||
participant Styles as Styling System
|
||||
participant DOM as DOM/SVG
|
||||
|
||||
Note over User, DOM: Mermaid Complete API Flow
|
||||
|
||||
%% Initialization Phase
|
||||
User->>+Mermaid: mermaid.initialize(config)
|
||||
Mermaid->>+API: mermaidAPI.initialize(config)
|
||||
|
||||
API->>API: assignWithDepth({}, userOptions)
|
||||
API->>API: handle legacy fontFamily config
|
||||
API->>Config: saveConfigFromInitialize(options)
|
||||
|
||||
alt Theme Configuration Available
|
||||
API->>API: check if theme in theme object
|
||||
API->>API: set themeVariables from theme
|
||||
else Default Theme
|
||||
API->>API: use default theme variables
|
||||
end
|
||||
|
||||
API->>Config: setSiteConfig(options) or getSiteConfig()
|
||||
API->>API: setLogLevel(config.logLevel)
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
|
||||
API-->>-Mermaid: initialization complete
|
||||
Mermaid-->>-User: ready to render
|
||||
|
||||
%% Content Loaded Event
|
||||
User->>DOM: document.load event
|
||||
DOM->>+Mermaid: contentLoaded()
|
||||
|
||||
opt startOnLoad is true
|
||||
Mermaid->>Config: getConfig()
|
||||
Config-->>Mermaid: { startOnLoad: true }
|
||||
Mermaid->>Mermaid: run()
|
||||
end
|
||||
|
||||
Mermaid-->>-DOM: event handling complete
|
||||
|
||||
%% Main Run Function
|
||||
User->>+Mermaid: mermaid.run(options)
|
||||
|
||||
Mermaid->>Mermaid: runThrowsErrors(options)
|
||||
Mermaid->>Config: getConfig()
|
||||
Config-->>Mermaid: configuration object
|
||||
|
||||
alt nodes provided
|
||||
Mermaid->>Mermaid: use provided nodes
|
||||
else querySelector provided
|
||||
Mermaid->>DOM: document.querySelectorAll(querySelector)
|
||||
DOM-->>Mermaid: nodesToProcess
|
||||
end
|
||||
|
||||
Mermaid->>Mermaid: new InitIDGenerator(deterministicIds, seed)
|
||||
|
||||
loop For each diagram element
|
||||
Mermaid->>DOM: check element.getAttribute('data-processed')
|
||||
|
||||
opt not processed
|
||||
Mermaid->>DOM: element.setAttribute('data-processed', 'true')
|
||||
Mermaid->>Mermaid: generate unique id
|
||||
Mermaid->>DOM: get element.innerHTML
|
||||
Mermaid->>Mermaid: entityDecode and clean text
|
||||
Mermaid->>Mermaid: detectInit(txt)
|
||||
|
||||
Mermaid->>Queue: render(id, txt, element)
|
||||
end
|
||||
end
|
||||
|
||||
Mermaid-->>-User: processing initiated
|
||||
|
||||
%% Render Function (Queued)
|
||||
activate Queue
|
||||
Queue->>+API: mermaidAPI.render(id, text, container)
|
||||
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
API->>+Preprocessor: processAndSetConfigs(text)
|
||||
|
||||
Preprocessor->>Preprocessor: preprocessDiagram(text)
|
||||
Preprocessor->>Config: reset()
|
||||
Preprocessor->>Config: addDirective(processed.config)
|
||||
Preprocessor-->>-API: { code, config }
|
||||
|
||||
API->>Config: getConfig()
|
||||
Config-->>API: current configuration
|
||||
|
||||
opt text length > maxTextSize
|
||||
API->>API: text = MAX_TEXTLENGTH_EXCEEDED_MSG
|
||||
end
|
||||
|
||||
API->>API: setup id selectors and element IDs
|
||||
API->>API: determine security level (sandbox/loose)
|
||||
|
||||
%% DOM Setup
|
||||
alt svgContainingElement provided
|
||||
alt isSandboxed
|
||||
API->>DOM: sandboxedIframe(select(svgContainingElement), iFrameID)
|
||||
API->>DOM: select iframe contentDocument body
|
||||
else
|
||||
API->>DOM: select(svgContainingElement)
|
||||
end
|
||||
else no container
|
||||
API->>API: removeExistingElements(document, id, divId, iFrameId)
|
||||
alt isSandboxed
|
||||
API->>DOM: sandboxedIframe(select('body'), iFrameID)
|
||||
else
|
||||
API->>DOM: select('body')
|
||||
end
|
||||
end
|
||||
|
||||
API->>DOM: appendDivSvgG(root, id, enclosingDivID, fontFamily, XMLNS_XLINK_STD)
|
||||
|
||||
%% Diagram Creation
|
||||
API->>+Diagram: Diagram.fromText(text, { title: processed.title })
|
||||
|
||||
Diagram->>DiagramAPI: detect diagram type
|
||||
Diagram->>DiagramAPI: load appropriate diagram
|
||||
Diagram-->>-API: diagram instance
|
||||
|
||||
opt parsing error occurred
|
||||
API->>+Diagram: Diagram.fromText('error')
|
||||
Diagram-->>-API: error diagram
|
||||
API->>API: store parseEncounteredException
|
||||
end
|
||||
|
||||
%% Style Generation
|
||||
API->>DOM: get svg element and firstChild
|
||||
API->>Renderer: diag.renderer.getClasses(text, diag)
|
||||
Renderer-->>API: diagramClassDefs
|
||||
|
||||
API->>+Styles: createUserStyles(config, diagramType, diagramClassDefs, idSelector)
|
||||
|
||||
Styles->>Styles: createCssStyles(config, classDefs)
|
||||
|
||||
opt config.themeCSS defined
|
||||
Styles->>Styles: append themeCSS
|
||||
end
|
||||
|
||||
opt fontFamily configured
|
||||
Styles->>Styles: add CSS variables for fonts
|
||||
end
|
||||
|
||||
opt classDefs exist
|
||||
loop For each styleClassDef
|
||||
opt has styles
|
||||
Styles->>Styles: cssImportantStyles for each CSS element
|
||||
end
|
||||
opt has textStyles
|
||||
Styles->>Styles: cssImportantStyles for tspan elements
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Styles->>Styles: getStyles(graphType, userCSSstyles, themeVariables)
|
||||
Styles->>Styles: serialize(compile(svgId{allStyles}), stringify)
|
||||
Styles-->>-API: compiled CSS rules
|
||||
|
||||
API->>DOM: create style element
|
||||
API->>DOM: style.innerHTML = rules
|
||||
API->>DOM: svg.insertBefore(style, firstChild)
|
||||
|
||||
%% Diagram Rendering
|
||||
API->>+Renderer: diag.renderer.draw(text, id, version, diag)
|
||||
|
||||
Renderer->>Renderer: diagram-specific rendering logic
|
||||
Renderer->>DOM: create SVG elements
|
||||
Renderer->>DOM: apply positioning and styling
|
||||
Renderer-->>-API: rendered diagram
|
||||
|
||||
opt rendering error
|
||||
alt suppressErrorRendering
|
||||
API->>API: removeTempElements()
|
||||
API->>Mermaid: throw error
|
||||
else
|
||||
API->>Renderer: errorRenderer.draw(text, id, version)
|
||||
end
|
||||
end
|
||||
|
||||
%% Accessibility and Cleanup
|
||||
API->>DOM: select svg element
|
||||
API->>Diagram: diag.db.getAccTitle()
|
||||
API->>Diagram: diag.db.getAccDescription()
|
||||
API->>API: addA11yInfo(diagramType, svgNode, a11yTitle, a11yDescr)
|
||||
|
||||
API->>DOM: set xmlns for foreignobject elements
|
||||
API->>DOM: get innerHTML from enclosing div
|
||||
|
||||
API->>+API: cleanUpSvgCode(svgCode, isSandboxed, arrowMarkerAbsolute)
|
||||
|
||||
opt not useArrowMarkerUrls and not sandboxed
|
||||
API->>API: replace marker-end URLs with anchors
|
||||
end
|
||||
|
||||
API->>API: decodeEntities(svgCode)
|
||||
API->>API: replace <br> with <br/>
|
||||
API-->>-API: cleaned SVG code
|
||||
|
||||
alt isSandboxed
|
||||
API->>+API: putIntoIFrame(svgCode, svgEl)
|
||||
API->>API: calculate iframe height
|
||||
API->>API: toBase64 encode SVG content
|
||||
API->>API: create iframe with sandbox attributes
|
||||
API-->>-API: iframe HTML
|
||||
else not loose security
|
||||
API->>API: DOMPurify.sanitize(svgCode, options)
|
||||
end
|
||||
|
||||
API->>API: attachFunctions()
|
||||
API->>API: removeTempElements()
|
||||
|
||||
opt parseEncounteredException
|
||||
API->>Mermaid: throw parseEncounteredException
|
||||
end
|
||||
|
||||
API-->>-Queue: { diagramType, svg: svgCode, bindFunctions }
|
||||
|
||||
%% Return to Web Integration
|
||||
activate Mermaid
|
||||
Queue-->>Mermaid: render result
|
||||
Mermaid->>DOM: element.innerHTML = svg
|
||||
|
||||
opt postRenderCallback
|
||||
Mermaid->>User: postRenderCallback(id)
|
||||
end
|
||||
|
||||
opt bindFunctions exist
|
||||
Mermaid->>DOM: bindFunctions(element)
|
||||
end
|
||||
deactivate Mermaid
|
||||
|
||||
%% Parse Function Flow
|
||||
User->>+Mermaid: mermaid.parse(text, parseOptions)
|
||||
activate Queue
|
||||
|
||||
Queue->>+API: mermaidAPI.parse(text, parseOptions)
|
||||
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
API->>Preprocessor: processAndSetConfigs(text)
|
||||
Preprocessor-->>API: { code, config }
|
||||
API->>Diagram: getDiagramFromText(code)
|
||||
Diagram-->>API: diagram instance
|
||||
API-->>-Queue: { diagramType: diagram.type, config }
|
||||
|
||||
Queue-->>-Mermaid: parse result
|
||||
Mermaid-->>-User: ParseResult or false
|
||||
|
||||
%% External Diagram Registration
|
||||
User->>+Mermaid: registerExternalDiagrams(diagrams, options)
|
||||
|
||||
Mermaid->>DiagramAPI: addDiagrams()
|
||||
Mermaid->>DiagramAPI: registerLazyLoadedDiagrams(...diagrams)
|
||||
|
||||
opt lazyLoad is false
|
||||
Mermaid->>DiagramAPI: loadRegisteredDiagrams()
|
||||
end
|
||||
|
||||
Mermaid-->>-User: registration complete
|
||||
|
||||
%% Error Handling
|
||||
Note over Mermaid, API: Error Handling Throughout
|
||||
alt Error occurs
|
||||
API->>Mermaid: throw error
|
||||
Mermaid->>+Mermaid: handleError(error, errors, parseError)
|
||||
|
||||
Mermaid->>Mermaid: log.warn(error)
|
||||
|
||||
alt isDetailedError
|
||||
Mermaid->>User: parseError(error.str, error.hash)
|
||||
else
|
||||
Mermaid->>User: parseError(error)
|
||||
end
|
||||
|
||||
opt not suppressErrors
|
||||
Mermaid->>User: throw error
|
||||
end
|
||||
|
||||
Mermaid-->>-User: error handled
|
||||
end
|
||||
|
||||
%% Configuration Details
|
||||
Note over Config: Configuration Functions
|
||||
Note right of Config: Functions:<br/>- reset()<br/>- getConfig()<br/>- setConfig()<br/>- getSiteConfig()<br/>- updateSiteConfig()<br/>- saveConfigFromInitialize()
|
||||
|
||||
Note over Styles: CSS Generation
|
||||
Note right of Styles: Features:<br/>- createCssStyles()<br/>- createUserStyles()<br/>- cssImportantStyles()<br/>- Theme integration<br/>- Class definitions
|
||||
|
||||
Note over API: Security Levels
|
||||
Note right of API: Modes:<br/>- sandbox: iframe isolation<br/>- loose: minimal sanitization<br/>- default: DOMPurify sanitization
|
||||
|
||||
Note over API: Helper Functions
|
||||
Note right of API: Utilities:<br/>- cleanUpSvgCode()<br/>- putIntoIFrame()<br/>- appendDivSvgG()<br/>- removeExistingElements()
|
180
docs/diagrams/mindmap-implementation-sequence.mmd
Normal file
180
docs/diagrams/mindmap-implementation-sequence.mmd
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapDb.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/detector.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapTypes.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/styles.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/svgDraw.ts"
|
||||
generationTime: 2025-01-28T16:00:00.000Z
|
||||
---
|
||||
sequenceDiagram
|
||||
participant User as User Input Text
|
||||
participant Detector as detector.ts
|
||||
participant Loader as DiagramLoader
|
||||
participant Definition as mindmap-definition.ts
|
||||
participant Parser as parser/mindmap.jison
|
||||
participant DB as MindmapDB
|
||||
participant Renderer as mindmapRenderer.ts
|
||||
participant Cytoscape as cytoscape.js
|
||||
participant SVGDraw as svgDraw.ts
|
||||
participant Styles as styles.ts
|
||||
participant Output as Final SVG
|
||||
|
||||
Note over User, Output: Mindmap Implementation Flow
|
||||
|
||||
%% Detection Phase
|
||||
User->>Detector: /^\s*mindmap/ text input
|
||||
activate Detector
|
||||
Detector->>Detector: detector(txt) validates pattern
|
||||
Detector->>Loader: loader() function called
|
||||
deactivate Detector
|
||||
|
||||
activate Loader
|
||||
Loader->>Definition: import mindmap-definition.js
|
||||
deactivate Loader
|
||||
|
||||
%% Core Structure Setup
|
||||
activate Definition
|
||||
Definition->>DB: get db() → new MindmapDB()
|
||||
Definition->>Renderer: setup renderer
|
||||
Definition->>Parser: setup parser
|
||||
Definition->>Styles: setup styles
|
||||
deactivate Definition
|
||||
|
||||
%% Database Initialization
|
||||
activate DB
|
||||
Note over DB: MindmapDB Constructor
|
||||
DB->>DB: initialize nodes array
|
||||
DB->>DB: setup nodeType constants
|
||||
DB->>DB: bind methods
|
||||
DB->>DB: clear() state
|
||||
|
||||
%% Parsing Phase
|
||||
activate Parser
|
||||
User->>Parser: mindmap syntax text
|
||||
|
||||
loop For each node in hierarchy
|
||||
Parser->>DB: addNode(level, id, descr, type)
|
||||
activate DB
|
||||
DB->>DB: sanitizeText(id, descr)
|
||||
DB->>DB: getType(startStr, endStr)
|
||||
Note right of DB: Shape Detection:<br/>[ → RECT<br/>( → ROUNDED_RECT<br/>(( → CIRCLE<br/>)) → BANG<br/>{{ → HEXAGON
|
||||
DB->>DB: getParent(level)
|
||||
DB->>DB: create MindmapNode
|
||||
DB->>DB: add to hierarchy
|
||||
deactivate DB
|
||||
end
|
||||
|
||||
opt Icon/Class Decoration
|
||||
Parser->>DB: decorateNode(decoration)
|
||||
DB->>DB: set icon/class properties
|
||||
end
|
||||
deactivate Parser
|
||||
|
||||
%% Data Preparation
|
||||
Renderer->>DB: getData() for layout
|
||||
activate DB
|
||||
DB->>DB: collect all nodes
|
||||
DB->>DB: build parent-child relationships
|
||||
DB-->>Renderer: return node hierarchy
|
||||
deactivate DB
|
||||
|
||||
%% Rendering Pipeline
|
||||
activate Renderer
|
||||
Note over Renderer: Rendering Phase
|
||||
|
||||
Renderer->>Cytoscape: initialize cytoscape
|
||||
activate Cytoscape
|
||||
|
||||
loop For each node in mindmap
|
||||
Renderer->>Cytoscape: addNodes(mindmap, cy, conf, level)
|
||||
Cytoscape->>Cytoscape: create node data
|
||||
Cytoscape->>Cytoscape: set position (x, y)
|
||||
end
|
||||
|
||||
loop For parent-child relationships
|
||||
Renderer->>Cytoscape: add edges
|
||||
Cytoscape->>Cytoscape: create edge data
|
||||
end
|
||||
|
||||
Renderer->>Cytoscape: configure cose-bilkent layout
|
||||
Cytoscape->>Cytoscape: calculate optimal positions
|
||||
Cytoscape-->>Renderer: return positioned graph
|
||||
deactivate Cytoscape
|
||||
|
||||
%% SVG Generation
|
||||
Renderer->>SVGDraw: drawNodes(db, svg, mindmap, section, conf)
|
||||
activate SVGDraw
|
||||
|
||||
loop For each node recursively
|
||||
SVGDraw->>SVGDraw: select shape function
|
||||
|
||||
alt Default Shape
|
||||
SVGDraw->>SVGDraw: defaultBkg() - rounded rectangle
|
||||
else Rectangle Shape
|
||||
SVGDraw->>SVGDraw: rectBkg() - sharp corners
|
||||
else Circle Shape
|
||||
SVGDraw->>SVGDraw: circleBkg() - perfect circle
|
||||
else Cloud Shape
|
||||
SVGDraw->>SVGDraw: cloudBkg() - organic curves
|
||||
else Bang Shape
|
||||
SVGDraw->>SVGDraw: bangBkg() - explosion style
|
||||
else Hexagon Shape
|
||||
SVGDraw->>SVGDraw: hexagonBkg() - six sides
|
||||
end
|
||||
|
||||
SVGDraw->>SVGDraw: create SVG elements
|
||||
SVGDraw->>SVGDraw: add text labels
|
||||
SVGDraw->>SVGDraw: position node
|
||||
|
||||
opt Node has children
|
||||
SVGDraw->>SVGDraw: drawNodes() recursive call
|
||||
end
|
||||
end
|
||||
deactivate SVGDraw
|
||||
|
||||
%% Edge Rendering
|
||||
Renderer->>Renderer: drawEdges(edgesEl, cy)
|
||||
loop For each edge
|
||||
Renderer->>Renderer: extract edge bounds
|
||||
Renderer->>Renderer: draw SVG path
|
||||
end
|
||||
|
||||
%% Styling Application
|
||||
Renderer->>Styles: getStyles(options)
|
||||
activate Styles
|
||||
|
||||
Styles->>Styles: genSections(options)
|
||||
loop For THEME_COLOR_LIMIT sections
|
||||
Styles->>Styles: generate color scale
|
||||
Styles->>Styles: create CSS rules
|
||||
Note right of Styles: .section-X fills<br/>.edge-depth-X widths<br/>.node-icon-X colors
|
||||
end
|
||||
|
||||
Styles->>Styles: apply theme integration
|
||||
Styles-->>Renderer: return compiled CSS
|
||||
deactivate Styles
|
||||
|
||||
%% Final Assembly
|
||||
Renderer->>Output: selectSvgElement()
|
||||
Renderer->>Output: setupGraphViewbox()
|
||||
Renderer->>Output: apply styles
|
||||
Renderer->>Output: add interactive elements
|
||||
deactivate Renderer
|
||||
|
||||
activate Output
|
||||
Note over Output: Final Mindmap Features
|
||||
Output->>Output: responsive layout
|
||||
Output->>Output: accessibility attributes
|
||||
Output->>Output: hover effects
|
||||
Output->>Output: click handling
|
||||
Output-->>User: rendered mindmap
|
||||
deactivate Output
|
||||
|
||||
%% Configuration Details
|
||||
Note over DB, Styles: Configuration Options
|
||||
Note right of DB: Padding Calculations:<br/>Base padding from config<br/>RECT: ×2 padding<br/>ROUNDED_RECT: ×2 padding<br/>HEXAGON: ×2 padding
|
||||
Note right of Styles: Section Management:<br/>MAX_SECTIONS = 12<br/>Dynamic color generation<br/>Git theme integration
|
||||
Note right of Renderer: Layout Parameters:<br/>Cytoscape configuration<br/>coseBilkent settings<br/>Node spacing rules
|
@@ -84,6 +84,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
LLM integrations to create mermaid diagrams using AI from text descriptions.
|
||||
|
||||
- [HueHive - Create mermaid diagrams with text](https://huehive.co/tools/diagrams)
|
||||
- [MCP Server Mermaid](https://github.com/hustcc/mcp-mermaid) - Generate mermaid diagram and chart with AI MCP dynamically.
|
||||
|
||||
### CRM/ERP
|
||||
|
||||
@@ -103,6 +104,7 @@ Blogging frameworks and platforms
|
||||
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
||||
- [WordPress](https://wordpress.org)
|
||||
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
||||
- [WP Documentation](https://wordpress.org/themes/wp-documentation/)
|
||||
|
||||
### CMS/ECM
|
||||
|
||||
|
@@ -16,9 +16,7 @@ Applications that support Mermaid files [SHOULD](https://datatracker.ietf.org/do
|
||||
|
||||
### MIME Type
|
||||
|
||||
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is `text/vnd.mermaid`.
|
||||
|
||||
Currently pending [IANA](https://www.iana.org/) recognition.
|
||||
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is [`text/vnd.mermaid`](https://www.iana.org/assignments/media-types/application/vnd.mermaid).
|
||||
|
||||
## Showcase
|
||||
|
||||
|
156
docs/layouts/development.md
Normal file
156
docs/layouts/development.md
Normal file
@@ -0,0 +1,156 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/development.md](../../packages/mermaid/src/docs/layouts/development.md).
|
||||
|
||||
# 🛠️ How to Create a New Layout Algorithm in Mermaid
|
||||
|
||||
Mermaid supports pluggable layout engines, and contributors can add custom layout algorithms to support specialized rendering needs such as clustered layouts, nested structures, or domain-specific visualizations.
|
||||
|
||||
This guide outlines the steps required to **create and integrate a new layout algorithm** into the Mermaid codebase.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Prerequisites
|
||||
|
||||
Before starting, ensure the following:
|
||||
|
||||
- You have [Node.js](https://nodejs.org/) installed.
|
||||
- You have [pnpm](https://pnpm.io/) installed globally:
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Step-by-Step Integration
|
||||
|
||||
### Refer [Mermaid Contributing Guide](../community/contributing.md)
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Implementing Your Custom Layout Algorithm
|
||||
|
||||
### 1. Create Your Layout Folder
|
||||
|
||||
Navigate to the relevant source directory and create a folder for your new algorithm:
|
||||
|
||||
```bash
|
||||
cd packages/mermaid/src/layout
|
||||
mkdir myCustomLayout
|
||||
touch myCustomLayout/index.ts
|
||||
```
|
||||
|
||||
> 📁 You can organize supporting files, utils, and types inside this folder.
|
||||
|
||||
### 2. Register the Layout Algorithm
|
||||
|
||||
Open the file:
|
||||
|
||||
```
|
||||
packages/mermaid/src/rendering-util/render.ts
|
||||
```
|
||||
|
||||
Inside the function `registerDefaultLayoutLoaders`, find the `layoutLoaders` array. Add your layout here:
|
||||
|
||||
```ts
|
||||
registerDefaultLayoutLoaders([
|
||||
...,
|
||||
{
|
||||
id: 'myCustomLayout',
|
||||
loader: () => import('../layout/myCustomLayout'),
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
This tells Mermaid how to load your layout dynamically by name (`id`).
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Your Algorithm
|
||||
|
||||
### 3. Create a Test File
|
||||
|
||||
To visually test your layout implementation, create a test HTML file in:
|
||||
|
||||
```
|
||||
cypress/platform/
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
touch cypress/platform/myCustomLayoutTest.html
|
||||
```
|
||||
|
||||
Inside the file, load your diagram like this:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module">
|
||||
import mermaid from '/dist/mermaid.esm.mjs';
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
layout: 'myCustomLayout', // Use your layout here
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mermaid">graph TD A[Node A] --> B[Node B] B --> C[Node C]</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 4. Open in Browser
|
||||
|
||||
After running `pnpm dev`, open your test in the browser:
|
||||
|
||||
```
|
||||
http://localhost:9000/myCustomLayoutTest.html
|
||||
```
|
||||
|
||||
You should see your diagram rendered using your new layout engine.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Tips
|
||||
|
||||
- Keep your layout algorithm modular and readable.
|
||||
- Use TypeScript types and helper functions for better structure.
|
||||
- Add comments and constraints where necessary.
|
||||
- If applicable, create a unit test and add a visual test for Cypress.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Example File Structure
|
||||
|
||||
```
|
||||
packages/
|
||||
└── mermaid/
|
||||
└── src/
|
||||
└── layout/
|
||||
└── myCustomLayout/
|
||||
├── index.ts
|
||||
├── utils.ts
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Checklist
|
||||
|
||||
- [ ] All dependencies installed via `pnpm i`
|
||||
- [ ] Layout folder and files created under `src/layout/`
|
||||
- [ ] Entry registered in `registerDefaultLayoutLoaders`
|
||||
- [ ] HTML test file added under `cypress/platform/`
|
||||
- [ ] Diagram renders as expected at `localhost:9000`
|
||||
- [ ] Code is linted and documented
|
||||
|
||||
---
|
||||
|
||||
> 💡 You’re now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!
|
138
docs/layouts/introduction.md
Normal file
138
docs/layouts/introduction.md
Normal file
@@ -0,0 +1,138 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/introduction.md](../../packages/mermaid/src/docs/layouts/introduction.md).
|
||||
|
||||
# 📊 Layout Algorithms in Mermaid
|
||||
|
||||
Mermaid is a popular JavaScript-based diagramming tool that supports auto-layout for graphs using pluggable layout engines. Layout algorithms play a critical role in rendering nodes and edges in a clean, readable, and meaningful way. Mermaid currently uses engines like **Dagre** and **ELK**, and will soon introduce a powerful new layout engine: **IPSep-CoLa**.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Dagre Layout
|
||||
|
||||
**Dagre** is a layout engine inspired by the **Sugiyama algorithm**, optimized for directed acyclic graphs (DAGs). It arranges nodes in layers and computes edge routing to minimize crossings and improve readability.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Layered (Sugiyama-style) layout**: Ideal for top-down or left-to-right flow.
|
||||
- **Edge routing**: Attempts to reduce edge crossings and bends.
|
||||
- **Ranking**: Vertices are assigned ranks to group related elements into the same level.
|
||||
- **Lightweight and fast**: Suitable for small to medium-sized graphs.
|
||||
|
||||
### Technical Overview:
|
||||
|
||||
- Works in four stages:
|
||||
1. **Cycle Removal**
|
||||
2. **Layer Assignment**
|
||||
3. **Node Ordering**
|
||||
4. **Coordinate Assignment**
|
||||
- Outputs crisp layouts where edge direction is clear and logical.
|
||||
|
||||
### Limitations:
|
||||
|
||||
- No native support for **grouped or nested structures**.
|
||||
- Not ideal for graphs with **non-hierarchical** or **dense cyclic connections**.
|
||||
- Limited edge label placement capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 🔸 ELK (Eclipse Layout Kernel)
|
||||
|
||||
**ELK** is a modular, extensible layout framework developed as part of the Eclipse ecosystem. It supports a wide variety of graph types and layout strategies.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Multiple layout styles**: Hierarchical, force-based, layered, orthogonal, etc.
|
||||
- **Support for ports**: Allows fine-grained edge anchoring on specific sides of nodes.
|
||||
- **Group and hierarchy awareness**: Ideal for nested and compartmentalized diagrams.
|
||||
- **Rich configuration**: Offers control over spacing, edge routing, direction, padding, and more.
|
||||
|
||||
### Technical Overview:
|
||||
|
||||
- Uses a **model-driven approach** with a well-defined intermediate representation (ELK Graph Model).
|
||||
- Different engines are plugged in depending on the chosen layout strategy.
|
||||
- Works well with large, complex, and deeply nested graphs.
|
||||
|
||||
### Limitations:
|
||||
|
||||
- Requires verbose configuration for best results.
|
||||
- Can be slower than Dagre for small or simple diagrams.
|
||||
- More complex to integrate and control dynamically.
|
||||
|
||||
---
|
||||
|
||||
## 🆕 IPSep-CoLa
|
||||
|
||||
### 🌐 Introduction
|
||||
|
||||
**IPSep-CoLa** stands for **Incremental Procedure for Separation Constraint Layout**, a next-generation layout algorithm tailored for **grouped, nested, and labeled graphs**. It is an enhancement over standard force-directed layouts, offering constraint enforcement and iterative refinement.
|
||||
|
||||
It is particularly useful for diagrams where:
|
||||
|
||||
- **Group integrity** is important (e.g., modules, clusters).
|
||||
- **Edge labels** need smart placement.
|
||||
- **Overlaps** must be prevented even under tight space constraints.
|
||||
|
||||
---
|
||||
|
||||
### ⚙️ How IPSep-CoLa Works
|
||||
|
||||
#### 1. **Constraint-Based Force Simulation**:
|
||||
|
||||
It builds on top of standard force-directed approaches (like CoLa), but adds **constraints** to influence the final positions of nodes:
|
||||
|
||||
- **Separation constraints**: Minimum distances between nodes, edge labels, and groups.
|
||||
- **Containment constraints**: Child nodes must stay within the bounds of parent groups.
|
||||
- **Alignment constraints**: Nodes can be aligned in rows or columns if desired.
|
||||
|
||||
#### 2. **Incremental Refinement**:
|
||||
|
||||
Unlike one-pass algorithms, IPSep-CoLa works in **phases**:
|
||||
|
||||
- Initial layout is produced using a base force simulation.
|
||||
- The layout is iteratively adjusted using **constraint solvers**.
|
||||
- Additional forces (spring, collision avoidance, containment) are incrementally added.
|
||||
|
||||
#### 3. **Edge Label Handling**:
|
||||
|
||||
One of the distinguishing features of IPSep-CoLa is its support for **multi-segment edge routing with mid-edge label positioning**, ensuring labels do not clutter or overlap.
|
||||
|
||||
---
|
||||
|
||||
### 📌 Use Cases
|
||||
|
||||
IPSep-CoLa is ideal for:
|
||||
|
||||
- **Hierarchical graphs** with complex nesting (e.g., software architecture, UML diagrams).
|
||||
- **Clustered views** (e.g., social network groupings).
|
||||
- **Diagrams with heavy labeling** where label placement affects readability.
|
||||
- **Diagrams with strict visual structure** needs — maintaining boundaries, margins, or padding.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Comparison Table
|
||||
|
||||
| Feature | Dagre | ELK | IPSep-CoLa |
|
||||
| ------------------------- | ----------- | ------------------- | ------------------------------ |
|
||||
| Layout Type | Layered DAG | Modular (varied) | Constraint-driven force layout |
|
||||
| Edge Labeling | ⚠️ Basic | ✅ Yes | ✅ Smart Placement |
|
||||
| Overlap Avoidance | ⚠️ Partial | ✅ Configurable | ✅ Automatic |
|
||||
| Layout Performance | ✅ Fast | ⚠️ Medium | ⚠️ Medium |
|
||||
| Customization Flexibility | ⚠️ Limited | ✅ Extensive | ✅ Moderate to High |
|
||||
| Best For | Simple DAGs | Complex hierarchies | Grouped and labeled graphs |
|
||||
|
||||
---
|
||||
|
||||
## 🧾 Summary
|
||||
|
||||
Each layout engine in Mermaid serves a different purpose:
|
||||
|
||||
- **Dagre** is best for fast, simple, and readable DAGs.
|
||||
- **ELK** is powerful for modular, layered, or port-based diagrams with a need for rich customization.
|
||||
- **IPSep-CoLa** will soon offer a flexible, constraint-respecting layout engine that excels at **visual clarity in grouped and complex diagrams**.
|
||||
|
||||
The addition of IPSep-CoLa to Mermaid's layout stack represents a significant leap forward in layout control and quality — making it easier than ever to visualize rich, structured, and annotated graphs.
|
||||
|
||||
---
|
46
docs/layouts/ipsepcola/implementation.md
Normal file
46
docs/layouts/ipsepcola/implementation.md
Normal file
@@ -0,0 +1,46 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/ipsepcola/implementation.md](../../../packages/mermaid/src/docs/layouts/ipsepcola/implementation.md).
|
||||
|
||||
## IPSEPCOLA Documentation :
|
||||
|
||||
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
|
||||
|
||||
## How IPSep-CoLa built :
|
||||
|
||||
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
|
||||
|
||||
1. Layer Assignment :
|
||||
The layer assignment algorithm organizes nodes into hierarchical layers to create a structured layout for directed graphs. It begins by detecting and temporarily removing cyclic edges using a depth-first search (DFS) approach, ensuring the graph becomes a Directed Acyclic Graph (DAG) for proper layering. The algorithm then performs a topological sort using Kahn's method, calculating node ranks (layers) based on in-degree counts. Each node's layer is determined by its position in the topological order, with parent nodes always appearing in higher layers than their children to maintain proper flow direction.
|
||||
|
||||
The implementation handles special cases like nested nodes by considering parent-child relationships when calculating layers. Nodes without dependencies are placed in layer 0, while subsequent nodes are assigned to layers one level below their nearest parent. The algorithm efficiently processes nodes using a queue system, decrementing in-degrees as it progresses, and ultimately stores the layer information directly in the node objects. Though cyclic edges are removed during processing, they could potentially be reintroduced after layer assignment if needed for visualization purposes.
|
||||
|
||||
2. Node ordering:
|
||||
After assigning layers to nodes, this step organizes nodes horizontally within each layer to minimize edge crossings and create a clean, readable layout. It uses the barycenter method—a technique that positions each node based on the average position of its connected neighbors (either incoming or outgoing). Nodes with no connections are pushed to the end of their layer.
|
||||
|
||||
The algorithm works in multiple passes (iterations) to refine the order: first adjusting nodes based on their incoming connections (from the layer above), then outgoing connections (to the layer below). Group nodes (like containers) are handled separately—their position is determined by averaging the positions of their children, ensuring they stay properly aligned with their contents. This approach keeps the layout structured while reducing visual clutter.
|
||||
|
||||
3. AssignInitial positions to node :
|
||||
This step calculates the starting (x, y) positions for each node based on its assigned layer (vertical level) and order (horizontal position). Nodes are spaced evenly—horizontally using nodeSpacing and vertically using layerHeight. For example, a node in layer 2 with order 3 will be placed at (3 \_ nodeSpacing, 2 \_ layerHeight). This creates a grid-like structure where nodes align neatly in rows (layers) and columns (orders).
|
||||
|
||||
The initial positioning is simple but crucial—it provides a structured starting point before more advanced adjustments (like reducing edge crossings or compacting the layout) are applied. Group nodes follow the same logic, ensuring they align with their children. This method ensures a readable, organized foundation for further refinement.
|
||||
|
||||
4. Force-Directed Simulation with Constraints :
|
||||
|
||||
- Spring Forces: Attracts connected nodes to maintain desired edge lengths.
|
||||
- Repulsion Forces: Pushes nodes apart to prevent overlaps.
|
||||
- Group Constraints: Ensures child nodes stay near their parent groups.
|
||||
- Cooling Factor: Gradually reduces movement to stabilize the layout.
|
||||
|
||||
5. Incremental Refinement :
|
||||
|
||||
- Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
|
||||
- Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
|
||||
- Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
|
||||
|
||||
6. Adjusting the Final Layout :
|
||||
This step takes the calculated node positions and applies them to the visual elements of the graph. Nodes are placed at their assigned (x, y) coordinates—regular nodes are positioned directly, while group nodes (clusters) are rendered as containers that may include other nodes. Edges (connections between nodes) are drawn based on their start and end points, ensuring they follow the structured layout.
|
||||
|
||||
The adjustment phase bridges the mathematical layout with the actual rendering, updating the SVG or canvas elements to reflect the computed positions. This ensures that the graph is not only logically organized but also visually coherent, with proper spacing, alignment, and connections. The result is a clean, readable diagram ready for display.
|
186
docs/layouts/ipsepcola/overview.md
Normal file
186
docs/layouts/ipsepcola/overview.md
Normal file
@@ -0,0 +1,186 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/ipsepcola/overview.md](../../../packages/mermaid/src/docs/layouts/ipsepcola/overview.md).
|
||||
|
||||
## IPSEPCOLA Documentation :
|
||||
|
||||
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
|
||||
|
||||
## Introduction :
|
||||
|
||||
IPSep-CoLa (Incremental Procedure for Separation Constraint Layout) is an advanced graph layout algorithm designed to handle complex diagrams with separation constraints, such as grouped nodes, edge labels, and hierarchical structures. Unlike traditional force-directed algorithms, IPSep-CoLa incrementally refines node positions while enforcing geometric constraints to prevent overlaps, maintain group cohesion, and optimize edge routing.
|
||||
|
||||
The algorithm is particularly effective for visualizing nested and clustered graphs, where maintaining clear separation between elements is crucial. It combines techniques from force-directed layout, constraint satisfaction, and incremental refinement to produce readable and aesthetically pleasing diagrams.
|
||||
|
||||
## How IPSep-CoLa Works :
|
||||
|
||||
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
|
||||
|
||||
1. Graph Preprocessing :
|
||||
Cycle Removal: Detects and temporarily removes cyclic dependencies to enable proper layering.
|
||||
Layer Assignment: Assigns nodes to hierarchical layers using topological sorting.
|
||||
Node Ordering: Uses the barycenter heuristic to minimize edge crossings within layers.
|
||||
|
||||
2. Force-Directed Simulation with Constraints :
|
||||
Spring Forces: Attracts connected nodes to maintain desired edge lengths.
|
||||
Repulsion Forces: Pushes nodes apart to prevent overlaps.
|
||||
Group Constraints: Ensures child nodes stay near their parent groups.
|
||||
Cooling Factor: Gradually reduces movement to stabilize the layout.
|
||||
|
||||
3. Incremental Refinement :
|
||||
Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
|
||||
Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
|
||||
Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
|
||||
|
||||
## Key Features :
|
||||
|
||||
1. Group-Aware Layout: Maintains separation between nested structures.
|
||||
2. Edge Label Placement: Uses edge labels as virtual nodes and automatically positions labels inside their parent groups.
|
||||
3. Stable Convergence: Uses cooling factors and incremental updates for smooth refinement.
|
||||
4. Support for Self-Loops & Parallel Edges: Avoids visual clutter with intelligent edge routing.
|
||||
|
||||
## Use Cases :
|
||||
|
||||
1. Hierarchical Diagrams (org charts, flowcharts, decision trees)
|
||||
2. Network Visualization (dependency graphs, data pipelines)
|
||||
3. Interactive Graph Editors (real-time layout adjustments)
|
||||
4. Clustered Data Visualization (UML diagrams, biological networks)
|
||||
|
||||
## **Examples**
|
||||
|
||||
### **Example 1**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
CEO --> MKT["Marketing Head"]
|
||||
CEO --> ENG["Engineering Head"]
|
||||
ENG --> DEV["Developer"]
|
||||
ENG --> QA["QA Tester"]
|
||||
```
|
||||
|
||||
### **Example 2**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
Start["Start"] --> Red{"Is it red?"}
|
||||
Red -- Yes --> Round{"Is it round?"}
|
||||
Red -- No --> NotApple["❌ Not an Apple"]
|
||||
Round -- Yes --> Apple["✅ It's an Apple"]
|
||||
Round -- No --> NotApple2["❌ Not an Apple"]
|
||||
```
|
||||
|
||||
### **Example 3**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
A[Module A] --> B[Module B]
|
||||
A --> C[Module C]
|
||||
B --> D[Module D]
|
||||
C --> D
|
||||
D --> E[Module E]
|
||||
```
|
||||
|
||||
### **Example 4**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
Source1["📦 Raw Data (CSV)"]
|
||||
Source2["🌐 API Data"]
|
||||
|
||||
Source1 --> Clean["🧹 Clean & Format"]
|
||||
Source2 --> Clean
|
||||
|
||||
Clean --> Transform["🔄 Transform Data"]
|
||||
Transform --> Load["📥 Load into Data Warehouse"]
|
||||
Load --> BI["📊 BI Dashboard"]
|
||||
```
|
||||
|
||||
### **Example 5**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
classDiagram
|
||||
class Person {
|
||||
-String name
|
||||
-int age
|
||||
+greet(): void
|
||||
}
|
||||
|
||||
class Employee {
|
||||
-int employeeId
|
||||
+calculateSalary(): float
|
||||
}
|
||||
|
||||
class Manager {
|
||||
-String department
|
||||
+assignTask(): void
|
||||
}
|
||||
|
||||
Person <|-- Employee
|
||||
Employee <|-- Manager
|
||||
```
|
||||
|
||||
### **Example 6**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Sunlight["☀️ Sunlight"] --> Leaf["🌿 Leaf"]
|
||||
Leaf --> Glucose["🍬 Glucose"]
|
||||
Leaf --> Oxygen["💨 Oxygen"]
|
||||
```
|
||||
|
||||
### **Example 7**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Internet["🌐 Internet"] --> Router["📡 Router"]
|
||||
Router --> Server1["🖥️ Server A"]
|
||||
Router --> Server2["🖥️ Server B"]
|
||||
Router --> Laptop["💻 Laptop"]
|
||||
|
||||
%% New device joins
|
||||
Router --> Mobile["📱 Mobile"]
|
||||
```
|
||||
|
||||
## Limitations :
|
||||
|
||||
1. Computational Cost: More iterations may be needed for large graphs (>1000 nodes).
|
||||
2. Parameter Tuning: Requires adjustments for different graph types.
|
||||
3. Non-Determinism: Small variations may occur between runs due to force simulation.
|
||||
|
||||
## Conclusion :
|
||||
|
||||
IPSep-CoLa provides a robust solution for constraint-based graph layout, particularly for structured and clustered diagrams. By combining incremental refinement with separation constraints, it achieves readable and well-organized visualizations. Future improvements could include GPU acceleration and adaptive parameter tuning for large-scale graphs.
|
@@ -1795,15 +1795,54 @@ It is possible to style the type of curve used for lines between items, if the d
|
||||
Available curve styles include `basis`, `bumpX`, `bumpY`, `cardinal`, `catmullRom`, `linear`, `monotoneX`, `monotoneY`,
|
||||
`natural`, `step`, `stepAfter`, and `stepBefore`.
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
|
||||
Line styling can be achieved in two ways:
|
||||
|
||||
1. Change the curve style of all the lines
|
||||
2. Change the curve style of a particular line
|
||||
|
||||
#### Diagram level curve style
|
||||
|
||||
In this example, a left-to-right graph uses the `stepBefore` curve style:
|
||||
|
||||
```
|
||||
%%{ init: { 'flowchart': { 'curve': 'stepBefore' } } }%%
|
||||
---
|
||||
config:
|
||||
flowchart:
|
||||
curve: stepBefore
|
||||
---
|
||||
graph LR
|
||||
```
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
#### Edge level curve style using Edge IDs (v\<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
You can assign IDs to [edges](#attaching-an-id-to-edges). After assigning an ID you can modify the line style by modifying the edge's `curve` property using the following syntax:
|
||||
|
||||
```mermaid-example
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
A e2@--> C
|
||||
e1@{ curve: linear }
|
||||
e2@{ curve: natural }
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
A e2@--> C
|
||||
e1@{ curve: linear }
|
||||
e2@{ curve: natural }
|
||||
```
|
||||
|
||||
```info
|
||||
Any edge curve style modified at the edge level overrides the diagram level style.
|
||||
```
|
||||
|
||||
```info
|
||||
If the same edge is modified multiple times the last modification will be rendered.
|
||||
```
|
||||
|
||||
### Styling a node
|
||||
|
||||
|
21
package.json
21
package.json
@@ -67,7 +67,7 @@
|
||||
"@argos-ci/cypress": "^5.0.2",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@cspell/eslint-plugin": "^8.19.3",
|
||||
"@cspell/eslint-plugin": "^8.19.4",
|
||||
"@cypress/code-coverage": "^3.12.49",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
@@ -94,17 +94,17 @@
|
||||
"cypress-split": "^1.24.14",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-cypress": "^4.3.0",
|
||||
"eslint-plugin-html": "^8.1.2",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"eslint-plugin-jsdoc": "^50.6.9",
|
||||
"eslint-plugin-html": "^8.1.3",
|
||||
"eslint-plugin-jest": "^28.14.0",
|
||||
"eslint-plugin-jsdoc": "^50.8.0",
|
||||
"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.0",
|
||||
"eslint-plugin-unicorn": "^59.0.1",
|
||||
"express": "^5.1.0",
|
||||
"globals": "^16.0.0",
|
||||
"globby": "^14.0.2",
|
||||
@@ -112,7 +112,7 @@
|
||||
"jest": "^30.0.4",
|
||||
"jison": "^0.4.18",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"langium-cli": "3.3.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"markdown-table": "^3.0.4",
|
||||
@@ -126,7 +126,7 @@
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.7.3",
|
||||
"typescript": "~5.7.3",
|
||||
"typescript-eslint": "^8.32.0",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.0.3",
|
||||
"vite-plugin-istanbul": "^7.0.0",
|
||||
"vitest": "^3.0.6"
|
||||
@@ -139,8 +139,13 @@
|
||||
"roughjs": "patches/roughjs.patch"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"canvas",
|
||||
"cypress",
|
||||
"esbuild"
|
||||
],
|
||||
"ignoredBuiltDependencies": [
|
||||
"sharp",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
14
packages/examples/CHANGELOG.md
Normal file
14
packages/examples/CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# @mermaid-js/examples
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#6453](https://github.com/mermaid-js/mermaid/pull/6453) [`4936ef5`](https://github.com/mermaid-js/mermaid/commit/4936ef5c306d2f892cca9a95a5deac4af6d4882b) Thanks [@sidharthv96](https://github.com/sidharthv96)! - feat: Add examples for diagrams in the `@mermaid-js/examples` package
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6510](https://github.com/mermaid-js/mermaid/pull/6510) [`7a38eb7`](https://github.com/mermaid-js/mermaid/commit/7a38eb715d795cd5c66cb59357d64ec197b432e6) Thanks [@sidharthv96](https://github.com/sidharthv96)! - chore: Move packet diagram out of beta
|
||||
|
||||
- Updated dependencies [[`5acbd7e`](https://github.com/mermaid-js/mermaid/commit/5acbd7e762469d9d89a9c77faf6617ee13367f3a), [`d90634b`](https://github.com/mermaid-js/mermaid/commit/d90634bf2b09e586b055729e07e9a1a31b21827c), [`7a38eb7`](https://github.com/mermaid-js/mermaid/commit/7a38eb715d795cd5c66cb59357d64ec197b432e6), [`3e3ae08`](https://github.com/mermaid-js/mermaid/commit/3e3ae089305e1c7b9948b9e149eba6854fe7f2d6), [`d3e2be3`](https://github.com/mermaid-js/mermaid/commit/d3e2be35be066adeb7fd502b4a24c223c3b53947), [`637680d`](https://github.com/mermaid-js/mermaid/commit/637680d4d9e39b4f8cb6f05b4cb261e8f5693ac3)]:
|
||||
- mermaid@11.9.0
|
41
packages/examples/README.md
Normal file
41
packages/examples/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @mermaid-js/examples
|
||||
|
||||
The `@mermaid-js/examples` package contains a collection of examples used by tools like [mermaid.live](https://mermaid.live) to help users get started with new diagrams.
|
||||
|
||||
You can duplicate an existing diagram example file, e.g., `packages/examples/src/examples/flowchart.ts`, and modify it with details specific to your diagram.
|
||||
|
||||
Then, import the example in the `packages/examples/src/index.ts` file and add it to the `examples` array.
|
||||
|
||||
Each diagram should have at least one example, which should be marked as the default. It's a good idea to add more examples to showcase different features of the diagram.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
pnpm add @mermaid-js/examples
|
||||
```
|
||||
|
||||
A sample usage of the package in mermaid.live, to get the default example for every diagram type:
|
||||
|
||||
```ts
|
||||
import { diagramData } from '@mermaid-js/examples';
|
||||
|
||||
type DiagramDefinition = (typeof diagramData)[number];
|
||||
|
||||
const isValidDiagram = (diagram: DiagramDefinition): diagram is Required<DiagramDefinition> => {
|
||||
return Boolean(diagram.name && diagram.examples && diagram.examples.length > 0);
|
||||
};
|
||||
|
||||
export const getSampleDiagrams = () => {
|
||||
const diagrams = diagramData
|
||||
.filter((d) => isValidDiagram(d))
|
||||
.map(({ examples, ...rest }) => ({
|
||||
...rest,
|
||||
example: examples?.filter(({ isDefault }) => isDefault)[0],
|
||||
}));
|
||||
const examples: Record<string, string> = {};
|
||||
for (const diagram of diagrams) {
|
||||
examples[diagram.name.replace(/ (Diagram|Chart|Graph)/, '')] = diagram.example.code;
|
||||
}
|
||||
return examples;
|
||||
};
|
||||
```
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mermaid-js/examples",
|
||||
"version": "0.0.1-beta.1",
|
||||
"version": "1.0.0",
|
||||
"description": "Mermaid examples package",
|
||||
"author": "Sidharth Vinod",
|
||||
"type": "module",
|
||||
@@ -16,6 +16,10 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
|
@@ -1 +0,0 @@
|
||||
../mermaid/src/docs/syntax/zenuml.md
|
384
packages/mermaid-zenuml/README.md
Normal file
384
packages/mermaid-zenuml/README.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# @mermaid-js/mermaid-zenuml
|
||||
|
||||
MermaidJS plugin for ZenUML integration - A powerful sequence diagram rendering engine.
|
||||
|
||||
> A Sequence diagram is an interaction diagram that shows how processes operate with one another and in what order.
|
||||
|
||||
Mermaid can render sequence diagrams with [ZenUML](https://zenuml.com). Note that ZenUML uses a different
|
||||
syntax than the original Sequence Diagram in mermaid.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
BookLibService.Borrow(id) {
|
||||
User = Session.GetUser()
|
||||
if(User.isActive) {
|
||||
try {
|
||||
BookRepository.Update(id, onLoan, User)
|
||||
receipt = new Receipt(id, dueDate)
|
||||
} catch (BookNotFoundException) {
|
||||
ErrorService.onException(BookNotFoundException)
|
||||
} finally {
|
||||
Connection.close()
|
||||
}
|
||||
}
|
||||
return receipt
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### With bundlers
|
||||
|
||||
```sh
|
||||
npm install @mermaid-js/mermaid-zenuml
|
||||
```
|
||||
|
||||
```ts
|
||||
import mermaid from 'mermaid';
|
||||
import zenuml from '@mermaid-js/mermaid-zenuml';
|
||||
|
||||
await mermaid.registerExternalDiagrams([zenuml]);
|
||||
```
|
||||
|
||||
### With CDN
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||
import zenuml from 'https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-zenuml@0.2.0/dist/mermaid-zenuml.core.mjs';
|
||||
await mermaid.registerExternalDiagrams([zenuml]);
|
||||
</script>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> ZenUML uses experimental lazy loading & async rendering features which could change in the future.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Once the plugin is registered, you can create ZenUML diagrams using the `zenuml` syntax:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
Controller.Get(id) {
|
||||
Service.Get(id) {
|
||||
item = Repository.Get(id)
|
||||
if(item) {
|
||||
return item
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
## ZenUML Syntax Reference
|
||||
|
||||
### Participants
|
||||
|
||||
The participants can be defined implicitly as in the first example on this page. The participants or actors are
|
||||
rendered in order of appearance in the diagram source text. Sometimes you might want to show the participants in a
|
||||
different order than how they appear in the first message. It is possible to specify the actor's order of
|
||||
appearance by doing the following:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Declare participant (optional)
|
||||
Bob
|
||||
Alice
|
||||
Alice->Bob: Hi Bob
|
||||
Bob->Alice: Hi Alice
|
||||
```
|
||||
|
||||
### Annotators
|
||||
|
||||
If you specifically want to use symbols instead of just rectangles with text you can do so by using the annotator syntax to declare participants as per below.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Annotators
|
||||
@Actor Alice
|
||||
@Database Bob
|
||||
Alice->Bob: Hi Bob
|
||||
Bob->Alice: Hi Alice
|
||||
```
|
||||
|
||||
Available annotators include:
|
||||
|
||||
- `@Actor` - Human figure
|
||||
- `@Database` - Database symbol
|
||||
- `@Boundary` - Boundary symbol
|
||||
- `@Control` - Control symbol
|
||||
- `@Entity` - Entity symbol
|
||||
- `@Queue` - Queue symbol
|
||||
|
||||
### Aliases
|
||||
|
||||
The participants can have a convenient identifier and a descriptive label.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Aliases
|
||||
A as Alice
|
||||
J as John
|
||||
A->J: Hello John, how are you?
|
||||
J->A: Great!
|
||||
```
|
||||
|
||||
## Messages
|
||||
|
||||
Messages can be one of:
|
||||
|
||||
1. Sync message
|
||||
2. Async message
|
||||
3. Creation message
|
||||
4. Reply message
|
||||
|
||||
### Sync message
|
||||
|
||||
You can think of a sync (blocking) method in a programming language.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Sync message
|
||||
A.SyncMessage
|
||||
A.SyncMessage(with, parameters) {
|
||||
B.nestedSyncMessage()
|
||||
}
|
||||
```
|
||||
|
||||
### Async message
|
||||
|
||||
You can think of an async (non-blocking) method in a programming language. Fire an event and forget about it.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Async message
|
||||
Alice->Bob: How are you?
|
||||
```
|
||||
|
||||
### Creation message
|
||||
|
||||
We use `new` keyword to create an object.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
new A1
|
||||
new A2(with, parameters)
|
||||
```
|
||||
|
||||
### Reply message
|
||||
|
||||
There are three ways to express a reply message:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
// 1. assign a variable from a sync message.
|
||||
a = A.SyncMessage()
|
||||
|
||||
// 1.1. optionally give the variable a type
|
||||
SomeType a = A.SyncMessage()
|
||||
|
||||
// 2. use return keyword
|
||||
A.SyncMessage() {
|
||||
return result
|
||||
}
|
||||
|
||||
// 3. use @return or @reply annotator on an async message
|
||||
@return
|
||||
A->B: result
|
||||
```
|
||||
|
||||
The third way `@return` is rarely used, but it is useful when you want to return to one level up.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Reply message
|
||||
Client->A.method() {
|
||||
B.method() {
|
||||
if(condition) {
|
||||
return x1
|
||||
// return early
|
||||
@return
|
||||
A->Client: x11
|
||||
}
|
||||
}
|
||||
return x2
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Nesting
|
||||
|
||||
Sync messages and Creation messages are naturally nestable with `{}`.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
A.method() {
|
||||
B.nested_sync_method()
|
||||
B->C: nested async message
|
||||
}
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
It is possible to add comments to a sequence diagram with `// comment` syntax.
|
||||
Comments will be rendered above the messages or fragments. Comments on other places
|
||||
are ignored. Markdown is supported.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
// a comment on a participant will not be rendered
|
||||
BookService
|
||||
// a comment on a message.
|
||||
// **Markdown** is supported.
|
||||
BookService.getBook()
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
It is possible to express loops in a ZenUML diagram. This is done by any of the
|
||||
following notations:
|
||||
|
||||
1. while
|
||||
2. for
|
||||
3. forEach, foreach
|
||||
4. loop
|
||||
|
||||
```zenuml
|
||||
while(condition) {
|
||||
...statements...
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
Alice->John: Hello John, how are you?
|
||||
while(true) {
|
||||
John->Alice: Great!
|
||||
}
|
||||
```
|
||||
|
||||
### Alt (Alternative paths)
|
||||
|
||||
It is possible to express alternative paths in a sequence diagram. This is done by the notation
|
||||
|
||||
```zenuml
|
||||
if(condition1) {
|
||||
...statements...
|
||||
} else if(condition2) {
|
||||
...statements...
|
||||
} else {
|
||||
...statements...
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
Alice->Bob: Hello Bob, how are you?
|
||||
if(is_sick) {
|
||||
Bob->Alice: Not so good :(
|
||||
} else {
|
||||
Bob->Alice: Feeling fresh like a daisy
|
||||
}
|
||||
```
|
||||
|
||||
### Opt (Optional)
|
||||
|
||||
It is possible to render an `opt` fragment. This is done by the notation
|
||||
|
||||
```zenuml
|
||||
opt {
|
||||
...statements...
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
Alice->Bob: Hello Bob, how are you?
|
||||
Bob->Alice: Not so good :(
|
||||
opt {
|
||||
Bob->Alice: Thanks for asking
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel
|
||||
|
||||
It is possible to show actions that are happening in parallel.
|
||||
|
||||
This is done by the notation
|
||||
|
||||
```zenuml
|
||||
par {
|
||||
statement1
|
||||
statement2
|
||||
statement3
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
par {
|
||||
Alice->Bob: Hello guys!
|
||||
Alice->John: Hello guys!
|
||||
}
|
||||
```
|
||||
|
||||
### Try/Catch/Finally (Break)
|
||||
|
||||
It is possible to indicate a stop of the sequence within the flow (usually used to model exceptions).
|
||||
|
||||
This is done by the notation
|
||||
|
||||
```
|
||||
try {
|
||||
...statements...
|
||||
} catch {
|
||||
...statements...
|
||||
} finally {
|
||||
...statements...
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
try {
|
||||
Consumer->API: Book something
|
||||
API->BookingService: Start booking process
|
||||
} catch {
|
||||
API->Consumer: show failure
|
||||
} finally {
|
||||
API->BookingService: rollback status
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
This package is part of the [Mermaid](https://github.com/mermaid-js/mermaid) project. See the main repository for contributing guidelines.
|
||||
|
||||
## Contributors
|
||||
|
||||
- [Peng Xiao](https://github.com/MrCoder)
|
||||
- [Sidharth Vinod](https://sidharth.dev)
|
||||
- [Dong Cai](https://github.com/dontry)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Links
|
||||
|
||||
- [ZenUML Official Website](https://zenuml.com)
|
||||
- [Mermaid Documentation](https://mermaid.js.org)
|
||||
- [GitHub Repository](https://github.com/mermaid-js/mermaid)
|
@@ -33,7 +33,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zenuml/core": "^3.31.1"
|
||||
"@zenuml/core": "^3.35.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
|
11
packages/mermaid-zenuml/src/zenuml.d.ts
vendored
Normal file
11
packages/mermaid-zenuml/src/zenuml.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
declare module '@zenuml/core' {
|
||||
interface RenderOptions {
|
||||
theme?: string;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export default class ZenUml {
|
||||
constructor(container: Element);
|
||||
render(text: string, options?: RenderOptions): Promise<void>;
|
||||
}
|
||||
}
|
@@ -53,7 +53,6 @@ export const draw = async function (text: string, id: string) {
|
||||
|
||||
const { foreignObject, container, app } = createForeignObject(id);
|
||||
svgContainer.appendChild(foreignObject);
|
||||
// @ts-expect-error @zenuml/core@3.0.0 exports the wrong type for ZenUml
|
||||
const zenuml = new ZenUml(app);
|
||||
// default is a theme name. More themes to be added and will be configurable in the future
|
||||
await zenuml.render(text, { theme: 'default', mode: 'static' });
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mermaid",
|
||||
"version": "11.8.1",
|
||||
"version": "11.9.0",
|
||||
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
|
||||
"type": "module",
|
||||
"module": "./dist/mermaid.core.mjs",
|
||||
@@ -79,7 +79,7 @@
|
||||
"dagre-d3-es": "7.0.11",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.9",
|
||||
"katex": "^0.16.22",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^16.0.0",
|
||||
@@ -105,13 +105,14 @@
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"ajv": "^8.17.1",
|
||||
"chokidar": "^3.6.0",
|
||||
"canvas": "^3.1.0",
|
||||
"chokidar": "3.6.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"csstree-validator": "^4.0.1",
|
||||
"globby": "^14.0.2",
|
||||
"jison": "^0.4.18",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"json-schema-to-typescript": "^15.0.4",
|
||||
"micromatch": "^4.0.8",
|
||||
"path-browserify": "^1.0.1",
|
||||
|
@@ -1,28 +1,25 @@
|
||||
import { MockedD3 } from './tests/MockedD3.js';
|
||||
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
|
||||
import type { D3Element } from './types.js';
|
||||
import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js';
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
import { expect } from 'vitest';
|
||||
|
||||
describe('accessibility', () => {
|
||||
const fauxSvgNode: MockedD3 = new MockedD3();
|
||||
|
||||
describe('setA11yDiagramInfo', () => {
|
||||
it('should set svg element role to "graphics-document document"', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('role', 'graphics-document document');
|
||||
jsdomIt('should set svg element role to "graphics-document document"', ({ svg }) => {
|
||||
setA11yDiagramInfo(svg, 'flowchart');
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('role')).toBe('graphics-document document');
|
||||
});
|
||||
|
||||
it('should set aria-roledescription to the diagram type', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart');
|
||||
jsdomIt('should set aria-roledescription to the diagram type', ({ svg }) => {
|
||||
setA11yDiagramInfo(svg, 'flowchart');
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-roledescription')).toBe('flowchart');
|
||||
});
|
||||
|
||||
it('should not set aria-roledescription if the diagram type is empty', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, '');
|
||||
expect(svgAttrSpy).toHaveBeenCalledTimes(1);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('role', expect.anything()); // only called to set the role
|
||||
jsdomIt('should not set aria-roledescription if the diagram type is empty', ({ svg }) => {
|
||||
setA11yDiagramInfo(svg, '');
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-roledescription')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,115 +36,78 @@ describe('accessibility', () => {
|
||||
expect(noInsertAttrSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// convenience functions to DRY up the spec
|
||||
|
||||
function expectAriaLabelledByItTitleId(
|
||||
svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string
|
||||
): void {
|
||||
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
|
||||
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`);
|
||||
}
|
||||
|
||||
function expectAriaDescribedByItDescId(
|
||||
svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string
|
||||
): void {
|
||||
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
|
||||
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`);
|
||||
}
|
||||
|
||||
function a11yTitleTagInserted(
|
||||
svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string,
|
||||
callNumber: number
|
||||
): void {
|
||||
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title);
|
||||
}
|
||||
|
||||
function a11yDescTagInserted(
|
||||
svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string,
|
||||
callNumber: number
|
||||
): void {
|
||||
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc);
|
||||
}
|
||||
|
||||
function a11yTagInserted(
|
||||
_svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string,
|
||||
callNumber: number,
|
||||
expectedPrefix: string,
|
||||
expectedText: string | undefined
|
||||
): void {
|
||||
const fauxInsertedD3: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3);
|
||||
const titleAttrSpy = vi.spyOn(fauxInsertedD3, 'attr').mockReturnValue(fauxInsertedD3);
|
||||
const titleTextSpy = vi.spyOn(fauxInsertedD3, 'text');
|
||||
|
||||
addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId);
|
||||
expect(svginsertpy).toHaveBeenCalledWith(expectedPrefix, ':first-child');
|
||||
expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`);
|
||||
expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText);
|
||||
}
|
||||
|
||||
describe('with a11y title', () => {
|
||||
const a11yTitle = 'a11y title';
|
||||
|
||||
describe('with a11y description', () => {
|
||||
const a11yDesc = 'a11y description';
|
||||
|
||||
it('should set aria-labelledby to the title id inserted as a child', () => {
|
||||
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
jsdomIt('should set aria-labelledby to the title id inserted as a child', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-labelledby')).toBe(`chart-title-${givenId}`);
|
||||
});
|
||||
|
||||
it('should set aria-describedby to the description id inserted as a child', () => {
|
||||
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
});
|
||||
jsdomIt(
|
||||
'should set aria-describedby to the description id inserted as a child',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${givenId}`);
|
||||
}
|
||||
);
|
||||
|
||||
it('should insert title tag as the first child with the text set to the accTitle given', () => {
|
||||
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2);
|
||||
});
|
||||
jsdomIt(
|
||||
'should insert title tag as the first child with the text set to the accTitle given',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const titleNode = ensureNodeFromSelector('title', svgNode);
|
||||
expect(titleNode?.innerHTML).toBe(a11yTitle);
|
||||
}
|
||||
);
|
||||
|
||||
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
|
||||
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
jsdomIt(
|
||||
'should insert desc tag as the 2nd child with the text set to accDescription given',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const descNode = ensureNodeFromSelector('desc', svgNode);
|
||||
expect(descNode?.innerHTML).toBe(a11yDesc);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe(`without a11y description`, () => {
|
||||
describe(`without a11y description`, {}, () => {
|
||||
const a11yDesc = undefined;
|
||||
|
||||
it('should set aria-labelledby to the title id inserted as a child', () => {
|
||||
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
jsdomIt('should set aria-labelledby to the title id inserted as a child', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-labelledby')).toBe(`chart-title-${givenId}`);
|
||||
});
|
||||
|
||||
it('should not set aria-describedby', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
|
||||
jsdomIt('should not set aria-describedby', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBeNull();
|
||||
});
|
||||
|
||||
it('should insert title tag as the first child with the text set to the accTitle given', () => {
|
||||
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
jsdomIt(
|
||||
'should insert title tag as the first child with the text set to the accTitle given',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const titleNode = ensureNodeFromSelector('title', svgNode);
|
||||
expect(titleNode?.innerHTML).toBe(a11yTitle);
|
||||
}
|
||||
);
|
||||
|
||||
it('should not insert description tag', () => {
|
||||
const fauxTitle: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
|
||||
jsdomIt('should not insert description tag', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const descNode = svgNode.querySelector('desc');
|
||||
expect(descNode).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -158,55 +118,66 @@ describe('accessibility', () => {
|
||||
describe('with a11y description', () => {
|
||||
const a11yDesc = 'a11y description';
|
||||
|
||||
it('should not set aria-labelledby', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
|
||||
jsdomIt('should not set aria-labelledby', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-labelledby')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not insert title tag', () => {
|
||||
const fauxTitle: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
|
||||
jsdomIt('should not insert title tag', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const titleNode = svgNode.querySelector('title');
|
||||
expect(titleNode).toBeNull();
|
||||
});
|
||||
|
||||
it('should set aria-describedby to the description id inserted as a child', () => {
|
||||
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
});
|
||||
jsdomIt(
|
||||
'should set aria-describedby to the description id inserted as a child',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${givenId}`);
|
||||
}
|
||||
);
|
||||
|
||||
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
|
||||
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
jsdomIt(
|
||||
'should insert desc tag as the 2nd child with the text set to accDescription given',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const descNode = ensureNodeFromSelector('desc', svgNode);
|
||||
expect(descNode?.innerHTML).toBe(a11yDesc);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('without a11y description', () => {
|
||||
const a11yDesc = undefined;
|
||||
|
||||
it('should not set aria-labelledby', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
|
||||
jsdomIt('should not set aria-labelledby', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-labelledby')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not set aria-describedby', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
|
||||
jsdomIt('should not set aria-describedby', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not insert title tag', () => {
|
||||
const fauxTitle: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
|
||||
jsdomIt('should not insert title tag', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const titleNode = svgNode.querySelector('title');
|
||||
expect(titleNode).toBeNull();
|
||||
});
|
||||
|
||||
it('should not insert description tag', () => {
|
||||
const fauxDesc: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
|
||||
jsdomIt('should not insert description tag', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const descNode = svgNode.querySelector('desc');
|
||||
expect(descNode).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -78,5 +78,41 @@ describe('diagram-orchestration', () => {
|
||||
flowchart: 1 "pie" pie: 2 "pie"`)
|
||||
).toBe('pie');
|
||||
});
|
||||
|
||||
it('should detect proper diagram when defaultRenderer is elk for flowchart', () => {
|
||||
expect(
|
||||
detectType('mindmap\n root\n Photograph\n Waterfall', {
|
||||
flowchart: { defaultRenderer: 'elk' },
|
||||
})
|
||||
).toBe('mindmap');
|
||||
expect(
|
||||
detectType(
|
||||
`
|
||||
classDiagram
|
||||
class Person {
|
||||
+String name
|
||||
-Int id
|
||||
#double age
|
||||
+Text demographicProfile
|
||||
}
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
)
|
||||
).toBe('class');
|
||||
expect(
|
||||
detectType(
|
||||
`
|
||||
erDiagram
|
||||
p[Photograph] {
|
||||
varchar(12) jobId
|
||||
date dateCreated
|
||||
}
|
||||
`,
|
||||
{
|
||||
flowchart: { defaultRenderer: 'elk' },
|
||||
}
|
||||
)
|
||||
).toBe('er');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,21 +1,12 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { db } from './architectureDb.js';
|
||||
import { parser } from './architectureParser.js';
|
||||
|
||||
const {
|
||||
clear,
|
||||
getDiagramTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
getServices,
|
||||
getGroups,
|
||||
getEdges,
|
||||
getJunctions,
|
||||
} = db;
|
||||
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
describe('architecture diagrams', () => {
|
||||
let db: ArchitectureDB;
|
||||
beforeEach(() => {
|
||||
clear();
|
||||
db = new ArchitectureDB();
|
||||
// @ts-expect-error since type is set to undefined we will have error
|
||||
parser.parser?.yy = db;
|
||||
});
|
||||
|
||||
describe('architecture diagram definitions', () => {
|
||||
@@ -36,7 +27,7 @@ describe('architecture diagrams', () => {
|
||||
it('should handle title on the first line', async () => {
|
||||
const str = `architecture-beta title Simple Architecture Diagram`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
});
|
||||
|
||||
it('should handle title on another line', async () => {
|
||||
@@ -44,7 +35,7 @@ describe('architecture diagrams', () => {
|
||||
title Simple Architecture Diagram
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
});
|
||||
|
||||
it('should handle accessibility title and description', async () => {
|
||||
@@ -53,8 +44,8 @@ describe('architecture diagrams', () => {
|
||||
accDescr: Accessibility Description
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getAccTitle()).toBe('Accessibility Title');
|
||||
expect(getAccDescription()).toBe('Accessibility Description');
|
||||
expect(db.getAccTitle()).toBe('Accessibility Title');
|
||||
expect(db.getAccDescription()).toBe('Accessibility Description');
|
||||
});
|
||||
|
||||
it('should handle multiline accessibility description', async () => {
|
||||
@@ -64,7 +55,7 @@ describe('architecture diagrams', () => {
|
||||
}
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getAccDescription()).toBe('Accessibility Description');
|
||||
expect(db.getAccDescription()).toBe('Accessibility Description');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
} from '../common/commonDb.js';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
ArchitectureDB,
|
||||
ArchitectureDirectionPair,
|
||||
ArchitectureDirectionPairMap,
|
||||
ArchitectureEdge,
|
||||
@@ -33,330 +33,333 @@ import {
|
||||
isArchitectureService,
|
||||
shiftPositionByArchitectureDirectionPair,
|
||||
} from './architectureTypes.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
|
||||
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
|
||||
DEFAULT_CONFIG.architecture;
|
||||
export class ArchitectureDB implements DiagramDB {
|
||||
private nodes: Record<string, ArchitectureNode> = {};
|
||||
private groups: Record<string, ArchitectureGroup> = {};
|
||||
private edges: ArchitectureEdge[] = [];
|
||||
private registeredIds: Record<string, 'node' | 'group'> = {};
|
||||
private dataStructures?: ArchitectureState['dataStructures'];
|
||||
private elements: Record<string, D3Element> = {};
|
||||
|
||||
const state = new ImperativeState<ArchitectureState>(() => ({
|
||||
nodes: {},
|
||||
groups: {},
|
||||
edges: [],
|
||||
registeredIds: {},
|
||||
config: DEFAULT_ARCHITECTURE_CONFIG,
|
||||
dataStructures: undefined,
|
||||
elements: {},
|
||||
}));
|
||||
|
||||
const clear = (): void => {
|
||||
state.reset();
|
||||
commonClear();
|
||||
};
|
||||
|
||||
const addService = function ({
|
||||
id,
|
||||
icon,
|
||||
in: parent,
|
||||
title,
|
||||
iconText,
|
||||
}: Omit<ArchitectureService, 'edges'>) {
|
||||
if (state.records.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The service id [${id}] is already in use by another ${state.records.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The service [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
|
||||
);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The service [${id}]'s parent is not a group`);
|
||||
}
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
state.records.registeredIds[id] = 'node';
|
||||
public clear(): void {
|
||||
this.nodes = {};
|
||||
this.groups = {};
|
||||
this.edges = [];
|
||||
this.registeredIds = {};
|
||||
this.dataStructures = undefined;
|
||||
this.elements = {};
|
||||
commonClear();
|
||||
}
|
||||
|
||||
state.records.nodes[id] = {
|
||||
public addService({
|
||||
id,
|
||||
type: 'service',
|
||||
icon,
|
||||
in: parent,
|
||||
title,
|
||||
iconText,
|
||||
title,
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
|
||||
const getServices = (): ArchitectureService[] =>
|
||||
Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService);
|
||||
|
||||
const addJunction = function ({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>) {
|
||||
state.records.registeredIds[id] = 'node';
|
||||
|
||||
state.records.nodes[id] = {
|
||||
id,
|
||||
type: 'junction',
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
|
||||
const getJunctions = (): ArchitectureJunction[] =>
|
||||
Object.values(state.records.nodes).filter<ArchitectureJunction>(isArchitectureJunction);
|
||||
|
||||
const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes);
|
||||
|
||||
const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id];
|
||||
|
||||
const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) {
|
||||
if (state.records.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group id [${id}] is already in use by another ${state.records.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The group [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === undefined) {
|
||||
}: Omit<ArchitectureService, 'edges'>): void {
|
||||
if (this.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
|
||||
`The service id [${id}] is already in use by another ${this.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The group [${id}]'s parent is not a group`);
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The service [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (this.registeredIds[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
|
||||
);
|
||||
}
|
||||
if (this.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The service [${id}]'s parent is not a group`);
|
||||
}
|
||||
}
|
||||
|
||||
this.registeredIds[id] = 'node';
|
||||
|
||||
this.nodes[id] = {
|
||||
id,
|
||||
type: 'service',
|
||||
icon,
|
||||
iconText,
|
||||
title,
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
|
||||
state.records.registeredIds[id] = 'group';
|
||||
|
||||
state.records.groups[id] = {
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
const getGroups = (): ArchitectureGroup[] => {
|
||||
return Object.values(state.records.groups);
|
||||
};
|
||||
|
||||
const addEdge = function ({
|
||||
lhsId,
|
||||
rhsId,
|
||||
lhsDir,
|
||||
rhsDir,
|
||||
lhsInto,
|
||||
rhsInto,
|
||||
lhsGroup,
|
||||
rhsGroup,
|
||||
title,
|
||||
}: ArchitectureEdge<string>) {
|
||||
if (!isArchitectureDirection(lhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}`
|
||||
);
|
||||
}
|
||||
if (!isArchitectureDirection(rhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}`
|
||||
);
|
||||
public getServices(): ArchitectureService[] {
|
||||
return Object.values(this.nodes).filter(isArchitectureService);
|
||||
}
|
||||
|
||||
if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
public addJunction({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>): void {
|
||||
this.registeredIds[id] = 'node';
|
||||
|
||||
this.nodes[id] = {
|
||||
id,
|
||||
type: 'junction',
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
|
||||
const lhsGroupId = state.records.nodes[lhsId].in;
|
||||
const rhsGroupId = state.records.nodes[rhsId].in;
|
||||
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
public getJunctions(): ArchitectureJunction[] {
|
||||
return Object.values(this.nodes).filter(isArchitectureJunction);
|
||||
}
|
||||
|
||||
const edge = {
|
||||
public getNodes(): ArchitectureNode[] {
|
||||
return Object.values(this.nodes);
|
||||
}
|
||||
|
||||
public getNode(id: string): ArchitectureNode | null {
|
||||
return this.nodes[id] ?? null;
|
||||
}
|
||||
|
||||
public addGroup({ id, icon, in: parent, title }: ArchitectureGroup): void {
|
||||
if (this.registeredIds?.[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group id [${id}] is already in use by another ${this.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The group [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (this.registeredIds?.[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
|
||||
);
|
||||
}
|
||||
if (this.registeredIds?.[parent] === 'node') {
|
||||
throw new Error(`The group [${id}]'s parent is not a group`);
|
||||
}
|
||||
}
|
||||
|
||||
this.registeredIds[id] = 'group';
|
||||
|
||||
this.groups[id] = {
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
public getGroups(): ArchitectureGroup[] {
|
||||
return Object.values(this.groups);
|
||||
}
|
||||
public addEdge({
|
||||
lhsId,
|
||||
lhsDir,
|
||||
lhsInto,
|
||||
lhsGroup,
|
||||
rhsId,
|
||||
lhsDir,
|
||||
rhsDir,
|
||||
lhsInto,
|
||||
rhsInto,
|
||||
lhsGroup,
|
||||
rhsGroup,
|
||||
title,
|
||||
};
|
||||
|
||||
state.records.edges.push(edge);
|
||||
if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) {
|
||||
state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
const getEdges = (): ArchitectureEdge[] => state.records.edges;
|
||||
|
||||
/**
|
||||
* Returns the current diagram's adjacency list, spatial map, & group alignments.
|
||||
* If they have not been created, run the algorithms to generate them.
|
||||
* @returns
|
||||
*/
|
||||
const getDataStructures = () => {
|
||||
if (state.records.dataStructures === undefined) {
|
||||
// Tracks how groups are aligned with one another. Generated while creating the adj list
|
||||
const groupAlignments: Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
> = {};
|
||||
|
||||
// Create an adjacency list of the diagram to perform BFS on
|
||||
// Outer reduce applied on all services
|
||||
// Inner reduce applied on the edges for a service
|
||||
const adjList = Object.entries(state.records.nodes).reduce<
|
||||
Record<string, ArchitectureDirectionPairMap>
|
||||
>((prevOuter, [id, service]) => {
|
||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
||||
// track the direction groups connect to one another
|
||||
const lhsGroupId = getNode(edge.lhsId)?.in;
|
||||
const rhsGroupId = getNode(edge.rhsId)?.in;
|
||||
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
|
||||
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
|
||||
if (alignment !== 'bend') {
|
||||
groupAlignments[lhsGroupId] ??= {};
|
||||
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
|
||||
groupAlignments[rhsGroupId] ??= {};
|
||||
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
|
||||
}
|
||||
}
|
||||
|
||||
if (edge.lhsId === id) {
|
||||
// source is LHS
|
||||
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.rhsId;
|
||||
}
|
||||
} else {
|
||||
// source is RHS
|
||||
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.lhsId;
|
||||
}
|
||||
}
|
||||
return prevInner;
|
||||
}, {});
|
||||
return prevOuter;
|
||||
}, {});
|
||||
|
||||
// Configuration for the initial pass of BFS
|
||||
const firstId = Object.keys(adjList)[0];
|
||||
const visited = { [firstId]: 1 };
|
||||
// If a key is present in this object, it has not been visited
|
||||
const notVisited = Object.keys(adjList).reduce(
|
||||
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
// Perform BFS on the adjacency list
|
||||
const BFS = (startingId: string): ArchitectureSpatialMap => {
|
||||
const spatialMap = { [startingId]: [0, 0] };
|
||||
const queue = [startingId];
|
||||
while (queue.length > 0) {
|
||||
const id = queue.shift();
|
||||
if (id) {
|
||||
visited[id] = 1;
|
||||
delete notVisited[id];
|
||||
const adj = adjList[id];
|
||||
const [posX, posY] = spatialMap[id];
|
||||
Object.entries(adj).forEach(([dir, rhsId]) => {
|
||||
if (!visited[rhsId]) {
|
||||
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
|
||||
[posX, posY],
|
||||
dir as ArchitectureDirectionPair
|
||||
);
|
||||
queue.push(rhsId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return spatialMap;
|
||||
};
|
||||
const spatialMaps = [BFS(firstId)];
|
||||
|
||||
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
|
||||
while (Object.keys(notVisited).length > 0) {
|
||||
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
|
||||
}: ArchitectureEdge): void {
|
||||
if (!isArchitectureDirection(lhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(lhsDir)}`
|
||||
);
|
||||
}
|
||||
state.records.dataStructures = {
|
||||
adjList,
|
||||
spatialMaps,
|
||||
groupAlignments,
|
||||
if (!isArchitectureDirection(rhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(rhsDir)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.nodes[lhsId] === undefined && this.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
if (this.nodes[rhsId] === undefined && this.groups[rhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
|
||||
const lhsGroupId = this.nodes[lhsId].in;
|
||||
const rhsGroupId = this.nodes[rhsId].in;
|
||||
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
|
||||
const edge = {
|
||||
lhsId,
|
||||
lhsDir,
|
||||
lhsInto,
|
||||
lhsGroup,
|
||||
rhsId,
|
||||
rhsDir,
|
||||
rhsInto,
|
||||
rhsGroup,
|
||||
title,
|
||||
};
|
||||
|
||||
this.edges.push(edge);
|
||||
if (this.nodes[lhsId] && this.nodes[rhsId]) {
|
||||
this.nodes[lhsId].edges.push(this.edges[this.edges.length - 1]);
|
||||
this.nodes[rhsId].edges.push(this.edges[this.edges.length - 1]);
|
||||
}
|
||||
}
|
||||
return state.records.dataStructures;
|
||||
};
|
||||
|
||||
const setElementForId = (id: string, element: D3Element) => {
|
||||
state.records.elements[id] = element;
|
||||
};
|
||||
const getElementById = (id: string) => state.records.elements[id];
|
||||
public getEdges(): ArchitectureEdge[] {
|
||||
return this.edges;
|
||||
}
|
||||
|
||||
const getConfig = (): Required<ArchitectureDiagramConfig> => {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_ARCHITECTURE_CONFIG,
|
||||
...commonGetConfig().architecture,
|
||||
});
|
||||
return config;
|
||||
};
|
||||
/**
|
||||
* Returns the current diagram's adjacency list, spatial map, & group alignments.
|
||||
* If they have not been created, run the algorithms to generate them.
|
||||
* @returns
|
||||
*/
|
||||
public getDataStructures() {
|
||||
if (this.dataStructures === undefined) {
|
||||
// Tracks how groups are aligned with one another. Generated while creating the adj list
|
||||
const groupAlignments: Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
> = {};
|
||||
|
||||
export const db: ArchitectureDB = {
|
||||
clear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
getConfig,
|
||||
// Create an adjacency list of the diagram to perform BFS on
|
||||
// Outer reduce applied on all services
|
||||
// Inner reduce applied on the edges for a service
|
||||
const adjList = Object.entries(this.nodes).reduce<
|
||||
Record<string, ArchitectureDirectionPairMap>
|
||||
>((prevOuter, [id, service]) => {
|
||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
||||
// track the direction groups connect to one another
|
||||
const lhsGroupId = this.getNode(edge.lhsId)?.in;
|
||||
const rhsGroupId = this.getNode(edge.rhsId)?.in;
|
||||
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
|
||||
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
|
||||
if (alignment !== 'bend') {
|
||||
groupAlignments[lhsGroupId] ??= {};
|
||||
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
|
||||
groupAlignments[rhsGroupId] ??= {};
|
||||
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
|
||||
}
|
||||
}
|
||||
|
||||
addService,
|
||||
getServices,
|
||||
addJunction,
|
||||
getJunctions,
|
||||
getNodes,
|
||||
getNode,
|
||||
addGroup,
|
||||
getGroups,
|
||||
addEdge,
|
||||
getEdges,
|
||||
setElementForId,
|
||||
getElementById,
|
||||
getDataStructures,
|
||||
};
|
||||
if (edge.lhsId === id) {
|
||||
// source is LHS
|
||||
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.rhsId;
|
||||
}
|
||||
} else {
|
||||
// source is RHS
|
||||
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.lhsId;
|
||||
}
|
||||
}
|
||||
return prevInner;
|
||||
}, {});
|
||||
return prevOuter;
|
||||
}, {});
|
||||
|
||||
// Configuration for the initial pass of BFS
|
||||
const firstId = Object.keys(adjList)[0];
|
||||
const visited = { [firstId]: 1 };
|
||||
// If a key is present in this object, it has not been visited
|
||||
const notVisited = Object.keys(adjList).reduce(
|
||||
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
// Perform BFS on the adjacency list
|
||||
const BFS = (startingId: string): ArchitectureSpatialMap => {
|
||||
const spatialMap = { [startingId]: [0, 0] };
|
||||
const queue = [startingId];
|
||||
while (queue.length > 0) {
|
||||
const id = queue.shift();
|
||||
if (id) {
|
||||
visited[id] = 1;
|
||||
delete notVisited[id];
|
||||
const adj = adjList[id];
|
||||
const [posX, posY] = spatialMap[id];
|
||||
Object.entries(adj).forEach(([dir, rhsId]) => {
|
||||
if (!visited[rhsId]) {
|
||||
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
|
||||
[posX, posY],
|
||||
dir as ArchitectureDirectionPair
|
||||
);
|
||||
queue.push(rhsId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return spatialMap;
|
||||
};
|
||||
const spatialMaps = [BFS(firstId)];
|
||||
|
||||
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
|
||||
while (Object.keys(notVisited).length > 0) {
|
||||
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
|
||||
}
|
||||
this.dataStructures = {
|
||||
adjList,
|
||||
spatialMaps,
|
||||
groupAlignments,
|
||||
};
|
||||
}
|
||||
return this.dataStructures;
|
||||
}
|
||||
|
||||
public setElementForId(id: string, element: D3Element): void {
|
||||
this.elements[id] = element;
|
||||
}
|
||||
|
||||
public getElementById(id: string): D3Element {
|
||||
return this.elements[id];
|
||||
}
|
||||
|
||||
public getConfig(): Required<ArchitectureDiagramConfig> {
|
||||
return cleanAndMerge({
|
||||
...DEFAULT_ARCHITECTURE_CONFIG,
|
||||
...commonGetConfig().architecture,
|
||||
});
|
||||
}
|
||||
|
||||
public getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
field: T
|
||||
): Required<ArchitectureDiagramConfig>[T] {
|
||||
return this.getConfig()[field];
|
||||
}
|
||||
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setAccDescription = setAccDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined
|
||||
* @param field - the config field to access
|
||||
* @returns
|
||||
*/
|
||||
export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
field: T
|
||||
): Required<ArchitectureDiagramConfig>[T] {
|
||||
return getConfig()[field];
|
||||
}
|
||||
// export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
// field: T
|
||||
// ): Required<ArchitectureDiagramConfig>[T] {
|
||||
// return db.getConfig()[field];
|
||||
// }
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { parser } from './architectureParser.js';
|
||||
import { db } from './architectureDb.js';
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
import styles from './architectureStyles.js';
|
||||
import { renderer } from './architectureRenderer.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new ArchitectureDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
@@ -1,24 +1,33 @@
|
||||
import type { Architecture } from '@mermaid-js/parser';
|
||||
import { parse } from '@mermaid-js/parser';
|
||||
import { log } from '../../logger.js';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import type { ArchitectureDB } from './architectureTypes.js';
|
||||
import { db } from './architectureDb.js';
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
|
||||
const populateDb = (ast: Architecture, db: ArchitectureDB) => {
|
||||
populateCommonDb(ast, db);
|
||||
ast.groups.map(db.addGroup);
|
||||
ast.groups.map((group) => db.addGroup(group));
|
||||
ast.services.map((service) => db.addService({ ...service, type: 'service' }));
|
||||
ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' }));
|
||||
// @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type?
|
||||
ast.edges.map(db.addEdge);
|
||||
ast.edges.map((edge) => db.addEdge(edge));
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parser: {
|
||||
// @ts-expect-error - ArchitectureDB is not assignable to DiagramDB
|
||||
yy: undefined,
|
||||
},
|
||||
parse: async (input: string): Promise<void> => {
|
||||
const ast: Architecture = await parse('architecture', input);
|
||||
log.debug(ast);
|
||||
const db = parser.parser?.yy;
|
||||
if (!(db instanceof ArchitectureDB)) {
|
||||
throw new Error(
|
||||
'parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||
);
|
||||
}
|
||||
populateDb(ast, db);
|
||||
},
|
||||
};
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { registerIconPacks } from '../../rendering-util/icons.js';
|
||||
import type { Position } from 'cytoscape';
|
||||
import cytoscape from 'cytoscape';
|
||||
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
|
||||
@@ -7,9 +6,10 @@ import { select } from 'd3';
|
||||
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { registerIconPacks } from '../../rendering-util/icons.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import { getConfigField } from './architectureDb.js';
|
||||
import type { ArchitectureDB } from './architectureDb.js';
|
||||
import { architectureIcons } from './architectureIcons.js';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
@@ -22,7 +22,6 @@ import type {
|
||||
NodeSingularData,
|
||||
} from './architectureTypes.js';
|
||||
import {
|
||||
type ArchitectureDB,
|
||||
type ArchitectureDirection,
|
||||
type ArchitectureEdge,
|
||||
type ArchitectureGroup,
|
||||
@@ -44,7 +43,7 @@ registerIconPacks([
|
||||
]);
|
||||
cytoscape.use(fcose);
|
||||
|
||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
|
||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||
services.forEach((service) => {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
@@ -54,15 +53,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
|
||||
icon: service.icon,
|
||||
label: service.title,
|
||||
parent: service.in,
|
||||
width: getConfigField('iconSize'),
|
||||
height: getConfigField('iconSize'),
|
||||
width: db.getConfigField('iconSize'),
|
||||
height: db.getConfigField('iconSize'),
|
||||
} as NodeSingularData,
|
||||
classes: 'node-service',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
|
||||
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||
junctions.forEach((junction) => {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
@@ -70,8 +69,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
|
||||
type: 'junction',
|
||||
id: junction.id,
|
||||
parent: junction.in,
|
||||
width: getConfigField('iconSize'),
|
||||
height: getConfigField('iconSize'),
|
||||
width: db.getConfigField('iconSize'),
|
||||
height: db.getConfigField('iconSize'),
|
||||
} as NodeSingularData,
|
||||
classes: 'node-junction',
|
||||
});
|
||||
@@ -257,7 +256,8 @@ function getAlignments(
|
||||
}
|
||||
|
||||
function getRelativeConstraints(
|
||||
spatialMaps: ArchitectureSpatialMap[]
|
||||
spatialMaps: ArchitectureSpatialMap[],
|
||||
db: ArchitectureDB
|
||||
): fcose.FcoseRelativePlacementConstraint[] {
|
||||
const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [];
|
||||
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
|
||||
@@ -296,7 +296,7 @@ function getRelativeConstraints(
|
||||
[ArchitectureDirectionName[
|
||||
getOppositeArchitectureDirection(dir as ArchitectureDirection)
|
||||
]]: currId,
|
||||
gap: 1.5 * getConfigField('iconSize'),
|
||||
gap: 1.5 * db.getConfigField('iconSize'),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -353,7 +353,7 @@ function layoutArchitecture(
|
||||
style: {
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'font-size': `${getConfigField('fontSize')}px`,
|
||||
'font-size': `${db.getConfigField('fontSize')}px`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -375,23 +375,32 @@ function layoutArchitecture(
|
||||
selector: '.node-group',
|
||||
style: {
|
||||
// @ts-ignore Incorrect library types
|
||||
padding: `${getConfigField('padding')}px`,
|
||||
padding: `${db.getConfigField('padding')}px`,
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
name: 'grid',
|
||||
boundingBox: {
|
||||
x1: 0,
|
||||
x2: 100,
|
||||
y1: 0,
|
||||
y2: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
// Remove element after layout
|
||||
renderEl.remove();
|
||||
|
||||
addGroups(groups, cy);
|
||||
addServices(services, cy);
|
||||
addJunctions(junctions, cy);
|
||||
addServices(services, cy, db);
|
||||
addJunctions(junctions, cy, db);
|
||||
addEdges(edges, cy);
|
||||
// Use the spatial map to create alignment arrays for fcose
|
||||
const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments);
|
||||
|
||||
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
|
||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
|
||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db);
|
||||
|
||||
const layout = cy.layout({
|
||||
name: 'fcose',
|
||||
@@ -406,7 +415,9 @@ function layoutArchitecture(
|
||||
const { parent: parentA } = nodeData(nodeA);
|
||||
const { parent: parentB } = nodeData(nodeB);
|
||||
const elasticity =
|
||||
parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize');
|
||||
parentA === parentB
|
||||
? 1.5 * db.getConfigField('iconSize')
|
||||
: 0.5 * db.getConfigField('iconSize');
|
||||
return elasticity;
|
||||
},
|
||||
edgeElasticity(edge: EdgeSingular) {
|
||||
@@ -526,11 +537,11 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
||||
|
||||
const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);
|
||||
|
||||
await drawEdges(edgesElem, cy);
|
||||
await drawGroups(groupElem, cy);
|
||||
await drawEdges(edgesElem, cy, db);
|
||||
await drawGroups(groupElem, cy, db);
|
||||
positionNodes(db, cy);
|
||||
|
||||
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth'));
|
||||
setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth'));
|
||||
};
|
||||
|
||||
export const renderer = { draw };
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
||||
import type cytoscape from 'cytoscape';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { db, getConfigField } from './architectureDb.js';
|
||||
import type { ArchitectureDB } from './architectureDb.js';
|
||||
import { architectureIcons } from './architectureIcons.js';
|
||||
import {
|
||||
ArchitectureDirectionArrow,
|
||||
@@ -16,14 +16,17 @@ import {
|
||||
isArchitectureDirectionY,
|
||||
isArchitecturePairXY,
|
||||
nodeData,
|
||||
type ArchitectureDB,
|
||||
type ArchitectureJunction,
|
||||
type ArchitectureService,
|
||||
} from './architectureTypes.js';
|
||||
|
||||
export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) {
|
||||
const padding = getConfigField('padding');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
export const drawEdges = async function (
|
||||
edgesEl: D3Element,
|
||||
cy: cytoscape.Core,
|
||||
db: ArchitectureDB
|
||||
) {
|
||||
const padding = db.getConfigField('padding');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
const halfIconSize = iconSize / 2;
|
||||
const arrowSize = iconSize / 6;
|
||||
const halfArrowSize = arrowSize / 2;
|
||||
@@ -183,13 +186,17 @@ export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core)
|
||||
);
|
||||
};
|
||||
|
||||
export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) {
|
||||
const padding = getConfigField('padding');
|
||||
export const drawGroups = async function (
|
||||
groupsEl: D3Element,
|
||||
cy: cytoscape.Core,
|
||||
db: ArchitectureDB
|
||||
) {
|
||||
const padding = db.getConfigField('padding');
|
||||
const groupIconSize = padding * 0.75;
|
||||
|
||||
const fontSize = getConfigField('fontSize');
|
||||
const fontSize = db.getConfigField('fontSize');
|
||||
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
const halfIconSize = iconSize / 2;
|
||||
|
||||
await Promise.all(
|
||||
@@ -266,7 +273,7 @@ export const drawServices = async function (
|
||||
): Promise<number> {
|
||||
for (const service of services) {
|
||||
const serviceElem = elem.append('g');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
|
||||
if (service.title) {
|
||||
const textElem = serviceElem.append('g');
|
||||
@@ -350,7 +357,7 @@ export const drawJunctions = function (
|
||||
) {
|
||||
junctions.forEach((junction) => {
|
||||
const junctionElem = elem.append('g');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
|
||||
const bkgElem = junctionElem.append('g');
|
||||
bkgElem
|
||||
|
@@ -92,7 +92,20 @@ export const setCssClass = function (itemIds: string, cssClassName: string) {
|
||||
const populateBlockDatabase = (_blockList: Block[], parent: Block): void => {
|
||||
const blockList = _blockList.flat();
|
||||
const children = [];
|
||||
const columnSettingBlock = blockList.find((b) => b?.type === 'column-setting');
|
||||
const column = columnSettingBlock?.columns ?? -1;
|
||||
for (const block of blockList) {
|
||||
if (
|
||||
typeof column === 'number' &&
|
||||
column > 0 &&
|
||||
block.type !== 'column-setting' &&
|
||||
typeof block.widthInColumns === 'number' &&
|
||||
block.widthInColumns > column
|
||||
) {
|
||||
log.warn(
|
||||
`Block ${block.id} width ${block.widthInColumns} exceeds configured column width ${column}`
|
||||
);
|
||||
}
|
||||
if (block.label) {
|
||||
block.label = sanitizeText(block.label);
|
||||
}
|
||||
|
@@ -270,7 +270,12 @@ function layoutBlocks(block: Block, db: BlockDB) {
|
||||
if (child.children) {
|
||||
layoutBlocks(child, db);
|
||||
}
|
||||
columnPos += child?.widthInColumns ?? 1;
|
||||
let columnsFilled = child?.widthInColumns ?? 1;
|
||||
if (columns > 0) {
|
||||
// Make sure overflowing lines do not affect later lines
|
||||
columnsFilled = Math.min(columnsFilled, columns - (columnPos % columns));
|
||||
}
|
||||
columnPos += columnsFilled;
|
||||
log.debug('abc88 columnsPos', child, columnPos);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// @ts-ignore: jison doesn't export types
|
||||
import block from './block.jison';
|
||||
import db from '../blockDB.js';
|
||||
import { log } from '../../../logger.js';
|
||||
|
||||
describe('Block diagram', function () {
|
||||
describe('when parsing a block diagram graph it should handle > ', function () {
|
||||
@@ -402,6 +403,25 @@ columns 1
|
||||
const B = blocks[0];
|
||||
expect(B.styles).toContain('fill:#f9F');
|
||||
});
|
||||
it('should log a warning when block width exceeds column width', () => {
|
||||
const str = `block-beta
|
||||
columns 1
|
||||
A:1
|
||||
B:2
|
||||
C:3
|
||||
D:4
|
||||
E:3
|
||||
F:2
|
||||
G:1`;
|
||||
|
||||
const logWarnSpy = vi.spyOn(log, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
block.parse(str);
|
||||
|
||||
expect(logWarnSpy).toHaveBeenCalledWith('Block B width 2 exceeds configured column width 1');
|
||||
|
||||
logWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('prototype properties', function () {
|
||||
|
@@ -15,4 +15,12 @@ describe('class diagram', function () {
|
||||
expect(() => parser.parse(`classDiagram\nnamespace ${prop} {\n\tclass A\n}`)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('backtick escaping', function () {
|
||||
it('should handle backtick-quoted namespace names', function () {
|
||||
expect(() =>
|
||||
parser.parse(`classDiagram\nnamespace \`A::B\` {\n\tclass \`IPC::Sender\`\n}`)
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -242,6 +242,7 @@ classLabel
|
||||
|
||||
namespaceName
|
||||
: alphaNumToken { $$=$1; }
|
||||
| classLiteralName { $$=$1; }
|
||||
| alphaNumToken DOT namespaceName { $$=$1+'.'+$3; }
|
||||
| alphaNumToken namespaceName { $$=$1+$2; }
|
||||
;
|
||||
|
@@ -11,7 +11,7 @@ const detector: DiagramDetector = (txt, config = {}): boolean => {
|
||||
// If diagram explicitly states flowchart-elk
|
||||
/^\s*flowchart-elk/.test(txt) ||
|
||||
// If a flowchart/graph diagram has their default renderer set to elk
|
||||
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
|
||||
(/^\s*(flowchart|graph)/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
|
||||
) {
|
||||
config.layout = 'elk';
|
||||
return true;
|
||||
|
@@ -125,4 +125,60 @@ describe('flow db getData', () => {
|
||||
const { edges } = flowDb.getData();
|
||||
expect(edges[0].curve).toBe('basis');
|
||||
});
|
||||
|
||||
it('should support modifying interpolate using edge id syntax', () => {
|
||||
flowDb.addVertex('A', { text: 'A', type: 'text' }, undefined, [], [], '', {}, undefined);
|
||||
flowDb.addVertex('B', { text: 'B', type: 'text' }, undefined, [], [], '', {}, undefined);
|
||||
flowDb.addVertex('C', { text: 'C', type: 'text' }, undefined, [], [], '', {}, undefined);
|
||||
flowDb.addVertex('D', { text: 'D', type: 'text' }, undefined, [], [], '', {}, undefined);
|
||||
flowDb.addLink(['A'], ['B'], {});
|
||||
flowDb.addLink(['A'], ['C'], { id: 'e2' });
|
||||
flowDb.addLink(['B'], ['D'], { id: 'e3' });
|
||||
flowDb.addLink(['C'], ['D'], {});
|
||||
flowDb.updateLinkInterpolate(['default'], 'stepBefore');
|
||||
flowDb.updateLinkInterpolate([0], 'basis');
|
||||
flowDb.addVertex(
|
||||
'e2',
|
||||
{ text: 'Shouldnt be used', type: 'text' },
|
||||
undefined,
|
||||
[],
|
||||
[],
|
||||
'',
|
||||
{},
|
||||
' curve: monotoneX '
|
||||
);
|
||||
flowDb.addVertex(
|
||||
'e3',
|
||||
{ text: 'Shouldnt be used', type: 'text' },
|
||||
undefined,
|
||||
[],
|
||||
[],
|
||||
'',
|
||||
{},
|
||||
' curve: catmullRom '
|
||||
);
|
||||
|
||||
const { edges } = flowDb.getData();
|
||||
expect(edges[0].curve).toBe('basis');
|
||||
expect(edges[1].curve).toBe('monotoneX');
|
||||
expect(edges[2].curve).toBe('catmullRom');
|
||||
expect(edges[3].curve).toBe('stepBefore');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flow db direction', () => {
|
||||
let flowDb: FlowDB;
|
||||
beforeEach(() => {
|
||||
flowDb = new FlowDB();
|
||||
});
|
||||
|
||||
it('should set direction to TB when TD is set', () => {
|
||||
flowDb.setDirection('TD');
|
||||
expect(flowDb.getDirection()).toBe('TB');
|
||||
});
|
||||
|
||||
it('should correctly set direction irrespective of leading spaces', () => {
|
||||
flowDb.setDirection(' TD');
|
||||
expect(flowDb.getDirection()).toBe('TB');
|
||||
});
|
||||
});
|
||||
|
@@ -139,6 +139,9 @@ export class FlowDB implements DiagramDB {
|
||||
if (edgeDoc?.animation !== undefined) {
|
||||
edge.animation = edgeDoc.animation;
|
||||
}
|
||||
if (edgeDoc?.curve !== undefined) {
|
||||
edge.interpolate = edgeDoc.curve;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -403,7 +406,8 @@ You have to call mermaid.initialize.`
|
||||
*
|
||||
*/
|
||||
public setDirection(dir: string) {
|
||||
this.direction = dir;
|
||||
this.direction = dir.trim();
|
||||
|
||||
if (/.*</.exec(this.direction)) {
|
||||
this.direction = 'RL';
|
||||
}
|
||||
|
@@ -37,6 +37,59 @@ describe('[Lines] when parsing', () => {
|
||||
expect(edges[1].interpolate).toBe('cardinal');
|
||||
});
|
||||
|
||||
it('should handle edge curve properties using edge ID', function () {
|
||||
const res = flow.parser.parse(
|
||||
'graph TD\n' +
|
||||
'A e1@-->B\n' +
|
||||
'A uniqueName@-->C\n' +
|
||||
'e1@{curve: basis}\n' +
|
||||
'uniqueName@{curve: cardinal}'
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].interpolate).toBe('basis');
|
||||
expect(edges[1].interpolate).toBe('cardinal');
|
||||
});
|
||||
|
||||
it('should handle edge curve properties using edge ID but without overriding default', function () {
|
||||
const res = flow.parser.parse(
|
||||
'graph TD\n' +
|
||||
'A e1@-->B\n' +
|
||||
'A-->C\n' +
|
||||
'linkStyle default interpolate linear\n' +
|
||||
'e1@{curve: stepAfter}'
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].interpolate).toBe('stepAfter');
|
||||
expect(edges.defaultInterpolate).toBe('linear');
|
||||
});
|
||||
|
||||
it('should handle edge curve properties using edge ID mixed with line interpolation', function () {
|
||||
const res = flow.parser.parse(
|
||||
'graph TD\n' +
|
||||
'A e1@-->B-->D\n' +
|
||||
'A-->C e4@-->D-->E\n' +
|
||||
'linkStyle default interpolate linear\n' +
|
||||
'linkStyle 1 interpolate basis\n' +
|
||||
'e1@{curve: monotoneX}\n' +
|
||||
'e4@{curve: stepBefore}'
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].interpolate).toBe('monotoneX');
|
||||
expect(edges[1].interpolate).toBe('basis');
|
||||
expect(edges.defaultInterpolate).toBe('linear');
|
||||
expect(edges[3].interpolate).toBe('stepBefore');
|
||||
expect(edges.defaultInterpolate).toBe('linear');
|
||||
});
|
||||
|
||||
it('should handle line interpolation multi-numbered definitions', function () {
|
||||
const res = flow.parser.parse(
|
||||
'graph TD\n' + 'A-->B\n' + 'A-->C\n' + 'linkStyle 0,1 interpolate basis'
|
||||
|
@@ -1,12 +1,14 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/mindmap.jison';
|
||||
import db from './mindmapDb.js';
|
||||
import { MindmapDB } from './mindmapDb.js';
|
||||
import renderer from './mindmapRenderer.js';
|
||||
import styles from './styles.js';
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
db,
|
||||
get db() {
|
||||
return new MindmapDB();
|
||||
},
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
|
@@ -1,12 +1,12 @@
|
||||
// @ts-expect-error No types available for JISON
|
||||
import { parser as mindmap } from './parser/mindmap.jison';
|
||||
import mindmapDB from './mindmapDb.js';
|
||||
import { MindmapDB } from './mindmapDb.js';
|
||||
// Todo fix utils functions for tests
|
||||
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
|
||||
|
||||
describe('when parsing a mindmap ', function () {
|
||||
beforeEach(function () {
|
||||
mindmap.yy = mindmapDB;
|
||||
mindmap.yy = new MindmapDB();
|
||||
mindmap.yy.clear();
|
||||
setLogLevel('trace');
|
||||
});
|
||||
|
@@ -5,70 +5,6 @@ import { log } from '../../logger.js';
|
||||
import type { MindmapNode } from './mindmapTypes.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
|
||||
let nodes: MindmapNode[] = [];
|
||||
let cnt = 0;
|
||||
let elements: Record<number, D3Element> = {};
|
||||
|
||||
const clear = () => {
|
||||
nodes = [];
|
||||
cnt = 0;
|
||||
elements = {};
|
||||
};
|
||||
|
||||
const getParent = function (level: number) {
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
if (nodes[i].level < level) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
// No parent found
|
||||
return null;
|
||||
};
|
||||
|
||||
const getMindmap = () => {
|
||||
return nodes.length > 0 ? nodes[0] : null;
|
||||
};
|
||||
|
||||
const addNode = (level: number, id: string, descr: string, type: number) => {
|
||||
log.info('addNode', level, id, descr, type);
|
||||
const conf = getConfig();
|
||||
let padding: number = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
||||
switch (type) {
|
||||
case nodeType.ROUNDED_RECT:
|
||||
case nodeType.RECT:
|
||||
case nodeType.HEXAGON:
|
||||
padding *= 2;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: cnt++,
|
||||
nodeId: sanitizeText(id, conf),
|
||||
level,
|
||||
descr: sanitizeText(descr, conf),
|
||||
type,
|
||||
children: [],
|
||||
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
||||
padding,
|
||||
} satisfies MindmapNode;
|
||||
|
||||
const parent = getParent(level);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
// Keep all nodes in the list
|
||||
nodes.push(node);
|
||||
} else {
|
||||
if (nodes.length === 0) {
|
||||
// First node, the root
|
||||
nodes.push(node);
|
||||
} else {
|
||||
// Syntax error ... there can only bee one root
|
||||
throw new Error(
|
||||
'There can be only one root. No parent could be found for ("' + node.descr + '")'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const nodeType = {
|
||||
DEFAULT: 0,
|
||||
NO_BORDER: 0,
|
||||
@@ -78,82 +14,149 @@ const nodeType = {
|
||||
CLOUD: 4,
|
||||
BANG: 5,
|
||||
HEXAGON: 6,
|
||||
};
|
||||
|
||||
const getType = (startStr: string, endStr: string): number => {
|
||||
log.debug('In get type', startStr, endStr);
|
||||
switch (startStr) {
|
||||
case '[':
|
||||
return nodeType.RECT;
|
||||
case '(':
|
||||
return endStr === ')' ? nodeType.ROUNDED_RECT : nodeType.CLOUD;
|
||||
case '((':
|
||||
return nodeType.CIRCLE;
|
||||
case ')':
|
||||
return nodeType.CLOUD;
|
||||
case '))':
|
||||
return nodeType.BANG;
|
||||
case '{{':
|
||||
return nodeType.HEXAGON;
|
||||
default:
|
||||
return nodeType.DEFAULT;
|
||||
}
|
||||
};
|
||||
|
||||
const setElementForId = (id: number, element: D3Element) => {
|
||||
elements[id] = element;
|
||||
};
|
||||
|
||||
const decorateNode = (decoration?: { class?: string; icon?: string }) => {
|
||||
if (!decoration) {
|
||||
return;
|
||||
}
|
||||
const config = getConfig();
|
||||
const node = nodes[nodes.length - 1];
|
||||
if (decoration.icon) {
|
||||
node.icon = sanitizeText(decoration.icon, config);
|
||||
}
|
||||
if (decoration.class) {
|
||||
node.class = sanitizeText(decoration.class, config);
|
||||
}
|
||||
};
|
||||
|
||||
const type2Str = (type: number) => {
|
||||
switch (type) {
|
||||
case nodeType.DEFAULT:
|
||||
return 'no-border';
|
||||
case nodeType.RECT:
|
||||
return 'rect';
|
||||
case nodeType.ROUNDED_RECT:
|
||||
return 'rounded-rect';
|
||||
case nodeType.CIRCLE:
|
||||
return 'circle';
|
||||
case nodeType.CLOUD:
|
||||
return 'cloud';
|
||||
case nodeType.BANG:
|
||||
return 'bang';
|
||||
case nodeType.HEXAGON:
|
||||
return 'hexgon'; // cspell: disable-line
|
||||
default:
|
||||
return 'no-border';
|
||||
}
|
||||
};
|
||||
|
||||
// Expose logger to grammar
|
||||
const getLogger = () => log;
|
||||
const getElementById = (id: number) => elements[id];
|
||||
|
||||
const db = {
|
||||
clear,
|
||||
addNode,
|
||||
getMindmap,
|
||||
nodeType,
|
||||
getType,
|
||||
setElementForId,
|
||||
decorateNode,
|
||||
type2Str,
|
||||
getLogger,
|
||||
getElementById,
|
||||
} as const;
|
||||
|
||||
export default db;
|
||||
export class MindmapDB {
|
||||
private nodes: MindmapNode[] = [];
|
||||
private count = 0;
|
||||
private elements: Record<number, D3Element> = {};
|
||||
public readonly nodeType: typeof nodeType;
|
||||
|
||||
constructor() {
|
||||
this.getLogger = this.getLogger.bind(this);
|
||||
this.nodeType = nodeType;
|
||||
this.clear();
|
||||
this.getType = this.getType.bind(this);
|
||||
this.getMindmap = this.getMindmap.bind(this);
|
||||
this.getElementById = this.getElementById.bind(this);
|
||||
this.getParent = this.getParent.bind(this);
|
||||
this.getMindmap = this.getMindmap.bind(this);
|
||||
this.addNode = this.addNode.bind(this);
|
||||
this.decorateNode = this.decorateNode.bind(this);
|
||||
}
|
||||
public clear() {
|
||||
this.nodes = [];
|
||||
this.count = 0;
|
||||
this.elements = {};
|
||||
}
|
||||
|
||||
public getParent(level: number): MindmapNode | null {
|
||||
for (let i = this.nodes.length - 1; i >= 0; i--) {
|
||||
if (this.nodes[i].level < level) {
|
||||
return this.nodes[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getMindmap(): MindmapNode | null {
|
||||
return this.nodes.length > 0 ? this.nodes[0] : null;
|
||||
}
|
||||
|
||||
public addNode(level: number, id: string, descr: string, type: number): void {
|
||||
log.info('addNode', level, id, descr, type);
|
||||
|
||||
const conf = getConfig();
|
||||
let padding = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
||||
|
||||
switch (type) {
|
||||
case this.nodeType.ROUNDED_RECT:
|
||||
case this.nodeType.RECT:
|
||||
case this.nodeType.HEXAGON:
|
||||
padding *= 2;
|
||||
break;
|
||||
}
|
||||
|
||||
const node: MindmapNode = {
|
||||
id: this.count++,
|
||||
nodeId: sanitizeText(id, conf),
|
||||
level,
|
||||
descr: sanitizeText(descr, conf),
|
||||
type,
|
||||
children: [],
|
||||
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
||||
padding,
|
||||
};
|
||||
|
||||
const parent = this.getParent(level);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
this.nodes.push(node);
|
||||
} else {
|
||||
if (this.nodes.length === 0) {
|
||||
this.nodes.push(node);
|
||||
} else {
|
||||
throw new Error(
|
||||
`There can be only one root. No parent could be found for ("${node.descr}")`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getType(startStr: string, endStr: string) {
|
||||
log.debug('In get type', startStr, endStr);
|
||||
switch (startStr) {
|
||||
case '[':
|
||||
return this.nodeType.RECT;
|
||||
case '(':
|
||||
return endStr === ')' ? this.nodeType.ROUNDED_RECT : this.nodeType.CLOUD;
|
||||
case '((':
|
||||
return this.nodeType.CIRCLE;
|
||||
case ')':
|
||||
return this.nodeType.CLOUD;
|
||||
case '))':
|
||||
return this.nodeType.BANG;
|
||||
case '{{':
|
||||
return this.nodeType.HEXAGON;
|
||||
default:
|
||||
return this.nodeType.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
public setElementForId(id: number, element: D3Element): void {
|
||||
this.elements[id] = element;
|
||||
}
|
||||
public getElementById(id: number) {
|
||||
return this.elements[id];
|
||||
}
|
||||
|
||||
public decorateNode(decoration?: { class?: string; icon?: string }): void {
|
||||
if (!decoration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = getConfig();
|
||||
const node = this.nodes[this.nodes.length - 1];
|
||||
if (decoration.icon) {
|
||||
node.icon = sanitizeText(decoration.icon, config);
|
||||
}
|
||||
if (decoration.class) {
|
||||
node.class = sanitizeText(decoration.class, config);
|
||||
}
|
||||
}
|
||||
|
||||
type2Str(type: number): string {
|
||||
switch (type) {
|
||||
case this.nodeType.DEFAULT:
|
||||
return 'no-border';
|
||||
case this.nodeType.RECT:
|
||||
return 'rect';
|
||||
case this.nodeType.ROUNDED_RECT:
|
||||
return 'rounded-rect';
|
||||
case this.nodeType.CIRCLE:
|
||||
return 'circle';
|
||||
case this.nodeType.CLOUD:
|
||||
return 'cloud';
|
||||
case this.nodeType.BANG:
|
||||
return 'bang';
|
||||
case this.nodeType.HEXAGON:
|
||||
return 'hexgon'; // cspell: disable-line
|
||||
default:
|
||||
return 'no-border';
|
||||
}
|
||||
}
|
||||
|
||||
public getLogger() {
|
||||
return log;
|
||||
}
|
||||
}
|
||||
|
@@ -9,10 +9,10 @@ import { log } from '../../logger.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import type { FilledMindMapNode, MindmapDB, MindmapNode } from './mindmapTypes.js';
|
||||
import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js';
|
||||
import { drawNode, positionNode } from './svgDraw.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
|
||||
import type { MindmapDB } from './mindmapDb.js';
|
||||
// Inject the layout algorithm into cytoscape
|
||||
cytoscape.use(coseBilkent);
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import type { RequiredDeep } from 'type-fest';
|
||||
import type mindmapDb from './mindmapDb.js';
|
||||
|
||||
export interface MindmapNode {
|
||||
id: number;
|
||||
@@ -19,4 +18,3 @@ export interface MindmapNode {
|
||||
}
|
||||
|
||||
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
||||
export type MindmapDB = typeof mindmapDb;
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import type { FilledMindMapNode, MindmapDB } from './mindmapTypes.js';
|
||||
import type { FilledMindMapNode } from './mindmapTypes.js';
|
||||
import type { Point, D3Element } from '../../types.js';
|
||||
import { parseFontSize } from '../../utils.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import type { MindmapDB } from './mindmapDb.js';
|
||||
|
||||
const MAX_SECTIONS = 12;
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { PacketDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
@@ -11,49 +12,42 @@ import {
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { PacketDB, PacketData, PacketWord } from './types.js';
|
||||
|
||||
const defaultPacketData: PacketData = {
|
||||
packet: [],
|
||||
};
|
||||
|
||||
let data: PacketData = structuredClone(defaultPacketData);
|
||||
|
||||
import type { PacketWord } from './types.js';
|
||||
const DEFAULT_PACKET_CONFIG: Required<PacketDiagramConfig> = DEFAULT_CONFIG.packet;
|
||||
|
||||
const getConfig = (): Required<PacketDiagramConfig> => {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_PACKET_CONFIG,
|
||||
...commonGetConfig().packet,
|
||||
});
|
||||
if (config.showBits) {
|
||||
config.paddingY += 10;
|
||||
export class PacketDB implements DiagramDB {
|
||||
private packet: PacketWord[] = [];
|
||||
|
||||
public getConfig() {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_PACKET_CONFIG,
|
||||
...commonGetConfig().packet,
|
||||
});
|
||||
if (config.showBits) {
|
||||
config.paddingY += 10;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
const getPacket = (): PacketWord[] => data.packet;
|
||||
|
||||
const pushWord = (word: PacketWord) => {
|
||||
if (word.length > 0) {
|
||||
data.packet.push(word);
|
||||
public getPacket() {
|
||||
return this.packet;
|
||||
}
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
commonClear();
|
||||
data = structuredClone(defaultPacketData);
|
||||
};
|
||||
public pushWord(word: PacketWord) {
|
||||
if (word.length > 0) {
|
||||
this.packet.push(word);
|
||||
}
|
||||
}
|
||||
|
||||
export const db: PacketDB = {
|
||||
pushWord,
|
||||
getPacket,
|
||||
getConfig,
|
||||
clear,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
};
|
||||
public clear() {
|
||||
commonClear();
|
||||
this.packet = [];
|
||||
}
|
||||
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setAccDescription = setAccDescription;
|
||||
}
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { db } from './db.js';
|
||||
import { PacketDB } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
import { renderer } from './renderer.js';
|
||||
import { styles } from './styles.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new PacketDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
@@ -1,24 +1,26 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { db } from './db.js';
|
||||
import { PacketDB } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
|
||||
const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db;
|
||||
|
||||
describe('packet diagrams', () => {
|
||||
let db: PacketDB;
|
||||
beforeEach(() => {
|
||||
clear();
|
||||
db = new PacketDB();
|
||||
if (parser.parser) {
|
||||
parser.parser.yy = db;
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle a packet-beta definition', async () => {
|
||||
const str = `packet-beta`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot('[]');
|
||||
expect(db.getPacket()).toMatchInlineSnapshot('[]');
|
||||
});
|
||||
|
||||
it('should handle a packet definition', async () => {
|
||||
const str = `packet`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot('[]');
|
||||
expect(db.getPacket()).toMatchInlineSnapshot('[]');
|
||||
});
|
||||
|
||||
it('should handle diagram with data and title', async () => {
|
||||
@@ -29,10 +31,10 @@ describe('packet diagrams', () => {
|
||||
0-10: "test"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
|
||||
expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
|
||||
expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
|
||||
expect(db.getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
|
||||
expect(db.getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -52,7 +54,7 @@ describe('packet diagrams', () => {
|
||||
11: "single"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -78,7 +80,7 @@ describe('packet diagrams', () => {
|
||||
+16: "word"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -104,7 +106,7 @@ describe('packet diagrams', () => {
|
||||
+16: "word"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -130,7 +132,7 @@ describe('packet diagrams', () => {
|
||||
11-90: "multiple"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -172,7 +174,7 @@ describe('packet diagrams', () => {
|
||||
17-63: "multiple"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
|
@@ -3,12 +3,12 @@ import { parse } from '@mermaid-js/parser';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import { db } from './db.js';
|
||||
import { PacketDB } from './db.js';
|
||||
import type { PacketBlock, PacketWord } from './types.js';
|
||||
|
||||
const maxPacketSize = 10_000;
|
||||
|
||||
const populate = (ast: Packet) => {
|
||||
const populate = (ast: Packet, db: PacketDB) => {
|
||||
populateCommonDb(ast, db);
|
||||
let lastBit = -1;
|
||||
let word: PacketWord = [];
|
||||
@@ -91,9 +91,17 @@ const getNextFittingBlock = (
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
// @ts-expect-error - PacketDB is not assignable to DiagramDB
|
||||
parser: { yy: undefined },
|
||||
parse: async (input: string): Promise<void> => {
|
||||
const ast: Packet = await parse('packet', input);
|
||||
const db = parser.parser?.yy;
|
||||
if (!(db instanceof PacketDB)) {
|
||||
throw new Error(
|
||||
'parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||
);
|
||||
}
|
||||
log.debug(ast);
|
||||
populate(ast);
|
||||
populate(ast, db);
|
||||
},
|
||||
};
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
|
||||
import { isLabelStyle } from '../../rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
|
||||
|
||||
import type { TreemapDiagramConfig, TreemapNode } from './types.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
import { isLabelStyle } from '../../rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
@@ -14,99 +14,82 @@ import {
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { TreemapDB, TreemapData, TreemapDiagramConfig, TreemapNode } from './types.js';
|
||||
export class TreeMapDB implements DiagramDB {
|
||||
private nodes: TreemapNode[] = [];
|
||||
private levels: Map<TreemapNode, number> = new Map<TreemapNode, number>();
|
||||
private outerNodes: TreemapNode[] = [];
|
||||
private classes: Map<string, DiagramStyleClassDef> = new Map<string, DiagramStyleClassDef>();
|
||||
private root?: TreemapNode;
|
||||
|
||||
const defaultTreemapData: TreemapData = {
|
||||
nodes: [],
|
||||
levels: new Map(),
|
||||
outerNodes: [],
|
||||
classes: new Map(),
|
||||
};
|
||||
|
||||
const state = new ImperativeState<TreemapData>(() => structuredClone(defaultTreemapData));
|
||||
|
||||
const getConfig = (): Required<TreemapDiagramConfig> => {
|
||||
// Use type assertion with unknown as intermediate step
|
||||
const defaultConfig = DEFAULT_CONFIG as unknown as { treemap: Required<TreemapDiagramConfig> };
|
||||
const userConfig = commonGetConfig() as unknown as { treemap?: Partial<TreemapDiagramConfig> };
|
||||
|
||||
return cleanAndMerge({
|
||||
...defaultConfig.treemap,
|
||||
...(userConfig.treemap ?? {}),
|
||||
}) as Required<TreemapDiagramConfig>;
|
||||
};
|
||||
|
||||
const getNodes = (): TreemapNode[] => state.records.nodes;
|
||||
|
||||
const addNode = (node: TreemapNode, level: number) => {
|
||||
const data = state.records;
|
||||
data.nodes.push(node);
|
||||
data.levels.set(node, level);
|
||||
|
||||
if (level === 0) {
|
||||
data.outerNodes.push(node);
|
||||
public getNodes() {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
// Set the root node if this is a level 0 node and we don't have a root yet
|
||||
if (level === 0 && !data.root) {
|
||||
data.root = node;
|
||||
public getConfig() {
|
||||
const defaultConfig = DEFAULT_CONFIG as unknown as { treemap: Required<TreemapDiagramConfig> };
|
||||
const userConfig = commonGetConfig() as unknown as { treemap?: Partial<TreemapDiagramConfig> };
|
||||
return cleanAndMerge({
|
||||
...defaultConfig.treemap,
|
||||
...(userConfig.treemap ?? {}),
|
||||
}) as Required<TreemapDiagramConfig>;
|
||||
}
|
||||
};
|
||||
|
||||
const getRoot = (): TreemapNode | undefined => ({ name: '', children: state.records.outerNodes });
|
||||
public addNode(node: TreemapNode, level: number) {
|
||||
this.nodes.push(node);
|
||||
this.levels.set(node, level);
|
||||
if (level === 0) {
|
||||
this.outerNodes.push(node);
|
||||
this.root ??= node;
|
||||
}
|
||||
}
|
||||
|
||||
const addClass = (id: string, _style: string) => {
|
||||
const classes = state.records.classes;
|
||||
const styleClass = classes.get(id) ?? { id, styles: [], textStyles: [] };
|
||||
classes.set(id, styleClass);
|
||||
public getRoot() {
|
||||
return { name: '', children: this.outerNodes };
|
||||
}
|
||||
|
||||
const styles = _style.replace(/\\,/g, '§§§').replace(/,/g, ';').replace(/§§§/g, ',').split(';');
|
||||
|
||||
if (styles) {
|
||||
styles.forEach((s) => {
|
||||
if (isLabelStyle(s)) {
|
||||
if (styleClass?.textStyles) {
|
||||
styleClass.textStyles.push(s);
|
||||
} else {
|
||||
styleClass.textStyles = [s];
|
||||
public addClass(id: string, _style: string) {
|
||||
const styleClass = this.classes.get(id) ?? { id, styles: [], textStyles: [] };
|
||||
const styles = _style.replace(/\\,/g, '§§§').replace(/,/g, ';').replace(/§§§/g, ',').split(';');
|
||||
if (styles) {
|
||||
styles.forEach((s) => {
|
||||
if (isLabelStyle(s)) {
|
||||
if (styleClass?.textStyles) {
|
||||
styleClass.textStyles.push(s);
|
||||
} else {
|
||||
styleClass.textStyles = [s];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (styleClass?.styles) {
|
||||
styleClass.styles.push(s);
|
||||
} else {
|
||||
styleClass.styles = [s];
|
||||
}
|
||||
});
|
||||
if (styleClass?.styles) {
|
||||
styleClass.styles.push(s);
|
||||
} else {
|
||||
styleClass.styles = [s];
|
||||
}
|
||||
});
|
||||
}
|
||||
this.classes.set(id, styleClass);
|
||||
}
|
||||
|
||||
classes.set(id, styleClass);
|
||||
};
|
||||
const getClasses = (): Map<string, DiagramStyleClassDef> => {
|
||||
return state.records.classes;
|
||||
};
|
||||
public getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
const getStylesForClass = (classSelector: string): string[] => {
|
||||
return state.records.classes.get(classSelector)?.styles ?? [];
|
||||
};
|
||||
public getStylesForClass(classSelector: string): string[] {
|
||||
return this.classes.get(classSelector)?.styles ?? [];
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
commonClear();
|
||||
state.reset();
|
||||
};
|
||||
public clear() {
|
||||
commonClear();
|
||||
this.nodes = [];
|
||||
this.levels = new Map();
|
||||
this.outerNodes = [];
|
||||
this.classes = new Map();
|
||||
this.root = undefined;
|
||||
}
|
||||
|
||||
export const db: TreemapDB = {
|
||||
getNodes,
|
||||
addNode,
|
||||
getRoot,
|
||||
getConfig,
|
||||
clear,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
addClass,
|
||||
getClasses,
|
||||
getStylesForClass,
|
||||
};
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setAccDescription = setAccDescription;
|
||||
}
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { db } from './db.js';
|
||||
import { TreeMapDB } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
import { renderer } from './renderer.js';
|
||||
import styles from './styles.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new TreeMapDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
@@ -2,15 +2,15 @@ import { parse } from '@mermaid-js/parser';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import { db } from './db.js';
|
||||
import type { TreemapNode, TreemapAst } from './types.js';
|
||||
import type { TreemapNode, TreemapAst, TreemapDB } from './types.js';
|
||||
import { buildHierarchy } from './utils.js';
|
||||
import { TreeMapDB } from './db.js';
|
||||
|
||||
/**
|
||||
* Populates the database with data from the Treemap AST
|
||||
* @param ast - The Treemap AST
|
||||
*/
|
||||
const populate = (ast: TreemapAst) => {
|
||||
const populate = (ast: TreemapAst, db: TreemapDB) => {
|
||||
// We need to bypass the type checking for populateCommonDb
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
populateCommonDb(ast as any, db);
|
||||
@@ -84,6 +84,8 @@ const getItemName = (item: { name?: string | number }): string => {
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
// @ts-expect-error - TreeMapDB is not assignable to DiagramDB
|
||||
parser: { yy: undefined },
|
||||
parse: async (text: string): Promise<void> => {
|
||||
try {
|
||||
// Use a generic parse that accepts any diagram type
|
||||
@@ -91,7 +93,13 @@ export const parser: ParserDefinition = {
|
||||
const parseFunc = parse as (diagramType: string, text: string) => Promise<TreemapAst>;
|
||||
const ast = await parseFunc('treemap', text);
|
||||
log.debug('Treemap AST:', ast);
|
||||
populate(ast);
|
||||
const db = parser.parser?.yy;
|
||||
if (!(db instanceof TreeMapDB)) {
|
||||
throw new Error(
|
||||
'parser.parser?.yy was not a TreemapDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||
);
|
||||
}
|
||||
populate(ast, db);
|
||||
} catch (error) {
|
||||
log.error('Error parsing treemap:', error);
|
||||
throw error;
|
||||
|
@@ -141,6 +141,7 @@ function sidebarAll() {
|
||||
],
|
||||
},
|
||||
...sidebarSyntax(),
|
||||
...sidebarAlgorithms(),
|
||||
...sidebarEcosystem(),
|
||||
...sidebarConfig(),
|
||||
...sidebarCommunity(),
|
||||
@@ -223,6 +224,27 @@ function sidebarEcosystem() {
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarAlgorithms() {
|
||||
return [
|
||||
{
|
||||
text: '🧠 Diagram Algorithms',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/layouts/introduction' },
|
||||
{ text: 'Layout Algorithm Development', link: '/layouts/development' },
|
||||
{
|
||||
text: 'IPSep-Cola',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Overview', link: '/layouts/ipsepcola/overview' },
|
||||
{ text: 'Implementation', link: '/layouts/ipsepcola/implementation' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarCommunity() {
|
||||
return [
|
||||
{
|
||||
|
@@ -302,7 +302,7 @@ If you are adding a feature, you will definitely need to add tests. Depending on
|
||||
|
||||
Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run.
|
||||
|
||||
Unit tests are mandatory for all code except the renderers. (The renderers are tested with integration tests.)
|
||||
Unit tests are mandatory for all code except the layout tests. (The layouts are tested with integration tests.)
|
||||
|
||||
We use [Vitest](https://vitest.dev) to run unit tests.
|
||||
|
||||
@@ -328,6 +328,30 @@ When using Docker prepend your command with `./run`:
|
||||
./run pnpm test
|
||||
```
|
||||
|
||||
##### Testing the DOM
|
||||
|
||||
One can use `jsdomIt` to test any part of Mermaid that interacts with the DOM, as long as it is not related to the layout.
|
||||
|
||||
The function `jsdomIt` ([developed in utils.ts](../../tests/util.ts)) overrides `it` from `vitest`, and creates a pseudo-browser environment that works almost like the real deal for the duration of the test. It uses JSDOM to create a DOM, and adds objects `window` and `document` to `global` to mock the browser environment.
|
||||
|
||||
> [!NOTE]
|
||||
> The layout cannot work in `jsdomIt` tests because JSDOM has no rendering engine, hence the pseudo-browser environment.
|
||||
|
||||
Example :
|
||||
|
||||
```typescript
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
|
||||
jsdomIt('should add element "thing" in the SVG', ({ svg }) => {
|
||||
// Code in this block runs in a pseudo-browser environment
|
||||
addThing(svg); // The svg item is the D3 selection of the SVG node
|
||||
const svgNode = ensureNodeFromSelector('svg'); // Retrieve the DOM node using the DOM API
|
||||
expect(svgNode.querySelector('thing')).not.toBeNull(); // Test the structure of the SVG
|
||||
});
|
||||
```
|
||||
|
||||
They can be used to test any method that interacts with the DOM, including for testing renderers. For renderers, additional integration testing is necessary to test the layout though.
|
||||
|
||||
#### Integration / End-to-End (E2E) Tests
|
||||
|
||||
These test the rendering and visual appearance of the diagrams.
|
||||
|
189
packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd
Normal file
189
packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDiagram.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDb.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/styles.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/types.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowChartShapes.js"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/elk/detector.ts"
|
||||
generationTime: 2025-07-23T10:31:53.266Z
|
||||
---
|
||||
flowchart TD
|
||||
%% Entry Points and Detection
|
||||
Input["User Input Text"] --> Detection{Detection Phase}
|
||||
|
||||
Detection --> flowDetector["flowDetector.ts<br/>detector(txt, config)"]
|
||||
Detection --> flowDetectorV2["flowDetector-v2.ts<br/>detector(txt, config)"]
|
||||
Detection --> elkDetector["elk/detector.ts<br/>detector(txt, config)"]
|
||||
|
||||
flowDetector --> |"Checks /^\s*graph/"| DetectLegacy{Legacy Flowchart?}
|
||||
flowDetectorV2 --> |"Checks /^\s*flowchart/"| DetectNew{New Flowchart?}
|
||||
elkDetector --> |"Checks /^\s*flowchart-elk/"| DetectElk{ELK Layout?}
|
||||
|
||||
DetectLegacy --> |Yes| LoadDiagram
|
||||
DetectNew --> |Yes| LoadDiagram
|
||||
DetectElk --> |Yes| LoadDiagram
|
||||
|
||||
%% Loading Phase
|
||||
LoadDiagram["loader() function"] --> flowDiagram["flowDiagram.ts<br/>diagram object"]
|
||||
|
||||
flowDiagram --> DiagramStructure{Diagram Components}
|
||||
DiagramStructure --> Parser["parser: flowParser"]
|
||||
DiagramStructure --> Database["db: new FlowDB()"]
|
||||
DiagramStructure --> Renderer["renderer: flowRenderer-v3-unified"]
|
||||
DiagramStructure --> Styles["styles: flowStyles"]
|
||||
DiagramStructure --> Init["init: (cnf: MermaidConfig)"]
|
||||
|
||||
%% Parser Phase
|
||||
Parser --> flowParser["parser/flowParser.ts<br/>newParser.parse(src)"]
|
||||
flowParser --> |"Preprocesses src"| RemoveWhitespace["Remove trailing whitespace<br/>src.replace(/}\s*\n/g, '}\n')"]
|
||||
RemoveWhitespace --> flowJison["parser/flow.jison<br/>flowJisonParser.parse(newSrc)"]
|
||||
|
||||
flowJison --> ParseGraph["Parse Graph Structure"]
|
||||
ParseGraph --> ParseVertices["Parse Vertices"]
|
||||
ParseGraph --> ParseEdges["Parse Edges"]
|
||||
ParseGraph --> ParseSubgraphs["Parse Subgraphs"]
|
||||
ParseGraph --> ParseClasses["Parse Classes"]
|
||||
ParseGraph --> ParseStyles["Parse Styles"]
|
||||
|
||||
%% Database Phase - FlowDB Class
|
||||
Database --> FlowDBClass["flowDb.ts<br/>FlowDB class"]
|
||||
|
||||
FlowDBClass --> DBInit["constructor()<br/>- Initialize counters<br/>- Bind methods<br/>- Setup toolTips<br/>- Call clear()"]
|
||||
|
||||
DBInit --> DBMethods{FlowDB Methods}
|
||||
|
||||
DBMethods --> addVertex["addVertex(id, textObj, type, style,<br/>classes, dir, props, metadata)"]
|
||||
DBMethods --> addLink["addLink(_start[], _end[], linkData)"]
|
||||
DBMethods --> addSingleLink["addSingleLink(_start, _end, type, id)"]
|
||||
DBMethods --> setDirection["setDirection(dir)"]
|
||||
DBMethods --> addSubGraph["addSubGraph(nodes[], id, title)"]
|
||||
DBMethods --> addClass["addClass(id, style)"]
|
||||
DBMethods --> setClass["setClass(ids, className)"]
|
||||
DBMethods --> setTooltip["setTooltip(ids, tooltip)"]
|
||||
DBMethods --> setClickEvent["setClickEvent(id, functionName, args)"]
|
||||
DBMethods --> setClickFun["setClickFun(id, functionName, args)"]
|
||||
|
||||
%% Vertex Processing
|
||||
addVertex --> VertexProcess{Vertex Processing}
|
||||
VertexProcess --> CreateVertex["Create FlowVertex object<br/>- id, labelType, domId<br/>- styles[], classes[]"]
|
||||
VertexProcess --> SanitizeText["sanitizeText(textObj.text)"]
|
||||
VertexProcess --> ParseMetadata["Parse YAML metadata<br/>yaml.load(yamlData)"]
|
||||
VertexProcess --> SetVertexProps["Set vertex properties<br/>- shape, label, icon, form<br/>- pos, img, constraint, w, h"]
|
||||
|
||||
%% Edge Processing
|
||||
addSingleLink --> EdgeProcess{Edge Processing}
|
||||
EdgeProcess --> CreateEdge["Create FlowEdge object<br/>- start, end, type, text<br/>- labelType, classes[]"]
|
||||
EdgeProcess --> ProcessLinkText["Process link text<br/>- sanitizeText()<br/>- strip quotes"]
|
||||
EdgeProcess --> SetEdgeProps["Set edge properties<br/>- type, stroke, length"]
|
||||
EdgeProcess --> GenerateEdgeId["Generate edge ID<br/>getEdgeId(start, end, counter)"]
|
||||
EdgeProcess --> ValidateEdgeLimit["Validate edge limit<br/>maxEdges check"]
|
||||
|
||||
%% Data Collection
|
||||
DBMethods --> GetData["getData()"]
|
||||
GetData --> CollectNodes["Collect nodes[] from vertices"]
|
||||
GetData --> CollectEdges["Collect edges[] from edges"]
|
||||
GetData --> ProcessSubGraphs["Process subgraphs<br/>- parentDB Map<br/>- subGraphDB Map"]
|
||||
GetData --> AddNodeFromVertex["addNodeFromVertex()<br/>for each vertex"]
|
||||
GetData --> ProcessEdgeTypes["destructEdgeType()<br/>arrowTypeStart, arrowTypeEnd"]
|
||||
|
||||
%% Node Creation
|
||||
AddNodeFromVertex --> NodeCreation{Node Creation}
|
||||
NodeCreation --> FindExistingNode["findNode(nodes, vertex.id)"]
|
||||
NodeCreation --> CreateBaseNode["Create base node<br/>- id, label, parentId<br/>- cssStyles, cssClasses<br/>- shape, domId, tooltip"]
|
||||
NodeCreation --> GetCompiledStyles["getCompiledStyles(classDefs)"]
|
||||
NodeCreation --> GetTypeFromVertex["getTypeFromVertex(vertex)"]
|
||||
|
||||
%% Rendering Phase
|
||||
Renderer --> flowRendererV3["flowRenderer-v3-unified.ts<br/>draw(text, id, version, diag)"]
|
||||
|
||||
flowRendererV3 --> RenderInit["Initialize rendering<br/>- getConfig()<br/>- handle securityLevel<br/>- getDiagramElement()"]
|
||||
|
||||
RenderInit --> GetLayoutData["diag.db.getData()<br/>as LayoutData"]
|
||||
GetLayoutData --> SetupLayoutData["Setup layout data<br/>- type, layoutAlgorithm<br/>- direction, spacing<br/>- markers, diagramId"]
|
||||
|
||||
SetupLayoutData --> CallRender["render(data4Layout, svg)"]
|
||||
CallRender --> SetupViewPort["setupViewPortForSVG(svg, padding)"]
|
||||
SetupViewPort --> ProcessLinks["Process vertex links<br/>- create anchor elements<br/>- handle click events"]
|
||||
|
||||
%% Shape Rendering
|
||||
CallRender --> ShapeSystem["flowChartShapes.js<br/>Shape Functions"]
|
||||
|
||||
ShapeSystem --> ShapeFunctions{Shape Functions}
|
||||
ShapeFunctions --> question["question(parent, bbox, node)"]
|
||||
ShapeFunctions --> hexagon["hexagon(parent, bbox, node)"]
|
||||
ShapeFunctions --> rect_left_inv_arrow["rect_left_inv_arrow(parent, bbox, node)"]
|
||||
ShapeFunctions --> lean_right["lean_right(parent, bbox, node)"]
|
||||
ShapeFunctions --> lean_left["lean_left(parent, bbox, node)"]
|
||||
|
||||
ShapeFunctions --> insertPolygonShape["insertPolygonShape(parent, w, h, points)"]
|
||||
ShapeFunctions --> intersectPolygon["intersectPolygon(node, points, point)"]
|
||||
ShapeFunctions --> intersectRect["intersectRect(node, point)"]
|
||||
|
||||
%% Styling System
|
||||
Styles --> stylesTS["styles.ts<br/>getStyles(options)"]
|
||||
stylesTS --> StyleOptions["FlowChartStyleOptions<br/>- arrowheadColor, border2<br/>- clusterBkg, mainBkg<br/>- fontFamily, textColor"]
|
||||
|
||||
StyleOptions --> GenerateCSS["Generate CSS styles<br/>- .label, .cluster-label<br/>- .node, .edgePath<br/>- .flowchart-link, .edgeLabel"]
|
||||
GenerateCSS --> GetIconStyles["getIconStyles()"]
|
||||
|
||||
%% Type System
|
||||
Parser --> TypeSystem["types.ts<br/>Type Definitions"]
|
||||
TypeSystem --> FlowVertex["FlowVertex interface"]
|
||||
TypeSystem --> FlowEdge["FlowEdge interface"]
|
||||
TypeSystem --> FlowClass["FlowClass interface"]
|
||||
TypeSystem --> FlowSubGraph["FlowSubGraph interface"]
|
||||
TypeSystem --> FlowVertexTypeParam["FlowVertexTypeParam<br/>Shape types"]
|
||||
|
||||
%% Utility Functions
|
||||
DBMethods --> UtilityFunctions{Utility Functions}
|
||||
UtilityFunctions --> lookUpDomId["lookUpDomId(id)"]
|
||||
UtilityFunctions --> getClasses["getClasses()"]
|
||||
UtilityFunctions --> getDirection["getDirection()"]
|
||||
UtilityFunctions --> getVertices["getVertices()"]
|
||||
UtilityFunctions --> getEdges["getEdges()"]
|
||||
UtilityFunctions --> getSubGraphs["getSubGraphs()"]
|
||||
UtilityFunctions --> clear["clear()"]
|
||||
UtilityFunctions --> defaultConfig["defaultConfig()"]
|
||||
|
||||
%% Event Handling
|
||||
ProcessLinks --> EventHandling{Event Handling}
|
||||
EventHandling --> setupToolTips["setupToolTips(element)"]
|
||||
EventHandling --> bindFunctions["bindFunctions(element)"]
|
||||
EventHandling --> runFunc["utils.runFunc(functionName, args)"]
|
||||
|
||||
%% Common Database Functions
|
||||
DBMethods --> CommonDB["commonDb.js functions"]
|
||||
CommonDB --> setAccTitle["setAccTitle()"]
|
||||
CommonDB --> getAccTitle["getAccTitle()"]
|
||||
CommonDB --> setAccDescription["setAccDescription()"]
|
||||
CommonDB --> getAccDescription["getAccDescription()"]
|
||||
CommonDB --> setDiagramTitle["setDiagramTitle()"]
|
||||
CommonDB --> getDiagramTitle["getDiagramTitle()"]
|
||||
CommonDB --> commonClear["clear()"]
|
||||
|
||||
%% Final Output
|
||||
ProcessLinks --> FinalSVG["Final SVG Output"]
|
||||
|
||||
%% Layout Algorithm Selection
|
||||
SetupLayoutData --> LayoutAlgorithm{Layout Algorithm}
|
||||
LayoutAlgorithm --> Dagre["dagre<br/>(default)"]
|
||||
LayoutAlgorithm --> DagreWrapper["dagre-wrapper<br/>(v2 renderer)"]
|
||||
LayoutAlgorithm --> ELK["elk<br/>(external package)"]
|
||||
|
||||
%% Testing Components
|
||||
FlowDBClass --> TestFiles["Test Files"]
|
||||
TestFiles --> flowDbSpec["flowDb.spec.ts"]
|
||||
TestFiles --> flowChartShapesSpec["flowChartShapes.spec.js"]
|
||||
TestFiles --> ParserTests["parser/*.spec.js files<br/>- flow-text.spec.js<br/>- flow-edges.spec.js<br/>- flow-style.spec.js<br/>- subgraph.spec.js"]
|
||||
|
||||
%% Configuration
|
||||
Init --> ConfigSetup["Configuration Setup"]
|
||||
ConfigSetup --> FlowchartConfig["cnf.flowchart config"]
|
||||
ConfigSetup --> ArrowMarkers["arrowMarkerAbsolute"]
|
||||
ConfigSetup --> LayoutConfig["layout config"]
|
||||
ConfigSetup --> SetConfig["setConfig() calls"]
|
307
packages/mermaid/src/docs/diagrams/mermaid-api-sequence.mmd
Normal file
307
packages/mermaid/src/docs/diagrams/mermaid-api-sequence.mmd
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/mermaidAPI.ts"
|
||||
- "File: /packages/mermaid/src/mermaid.ts"
|
||||
generationTime: 2025-01-28T16:30:00.000Z
|
||||
---
|
||||
sequenceDiagram
|
||||
participant User as User/Browser
|
||||
participant Mermaid as mermaid.ts
|
||||
participant Queue as executionQueue
|
||||
participant API as mermaidAPI.ts
|
||||
participant Config as configApi
|
||||
participant Preprocessor as preprocessDiagram
|
||||
participant DiagramAPI as diagram-api
|
||||
participant Diagram as Diagram.fromText
|
||||
participant Renderer as diagram.renderer
|
||||
participant Styles as Styling System
|
||||
participant DOM as DOM/SVG
|
||||
|
||||
Note over User, DOM: Mermaid Complete API Flow
|
||||
|
||||
%% Initialization Phase
|
||||
User->>+Mermaid: mermaid.initialize(config)
|
||||
Mermaid->>+API: mermaidAPI.initialize(config)
|
||||
|
||||
API->>API: assignWithDepth({}, userOptions)
|
||||
API->>API: handle legacy fontFamily config
|
||||
API->>Config: saveConfigFromInitialize(options)
|
||||
|
||||
alt Theme Configuration Available
|
||||
API->>API: check if theme in theme object
|
||||
API->>API: set themeVariables from theme
|
||||
else Default Theme
|
||||
API->>API: use default theme variables
|
||||
end
|
||||
|
||||
API->>Config: setSiteConfig(options) or getSiteConfig()
|
||||
API->>API: setLogLevel(config.logLevel)
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
|
||||
API-->>-Mermaid: initialization complete
|
||||
Mermaid-->>-User: ready to render
|
||||
|
||||
%% Content Loaded Event
|
||||
User->>DOM: document.load event
|
||||
DOM->>+Mermaid: contentLoaded()
|
||||
|
||||
opt startOnLoad is true
|
||||
Mermaid->>Config: getConfig()
|
||||
Config-->>Mermaid: { startOnLoad: true }
|
||||
Mermaid->>Mermaid: run()
|
||||
end
|
||||
|
||||
Mermaid-->>-DOM: event handling complete
|
||||
|
||||
%% Main Run Function
|
||||
User->>+Mermaid: mermaid.run(options)
|
||||
|
||||
Mermaid->>Mermaid: runThrowsErrors(options)
|
||||
Mermaid->>Config: getConfig()
|
||||
Config-->>Mermaid: configuration object
|
||||
|
||||
alt nodes provided
|
||||
Mermaid->>Mermaid: use provided nodes
|
||||
else querySelector provided
|
||||
Mermaid->>DOM: document.querySelectorAll(querySelector)
|
||||
DOM-->>Mermaid: nodesToProcess
|
||||
end
|
||||
|
||||
Mermaid->>Mermaid: new InitIDGenerator(deterministicIds, seed)
|
||||
|
||||
loop For each diagram element
|
||||
Mermaid->>DOM: check element.getAttribute('data-processed')
|
||||
|
||||
opt not processed
|
||||
Mermaid->>DOM: element.setAttribute('data-processed', 'true')
|
||||
Mermaid->>Mermaid: generate unique id
|
||||
Mermaid->>DOM: get element.innerHTML
|
||||
Mermaid->>Mermaid: entityDecode and clean text
|
||||
Mermaid->>Mermaid: detectInit(txt)
|
||||
|
||||
Mermaid->>Queue: render(id, txt, element)
|
||||
end
|
||||
end
|
||||
|
||||
Mermaid-->>-User: processing initiated
|
||||
|
||||
%% Render Function (Queued)
|
||||
activate Queue
|
||||
Queue->>+API: mermaidAPI.render(id, text, container)
|
||||
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
API->>+Preprocessor: processAndSetConfigs(text)
|
||||
|
||||
Preprocessor->>Preprocessor: preprocessDiagram(text)
|
||||
Preprocessor->>Config: reset()
|
||||
Preprocessor->>Config: addDirective(processed.config)
|
||||
Preprocessor-->>-API: { code, config }
|
||||
|
||||
API->>Config: getConfig()
|
||||
Config-->>API: current configuration
|
||||
|
||||
opt text length > maxTextSize
|
||||
API->>API: text = MAX_TEXTLENGTH_EXCEEDED_MSG
|
||||
end
|
||||
|
||||
API->>API: setup id selectors and element IDs
|
||||
API->>API: determine security level (sandbox/loose)
|
||||
|
||||
%% DOM Setup
|
||||
alt svgContainingElement provided
|
||||
alt isSandboxed
|
||||
API->>DOM: sandboxedIframe(select(svgContainingElement), iFrameID)
|
||||
API->>DOM: select iframe contentDocument body
|
||||
else
|
||||
API->>DOM: select(svgContainingElement)
|
||||
end
|
||||
else no container
|
||||
API->>API: removeExistingElements(document, id, divId, iFrameId)
|
||||
alt isSandboxed
|
||||
API->>DOM: sandboxedIframe(select('body'), iFrameID)
|
||||
else
|
||||
API->>DOM: select('body')
|
||||
end
|
||||
end
|
||||
|
||||
API->>DOM: appendDivSvgG(root, id, enclosingDivID, fontFamily, XMLNS_XLINK_STD)
|
||||
|
||||
%% Diagram Creation
|
||||
API->>+Diagram: Diagram.fromText(text, { title: processed.title })
|
||||
|
||||
Diagram->>DiagramAPI: detect diagram type
|
||||
Diagram->>DiagramAPI: load appropriate diagram
|
||||
Diagram-->>-API: diagram instance
|
||||
|
||||
opt parsing error occurred
|
||||
API->>+Diagram: Diagram.fromText('error')
|
||||
Diagram-->>-API: error diagram
|
||||
API->>API: store parseEncounteredException
|
||||
end
|
||||
|
||||
%% Style Generation
|
||||
API->>DOM: get svg element and firstChild
|
||||
API->>Renderer: diag.renderer.getClasses(text, diag)
|
||||
Renderer-->>API: diagramClassDefs
|
||||
|
||||
API->>+Styles: createUserStyles(config, diagramType, diagramClassDefs, idSelector)
|
||||
|
||||
Styles->>Styles: createCssStyles(config, classDefs)
|
||||
|
||||
opt config.themeCSS defined
|
||||
Styles->>Styles: append themeCSS
|
||||
end
|
||||
|
||||
opt fontFamily configured
|
||||
Styles->>Styles: add CSS variables for fonts
|
||||
end
|
||||
|
||||
opt classDefs exist
|
||||
loop For each styleClassDef
|
||||
opt has styles
|
||||
Styles->>Styles: cssImportantStyles for each CSS element
|
||||
end
|
||||
opt has textStyles
|
||||
Styles->>Styles: cssImportantStyles for tspan elements
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Styles->>Styles: getStyles(graphType, userCSSstyles, themeVariables)
|
||||
Styles->>Styles: serialize(compile(svgId{allStyles}), stringify)
|
||||
Styles-->>-API: compiled CSS rules
|
||||
|
||||
API->>DOM: create style element
|
||||
API->>DOM: style.innerHTML = rules
|
||||
API->>DOM: svg.insertBefore(style, firstChild)
|
||||
|
||||
%% Diagram Rendering
|
||||
API->>+Renderer: diag.renderer.draw(text, id, version, diag)
|
||||
|
||||
Renderer->>Renderer: diagram-specific rendering logic
|
||||
Renderer->>DOM: create SVG elements
|
||||
Renderer->>DOM: apply positioning and styling
|
||||
Renderer-->>-API: rendered diagram
|
||||
|
||||
opt rendering error
|
||||
alt suppressErrorRendering
|
||||
API->>API: removeTempElements()
|
||||
API->>Mermaid: throw error
|
||||
else
|
||||
API->>Renderer: errorRenderer.draw(text, id, version)
|
||||
end
|
||||
end
|
||||
|
||||
%% Accessibility and Cleanup
|
||||
API->>DOM: select svg element
|
||||
API->>Diagram: diag.db.getAccTitle()
|
||||
API->>Diagram: diag.db.getAccDescription()
|
||||
API->>API: addA11yInfo(diagramType, svgNode, a11yTitle, a11yDescr)
|
||||
|
||||
API->>DOM: set xmlns for foreignobject elements
|
||||
API->>DOM: get innerHTML from enclosing div
|
||||
|
||||
API->>+API: cleanUpSvgCode(svgCode, isSandboxed, arrowMarkerAbsolute)
|
||||
|
||||
opt not useArrowMarkerUrls and not sandboxed
|
||||
API->>API: replace marker-end URLs with anchors
|
||||
end
|
||||
|
||||
API->>API: decodeEntities(svgCode)
|
||||
API->>API: replace <br> with <br/>
|
||||
API-->>-API: cleaned SVG code
|
||||
|
||||
alt isSandboxed
|
||||
API->>+API: putIntoIFrame(svgCode, svgEl)
|
||||
API->>API: calculate iframe height
|
||||
API->>API: toBase64 encode SVG content
|
||||
API->>API: create iframe with sandbox attributes
|
||||
API-->>-API: iframe HTML
|
||||
else not loose security
|
||||
API->>API: DOMPurify.sanitize(svgCode, options)
|
||||
end
|
||||
|
||||
API->>API: attachFunctions()
|
||||
API->>API: removeTempElements()
|
||||
|
||||
opt parseEncounteredException
|
||||
API->>Mermaid: throw parseEncounteredException
|
||||
end
|
||||
|
||||
API-->>-Queue: { diagramType, svg: svgCode, bindFunctions }
|
||||
|
||||
%% Return to Web Integration
|
||||
activate Mermaid
|
||||
Queue-->>Mermaid: render result
|
||||
Mermaid->>DOM: element.innerHTML = svg
|
||||
|
||||
opt postRenderCallback
|
||||
Mermaid->>User: postRenderCallback(id)
|
||||
end
|
||||
|
||||
opt bindFunctions exist
|
||||
Mermaid->>DOM: bindFunctions(element)
|
||||
end
|
||||
deactivate Mermaid
|
||||
|
||||
%% Parse Function Flow
|
||||
User->>+Mermaid: mermaid.parse(text, parseOptions)
|
||||
activate Queue
|
||||
|
||||
Queue->>+API: mermaidAPI.parse(text, parseOptions)
|
||||
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
API->>Preprocessor: processAndSetConfigs(text)
|
||||
Preprocessor-->>API: { code, config }
|
||||
API->>Diagram: getDiagramFromText(code)
|
||||
Diagram-->>API: diagram instance
|
||||
API-->>-Queue: { diagramType: diagram.type, config }
|
||||
|
||||
Queue-->>-Mermaid: parse result
|
||||
Mermaid-->>-User: ParseResult or false
|
||||
|
||||
%% External Diagram Registration
|
||||
User->>+Mermaid: registerExternalDiagrams(diagrams, options)
|
||||
|
||||
Mermaid->>DiagramAPI: addDiagrams()
|
||||
Mermaid->>DiagramAPI: registerLazyLoadedDiagrams(...diagrams)
|
||||
|
||||
opt lazyLoad is false
|
||||
Mermaid->>DiagramAPI: loadRegisteredDiagrams()
|
||||
end
|
||||
|
||||
Mermaid-->>-User: registration complete
|
||||
|
||||
%% Error Handling
|
||||
Note over Mermaid, API: Error Handling Throughout
|
||||
alt Error occurs
|
||||
API->>Mermaid: throw error
|
||||
Mermaid->>+Mermaid: handleError(error, errors, parseError)
|
||||
|
||||
Mermaid->>Mermaid: log.warn(error)
|
||||
|
||||
alt isDetailedError
|
||||
Mermaid->>User: parseError(error.str, error.hash)
|
||||
else
|
||||
Mermaid->>User: parseError(error)
|
||||
end
|
||||
|
||||
opt not suppressErrors
|
||||
Mermaid->>User: throw error
|
||||
end
|
||||
|
||||
Mermaid-->>-User: error handled
|
||||
end
|
||||
|
||||
%% Configuration Details
|
||||
Note over Config: Configuration Functions
|
||||
Note right of Config: Functions:<br/>- reset()<br/>- getConfig()<br/>- setConfig()<br/>- getSiteConfig()<br/>- updateSiteConfig()<br/>- saveConfigFromInitialize()
|
||||
|
||||
Note over Styles: CSS Generation
|
||||
Note right of Styles: Features:<br/>- createCssStyles()<br/>- createUserStyles()<br/>- cssImportantStyles()<br/>- Theme integration<br/>- Class definitions
|
||||
|
||||
Note over API: Security Levels
|
||||
Note right of API: Modes:<br/>- sandbox: iframe isolation<br/>- loose: minimal sanitization<br/>- default: DOMPurify sanitization
|
||||
|
||||
Note over API: Helper Functions
|
||||
Note right of API: Utilities:<br/>- cleanUpSvgCode()<br/>- putIntoIFrame()<br/>- appendDivSvgG()<br/>- removeExistingElements()
|
@@ -0,0 +1,180 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapDb.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/detector.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapTypes.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/styles.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/svgDraw.ts"
|
||||
generationTime: 2025-01-28T16:00:00.000Z
|
||||
---
|
||||
sequenceDiagram
|
||||
participant User as User Input Text
|
||||
participant Detector as detector.ts
|
||||
participant Loader as DiagramLoader
|
||||
participant Definition as mindmap-definition.ts
|
||||
participant Parser as parser/mindmap.jison
|
||||
participant DB as MindmapDB
|
||||
participant Renderer as mindmapRenderer.ts
|
||||
participant Cytoscape as cytoscape.js
|
||||
participant SVGDraw as svgDraw.ts
|
||||
participant Styles as styles.ts
|
||||
participant Output as Final SVG
|
||||
|
||||
Note over User, Output: Mindmap Implementation Flow
|
||||
|
||||
%% Detection Phase
|
||||
User->>Detector: /^\s*mindmap/ text input
|
||||
activate Detector
|
||||
Detector->>Detector: detector(txt) validates pattern
|
||||
Detector->>Loader: loader() function called
|
||||
deactivate Detector
|
||||
|
||||
activate Loader
|
||||
Loader->>Definition: import mindmap-definition.js
|
||||
deactivate Loader
|
||||
|
||||
%% Core Structure Setup
|
||||
activate Definition
|
||||
Definition->>DB: get db() → new MindmapDB()
|
||||
Definition->>Renderer: setup renderer
|
||||
Definition->>Parser: setup parser
|
||||
Definition->>Styles: setup styles
|
||||
deactivate Definition
|
||||
|
||||
%% Database Initialization
|
||||
activate DB
|
||||
Note over DB: MindmapDB Constructor
|
||||
DB->>DB: initialize nodes array
|
||||
DB->>DB: setup nodeType constants
|
||||
DB->>DB: bind methods
|
||||
DB->>DB: clear() state
|
||||
|
||||
%% Parsing Phase
|
||||
activate Parser
|
||||
User->>Parser: mindmap syntax text
|
||||
|
||||
loop For each node in hierarchy
|
||||
Parser->>DB: addNode(level, id, descr, type)
|
||||
activate DB
|
||||
DB->>DB: sanitizeText(id, descr)
|
||||
DB->>DB: getType(startStr, endStr)
|
||||
Note right of DB: Shape Detection:<br/>[ → RECT<br/>( → ROUNDED_RECT<br/>(( → CIRCLE<br/>)) → BANG<br/>{{ → HEXAGON
|
||||
DB->>DB: getParent(level)
|
||||
DB->>DB: create MindmapNode
|
||||
DB->>DB: add to hierarchy
|
||||
deactivate DB
|
||||
end
|
||||
|
||||
opt Icon/Class Decoration
|
||||
Parser->>DB: decorateNode(decoration)
|
||||
DB->>DB: set icon/class properties
|
||||
end
|
||||
deactivate Parser
|
||||
|
||||
%% Data Preparation
|
||||
Renderer->>DB: getData() for layout
|
||||
activate DB
|
||||
DB->>DB: collect all nodes
|
||||
DB->>DB: build parent-child relationships
|
||||
DB-->>Renderer: return node hierarchy
|
||||
deactivate DB
|
||||
|
||||
%% Rendering Pipeline
|
||||
activate Renderer
|
||||
Note over Renderer: Rendering Phase
|
||||
|
||||
Renderer->>Cytoscape: initialize cytoscape
|
||||
activate Cytoscape
|
||||
|
||||
loop For each node in mindmap
|
||||
Renderer->>Cytoscape: addNodes(mindmap, cy, conf, level)
|
||||
Cytoscape->>Cytoscape: create node data
|
||||
Cytoscape->>Cytoscape: set position (x, y)
|
||||
end
|
||||
|
||||
loop For parent-child relationships
|
||||
Renderer->>Cytoscape: add edges
|
||||
Cytoscape->>Cytoscape: create edge data
|
||||
end
|
||||
|
||||
Renderer->>Cytoscape: configure cose-bilkent layout
|
||||
Cytoscape->>Cytoscape: calculate optimal positions
|
||||
Cytoscape-->>Renderer: return positioned graph
|
||||
deactivate Cytoscape
|
||||
|
||||
%% SVG Generation
|
||||
Renderer->>SVGDraw: drawNodes(db, svg, mindmap, section, conf)
|
||||
activate SVGDraw
|
||||
|
||||
loop For each node recursively
|
||||
SVGDraw->>SVGDraw: select shape function
|
||||
|
||||
alt Default Shape
|
||||
SVGDraw->>SVGDraw: defaultBkg() - rounded rectangle
|
||||
else Rectangle Shape
|
||||
SVGDraw->>SVGDraw: rectBkg() - sharp corners
|
||||
else Circle Shape
|
||||
SVGDraw->>SVGDraw: circleBkg() - perfect circle
|
||||
else Cloud Shape
|
||||
SVGDraw->>SVGDraw: cloudBkg() - organic curves
|
||||
else Bang Shape
|
||||
SVGDraw->>SVGDraw: bangBkg() - explosion style
|
||||
else Hexagon Shape
|
||||
SVGDraw->>SVGDraw: hexagonBkg() - six sides
|
||||
end
|
||||
|
||||
SVGDraw->>SVGDraw: create SVG elements
|
||||
SVGDraw->>SVGDraw: add text labels
|
||||
SVGDraw->>SVGDraw: position node
|
||||
|
||||
opt Node has children
|
||||
SVGDraw->>SVGDraw: drawNodes() recursive call
|
||||
end
|
||||
end
|
||||
deactivate SVGDraw
|
||||
|
||||
%% Edge Rendering
|
||||
Renderer->>Renderer: drawEdges(edgesEl, cy)
|
||||
loop For each edge
|
||||
Renderer->>Renderer: extract edge bounds
|
||||
Renderer->>Renderer: draw SVG path
|
||||
end
|
||||
|
||||
%% Styling Application
|
||||
Renderer->>Styles: getStyles(options)
|
||||
activate Styles
|
||||
|
||||
Styles->>Styles: genSections(options)
|
||||
loop For THEME_COLOR_LIMIT sections
|
||||
Styles->>Styles: generate color scale
|
||||
Styles->>Styles: create CSS rules
|
||||
Note right of Styles: .section-X fills<br/>.edge-depth-X widths<br/>.node-icon-X colors
|
||||
end
|
||||
|
||||
Styles->>Styles: apply theme integration
|
||||
Styles-->>Renderer: return compiled CSS
|
||||
deactivate Styles
|
||||
|
||||
%% Final Assembly
|
||||
Renderer->>Output: selectSvgElement()
|
||||
Renderer->>Output: setupGraphViewbox()
|
||||
Renderer->>Output: apply styles
|
||||
Renderer->>Output: add interactive elements
|
||||
deactivate Renderer
|
||||
|
||||
activate Output
|
||||
Note over Output: Final Mindmap Features
|
||||
Output->>Output: responsive layout
|
||||
Output->>Output: accessibility attributes
|
||||
Output->>Output: hover effects
|
||||
Output->>Output: click handling
|
||||
Output-->>User: rendered mindmap
|
||||
deactivate Output
|
||||
|
||||
%% Configuration Details
|
||||
Note over DB, Styles: Configuration Options
|
||||
Note right of DB: Padding Calculations:<br/>Base padding from config<br/>RECT: ×2 padding<br/>ROUNDED_RECT: ×2 padding<br/>HEXAGON: ×2 padding
|
||||
Note right of Styles: Section Management:<br/>MAX_SECTIONS = 12<br/>Dynamic color generation<br/>Git theme integration
|
||||
Note right of Renderer: Layout Parameters:<br/>Cytoscape configuration<br/>coseBilkent settings<br/>Node spacing rules
|
@@ -79,6 +79,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
LLM integrations to create mermaid diagrams using AI from text descriptions.
|
||||
|
||||
- [HueHive - Create mermaid diagrams with text](https://huehive.co/tools/diagrams)
|
||||
- [MCP Server Mermaid](https://github.com/hustcc/mcp-mermaid) - Generate mermaid diagram and chart with AI MCP dynamically.
|
||||
|
||||
### CRM/ERP
|
||||
|
||||
@@ -98,6 +99,7 @@ Blogging frameworks and platforms
|
||||
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
||||
- [WordPress](https://wordpress.org)
|
||||
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
||||
- [WP Documentation](https://wordpress.org/themes/wp-documentation/)
|
||||
|
||||
### CMS/ECM
|
||||
|
||||
|
@@ -10,9 +10,7 @@ Applications that support Mermaid files [SHOULD](https://datatracker.ietf.org/do
|
||||
|
||||
### MIME Type
|
||||
|
||||
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is `text/vnd.mermaid`.
|
||||
|
||||
Currently pending [IANA](https://www.iana.org/) recognition.
|
||||
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is [`text/vnd.mermaid`](https://www.iana.org/assignments/media-types/application/vnd.mermaid).
|
||||
|
||||
## Showcase
|
||||
|
||||
|
150
packages/mermaid/src/docs/layouts/development.md
Normal file
150
packages/mermaid/src/docs/layouts/development.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 🛠️ How to Create a New Layout Algorithm in Mermaid
|
||||
|
||||
Mermaid supports pluggable layout engines, and contributors can add custom layout algorithms to support specialized rendering needs such as clustered layouts, nested structures, or domain-specific visualizations.
|
||||
|
||||
This guide outlines the steps required to **create and integrate a new layout algorithm** into the Mermaid codebase.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Prerequisites
|
||||
|
||||
Before starting, ensure the following:
|
||||
|
||||
- You have [Node.js](https://nodejs.org/) installed.
|
||||
- You have [pnpm](https://pnpm.io/) installed globally:
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Step-by-Step Integration
|
||||
|
||||
### Refer [Mermaid Contributing Guide](../community/contributing.md)
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Implementing Your Custom Layout Algorithm
|
||||
|
||||
### 1. Create Your Layout Folder
|
||||
|
||||
Navigate to the relevant source directory and create a folder for your new algorithm:
|
||||
|
||||
```bash
|
||||
cd packages/mermaid/src/layout
|
||||
mkdir myCustomLayout
|
||||
touch myCustomLayout/index.ts
|
||||
```
|
||||
|
||||
> 📁 You can organize supporting files, utils, and types inside this folder.
|
||||
|
||||
### 2. Register the Layout Algorithm
|
||||
|
||||
Open the file:
|
||||
|
||||
```
|
||||
packages/mermaid/src/rendering-util/render.ts
|
||||
```
|
||||
|
||||
Inside the function `registerDefaultLayoutLoaders`, find the `layoutLoaders` array. Add your layout here:
|
||||
|
||||
```ts
|
||||
registerDefaultLayoutLoaders([
|
||||
...,
|
||||
{
|
||||
id: 'myCustomLayout',
|
||||
loader: () => import('../layout/myCustomLayout'),
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
This tells Mermaid how to load your layout dynamically by name (`id`).
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Your Algorithm
|
||||
|
||||
### 3. Create a Test File
|
||||
|
||||
To visually test your layout implementation, create a test HTML file in:
|
||||
|
||||
```
|
||||
cypress/platform/
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
touch cypress/platform/myCustomLayoutTest.html
|
||||
```
|
||||
|
||||
Inside the file, load your diagram like this:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module">
|
||||
import mermaid from '/dist/mermaid.esm.mjs';
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
layout: 'myCustomLayout', // Use your layout here
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mermaid">graph TD A[Node A] --> B[Node B] B --> C[Node C]</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 4. Open in Browser
|
||||
|
||||
After running `pnpm dev`, open your test in the browser:
|
||||
|
||||
```
|
||||
http://localhost:9000/myCustomLayoutTest.html
|
||||
```
|
||||
|
||||
You should see your diagram rendered using your new layout engine.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Tips
|
||||
|
||||
- Keep your layout algorithm modular and readable.
|
||||
- Use TypeScript types and helper functions for better structure.
|
||||
- Add comments and constraints where necessary.
|
||||
- If applicable, create a unit test and add a visual test for Cypress.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Example File Structure
|
||||
|
||||
```
|
||||
packages/
|
||||
└── mermaid/
|
||||
└── src/
|
||||
└── layout/
|
||||
└── myCustomLayout/
|
||||
├── index.ts
|
||||
├── utils.ts
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Checklist
|
||||
|
||||
- [ ] All dependencies installed via `pnpm i`
|
||||
- [ ] Layout folder and files created under `src/layout/`
|
||||
- [ ] Entry registered in `registerDefaultLayoutLoaders`
|
||||
- [ ] HTML test file added under `cypress/platform/`
|
||||
- [ ] Diagram renders as expected at `localhost:9000`
|
||||
- [ ] Code is linted and documented
|
||||
|
||||
---
|
||||
|
||||
> 💡 You’re now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!
|
132
packages/mermaid/src/docs/layouts/introduction.md
Normal file
132
packages/mermaid/src/docs/layouts/introduction.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 📊 Layout Algorithms in Mermaid
|
||||
|
||||
Mermaid is a popular JavaScript-based diagramming tool that supports auto-layout for graphs using pluggable layout engines. Layout algorithms play a critical role in rendering nodes and edges in a clean, readable, and meaningful way. Mermaid currently uses engines like **Dagre** and **ELK**, and will soon introduce a powerful new layout engine: **IPSep-CoLa**.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Dagre Layout
|
||||
|
||||
**Dagre** is a layout engine inspired by the **Sugiyama algorithm**, optimized for directed acyclic graphs (DAGs). It arranges nodes in layers and computes edge routing to minimize crossings and improve readability.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Layered (Sugiyama-style) layout**: Ideal for top-down or left-to-right flow.
|
||||
- **Edge routing**: Attempts to reduce edge crossings and bends.
|
||||
- **Ranking**: Vertices are assigned ranks to group related elements into the same level.
|
||||
- **Lightweight and fast**: Suitable for small to medium-sized graphs.
|
||||
|
||||
### Technical Overview:
|
||||
|
||||
- Works in four stages:
|
||||
1. **Cycle Removal**
|
||||
2. **Layer Assignment**
|
||||
3. **Node Ordering**
|
||||
4. **Coordinate Assignment**
|
||||
- Outputs crisp layouts where edge direction is clear and logical.
|
||||
|
||||
### Limitations:
|
||||
|
||||
- No native support for **grouped or nested structures**.
|
||||
- Not ideal for graphs with **non-hierarchical** or **dense cyclic connections**.
|
||||
- Limited edge label placement capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 🔸 ELK (Eclipse Layout Kernel)
|
||||
|
||||
**ELK** is a modular, extensible layout framework developed as part of the Eclipse ecosystem. It supports a wide variety of graph types and layout strategies.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Multiple layout styles**: Hierarchical, force-based, layered, orthogonal, etc.
|
||||
- **Support for ports**: Allows fine-grained edge anchoring on specific sides of nodes.
|
||||
- **Group and hierarchy awareness**: Ideal for nested and compartmentalized diagrams.
|
||||
- **Rich configuration**: Offers control over spacing, edge routing, direction, padding, and more.
|
||||
|
||||
### Technical Overview:
|
||||
|
||||
- Uses a **model-driven approach** with a well-defined intermediate representation (ELK Graph Model).
|
||||
- Different engines are plugged in depending on the chosen layout strategy.
|
||||
- Works well with large, complex, and deeply nested graphs.
|
||||
|
||||
### Limitations:
|
||||
|
||||
- Requires verbose configuration for best results.
|
||||
- Can be slower than Dagre for small or simple diagrams.
|
||||
- More complex to integrate and control dynamically.
|
||||
|
||||
---
|
||||
|
||||
## 🆕 IPSep-CoLa
|
||||
|
||||
### 🌐 Introduction
|
||||
|
||||
**IPSep-CoLa** stands for **Incremental Procedure for Separation Constraint Layout**, a next-generation layout algorithm tailored for **grouped, nested, and labeled graphs**. It is an enhancement over standard force-directed layouts, offering constraint enforcement and iterative refinement.
|
||||
|
||||
It is particularly useful for diagrams where:
|
||||
|
||||
- **Group integrity** is important (e.g., modules, clusters).
|
||||
- **Edge labels** need smart placement.
|
||||
- **Overlaps** must be prevented even under tight space constraints.
|
||||
|
||||
---
|
||||
|
||||
### ⚙️ How IPSep-CoLa Works
|
||||
|
||||
#### 1. **Constraint-Based Force Simulation**:
|
||||
|
||||
It builds on top of standard force-directed approaches (like CoLa), but adds **constraints** to influence the final positions of nodes:
|
||||
|
||||
- **Separation constraints**: Minimum distances between nodes, edge labels, and groups.
|
||||
- **Containment constraints**: Child nodes must stay within the bounds of parent groups.
|
||||
- **Alignment constraints**: Nodes can be aligned in rows or columns if desired.
|
||||
|
||||
#### 2. **Incremental Refinement**:
|
||||
|
||||
Unlike one-pass algorithms, IPSep-CoLa works in **phases**:
|
||||
|
||||
- Initial layout is produced using a base force simulation.
|
||||
- The layout is iteratively adjusted using **constraint solvers**.
|
||||
- Additional forces (spring, collision avoidance, containment) are incrementally added.
|
||||
|
||||
#### 3. **Edge Label Handling**:
|
||||
|
||||
One of the distinguishing features of IPSep-CoLa is its support for **multi-segment edge routing with mid-edge label positioning**, ensuring labels do not clutter or overlap.
|
||||
|
||||
---
|
||||
|
||||
### 📌 Use Cases
|
||||
|
||||
IPSep-CoLa is ideal for:
|
||||
|
||||
- **Hierarchical graphs** with complex nesting (e.g., software architecture, UML diagrams).
|
||||
- **Clustered views** (e.g., social network groupings).
|
||||
- **Diagrams with heavy labeling** where label placement affects readability.
|
||||
- **Diagrams with strict visual structure** needs — maintaining boundaries, margins, or padding.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Comparison Table
|
||||
|
||||
| Feature | Dagre | ELK | IPSep-CoLa |
|
||||
| ------------------------- | ----------- | ------------------- | ------------------------------ |
|
||||
| Layout Type | Layered DAG | Modular (varied) | Constraint-driven force layout |
|
||||
| Edge Labeling | ⚠️ Basic | ✅ Yes | ✅ Smart Placement |
|
||||
| Overlap Avoidance | ⚠️ Partial | ✅ Configurable | ✅ Automatic |
|
||||
| Layout Performance | ✅ Fast | ⚠️ Medium | ⚠️ Medium |
|
||||
| Customization Flexibility | ⚠️ Limited | ✅ Extensive | ✅ Moderate to High |
|
||||
| Best For | Simple DAGs | Complex hierarchies | Grouped and labeled graphs |
|
||||
|
||||
---
|
||||
|
||||
## 🧾 Summary
|
||||
|
||||
Each layout engine in Mermaid serves a different purpose:
|
||||
|
||||
- **Dagre** is best for fast, simple, and readable DAGs.
|
||||
- **ELK** is powerful for modular, layered, or port-based diagrams with a need for rich customization.
|
||||
- **IPSep-CoLa** will soon offer a flexible, constraint-respecting layout engine that excels at **visual clarity in grouped and complex diagrams**.
|
||||
|
||||
The addition of IPSep-CoLa to Mermaid's layout stack represents a significant leap forward in layout control and quality — making it easier than ever to visualize rich, structured, and annotated graphs.
|
||||
|
||||
---
|
@@ -0,0 +1,40 @@
|
||||
## IPSEPCOLA Documentation :
|
||||
|
||||
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
|
||||
|
||||
## How IPSep-CoLa built :
|
||||
|
||||
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
|
||||
|
||||
1. Layer Assignment :
|
||||
The layer assignment algorithm organizes nodes into hierarchical layers to create a structured layout for directed graphs. It begins by detecting and temporarily removing cyclic edges using a depth-first search (DFS) approach, ensuring the graph becomes a Directed Acyclic Graph (DAG) for proper layering. The algorithm then performs a topological sort using Kahn's method, calculating node ranks (layers) based on in-degree counts. Each node's layer is determined by its position in the topological order, with parent nodes always appearing in higher layers than their children to maintain proper flow direction.
|
||||
|
||||
The implementation handles special cases like nested nodes by considering parent-child relationships when calculating layers. Nodes without dependencies are placed in layer 0, while subsequent nodes are assigned to layers one level below their nearest parent. The algorithm efficiently processes nodes using a queue system, decrementing in-degrees as it progresses, and ultimately stores the layer information directly in the node objects. Though cyclic edges are removed during processing, they could potentially be reintroduced after layer assignment if needed for visualization purposes.
|
||||
|
||||
2. Node ordering:
|
||||
After assigning layers to nodes, this step organizes nodes horizontally within each layer to minimize edge crossings and create a clean, readable layout. It uses the barycenter method—a technique that positions each node based on the average position of its connected neighbors (either incoming or outgoing). Nodes with no connections are pushed to the end of their layer.
|
||||
|
||||
The algorithm works in multiple passes (iterations) to refine the order: first adjusting nodes based on their incoming connections (from the layer above), then outgoing connections (to the layer below). Group nodes (like containers) are handled separately—their position is determined by averaging the positions of their children, ensuring they stay properly aligned with their contents. This approach keeps the layout structured while reducing visual clutter.
|
||||
|
||||
3. AssignInitial positions to node :
|
||||
This step calculates the starting (x, y) positions for each node based on its assigned layer (vertical level) and order (horizontal position). Nodes are spaced evenly—horizontally using nodeSpacing and vertically using layerHeight. For example, a node in layer 2 with order 3 will be placed at (3 _ nodeSpacing, 2 _ layerHeight). This creates a grid-like structure where nodes align neatly in rows (layers) and columns (orders).
|
||||
|
||||
The initial positioning is simple but crucial—it provides a structured starting point before more advanced adjustments (like reducing edge crossings or compacting the layout) are applied. Group nodes follow the same logic, ensuring they align with their children. This method ensures a readable, organized foundation for further refinement.
|
||||
|
||||
4. Force-Directed Simulation with Constraints :
|
||||
|
||||
- Spring Forces: Attracts connected nodes to maintain desired edge lengths.
|
||||
- Repulsion Forces: Pushes nodes apart to prevent overlaps.
|
||||
- Group Constraints: Ensures child nodes stay near their parent groups.
|
||||
- Cooling Factor: Gradually reduces movement to stabilize the layout.
|
||||
|
||||
5. Incremental Refinement :
|
||||
|
||||
- Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
|
||||
- Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
|
||||
- Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
|
||||
|
||||
6. Adjusting the Final Layout :
|
||||
This step takes the calculated node positions and applies them to the visual elements of the graph. Nodes are placed at their assigned (x, y) coordinates—regular nodes are positioned directly, while group nodes (clusters) are rendered as containers that may include other nodes. Edges (connections between nodes) are drawn based on their start and end points, ensuring they follow the structured layout.
|
||||
|
||||
The adjustment phase bridges the mathematical layout with the actual rendering, updating the SVG or canvas elements to reflect the computed positions. This ensures that the graph is not only logically organized but also visually coherent, with proper spacing, alignment, and connections. The result is a clean, readable diagram ready for display.
|
180
packages/mermaid/src/docs/layouts/ipsepcola/overview.md
Normal file
180
packages/mermaid/src/docs/layouts/ipsepcola/overview.md
Normal file
@@ -0,0 +1,180 @@
|
||||
## IPSEPCOLA Documentation :
|
||||
|
||||
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
|
||||
|
||||
## Introduction :
|
||||
|
||||
IPSep-CoLa (Incremental Procedure for Separation Constraint Layout) is an advanced graph layout algorithm designed to handle complex diagrams with separation constraints, such as grouped nodes, edge labels, and hierarchical structures. Unlike traditional force-directed algorithms, IPSep-CoLa incrementally refines node positions while enforcing geometric constraints to prevent overlaps, maintain group cohesion, and optimize edge routing.
|
||||
|
||||
The algorithm is particularly effective for visualizing nested and clustered graphs, where maintaining clear separation between elements is crucial. It combines techniques from force-directed layout, constraint satisfaction, and incremental refinement to produce readable and aesthetically pleasing diagrams.
|
||||
|
||||
## How IPSep-CoLa Works :
|
||||
|
||||
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
|
||||
|
||||
1. Graph Preprocessing :
|
||||
Cycle Removal: Detects and temporarily removes cyclic dependencies to enable proper layering.
|
||||
Layer Assignment: Assigns nodes to hierarchical layers using topological sorting.
|
||||
Node Ordering: Uses the barycenter heuristic to minimize edge crossings within layers.
|
||||
|
||||
2. Force-Directed Simulation with Constraints :
|
||||
Spring Forces: Attracts connected nodes to maintain desired edge lengths.
|
||||
Repulsion Forces: Pushes nodes apart to prevent overlaps.
|
||||
Group Constraints: Ensures child nodes stay near their parent groups.
|
||||
Cooling Factor: Gradually reduces movement to stabilize the layout.
|
||||
|
||||
3. Incremental Refinement :
|
||||
Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
|
||||
Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
|
||||
Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
|
||||
|
||||
## Key Features :
|
||||
|
||||
1. Group-Aware Layout: Maintains separation between nested structures.
|
||||
2. Edge Label Placement: Uses edge labels as virtual nodes and automatically positions labels inside their parent groups.
|
||||
3. Stable Convergence: Uses cooling factors and incremental updates for smooth refinement.
|
||||
4. Support for Self-Loops & Parallel Edges: Avoids visual clutter with intelligent edge routing.
|
||||
|
||||
## Use Cases :
|
||||
|
||||
1. Hierarchical Diagrams (org charts, flowcharts, decision trees)
|
||||
2. Network Visualization (dependency graphs, data pipelines)
|
||||
3. Interactive Graph Editors (real-time layout adjustments)
|
||||
4. Clustered Data Visualization (UML diagrams, biological networks)
|
||||
|
||||
## **Examples**
|
||||
|
||||
### **Example 1**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
CEO --> MKT["Marketing Head"]
|
||||
CEO --> ENG["Engineering Head"]
|
||||
ENG --> DEV["Developer"]
|
||||
ENG --> QA["QA Tester"]
|
||||
```
|
||||
|
||||
### **Example 2**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
Start["Start"] --> Red{"Is it red?"}
|
||||
Red -- Yes --> Round{"Is it round?"}
|
||||
Red -- No --> NotApple["❌ Not an Apple"]
|
||||
Round -- Yes --> Apple["✅ It's an Apple"]
|
||||
Round -- No --> NotApple2["❌ Not an Apple"]
|
||||
```
|
||||
|
||||
### **Example 3**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
A[Module A] --> B[Module B]
|
||||
A --> C[Module C]
|
||||
B --> D[Module D]
|
||||
C --> D
|
||||
D --> E[Module E]
|
||||
```
|
||||
|
||||
### **Example 4**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
Source1["📦 Raw Data (CSV)"]
|
||||
Source2["🌐 API Data"]
|
||||
|
||||
Source1 --> Clean["🧹 Clean & Format"]
|
||||
Source2 --> Clean
|
||||
|
||||
Clean --> Transform["🔄 Transform Data"]
|
||||
Transform --> Load["📥 Load into Data Warehouse"]
|
||||
Load --> BI["📊 BI Dashboard"]
|
||||
```
|
||||
|
||||
### **Example 5**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
classDiagram
|
||||
class Person {
|
||||
-String name
|
||||
-int age
|
||||
+greet(): void
|
||||
}
|
||||
|
||||
class Employee {
|
||||
-int employeeId
|
||||
+calculateSalary(): float
|
||||
}
|
||||
|
||||
class Manager {
|
||||
-String department
|
||||
+assignTask(): void
|
||||
}
|
||||
|
||||
Person <|-- Employee
|
||||
Employee <|-- Manager
|
||||
```
|
||||
|
||||
### **Example 6**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Sunlight["☀️ Sunlight"] --> Leaf["🌿 Leaf"]
|
||||
Leaf --> Glucose["🍬 Glucose"]
|
||||
Leaf --> Oxygen["💨 Oxygen"]
|
||||
```
|
||||
|
||||
### **Example 7**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Internet["🌐 Internet"] --> Router["📡 Router"]
|
||||
Router --> Server1["🖥️ Server A"]
|
||||
Router --> Server2["🖥️ Server B"]
|
||||
Router --> Laptop["💻 Laptop"]
|
||||
|
||||
%% New device joins
|
||||
Router --> Mobile["📱 Mobile"]
|
||||
```
|
||||
|
||||
## Limitations :
|
||||
|
||||
1. Computational Cost: More iterations may be needed for large graphs (>1000 nodes).
|
||||
2. Parameter Tuning: Requires adjustments for different graph types.
|
||||
3. Non-Determinism: Small variations may occur between runs due to force simulation.
|
||||
|
||||
## Conclusion :
|
||||
|
||||
IPSep-CoLa provides a robust solution for constraint-based graph layout, particularly for structured and clustered diagrams. By combining incremental refinement with separation constraints, it achieves readable and well-organized visualizations. Future improvements could include GPU acceleration and adaptive parameter tuning for large-scale graphs.
|
@@ -1135,15 +1135,46 @@ It is possible to style the type of curve used for lines between items, if the d
|
||||
Available curve styles include `basis`, `bumpX`, `bumpY`, `cardinal`, `catmullRom`, `linear`, `monotoneX`, `monotoneY`,
|
||||
`natural`, `step`, `stepAfter`, and `stepBefore`.
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
|
||||
Line styling can be achieved in two ways:
|
||||
|
||||
1. Change the curve style of all the lines
|
||||
2. Change the curve style of a particular line
|
||||
|
||||
#### Diagram level curve style
|
||||
|
||||
In this example, a left-to-right graph uses the `stepBefore` curve style:
|
||||
|
||||
```
|
||||
%%{ init: { 'flowchart': { 'curve': 'stepBefore' } } }%%
|
||||
---
|
||||
config:
|
||||
flowchart:
|
||||
curve: stepBefore
|
||||
---
|
||||
graph LR
|
||||
```
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
#### Edge level curve style using Edge IDs (v<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
You can assign IDs to [edges](#attaching-an-id-to-edges). After assigning an ID you can modify the line style by modifying the edge's `curve` property using the following syntax:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
A e2@--> C
|
||||
e1@{ curve: linear }
|
||||
e2@{ curve: natural }
|
||||
```
|
||||
|
||||
```info
|
||||
Any edge curve style modified at the edge level overrides the diagram level style.
|
||||
```
|
||||
|
||||
```info
|
||||
If the same edge is modified multiple times the last modification will be rendered.
|
||||
```
|
||||
|
||||
### Styling a node
|
||||
|
||||
|
@@ -1,40 +1,5 @@
|
||||
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// -------------------------------------
|
||||
// Mocks and mocking
|
||||
|
||||
import { MockedD3 } from './tests/MockedD3.js';
|
||||
|
||||
// Note: If running this directly from within an IDE, the mocks directory must be at packages/mermaid/mocks
|
||||
vi.mock('d3');
|
||||
vi.mock('dagre-d3');
|
||||
|
||||
// mermaidAPI.spec.ts:
|
||||
import * as accessibility from './accessibility.js'; // Import it this way so we can use spyOn(accessibility,...)
|
||||
vi.mock('./accessibility.js', () => ({
|
||||
setA11yDiagramInfo: vi.fn(),
|
||||
addSVGa11yTitleDescription: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the renderers specifically so we can test render(). Need to mock draw() for each renderer
|
||||
vi.mock('./diagrams/c4/c4Renderer.js');
|
||||
vi.mock('./diagrams/class/classRenderer.js');
|
||||
vi.mock('./diagrams/class/classRenderer-v2.js');
|
||||
vi.mock('./diagrams/er/erRenderer.js');
|
||||
vi.mock('./diagrams/flowchart/flowRenderer-v2.js');
|
||||
vi.mock('./diagrams/git/gitGraphRenderer.js');
|
||||
vi.mock('./diagrams/gantt/ganttRenderer.js');
|
||||
vi.mock('./diagrams/user-journey/journeyRenderer.js');
|
||||
vi.mock('./diagrams/pie/pieRenderer.js');
|
||||
vi.mock('./diagrams/packet/renderer.js');
|
||||
vi.mock('./diagrams/xychart/xychartRenderer.js');
|
||||
vi.mock('./diagrams/requirement/requirementRenderer.js');
|
||||
vi.mock('./diagrams/sequence/sequenceRenderer.js');
|
||||
vi.mock('./diagrams/radar/renderer.js');
|
||||
vi.mock('./diagrams/architecture/architectureRenderer.js');
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
import assignWithDepth from './assignWithDepth.js';
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
import mermaid from './mermaid.js';
|
||||
@@ -75,6 +40,9 @@ import { SequenceDB } from './diagrams/sequence/sequenceDb.js';
|
||||
import { decodeEntities, encodeEntities } from './utils.js';
|
||||
import { toBase64 } from './utils/base64.js';
|
||||
import { StateDB } from './diagrams/state/stateDb.js';
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
import { select } from 'd3';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
/**
|
||||
* @see https://vitest.dev/guide/mocking.html Mock part of a module
|
||||
@@ -225,63 +193,49 @@ describe('mermaidAPI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const fauxParentNode = new MockedD3();
|
||||
const fauxEnclosingDiv = new MockedD3();
|
||||
const fauxSvgNode = new MockedD3();
|
||||
|
||||
describe('appendDivSvgG', () => {
|
||||
const fauxGNode = new MockedD3();
|
||||
const parent_append_spy = vi.spyOn(fauxParentNode, 'append').mockReturnValue(fauxEnclosingDiv);
|
||||
const div_append_spy = vi.spyOn(fauxEnclosingDiv, 'append').mockReturnValue(fauxSvgNode);
|
||||
// @ts-ignore @todo TODO why is this getting a type error?
|
||||
const div_attr_spy = vi.spyOn(fauxEnclosingDiv, 'attr').mockReturnValue(fauxEnclosingDiv);
|
||||
const svg_append_spy = vi.spyOn(fauxSvgNode, 'append').mockReturnValue(fauxGNode);
|
||||
// @ts-ignore @todo TODO why is this getting a type error?
|
||||
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
|
||||
// cspell:ignore dthe
|
||||
|
||||
it('appends a div node', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(parent_append_spy).toHaveBeenCalledWith('div');
|
||||
expect(div_append_spy).toHaveBeenCalledWith('svg');
|
||||
jsdomIt('appends a div node', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const divNode = ensureNodeFromSelector('div');
|
||||
const svgNode = ensureNodeFromSelector('svg', divNode);
|
||||
ensureNodeFromSelector('g', svgNode);
|
||||
});
|
||||
it('the id for the div is "d" with the id appended', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
|
||||
jsdomIt('the id for the div is "d" with the id appended', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const divNode = ensureNodeFromSelector('div');
|
||||
expect(divNode?.getAttribute('id')).toBe('dtheId');
|
||||
});
|
||||
|
||||
it('sets the style for the div if one is given', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'given div style', 'given x link');
|
||||
expect(div_attr_spy).toHaveBeenCalledWith('style', 'given div style');
|
||||
jsdomIt('sets the style for the div if one is given', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId', 'given div style', 'given x link');
|
||||
const divNode = ensureNodeFromSelector('div');
|
||||
expect(divNode?.getAttribute('style')).toBe('given div style');
|
||||
});
|
||||
|
||||
it('appends a svg node to the div node', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
|
||||
jsdomIt('sets the svg width to 100%', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const svgNode = ensureNodeFromSelector('div > svg');
|
||||
expect(svgNode.getAttribute('width')).toBe('100%');
|
||||
});
|
||||
it('sets the svg width to 100%', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%');
|
||||
jsdomIt('the svg id is the id', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const svgNode = ensureNodeFromSelector('div > svg');
|
||||
expect(svgNode.getAttribute('id')).toBe('theId');
|
||||
});
|
||||
it('the svg id is the id', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId');
|
||||
jsdomIt('the svg xml namespace is the 2000 standard', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const svgNode = ensureNodeFromSelector('div > svg');
|
||||
expect(svgNode.getAttribute('xmlns')).toBe('http://www.w3.org/2000/svg');
|
||||
});
|
||||
it('the svg xml namespace is the 2000 standard', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg');
|
||||
jsdomIt('sets the svg xlink if one is given', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId', 'div style', 'given x link');
|
||||
const svgNode = ensureNodeFromSelector('div > svg');
|
||||
expect(svgNode.getAttribute('xmlns:xlink')).toBe('given x link');
|
||||
});
|
||||
it('sets the svg xlink if one is given', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'div style', 'given x link');
|
||||
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns:xlink', 'given x link');
|
||||
});
|
||||
it('appends a g (group) node to the svg node', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(svg_append_spy).toHaveBeenCalledWith('g');
|
||||
});
|
||||
it('returns the given parentRoot d3 nodes', () => {
|
||||
expect(appendDivSvgG(fauxParentNode, 'theId', 'dtheId')).toEqual(fauxParentNode);
|
||||
jsdomIt('returns the given parentRoot d3 nodes', ({ body }) => {
|
||||
expect(appendDivSvgG(body, 'theId', 'dtheId')).toEqual(body);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -782,9 +736,9 @@ graph TD;A--x|text including URL space|B;`)
|
||||
// render(id, text, cb?, svgContainingElement?)
|
||||
|
||||
// Test all diagram types. Note that old flowchart 'graph' type will invoke the flowRenderer-v2. (See the flowchart v2 detector.)
|
||||
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different than was is put in the diagram text (ex: in -v2 diagrams)
|
||||
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different from what is put in the diagram text (ex: in -v2 diagrams)
|
||||
const diagramTypesAndExpectations = [
|
||||
{ textDiagramType: 'C4Context', expectedType: 'c4' },
|
||||
// { textDiagramType: 'C4Context', expectedType: 'c4' }, TODO : setAccTitle not called in C4 jison parser
|
||||
{ textDiagramType: 'classDiagram', expectedType: 'class' },
|
||||
{ textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' },
|
||||
{ textDiagramType: 'erDiagram', expectedType: 'er' },
|
||||
@@ -796,7 +750,11 @@ graph TD;A--x|text including URL space|B;`)
|
||||
{ textDiagramType: 'pie', expectedType: 'pie' },
|
||||
{ textDiagramType: 'packet', expectedType: 'packet' },
|
||||
{ textDiagramType: 'packet-beta', expectedType: 'packet' },
|
||||
{ textDiagramType: 'xychart-beta', expectedType: 'xychart' },
|
||||
{
|
||||
textDiagramType: 'xychart-beta',
|
||||
expectedType: 'xychart',
|
||||
content: 'x-axis "Attempts" 10000 --> 10000\ny-axis "Passing tests" 1 --> 1\nbar [1]',
|
||||
},
|
||||
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
|
||||
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
|
||||
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
|
||||
@@ -812,20 +770,25 @@ graph TD;A--x|text including URL space|B;`)
|
||||
diagramTypesAndExpectations.forEach((testedDiagram) => {
|
||||
describe(`${testedDiagram.textDiagramType}`, () => {
|
||||
const diagramType = testedDiagram.textDiagramType;
|
||||
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`;
|
||||
const content = testedDiagram.content || '';
|
||||
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n ${content}`;
|
||||
const expectedDiagramType = testedDiagram.expectedType;
|
||||
|
||||
it('should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription', async () => {
|
||||
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
|
||||
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
|
||||
const result = await mermaidAPI.render(id, diagramText);
|
||||
expect(result.diagramType).toBe(expectedDiagramType);
|
||||
expect(a11yDiagramInfo_spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expectedDiagramType
|
||||
);
|
||||
expect(a11yTitleDesc_spy).toHaveBeenCalled();
|
||||
});
|
||||
jsdomIt(
|
||||
'should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription',
|
||||
async () => {
|
||||
const { svg } = await mermaidAPI.render(id, diagramText);
|
||||
const dom = new JSDOM(svg);
|
||||
const svgNode = ensureNodeFromSelector('svg', dom.window.document);
|
||||
const descNode = ensureNodeFromSelector('desc', svgNode);
|
||||
const titleNode = ensureNodeFromSelector('title', svgNode);
|
||||
expect(svgNode.getAttribute('aria-roledescription')).toBe(expectedDiagramType);
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${id}`);
|
||||
expect(descNode.getAttribute('id')).toBe(`chart-desc-${id}`);
|
||||
expect(descNode.innerHTML).toBe(a11yDescr);
|
||||
expect(titleNode.innerHTML).toBe(a11yTitle);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user