Merge branch 'develop' into patch-1

This commit is contained in:
Sidharth Vinod 2025-01-25 12:14:31 +05:30 committed by GitHub
commit 3536ceb5d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 4981 additions and 2552 deletions

View File

@ -0,0 +1,5 @@
---
'mermaid': patch
---
Fixes for consistent edge id creation & handling edge cases for animate edge feature

View File

@ -0,0 +1,5 @@
---
'mermaid': patch
---
Fix for issue #6195 - allowing @ signs inside node labels

View File

@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram

View File

@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: revert state db to resolve getData returning empty nodes and edges

View File

@ -0,0 +1,8 @@
---
'mermaid': minor
---
Flowchart new syntax for node metadata bugs
- Incorrect label mapping for nodes when using `&`
- Syntax error when `}` with trailing spaces before new line

View File

@ -0,0 +1,5 @@
---
'mermaid': minor
---
Adding support for animation of flowchart edges

View File

@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each flowchart

View File

@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Gantt, Sankey and User Journey diagram are now able to pick font-family from mermaid config.

View File

@ -0,0 +1,5 @@
---
'mermaid': patch
---
`mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram

View File

@ -42,4 +42,4 @@ jobs:
working-directory: ./packages/mermaid working-directory: ./packages/mermaid
run: pnpm run docs:build run: pnpm run docs:build
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c # main - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef # main

View File

@ -7,6 +7,9 @@ on:
- master - master
- release/** - release/**
pull_request: pull_request:
issue_comment:
types:
- created
merge_group: merge_group:
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
@ -28,8 +31,12 @@ env:
) || ) ||
github.event.before github.event.before
}} }}
# Check if this is a new comment with '/visual-test'
RUN_VISUAL_TEST: >-
${{ github.event_name == 'issue_comment' && github.event.action == 'created' && contains(github.event.comment.body, '/visual-test') && github.event.issue.pull_request != null }}
jobs: jobs:
cache: cache:
if: ${{ github.event_name != 'issue_comment' || contains(github.event.comment.body, '/visual-test') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1 image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1
@ -127,16 +134,17 @@ jobs:
# e.g. if this action was run from a fork # e.g. if this action was run from a fork
record: ${{ secrets.CYPRESS_RECORD_KEY != '' }} record: ${{ secrets.CYPRESS_RECORD_KEY != '' }}
env: env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} # Only set Argos environment variables if the visual test comment trigger is present
VITEST_COVERAGE: true ARGOS_TOKEN: ${{ env.RUN_VISUAL_TEST == 'true' && secrets.ARGOS_TOKEN || '' }}
ARGOS_PARALLEL: ${{ env.RUN_VISUAL_TEST == 'true' }}
ARGOS_PARALLEL_TOTAL: ${{ env.RUN_VISUAL_TEST == 'true' && strategy.job-total || 1 }}
ARGOS_PARALLEL_INDEX: ${{ env.RUN_VISUAL_TEST == 'true' && matrix.containers || 1 }}
CYPRESS_COMMIT: ${{ github.sha }} CYPRESS_COMMIT: ${{ github.sha }}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} CYPRESS_RECORD_KEY: ${{ env.RUN_VISUAL_TEST == 'true' && secrets.CYPRESS_RECORD_KEY || ''}}
ARGOS_PARALLEL: true
ARGOS_PARALLEL_TOTAL: ${{ strategy.job-total }}
ARGOS_PARALLEL_INDEX: ${{ matrix.containers }}
SPLIT: ${{ strategy.job-total }} SPLIT: ${{ strategy.job-total }}
SPLIT_INDEX: ${{ strategy.job-index }} SPLIT_INDEX: ${{ strategy.job-index }}
SPLIT_FILE: 'cypress/timings.json' SPLIT_FILE: 'cypress/timings.json'
VITEST_COVERAGE: true
- name: Upload Coverage to Codecov - name: Upload Coverage to Codecov
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0

View File

@ -26,7 +26,7 @@ jobs:
results_format: sarif results_format: sarif
publish_results: true publish_results: true
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.pre.node20
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif

View File

@ -23,7 +23,7 @@ export default eyesPlugin(
}); });
// copy any needed variables from process.env to config.env // copy any needed variables from process.env to config.env
config.env.useAppli = process.env.USE_APPLI ? true : false; config.env.useAppli = process.env.USE_APPLI ? true : false;
config.env.useArgos = !!process.env.CI; config.env.useArgos = !!process.env.CI && !!process.env.ARGOS_TOKEN;
if (config.env.useArgos) { if (config.env.useArgos) {
registerArgosTask(on, config, { registerArgosTask(on, config, {

View File

@ -1076,4 +1076,41 @@ end
); );
}); });
}); });
describe('New @ sytax for node metadata edge cases', () => {
it('should be possible to use @ syntax to add labels on multi nodes', () => {
imgSnapshotTest(
`flowchart TB
n2["label for n2"] & n4@{ label: "labe for n4"} & n5@{ label: "labe for n5"}
`,
{}
);
});
it('should be possible to use @ syntax to add labels with trail spaces and &', () => {
imgSnapshotTest(
`flowchart TB
n2["label for n2"] & n4@{ label: "labe for n4"} & n5@{ label: "labe for n5"}
`,
{}
);
});
it('should be possible to use @ syntax to add labels with trail spaces', () => {
imgSnapshotTest(
`flowchart TB
n2["label for n2"]
n4@{ label: "labe for n4"}
n5@{ label: "labe for n5"}
`,
{}
);
});
it('should be possible to use @ syntax to add labels with trail spaces and edge/link', () => {
imgSnapshotTest(
`flowchart TD
A["A"] --> B["for B"] & C@{ label: "for c"} & E@{label : "for E"}
D@{label: "for D"}
`,
{}
);
});
});
}); });

View File

@ -78,35 +78,78 @@
font-family: monospace; font-family: monospace;
font-size: 72px; font-size: 72px;
} }
pre { pre {
width: 100%; width: 100%;
} }
/* tspan { /* tspan {
font-size: 6px !important; font-size: 6px !important;
} */ } */
/* .flowchart-link {
stroke-dasharray: 4, 4 !important;
animation: flow 1s linear infinite;
animation: dashdraw 4.93282s linear infinite;
stroke-width: 2px !important;
} */
@keyframes dashdraw {
from {
stroke-dashoffset: 0;
}
}
/*stroke-width:2;stroke-dasharray:10.000000,9.865639;stroke-dashoffset:-198.656393;animation: 4.932820s linear infinite;*/
/* stroke-width:2;stroke-dasharray:10.000000,9.865639;stroke-dashoffset:-198.656393;animation: dashdraw 4.932820s linear infinite;*/
</style> </style>
</head> </head>
<body> <body>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
flowchart LR flowchart LR
subgraph S2 AB["apa@apa@"] --> B(("`apa@apa`"))
subgraph s1["APA"]
D{"Use the editor"}
end
D -- Mermaid js --> I{"fa:fa-code Text"}
D --> I
D --> I
end
</pre> </pre>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid">
flowchart
D(("for D"))
</pre>
<pre id="diagram4" class="mermaid">
flowchart LR
A e1@==> B
e1@{ animate: true}
</pre>
<pre id="diagram4" class="mermaid">
flowchart LR
A e1@--> B
classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
class e1 animate
</pre>
<h2>infinite</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
</pre>
<h2>Mermaid - edge-animation-slow</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
e1@{ animation: fast}
</pre>
<h2>Mermaid - edge-animation-fast</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear;
class e1 edge-animation-fast
</pre>
<pre id="diagram4" class="mermaid2">
info </pre
>
<pre id="diagram4" class="mermaid2">
--- ---
config: config:
layout: elk layout: elk
@ -131,7 +174,7 @@ config:
end end
end end
</pre> </pre>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid2">
--- ---
config: config:
layout: elk layout: elk
@ -144,7 +187,7 @@ config:
D-->I D-->I
D-->I D-->I
</pre> </pre>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid2">
--- ---
config: config:
layout: elk layout: elk
@ -183,7 +226,7 @@ flowchart LR
n8@{ shape: rect} n8@{ shape: rect}
</pre> </pre>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid2">
--- ---
config: config:
layout: elk layout: elk
@ -199,7 +242,7 @@ flowchart LR
</pre> </pre>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid2">
--- ---
config: config:
layout: elk layout: elk
@ -208,7 +251,7 @@ flowchart LR
A{A} --> B & C A{A} --> B & C
</pre </pre
> >
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid2">
--- ---
config: config:
layout: elk layout: elk
@ -220,7 +263,7 @@ flowchart LR
end end
</pre </pre
> >
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid2">
--- ---
config: config:
layout: elk layout: elk
@ -391,7 +434,10 @@ kanban
window.callback = function () { window.callback = function () {
alert('A callback was triggered'); alert('A callback was triggered');
}; };
mermaid.initialize({ function callback() {
alert('It worked');
}
await mermaid.initialize({
// theme: 'base', // theme: 'base',
// theme: 'default', // theme: 'default',
// theme: 'forest', // theme: 'forest',
@ -403,9 +449,11 @@ kanban
// layout: 'fixed', // layout: 'fixed',
// htmlLabels: false, // htmlLabels: false,
flowchart: { titleTopMargin: 10 }, flowchart: { titleTopMargin: 10 },
// fontFamily: 'Caveat', // fontFamily: 'Caveat',
// fontFamily: 'Kalam', // fontFamily: 'Kalam',
// fontFamily: 'courier', // fontFamily: 'courier',
fontFamily: 'arial',
sequence: { sequence: {
actorFontFamily: 'courier', actorFontFamily: 'courier',
noteFontFamily: 'courier', noteFontFamily: 'courier',
@ -417,10 +465,9 @@ kanban
fontSize: 12, fontSize: 12,
logLevel: 0, logLevel: 0,
securityLevel: 'loose', securityLevel: 'loose',
callback,
}); });
function callback() {
alert('It worked');
}
mermaid.parseError = function (err, hash) { mermaid.parseError = function (err, hash) {
console.error('In parse error:'); console.error('In parse error:');
console.error(err); console.error(err);

View File

@ -62,56 +62,23 @@
<body style="display: flex; gap: 2rem; flex-direction: row"> <body style="display: flex; gap: 2rem; flex-direction: row">
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid">
flowchart LR flowchart
A@{ icon: "fa:window-minimize", form: circle } A --> A
E@{ icon: "fa:window-minimize", form: circle } subgraph B
B@{ icon: "fa:bell", form: circle } B1 --> B1
B2@{ icon: "fa:bell", form: circle } end
C@{ icon: "fa:address-book", form: square } subgraph C
D@{ icon: "fa:star-half", form: square } subgraph C1
A --> E C2 --> C2
B --> B2 subgraph D
D1 --> D1
end
D --> D
end
C1 --> C1
end
</pre> </pre>
<pre id="diagram4" class="mermaid2">
flowchart TB
A --test2--> B2@{ icon: "fa:bell", form: "rounded", label: "B2 aiduaid uyawduad uaduabd uyduadb", pos: "b" }
B2 --test--> C
D --> B2 --> E
style B2 fill:#f9f,stroke:#333,stroke-width:4px
</pre
>
<pre id="diagram43" class="mermaid2">
flowchart BT
A --test2--> B2@{ icon: "fa:bell", form: "square", label: "B2", pos: "t", h: 40, w: 30 }
B2 --test--> C
D --> B2 --> E
</pre
>
<pre id="diagram4" class="mermaid2">
flowchart BT
A --test2--> B2@{ icon: "fa:bell", label: "B2 awiugdawu uydgayuiwd wuydguy", pos: "b", h: 40, w: 30 }
B2 --test--> C
</pre
>
<pre id="diagram43" class="mermaid2">
flowchart BT
A --test2--> B2@{ icon: "fa:bell", label: "B2 dawuygd ayuwgd uy", pos: "t", h: 40, w: 30 }
B2 --test--> C
</pre
>
<pre id="diagram6" class="mermaid2">
flowchart TB
A --> B2@{ icon: "fa:bell", form: "circle", label: "test augfuyfavf ydvaubfuac", pos: "t", w: 200, h: 100 } --> C
</pre
>
<pre id="diagram6" class="mermaid2">
flowchart TB
A --> B2@{ icon: "fa:bell", form: "circle", label: "test augfuyfavf ydvaubfuac", pos: "b", w: 200, h: 100 } --> C
D --> B2 --> E
</pre
>
<script type="module"> <script type="module">
import mermaid from './mermaid.esm.mjs'; import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs'; import layouts from './mermaid-layout-elk.esm.mjs';

View File

@ -20,7 +20,7 @@
#### Defined in #### Defined in
[packages/mermaid/src/rendering-util/types.ts:144](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L144) [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 #### Defined in
[packages/mermaid/src/rendering-util/types.ts:143](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L143) [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147)
--- ---
@ -40,4 +40,4 @@
#### Defined in #### Defined in
[packages/mermaid/src/rendering-util/types.ts:142](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L142) [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

@ -19,4 +19,4 @@ The `parseError` function will not be called.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L59) [packages/mermaid/src/types.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L64)

View File

@ -18,7 +18,7 @@ The config passed as YAML frontmatter or directives
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:70](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L70) [packages/mermaid/src/types.ts:75](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L75)
--- ---
@ -30,4 +30,4 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:66](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L66) [packages/mermaid/src/types.ts:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L71)

View File

@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98) [packages/mermaid/src/types.ts:103](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L103)
--- ---
@ -51,7 +51,7 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88) [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
--- ---
@ -63,4 +63,4 @@ The svg code for the rendered graph.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84) [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)

View File

@ -99,8 +99,7 @@ Blogging frameworks and platforms
- [Nextra](https://nextra.site/) - [Nextra](https://nextra.site/)
- [Mermaid](https://nextra.site/docs/guide/mermaid) - [Mermaid](https://nextra.site/docs/guide/mermaid)
- [WordPress](https://wordpress.org) - [WordPress](https://wordpress.org)
- [WordPress Markdown Editor](https://wordpress.org/plugins/wp-githuber-md) - [MerPRess](https://wordpress.org/plugins/merpress/)
- [WP-ReliableMD](https://wordpress.org/plugins/wp-reliablemd/)
### CMS/ECM ### CMS/ECM

View File

@ -1183,6 +1183,91 @@ flowchart TB
B --> D B --> D
``` ```
### Attaching an ID to Edges
Mermaid now supports assigning IDs to edges, similar to how IDs and metadata can be attached to nodes. This feature lays the groundwork for more advanced styling, classes, and animation capabilities on edges.
**Syntax:**
To give an edge an ID, prepend the edge syntax with the ID followed by an `@` character. For example:
```mermaid-example
flowchart LR
A e1@> B
```
```mermaid
flowchart LR
A e1@> B
```
In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
### Turning an Animation On
Once you have assigned an ID to an edge, you can turn on animations for that edge by defining the edges properties:
```mermaid-example
flowchart LR
A e1@==> B
e1@{ animate: true }
```
```mermaid
flowchart LR
A e1@==> B
e1@{ animate: true }
```
This tells Mermaid that the edge `e1` should be animated.
### Selecting Type of Animation
In the initial version, two animation speeds are supported: `fast` and `slow`. Selecting a specific animation type is a shorthand for enabling animation and setting the animation speed in one go.
**Examples:**
```mermaid-example
flowchart LR
A e1@> B
e1@{ animation: fast }
```
```mermaid
flowchart LR
A e1@> B
e1@{ animation: fast }
```
This is equivalent to `{ animate: true, animation: fast }`.
### Using classDef Statements for Animations
You can also animate edges by assigning a class to them and then defining animation properties in a `classDef` statement. For example:
```mermaid-example
flowchart LR
A e1@> B
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
```
```mermaid
flowchart LR
A e1@> B
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
```
In this snippet:
- `e1@-->` creates an edge with ID `e1`.
- `classDef animate` defines a class named `animate` with styling and animation properties.
- `class e1 animate` applies the `animate` class to the edge `e1`.
**Note on Escaping Commas:**
When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaids style definitions.
## New arrow types ## New arrow types
There are new types of arrows supported: There are new types of arrows supported:

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,11 @@
import { parser } from './parser/classDiagram.jison'; import { parser } from './parser/classDiagram.jison';
import classDb from './classDb.js'; import { ClassDB } from './classDb.js';
describe('class diagram, ', function () { describe('class diagram, ', function () {
describe('when parsing data from a classDiagram it', function () { describe('when parsing data from a classDiagram it', function () {
let classDb;
beforeEach(function () { beforeEach(function () {
classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
parser.yy.clear(); parser.yy.clear();
}); });

View File

@ -1,13 +1,15 @@
import type { DiagramDefinition } from '../../diagram-api/types.js'; import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types // @ts-ignore: JISON doesn't support types
import parser from './parser/classDiagram.jison'; import parser from './parser/classDiagram.jison';
import db from './classDb.js'; import { ClassDB } from './classDb.js';
import styles from './styles.js'; import styles from './styles.js';
import renderer from './classRenderer-v3-unified.js'; import renderer from './classRenderer-v3-unified.js';
export const diagram: DiagramDefinition = { export const diagram: DiagramDefinition = {
parser, parser,
db, get db() {
return new ClassDB();
},
renderer, renderer,
styles, styles,
init: (cnf) => { init: (cnf) => {
@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = {
cnf.class = {}; cnf.class = {};
} }
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
db.clear();
}, },
}; };

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/unbound-method -- Broken for Vitest mocks, see https://github.com/vitest-dev/eslint-plugin-vitest/pull/286 */
// @ts-expect-error Jison doesn't export types // @ts-expect-error Jison doesn't export types
import { parser } from './parser/classDiagram.jison'; import { parser } from './parser/classDiagram.jison';
import classDb from './classDb.js'; import { ClassDB } from './classDb.js';
import { vi, describe, it, expect } from 'vitest'; import { vi, describe, it, expect } from 'vitest';
import type { ClassMap, NamespaceNode } from './classTypes.js'; import type { ClassMap, NamespaceNode } from './classTypes.js';
const spyOn = vi.spyOn; const spyOn = vi.spyOn;
@ -10,8 +11,9 @@ const abstractCssStyle = 'font-style:italic;';
describe('given a basic class diagram, ', function () { describe('given a basic class diagram, ', function () {
describe('when parsing class definition', function () { describe('when parsing class definition', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
it('should handle classes within namespaces', () => { it('should handle classes within namespaces', () => {
@ -564,8 +566,9 @@ class C13["With Città foreign language"]
}); });
describe('when parsing class defined in brackets', function () { describe('when parsing class defined in brackets', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
@ -656,8 +659,9 @@ class C13["With Città foreign language"]
}); });
describe('when parsing comments', function () { describe('when parsing comments', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
@ -746,8 +750,9 @@ foo()
}); });
describe('when parsing click statements', function () { describe('when parsing click statements', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
it('should handle href link', function () { it('should handle href link', function () {
@ -857,8 +862,9 @@ foo()
}); });
describe('when parsing annotations', function () { describe('when parsing annotations', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
@ -921,8 +927,9 @@ foo()
describe('given a class diagram with members and methods ', function () { describe('given a class diagram with members and methods ', function () {
describe('when parsing members', function () { describe('when parsing members', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
@ -980,8 +987,9 @@ describe('given a class diagram with members and methods ', function () {
}); });
describe('when parsing method definition', function () { describe('when parsing method definition', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
@ -1067,8 +1075,9 @@ describe('given a class diagram with members and methods ', function () {
describe('given a class diagram with generics, ', function () { describe('given a class diagram with generics, ', function () {
describe('when parsing valid generic classes', function () { describe('when parsing valid generic classes', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
@ -1180,8 +1189,9 @@ namespace space {
describe('given a class diagram with relationships, ', function () { describe('given a class diagram with relationships, ', function () {
describe('when parsing basic relationships', function () { describe('when parsing basic relationships', function () {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
}); });
@ -1714,7 +1724,9 @@ class Class2
}); });
describe('when parsing classDiagram with text labels', () => { describe('when parsing classDiagram with text labels', () => {
let classDb: ClassDB;
beforeEach(function () { beforeEach(function () {
classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
parser.yy.clear(); parser.yy.clear();
}); });
@ -1897,3 +1909,40 @@ class C13["With Città foreign language"]
}); });
}); });
}); });
describe('class db class', () => {
let classDb: ClassDB;
beforeEach(() => {
classDb = new ClassDB();
});
// This is to ensure that functions used in class JISON are exposed as function from ClassDB
it('should have functions used in class JISON as own property', () => {
const functionsUsedInParser = [
'addRelation',
'cleanupLabel',
'setAccTitle',
'setAccDescription',
'addClassesToNamespace',
'addNamespace',
'setCssClass',
'addMembers',
'addClass',
'setClassLabel',
'addAnnotation',
'addMember',
'addNote',
'defineClass',
'setDirection',
'relationType',
'lineType',
'setClickEvent',
'setTooltip',
'setLink',
'setCssStyle',
] as const satisfies (keyof ClassDB)[];
for (const fun of functionsUsedInParser) {
expect(Object.hasOwn(classDb, fun)).toBe(true);
}
});
});

View File

@ -1,13 +1,15 @@
import type { DiagramDefinition } from '../../diagram-api/types.js'; import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types // @ts-ignore: JISON doesn't support types
import parser from './parser/classDiagram.jison'; import parser from './parser/classDiagram.jison';
import db from './classDb.js'; import { ClassDB } from './classDb.js';
import styles from './styles.js'; import styles from './styles.js';
import renderer from './classRenderer-v3-unified.js'; import renderer from './classRenderer-v3-unified.js';
export const diagram: DiagramDefinition = { export const diagram: DiagramDefinition = {
parser, parser,
db, get db() {
return new ClassDB();
},
renderer, renderer,
styles, styles,
init: (cnf) => { init: (cnf) => {
@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = {
cnf.class = {}; cnf.class = {};
} }
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
db.clear();
}, },
}; };

View File

@ -1,8 +1,10 @@
import { parser } from './classDiagram.jison'; import { parser } from './classDiagram.jison';
import classDb from '../classDb.js'; import { ClassDB } from '../classDb.js';
describe('class diagram', function () { describe('class diagram', function () {
let classDb;
beforeEach(function () { beforeEach(function () {
classDb = new ClassDB();
parser.yy = classDb; parser.yy = classDb;
parser.yy.clear(); parser.yy.clear();
}); });

View File

@ -1,9 +1,11 @@
import flowDb from './flowDb.js'; import { FlowDB } from './flowDb.js';
import type { FlowSubGraph } from './types.js'; import type { FlowSubGraph } from './types.js';
describe('flow db subgraphs', () => { describe('flow db subgraphs', () => {
let flowDb: FlowDB;
let subgraphs: FlowSubGraph[]; let subgraphs: FlowSubGraph[];
beforeEach(() => { beforeEach(() => {
flowDb = new FlowDB();
subgraphs = [ subgraphs = [
{ nodes: ['a', 'b', 'c', 'e'] }, { nodes: ['a', 'b', 'c', 'e'] },
{ nodes: ['f', 'g', 'h'] }, { nodes: ['f', 'g', 'h'] },
@ -44,8 +46,9 @@ describe('flow db subgraphs', () => {
}); });
describe('flow db addClass', () => { describe('flow db addClass', () => {
let flowDb: FlowDB;
beforeEach(() => { beforeEach(() => {
flowDb.clear(); flowDb = new FlowDB();
}); });
it('should detect many classes', () => { it('should detect many classes', () => {
flowDb.addClass('a,b', ['stroke-width: 8px']); flowDb.addClass('a,b', ['stroke-width: 8px']);
@ -65,3 +68,33 @@ describe('flow db addClass', () => {
expect(classes.get('a')?.styles).toEqual(['stroke-width: 8px']); expect(classes.get('a')?.styles).toEqual(['stroke-width: 8px']);
}); });
}); });
describe('flow db class', () => {
let flowDb: FlowDB;
beforeEach(() => {
flowDb = new FlowDB();
});
// This is to ensure that functions used in flow JISON are exposed as function from FlowDB
it('should have functions used in flow JISON as own property', () => {
const functionsUsedInParser = [
'setDirection',
'addSubGraph',
'setAccTitle',
'setAccDescription',
'addVertex',
'addLink',
'setClass',
'destructLink',
'addClass',
'setClickEvent',
'setTooltip',
'setLink',
'updateLink',
'updateLinkInterpolate',
] as const satisfies (keyof FlowDB)[];
for (const fun of functionsUsedInParser) {
expect(Object.hasOwn(flowDb, fun)).toBe(true);
}
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,17 @@
import type { MermaidConfig } from '../../config.type.js'; import type { MermaidConfig } from '../../config.type.js';
import { setConfig } from '../../diagram-api/diagramAPI.js'; import { setConfig } from '../../diagram-api/diagramAPI.js';
import flowDb from './flowDb.js'; import { FlowDB } from './flowDb.js';
import renderer from './flowRenderer-v3-unified.js'; import renderer from './flowRenderer-v3-unified.js';
// @ts-ignore: JISON doesn't support types // @ts-ignore: JISON doesn't support types
import flowParser from './parser/flow.jison'; //import flowParser from './parser/flow.jison';
import flowParser from './parser/flowParser.ts';
import flowStyles from './styles.js'; import flowStyles from './styles.js';
export const diagram = { export const diagram = {
parser: flowParser, parser: flowParser,
db: flowDb, get db() {
return new FlowDB();
},
renderer, renderer,
styles: flowStyles, styles: flowStyles,
init: (cnf: MermaidConfig) => { init: (cnf: MermaidConfig) => {
@ -20,7 +23,5 @@ export const diagram = {
} }
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
flowDb.clear();
flowDb.setGen('gen-2');
}, },
}; };

View File

@ -7,7 +7,6 @@ import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/rende
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
import type { LayoutData } from '../../rendering-util/types.js'; import type { LayoutData } from '../../rendering-util/types.js';
import utils from '../../utils.js'; import utils from '../../utils.js';
import { getDirection } from './flowDb.js';
export const getClasses = function ( export const getClasses = function (
text: string, text: string,
@ -37,7 +36,7 @@ export const draw = async function (text: string, id: string, _version: string,
log.debug('Data: ', data4Layout); log.debug('Data: ', data4Layout);
// Create the root SVG // Create the root SVG
const svg = getDiagramElement(id, securityLevel); const svg = getDiagramElement(id, securityLevel);
const direction = getDirection(); const direction = diag.db.getDirection();
data4Layout.type = diag.type; data4Layout.type = diag.type;
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('[Arrows] when parsing', () => { describe('[Arrows] when parsing', () => {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
import { cleanupComments } from '../../../diagram-api/comments.js'; import { cleanupComments } from '../../../diagram-api/comments.js';
@ -9,7 +9,7 @@ setConfig({
describe('[Comments] when parsing', () => { describe('[Comments] when parsing', () => {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('when parsing directions', function () { describe('when parsing directions', function () {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
flow.parser.yy.setGen('gen-2'); flow.parser.yy.setGen('gen-2');
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -39,10 +39,31 @@ const doubleEndedEdges = [
{ edgeStart: '<==', edgeEnd: '==>', stroke: 'thick', type: 'double_arrow_point' }, { edgeStart: '<==', edgeEnd: '==>', stroke: 'thick', type: 'double_arrow_point' },
{ edgeStart: '<-.', edgeEnd: '.->', stroke: 'dotted', type: 'double_arrow_point' }, { edgeStart: '<-.', edgeEnd: '.->', stroke: 'dotted', type: 'double_arrow_point' },
]; ];
const regularEdges = [
{ edgeStart: '--', edgeEnd: '--x', stroke: 'normal', type: 'arrow_cross' },
{ edgeStart: '==', edgeEnd: '==x', stroke: 'thick', type: 'arrow_cross' },
{ edgeStart: '-.', edgeEnd: '.-x', stroke: 'dotted', type: 'arrow_cross' },
{ edgeStart: '--', edgeEnd: '--o', stroke: 'normal', type: 'arrow_circle' },
{ edgeStart: '==', edgeEnd: '==o', stroke: 'thick', type: 'arrow_circle' },
{ edgeStart: '-.', edgeEnd: '.-o', stroke: 'dotted', type: 'arrow_circle' },
{ edgeStart: '--', edgeEnd: '-->', stroke: 'normal', type: 'arrow_point' },
{ edgeStart: '==', edgeEnd: '==>', stroke: 'thick', type: 'arrow_point' },
{ edgeStart: '-.', edgeEnd: '.->', stroke: 'dotted', type: 'arrow_point' },
{ edgeStart: '--', edgeEnd: '----x', stroke: 'normal', type: 'arrow_cross' },
{ edgeStart: '==', edgeEnd: '====x', stroke: 'thick', type: 'arrow_cross' },
{ edgeStart: '-.', edgeEnd: '...-x', stroke: 'dotted', type: 'arrow_cross' },
{ edgeStart: '--', edgeEnd: '----o', stroke: 'normal', type: 'arrow_circle' },
{ edgeStart: '==', edgeEnd: '====o', stroke: 'thick', type: 'arrow_circle' },
{ edgeStart: '-.', edgeEnd: '...-o', stroke: 'dotted', type: 'arrow_circle' },
{ edgeStart: '--', edgeEnd: '---->', stroke: 'normal', type: 'arrow_point' },
{ edgeStart: '==', edgeEnd: '====>', stroke: 'thick', type: 'arrow_point' },
{ edgeStart: '-.', edgeEnd: '...->', stroke: 'dotted', type: 'arrow_point' },
];
describe('[Edges] when parsing', () => { describe('[Edges] when parsing', () => {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });
@ -67,6 +88,74 @@ describe('[Edges] when parsing', () => {
expect(edges[0].type).toBe('arrow_circle'); expect(edges[0].type).toBe('arrow_circle');
}); });
describe('edges with ids', function () {
describe('open ended edges with ids and labels', function () {
regularEdges.forEach((edgeType) => {
it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {
const res = flow.parser.parse(
`flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;`
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('B').id).toBe('B');
expect(edges.length).toBe(1);
expect(edges[0].id).toBe('e1');
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe(`${edgeType.type}`);
expect(edges[0].text).toBe('');
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
});
it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () {
const res = flow.parser.parse(
`flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;`
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('B').id).toBe('B');
expect(edges.length).toBe(1);
expect(edges[0].id).toBe('e1');
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe(`${edgeType.type}`);
expect(edges[0].text).toBe('');
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
});
});
it('should handle normal edges where you also have a node with metadata', function () {
const res = flow.parser.parse(`flowchart LR
A id1@-->B
A@{ shape: 'rect' }
`);
const edges = flow.parser.yy.getEdges();
expect(edges[0].id).toBe('id1');
});
});
describe('double ended edges with ids and labels', function () {
doubleEndedEdges.forEach((edgeType) => {
it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () {
const res = flow.parser.parse(
`flowchart TD;\nA e1@${edgeType.edgeStart} label ${edgeType.edgeEnd} B;`
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('B').id).toBe('B');
expect(edges.length).toBe(1);
expect(edges[0].id).toBe('e1');
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe(`${edgeType.type}`);
expect(edges[0].text).toBe('label');
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
});
});
});
});
describe('edges', function () { describe('edges', function () {
doubleEndedEdges.forEach((edgeType) => { doubleEndedEdges.forEach((edgeType) => {
it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () { it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('[Text] when parsing', () => { describe('[Text] when parsing', () => {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
import { vi } from 'vitest'; import { vi } from 'vitest';
const spyOn = vi.spyOn; const spyOn = vi.spyOn;
@ -9,7 +9,9 @@ setConfig({
}); });
describe('[Interactions] when parsing', () => { describe('[Interactions] when parsing', () => {
let flowDb;
beforeEach(function () { beforeEach(function () {
flowDb = new FlowDB();
flow.parser.yy = flowDb; flow.parser.yy = flowDb;
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('[Lines] when parsing', () => { describe('[Lines] when parsing', () => {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('parsing a flow chart with markdown strings', function () { describe('parsing a flow chart with markdown strings', function () {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('when parsing directions', function () { describe('when parsing directions', function () {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
flow.parser.yy.setGen('gen-2'); flow.parser.yy.setGen('gen-2');
}); });
@ -251,7 +251,7 @@ describe('when parsing directions', function () {
expect(data4Layout.nodes[0].shape).toEqual('squareRect'); expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a<br/>multiline string'); expect(data4Layout.nodes[0].label).toEqual('This is a<br/>multiline string');
}); });
it(' should be possible to use } in strings', function () { it('should be possible to use } in strings', function () {
const res = flow.parser.parse(`flowchart TB const res = flow.parser.parse(`flowchart TB
A@{ A@{
label: "This is a string with }" label: "This is a string with }"
@ -264,7 +264,7 @@ describe('when parsing directions', function () {
expect(data4Layout.nodes[0].shape).toEqual('squareRect'); expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with }'); expect(data4Layout.nodes[0].label).toEqual('This is a string with }');
}); });
it(' should be possible to use @ in strings', function () { it('should be possible to use @ in strings', function () {
const res = flow.parser.parse(`flowchart TB const res = flow.parser.parse(`flowchart TB
A@{ A@{
label: "This is a string with @" label: "This is a string with @"
@ -277,7 +277,7 @@ describe('when parsing directions', function () {
expect(data4Layout.nodes[0].shape).toEqual('squareRect'); expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with @'); expect(data4Layout.nodes[0].label).toEqual('This is a string with @');
}); });
it(' should be possible to use @ in strings', function () { it('should be possible to use @ in strings', function () {
const res = flow.parser.parse(`flowchart TB const res = flow.parser.parse(`flowchart TB
A@{ A@{
label: "This is a string with}" label: "This is a string with}"
@ -290,4 +290,126 @@ describe('when parsing directions', function () {
expect(data4Layout.nodes[0].shape).toEqual('squareRect'); expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with}'); expect(data4Layout.nodes[0].label).toEqual('This is a string with}');
}); });
it('should be possible to use @ syntax to add labels on multi nodes', function () {
const res = flow.parser.parse(`flowchart TB
n2["label for n2"] & n4@{ label: "labe for n4"} & n5@{ label: "labe for n5"}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(3);
expect(data4Layout.nodes[0].label).toEqual('label for n2');
expect(data4Layout.nodes[1].label).toEqual('labe for n4');
expect(data4Layout.nodes[2].label).toEqual('labe for n5');
});
it('should be possible to use @ syntax to add labels on multi nodes with edge/link', function () {
const res = flow.parser.parse(`flowchart TD
A["A"] --> B["for B"] & C@{ label: "for c"} & E@{label : "for E"}
D@{label: "for D"}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(5);
expect(data4Layout.nodes[0].label).toEqual('A');
expect(data4Layout.nodes[1].label).toEqual('for B');
expect(data4Layout.nodes[2].label).toEqual('for c');
expect(data4Layout.nodes[3].label).toEqual('for E');
expect(data4Layout.nodes[4].label).toEqual('for D');
});
it('should be possible to use @ syntax in labels', function () {
const res = flow.parser.parse(`flowchart TD
A["@A@"] --> B["@for@ B@"] & C@{ label: "@for@ c@"} & E{"\`@for@ E@\`"} & D(("@for@ D@"))
H1{{"@for@ H@"}}
H2{{"\`@for@ H@\`"}}
Q1{"@for@ Q@"}
Q2{"\`@for@ Q@\`"}
AS1>"@for@ AS@"]
AS2>"\`@for@ AS@\`"]
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(11);
expect(data4Layout.nodes[0].label).toEqual('@A@');
expect(data4Layout.nodes[1].label).toEqual('@for@ B@');
expect(data4Layout.nodes[2].label).toEqual('@for@ c@');
expect(data4Layout.nodes[3].label).toEqual('@for@ E@');
expect(data4Layout.nodes[4].label).toEqual('@for@ D@');
expect(data4Layout.nodes[5].label).toEqual('@for@ H@');
expect(data4Layout.nodes[6].label).toEqual('@for@ H@');
expect(data4Layout.nodes[7].label).toEqual('@for@ Q@');
expect(data4Layout.nodes[8].label).toEqual('@for@ Q@');
expect(data4Layout.nodes[9].label).toEqual('@for@ AS@');
expect(data4Layout.nodes[10].label).toEqual('@for@ AS@');
});
it('should handle unique edge creation with using @ and &', function () {
const res = flow.parser.parse(`flowchart TD
A & B e1@--> C & D
A1 e2@--> C1 & D1
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(7);
expect(data4Layout.edges.length).toBe(6);
expect(data4Layout.edges[0].id).toEqual('L_A_C_0');
expect(data4Layout.edges[1].id).toEqual('L_A_D_0');
expect(data4Layout.edges[2].id).toEqual('e1');
expect(data4Layout.edges[3].id).toEqual('L_B_D_0');
expect(data4Layout.edges[4].id).toEqual('e2');
expect(data4Layout.edges[5].id).toEqual('L_A1_D1_0');
});
it('should handle redefine same edge ids again', function () {
const res = flow.parser.parse(`flowchart TD
A & B e1@--> C & D
A1 e1@--> C1 & D1
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(7);
expect(data4Layout.edges.length).toBe(6);
expect(data4Layout.edges[0].id).toEqual('L_A_C_0');
expect(data4Layout.edges[1].id).toEqual('L_A_D_0');
expect(data4Layout.edges[2].id).toEqual('e1');
expect(data4Layout.edges[3].id).toEqual('L_B_D_0');
expect(data4Layout.edges[4].id).toEqual('L_A1_C1_0');
expect(data4Layout.edges[5].id).toEqual('L_A1_D1_0');
});
it('should handle overriding edge animate again', function () {
const res = flow.parser.parse(`flowchart TD
A e1@--> B
C e2@--> D
E e3@--> F
e1@{ animate: true }
e2@{ animate: false }
e3@{ animate: true }
e3@{ animate: false }
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(6);
expect(data4Layout.edges.length).toBe(3);
expect(data4Layout.edges[0].id).toEqual('e1');
expect(data4Layout.edges[0].animate).toEqual(true);
expect(data4Layout.edges[1].id).toEqual('e2');
expect(data4Layout.edges[1].animate).toEqual(false);
expect(data4Layout.edges[2].id).toEqual('e3');
expect(data4Layout.edges[2].animate).toEqual(false);
});
it.skip('should be possible to use @ syntax to add labels with trail spaces', function () {
const res = flow.parser.parse(
`flowchart TB
n2["label for n2"] & n4@{ label: "labe for n4"} & n5@{ label: "labe for n5"} `
);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(3);
expect(data4Layout.nodes[0].label).toEqual('label for n2');
expect(data4Layout.nodes[1].label).toEqual('labe for n4');
expect(data4Layout.nodes[2].label).toEqual('labe for n5');
});
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -31,7 +31,7 @@ const specialChars = ['#', ':', '0', '&', ',', '*', '.', '\\', 'v', '-', '/', '_
describe('[Singlenodes] when parsing', () => { describe('[Singlenodes] when parsing', () => {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('[Style] when parsing', () => { describe('[Style] when parsing', () => {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
flow.parser.yy.setGen('gen-2'); flow.parser.yy.setGen('gen-2');
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('[Text] when parsing', () => { describe('[Text] when parsing', () => {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('when parsing flowcharts', function () { describe('when parsing flowcharts', function () {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
flow.parser.yy.setGen('gen-2'); flow.parser.yy.setGen('gen-2');
}); });

View File

@ -141,6 +141,7 @@ that id.
.*direction\s+RL[^\n]* return 'direction_rl'; .*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr'; .*direction\s+LR[^\n]* return 'direction_lr';
[^\s\"]+\@(?=[^\{\"]) { return 'LINK_ID'; }
[0-9]+ return 'NUM'; [0-9]+ return 'NUM';
\# return 'BRKT'; \# return 'BRKT';
":::" return 'STYLE_SEPARATOR'; ":::" return 'STYLE_SEPARATOR';
@ -201,7 +202,9 @@ that id.
"*" return 'MULT'; "*" return 'MULT';
"#" return 'BRKT'; "#" return 'BRKT';
"&" return 'AMP'; "&" return 'AMP';
([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ return 'NODE_STRING'; ([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ {
return 'NODE_STRING';
}
"-" return 'MINUS' "-" return 'MINUS'
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]| [\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]| [\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
@ -361,7 +364,7 @@ spaceList
statement statement
: vertexStatement separator : vertexStatement separator
{ /* console.warn('finat vs', $vertexStatement.nodes); */ $$=$vertexStatement.nodes} { $$=$vertexStatement.nodes}
| styleStatement separator | styleStatement separator
{$$=[];} {$$=[];}
| linkStyleStatement separator | linkStyleStatement separator
@ -396,7 +399,7 @@ shapeData:
; ;
vertexStatement: vertexStatement link node shapeData vertexStatement: vertexStatement link node shapeData
{ /* console.warn('vs shapeData',$vertexStatement.stmt,$node, $shapeData);*/ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } } { /* console.warn('vs shapeData',$vertexStatement.stmt,$node, $shapeData);*/ yy.addVertex($node[$node.length-1],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
| vertexStatement link node | vertexStatement link node
{ /*console.warn('vs',$vertexStatement.stmt,$node);*/ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } } { /*console.warn('vs',$vertexStatement.stmt,$node);*/ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
| vertexStatement link node spaceList | vertexStatement link node spaceList
@ -404,7 +407,7 @@ vertexStatement: vertexStatement link node shapeData
|node spaceList { /*console.warn('vertexStatement: node spaceList', $node);*/ $$ = {stmt: $node, nodes:$node }} |node spaceList { /*console.warn('vertexStatement: node spaceList', $node);*/ $$ = {stmt: $node, nodes:$node }}
|node shapeData { |node shapeData {
/*console.warn('vertexStatement: node shapeData', $node[0], $shapeData);*/ /*console.warn('vertexStatement: node shapeData', $node[0], $shapeData);*/
yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); yy.addVertex($node[$node.length-1],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData);
$$ = {stmt: $node, nodes:$node, shapeData: $shapeData} $$ = {stmt: $node, nodes:$node, shapeData: $shapeData}
} }
|node { /* console.warn('vertexStatement: single node', $node); */ $$ = {stmt: $node, nodes:$node }} |node { /* console.warn('vertexStatement: single node', $node); */ $$ = {stmt: $node, nodes:$node }}
@ -413,7 +416,7 @@ vertexStatement: vertexStatement link node shapeData
node: styledVertex node: styledVertex
{ /*console.warn('nod', $styledVertex);*/ $$ = [$styledVertex];} { /*console.warn('nod', $styledVertex);*/ $$ = [$styledVertex];}
| node shapeData spaceList AMP spaceList styledVertex | node shapeData spaceList AMP spaceList styledVertex
{ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); $$ = $node.concat($styledVertex); /*console.warn('pip2', $node[0], $styledVertex, $$);*/ } { yy.addVertex($node[$node.length-1],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); $$ = $node.concat($styledVertex); /*console.warn('pip2', $node[0], $styledVertex, $$);*/ }
| node spaceList AMP spaceList styledVertex | node spaceList AMP spaceList styledVertex
{ $$ = $node.concat($styledVertex); /*console.warn('pip', $node[0], $styledVertex, $$);*/ } { $$ = $node.concat($styledVertex); /*console.warn('pip', $node[0], $styledVertex, $$);*/ }
; ;
@ -472,6 +475,8 @@ link: linkStatement arrowText
{$$ = $linkStatement;} {$$ = $linkStatement;}
| START_LINK edgeText LINK | START_LINK edgeText LINK
{var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText};} {var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText};}
| LINK_ID START_LINK edgeText LINK
{var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText, "id": $LINK_ID};}
; ;
edgeText: edgeTextToken edgeText: edgeTextToken
@ -487,6 +492,8 @@ edgeText: edgeTextToken
linkStatement: LINK linkStatement: LINK
{var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};} {var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};}
| LINK_ID LINK
{var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length, "id": $LINK_ID};}
; ;
arrowText: arrowText:

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { cleanupComments } from '../../../diagram-api/comments.js'; import { cleanupComments } from '../../../diagram-api/comments.js';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
@ -9,7 +9,7 @@ setConfig({
describe('parsing a flow chart', function () { describe('parsing a flow chart', function () {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
}); });

View File

@ -0,0 +1,12 @@
// @ts-ignore: JISON doesn't support types
import flowJisonParser from './flow.jison';
const newParser = Object.assign({}, flowJisonParser);
newParser.parse = (src: string): unknown => {
// remove the trailing whitespace after closing curly braces when ending a line break
const newSrc = src.replace(/}\s*\n/g, '}\n');
return flowJisonParser.parse(newSrc);
};
export default newParser;

View File

@ -1,5 +1,5 @@
import flowDb from '../flowDb.js'; import { FlowDB } from '../flowDb.js';
import flow from './flow.jison'; import flow from './flowParser.ts';
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
setConfig({ setConfig({
@ -8,7 +8,7 @@ setConfig({
describe('when parsing subgraphs', function () { describe('when parsing subgraphs', function () {
beforeEach(function () { beforeEach(function () {
flow.parser.yy = flowDb; flow.parser.yy = new FlowDB();
flow.parser.yy.clear(); flow.parser.yy.clear();
flow.parser.yy.setGen('gen-2'); flow.parser.yy.setGen('gen-2');
}); });

View File

@ -53,6 +53,7 @@ export interface FlowText {
} }
export interface FlowEdge { export interface FlowEdge {
isUserDefinedId: boolean;
start: string; start: string;
end: string; end: string;
interpolate?: string; interpolate?: string;
@ -62,6 +63,10 @@ export interface FlowEdge {
length?: number; length?: number;
text: string; text: string;
labelType: 'text'; labelType: 'text';
classes: string[];
id?: string;
animation?: 'fast' | 'slow';
animate?: boolean;
} }
export interface FlowClass { export interface FlowClass {

View File

@ -1,7 +1,7 @@
const getStyles = (options) => const getStyles = (options) =>
` `
.mermaid-main-font { .mermaid-main-font {
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif); font-family: ${options.fontFamily};
} }
.exclude-range { .exclude-range {
@ -45,7 +45,7 @@ const getStyles = (options) =>
.sectionTitle { .sectionTitle {
text-anchor: start; text-anchor: start;
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif); font-family: ${options.fontFamily};
} }
@ -86,13 +86,13 @@ const getStyles = (options) =>
.taskText { .taskText {
text-anchor: middle; text-anchor: middle;
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif); font-family: ${options.fontFamily};
} }
.taskTextOutsideRight { .taskTextOutsideRight {
fill: ${options.taskTextDarkColor}; fill: ${options.taskTextDarkColor};
text-anchor: start; text-anchor: start;
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif); font-family: ${options.fontFamily};
} }
.taskTextOutsideLeft { .taskTextOutsideLeft {
@ -248,7 +248,7 @@ const getStyles = (options) =>
text-anchor: middle; text-anchor: middle;
font-size: 18px; font-size: 18px;
fill: ${options.titleColor || options.textColor}; fill: ${options.titleColor || options.textColor};
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif); font-family: ${options.fontFamily};
} }
`; `;

View File

@ -4,11 +4,13 @@ import parser from './parser/sankey.jison';
import db from './sankeyDB.js'; import db from './sankeyDB.js';
import renderer from './sankeyRenderer.js'; import renderer from './sankeyRenderer.js';
import { prepareTextForParsing } from './sankeyUtils.js'; import { prepareTextForParsing } from './sankeyUtils.js';
import sankeyStyles from './styles.js';
const originalParse = parser.parse.bind(parser); const originalParse = parser.parse.bind(parser);
parser.parse = (text: string) => originalParse(prepareTextForParsing(text)); parser.parse = (text: string) => originalParse(prepareTextForParsing(text));
export const diagram: DiagramDefinition = { export const diagram: DiagramDefinition = {
styles: sankeyStyles,
parser, parser,
db, db,
renderer, renderer,

View File

@ -136,7 +136,6 @@ export const draw = function (text: string, id: string, _version: string, diagOb
svg svg
.append('g') .append('g')
.attr('class', 'node-labels') .attr('class', 'node-labels')
.attr('font-family', 'sans-serif')
.attr('font-size', 14) .attr('font-size', 14)
.selectAll('text') .selectAll('text')
.data(graph.nodes) .data(graph.nodes)

View File

@ -0,0 +1,6 @@
const getStyles = (options) =>
`.label {
font-family: ${options.fontFamily};
}`;
export default getStyles;

View File

@ -15,6 +15,5 @@ export const diagram: DiagramDefinition = {
cnf.state = {}; cnf.state = {};
} }
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
db.clear();
}, },
}; };

View File

@ -15,6 +15,5 @@ export const diagram: DiagramDefinition = {
cnf.state = {}; cnf.state = {};
} }
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
db.clear();
}, },
}; };

View File

@ -1,7 +1,6 @@
const getStyles = (options) => const getStyles = (options) =>
`.label { `.label {
font-family: 'trebuchet ms', verdana, arial, sans-serif; font-family: ${options.fontFamily};
font-family: var(--mermaid-font-family);
color: ${options.textColor}; color: ${options.textColor};
} }
.mouth { .mouth {
@ -79,8 +78,7 @@ const getStyles = (options) =>
text-align: center; text-align: center;
max-width: 200px; max-width: 200px;
padding: 2px; padding: 2px;
font-family: 'trebuchet ms', verdana, arial, sans-serif; font-family: ${options.fontFamily};
font-family: var(--mermaid-font-family);
font-size: 12px; font-size: 12px;
background: ${options.tertiaryColor}; background: ${options.tertiaryColor};
border: 1px solid ${options.border2}; border: 1px solid ${options.border2};

View File

@ -94,8 +94,7 @@ Blogging frameworks and platforms
- [Nextra](https://nextra.site/) - [Nextra](https://nextra.site/)
- [Mermaid](https://nextra.site/docs/guide/mermaid) - [Mermaid](https://nextra.site/docs/guide/mermaid)
- [WordPress](https://wordpress.org) - [WordPress](https://wordpress.org)
- [WordPress Markdown Editor](https://wordpress.org/plugins/wp-githuber-md) - [MerPRess](https://wordpress.org/plugins/merpress/)
- [WP-ReliableMD](https://wordpress.org/plugins/wp-reliablemd/)
### CMS/ECM ### CMS/ECM

View File

@ -711,6 +711,67 @@ flowchart TB
B --> D B --> D
``` ```
### Attaching an ID to Edges
Mermaid now supports assigning IDs to edges, similar to how IDs and metadata can be attached to nodes. This feature lays the groundwork for more advanced styling, classes, and animation capabilities on edges.
**Syntax:**
To give an edge an ID, prepend the edge syntax with the ID followed by an `@` character. For example:
```mermaid
flowchart LR
A e1@> B
```
In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
### Turning an Animation On
Once you have assigned an ID to an edge, you can turn on animations for that edge by defining the edges properties:
```mermaid
flowchart LR
A e1@==> B
e1@{ animate: true }
```
This tells Mermaid that the edge `e1` should be animated.
### Selecting Type of Animation
In the initial version, two animation speeds are supported: `fast` and `slow`. Selecting a specific animation type is a shorthand for enabling animation and setting the animation speed in one go.
**Examples:**
```mermaid
flowchart LR
A e1@> B
e1@{ animation: fast }
```
This is equivalent to `{ animate: true, animation: fast }`.
### Using classDef Statements for Animations
You can also animate edges by assigning a class to them and then defining animation properties in a `classDef` statement. For example:
```mermaid
flowchart LR
A e1@> B
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
```
In this snippet:
- `e1@-->` creates an edge with ID `e1`.
- `classDef animate` defines a class named `animate` with styling and animation properties.
- `class e1 animate` applies the `animate` class to the edge `e1`.
**Note on Escaping Commas:**
When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaids style definitions.
## New arrow types ## New arrow types
There are new types of arrows supported: There are new types of arrows supported:

View File

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
// ------------------------------------- // -------------------------------------
// Mocks and mocking // Mocks and mocking
@ -69,6 +69,8 @@ import { compile, serialize } from 'stylis';
import { Diagram } from './Diagram.js'; import { Diagram } from './Diagram.js';
import { decodeEntities, encodeEntities } from './utils.js'; import { decodeEntities, encodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js'; import { toBase64 } from './utils/base64.js';
import { ClassDB } from './diagrams/class/classDb.js';
import { FlowDB } from './diagrams/flowchart/flowDb.js';
/** /**
* @see https://vitest.dev/guide/mocking.html Mock part of a module * @see https://vitest.dev/guide/mocking.html Mock part of a module
@ -832,5 +834,93 @@ graph TD;A--x|text including URL space|B;`)
expect(diagram).toBeInstanceOf(Diagram); expect(diagram).toBeInstanceOf(Diagram);
expect(diagram.type).toBe('flowchart-v2'); expect(diagram.type).toBe('flowchart-v2');
}); });
it('should not modify db when rendering different diagrams', async () => {
const flowDiagram1 = await mermaidAPI.getDiagramFromText(
`flowchart LR
A -- text --> B -- text2 --> C`
);
const flowDiagram2 = await mermaidAPI.getDiagramFromText(
`flowchart TD
A -- text --> B -- text2 --> C`
);
// Since flowDiagram will return new Db object each time, we can compare the db to be different.
expect(flowDiagram1.db).not.toBe(flowDiagram2.db);
assert(flowDiagram1.db instanceof FlowDB);
assert(flowDiagram2.db instanceof FlowDB);
expect(flowDiagram1.db.getDirection()).not.toEqual(flowDiagram2.db.getDirection());
const classDiagram1 = await mermaidAPI.getDiagramFromText(
`classDiagram
direction TB
class Student {
-idCard : IdCard
}
class IdCard{
-id : int
-name : string
}
class Bike{
-id : int
-name : string
}
Student "1" --o "1" IdCard : carries
Student "1" --o "1" Bike : rides`
);
const classDiagram2 = await mermaidAPI.getDiagramFromText(
`classDiagram
direction LR
class Student {
-idCard : IdCard
}
class IdCard{
-id : int
-name : string
}
class Bike{
-id : int
-name : string
}
Student "1" --o "1" IdCard : carries
Student "1" --o "1" Bike : rides`
);
// Since classDiagram will return new Db object each time, we can compare the db to be different.
expect(classDiagram1.db).not.toBe(classDiagram2.db);
assert(classDiagram1.db instanceof ClassDB);
assert(classDiagram2.db instanceof ClassDB);
expect(classDiagram1.db.getDirection()).not.toEqual(classDiagram2.db.getDirection());
const sequenceDiagram1 = await mermaidAPI.getDiagramFromText(
`sequenceDiagram
Alice->>+John: Hello John, how are you?
Alice->>+John: John, can you hear me?
John-->>-Alice: Hi Alice, I can hear you!
John-->>-Alice: I feel great!`
);
const sequenceDiagram2 = await mermaidAPI.getDiagramFromText(
`sequenceDiagram
Alice->>+John: Hello John, how are you?
Alice->>+John: John, can you hear me?
John-->>-Alice: Hi Alice, I can hear you!
John-->>-Alice: I feel great!`
);
// Since sequenceDiagram will return same Db object each time, we can compare the db to be same.
expect(sequenceDiagram1.db).toBe(sequenceDiagram2.db);
});
});
// Sequence Diagram currently uses a singleton DB, so this test will fail
it.fails('should not modify db when rendering different sequence diagrams', async () => {
const sequenceDiagram1 = await mermaidAPI.getDiagramFromText(
`sequenceDiagram
Alice->>Bob: Hello Bob, how are you?
Bob-->>John: How about you John?`
);
const sequenceDiagram2 = await mermaidAPI.getDiagramFromText(
`sequenceDiagram
Alice->>Bob: Hello Bob, how are you?
Bob-->>John: How about you John?`
);
expect(sequenceDiagram1.db).not.toBe(sequenceDiagram2.db);
}); });
}); });

View File

@ -9,6 +9,7 @@ import { curveBasis, line, select } from 'd3';
import rough from 'roughjs'; import rough from 'roughjs';
import createLabel from './createLabel.js'; import createLabel from './createLabel.js';
import { addEdgeMarkers } from './edgeMarker.ts'; import { addEdgeMarkers } from './edgeMarker.ts';
import { isLabelStyle } from './shapes/handDrawnShapeStyles.js';
const edgeLabels = new Map(); const edgeLabels = new Map();
const terminalLabels = new Map(); const terminalLabels = new Map();
@ -428,6 +429,13 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
let pointsHasChanged = false; let pointsHasChanged = false;
const tail = startNode; const tail = startNode;
var head = endNode; var head = endNode;
const edgeClassStyles = [];
for (const key in edge.cssCompiledStyles) {
if (isLabelStyle(key)) {
continue;
}
edgeClassStyles.push(edge.cssCompiledStyles[key]);
}
if (head.intersect && tail.intersect) { if (head.intersect && tail.intersect) {
points = points.slice(1, edge.points.length - 1); points = points.slice(1, edge.points.length - 1);
@ -501,6 +509,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
let svgPath; let svgPath;
let linePath = lineFunction(lineData); let linePath = lineFunction(lineData);
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style]; const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
if (edge.look === 'handDrawn') { if (edge.look === 'handDrawn') {
const rc = rough.svg(elem); const rc = rough.svg(elem);
Object.assign([], lineData); Object.assign([], lineData);
@ -521,12 +530,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
svgPath.attr('d', d); svgPath.attr('d', d);
elem.node().appendChild(svgPath.node()); elem.node().appendChild(svgPath.node());
} else { } else {
const stylesFromClasses = edgeClassStyles.join(';');
const styles = edgeStyles ? edgeStyles.reduce((acc, style) => acc + style + ';', '') : '';
let animationClass = '';
if (edge.animate) {
animationClass = ' edge-animation-fast';
}
if (edge.animation) {
animationClass = ' edge-animation-' + edge.animation;
}
svgPath = elem svgPath = elem
.append('path') .append('path')
.attr('d', linePath) .attr('d', linePath)
.attr('id', edge.id) .attr('id', edge.id)
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '')) .attr(
.attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : ''); 'class',
' ' +
strokeClasses +
(edge.classes ? ' ' + edge.classes : '') +
(animationClass ? animationClass : '')
)
.attr('style', stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles);
} }
// DEBUG code, DO NOT REMOVE // DEBUG code, DO NOT REMOVE

View File

@ -32,7 +32,28 @@ export const styles2Map = (styles: string[]) => {
}); });
return styleMap; return styleMap;
}; };
export const isLabelStyle = (key: string) => {
return (
key === 'color' ||
key === 'font-size' ||
key === 'font-family' ||
key === 'font-weight' ||
key === 'font-style' ||
key === 'text-decoration' ||
key === 'text-align' ||
key === 'text-transform' ||
key === 'line-height' ||
key === 'letter-spacing' ||
key === 'word-spacing' ||
key === 'text-shadow' ||
key === 'text-overflow' ||
key === 'white-space' ||
key === 'word-wrap' ||
key === 'word-break' ||
key === 'overflow-wrap' ||
key === 'hyphens'
);
};
export const styles2String = (node: Node) => { export const styles2String = (node: Node) => {
const { stylesArray } = compileStyles(node); const { stylesArray } = compileStyles(node);
const labelStyles: string[] = []; const labelStyles: string[] = [];
@ -42,26 +63,7 @@ export const styles2String = (node: Node) => {
stylesArray.forEach((style) => { stylesArray.forEach((style) => {
const key = style[0]; const key = style[0];
if ( if (isLabelStyle(key)) {
key === 'color' ||
key === 'font-size' ||
key === 'font-family' ||
key === 'font-weight' ||
key === 'font-style' ||
key === 'text-decoration' ||
key === 'text-align' ||
key === 'text-transform' ||
key === 'line-height' ||
key === 'letter-spacing' ||
key === 'word-spacing' ||
key === 'text-shadow' ||
key === 'text-overflow' ||
key === 'white-space' ||
key === 'word-wrap' ||
key === 'word-break' ||
key === 'overflow-wrap' ||
key === 'hyphens'
) {
labelStyles.push(style.join(':') + ' !important'); labelStyles.push(style.join(':') + ' !important');
} else { } else {
nodeStyles.push(style.join(':') + ' !important'); nodeStyles.push(style.join(':') + ' !important');

View File

@ -96,11 +96,14 @@ export interface Edge {
label?: string; label?: string;
classes?: string; classes?: string;
style?: string[]; style?: string[];
animate?: boolean;
animation?: 'fast' | 'slow';
// Properties common to both Flowchart and State Diagram edges // Properties common to both Flowchart and State Diagram edges
arrowhead?: string; arrowhead?: string;
arrowheadStyle?: string; arrowheadStyle?: string;
arrowTypeEnd?: string; arrowTypeEnd?: string;
arrowTypeStart?: string; arrowTypeStart?: string;
cssCompiledStyles?: string[];
// Flowchart specific properties // Flowchart specific properties
defaultInterpolate?: string; defaultInterpolate?: string;
end?: string; end?: string;
@ -122,6 +125,7 @@ export interface Edge {
pattern?: string; pattern?: string;
thickness?: 'normal' | 'thick' | 'invisible' | 'dotted'; thickness?: 'normal' | 'thick' | 'invisible' | 'dotted';
look?: string; look?: string;
isUserDefinedId?: boolean;
} }
export interface RectOptions { export interface RectOptions {

View File

@ -27,7 +27,28 @@ const getStyles = (
font-size: ${options.fontSize}; font-size: ${options.fontSize};
fill: ${options.textColor} fill: ${options.textColor}
} }
@keyframes edge-animation-frame {
from {
stroke-dashoffset: 0;
}
}
@keyframes dash {
to {
stroke-dashoffset: 0;
}
}
& .edge-animation-slow {
stroke-dasharray: 9,5 !important;
stroke-dashoffset: 900;
animation: dash 50s linear infinite;
stroke-linecap: round;
}
& .edge-animation-fast {
stroke-dasharray: 9,5 !important;
stroke-dashoffset: 900;
animation: dash 20s linear infinite;
stroke-linecap: round;
}
/* Classes common for multiple diagrams */ /* Classes common for multiple diagrams */
& .error-icon { & .error-icon {

View File

@ -12,6 +12,11 @@ export interface NodeMetaData {
assigned?: string; assigned?: string;
ticket?: string; ticket?: string;
} }
export interface EdgeMetaData {
animation?: 'fast' | 'slow';
animate?: boolean;
}
import type { MermaidConfig } from './config.type.js'; import type { MermaidConfig } from './config.type.js';
export interface Point { export interface Point {

View File

@ -937,8 +937,12 @@ export const getEdgeId = (
counter?: number; counter?: number;
prefix?: string; prefix?: string;
suffix?: string; suffix?: string;
} },
id?: string
) => { ) => {
if (id) {
return id;
}
return `${prefix ? `${prefix}_` : ''}${from}_${to}_${counter}${suffix ? `_${suffix}` : ''}`; return `${prefix ? `${prefix}_` : ''}${from}_${to}_${counter}${suffix ? `_${suffix}` : ''}`;
}; };

3062
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff