Compare commits

..

3 Commits

Author SHA1 Message Date
shubham-mermaid
526374e04a on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com> 2025-07-15 00:40:44 +05:30
shubham-mermaid
5493d4344c Merge branch 'develop' into test-validate-lockfile-workflow 2025-07-15 00:40:30 +05:30
omkarht
63d84f125b added changeset 2025-07-14 19:40:53 +05:30
122 changed files with 2961 additions and 10594 deletions

View File

@@ -1,5 +0,0 @@
---
'@mermaid-js/mermaid-zenuml': patch
---
Fixed a critical bug that the ZenUML diagram is not rendered.

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
feat: Added IpSepCoLa algorithm for layout

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Add `getRegisteredDiagramsMetadata` to `mermaid`, which returns all the registered diagram IDs in mermaid

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Update casing of ID in requirement diagram

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
feat: Added support for per link curve styling in flowchart diagram using edge ids

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Make flowchart elk detector regex match less greedy

View File

@@ -1,8 +0,0 @@
---
'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).

View File

@@ -1,7 +0,0 @@
---
'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.

View File

@@ -0,0 +1,5 @@
---
'@mermaid-js/examples': minor
---
feat: Add examples for diagrams in the `@mermaid-js/examples` package

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Add escaped class literal name on namespace

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
chore: migrate to class-based ArchitectureDB implementation

View File

@@ -0,0 +1,7 @@
---
'@mermaid-js/examples': patch
'mermaid': patch
'@mermaid-js/parser': patch
---
chore: Move packet diagram out of beta

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Update flowchart direction TD's behavior to be the same as TB

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: adjust sequence diagram title positioning to prevent overlap with top border in Safari

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
chore: downgrade chokidar to 3.6.0 and ignore updates for hot reload stability

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
chore: Update packet diagram to use new class-based database structure

View File

@@ -0,0 +1,7 @@
---
'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.

View File

@@ -12,7 +12,6 @@ gantt
gitgraph
gzipped
handDrawn
ipsepcola
kanban
marginx
marginy

3
.github/lychee.toml vendored
View File

@@ -52,9 +52,6 @@ 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",

View File

@@ -58,7 +58,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Commit and create pull request
uses: peter-evans/create-pull-request@07cbaebb4bfc9c5d7db426ea5a5f585df29dd0a0
uses: peter-evans/create-pull-request@889dce9eaba7900ce30494f5e1ac7220b27e5c81
with:
add-paths: |
cypress/timings.json

View File

@@ -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,12 +46,11 @@ 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
@@ -63,6 +62,7 @@ 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.

View File

@@ -1 +0,0 @@
./packages/mermaid/CHANGELOG.md

1005
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

13
__mocks__/d3.ts Normal file
View File

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

View File

@@ -384,17 +384,4 @@ 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
`,
{}
);
});
});

View File

@@ -495,34 +495,4 @@ 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\`
`,
{}
);
});
});

View File

@@ -354,19 +354,4 @@ 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' } }
);
});
});
});

View File

@@ -1113,24 +1113,4 @@ 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 }
`
);
});
});

View File

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

View File

@@ -246,22 +246,5 @@ 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 */
});

View File

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

View File

@@ -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 layout tests. (The layouts are tested with integration tests.)
Unit tests are mandatory for all code except the renderers. (The renderers are tested with integration tests.)
We use [Vitest](https://vitest.dev) to run unit tests.
@@ -327,30 +327,6 @@ 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.

View File

@@ -10,7 +10,7 @@
# Interface: LayoutData
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)
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)
## Indexable
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:153](https://github.co
> **config**: [`MermaidConfig`](MermaidConfig.md)
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)
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)
---
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:156](https://github.co
> **edges**: `Edge`\[]
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)
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)
---
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:155](https://github.co
> **nodes**: `Node`\[]
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)
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)

View File

@@ -10,7 +10,7 @@
# Interface: ParseOptions
Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72)
Defined in: [packages/mermaid/src/types.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L59)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mer
> `optional` **suppressErrors**: `boolean`
Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77)
Defined in: [packages/mermaid/src/types.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L64)
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
The `parseError` function will not be called.

View File

@@ -10,7 +10,7 @@
# Interface: ParseResult
Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
Defined in: [packages/mermaid/src/types.ts:67](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L67)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mer
> **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
Defined in: [packages/mermaid/src/types.ts:75](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L75)
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:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
Defined in: [packages/mermaid/src/types.ts:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L71)
The diagram type, e.g. 'flowchart', 'sequence', etc.

View File

@@ -10,7 +10,7 @@
# Interface: RenderResult
Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
Defined in: [packages/mermaid/src/types.ts:85](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L85)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mer
> `optional` **bindFunctions**: (`element`) => `void`
Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
Defined in: [packages/mermaid/src/types.ts:103](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L103)
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:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
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:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
The svg code for the rendered graph.

View File

@@ -1,189 +0,0 @@
---
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"]

View File

@@ -1,307 +0,0 @@
---
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()

View File

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

View File

@@ -84,7 +84,6 @@ 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
@@ -104,7 +103,6 @@ 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

View File

@@ -16,7 +16,9 @@ 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`](https://www.iana.org/assignments/media-types/application/vnd.mermaid).
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.
## Showcase

View File

@@ -1,156 +0,0 @@
> **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
---
> 💡 Youre now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!

View File

@@ -1,138 +0,0 @@
> **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.
---

View File

@@ -1,46 +0,0 @@
> **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.

View File

@@ -1,186 +0,0 @@
> **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.

View File

@@ -1795,54 +1795,15 @@ 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:
```
---
config:
flowchart:
curve: stepBefore
---
%%{ init: { 'flowchart': { 'curve': 'stepBefore' } } }%%
graph LR
```
#### 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.
```
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.
### Styling a node

View File

@@ -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.4",
"@cspell/eslint-plugin": "^8.19.3",
"@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.8",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-cypress": "^4.3.0",
"eslint-plugin-html": "^8.1.3",
"eslint-plugin-jest": "^28.14.0",
"eslint-plugin-jsdoc": "^50.8.0",
"eslint-plugin-html": "^8.1.2",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-jsdoc": "^50.6.9",
"eslint-plugin-json": "^4.0.1",
"eslint-plugin-lodash": "^8.0.0",
"eslint-plugin-markdown": "^5.1.0",
"eslint-plugin-no-only-tests": "^3.3.0",
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-unicorn": "^59.0.1",
"eslint-plugin-unicorn": "^59.0.0",
"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.1.0",
"jsdom": "^26.0.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.38.0",
"typescript-eslint": "^8.32.0",
"vite": "^7.0.3",
"vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.0.6"
@@ -139,13 +139,8 @@
"roughjs": "patches/roughjs.patch"
},
"onlyBuiltDependencies": [
"canvas",
"cypress",
"esbuild"
],
"ignoredBuiltDependencies": [
"sharp",
"vue-demi"
]
}
}

View File

@@ -1,14 +0,0 @@
# @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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/examples",
"version": "1.0.0",
"version": "0.0.1-beta.1",
"description": "Mermaid examples package",
"author": "Sidharth Vinod",
"type": "module",
@@ -16,10 +16,6 @@
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid"
},
"scripts": {
"clean": "rimraf dist"
},

View File

@@ -1,384 +0,0 @@
# @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)

View File

@@ -0,0 +1 @@
../mermaid/src/docs/syntax/zenuml.md

View File

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

View File

@@ -1,11 +0,0 @@
declare module '@zenuml/core' {
interface RenderOptions {
theme?: string;
mode?: string;
}
export default class ZenUml {
constructor(container: Element);
render(text: string, options?: RenderOptions): Promise<void>;
}
}

View File

@@ -53,6 +53,7 @@ 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

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "11.9.0",
"version": "11.8.1",
"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.22",
"katex": "^0.16.9",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^16.0.0",
@@ -105,14 +105,13 @@
"@types/stylis": "^4.2.7",
"@types/uuid": "^10.0.0",
"ajv": "^8.17.1",
"canvas": "^3.1.0",
"chokidar": "3.6.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.1.0",
"jsdom": "^26.0.0",
"json-schema-to-typescript": "^15.0.4",
"micromatch": "^4.0.8",
"path-browserify": "^1.0.1",

View File

@@ -1,25 +1,28 @@
import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js';
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
import { expect } from 'vitest';
import { MockedD3 } from './tests/MockedD3.js';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
import type { D3Element } from './types.js';
describe('accessibility', () => {
const fauxSvgNode: MockedD3 = new MockedD3();
describe('setA11yDiagramInfo', () => {
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 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 aria-roledescription to the diagram type', ({ svg }) => {
setA11yDiagramInfo(svg, 'flowchart');
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-roledescription')).toBe('flowchart');
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 not set aria-roledescription if the diagram type is empty', ({ svg }) => {
setA11yDiagramInfo(svg, '');
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-roledescription')).toBeNull();
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
});
});
@@ -36,78 +39,115 @@ 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';
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-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByItTitleId(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 set aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
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 title tag as the first child with the text set to the accTitle given', () => {
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2);
});
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);
}
);
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
});
describe(`without a11y description`, {}, () => {
describe(`without a11y description`, () => {
const a11yDesc = undefined;
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-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
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 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 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 title tag as the first child with the text set to the accTitle given', () => {
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
jsdomIt('should not insert description tag', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const descNode = svgNode.querySelector('desc');
expect(descNode).toBeNull();
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');
});
});
});
@@ -118,66 +158,55 @@ describe('accessibility', () => {
describe('with a11y description', () => {
const a11yDesc = 'a11y description';
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-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 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 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 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 set aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
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);
}
);
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
});
describe('without a11y description', () => {
const a11yDesc = undefined;
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-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-describedby', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-describedby')).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 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 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 description tag', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const descNode = svgNode.querySelector('desc');
expect(descNode).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');
});
});
});

View File

@@ -78,41 +78,5 @@ 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');
});
});
});

View File

@@ -1,12 +1,21 @@
import { it, describe, expect } from 'vitest';
import { db } from './architectureDb.js';
import { parser } from './architectureParser.js';
import { ArchitectureDB } from './architectureDb.js';
const {
clear,
getDiagramTitle,
getAccTitle,
getAccDescription,
getServices,
getGroups,
getEdges,
getJunctions,
} = db;
describe('architecture diagrams', () => {
let db: ArchitectureDB;
beforeEach(() => {
db = new ArchitectureDB();
// @ts-expect-error since type is set to undefined we will have error
parser.parser?.yy = db;
clear();
});
describe('architecture diagram definitions', () => {
@@ -27,7 +36,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(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
});
it('should handle title on another line', async () => {
@@ -35,7 +44,7 @@ describe('architecture diagrams', () => {
title Simple Architecture Diagram
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
});
it('should handle accessibility title and description', async () => {
@@ -44,8 +53,8 @@ describe('architecture diagrams', () => {
accDescr: Accessibility Description
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getAccTitle()).toBe('Accessibility Title');
expect(db.getAccDescription()).toBe('Accessibility Description');
expect(getAccTitle()).toBe('Accessibility Title');
expect(getAccDescription()).toBe('Accessibility Description');
});
it('should handle multiline accessibility description', async () => {
@@ -55,7 +64,7 @@ describe('architecture diagrams', () => {
}
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getAccDescription()).toBe('Accessibility Description');
expect(getAccDescription()).toBe('Accessibility Description');
});
});
});

View File

@@ -1,9 +1,8 @@
import { getConfig as commonGetConfig } from '../../config.js';
import type { ArchitectureDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import type { DiagramDB } from '../../diagram-api/types.js';
import { getConfig as commonGetConfig } from '../../config.js';
import type { D3Element } from '../../types.js';
import { cleanAndMerge } from '../../utils.js';
import { ImperativeState } from '../../utils/imperativeState.js';
import {
clear as commonClear,
getAccDescription,
@@ -15,6 +14,7 @@ import {
} from '../common/commonDb.js';
import type {
ArchitectureAlignment,
ArchitectureDB,
ArchitectureDirectionPair,
ArchitectureDirectionPairMap,
ArchitectureEdge,
@@ -33,333 +33,330 @@ 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> = {};
constructor() {
this.clear();
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`);
}
}
public clear(): void {
this.nodes = {};
this.groups = {};
this.edges = [];
this.registeredIds = {};
this.dataStructures = undefined;
this.elements = {};
commonClear();
state.records.registeredIds[id] = 'node';
state.records.nodes[id] = {
id,
type: 'service',
icon,
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) {
throw new Error(
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
);
}
if (state.records.registeredIds[parent] === 'node') {
throw new Error(`The group [${id}]'s parent is not a group`);
}
}
public addService({
state.records.registeredIds[id] = 'group';
state.records.groups[id] = {
id,
icon,
in: parent,
title,
iconText,
}: Omit<ArchitectureService, 'edges'>): void {
if (this.registeredIds[id] !== undefined) {
throw new Error(
`The service id [${id}] is already in use by another ${this.registeredIds[id]}`
);
}
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`);
}
}
in: parent,
};
};
const getGroups = (): ArchitectureGroup[] => {
return Object.values(state.records.groups);
};
this.registeredIds[id] = 'node';
this.nodes[id] = {
id,
type: 'service',
icon,
iconText,
title,
edges: [],
in: parent,
};
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);
}
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({
const edge = {
lhsId,
rhsId,
lhsDir,
rhsDir,
lhsInto,
rhsInto,
lhsGroup,
rhsId,
rhsDir,
rhsInto,
rhsGroup,
title,
}: 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)}`
);
}
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]);
}
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]);
}
};
public getEdges(): ArchitectureEdge[] {
return this.edges;
}
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
*/
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'>>
> = {};
/**
* 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(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;
}
}
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);
}
});
// 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;
}
}
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]));
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);
}
});
}
}
this.dataStructures = {
adjList,
spatialMaps,
groupAlignments,
};
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]));
}
return this.dataStructures;
state.records.dataStructures = {
adjList,
spatialMaps,
groupAlignments,
};
}
return state.records.dataStructures;
};
public setElementForId(id: string, element: D3Element): void {
this.elements[id] = element;
}
const setElementForId = (id: string, element: D3Element) => {
state.records.elements[id] = element;
};
const getElementById = (id: string) => state.records.elements[id];
public getElementById(id: string): D3Element {
return this.elements[id];
}
const getConfig = (): Required<ArchitectureDiagramConfig> => {
const config = cleanAndMerge({
...DEFAULT_ARCHITECTURE_CONFIG,
...commonGetConfig().architecture,
});
return config;
};
public getConfig(): Required<ArchitectureDiagramConfig> {
return cleanAndMerge({
...DEFAULT_ARCHITECTURE_CONFIG,
...commonGetConfig().architecture,
});
}
export const db: ArchitectureDB = {
clear,
setDiagramTitle,
getDiagramTitle,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
getConfig,
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;
}
addService,
getServices,
addJunction,
getJunctions,
getNodes,
getNode,
addGroup,
getGroups,
addEdge,
getEdges,
setElementForId,
getElementById,
getDataStructures,
};
/**
* 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 db.getConfig()[field];
// }
export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
field: T
): Required<ArchitectureDiagramConfig>[T] {
return getConfig()[field];
}

View File

@@ -1,14 +1,12 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { parser } from './architectureParser.js';
import { ArchitectureDB } from './architectureDb.js';
import { db } from './architectureDb.js';
import styles from './architectureStyles.js';
import { renderer } from './architectureRenderer.js';
export const diagram: DiagramDefinition = {
parser,
get db() {
return new ArchitectureDB();
},
db,
renderer,
styles,
};

View File

@@ -1,33 +1,24 @@
import type { Architecture } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import { ArchitectureDB } from './architectureDb.js';
import type { ArchitectureDB } from './architectureTypes.js';
import { db } from './architectureDb.js';
const populateDb = (ast: Architecture, db: ArchitectureDB) => {
populateCommonDb(ast, db);
ast.groups.map((group) => db.addGroup(group));
ast.groups.map(db.addGroup);
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((edge) => db.addEdge(edge));
ast.edges.map(db.addEdge);
};
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);
},
};

View File

@@ -1,3 +1,4 @@
import { registerIconPacks } from '../../rendering-util/icons.js';
import type { Position } from 'cytoscape';
import cytoscape from 'cytoscape';
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
@@ -6,10 +7,9 @@ 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 type { ArchitectureDB } from './architectureDb.js';
import { getConfigField } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js';
import type {
ArchitectureAlignment,
@@ -22,6 +22,7 @@ import type {
NodeSingularData,
} from './architectureTypes.js';
import {
type ArchitectureDB,
type ArchitectureDirection,
type ArchitectureEdge,
type ArchitectureGroup,
@@ -43,7 +44,7 @@ registerIconPacks([
]);
cytoscape.use(fcose);
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
services.forEach((service) => {
cy.add({
group: 'nodes',
@@ -53,15 +54,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: Ar
icon: service.icon,
label: service.title,
parent: service.in,
width: db.getConfigField('iconSize'),
height: db.getConfigField('iconSize'),
width: getConfigField('iconSize'),
height: getConfigField('iconSize'),
} as NodeSingularData,
classes: 'node-service',
});
});
}
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) {
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
junctions.forEach((junction) => {
cy.add({
group: 'nodes',
@@ -69,8 +70,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db:
type: 'junction',
id: junction.id,
parent: junction.in,
width: db.getConfigField('iconSize'),
height: db.getConfigField('iconSize'),
width: getConfigField('iconSize'),
height: getConfigField('iconSize'),
} as NodeSingularData,
classes: 'node-junction',
});
@@ -256,8 +257,7 @@ function getAlignments(
}
function getRelativeConstraints(
spatialMaps: ArchitectureSpatialMap[],
db: ArchitectureDB
spatialMaps: ArchitectureSpatialMap[]
): 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 * db.getConfigField('iconSize'),
gap: 1.5 * getConfigField('iconSize'),
});
}
});
@@ -353,7 +353,7 @@ function layoutArchitecture(
style: {
'text-valign': 'bottom',
'text-halign': 'center',
'font-size': `${db.getConfigField('fontSize')}px`,
'font-size': `${getConfigField('fontSize')}px`,
},
},
{
@@ -375,32 +375,23 @@ function layoutArchitecture(
selector: '.node-group',
style: {
// @ts-ignore Incorrect library types
padding: `${db.getConfigField('padding')}px`,
padding: `${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, db);
addJunctions(junctions, cy, db);
addServices(services, cy);
addJunctions(junctions, cy);
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, db);
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
const layout = cy.layout({
name: 'fcose',
@@ -415,9 +406,7 @@ function layoutArchitecture(
const { parent: parentA } = nodeData(nodeA);
const { parent: parentB } = nodeData(nodeB);
const elasticity =
parentA === parentB
? 1.5 * db.getConfigField('iconSize')
: 0.5 * db.getConfigField('iconSize');
parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize');
return elasticity;
},
edgeElasticity(edge: EdgeSingular) {
@@ -537,11 +526,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, db);
await drawGroups(groupElem, cy, db);
await drawEdges(edgesElem, cy);
await drawGroups(groupElem, cy);
positionNodes(db, cy);
setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth'));
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth'));
};
export const renderer = { draw };

View File

@@ -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 type { ArchitectureDB } from './architectureDb.js';
import { db, getConfigField } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js';
import {
ArchitectureDirectionArrow,
@@ -16,17 +16,14 @@ import {
isArchitectureDirectionY,
isArchitecturePairXY,
nodeData,
type ArchitectureDB,
type ArchitectureJunction,
type ArchitectureService,
} from './architectureTypes.js';
export const drawEdges = async function (
edgesEl: D3Element,
cy: cytoscape.Core,
db: ArchitectureDB
) {
const padding = db.getConfigField('padding');
const iconSize = db.getConfigField('iconSize');
export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) {
const padding = getConfigField('padding');
const iconSize = getConfigField('iconSize');
const halfIconSize = iconSize / 2;
const arrowSize = iconSize / 6;
const halfArrowSize = arrowSize / 2;
@@ -186,17 +183,13 @@ export const drawEdges = async function (
);
};
export const drawGroups = async function (
groupsEl: D3Element,
cy: cytoscape.Core,
db: ArchitectureDB
) {
const padding = db.getConfigField('padding');
export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) {
const padding = getConfigField('padding');
const groupIconSize = padding * 0.75;
const fontSize = db.getConfigField('fontSize');
const fontSize = getConfigField('fontSize');
const iconSize = db.getConfigField('iconSize');
const iconSize = getConfigField('iconSize');
const halfIconSize = iconSize / 2;
await Promise.all(
@@ -273,7 +266,7 @@ export const drawServices = async function (
): Promise<number> {
for (const service of services) {
const serviceElem = elem.append('g');
const iconSize = db.getConfigField('iconSize');
const iconSize = getConfigField('iconSize');
if (service.title) {
const textElem = serviceElem.append('g');
@@ -357,7 +350,7 @@ export const drawJunctions = function (
) {
junctions.forEach((junction) => {
const junctionElem = elem.append('g');
const iconSize = db.getConfigField('iconSize');
const iconSize = getConfigField('iconSize');
const bkgElem = junctionElem.append('g');
bkgElem

View File

@@ -92,20 +92,7 @@ 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);
}

View File

@@ -270,12 +270,7 @@ function layoutBlocks(block: Block, db: BlockDB) {
if (child.children) {
layoutBlocks(child, db);
}
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;
columnPos += child?.widthInColumns ?? 1;
log.debug('abc88 columnsPos', child, columnPos);
}
}

View File

@@ -1,7 +1,6 @@
// @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 () {
@@ -403,25 +402,6 @@ 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 () {

View File

@@ -15,12 +15,4 @@ 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();
});
});
});

View File

@@ -242,7 +242,6 @@ classLabel
namespaceName
: alphaNumToken { $$=$1; }
| classLiteralName { $$=$1; }
| alphaNumToken DOT namespaceName { $$=$1+'.'+$3; }
| alphaNumToken namespaceName { $$=$1+$2; }
;

View File

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

View File

@@ -125,60 +125,4 @@ 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');
});
});

View File

@@ -139,9 +139,6 @@ export class FlowDB implements DiagramDB {
if (edgeDoc?.animation !== undefined) {
edge.animation = edgeDoc.animation;
}
if (edgeDoc?.curve !== undefined) {
edge.interpolate = edgeDoc.curve;
}
return;
}
@@ -406,8 +403,7 @@ You have to call mermaid.initialize.`
*
*/
public setDirection(dir: string) {
this.direction = dir.trim();
this.direction = dir;
if (/.*</.exec(this.direction)) {
this.direction = 'RL';
}

View File

@@ -37,59 +37,6 @@ 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'

View File

@@ -1,14 +1,12 @@
// @ts-ignore: JISON doesn't support types
import parser from './parser/mindmap.jison';
import { MindmapDB } from './mindmapDb.js';
import db 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 = {
get db() {
return new MindmapDB();
},
db,
renderer,
parser,
styles,

View File

@@ -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 = new MindmapDB();
mindmap.yy = mindmapDB;
mindmap.yy.clear();
setLogLevel('trace');
});

View File

@@ -5,6 +5,70 @@ 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,
@@ -14,149 +78,82 @@ 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 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;
}
}
export default db;

View File

@@ -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, MindmapNode } from './mindmapTypes.js';
import type { FilledMindMapNode, MindmapDB, 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);

View File

@@ -1,4 +1,5 @@
import type { RequiredDeep } from 'type-fest';
import type mindmapDb from './mindmapDb.js';
export interface MindmapNode {
id: number;
@@ -18,3 +19,4 @@ export interface MindmapNode {
}
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
export type MindmapDB = typeof mindmapDb;

View File

@@ -1,9 +1,8 @@
import { createText } from '../../rendering-util/createText.js';
import type { FilledMindMapNode } from './mindmapTypes.js';
import type { FilledMindMapNode, MindmapDB } 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;

View File

@@ -1,7 +1,6 @@
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,
@@ -12,42 +11,49 @@ import {
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import type { PacketWord } from './types.js';
import type { PacketDB, PacketData, PacketWord } from './types.js';
const defaultPacketData: PacketData = {
packet: [],
};
let data: PacketData = structuredClone(defaultPacketData);
const DEFAULT_PACKET_CONFIG: Required<PacketDiagramConfig> = DEFAULT_CONFIG.packet;
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;
const getConfig = (): Required<PacketDiagramConfig> => {
const config = cleanAndMerge({
...DEFAULT_PACKET_CONFIG,
...commonGetConfig().packet,
});
if (config.showBits) {
config.paddingY += 10;
}
return config;
};
public getPacket() {
return this.packet;
const getPacket = (): PacketWord[] => data.packet;
const pushWord = (word: PacketWord) => {
if (word.length > 0) {
data.packet.push(word);
}
};
public pushWord(word: PacketWord) {
if (word.length > 0) {
this.packet.push(word);
}
}
const clear = () => {
commonClear();
data = structuredClone(defaultPacketData);
};
public clear() {
commonClear();
this.packet = [];
}
public setAccTitle = setAccTitle;
public getAccTitle = getAccTitle;
public setDiagramTitle = setDiagramTitle;
public getDiagramTitle = getDiagramTitle;
public getAccDescription = getAccDescription;
public setAccDescription = setAccDescription;
}
export const db: PacketDB = {
pushWord,
getPacket,
getConfig,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
};

View File

@@ -1,14 +1,12 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { PacketDB } from './db.js';
import { db } from './db.js';
import { parser } from './parser.js';
import { renderer } from './renderer.js';
import { styles } from './styles.js';
export const diagram: DiagramDefinition = {
parser,
get db() {
return new PacketDB();
},
db,
renderer,
styles,
};

View File

@@ -1,26 +1,24 @@
import { it, describe, expect } from 'vitest';
import { PacketDB } from './db.js';
import { db } from './db.js';
import { parser } from './parser.js';
const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db;
describe('packet diagrams', () => {
let db: PacketDB;
beforeEach(() => {
db = new PacketDB();
if (parser.parser) {
parser.parser.yy = db;
}
clear();
});
it('should handle a packet-beta definition', async () => {
const str = `packet-beta`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getPacket()).toMatchInlineSnapshot('[]');
expect(getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle a packet definition', async () => {
const str = `packet`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getPacket()).toMatchInlineSnapshot('[]');
expect(getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle diagram with data and title', async () => {
@@ -31,10 +29,10 @@ describe('packet diagrams', () => {
0-10: "test"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
expect(db.getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
expect(db.getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
expect(db.getPacket()).toMatchInlineSnapshot(`
expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -54,7 +52,7 @@ describe('packet diagrams', () => {
11: "single"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getPacket()).toMatchInlineSnapshot(`
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -80,7 +78,7 @@ describe('packet diagrams', () => {
+16: "word"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getPacket()).toMatchInlineSnapshot(`
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -106,7 +104,7 @@ describe('packet diagrams', () => {
+16: "word"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getPacket()).toMatchInlineSnapshot(`
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -132,7 +130,7 @@ describe('packet diagrams', () => {
11-90: "multiple"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getPacket()).toMatchInlineSnapshot(`
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -174,7 +172,7 @@ describe('packet diagrams', () => {
17-63: "multiple"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(db.getPacket()).toMatchInlineSnapshot(`
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{

View File

@@ -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 { PacketDB } from './db.js';
import { db } from './db.js';
import type { PacketBlock, PacketWord } from './types.js';
const maxPacketSize = 10_000;
const populate = (ast: Packet, db: PacketDB) => {
const populate = (ast: Packet) => {
populateCommonDb(ast, db);
let lastBit = -1;
let word: PacketWord = [];
@@ -91,17 +91,9 @@ 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, db);
populate(ast);
},
};

View File

@@ -1,10 +1,10 @@
import type { DiagramDB } from '../../diagram-api/types.js';
import type { DiagramStyleClassDef } from '../../diagram-api/types.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 DEFAULT_CONFIG from '../../defaultConfig.js';
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
import { isLabelStyle } from '../../rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import { cleanAndMerge } from '../../utils.js';
import { ImperativeState } from '../../utils/imperativeState.js';
import {
clear as commonClear,
getAccDescription,
@@ -14,82 +14,99 @@ import {
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.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;
import type { TreemapDB, TreemapData, TreemapDiagramConfig, TreemapNode } from './types.js';
public getNodes() {
return this.nodes;
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 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>;
// 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 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 getRoot = (): TreemapNode | undefined => ({ name: '', children: state.records.outerNodes });
public getRoot() {
return { name: '', children: this.outerNodes };
}
const addClass = (id: string, _style: string) => {
const classes = state.records.classes;
const styleClass = classes.get(id) ?? { id, styles: [], textStyles: [] };
classes.set(id, styleClass);
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);
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.styles = [s];
styleClass.textStyles = [s];
}
});
}
this.classes.set(id, styleClass);
}
if (styleClass?.styles) {
styleClass.styles.push(s);
} else {
styleClass.styles = [s];
}
});
}
public getClasses() {
return this.classes;
}
classes.set(id, styleClass);
};
const getClasses = (): Map<string, DiagramStyleClassDef> => {
return state.records.classes;
};
public getStylesForClass(classSelector: string): string[] {
return this.classes.get(classSelector)?.styles ?? [];
}
const getStylesForClass = (classSelector: string): string[] => {
return state.records.classes.get(classSelector)?.styles ?? [];
};
public clear() {
commonClear();
this.nodes = [];
this.levels = new Map();
this.outerNodes = [];
this.classes = new Map();
this.root = undefined;
}
const clear = () => {
commonClear();
state.reset();
};
public setAccTitle = setAccTitle;
public getAccTitle = getAccTitle;
public setDiagramTitle = setDiagramTitle;
public getDiagramTitle = getDiagramTitle;
public getAccDescription = getAccDescription;
public setAccDescription = setAccDescription;
}
export const db: TreemapDB = {
getNodes,
addNode,
getRoot,
getConfig,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
addClass,
getClasses,
getStylesForClass,
};

View File

@@ -1,14 +1,12 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { TreeMapDB } from './db.js';
import { db } from './db.js';
import { parser } from './parser.js';
import { renderer } from './renderer.js';
import styles from './styles.js';
export const diagram: DiagramDefinition = {
parser,
get db() {
return new TreeMapDB();
},
db,
renderer,
styles,
};

View File

@@ -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 type { TreemapNode, TreemapAst, TreemapDB } from './types.js';
import { db } from './db.js';
import type { TreemapNode, TreemapAst } 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, db: TreemapDB) => {
const populate = (ast: TreemapAst) => {
// We need to bypass the type checking for populateCommonDb
// eslint-disable-next-line @typescript-eslint/no-explicit-any
populateCommonDb(ast as any, db);
@@ -84,8 +84,6 @@ 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
@@ -93,13 +91,7 @@ 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);
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);
populate(ast);
} catch (error) {
log.error('Error parsing treemap:', error);
throw error;

View File

@@ -141,7 +141,6 @@ function sidebarAll() {
],
},
...sidebarSyntax(),
...sidebarAlgorithms(),
...sidebarEcosystem(),
...sidebarConfig(),
...sidebarCommunity(),
@@ -224,27 +223,6 @@ 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 [
{

View File

@@ -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 layout tests. (The layouts are tested with integration tests.)
Unit tests are mandatory for all code except the renderers. (The renderers are tested with integration tests.)
We use [Vitest](https://vitest.dev) to run unit tests.
@@ -328,30 +328,6 @@ 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.

View File

@@ -1,189 +0,0 @@
---
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"]

View File

@@ -1,307 +0,0 @@
---
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()

View File

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

View File

@@ -79,7 +79,6 @@ 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
@@ -99,7 +98,6 @@ 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

View File

@@ -10,7 +10,9 @@ 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`](https://www.iana.org/assignments/media-types/application/vnd.mermaid).
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.
## Showcase

View File

@@ -1,150 +0,0 @@
# 🛠️ 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
---
> 💡 Youre now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!

View File

@@ -1,132 +0,0 @@
# 📊 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.
---

View File

@@ -1,40 +0,0 @@
## 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.

View File

@@ -1,180 +0,0 @@
## 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.

View File

@@ -1135,46 +1135,15 @@ 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:
```
---
config:
flowchart:
curve: stepBefore
---
%%{ init: { 'flowchart': { 'curve': 'stepBefore' } } }%%
graph LR
```
#### 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.
```
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.
### Styling a node

View File

@@ -1,5 +1,40 @@
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';
@@ -40,9 +75,6 @@ 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
@@ -193,49 +225,63 @@ 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
jsdomIt('appends a div node', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId');
const divNode = ensureNodeFromSelector('div');
const svgNode = ensureNodeFromSelector('svg', divNode);
ensureNodeFromSelector('g', svgNode);
it('appends a div node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(parent_append_spy).toHaveBeenCalledWith('div');
expect(div_append_spy).toHaveBeenCalledWith('svg');
});
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('the id for the div is "d" with the id appended', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
});
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('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 svg width to 100%', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId');
const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('width')).toBe('100%');
it('appends a svg node to the div node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
});
jsdomIt('the svg id is the id', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId');
const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('id')).toBe('theId');
it('sets the svg width to 100%', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%');
});
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 id is the id', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId');
});
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('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('returns the given parentRoot d3 nodes', ({ body }) => {
expect(appendDivSvgG(body, 'theId', 'dtheId')).toEqual(body);
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);
});
});
@@ -736,9 +782,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 from what 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 than was is put in the diagram text (ex: in -v2 diagrams)
const diagramTypesAndExpectations = [
// { textDiagramType: 'C4Context', expectedType: 'c4' }, TODO : setAccTitle not called in C4 jison parser
{ textDiagramType: 'C4Context', expectedType: 'c4' },
{ textDiagramType: 'classDiagram', expectedType: 'class' },
{ textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' },
{ textDiagramType: 'erDiagram', expectedType: 'er' },
@@ -750,11 +796,7 @@ 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',
content: 'x-axis "Attempts" 10000 --> 10000\ny-axis "Passing tests" 1 --> 1\nbar [1]',
},
{ textDiagramType: 'xychart-beta', expectedType: 'xychart' },
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
@@ -770,25 +812,20 @@ graph TD;A--x|text including URL space|B;`)
diagramTypesAndExpectations.forEach((testedDiagram) => {
describe(`${testedDiagram.textDiagramType}`, () => {
const diagramType = testedDiagram.textDiagramType;
const content = testedDiagram.content || '';
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n ${content}`;
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`;
const expectedDiagramType = testedDiagram.expectedType;
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);
}
);
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();
});
});
});
});

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