mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-19 00:06:47 +02:00
Compare commits
135 Commits
test-valid
...
usecase_di
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5c04a8d09d | ||
![]() |
d93d9a521d | ||
![]() |
c99bce6bab | ||
![]() |
4ab98c2ec7 | ||
![]() |
aeb51e56e2 | ||
![]() |
ddcd8a5e73 | ||
![]() |
e464d080ef | ||
![]() |
1a9b94ca2d | ||
![]() |
e4b33a1d99 | ||
![]() |
e27a9da61d | ||
![]() |
03cf10003f | ||
![]() |
8e31fdb611 | ||
![]() |
5dd748148f | ||
![]() |
895f9d43ff | ||
![]() |
fb890a2be8 | ||
![]() |
5986189a52 | ||
![]() |
1988dfc956 | ||
![]() |
e48b0ba61d | ||
![]() |
1a4b8662cf | ||
![]() |
6083463c8e | ||
![]() |
1a14e331ea | ||
![]() |
ebb6680eba | ||
![]() |
6d1d46f88a | ||
![]() |
435790f931 | ||
![]() |
ffe9c1090e | ||
![]() |
a476e99d4c | ||
![]() |
0cc0b63e52 | ||
![]() |
694844050a | ||
![]() |
1be1620000 | ||
![]() |
c36cd05c45 | ||
![]() |
b7a591b8d3 | ||
![]() |
8bb29fc879 | ||
![]() |
e073c80019 | ||
![]() |
01aaef39b4 | ||
![]() |
3d640fc620 | ||
![]() |
724197c910 | ||
![]() |
6180c5f2ff | ||
![]() |
a9f7a94ae3 | ||
![]() |
3ffe9618ae | ||
![]() |
da539c1fa1 | ||
![]() |
5e8aa2dccf | ||
![]() |
ac04172cf8 | ||
![]() |
cf5b4b89a8 | ||
![]() |
1c269e0432 | ||
![]() |
999b836508 | ||
![]() |
326e4e3693 | ||
![]() |
9c92da487f | ||
![]() |
10752f1357 | ||
![]() |
1a80854242 | ||
![]() |
fc9c600a31 | ||
![]() |
da8ce0b93e | ||
![]() |
00a79353fc | ||
![]() |
1ceeca1ef1 | ||
![]() |
94890390ef | ||
![]() |
adfeb093cb | ||
![]() |
366d217928 | ||
![]() |
b94f1336ab | ||
![]() |
020c6d66e0 | ||
![]() |
cfc2551bdc | ||
![]() |
000308c8f5 | ||
![]() |
6039a8b930 | ||
![]() |
cd282f2245 | ||
![]() |
a27d90fe9c | ||
![]() |
64bf34b9ab | ||
![]() |
9faf2f9fb2 | ||
![]() |
f683b03645 | ||
![]() |
9cef40d164 | ||
![]() |
04612e078a | ||
![]() |
af585bdcc7 | ||
![]() |
37bfa2aa75 | ||
![]() |
54640ce476 | ||
![]() |
47b4c56b2b | ||
![]() |
6b1b0bf151 | ||
![]() |
7ba332ad4a | ||
![]() |
412d2a09d3 | ||
![]() |
7886fed8b2 | ||
![]() |
af3d5b6528 | ||
![]() |
404286a90d | ||
![]() |
827a9af790 | ||
![]() |
95733b6295 | ||
![]() |
8b86d617e7 | ||
![]() |
cc2112c7aa | ||
![]() |
767754f4fb | ||
![]() |
cff59c58b4 | ||
![]() |
ac976245ad | ||
![]() |
5b241bbb97 | ||
![]() |
7e5e47843b | ||
![]() |
9e2cd1a926 | ||
![]() |
abf2227faf | ||
![]() |
7db942b0e1 | ||
![]() |
70041c806f | ||
![]() |
77e2703f72 | ||
![]() |
771801b366 | ||
![]() |
a9a0a9b2de | ||
![]() |
1e5e835c41 | ||
![]() |
1f3f8da0f7 | ||
![]() |
b11f40e8ce | ||
![]() |
1fe045e638 | ||
![]() |
1c750ffc70 | ||
![]() |
b0ec93f29c | ||
![]() |
6b071c135a | ||
![]() |
42a3c3487f | ||
![]() |
d7b8ed2c5a | ||
![]() |
96c21c7e54 | ||
![]() |
e95e4d155a | ||
![]() |
688170558c | ||
![]() |
832f012e10 | ||
![]() |
2e2e8c4152 | ||
![]() |
6ff6e08c4b | ||
![]() |
d3e2be35be | ||
![]() |
260a045da0 | ||
![]() |
34e91f8b65 | ||
![]() |
627ee1f34d | ||
![]() |
28840ebd84 | ||
![]() |
4145879003 | ||
![]() |
e097b480d5 | ||
![]() |
003d1c7a70 | ||
![]() |
daf8d8d3be | ||
![]() |
d7a55b422b | ||
![]() |
12e3d31437 | ||
![]() |
ad024b01d6 | ||
![]() |
c12aea588c | ||
![]() |
9dfbf1166d | ||
![]() |
98bf9b4cb4 | ||
![]() |
e9ce8cf4da | ||
![]() |
d90634bf2b | ||
![]() |
90707e8062 | ||
![]() |
7e23f984e6 | ||
![]() |
b3a12237c0 | ||
![]() |
12c94a177b | ||
![]() |
71b04f93b0 | ||
![]() |
27185f62e4 | ||
![]() |
939da082b2 | ||
![]() |
e70be4f155 | ||
![]() |
6621f6ddb2 |
5
.changeset/beige-peas-shave.md
Normal file
5
.changeset/beige-peas-shave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@mermaid-js/mermaid-zenuml': patch
|
||||
---
|
||||
|
||||
Fixed a critical bug that the ZenUML diagram is not rendered.
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add `getRegisteredDiagramsMetadata` to `mermaid`, which returns all the registered diagram IDs in mermaid
|
5
.changeset/large-mirrors-cheer.md
Normal file
5
.changeset/large-mirrors-cheer.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Update casing of ID in requirement diagram
|
5
.changeset/lemon-masks-unite.md
Normal file
5
.changeset/lemon-masks-unite.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Added support for per link curve styling in flowchart diagram using edge ids
|
5
.changeset/light-flowers-judge.md
Normal file
5
.changeset/light-flowers-judge.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Make flowchart elk detector regex match less greedy
|
8
.changeset/lovely-queens-own.md
Normal file
8
.changeset/lovely-queens-own.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix(block): overflowing blocks no longer affect later lines
|
||||
|
||||
This may change the layout of block diagrams that have overflowing lines
|
||||
(i.e. block diagrams that use up more columns that the `columns` specifier).
|
7
.changeset/ninety-roses-turn.md
Normal file
7
.changeset/ninety-roses-turn.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: log warning for blocks exceeding column width
|
||||
|
||||
This update adds a validation check that logs a warning message when a block's width exceeds the defined column layout.
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@mermaid-js/examples': minor
|
||||
---
|
||||
|
||||
feat: Add examples for diagrams in the `@mermaid-js/examples` package
|
5
.changeset/rare-women-fly.md
Normal file
5
.changeset/rare-women-fly.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Add escaped class literal name on namespace
|
5
.changeset/silver-eyes-build.md
Normal file
5
.changeset/silver-eyes-build.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: migrate to class-based ArchitectureDB implementation
|
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'@mermaid-js/examples': patch
|
||||
'mermaid': patch
|
||||
'@mermaid-js/parser': patch
|
||||
---
|
||||
|
||||
chore: Move packet diagram out of beta
|
5
.changeset/smart-humans-cover.md
Normal file
5
.changeset/smart-humans-cover.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Update flowchart direction TD's behavior to be the same as TB
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: adjust sequence diagram title positioning to prevent overlap with top border in Safari
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: downgrade chokidar to 3.6.0 and ignore updates for hot reload stability
|
5
.changeset/vast-buses-see.md
Normal file
5
.changeset/vast-buses-see.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: Update packet diagram to use new class-based database structure
|
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix(timeline): fix loading `leftMargin` from config
|
||||
|
||||
The `timeline.leftMargin` config value should now correctly control the size of the left margin, instead of being ignored.
|
@@ -143,6 +143,7 @@ typeof
|
||||
typestr
|
||||
unshift
|
||||
urlsafe
|
||||
usecase
|
||||
verifymethod
|
||||
VERIFYMTHD
|
||||
WARN_DOCSDIR_DOESNT_MATCH
|
||||
|
3
.github/lychee.toml
vendored
3
.github/lychee.toml
vendored
@@ -52,6 +52,9 @@ exclude = [
|
||||
# Swimm returns 404, even though the link is valid
|
||||
"https://docs.swimm.io",
|
||||
|
||||
# Certificate Error
|
||||
"https://noteshub.app",
|
||||
|
||||
# Timeout
|
||||
"https://huehive.co",
|
||||
"https://foswiki.org",
|
||||
|
2
.github/workflows/e2e-timings.yml
vendored
2
.github/workflows/e2e-timings.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and create pull request
|
||||
uses: peter-evans/create-pull-request@889dce9eaba7900ce30494f5e1ac7220b27e5c81
|
||||
uses: peter-evans/create-pull-request@07cbaebb4bfc9c5d7db426ea5a5f585df29dd0a0
|
||||
with:
|
||||
add-paths: |
|
||||
cypress/timings.json
|
||||
|
6
.github/workflows/validate-lockfile.yml
vendored
6
.github/workflows/validate-lockfile.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
# 2) No unwanted vitepress paths
|
||||
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then
|
||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run \`rm -rf packages/mermaid/src/vitepress && pnpm install\` to regenerate.")
|
||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run `rm -rf packages/mermaid/src/vitepress && pnpm install` to regenerate.")
|
||||
fi
|
||||
|
||||
# 3) Lockfile only changes when package.json changes
|
||||
@@ -46,11 +46,12 @@ jobs:
|
||||
|
||||
# If any issues, output them and fail
|
||||
if [ ${#issues[@]} -gt 0 ]; then
|
||||
# Use the new GITHUB_OUTPUT approach to set a multiline output
|
||||
{
|
||||
echo "errors<<EOF"
|
||||
printf '%s\n' "${issues[@]}"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
} >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -62,7 +63,6 @@ jobs:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
The following issue(s) were detected:
|
||||
|
||||
${{ steps.validate.outputs.errors }}
|
||||
|
||||
Please address these and push an update.
|
||||
|
1005
CHANGELOG.md
1005
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
1
CHANGELOG.md
Symbolic link
1
CHANGELOG.md
Symbolic link
@@ -0,0 +1 @@
|
||||
./packages/mermaid/CHANGELOG.md
|
@@ -1,13 +0,0 @@
|
||||
import { MockedD3 } from '../packages/mermaid/src/tests/MockedD3.js';
|
||||
|
||||
export const select = function () {
|
||||
return new MockedD3();
|
||||
};
|
||||
|
||||
export const selectAll = function () {
|
||||
return new MockedD3();
|
||||
};
|
||||
|
||||
export const curveBasis = 'basis';
|
||||
export const curveLinear = 'linear';
|
||||
export const curveCardinal = 'cardinal';
|
@@ -384,4 +384,17 @@ describe('Block diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('BL30: block should overflow if too wide for columns', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
columns 2
|
||||
fit:2
|
||||
overflow:3
|
||||
short:1
|
||||
also_overflow:2
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -495,4 +495,34 @@ describe('Class diagram', () => {
|
||||
cy.get('a').should('have.attr', 'target', '_blank').should('have.attr', 'rel', 'noopener');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Include char sequence "graph" in text (#6795)', () => {
|
||||
it('has a label with char sequence "graph"', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
class Person {
|
||||
+String name
|
||||
-Int id
|
||||
#double age
|
||||
+Text demographicProfile
|
||||
}
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle backticks for namespace and class names', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
namespace \`A::B\` {
|
||||
class \`IPC::Sender\`
|
||||
}
|
||||
RenderProcessHost --|> \`IPC::Sender\`
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -354,4 +354,19 @@ ORDER ||--|{ LINE-ITEM : contains
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
describe('Include char sequence "graph" in text (#6795)', () => {
|
||||
it('has a label with char sequence "graph"', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
p[Photograph] {
|
||||
varchar(12) jobId
|
||||
date dateCreated
|
||||
}
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1113,4 +1113,24 @@ end
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('6617: Per Link Curve Styling using edge Ids', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart TD
|
||||
A e1@-->B e5@--> E
|
||||
E e7@--> D
|
||||
B e3@-->D
|
||||
A e2@-->C e4@-->D
|
||||
C e6@--> F
|
||||
F e8@--> D
|
||||
e1@{ curve: natural }
|
||||
e2@{ curve: stepAfter }
|
||||
e3@{ curve: monotoneY }
|
||||
e4@{ curve: bumpY }
|
||||
e5@{ curve: linear }
|
||||
e6@{ curve: catmullRom }
|
||||
e7@{ curve: cardinal }
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -246,5 +246,22 @@ Word!\`]
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Include char sequence "graph" in text (#6795)', () => {
|
||||
it('has a label with char sequence "graph"', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
root
|
||||
Photograph
|
||||
Waterfall
|
||||
Landscape
|
||||
Geography
|
||||
Mountains
|
||||
Rocks
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
/* The end */
|
||||
});
|
||||
|
@@ -301,7 +301,7 @@ If you are adding a feature, you will definitely need to add tests. Depending on
|
||||
|
||||
Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run.
|
||||
|
||||
Unit tests are mandatory for all code except the renderers. (The renderers are tested with integration tests.)
|
||||
Unit tests are mandatory for all code except the layout tests. (The layouts are tested with integration tests.)
|
||||
|
||||
We use [Vitest](https://vitest.dev) to run unit tests.
|
||||
|
||||
@@ -327,6 +327,30 @@ When using Docker prepend your command with `./run`:
|
||||
./run pnpm test
|
||||
```
|
||||
|
||||
##### Testing the DOM
|
||||
|
||||
One can use `jsdomIt` to test any part of Mermaid that interacts with the DOM, as long as it is not related to the layout.
|
||||
|
||||
The function `jsdomIt` ([developed in utils.ts](../../tests/util.ts)) overrides `it` from `vitest`, and creates a pseudo-browser environment that works almost like the real deal for the duration of the test. It uses JSDOM to create a DOM, and adds objects `window` and `document` to `global` to mock the browser environment.
|
||||
|
||||
> \[!NOTE]
|
||||
> The layout cannot work in `jsdomIt` tests because JSDOM has no rendering engine, hence the pseudo-browser environment.
|
||||
|
||||
Example :
|
||||
|
||||
```typescript
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
|
||||
jsdomIt('should add element "thing" in the SVG', ({ svg }) => {
|
||||
// Code in this block runs in a pseudo-browser environment
|
||||
addThing(svg); // The svg item is the D3 selection of the SVG node
|
||||
const svgNode = ensureNodeFromSelector('svg'); // Retrieve the DOM node using the DOM API
|
||||
expect(svgNode.querySelector('thing')).not.toBeNull(); // Test the structure of the SVG
|
||||
});
|
||||
```
|
||||
|
||||
They can be used to test any method that interacts with the DOM, including for testing renderers. For renderers, additional integration testing is necessary to test the layout though.
|
||||
|
||||
#### Integration / End-to-End (E2E) Tests
|
||||
|
||||
These test the rendering and visual appearance of the diagrams.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseOptions
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L59)
|
||||
Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:59](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **suppressErrors**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L64)
|
||||
Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77)
|
||||
|
||||
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
||||
The `parseError` function will not be called.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:67](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L67)
|
||||
Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:67](https://github.com/mermaid-js/mer
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:75](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L75)
|
||||
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
|
||||
|
||||
The config passed as YAML frontmatter or directives
|
||||
|
||||
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L71)
|
||||
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: RenderResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:85](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L85)
|
||||
Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:85](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **bindFunctions**: (`element`) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:103](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L103)
|
||||
Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
|
||||
|
||||
Bind function to be called after the svg has been inserted into the DOM.
|
||||
This is necessary for adding event listeners to the elements in the svg.
|
||||
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
|
||||
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
> **svg**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
|
||||
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
|
||||
|
||||
The svg code for the rendered graph.
|
||||
|
189
docs/diagrams/flowchart-code-flow.mmd
Normal file
189
docs/diagrams/flowchart-code-flow.mmd
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDiagram.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDb.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/styles.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/types.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowChartShapes.js"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/elk/detector.ts"
|
||||
generationTime: 2025-07-23T10:31:53.266Z
|
||||
---
|
||||
flowchart TD
|
||||
%% Entry Points and Detection
|
||||
Input["User Input Text"] --> Detection{Detection Phase}
|
||||
|
||||
Detection --> flowDetector["flowDetector.ts<br/>detector(txt, config)"]
|
||||
Detection --> flowDetectorV2["flowDetector-v2.ts<br/>detector(txt, config)"]
|
||||
Detection --> elkDetector["elk/detector.ts<br/>detector(txt, config)"]
|
||||
|
||||
flowDetector --> |"Checks /^\s*graph/"| DetectLegacy{Legacy Flowchart?}
|
||||
flowDetectorV2 --> |"Checks /^\s*flowchart/"| DetectNew{New Flowchart?}
|
||||
elkDetector --> |"Checks /^\s*flowchart-elk/"| DetectElk{ELK Layout?}
|
||||
|
||||
DetectLegacy --> |Yes| LoadDiagram
|
||||
DetectNew --> |Yes| LoadDiagram
|
||||
DetectElk --> |Yes| LoadDiagram
|
||||
|
||||
%% Loading Phase
|
||||
LoadDiagram["loader() function"] --> flowDiagram["flowDiagram.ts<br/>diagram object"]
|
||||
|
||||
flowDiagram --> DiagramStructure{Diagram Components}
|
||||
DiagramStructure --> Parser["parser: flowParser"]
|
||||
DiagramStructure --> Database["db: new FlowDB()"]
|
||||
DiagramStructure --> Renderer["renderer: flowRenderer-v3-unified"]
|
||||
DiagramStructure --> Styles["styles: flowStyles"]
|
||||
DiagramStructure --> Init["init: (cnf: MermaidConfig)"]
|
||||
|
||||
%% Parser Phase
|
||||
Parser --> flowParser["parser/flowParser.ts<br/>newParser.parse(src)"]
|
||||
flowParser --> |"Preprocesses src"| RemoveWhitespace["Remove trailing whitespace<br/>src.replace(/}\s*\n/g, '}\n')"]
|
||||
RemoveWhitespace --> flowJison["parser/flow.jison<br/>flowJisonParser.parse(newSrc)"]
|
||||
|
||||
flowJison --> ParseGraph["Parse Graph Structure"]
|
||||
ParseGraph --> ParseVertices["Parse Vertices"]
|
||||
ParseGraph --> ParseEdges["Parse Edges"]
|
||||
ParseGraph --> ParseSubgraphs["Parse Subgraphs"]
|
||||
ParseGraph --> ParseClasses["Parse Classes"]
|
||||
ParseGraph --> ParseStyles["Parse Styles"]
|
||||
|
||||
%% Database Phase - FlowDB Class
|
||||
Database --> FlowDBClass["flowDb.ts<br/>FlowDB class"]
|
||||
|
||||
FlowDBClass --> DBInit["constructor()<br/>- Initialize counters<br/>- Bind methods<br/>- Setup toolTips<br/>- Call clear()"]
|
||||
|
||||
DBInit --> DBMethods{FlowDB Methods}
|
||||
|
||||
DBMethods --> addVertex["addVertex(id, textObj, type, style,<br/>classes, dir, props, metadata)"]
|
||||
DBMethods --> addLink["addLink(_start[], _end[], linkData)"]
|
||||
DBMethods --> addSingleLink["addSingleLink(_start, _end, type, id)"]
|
||||
DBMethods --> setDirection["setDirection(dir)"]
|
||||
DBMethods --> addSubGraph["addSubGraph(nodes[], id, title)"]
|
||||
DBMethods --> addClass["addClass(id, style)"]
|
||||
DBMethods --> setClass["setClass(ids, className)"]
|
||||
DBMethods --> setTooltip["setTooltip(ids, tooltip)"]
|
||||
DBMethods --> setClickEvent["setClickEvent(id, functionName, args)"]
|
||||
DBMethods --> setClickFun["setClickFun(id, functionName, args)"]
|
||||
|
||||
%% Vertex Processing
|
||||
addVertex --> VertexProcess{Vertex Processing}
|
||||
VertexProcess --> CreateVertex["Create FlowVertex object<br/>- id, labelType, domId<br/>- styles[], classes[]"]
|
||||
VertexProcess --> SanitizeText["sanitizeText(textObj.text)"]
|
||||
VertexProcess --> ParseMetadata["Parse YAML metadata<br/>yaml.load(yamlData)"]
|
||||
VertexProcess --> SetVertexProps["Set vertex properties<br/>- shape, label, icon, form<br/>- pos, img, constraint, w, h"]
|
||||
|
||||
%% Edge Processing
|
||||
addSingleLink --> EdgeProcess{Edge Processing}
|
||||
EdgeProcess --> CreateEdge["Create FlowEdge object<br/>- start, end, type, text<br/>- labelType, classes[]"]
|
||||
EdgeProcess --> ProcessLinkText["Process link text<br/>- sanitizeText()<br/>- strip quotes"]
|
||||
EdgeProcess --> SetEdgeProps["Set edge properties<br/>- type, stroke, length"]
|
||||
EdgeProcess --> GenerateEdgeId["Generate edge ID<br/>getEdgeId(start, end, counter)"]
|
||||
EdgeProcess --> ValidateEdgeLimit["Validate edge limit<br/>maxEdges check"]
|
||||
|
||||
%% Data Collection
|
||||
DBMethods --> GetData["getData()"]
|
||||
GetData --> CollectNodes["Collect nodes[] from vertices"]
|
||||
GetData --> CollectEdges["Collect edges[] from edges"]
|
||||
GetData --> ProcessSubGraphs["Process subgraphs<br/>- parentDB Map<br/>- subGraphDB Map"]
|
||||
GetData --> AddNodeFromVertex["addNodeFromVertex()<br/>for each vertex"]
|
||||
GetData --> ProcessEdgeTypes["destructEdgeType()<br/>arrowTypeStart, arrowTypeEnd"]
|
||||
|
||||
%% Node Creation
|
||||
AddNodeFromVertex --> NodeCreation{Node Creation}
|
||||
NodeCreation --> FindExistingNode["findNode(nodes, vertex.id)"]
|
||||
NodeCreation --> CreateBaseNode["Create base node<br/>- id, label, parentId<br/>- cssStyles, cssClasses<br/>- shape, domId, tooltip"]
|
||||
NodeCreation --> GetCompiledStyles["getCompiledStyles(classDefs)"]
|
||||
NodeCreation --> GetTypeFromVertex["getTypeFromVertex(vertex)"]
|
||||
|
||||
%% Rendering Phase
|
||||
Renderer --> flowRendererV3["flowRenderer-v3-unified.ts<br/>draw(text, id, version, diag)"]
|
||||
|
||||
flowRendererV3 --> RenderInit["Initialize rendering<br/>- getConfig()<br/>- handle securityLevel<br/>- getDiagramElement()"]
|
||||
|
||||
RenderInit --> GetLayoutData["diag.db.getData()<br/>as LayoutData"]
|
||||
GetLayoutData --> SetupLayoutData["Setup layout data<br/>- type, layoutAlgorithm<br/>- direction, spacing<br/>- markers, diagramId"]
|
||||
|
||||
SetupLayoutData --> CallRender["render(data4Layout, svg)"]
|
||||
CallRender --> SetupViewPort["setupViewPortForSVG(svg, padding)"]
|
||||
SetupViewPort --> ProcessLinks["Process vertex links<br/>- create anchor elements<br/>- handle click events"]
|
||||
|
||||
%% Shape Rendering
|
||||
CallRender --> ShapeSystem["flowChartShapes.js<br/>Shape Functions"]
|
||||
|
||||
ShapeSystem --> ShapeFunctions{Shape Functions}
|
||||
ShapeFunctions --> question["question(parent, bbox, node)"]
|
||||
ShapeFunctions --> hexagon["hexagon(parent, bbox, node)"]
|
||||
ShapeFunctions --> rect_left_inv_arrow["rect_left_inv_arrow(parent, bbox, node)"]
|
||||
ShapeFunctions --> lean_right["lean_right(parent, bbox, node)"]
|
||||
ShapeFunctions --> lean_left["lean_left(parent, bbox, node)"]
|
||||
|
||||
ShapeFunctions --> insertPolygonShape["insertPolygonShape(parent, w, h, points)"]
|
||||
ShapeFunctions --> intersectPolygon["intersectPolygon(node, points, point)"]
|
||||
ShapeFunctions --> intersectRect["intersectRect(node, point)"]
|
||||
|
||||
%% Styling System
|
||||
Styles --> stylesTS["styles.ts<br/>getStyles(options)"]
|
||||
stylesTS --> StyleOptions["FlowChartStyleOptions<br/>- arrowheadColor, border2<br/>- clusterBkg, mainBkg<br/>- fontFamily, textColor"]
|
||||
|
||||
StyleOptions --> GenerateCSS["Generate CSS styles<br/>- .label, .cluster-label<br/>- .node, .edgePath<br/>- .flowchart-link, .edgeLabel"]
|
||||
GenerateCSS --> GetIconStyles["getIconStyles()"]
|
||||
|
||||
%% Type System
|
||||
Parser --> TypeSystem["types.ts<br/>Type Definitions"]
|
||||
TypeSystem --> FlowVertex["FlowVertex interface"]
|
||||
TypeSystem --> FlowEdge["FlowEdge interface"]
|
||||
TypeSystem --> FlowClass["FlowClass interface"]
|
||||
TypeSystem --> FlowSubGraph["FlowSubGraph interface"]
|
||||
TypeSystem --> FlowVertexTypeParam["FlowVertexTypeParam<br/>Shape types"]
|
||||
|
||||
%% Utility Functions
|
||||
DBMethods --> UtilityFunctions{Utility Functions}
|
||||
UtilityFunctions --> lookUpDomId["lookUpDomId(id)"]
|
||||
UtilityFunctions --> getClasses["getClasses()"]
|
||||
UtilityFunctions --> getDirection["getDirection()"]
|
||||
UtilityFunctions --> getVertices["getVertices()"]
|
||||
UtilityFunctions --> getEdges["getEdges()"]
|
||||
UtilityFunctions --> getSubGraphs["getSubGraphs()"]
|
||||
UtilityFunctions --> clear["clear()"]
|
||||
UtilityFunctions --> defaultConfig["defaultConfig()"]
|
||||
|
||||
%% Event Handling
|
||||
ProcessLinks --> EventHandling{Event Handling}
|
||||
EventHandling --> setupToolTips["setupToolTips(element)"]
|
||||
EventHandling --> bindFunctions["bindFunctions(element)"]
|
||||
EventHandling --> runFunc["utils.runFunc(functionName, args)"]
|
||||
|
||||
%% Common Database Functions
|
||||
DBMethods --> CommonDB["commonDb.js functions"]
|
||||
CommonDB --> setAccTitle["setAccTitle()"]
|
||||
CommonDB --> getAccTitle["getAccTitle()"]
|
||||
CommonDB --> setAccDescription["setAccDescription()"]
|
||||
CommonDB --> getAccDescription["getAccDescription()"]
|
||||
CommonDB --> setDiagramTitle["setDiagramTitle()"]
|
||||
CommonDB --> getDiagramTitle["getDiagramTitle()"]
|
||||
CommonDB --> commonClear["clear()"]
|
||||
|
||||
%% Final Output
|
||||
ProcessLinks --> FinalSVG["Final SVG Output"]
|
||||
|
||||
%% Layout Algorithm Selection
|
||||
SetupLayoutData --> LayoutAlgorithm{Layout Algorithm}
|
||||
LayoutAlgorithm --> Dagre["dagre<br/>(default)"]
|
||||
LayoutAlgorithm --> DagreWrapper["dagre-wrapper<br/>(v2 renderer)"]
|
||||
LayoutAlgorithm --> ELK["elk<br/>(external package)"]
|
||||
|
||||
%% Testing Components
|
||||
FlowDBClass --> TestFiles["Test Files"]
|
||||
TestFiles --> flowDbSpec["flowDb.spec.ts"]
|
||||
TestFiles --> flowChartShapesSpec["flowChartShapes.spec.js"]
|
||||
TestFiles --> ParserTests["parser/*.spec.js files<br/>- flow-text.spec.js<br/>- flow-edges.spec.js<br/>- flow-style.spec.js<br/>- subgraph.spec.js"]
|
||||
|
||||
%% Configuration
|
||||
Init --> ConfigSetup["Configuration Setup"]
|
||||
ConfigSetup --> FlowchartConfig["cnf.flowchart config"]
|
||||
ConfigSetup --> ArrowMarkers["arrowMarkerAbsolute"]
|
||||
ConfigSetup --> LayoutConfig["layout config"]
|
||||
ConfigSetup --> SetConfig["setConfig() calls"]
|
307
docs/diagrams/mermaid-api-sequence.mmd
Normal file
307
docs/diagrams/mermaid-api-sequence.mmd
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/mermaidAPI.ts"
|
||||
- "File: /packages/mermaid/src/mermaid.ts"
|
||||
generationTime: 2025-01-28T16:30:00.000Z
|
||||
---
|
||||
sequenceDiagram
|
||||
participant User as User/Browser
|
||||
participant Mermaid as mermaid.ts
|
||||
participant Queue as executionQueue
|
||||
participant API as mermaidAPI.ts
|
||||
participant Config as configApi
|
||||
participant Preprocessor as preprocessDiagram
|
||||
participant DiagramAPI as diagram-api
|
||||
participant Diagram as Diagram.fromText
|
||||
participant Renderer as diagram.renderer
|
||||
participant Styles as Styling System
|
||||
participant DOM as DOM/SVG
|
||||
|
||||
Note over User, DOM: Mermaid Complete API Flow
|
||||
|
||||
%% Initialization Phase
|
||||
User->>+Mermaid: mermaid.initialize(config)
|
||||
Mermaid->>+API: mermaidAPI.initialize(config)
|
||||
|
||||
API->>API: assignWithDepth({}, userOptions)
|
||||
API->>API: handle legacy fontFamily config
|
||||
API->>Config: saveConfigFromInitialize(options)
|
||||
|
||||
alt Theme Configuration Available
|
||||
API->>API: check if theme in theme object
|
||||
API->>API: set themeVariables from theme
|
||||
else Default Theme
|
||||
API->>API: use default theme variables
|
||||
end
|
||||
|
||||
API->>Config: setSiteConfig(options) or getSiteConfig()
|
||||
API->>API: setLogLevel(config.logLevel)
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
|
||||
API-->>-Mermaid: initialization complete
|
||||
Mermaid-->>-User: ready to render
|
||||
|
||||
%% Content Loaded Event
|
||||
User->>DOM: document.load event
|
||||
DOM->>+Mermaid: contentLoaded()
|
||||
|
||||
opt startOnLoad is true
|
||||
Mermaid->>Config: getConfig()
|
||||
Config-->>Mermaid: { startOnLoad: true }
|
||||
Mermaid->>Mermaid: run()
|
||||
end
|
||||
|
||||
Mermaid-->>-DOM: event handling complete
|
||||
|
||||
%% Main Run Function
|
||||
User->>+Mermaid: mermaid.run(options)
|
||||
|
||||
Mermaid->>Mermaid: runThrowsErrors(options)
|
||||
Mermaid->>Config: getConfig()
|
||||
Config-->>Mermaid: configuration object
|
||||
|
||||
alt nodes provided
|
||||
Mermaid->>Mermaid: use provided nodes
|
||||
else querySelector provided
|
||||
Mermaid->>DOM: document.querySelectorAll(querySelector)
|
||||
DOM-->>Mermaid: nodesToProcess
|
||||
end
|
||||
|
||||
Mermaid->>Mermaid: new InitIDGenerator(deterministicIds, seed)
|
||||
|
||||
loop For each diagram element
|
||||
Mermaid->>DOM: check element.getAttribute('data-processed')
|
||||
|
||||
opt not processed
|
||||
Mermaid->>DOM: element.setAttribute('data-processed', 'true')
|
||||
Mermaid->>Mermaid: generate unique id
|
||||
Mermaid->>DOM: get element.innerHTML
|
||||
Mermaid->>Mermaid: entityDecode and clean text
|
||||
Mermaid->>Mermaid: detectInit(txt)
|
||||
|
||||
Mermaid->>Queue: render(id, txt, element)
|
||||
end
|
||||
end
|
||||
|
||||
Mermaid-->>-User: processing initiated
|
||||
|
||||
%% Render Function (Queued)
|
||||
activate Queue
|
||||
Queue->>+API: mermaidAPI.render(id, text, container)
|
||||
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
API->>+Preprocessor: processAndSetConfigs(text)
|
||||
|
||||
Preprocessor->>Preprocessor: preprocessDiagram(text)
|
||||
Preprocessor->>Config: reset()
|
||||
Preprocessor->>Config: addDirective(processed.config)
|
||||
Preprocessor-->>-API: { code, config }
|
||||
|
||||
API->>Config: getConfig()
|
||||
Config-->>API: current configuration
|
||||
|
||||
opt text length > maxTextSize
|
||||
API->>API: text = MAX_TEXTLENGTH_EXCEEDED_MSG
|
||||
end
|
||||
|
||||
API->>API: setup id selectors and element IDs
|
||||
API->>API: determine security level (sandbox/loose)
|
||||
|
||||
%% DOM Setup
|
||||
alt svgContainingElement provided
|
||||
alt isSandboxed
|
||||
API->>DOM: sandboxedIframe(select(svgContainingElement), iFrameID)
|
||||
API->>DOM: select iframe contentDocument body
|
||||
else
|
||||
API->>DOM: select(svgContainingElement)
|
||||
end
|
||||
else no container
|
||||
API->>API: removeExistingElements(document, id, divId, iFrameId)
|
||||
alt isSandboxed
|
||||
API->>DOM: sandboxedIframe(select('body'), iFrameID)
|
||||
else
|
||||
API->>DOM: select('body')
|
||||
end
|
||||
end
|
||||
|
||||
API->>DOM: appendDivSvgG(root, id, enclosingDivID, fontFamily, XMLNS_XLINK_STD)
|
||||
|
||||
%% Diagram Creation
|
||||
API->>+Diagram: Diagram.fromText(text, { title: processed.title })
|
||||
|
||||
Diagram->>DiagramAPI: detect diagram type
|
||||
Diagram->>DiagramAPI: load appropriate diagram
|
||||
Diagram-->>-API: diagram instance
|
||||
|
||||
opt parsing error occurred
|
||||
API->>+Diagram: Diagram.fromText('error')
|
||||
Diagram-->>-API: error diagram
|
||||
API->>API: store parseEncounteredException
|
||||
end
|
||||
|
||||
%% Style Generation
|
||||
API->>DOM: get svg element and firstChild
|
||||
API->>Renderer: diag.renderer.getClasses(text, diag)
|
||||
Renderer-->>API: diagramClassDefs
|
||||
|
||||
API->>+Styles: createUserStyles(config, diagramType, diagramClassDefs, idSelector)
|
||||
|
||||
Styles->>Styles: createCssStyles(config, classDefs)
|
||||
|
||||
opt config.themeCSS defined
|
||||
Styles->>Styles: append themeCSS
|
||||
end
|
||||
|
||||
opt fontFamily configured
|
||||
Styles->>Styles: add CSS variables for fonts
|
||||
end
|
||||
|
||||
opt classDefs exist
|
||||
loop For each styleClassDef
|
||||
opt has styles
|
||||
Styles->>Styles: cssImportantStyles for each CSS element
|
||||
end
|
||||
opt has textStyles
|
||||
Styles->>Styles: cssImportantStyles for tspan elements
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Styles->>Styles: getStyles(graphType, userCSSstyles, themeVariables)
|
||||
Styles->>Styles: serialize(compile(svgId{allStyles}), stringify)
|
||||
Styles-->>-API: compiled CSS rules
|
||||
|
||||
API->>DOM: create style element
|
||||
API->>DOM: style.innerHTML = rules
|
||||
API->>DOM: svg.insertBefore(style, firstChild)
|
||||
|
||||
%% Diagram Rendering
|
||||
API->>+Renderer: diag.renderer.draw(text, id, version, diag)
|
||||
|
||||
Renderer->>Renderer: diagram-specific rendering logic
|
||||
Renderer->>DOM: create SVG elements
|
||||
Renderer->>DOM: apply positioning and styling
|
||||
Renderer-->>-API: rendered diagram
|
||||
|
||||
opt rendering error
|
||||
alt suppressErrorRendering
|
||||
API->>API: removeTempElements()
|
||||
API->>Mermaid: throw error
|
||||
else
|
||||
API->>Renderer: errorRenderer.draw(text, id, version)
|
||||
end
|
||||
end
|
||||
|
||||
%% Accessibility and Cleanup
|
||||
API->>DOM: select svg element
|
||||
API->>Diagram: diag.db.getAccTitle()
|
||||
API->>Diagram: diag.db.getAccDescription()
|
||||
API->>API: addA11yInfo(diagramType, svgNode, a11yTitle, a11yDescr)
|
||||
|
||||
API->>DOM: set xmlns for foreignobject elements
|
||||
API->>DOM: get innerHTML from enclosing div
|
||||
|
||||
API->>+API: cleanUpSvgCode(svgCode, isSandboxed, arrowMarkerAbsolute)
|
||||
|
||||
opt not useArrowMarkerUrls and not sandboxed
|
||||
API->>API: replace marker-end URLs with anchors
|
||||
end
|
||||
|
||||
API->>API: decodeEntities(svgCode)
|
||||
API->>API: replace <br> with <br/>
|
||||
API-->>-API: cleaned SVG code
|
||||
|
||||
alt isSandboxed
|
||||
API->>+API: putIntoIFrame(svgCode, svgEl)
|
||||
API->>API: calculate iframe height
|
||||
API->>API: toBase64 encode SVG content
|
||||
API->>API: create iframe with sandbox attributes
|
||||
API-->>-API: iframe HTML
|
||||
else not loose security
|
||||
API->>API: DOMPurify.sanitize(svgCode, options)
|
||||
end
|
||||
|
||||
API->>API: attachFunctions()
|
||||
API->>API: removeTempElements()
|
||||
|
||||
opt parseEncounteredException
|
||||
API->>Mermaid: throw parseEncounteredException
|
||||
end
|
||||
|
||||
API-->>-Queue: { diagramType, svg: svgCode, bindFunctions }
|
||||
|
||||
%% Return to Web Integration
|
||||
activate Mermaid
|
||||
Queue-->>Mermaid: render result
|
||||
Mermaid->>DOM: element.innerHTML = svg
|
||||
|
||||
opt postRenderCallback
|
||||
Mermaid->>User: postRenderCallback(id)
|
||||
end
|
||||
|
||||
opt bindFunctions exist
|
||||
Mermaid->>DOM: bindFunctions(element)
|
||||
end
|
||||
deactivate Mermaid
|
||||
|
||||
%% Parse Function Flow
|
||||
User->>+Mermaid: mermaid.parse(text, parseOptions)
|
||||
activate Queue
|
||||
|
||||
Queue->>+API: mermaidAPI.parse(text, parseOptions)
|
||||
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
API->>Preprocessor: processAndSetConfigs(text)
|
||||
Preprocessor-->>API: { code, config }
|
||||
API->>Diagram: getDiagramFromText(code)
|
||||
Diagram-->>API: diagram instance
|
||||
API-->>-Queue: { diagramType: diagram.type, config }
|
||||
|
||||
Queue-->>-Mermaid: parse result
|
||||
Mermaid-->>-User: ParseResult or false
|
||||
|
||||
%% External Diagram Registration
|
||||
User->>+Mermaid: registerExternalDiagrams(diagrams, options)
|
||||
|
||||
Mermaid->>DiagramAPI: addDiagrams()
|
||||
Mermaid->>DiagramAPI: registerLazyLoadedDiagrams(...diagrams)
|
||||
|
||||
opt lazyLoad is false
|
||||
Mermaid->>DiagramAPI: loadRegisteredDiagrams()
|
||||
end
|
||||
|
||||
Mermaid-->>-User: registration complete
|
||||
|
||||
%% Error Handling
|
||||
Note over Mermaid, API: Error Handling Throughout
|
||||
alt Error occurs
|
||||
API->>Mermaid: throw error
|
||||
Mermaid->>+Mermaid: handleError(error, errors, parseError)
|
||||
|
||||
Mermaid->>Mermaid: log.warn(error)
|
||||
|
||||
alt isDetailedError
|
||||
Mermaid->>User: parseError(error.str, error.hash)
|
||||
else
|
||||
Mermaid->>User: parseError(error)
|
||||
end
|
||||
|
||||
opt not suppressErrors
|
||||
Mermaid->>User: throw error
|
||||
end
|
||||
|
||||
Mermaid-->>-User: error handled
|
||||
end
|
||||
|
||||
%% Configuration Details
|
||||
Note over Config: Configuration Functions
|
||||
Note right of Config: Functions:<br/>- reset()<br/>- getConfig()<br/>- setConfig()<br/>- getSiteConfig()<br/>- updateSiteConfig()<br/>- saveConfigFromInitialize()
|
||||
|
||||
Note over Styles: CSS Generation
|
||||
Note right of Styles: Features:<br/>- createCssStyles()<br/>- createUserStyles()<br/>- cssImportantStyles()<br/>- Theme integration<br/>- Class definitions
|
||||
|
||||
Note over API: Security Levels
|
||||
Note right of API: Modes:<br/>- sandbox: iframe isolation<br/>- loose: minimal sanitization<br/>- default: DOMPurify sanitization
|
||||
|
||||
Note over API: Helper Functions
|
||||
Note right of API: Utilities:<br/>- cleanUpSvgCode()<br/>- putIntoIFrame()<br/>- appendDivSvgG()<br/>- removeExistingElements()
|
180
docs/diagrams/mindmap-implementation-sequence.mmd
Normal file
180
docs/diagrams/mindmap-implementation-sequence.mmd
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapDb.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/detector.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapTypes.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/styles.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/svgDraw.ts"
|
||||
generationTime: 2025-01-28T16:00:00.000Z
|
||||
---
|
||||
sequenceDiagram
|
||||
participant User as User Input Text
|
||||
participant Detector as detector.ts
|
||||
participant Loader as DiagramLoader
|
||||
participant Definition as mindmap-definition.ts
|
||||
participant Parser as parser/mindmap.jison
|
||||
participant DB as MindmapDB
|
||||
participant Renderer as mindmapRenderer.ts
|
||||
participant Cytoscape as cytoscape.js
|
||||
participant SVGDraw as svgDraw.ts
|
||||
participant Styles as styles.ts
|
||||
participant Output as Final SVG
|
||||
|
||||
Note over User, Output: Mindmap Implementation Flow
|
||||
|
||||
%% Detection Phase
|
||||
User->>Detector: /^\s*mindmap/ text input
|
||||
activate Detector
|
||||
Detector->>Detector: detector(txt) validates pattern
|
||||
Detector->>Loader: loader() function called
|
||||
deactivate Detector
|
||||
|
||||
activate Loader
|
||||
Loader->>Definition: import mindmap-definition.js
|
||||
deactivate Loader
|
||||
|
||||
%% Core Structure Setup
|
||||
activate Definition
|
||||
Definition->>DB: get db() → new MindmapDB()
|
||||
Definition->>Renderer: setup renderer
|
||||
Definition->>Parser: setup parser
|
||||
Definition->>Styles: setup styles
|
||||
deactivate Definition
|
||||
|
||||
%% Database Initialization
|
||||
activate DB
|
||||
Note over DB: MindmapDB Constructor
|
||||
DB->>DB: initialize nodes array
|
||||
DB->>DB: setup nodeType constants
|
||||
DB->>DB: bind methods
|
||||
DB->>DB: clear() state
|
||||
|
||||
%% Parsing Phase
|
||||
activate Parser
|
||||
User->>Parser: mindmap syntax text
|
||||
|
||||
loop For each node in hierarchy
|
||||
Parser->>DB: addNode(level, id, descr, type)
|
||||
activate DB
|
||||
DB->>DB: sanitizeText(id, descr)
|
||||
DB->>DB: getType(startStr, endStr)
|
||||
Note right of DB: Shape Detection:<br/>[ → RECT<br/>( → ROUNDED_RECT<br/>(( → CIRCLE<br/>)) → BANG<br/>{{ → HEXAGON
|
||||
DB->>DB: getParent(level)
|
||||
DB->>DB: create MindmapNode
|
||||
DB->>DB: add to hierarchy
|
||||
deactivate DB
|
||||
end
|
||||
|
||||
opt Icon/Class Decoration
|
||||
Parser->>DB: decorateNode(decoration)
|
||||
DB->>DB: set icon/class properties
|
||||
end
|
||||
deactivate Parser
|
||||
|
||||
%% Data Preparation
|
||||
Renderer->>DB: getData() for layout
|
||||
activate DB
|
||||
DB->>DB: collect all nodes
|
||||
DB->>DB: build parent-child relationships
|
||||
DB-->>Renderer: return node hierarchy
|
||||
deactivate DB
|
||||
|
||||
%% Rendering Pipeline
|
||||
activate Renderer
|
||||
Note over Renderer: Rendering Phase
|
||||
|
||||
Renderer->>Cytoscape: initialize cytoscape
|
||||
activate Cytoscape
|
||||
|
||||
loop For each node in mindmap
|
||||
Renderer->>Cytoscape: addNodes(mindmap, cy, conf, level)
|
||||
Cytoscape->>Cytoscape: create node data
|
||||
Cytoscape->>Cytoscape: set position (x, y)
|
||||
end
|
||||
|
||||
loop For parent-child relationships
|
||||
Renderer->>Cytoscape: add edges
|
||||
Cytoscape->>Cytoscape: create edge data
|
||||
end
|
||||
|
||||
Renderer->>Cytoscape: configure cose-bilkent layout
|
||||
Cytoscape->>Cytoscape: calculate optimal positions
|
||||
Cytoscape-->>Renderer: return positioned graph
|
||||
deactivate Cytoscape
|
||||
|
||||
%% SVG Generation
|
||||
Renderer->>SVGDraw: drawNodes(db, svg, mindmap, section, conf)
|
||||
activate SVGDraw
|
||||
|
||||
loop For each node recursively
|
||||
SVGDraw->>SVGDraw: select shape function
|
||||
|
||||
alt Default Shape
|
||||
SVGDraw->>SVGDraw: defaultBkg() - rounded rectangle
|
||||
else Rectangle Shape
|
||||
SVGDraw->>SVGDraw: rectBkg() - sharp corners
|
||||
else Circle Shape
|
||||
SVGDraw->>SVGDraw: circleBkg() - perfect circle
|
||||
else Cloud Shape
|
||||
SVGDraw->>SVGDraw: cloudBkg() - organic curves
|
||||
else Bang Shape
|
||||
SVGDraw->>SVGDraw: bangBkg() - explosion style
|
||||
else Hexagon Shape
|
||||
SVGDraw->>SVGDraw: hexagonBkg() - six sides
|
||||
end
|
||||
|
||||
SVGDraw->>SVGDraw: create SVG elements
|
||||
SVGDraw->>SVGDraw: add text labels
|
||||
SVGDraw->>SVGDraw: position node
|
||||
|
||||
opt Node has children
|
||||
SVGDraw->>SVGDraw: drawNodes() recursive call
|
||||
end
|
||||
end
|
||||
deactivate SVGDraw
|
||||
|
||||
%% Edge Rendering
|
||||
Renderer->>Renderer: drawEdges(edgesEl, cy)
|
||||
loop For each edge
|
||||
Renderer->>Renderer: extract edge bounds
|
||||
Renderer->>Renderer: draw SVG path
|
||||
end
|
||||
|
||||
%% Styling Application
|
||||
Renderer->>Styles: getStyles(options)
|
||||
activate Styles
|
||||
|
||||
Styles->>Styles: genSections(options)
|
||||
loop For THEME_COLOR_LIMIT sections
|
||||
Styles->>Styles: generate color scale
|
||||
Styles->>Styles: create CSS rules
|
||||
Note right of Styles: .section-X fills<br/>.edge-depth-X widths<br/>.node-icon-X colors
|
||||
end
|
||||
|
||||
Styles->>Styles: apply theme integration
|
||||
Styles-->>Renderer: return compiled CSS
|
||||
deactivate Styles
|
||||
|
||||
%% Final Assembly
|
||||
Renderer->>Output: selectSvgElement()
|
||||
Renderer->>Output: setupGraphViewbox()
|
||||
Renderer->>Output: apply styles
|
||||
Renderer->>Output: add interactive elements
|
||||
deactivate Renderer
|
||||
|
||||
activate Output
|
||||
Note over Output: Final Mindmap Features
|
||||
Output->>Output: responsive layout
|
||||
Output->>Output: accessibility attributes
|
||||
Output->>Output: hover effects
|
||||
Output->>Output: click handling
|
||||
Output-->>User: rendered mindmap
|
||||
deactivate Output
|
||||
|
||||
%% Configuration Details
|
||||
Note over DB, Styles: Configuration Options
|
||||
Note right of DB: Padding Calculations:<br/>Base padding from config<br/>RECT: ×2 padding<br/>ROUNDED_RECT: ×2 padding<br/>HEXAGON: ×2 padding
|
||||
Note right of Styles: Section Management:<br/>MAX_SECTIONS = 12<br/>Dynamic color generation<br/>Git theme integration
|
||||
Note right of Renderer: Layout Parameters:<br/>Cytoscape configuration<br/>coseBilkent settings<br/>Node spacing rules
|
@@ -84,6 +84,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
LLM integrations to create mermaid diagrams using AI from text descriptions.
|
||||
|
||||
- [HueHive - Create mermaid diagrams with text](https://huehive.co/tools/diagrams)
|
||||
- [MCP Server Mermaid](https://github.com/hustcc/mcp-mermaid) - Generate mermaid diagram and chart with AI MCP dynamically.
|
||||
|
||||
### CRM/ERP
|
||||
|
||||
@@ -103,6 +104,7 @@ Blogging frameworks and platforms
|
||||
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
||||
- [WordPress](https://wordpress.org)
|
||||
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
||||
- [WP Documentation](https://wordpress.org/themes/wp-documentation/)
|
||||
|
||||
### CMS/ECM
|
||||
|
||||
|
@@ -16,9 +16,7 @@ Applications that support Mermaid files [SHOULD](https://datatracker.ietf.org/do
|
||||
|
||||
### MIME Type
|
||||
|
||||
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is `text/vnd.mermaid`.
|
||||
|
||||
Currently pending [IANA](https://www.iana.org/) recognition.
|
||||
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is [`text/vnd.mermaid`](https://www.iana.org/assignments/media-types/application/vnd.mermaid).
|
||||
|
||||
## Showcase
|
||||
|
||||
|
@@ -1795,15 +1795,54 @@ It is possible to style the type of curve used for lines between items, if the d
|
||||
Available curve styles include `basis`, `bumpX`, `bumpY`, `cardinal`, `catmullRom`, `linear`, `monotoneX`, `monotoneY`,
|
||||
`natural`, `step`, `stepAfter`, and `stepBefore`.
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
|
||||
Line styling can be achieved in two ways:
|
||||
|
||||
1. Change the curve style of all the lines
|
||||
2. Change the curve style of a particular line
|
||||
|
||||
#### Diagram level curve style
|
||||
|
||||
In this example, a left-to-right graph uses the `stepBefore` curve style:
|
||||
|
||||
```
|
||||
%%{ init: { 'flowchart': { 'curve': 'stepBefore' } } }%%
|
||||
---
|
||||
config:
|
||||
flowchart:
|
||||
curve: stepBefore
|
||||
---
|
||||
graph LR
|
||||
```
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
#### Edge level curve style using Edge IDs (v\<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
You can assign IDs to [edges](#attaching-an-id-to-edges). After assigning an ID you can modify the line style by modifying the edge's `curve` property using the following syntax:
|
||||
|
||||
```mermaid-example
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
A e2@--> C
|
||||
e1@{ curve: linear }
|
||||
e2@{ curve: natural }
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
A e2@--> C
|
||||
e1@{ curve: linear }
|
||||
e2@{ curve: natural }
|
||||
```
|
||||
|
||||
```info
|
||||
Any edge curve style modified at the edge level overrides the diagram level style.
|
||||
```
|
||||
|
||||
```info
|
||||
If the same edge is modified multiple times the last modification will be rendered.
|
||||
```
|
||||
|
||||
### Styling a node
|
||||
|
||||
|
21
package.json
21
package.json
@@ -67,7 +67,7 @@
|
||||
"@argos-ci/cypress": "^5.0.2",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@cspell/eslint-plugin": "^8.19.3",
|
||||
"@cspell/eslint-plugin": "^8.19.4",
|
||||
"@cypress/code-coverage": "^3.12.49",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
@@ -94,17 +94,17 @@
|
||||
"cypress-split": "^1.24.14",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-cypress": "^4.3.0",
|
||||
"eslint-plugin-html": "^8.1.2",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"eslint-plugin-jsdoc": "^50.6.9",
|
||||
"eslint-plugin-html": "^8.1.3",
|
||||
"eslint-plugin-jest": "^28.14.0",
|
||||
"eslint-plugin-jsdoc": "^50.8.0",
|
||||
"eslint-plugin-json": "^4.0.1",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"eslint-plugin-markdown": "^5.1.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-unicorn": "^59.0.0",
|
||||
"eslint-plugin-unicorn": "^59.0.1",
|
||||
"express": "^5.1.0",
|
||||
"globals": "^16.0.0",
|
||||
"globby": "^14.0.2",
|
||||
@@ -112,7 +112,7 @@
|
||||
"jest": "^30.0.4",
|
||||
"jison": "^0.4.18",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"langium-cli": "3.3.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"markdown-table": "^3.0.4",
|
||||
@@ -126,7 +126,7 @@
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.7.3",
|
||||
"typescript": "~5.7.3",
|
||||
"typescript-eslint": "^8.32.0",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.0.3",
|
||||
"vite-plugin-istanbul": "^7.0.0",
|
||||
"vitest": "^3.0.6"
|
||||
@@ -139,8 +139,13 @@
|
||||
"roughjs": "patches/roughjs.patch"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"canvas",
|
||||
"cypress",
|
||||
"esbuild"
|
||||
],
|
||||
"ignoredBuiltDependencies": [
|
||||
"sharp",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
14
packages/examples/CHANGELOG.md
Normal file
14
packages/examples/CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# @mermaid-js/examples
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#6453](https://github.com/mermaid-js/mermaid/pull/6453) [`4936ef5`](https://github.com/mermaid-js/mermaid/commit/4936ef5c306d2f892cca9a95a5deac4af6d4882b) Thanks [@sidharthv96](https://github.com/sidharthv96)! - feat: Add examples for diagrams in the `@mermaid-js/examples` package
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6510](https://github.com/mermaid-js/mermaid/pull/6510) [`7a38eb7`](https://github.com/mermaid-js/mermaid/commit/7a38eb715d795cd5c66cb59357d64ec197b432e6) Thanks [@sidharthv96](https://github.com/sidharthv96)! - chore: Move packet diagram out of beta
|
||||
|
||||
- Updated dependencies [[`5acbd7e`](https://github.com/mermaid-js/mermaid/commit/5acbd7e762469d9d89a9c77faf6617ee13367f3a), [`d90634b`](https://github.com/mermaid-js/mermaid/commit/d90634bf2b09e586b055729e07e9a1a31b21827c), [`7a38eb7`](https://github.com/mermaid-js/mermaid/commit/7a38eb715d795cd5c66cb59357d64ec197b432e6), [`3e3ae08`](https://github.com/mermaid-js/mermaid/commit/3e3ae089305e1c7b9948b9e149eba6854fe7f2d6), [`d3e2be3`](https://github.com/mermaid-js/mermaid/commit/d3e2be35be066adeb7fd502b4a24c223c3b53947), [`637680d`](https://github.com/mermaid-js/mermaid/commit/637680d4d9e39b4f8cb6f05b4cb261e8f5693ac3)]:
|
||||
- mermaid@11.9.0
|
41
packages/examples/README.md
Normal file
41
packages/examples/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @mermaid-js/examples
|
||||
|
||||
The `@mermaid-js/examples` package contains a collection of examples used by tools like [mermaid.live](https://mermaid.live) to help users get started with new diagrams.
|
||||
|
||||
You can duplicate an existing diagram example file, e.g., `packages/examples/src/examples/flowchart.ts`, and modify it with details specific to your diagram.
|
||||
|
||||
Then, import the example in the `packages/examples/src/index.ts` file and add it to the `examples` array.
|
||||
|
||||
Each diagram should have at least one example, which should be marked as the default. It's a good idea to add more examples to showcase different features of the diagram.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
pnpm add @mermaid-js/examples
|
||||
```
|
||||
|
||||
A sample usage of the package in mermaid.live, to get the default example for every diagram type:
|
||||
|
||||
```ts
|
||||
import { diagramData } from '@mermaid-js/examples';
|
||||
|
||||
type DiagramDefinition = (typeof diagramData)[number];
|
||||
|
||||
const isValidDiagram = (diagram: DiagramDefinition): diagram is Required<DiagramDefinition> => {
|
||||
return Boolean(diagram.name && diagram.examples && diagram.examples.length > 0);
|
||||
};
|
||||
|
||||
export const getSampleDiagrams = () => {
|
||||
const diagrams = diagramData
|
||||
.filter((d) => isValidDiagram(d))
|
||||
.map(({ examples, ...rest }) => ({
|
||||
...rest,
|
||||
example: examples?.filter(({ isDefault }) => isDefault)[0],
|
||||
}));
|
||||
const examples: Record<string, string> = {};
|
||||
for (const diagram of diagrams) {
|
||||
examples[diagram.name.replace(/ (Diagram|Chart|Graph)/, '')] = diagram.example.code;
|
||||
}
|
||||
return examples;
|
||||
};
|
||||
```
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mermaid-js/examples",
|
||||
"version": "0.0.1-beta.1",
|
||||
"version": "1.0.0",
|
||||
"description": "Mermaid examples package",
|
||||
"author": "Sidharth Vinod",
|
||||
"type": "module",
|
||||
@@ -16,6 +16,10 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
|
@@ -1 +0,0 @@
|
||||
../mermaid/src/docs/syntax/zenuml.md
|
384
packages/mermaid-zenuml/README.md
Normal file
384
packages/mermaid-zenuml/README.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# @mermaid-js/mermaid-zenuml
|
||||
|
||||
MermaidJS plugin for ZenUML integration - A powerful sequence diagram rendering engine.
|
||||
|
||||
> A Sequence diagram is an interaction diagram that shows how processes operate with one another and in what order.
|
||||
|
||||
Mermaid can render sequence diagrams with [ZenUML](https://zenuml.com). Note that ZenUML uses a different
|
||||
syntax than the original Sequence Diagram in mermaid.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
BookLibService.Borrow(id) {
|
||||
User = Session.GetUser()
|
||||
if(User.isActive) {
|
||||
try {
|
||||
BookRepository.Update(id, onLoan, User)
|
||||
receipt = new Receipt(id, dueDate)
|
||||
} catch (BookNotFoundException) {
|
||||
ErrorService.onException(BookNotFoundException)
|
||||
} finally {
|
||||
Connection.close()
|
||||
}
|
||||
}
|
||||
return receipt
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### With bundlers
|
||||
|
||||
```sh
|
||||
npm install @mermaid-js/mermaid-zenuml
|
||||
```
|
||||
|
||||
```ts
|
||||
import mermaid from 'mermaid';
|
||||
import zenuml from '@mermaid-js/mermaid-zenuml';
|
||||
|
||||
await mermaid.registerExternalDiagrams([zenuml]);
|
||||
```
|
||||
|
||||
### With CDN
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||
import zenuml from 'https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-zenuml@0.2.0/dist/mermaid-zenuml.core.mjs';
|
||||
await mermaid.registerExternalDiagrams([zenuml]);
|
||||
</script>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> ZenUML uses experimental lazy loading & async rendering features which could change in the future.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Once the plugin is registered, you can create ZenUML diagrams using the `zenuml` syntax:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
Controller.Get(id) {
|
||||
Service.Get(id) {
|
||||
item = Repository.Get(id)
|
||||
if(item) {
|
||||
return item
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
## ZenUML Syntax Reference
|
||||
|
||||
### Participants
|
||||
|
||||
The participants can be defined implicitly as in the first example on this page. The participants or actors are
|
||||
rendered in order of appearance in the diagram source text. Sometimes you might want to show the participants in a
|
||||
different order than how they appear in the first message. It is possible to specify the actor's order of
|
||||
appearance by doing the following:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Declare participant (optional)
|
||||
Bob
|
||||
Alice
|
||||
Alice->Bob: Hi Bob
|
||||
Bob->Alice: Hi Alice
|
||||
```
|
||||
|
||||
### Annotators
|
||||
|
||||
If you specifically want to use symbols instead of just rectangles with text you can do so by using the annotator syntax to declare participants as per below.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Annotators
|
||||
@Actor Alice
|
||||
@Database Bob
|
||||
Alice->Bob: Hi Bob
|
||||
Bob->Alice: Hi Alice
|
||||
```
|
||||
|
||||
Available annotators include:
|
||||
|
||||
- `@Actor` - Human figure
|
||||
- `@Database` - Database symbol
|
||||
- `@Boundary` - Boundary symbol
|
||||
- `@Control` - Control symbol
|
||||
- `@Entity` - Entity symbol
|
||||
- `@Queue` - Queue symbol
|
||||
|
||||
### Aliases
|
||||
|
||||
The participants can have a convenient identifier and a descriptive label.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Aliases
|
||||
A as Alice
|
||||
J as John
|
||||
A->J: Hello John, how are you?
|
||||
J->A: Great!
|
||||
```
|
||||
|
||||
## Messages
|
||||
|
||||
Messages can be one of:
|
||||
|
||||
1. Sync message
|
||||
2. Async message
|
||||
3. Creation message
|
||||
4. Reply message
|
||||
|
||||
### Sync message
|
||||
|
||||
You can think of a sync (blocking) method in a programming language.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Sync message
|
||||
A.SyncMessage
|
||||
A.SyncMessage(with, parameters) {
|
||||
B.nestedSyncMessage()
|
||||
}
|
||||
```
|
||||
|
||||
### Async message
|
||||
|
||||
You can think of an async (non-blocking) method in a programming language. Fire an event and forget about it.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Async message
|
||||
Alice->Bob: How are you?
|
||||
```
|
||||
|
||||
### Creation message
|
||||
|
||||
We use `new` keyword to create an object.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
new A1
|
||||
new A2(with, parameters)
|
||||
```
|
||||
|
||||
### Reply message
|
||||
|
||||
There are three ways to express a reply message:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
// 1. assign a variable from a sync message.
|
||||
a = A.SyncMessage()
|
||||
|
||||
// 1.1. optionally give the variable a type
|
||||
SomeType a = A.SyncMessage()
|
||||
|
||||
// 2. use return keyword
|
||||
A.SyncMessage() {
|
||||
return result
|
||||
}
|
||||
|
||||
// 3. use @return or @reply annotator on an async message
|
||||
@return
|
||||
A->B: result
|
||||
```
|
||||
|
||||
The third way `@return` is rarely used, but it is useful when you want to return to one level up.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Reply message
|
||||
Client->A.method() {
|
||||
B.method() {
|
||||
if(condition) {
|
||||
return x1
|
||||
// return early
|
||||
@return
|
||||
A->Client: x11
|
||||
}
|
||||
}
|
||||
return x2
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Nesting
|
||||
|
||||
Sync messages and Creation messages are naturally nestable with `{}`.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
A.method() {
|
||||
B.nested_sync_method()
|
||||
B->C: nested async message
|
||||
}
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
It is possible to add comments to a sequence diagram with `// comment` syntax.
|
||||
Comments will be rendered above the messages or fragments. Comments on other places
|
||||
are ignored. Markdown is supported.
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
// a comment on a participant will not be rendered
|
||||
BookService
|
||||
// a comment on a message.
|
||||
// **Markdown** is supported.
|
||||
BookService.getBook()
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
It is possible to express loops in a ZenUML diagram. This is done by any of the
|
||||
following notations:
|
||||
|
||||
1. while
|
||||
2. for
|
||||
3. forEach, foreach
|
||||
4. loop
|
||||
|
||||
```zenuml
|
||||
while(condition) {
|
||||
...statements...
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
Alice->John: Hello John, how are you?
|
||||
while(true) {
|
||||
John->Alice: Great!
|
||||
}
|
||||
```
|
||||
|
||||
### Alt (Alternative paths)
|
||||
|
||||
It is possible to express alternative paths in a sequence diagram. This is done by the notation
|
||||
|
||||
```zenuml
|
||||
if(condition1) {
|
||||
...statements...
|
||||
} else if(condition2) {
|
||||
...statements...
|
||||
} else {
|
||||
...statements...
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
Alice->Bob: Hello Bob, how are you?
|
||||
if(is_sick) {
|
||||
Bob->Alice: Not so good :(
|
||||
} else {
|
||||
Bob->Alice: Feeling fresh like a daisy
|
||||
}
|
||||
```
|
||||
|
||||
### Opt (Optional)
|
||||
|
||||
It is possible to render an `opt` fragment. This is done by the notation
|
||||
|
||||
```zenuml
|
||||
opt {
|
||||
...statements...
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
Alice->Bob: Hello Bob, how are you?
|
||||
Bob->Alice: Not so good :(
|
||||
opt {
|
||||
Bob->Alice: Thanks for asking
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel
|
||||
|
||||
It is possible to show actions that are happening in parallel.
|
||||
|
||||
This is done by the notation
|
||||
|
||||
```zenuml
|
||||
par {
|
||||
statement1
|
||||
statement2
|
||||
statement3
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
par {
|
||||
Alice->Bob: Hello guys!
|
||||
Alice->John: Hello guys!
|
||||
}
|
||||
```
|
||||
|
||||
### Try/Catch/Finally (Break)
|
||||
|
||||
It is possible to indicate a stop of the sequence within the flow (usually used to model exceptions).
|
||||
|
||||
This is done by the notation
|
||||
|
||||
```
|
||||
try {
|
||||
...statements...
|
||||
} catch {
|
||||
...statements...
|
||||
} finally {
|
||||
...statements...
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
try {
|
||||
Consumer->API: Book something
|
||||
API->BookingService: Start booking process
|
||||
} catch {
|
||||
API->Consumer: show failure
|
||||
} finally {
|
||||
API->BookingService: rollback status
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
This package is part of the [Mermaid](https://github.com/mermaid-js/mermaid) project. See the main repository for contributing guidelines.
|
||||
|
||||
## Contributors
|
||||
|
||||
- [Peng Xiao](https://github.com/MrCoder)
|
||||
- [Sidharth Vinod](https://sidharth.dev)
|
||||
- [Dong Cai](https://github.com/dontry)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Links
|
||||
|
||||
- [ZenUML Official Website](https://zenuml.com)
|
||||
- [Mermaid Documentation](https://mermaid.js.org)
|
||||
- [GitHub Repository](https://github.com/mermaid-js/mermaid)
|
@@ -33,7 +33,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zenuml/core": "^3.31.1"
|
||||
"@zenuml/core": "^3.35.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
|
11
packages/mermaid-zenuml/src/zenuml.d.ts
vendored
Normal file
11
packages/mermaid-zenuml/src/zenuml.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
declare module '@zenuml/core' {
|
||||
interface RenderOptions {
|
||||
theme?: string;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export default class ZenUml {
|
||||
constructor(container: Element);
|
||||
render(text: string, options?: RenderOptions): Promise<void>;
|
||||
}
|
||||
}
|
@@ -53,7 +53,6 @@ export const draw = async function (text: string, id: string) {
|
||||
|
||||
const { foreignObject, container, app } = createForeignObject(id);
|
||||
svgContainer.appendChild(foreignObject);
|
||||
// @ts-expect-error @zenuml/core@3.0.0 exports the wrong type for ZenUml
|
||||
const zenuml = new ZenUml(app);
|
||||
// default is a theme name. More themes to be added and will be configurable in the future
|
||||
await zenuml.render(text, { theme: 'default', mode: 'static' });
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mermaid",
|
||||
"version": "11.8.1",
|
||||
"version": "11.9.0",
|
||||
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
|
||||
"type": "module",
|
||||
"module": "./dist/mermaid.core.mjs",
|
||||
@@ -79,7 +79,7 @@
|
||||
"dagre-d3-es": "7.0.11",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.9",
|
||||
"katex": "^0.16.22",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^16.0.0",
|
||||
@@ -105,13 +105,14 @@
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"ajv": "^8.17.1",
|
||||
"chokidar": "^3.6.0",
|
||||
"canvas": "^3.1.0",
|
||||
"chokidar": "3.6.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"csstree-validator": "^4.0.1",
|
||||
"globby": "^14.0.2",
|
||||
"jison": "^0.4.18",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"json-schema-to-typescript": "^15.0.4",
|
||||
"micromatch": "^4.0.8",
|
||||
"path-browserify": "^1.0.1",
|
||||
|
@@ -1,28 +1,25 @@
|
||||
import { MockedD3 } from './tests/MockedD3.js';
|
||||
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
|
||||
import type { D3Element } from './types.js';
|
||||
import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js';
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
import { expect } from 'vitest';
|
||||
|
||||
describe('accessibility', () => {
|
||||
const fauxSvgNode: MockedD3 = new MockedD3();
|
||||
|
||||
describe('setA11yDiagramInfo', () => {
|
||||
it('should set svg element role to "graphics-document document"', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('role', 'graphics-document document');
|
||||
jsdomIt('should set svg element role to "graphics-document document"', ({ svg }) => {
|
||||
setA11yDiagramInfo(svg, 'flowchart');
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('role')).toBe('graphics-document document');
|
||||
});
|
||||
|
||||
it('should set aria-roledescription to the diagram type', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart');
|
||||
jsdomIt('should set aria-roledescription to the diagram type', ({ svg }) => {
|
||||
setA11yDiagramInfo(svg, 'flowchart');
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-roledescription')).toBe('flowchart');
|
||||
});
|
||||
|
||||
it('should not set aria-roledescription if the diagram type is empty', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, '');
|
||||
expect(svgAttrSpy).toHaveBeenCalledTimes(1);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('role', expect.anything()); // only called to set the role
|
||||
jsdomIt('should not set aria-roledescription if the diagram type is empty', ({ svg }) => {
|
||||
setA11yDiagramInfo(svg, '');
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-roledescription')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,115 +36,78 @@ describe('accessibility', () => {
|
||||
expect(noInsertAttrSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// convenience functions to DRY up the spec
|
||||
|
||||
function expectAriaLabelledByItTitleId(
|
||||
svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string
|
||||
): void {
|
||||
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
|
||||
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`);
|
||||
}
|
||||
|
||||
function expectAriaDescribedByItDescId(
|
||||
svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string
|
||||
): void {
|
||||
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
|
||||
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`);
|
||||
}
|
||||
|
||||
function a11yTitleTagInserted(
|
||||
svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string,
|
||||
callNumber: number
|
||||
): void {
|
||||
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title);
|
||||
}
|
||||
|
||||
function a11yDescTagInserted(
|
||||
svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string,
|
||||
callNumber: number
|
||||
): void {
|
||||
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc);
|
||||
}
|
||||
|
||||
function a11yTagInserted(
|
||||
_svgD3Node: D3Element,
|
||||
title: string | undefined,
|
||||
desc: string | undefined,
|
||||
givenId: string,
|
||||
callNumber: number,
|
||||
expectedPrefix: string,
|
||||
expectedText: string | undefined
|
||||
): void {
|
||||
const fauxInsertedD3: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3);
|
||||
const titleAttrSpy = vi.spyOn(fauxInsertedD3, 'attr').mockReturnValue(fauxInsertedD3);
|
||||
const titleTextSpy = vi.spyOn(fauxInsertedD3, 'text');
|
||||
|
||||
addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId);
|
||||
expect(svginsertpy).toHaveBeenCalledWith(expectedPrefix, ':first-child');
|
||||
expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`);
|
||||
expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText);
|
||||
}
|
||||
|
||||
describe('with a11y title', () => {
|
||||
const a11yTitle = 'a11y title';
|
||||
|
||||
describe('with a11y description', () => {
|
||||
const a11yDesc = 'a11y description';
|
||||
|
||||
it('should set aria-labelledby to the title id inserted as a child', () => {
|
||||
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
jsdomIt('should set aria-labelledby to the title id inserted as a child', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-labelledby')).toBe(`chart-title-${givenId}`);
|
||||
});
|
||||
|
||||
it('should set aria-describedby to the description id inserted as a child', () => {
|
||||
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
});
|
||||
jsdomIt(
|
||||
'should set aria-describedby to the description id inserted as a child',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${givenId}`);
|
||||
}
|
||||
);
|
||||
|
||||
it('should insert title tag as the first child with the text set to the accTitle given', () => {
|
||||
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2);
|
||||
});
|
||||
jsdomIt(
|
||||
'should insert title tag as the first child with the text set to the accTitle given',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const titleNode = ensureNodeFromSelector('title', svgNode);
|
||||
expect(titleNode?.innerHTML).toBe(a11yTitle);
|
||||
}
|
||||
);
|
||||
|
||||
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
|
||||
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
jsdomIt(
|
||||
'should insert desc tag as the 2nd child with the text set to accDescription given',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const descNode = ensureNodeFromSelector('desc', svgNode);
|
||||
expect(descNode?.innerHTML).toBe(a11yDesc);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe(`without a11y description`, () => {
|
||||
describe(`without a11y description`, {}, () => {
|
||||
const a11yDesc = undefined;
|
||||
|
||||
it('should set aria-labelledby to the title id inserted as a child', () => {
|
||||
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
jsdomIt('should set aria-labelledby to the title id inserted as a child', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-labelledby')).toBe(`chart-title-${givenId}`);
|
||||
});
|
||||
|
||||
it('should not set aria-describedby', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
|
||||
jsdomIt('should not set aria-describedby', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBeNull();
|
||||
});
|
||||
|
||||
it('should insert title tag as the first child with the text set to the accTitle given', () => {
|
||||
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
jsdomIt(
|
||||
'should insert title tag as the first child with the text set to the accTitle given',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const titleNode = ensureNodeFromSelector('title', svgNode);
|
||||
expect(titleNode?.innerHTML).toBe(a11yTitle);
|
||||
}
|
||||
);
|
||||
|
||||
it('should not insert description tag', () => {
|
||||
const fauxTitle: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
|
||||
jsdomIt('should not insert description tag', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const descNode = svgNode.querySelector('desc');
|
||||
expect(descNode).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -158,55 +118,66 @@ describe('accessibility', () => {
|
||||
describe('with a11y description', () => {
|
||||
const a11yDesc = 'a11y description';
|
||||
|
||||
it('should not set aria-labelledby', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
|
||||
jsdomIt('should not set aria-labelledby', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-labelledby')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not insert title tag', () => {
|
||||
const fauxTitle: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
|
||||
jsdomIt('should not insert title tag', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const titleNode = svgNode.querySelector('title');
|
||||
expect(titleNode).toBeNull();
|
||||
});
|
||||
|
||||
it('should set aria-describedby to the description id inserted as a child', () => {
|
||||
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
});
|
||||
jsdomIt(
|
||||
'should set aria-describedby to the description id inserted as a child',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${givenId}`);
|
||||
}
|
||||
);
|
||||
|
||||
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
|
||||
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
jsdomIt(
|
||||
'should insert desc tag as the 2nd child with the text set to accDescription given',
|
||||
({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const descNode = ensureNodeFromSelector('desc', svgNode);
|
||||
expect(descNode?.innerHTML).toBe(a11yDesc);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('without a11y description', () => {
|
||||
const a11yDesc = undefined;
|
||||
|
||||
it('should not set aria-labelledby', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
|
||||
jsdomIt('should not set aria-labelledby', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-labelledby')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not set aria-describedby', () => {
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
|
||||
jsdomIt('should not set aria-describedby', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not insert title tag', () => {
|
||||
const fauxTitle: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
|
||||
jsdomIt('should not insert title tag', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const titleNode = svgNode.querySelector('title');
|
||||
expect(titleNode).toBeNull();
|
||||
});
|
||||
|
||||
it('should not insert description tag', () => {
|
||||
const fauxDesc: MockedD3 = new MockedD3();
|
||||
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
|
||||
jsdomIt('should not insert description tag', ({ svg }) => {
|
||||
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
|
||||
const svgNode = ensureNodeFromSelector('svg');
|
||||
const descNode = svgNode.querySelector('desc');
|
||||
expect(descNode).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -78,5 +78,41 @@ describe('diagram-orchestration', () => {
|
||||
flowchart: 1 "pie" pie: 2 "pie"`)
|
||||
).toBe('pie');
|
||||
});
|
||||
|
||||
it('should detect proper diagram when defaultRenderer is elk for flowchart', () => {
|
||||
expect(
|
||||
detectType('mindmap\n root\n Photograph\n Waterfall', {
|
||||
flowchart: { defaultRenderer: 'elk' },
|
||||
})
|
||||
).toBe('mindmap');
|
||||
expect(
|
||||
detectType(
|
||||
`
|
||||
classDiagram
|
||||
class Person {
|
||||
+String name
|
||||
-Int id
|
||||
#double age
|
||||
+Text demographicProfile
|
||||
}
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
)
|
||||
).toBe('class');
|
||||
expect(
|
||||
detectType(
|
||||
`
|
||||
erDiagram
|
||||
p[Photograph] {
|
||||
varchar(12) jobId
|
||||
date dateCreated
|
||||
}
|
||||
`,
|
||||
{
|
||||
flowchart: { defaultRenderer: 'elk' },
|
||||
}
|
||||
)
|
||||
).toBe('er');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
import { treemap } from '../diagrams/treemap/detector.js';
|
||||
import { usecase } from '../diagrams/usecase/detector.js';
|
||||
import '../type.d.ts';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
|
||||
xychart,
|
||||
block,
|
||||
radar,
|
||||
treemap
|
||||
treemap,
|
||||
usecase
|
||||
);
|
||||
};
|
||||
|
@@ -1,21 +1,12 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { db } from './architectureDb.js';
|
||||
import { parser } from './architectureParser.js';
|
||||
|
||||
const {
|
||||
clear,
|
||||
getDiagramTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
getServices,
|
||||
getGroups,
|
||||
getEdges,
|
||||
getJunctions,
|
||||
} = db;
|
||||
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
describe('architecture diagrams', () => {
|
||||
let db: ArchitectureDB;
|
||||
beforeEach(() => {
|
||||
clear();
|
||||
db = new ArchitectureDB();
|
||||
// @ts-expect-error since type is set to undefined we will have error
|
||||
parser.parser?.yy = db;
|
||||
});
|
||||
|
||||
describe('architecture diagram definitions', () => {
|
||||
@@ -36,7 +27,7 @@ describe('architecture diagrams', () => {
|
||||
it('should handle title on the first line', async () => {
|
||||
const str = `architecture-beta title Simple Architecture Diagram`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
});
|
||||
|
||||
it('should handle title on another line', async () => {
|
||||
@@ -44,7 +35,7 @@ describe('architecture diagrams', () => {
|
||||
title Simple Architecture Diagram
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
});
|
||||
|
||||
it('should handle accessibility title and description', async () => {
|
||||
@@ -53,8 +44,8 @@ describe('architecture diagrams', () => {
|
||||
accDescr: Accessibility Description
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getAccTitle()).toBe('Accessibility Title');
|
||||
expect(getAccDescription()).toBe('Accessibility Description');
|
||||
expect(db.getAccTitle()).toBe('Accessibility Title');
|
||||
expect(db.getAccDescription()).toBe('Accessibility Description');
|
||||
});
|
||||
|
||||
it('should handle multiline accessibility description', async () => {
|
||||
@@ -64,7 +55,7 @@ describe('architecture diagrams', () => {
|
||||
}
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getAccDescription()).toBe('Accessibility Description');
|
||||
expect(db.getAccDescription()).toBe('Accessibility Description');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
} from '../common/commonDb.js';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
ArchitectureDB,
|
||||
ArchitectureDirectionPair,
|
||||
ArchitectureDirectionPairMap,
|
||||
ArchitectureEdge,
|
||||
@@ -33,330 +33,333 @@ import {
|
||||
isArchitectureService,
|
||||
shiftPositionByArchitectureDirectionPair,
|
||||
} from './architectureTypes.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
|
||||
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
|
||||
DEFAULT_CONFIG.architecture;
|
||||
export class ArchitectureDB implements DiagramDB {
|
||||
private nodes: Record<string, ArchitectureNode> = {};
|
||||
private groups: Record<string, ArchitectureGroup> = {};
|
||||
private edges: ArchitectureEdge[] = [];
|
||||
private registeredIds: Record<string, 'node' | 'group'> = {};
|
||||
private dataStructures?: ArchitectureState['dataStructures'];
|
||||
private elements: Record<string, D3Element> = {};
|
||||
|
||||
const state = new ImperativeState<ArchitectureState>(() => ({
|
||||
nodes: {},
|
||||
groups: {},
|
||||
edges: [],
|
||||
registeredIds: {},
|
||||
config: DEFAULT_ARCHITECTURE_CONFIG,
|
||||
dataStructures: undefined,
|
||||
elements: {},
|
||||
}));
|
||||
|
||||
const clear = (): void => {
|
||||
state.reset();
|
||||
commonClear();
|
||||
};
|
||||
|
||||
const addService = function ({
|
||||
id,
|
||||
icon,
|
||||
in: parent,
|
||||
title,
|
||||
iconText,
|
||||
}: Omit<ArchitectureService, 'edges'>) {
|
||||
if (state.records.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The service id [${id}] is already in use by another ${state.records.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The service [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
|
||||
);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The service [${id}]'s parent is not a group`);
|
||||
}
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
state.records.registeredIds[id] = 'node';
|
||||
public clear(): void {
|
||||
this.nodes = {};
|
||||
this.groups = {};
|
||||
this.edges = [];
|
||||
this.registeredIds = {};
|
||||
this.dataStructures = undefined;
|
||||
this.elements = {};
|
||||
commonClear();
|
||||
}
|
||||
|
||||
state.records.nodes[id] = {
|
||||
public addService({
|
||||
id,
|
||||
type: 'service',
|
||||
icon,
|
||||
in: parent,
|
||||
title,
|
||||
iconText,
|
||||
title,
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
|
||||
const getServices = (): ArchitectureService[] =>
|
||||
Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService);
|
||||
|
||||
const addJunction = function ({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>) {
|
||||
state.records.registeredIds[id] = 'node';
|
||||
|
||||
state.records.nodes[id] = {
|
||||
id,
|
||||
type: 'junction',
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
|
||||
const getJunctions = (): ArchitectureJunction[] =>
|
||||
Object.values(state.records.nodes).filter<ArchitectureJunction>(isArchitectureJunction);
|
||||
|
||||
const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes);
|
||||
|
||||
const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id];
|
||||
|
||||
const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) {
|
||||
if (state.records.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group id [${id}] is already in use by another ${state.records.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The group [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === undefined) {
|
||||
}: Omit<ArchitectureService, 'edges'>): void {
|
||||
if (this.registeredIds[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
|
||||
`The service id [${id}] is already in use by another ${this.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The group [${id}]'s parent is not a group`);
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The service [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (this.registeredIds[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
|
||||
);
|
||||
}
|
||||
if (this.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The service [${id}]'s parent is not a group`);
|
||||
}
|
||||
}
|
||||
|
||||
this.registeredIds[id] = 'node';
|
||||
|
||||
this.nodes[id] = {
|
||||
id,
|
||||
type: 'service',
|
||||
icon,
|
||||
iconText,
|
||||
title,
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
|
||||
state.records.registeredIds[id] = 'group';
|
||||
|
||||
state.records.groups[id] = {
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
in: parent,
|
||||
};
|
||||
};
|
||||
const getGroups = (): ArchitectureGroup[] => {
|
||||
return Object.values(state.records.groups);
|
||||
};
|
||||
|
||||
const addEdge = function ({
|
||||
lhsId,
|
||||
rhsId,
|
||||
lhsDir,
|
||||
rhsDir,
|
||||
lhsInto,
|
||||
rhsInto,
|
||||
lhsGroup,
|
||||
rhsGroup,
|
||||
title,
|
||||
}: ArchitectureEdge<string>) {
|
||||
if (!isArchitectureDirection(lhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}`
|
||||
);
|
||||
}
|
||||
if (!isArchitectureDirection(rhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}`
|
||||
);
|
||||
public getServices(): ArchitectureService[] {
|
||||
return Object.values(this.nodes).filter(isArchitectureService);
|
||||
}
|
||||
|
||||
if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
public addJunction({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>): void {
|
||||
this.registeredIds[id] = 'node';
|
||||
|
||||
this.nodes[id] = {
|
||||
id,
|
||||
type: 'junction',
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
|
||||
const lhsGroupId = state.records.nodes[lhsId].in;
|
||||
const rhsGroupId = state.records.nodes[rhsId].in;
|
||||
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
public getJunctions(): ArchitectureJunction[] {
|
||||
return Object.values(this.nodes).filter(isArchitectureJunction);
|
||||
}
|
||||
|
||||
const edge = {
|
||||
public getNodes(): ArchitectureNode[] {
|
||||
return Object.values(this.nodes);
|
||||
}
|
||||
|
||||
public getNode(id: string): ArchitectureNode | null {
|
||||
return this.nodes[id] ?? null;
|
||||
}
|
||||
|
||||
public addGroup({ id, icon, in: parent, title }: ArchitectureGroup): void {
|
||||
if (this.registeredIds?.[id] !== undefined) {
|
||||
throw new Error(
|
||||
`The group id [${id}] is already in use by another ${this.registeredIds[id]}`
|
||||
);
|
||||
}
|
||||
if (parent !== undefined) {
|
||||
if (id === parent) {
|
||||
throw new Error(`The group [${id}] cannot be placed within itself`);
|
||||
}
|
||||
if (this.registeredIds?.[parent] === undefined) {
|
||||
throw new Error(
|
||||
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
|
||||
);
|
||||
}
|
||||
if (this.registeredIds?.[parent] === 'node') {
|
||||
throw new Error(`The group [${id}]'s parent is not a group`);
|
||||
}
|
||||
}
|
||||
|
||||
this.registeredIds[id] = 'group';
|
||||
|
||||
this.groups[id] = {
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
public getGroups(): ArchitectureGroup[] {
|
||||
return Object.values(this.groups);
|
||||
}
|
||||
public addEdge({
|
||||
lhsId,
|
||||
lhsDir,
|
||||
lhsInto,
|
||||
lhsGroup,
|
||||
rhsId,
|
||||
lhsDir,
|
||||
rhsDir,
|
||||
lhsInto,
|
||||
rhsInto,
|
||||
lhsGroup,
|
||||
rhsGroup,
|
||||
title,
|
||||
};
|
||||
|
||||
state.records.edges.push(edge);
|
||||
if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) {
|
||||
state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
const getEdges = (): ArchitectureEdge[] => state.records.edges;
|
||||
|
||||
/**
|
||||
* Returns the current diagram's adjacency list, spatial map, & group alignments.
|
||||
* If they have not been created, run the algorithms to generate them.
|
||||
* @returns
|
||||
*/
|
||||
const getDataStructures = () => {
|
||||
if (state.records.dataStructures === undefined) {
|
||||
// Tracks how groups are aligned with one another. Generated while creating the adj list
|
||||
const groupAlignments: Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
> = {};
|
||||
|
||||
// Create an adjacency list of the diagram to perform BFS on
|
||||
// Outer reduce applied on all services
|
||||
// Inner reduce applied on the edges for a service
|
||||
const adjList = Object.entries(state.records.nodes).reduce<
|
||||
Record<string, ArchitectureDirectionPairMap>
|
||||
>((prevOuter, [id, service]) => {
|
||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
||||
// track the direction groups connect to one another
|
||||
const lhsGroupId = getNode(edge.lhsId)?.in;
|
||||
const rhsGroupId = getNode(edge.rhsId)?.in;
|
||||
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
|
||||
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
|
||||
if (alignment !== 'bend') {
|
||||
groupAlignments[lhsGroupId] ??= {};
|
||||
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
|
||||
groupAlignments[rhsGroupId] ??= {};
|
||||
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
|
||||
}
|
||||
}
|
||||
|
||||
if (edge.lhsId === id) {
|
||||
// source is LHS
|
||||
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.rhsId;
|
||||
}
|
||||
} else {
|
||||
// source is RHS
|
||||
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.lhsId;
|
||||
}
|
||||
}
|
||||
return prevInner;
|
||||
}, {});
|
||||
return prevOuter;
|
||||
}, {});
|
||||
|
||||
// Configuration for the initial pass of BFS
|
||||
const firstId = Object.keys(adjList)[0];
|
||||
const visited = { [firstId]: 1 };
|
||||
// If a key is present in this object, it has not been visited
|
||||
const notVisited = Object.keys(adjList).reduce(
|
||||
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
// Perform BFS on the adjacency list
|
||||
const BFS = (startingId: string): ArchitectureSpatialMap => {
|
||||
const spatialMap = { [startingId]: [0, 0] };
|
||||
const queue = [startingId];
|
||||
while (queue.length > 0) {
|
||||
const id = queue.shift();
|
||||
if (id) {
|
||||
visited[id] = 1;
|
||||
delete notVisited[id];
|
||||
const adj = adjList[id];
|
||||
const [posX, posY] = spatialMap[id];
|
||||
Object.entries(adj).forEach(([dir, rhsId]) => {
|
||||
if (!visited[rhsId]) {
|
||||
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
|
||||
[posX, posY],
|
||||
dir as ArchitectureDirectionPair
|
||||
);
|
||||
queue.push(rhsId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return spatialMap;
|
||||
};
|
||||
const spatialMaps = [BFS(firstId)];
|
||||
|
||||
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
|
||||
while (Object.keys(notVisited).length > 0) {
|
||||
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
|
||||
}: ArchitectureEdge): void {
|
||||
if (!isArchitectureDirection(lhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(lhsDir)}`
|
||||
);
|
||||
}
|
||||
state.records.dataStructures = {
|
||||
adjList,
|
||||
spatialMaps,
|
||||
groupAlignments,
|
||||
if (!isArchitectureDirection(rhsDir)) {
|
||||
throw new Error(
|
||||
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(rhsDir)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.nodes[lhsId] === undefined && this.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
if (this.nodes[rhsId] === undefined && this.groups[rhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
|
||||
const lhsGroupId = this.nodes[lhsId].in;
|
||||
const rhsGroupId = this.nodes[rhsId].in;
|
||||
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
|
||||
);
|
||||
}
|
||||
|
||||
const edge = {
|
||||
lhsId,
|
||||
lhsDir,
|
||||
lhsInto,
|
||||
lhsGroup,
|
||||
rhsId,
|
||||
rhsDir,
|
||||
rhsInto,
|
||||
rhsGroup,
|
||||
title,
|
||||
};
|
||||
|
||||
this.edges.push(edge);
|
||||
if (this.nodes[lhsId] && this.nodes[rhsId]) {
|
||||
this.nodes[lhsId].edges.push(this.edges[this.edges.length - 1]);
|
||||
this.nodes[rhsId].edges.push(this.edges[this.edges.length - 1]);
|
||||
}
|
||||
}
|
||||
return state.records.dataStructures;
|
||||
};
|
||||
|
||||
const setElementForId = (id: string, element: D3Element) => {
|
||||
state.records.elements[id] = element;
|
||||
};
|
||||
const getElementById = (id: string) => state.records.elements[id];
|
||||
public getEdges(): ArchitectureEdge[] {
|
||||
return this.edges;
|
||||
}
|
||||
|
||||
const getConfig = (): Required<ArchitectureDiagramConfig> => {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_ARCHITECTURE_CONFIG,
|
||||
...commonGetConfig().architecture,
|
||||
});
|
||||
return config;
|
||||
};
|
||||
/**
|
||||
* Returns the current diagram's adjacency list, spatial map, & group alignments.
|
||||
* If they have not been created, run the algorithms to generate them.
|
||||
* @returns
|
||||
*/
|
||||
public getDataStructures() {
|
||||
if (this.dataStructures === undefined) {
|
||||
// Tracks how groups are aligned with one another. Generated while creating the adj list
|
||||
const groupAlignments: Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
> = {};
|
||||
|
||||
export const db: ArchitectureDB = {
|
||||
clear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
getConfig,
|
||||
// Create an adjacency list of the diagram to perform BFS on
|
||||
// Outer reduce applied on all services
|
||||
// Inner reduce applied on the edges for a service
|
||||
const adjList = Object.entries(this.nodes).reduce<
|
||||
Record<string, ArchitectureDirectionPairMap>
|
||||
>((prevOuter, [id, service]) => {
|
||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
||||
// track the direction groups connect to one another
|
||||
const lhsGroupId = this.getNode(edge.lhsId)?.in;
|
||||
const rhsGroupId = this.getNode(edge.rhsId)?.in;
|
||||
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
|
||||
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
|
||||
if (alignment !== 'bend') {
|
||||
groupAlignments[lhsGroupId] ??= {};
|
||||
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
|
||||
groupAlignments[rhsGroupId] ??= {};
|
||||
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
|
||||
}
|
||||
}
|
||||
|
||||
addService,
|
||||
getServices,
|
||||
addJunction,
|
||||
getJunctions,
|
||||
getNodes,
|
||||
getNode,
|
||||
addGroup,
|
||||
getGroups,
|
||||
addEdge,
|
||||
getEdges,
|
||||
setElementForId,
|
||||
getElementById,
|
||||
getDataStructures,
|
||||
};
|
||||
if (edge.lhsId === id) {
|
||||
// source is LHS
|
||||
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.rhsId;
|
||||
}
|
||||
} else {
|
||||
// source is RHS
|
||||
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
|
||||
if (pair) {
|
||||
prevInner[pair] = edge.lhsId;
|
||||
}
|
||||
}
|
||||
return prevInner;
|
||||
}, {});
|
||||
return prevOuter;
|
||||
}, {});
|
||||
|
||||
// Configuration for the initial pass of BFS
|
||||
const firstId = Object.keys(adjList)[0];
|
||||
const visited = { [firstId]: 1 };
|
||||
// If a key is present in this object, it has not been visited
|
||||
const notVisited = Object.keys(adjList).reduce(
|
||||
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
// Perform BFS on the adjacency list
|
||||
const BFS = (startingId: string): ArchitectureSpatialMap => {
|
||||
const spatialMap = { [startingId]: [0, 0] };
|
||||
const queue = [startingId];
|
||||
while (queue.length > 0) {
|
||||
const id = queue.shift();
|
||||
if (id) {
|
||||
visited[id] = 1;
|
||||
delete notVisited[id];
|
||||
const adj = adjList[id];
|
||||
const [posX, posY] = spatialMap[id];
|
||||
Object.entries(adj).forEach(([dir, rhsId]) => {
|
||||
if (!visited[rhsId]) {
|
||||
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
|
||||
[posX, posY],
|
||||
dir as ArchitectureDirectionPair
|
||||
);
|
||||
queue.push(rhsId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return spatialMap;
|
||||
};
|
||||
const spatialMaps = [BFS(firstId)];
|
||||
|
||||
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
|
||||
while (Object.keys(notVisited).length > 0) {
|
||||
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
|
||||
}
|
||||
this.dataStructures = {
|
||||
adjList,
|
||||
spatialMaps,
|
||||
groupAlignments,
|
||||
};
|
||||
}
|
||||
return this.dataStructures;
|
||||
}
|
||||
|
||||
public setElementForId(id: string, element: D3Element): void {
|
||||
this.elements[id] = element;
|
||||
}
|
||||
|
||||
public getElementById(id: string): D3Element {
|
||||
return this.elements[id];
|
||||
}
|
||||
|
||||
public getConfig(): Required<ArchitectureDiagramConfig> {
|
||||
return cleanAndMerge({
|
||||
...DEFAULT_ARCHITECTURE_CONFIG,
|
||||
...commonGetConfig().architecture,
|
||||
});
|
||||
}
|
||||
|
||||
public getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
field: T
|
||||
): Required<ArchitectureDiagramConfig>[T] {
|
||||
return this.getConfig()[field];
|
||||
}
|
||||
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setAccDescription = setAccDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined
|
||||
* @param field - the config field to access
|
||||
* @returns
|
||||
*/
|
||||
export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
field: T
|
||||
): Required<ArchitectureDiagramConfig>[T] {
|
||||
return getConfig()[field];
|
||||
}
|
||||
// export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
// field: T
|
||||
// ): Required<ArchitectureDiagramConfig>[T] {
|
||||
// return db.getConfig()[field];
|
||||
// }
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { parser } from './architectureParser.js';
|
||||
import { db } from './architectureDb.js';
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
import styles from './architectureStyles.js';
|
||||
import { renderer } from './architectureRenderer.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new ArchitectureDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
@@ -1,24 +1,33 @@
|
||||
import type { Architecture } from '@mermaid-js/parser';
|
||||
import { parse } from '@mermaid-js/parser';
|
||||
import { log } from '../../logger.js';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import type { ArchitectureDB } from './architectureTypes.js';
|
||||
import { db } from './architectureDb.js';
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
|
||||
const populateDb = (ast: Architecture, db: ArchitectureDB) => {
|
||||
populateCommonDb(ast, db);
|
||||
ast.groups.map(db.addGroup);
|
||||
ast.groups.map((group) => db.addGroup(group));
|
||||
ast.services.map((service) => db.addService({ ...service, type: 'service' }));
|
||||
ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' }));
|
||||
// @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type?
|
||||
ast.edges.map(db.addEdge);
|
||||
ast.edges.map((edge) => db.addEdge(edge));
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parser: {
|
||||
// @ts-expect-error - ArchitectureDB is not assignable to DiagramDB
|
||||
yy: undefined,
|
||||
},
|
||||
parse: async (input: string): Promise<void> => {
|
||||
const ast: Architecture = await parse('architecture', input);
|
||||
log.debug(ast);
|
||||
const db = parser.parser?.yy;
|
||||
if (!(db instanceof ArchitectureDB)) {
|
||||
throw new Error(
|
||||
'parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||
);
|
||||
}
|
||||
populateDb(ast, db);
|
||||
},
|
||||
};
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { registerIconPacks } from '../../rendering-util/icons.js';
|
||||
import type { Position } from 'cytoscape';
|
||||
import cytoscape from 'cytoscape';
|
||||
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
|
||||
@@ -7,9 +6,10 @@ import { select } from 'd3';
|
||||
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { registerIconPacks } from '../../rendering-util/icons.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import { getConfigField } from './architectureDb.js';
|
||||
import type { ArchitectureDB } from './architectureDb.js';
|
||||
import { architectureIcons } from './architectureIcons.js';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
@@ -22,7 +22,6 @@ import type {
|
||||
NodeSingularData,
|
||||
} from './architectureTypes.js';
|
||||
import {
|
||||
type ArchitectureDB,
|
||||
type ArchitectureDirection,
|
||||
type ArchitectureEdge,
|
||||
type ArchitectureGroup,
|
||||
@@ -44,7 +43,7 @@ registerIconPacks([
|
||||
]);
|
||||
cytoscape.use(fcose);
|
||||
|
||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
|
||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||
services.forEach((service) => {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
@@ -54,15 +53,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
|
||||
icon: service.icon,
|
||||
label: service.title,
|
||||
parent: service.in,
|
||||
width: getConfigField('iconSize'),
|
||||
height: getConfigField('iconSize'),
|
||||
width: db.getConfigField('iconSize'),
|
||||
height: db.getConfigField('iconSize'),
|
||||
} as NodeSingularData,
|
||||
classes: 'node-service',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
|
||||
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||
junctions.forEach((junction) => {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
@@ -70,8 +69,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
|
||||
type: 'junction',
|
||||
id: junction.id,
|
||||
parent: junction.in,
|
||||
width: getConfigField('iconSize'),
|
||||
height: getConfigField('iconSize'),
|
||||
width: db.getConfigField('iconSize'),
|
||||
height: db.getConfigField('iconSize'),
|
||||
} as NodeSingularData,
|
||||
classes: 'node-junction',
|
||||
});
|
||||
@@ -257,7 +256,8 @@ function getAlignments(
|
||||
}
|
||||
|
||||
function getRelativeConstraints(
|
||||
spatialMaps: ArchitectureSpatialMap[]
|
||||
spatialMaps: ArchitectureSpatialMap[],
|
||||
db: ArchitectureDB
|
||||
): fcose.FcoseRelativePlacementConstraint[] {
|
||||
const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [];
|
||||
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
|
||||
@@ -296,7 +296,7 @@ function getRelativeConstraints(
|
||||
[ArchitectureDirectionName[
|
||||
getOppositeArchitectureDirection(dir as ArchitectureDirection)
|
||||
]]: currId,
|
||||
gap: 1.5 * getConfigField('iconSize'),
|
||||
gap: 1.5 * db.getConfigField('iconSize'),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -353,7 +353,7 @@ function layoutArchitecture(
|
||||
style: {
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'font-size': `${getConfigField('fontSize')}px`,
|
||||
'font-size': `${db.getConfigField('fontSize')}px`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -375,23 +375,32 @@ function layoutArchitecture(
|
||||
selector: '.node-group',
|
||||
style: {
|
||||
// @ts-ignore Incorrect library types
|
||||
padding: `${getConfigField('padding')}px`,
|
||||
padding: `${db.getConfigField('padding')}px`,
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
name: 'grid',
|
||||
boundingBox: {
|
||||
x1: 0,
|
||||
x2: 100,
|
||||
y1: 0,
|
||||
y2: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
// Remove element after layout
|
||||
renderEl.remove();
|
||||
|
||||
addGroups(groups, cy);
|
||||
addServices(services, cy);
|
||||
addJunctions(junctions, cy);
|
||||
addServices(services, cy, db);
|
||||
addJunctions(junctions, cy, db);
|
||||
addEdges(edges, cy);
|
||||
// Use the spatial map to create alignment arrays for fcose
|
||||
const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments);
|
||||
|
||||
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
|
||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
|
||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db);
|
||||
|
||||
const layout = cy.layout({
|
||||
name: 'fcose',
|
||||
@@ -406,7 +415,9 @@ function layoutArchitecture(
|
||||
const { parent: parentA } = nodeData(nodeA);
|
||||
const { parent: parentB } = nodeData(nodeB);
|
||||
const elasticity =
|
||||
parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize');
|
||||
parentA === parentB
|
||||
? 1.5 * db.getConfigField('iconSize')
|
||||
: 0.5 * db.getConfigField('iconSize');
|
||||
return elasticity;
|
||||
},
|
||||
edgeElasticity(edge: EdgeSingular) {
|
||||
@@ -526,11 +537,11 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
||||
|
||||
const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);
|
||||
|
||||
await drawEdges(edgesElem, cy);
|
||||
await drawGroups(groupElem, cy);
|
||||
await drawEdges(edgesElem, cy, db);
|
||||
await drawGroups(groupElem, cy, db);
|
||||
positionNodes(db, cy);
|
||||
|
||||
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth'));
|
||||
setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth'));
|
||||
};
|
||||
|
||||
export const renderer = { draw };
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
||||
import type cytoscape from 'cytoscape';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { db, getConfigField } from './architectureDb.js';
|
||||
import type { ArchitectureDB } from './architectureDb.js';
|
||||
import { architectureIcons } from './architectureIcons.js';
|
||||
import {
|
||||
ArchitectureDirectionArrow,
|
||||
@@ -16,14 +16,17 @@ import {
|
||||
isArchitectureDirectionY,
|
||||
isArchitecturePairXY,
|
||||
nodeData,
|
||||
type ArchitectureDB,
|
||||
type ArchitectureJunction,
|
||||
type ArchitectureService,
|
||||
} from './architectureTypes.js';
|
||||
|
||||
export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) {
|
||||
const padding = getConfigField('padding');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
export const drawEdges = async function (
|
||||
edgesEl: D3Element,
|
||||
cy: cytoscape.Core,
|
||||
db: ArchitectureDB
|
||||
) {
|
||||
const padding = db.getConfigField('padding');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
const halfIconSize = iconSize / 2;
|
||||
const arrowSize = iconSize / 6;
|
||||
const halfArrowSize = arrowSize / 2;
|
||||
@@ -183,13 +186,17 @@ export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core)
|
||||
);
|
||||
};
|
||||
|
||||
export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) {
|
||||
const padding = getConfigField('padding');
|
||||
export const drawGroups = async function (
|
||||
groupsEl: D3Element,
|
||||
cy: cytoscape.Core,
|
||||
db: ArchitectureDB
|
||||
) {
|
||||
const padding = db.getConfigField('padding');
|
||||
const groupIconSize = padding * 0.75;
|
||||
|
||||
const fontSize = getConfigField('fontSize');
|
||||
const fontSize = db.getConfigField('fontSize');
|
||||
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
const halfIconSize = iconSize / 2;
|
||||
|
||||
await Promise.all(
|
||||
@@ -266,7 +273,7 @@ export const drawServices = async function (
|
||||
): Promise<number> {
|
||||
for (const service of services) {
|
||||
const serviceElem = elem.append('g');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
|
||||
if (service.title) {
|
||||
const textElem = serviceElem.append('g');
|
||||
@@ -350,7 +357,7 @@ export const drawJunctions = function (
|
||||
) {
|
||||
junctions.forEach((junction) => {
|
||||
const junctionElem = elem.append('g');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
const iconSize = db.getConfigField('iconSize');
|
||||
|
||||
const bkgElem = junctionElem.append('g');
|
||||
bkgElem
|
||||
|
@@ -92,7 +92,20 @@ export const setCssClass = function (itemIds: string, cssClassName: string) {
|
||||
const populateBlockDatabase = (_blockList: Block[], parent: Block): void => {
|
||||
const blockList = _blockList.flat();
|
||||
const children = [];
|
||||
const columnSettingBlock = blockList.find((b) => b?.type === 'column-setting');
|
||||
const column = columnSettingBlock?.columns ?? -1;
|
||||
for (const block of blockList) {
|
||||
if (
|
||||
typeof column === 'number' &&
|
||||
column > 0 &&
|
||||
block.type !== 'column-setting' &&
|
||||
typeof block.widthInColumns === 'number' &&
|
||||
block.widthInColumns > column
|
||||
) {
|
||||
log.warn(
|
||||
`Block ${block.id} width ${block.widthInColumns} exceeds configured column width ${column}`
|
||||
);
|
||||
}
|
||||
if (block.label) {
|
||||
block.label = sanitizeText(block.label);
|
||||
}
|
||||
|
@@ -270,7 +270,12 @@ function layoutBlocks(block: Block, db: BlockDB) {
|
||||
if (child.children) {
|
||||
layoutBlocks(child, db);
|
||||
}
|
||||
columnPos += child?.widthInColumns ?? 1;
|
||||
let columnsFilled = child?.widthInColumns ?? 1;
|
||||
if (columns > 0) {
|
||||
// Make sure overflowing lines do not affect later lines
|
||||
columnsFilled = Math.min(columnsFilled, columns - (columnPos % columns));
|
||||
}
|
||||
columnPos += columnsFilled;
|
||||
log.debug('abc88 columnsPos', child, columnPos);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// @ts-ignore: jison doesn't export types
|
||||
import block from './block.jison';
|
||||
import db from '../blockDB.js';
|
||||
import { log } from '../../../logger.js';
|
||||
|
||||
describe('Block diagram', function () {
|
||||
describe('when parsing a block diagram graph it should handle > ', function () {
|
||||
@@ -402,6 +403,25 @@ columns 1
|
||||
const B = blocks[0];
|
||||
expect(B.styles).toContain('fill:#f9F');
|
||||
});
|
||||
it('should log a warning when block width exceeds column width', () => {
|
||||
const str = `block-beta
|
||||
columns 1
|
||||
A:1
|
||||
B:2
|
||||
C:3
|
||||
D:4
|
||||
E:3
|
||||
F:2
|
||||
G:1`;
|
||||
|
||||
const logWarnSpy = vi.spyOn(log, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
block.parse(str);
|
||||
|
||||
expect(logWarnSpy).toHaveBeenCalledWith('Block B width 2 exceeds configured column width 1');
|
||||
|
||||
logWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('prototype properties', function () {
|
||||
|
@@ -15,4 +15,12 @@ describe('class diagram', function () {
|
||||
expect(() => parser.parse(`classDiagram\nnamespace ${prop} {\n\tclass A\n}`)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('backtick escaping', function () {
|
||||
it('should handle backtick-quoted namespace names', function () {
|
||||
expect(() =>
|
||||
parser.parse(`classDiagram\nnamespace \`A::B\` {\n\tclass \`IPC::Sender\`\n}`)
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -242,6 +242,7 @@ classLabel
|
||||
|
||||
namespaceName
|
||||
: alphaNumToken { $$=$1; }
|
||||
| classLiteralName { $$=$1; }
|
||||
| alphaNumToken DOT namespaceName { $$=$1+'.'+$3; }
|
||||
| alphaNumToken namespaceName { $$=$1+$2; }
|
||||
;
|
||||
|
@@ -11,7 +11,7 @@ const detector: DiagramDetector = (txt, config = {}): boolean => {
|
||||
// If diagram explicitly states flowchart-elk
|
||||
/^\s*flowchart-elk/.test(txt) ||
|
||||
// If a flowchart/graph diagram has their default renderer set to elk
|
||||
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
|
||||
(/^\s*(flowchart|graph)/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
|
||||
) {
|
||||
config.layout = 'elk';
|
||||
return true;
|
||||
|
@@ -125,4 +125,60 @@ describe('flow db getData', () => {
|
||||
const { edges } = flowDb.getData();
|
||||
expect(edges[0].curve).toBe('basis');
|
||||
});
|
||||
|
||||
it('should support modifying interpolate using edge id syntax', () => {
|
||||
flowDb.addVertex('A', { text: 'A', type: 'text' }, undefined, [], [], '', {}, undefined);
|
||||
flowDb.addVertex('B', { text: 'B', type: 'text' }, undefined, [], [], '', {}, undefined);
|
||||
flowDb.addVertex('C', { text: 'C', type: 'text' }, undefined, [], [], '', {}, undefined);
|
||||
flowDb.addVertex('D', { text: 'D', type: 'text' }, undefined, [], [], '', {}, undefined);
|
||||
flowDb.addLink(['A'], ['B'], {});
|
||||
flowDb.addLink(['A'], ['C'], { id: 'e2' });
|
||||
flowDb.addLink(['B'], ['D'], { id: 'e3' });
|
||||
flowDb.addLink(['C'], ['D'], {});
|
||||
flowDb.updateLinkInterpolate(['default'], 'stepBefore');
|
||||
flowDb.updateLinkInterpolate([0], 'basis');
|
||||
flowDb.addVertex(
|
||||
'e2',
|
||||
{ text: 'Shouldnt be used', type: 'text' },
|
||||
undefined,
|
||||
[],
|
||||
[],
|
||||
'',
|
||||
{},
|
||||
' curve: monotoneX '
|
||||
);
|
||||
flowDb.addVertex(
|
||||
'e3',
|
||||
{ text: 'Shouldnt be used', type: 'text' },
|
||||
undefined,
|
||||
[],
|
||||
[],
|
||||
'',
|
||||
{},
|
||||
' curve: catmullRom '
|
||||
);
|
||||
|
||||
const { edges } = flowDb.getData();
|
||||
expect(edges[0].curve).toBe('basis');
|
||||
expect(edges[1].curve).toBe('monotoneX');
|
||||
expect(edges[2].curve).toBe('catmullRom');
|
||||
expect(edges[3].curve).toBe('stepBefore');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flow db direction', () => {
|
||||
let flowDb: FlowDB;
|
||||
beforeEach(() => {
|
||||
flowDb = new FlowDB();
|
||||
});
|
||||
|
||||
it('should set direction to TB when TD is set', () => {
|
||||
flowDb.setDirection('TD');
|
||||
expect(flowDb.getDirection()).toBe('TB');
|
||||
});
|
||||
|
||||
it('should correctly set direction irrespective of leading spaces', () => {
|
||||
flowDb.setDirection(' TD');
|
||||
expect(flowDb.getDirection()).toBe('TB');
|
||||
});
|
||||
});
|
||||
|
@@ -139,6 +139,9 @@ export class FlowDB implements DiagramDB {
|
||||
if (edgeDoc?.animation !== undefined) {
|
||||
edge.animation = edgeDoc.animation;
|
||||
}
|
||||
if (edgeDoc?.curve !== undefined) {
|
||||
edge.interpolate = edgeDoc.curve;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -403,7 +406,8 @@ You have to call mermaid.initialize.`
|
||||
*
|
||||
*/
|
||||
public setDirection(dir: string) {
|
||||
this.direction = dir;
|
||||
this.direction = dir.trim();
|
||||
|
||||
if (/.*</.exec(this.direction)) {
|
||||
this.direction = 'RL';
|
||||
}
|
||||
|
@@ -37,6 +37,59 @@ describe('[Lines] when parsing', () => {
|
||||
expect(edges[1].interpolate).toBe('cardinal');
|
||||
});
|
||||
|
||||
it('should handle edge curve properties using edge ID', function () {
|
||||
const res = flow.parser.parse(
|
||||
'graph TD\n' +
|
||||
'A e1@-->B\n' +
|
||||
'A uniqueName@-->C\n' +
|
||||
'e1@{curve: basis}\n' +
|
||||
'uniqueName@{curve: cardinal}'
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].interpolate).toBe('basis');
|
||||
expect(edges[1].interpolate).toBe('cardinal');
|
||||
});
|
||||
|
||||
it('should handle edge curve properties using edge ID but without overriding default', function () {
|
||||
const res = flow.parser.parse(
|
||||
'graph TD\n' +
|
||||
'A e1@-->B\n' +
|
||||
'A-->C\n' +
|
||||
'linkStyle default interpolate linear\n' +
|
||||
'e1@{curve: stepAfter}'
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].interpolate).toBe('stepAfter');
|
||||
expect(edges.defaultInterpolate).toBe('linear');
|
||||
});
|
||||
|
||||
it('should handle edge curve properties using edge ID mixed with line interpolation', function () {
|
||||
const res = flow.parser.parse(
|
||||
'graph TD\n' +
|
||||
'A e1@-->B-->D\n' +
|
||||
'A-->C e4@-->D-->E\n' +
|
||||
'linkStyle default interpolate linear\n' +
|
||||
'linkStyle 1 interpolate basis\n' +
|
||||
'e1@{curve: monotoneX}\n' +
|
||||
'e4@{curve: stepBefore}'
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].interpolate).toBe('monotoneX');
|
||||
expect(edges[1].interpolate).toBe('basis');
|
||||
expect(edges.defaultInterpolate).toBe('linear');
|
||||
expect(edges[3].interpolate).toBe('stepBefore');
|
||||
expect(edges.defaultInterpolate).toBe('linear');
|
||||
});
|
||||
|
||||
it('should handle line interpolation multi-numbered definitions', function () {
|
||||
const res = flow.parser.parse(
|
||||
'graph TD\n' + 'A-->B\n' + 'A-->C\n' + 'linkStyle 0,1 interpolate basis'
|
||||
|
@@ -1,12 +1,14 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/mindmap.jison';
|
||||
import db from './mindmapDb.js';
|
||||
import { MindmapDB } from './mindmapDb.js';
|
||||
import renderer from './mindmapRenderer.js';
|
||||
import styles from './styles.js';
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
db,
|
||||
get db() {
|
||||
return new MindmapDB();
|
||||
},
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
|
@@ -1,12 +1,12 @@
|
||||
// @ts-expect-error No types available for JISON
|
||||
import { parser as mindmap } from './parser/mindmap.jison';
|
||||
import mindmapDB from './mindmapDb.js';
|
||||
import { MindmapDB } from './mindmapDb.js';
|
||||
// Todo fix utils functions for tests
|
||||
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
|
||||
|
||||
describe('when parsing a mindmap ', function () {
|
||||
beforeEach(function () {
|
||||
mindmap.yy = mindmapDB;
|
||||
mindmap.yy = new MindmapDB();
|
||||
mindmap.yy.clear();
|
||||
setLogLevel('trace');
|
||||
});
|
||||
|
@@ -5,70 +5,6 @@ import { log } from '../../logger.js';
|
||||
import type { MindmapNode } from './mindmapTypes.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
|
||||
let nodes: MindmapNode[] = [];
|
||||
let cnt = 0;
|
||||
let elements: Record<number, D3Element> = {};
|
||||
|
||||
const clear = () => {
|
||||
nodes = [];
|
||||
cnt = 0;
|
||||
elements = {};
|
||||
};
|
||||
|
||||
const getParent = function (level: number) {
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
if (nodes[i].level < level) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
// No parent found
|
||||
return null;
|
||||
};
|
||||
|
||||
const getMindmap = () => {
|
||||
return nodes.length > 0 ? nodes[0] : null;
|
||||
};
|
||||
|
||||
const addNode = (level: number, id: string, descr: string, type: number) => {
|
||||
log.info('addNode', level, id, descr, type);
|
||||
const conf = getConfig();
|
||||
let padding: number = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
||||
switch (type) {
|
||||
case nodeType.ROUNDED_RECT:
|
||||
case nodeType.RECT:
|
||||
case nodeType.HEXAGON:
|
||||
padding *= 2;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: cnt++,
|
||||
nodeId: sanitizeText(id, conf),
|
||||
level,
|
||||
descr: sanitizeText(descr, conf),
|
||||
type,
|
||||
children: [],
|
||||
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
||||
padding,
|
||||
} satisfies MindmapNode;
|
||||
|
||||
const parent = getParent(level);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
// Keep all nodes in the list
|
||||
nodes.push(node);
|
||||
} else {
|
||||
if (nodes.length === 0) {
|
||||
// First node, the root
|
||||
nodes.push(node);
|
||||
} else {
|
||||
// Syntax error ... there can only bee one root
|
||||
throw new Error(
|
||||
'There can be only one root. No parent could be found for ("' + node.descr + '")'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const nodeType = {
|
||||
DEFAULT: 0,
|
||||
NO_BORDER: 0,
|
||||
@@ -78,82 +14,149 @@ const nodeType = {
|
||||
CLOUD: 4,
|
||||
BANG: 5,
|
||||
HEXAGON: 6,
|
||||
};
|
||||
|
||||
const getType = (startStr: string, endStr: string): number => {
|
||||
log.debug('In get type', startStr, endStr);
|
||||
switch (startStr) {
|
||||
case '[':
|
||||
return nodeType.RECT;
|
||||
case '(':
|
||||
return endStr === ')' ? nodeType.ROUNDED_RECT : nodeType.CLOUD;
|
||||
case '((':
|
||||
return nodeType.CIRCLE;
|
||||
case ')':
|
||||
return nodeType.CLOUD;
|
||||
case '))':
|
||||
return nodeType.BANG;
|
||||
case '{{':
|
||||
return nodeType.HEXAGON;
|
||||
default:
|
||||
return nodeType.DEFAULT;
|
||||
}
|
||||
};
|
||||
|
||||
const setElementForId = (id: number, element: D3Element) => {
|
||||
elements[id] = element;
|
||||
};
|
||||
|
||||
const decorateNode = (decoration?: { class?: string; icon?: string }) => {
|
||||
if (!decoration) {
|
||||
return;
|
||||
}
|
||||
const config = getConfig();
|
||||
const node = nodes[nodes.length - 1];
|
||||
if (decoration.icon) {
|
||||
node.icon = sanitizeText(decoration.icon, config);
|
||||
}
|
||||
if (decoration.class) {
|
||||
node.class = sanitizeText(decoration.class, config);
|
||||
}
|
||||
};
|
||||
|
||||
const type2Str = (type: number) => {
|
||||
switch (type) {
|
||||
case nodeType.DEFAULT:
|
||||
return 'no-border';
|
||||
case nodeType.RECT:
|
||||
return 'rect';
|
||||
case nodeType.ROUNDED_RECT:
|
||||
return 'rounded-rect';
|
||||
case nodeType.CIRCLE:
|
||||
return 'circle';
|
||||
case nodeType.CLOUD:
|
||||
return 'cloud';
|
||||
case nodeType.BANG:
|
||||
return 'bang';
|
||||
case nodeType.HEXAGON:
|
||||
return 'hexgon'; // cspell: disable-line
|
||||
default:
|
||||
return 'no-border';
|
||||
}
|
||||
};
|
||||
|
||||
// Expose logger to grammar
|
||||
const getLogger = () => log;
|
||||
const getElementById = (id: number) => elements[id];
|
||||
|
||||
const db = {
|
||||
clear,
|
||||
addNode,
|
||||
getMindmap,
|
||||
nodeType,
|
||||
getType,
|
||||
setElementForId,
|
||||
decorateNode,
|
||||
type2Str,
|
||||
getLogger,
|
||||
getElementById,
|
||||
} as const;
|
||||
|
||||
export default db;
|
||||
export class MindmapDB {
|
||||
private nodes: MindmapNode[] = [];
|
||||
private count = 0;
|
||||
private elements: Record<number, D3Element> = {};
|
||||
public readonly nodeType: typeof nodeType;
|
||||
|
||||
constructor() {
|
||||
this.getLogger = this.getLogger.bind(this);
|
||||
this.nodeType = nodeType;
|
||||
this.clear();
|
||||
this.getType = this.getType.bind(this);
|
||||
this.getMindmap = this.getMindmap.bind(this);
|
||||
this.getElementById = this.getElementById.bind(this);
|
||||
this.getParent = this.getParent.bind(this);
|
||||
this.getMindmap = this.getMindmap.bind(this);
|
||||
this.addNode = this.addNode.bind(this);
|
||||
this.decorateNode = this.decorateNode.bind(this);
|
||||
}
|
||||
public clear() {
|
||||
this.nodes = [];
|
||||
this.count = 0;
|
||||
this.elements = {};
|
||||
}
|
||||
|
||||
public getParent(level: number): MindmapNode | null {
|
||||
for (let i = this.nodes.length - 1; i >= 0; i--) {
|
||||
if (this.nodes[i].level < level) {
|
||||
return this.nodes[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getMindmap(): MindmapNode | null {
|
||||
return this.nodes.length > 0 ? this.nodes[0] : null;
|
||||
}
|
||||
|
||||
public addNode(level: number, id: string, descr: string, type: number): void {
|
||||
log.info('addNode', level, id, descr, type);
|
||||
|
||||
const conf = getConfig();
|
||||
let padding = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
||||
|
||||
switch (type) {
|
||||
case this.nodeType.ROUNDED_RECT:
|
||||
case this.nodeType.RECT:
|
||||
case this.nodeType.HEXAGON:
|
||||
padding *= 2;
|
||||
break;
|
||||
}
|
||||
|
||||
const node: MindmapNode = {
|
||||
id: this.count++,
|
||||
nodeId: sanitizeText(id, conf),
|
||||
level,
|
||||
descr: sanitizeText(descr, conf),
|
||||
type,
|
||||
children: [],
|
||||
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
||||
padding,
|
||||
};
|
||||
|
||||
const parent = this.getParent(level);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
this.nodes.push(node);
|
||||
} else {
|
||||
if (this.nodes.length === 0) {
|
||||
this.nodes.push(node);
|
||||
} else {
|
||||
throw new Error(
|
||||
`There can be only one root. No parent could be found for ("${node.descr}")`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getType(startStr: string, endStr: string) {
|
||||
log.debug('In get type', startStr, endStr);
|
||||
switch (startStr) {
|
||||
case '[':
|
||||
return this.nodeType.RECT;
|
||||
case '(':
|
||||
return endStr === ')' ? this.nodeType.ROUNDED_RECT : this.nodeType.CLOUD;
|
||||
case '((':
|
||||
return this.nodeType.CIRCLE;
|
||||
case ')':
|
||||
return this.nodeType.CLOUD;
|
||||
case '))':
|
||||
return this.nodeType.BANG;
|
||||
case '{{':
|
||||
return this.nodeType.HEXAGON;
|
||||
default:
|
||||
return this.nodeType.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
public setElementForId(id: number, element: D3Element): void {
|
||||
this.elements[id] = element;
|
||||
}
|
||||
public getElementById(id: number) {
|
||||
return this.elements[id];
|
||||
}
|
||||
|
||||
public decorateNode(decoration?: { class?: string; icon?: string }): void {
|
||||
if (!decoration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = getConfig();
|
||||
const node = this.nodes[this.nodes.length - 1];
|
||||
if (decoration.icon) {
|
||||
node.icon = sanitizeText(decoration.icon, config);
|
||||
}
|
||||
if (decoration.class) {
|
||||
node.class = sanitizeText(decoration.class, config);
|
||||
}
|
||||
}
|
||||
|
||||
type2Str(type: number): string {
|
||||
switch (type) {
|
||||
case this.nodeType.DEFAULT:
|
||||
return 'no-border';
|
||||
case this.nodeType.RECT:
|
||||
return 'rect';
|
||||
case this.nodeType.ROUNDED_RECT:
|
||||
return 'rounded-rect';
|
||||
case this.nodeType.CIRCLE:
|
||||
return 'circle';
|
||||
case this.nodeType.CLOUD:
|
||||
return 'cloud';
|
||||
case this.nodeType.BANG:
|
||||
return 'bang';
|
||||
case this.nodeType.HEXAGON:
|
||||
return 'hexgon'; // cspell: disable-line
|
||||
default:
|
||||
return 'no-border';
|
||||
}
|
||||
}
|
||||
|
||||
public getLogger() {
|
||||
return log;
|
||||
}
|
||||
}
|
||||
|
@@ -9,10 +9,10 @@ import { log } from '../../logger.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import type { FilledMindMapNode, MindmapDB, MindmapNode } from './mindmapTypes.js';
|
||||
import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js';
|
||||
import { drawNode, positionNode } from './svgDraw.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
|
||||
import type { MindmapDB } from './mindmapDb.js';
|
||||
// Inject the layout algorithm into cytoscape
|
||||
cytoscape.use(coseBilkent);
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import type { RequiredDeep } from 'type-fest';
|
||||
import type mindmapDb from './mindmapDb.js';
|
||||
|
||||
export interface MindmapNode {
|
||||
id: number;
|
||||
@@ -19,4 +18,3 @@ export interface MindmapNode {
|
||||
}
|
||||
|
||||
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
||||
export type MindmapDB = typeof mindmapDb;
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import type { FilledMindMapNode, MindmapDB } from './mindmapTypes.js';
|
||||
import type { FilledMindMapNode } from './mindmapTypes.js';
|
||||
import type { Point, D3Element } from '../../types.js';
|
||||
import { parseFontSize } from '../../utils.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import type { MindmapDB } from './mindmapDb.js';
|
||||
|
||||
const MAX_SECTIONS = 12;
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { PacketDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
@@ -11,49 +12,42 @@ import {
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { PacketDB, PacketData, PacketWord } from './types.js';
|
||||
|
||||
const defaultPacketData: PacketData = {
|
||||
packet: [],
|
||||
};
|
||||
|
||||
let data: PacketData = structuredClone(defaultPacketData);
|
||||
|
||||
import type { PacketWord } from './types.js';
|
||||
const DEFAULT_PACKET_CONFIG: Required<PacketDiagramConfig> = DEFAULT_CONFIG.packet;
|
||||
|
||||
const getConfig = (): Required<PacketDiagramConfig> => {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_PACKET_CONFIG,
|
||||
...commonGetConfig().packet,
|
||||
});
|
||||
if (config.showBits) {
|
||||
config.paddingY += 10;
|
||||
export class PacketDB implements DiagramDB {
|
||||
private packet: PacketWord[] = [];
|
||||
|
||||
public getConfig() {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_PACKET_CONFIG,
|
||||
...commonGetConfig().packet,
|
||||
});
|
||||
if (config.showBits) {
|
||||
config.paddingY += 10;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
const getPacket = (): PacketWord[] => data.packet;
|
||||
|
||||
const pushWord = (word: PacketWord) => {
|
||||
if (word.length > 0) {
|
||||
data.packet.push(word);
|
||||
public getPacket() {
|
||||
return this.packet;
|
||||
}
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
commonClear();
|
||||
data = structuredClone(defaultPacketData);
|
||||
};
|
||||
public pushWord(word: PacketWord) {
|
||||
if (word.length > 0) {
|
||||
this.packet.push(word);
|
||||
}
|
||||
}
|
||||
|
||||
export const db: PacketDB = {
|
||||
pushWord,
|
||||
getPacket,
|
||||
getConfig,
|
||||
clear,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
};
|
||||
public clear() {
|
||||
commonClear();
|
||||
this.packet = [];
|
||||
}
|
||||
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setAccDescription = setAccDescription;
|
||||
}
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { db } from './db.js';
|
||||
import { PacketDB } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
import { renderer } from './renderer.js';
|
||||
import { styles } from './styles.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new PacketDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
@@ -1,24 +1,26 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { db } from './db.js';
|
||||
import { PacketDB } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
|
||||
const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db;
|
||||
|
||||
describe('packet diagrams', () => {
|
||||
let db: PacketDB;
|
||||
beforeEach(() => {
|
||||
clear();
|
||||
db = new PacketDB();
|
||||
if (parser.parser) {
|
||||
parser.parser.yy = db;
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle a packet-beta definition', async () => {
|
||||
const str = `packet-beta`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot('[]');
|
||||
expect(db.getPacket()).toMatchInlineSnapshot('[]');
|
||||
});
|
||||
|
||||
it('should handle a packet definition', async () => {
|
||||
const str = `packet`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot('[]');
|
||||
expect(db.getPacket()).toMatchInlineSnapshot('[]');
|
||||
});
|
||||
|
||||
it('should handle diagram with data and title', async () => {
|
||||
@@ -29,10 +31,10 @@ describe('packet diagrams', () => {
|
||||
0-10: "test"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
|
||||
expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
|
||||
expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
|
||||
expect(db.getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
|
||||
expect(db.getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -52,7 +54,7 @@ describe('packet diagrams', () => {
|
||||
11: "single"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -78,7 +80,7 @@ describe('packet diagrams', () => {
|
||||
+16: "word"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -104,7 +106,7 @@ describe('packet diagrams', () => {
|
||||
+16: "word"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -130,7 +132,7 @@ describe('packet diagrams', () => {
|
||||
11-90: "multiple"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
@@ -172,7 +174,7 @@ describe('packet diagrams', () => {
|
||||
17-63: "multiple"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
|
@@ -3,12 +3,12 @@ import { parse } from '@mermaid-js/parser';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import { db } from './db.js';
|
||||
import { PacketDB } from './db.js';
|
||||
import type { PacketBlock, PacketWord } from './types.js';
|
||||
|
||||
const maxPacketSize = 10_000;
|
||||
|
||||
const populate = (ast: Packet) => {
|
||||
const populate = (ast: Packet, db: PacketDB) => {
|
||||
populateCommonDb(ast, db);
|
||||
let lastBit = -1;
|
||||
let word: PacketWord = [];
|
||||
@@ -91,9 +91,17 @@ const getNextFittingBlock = (
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
// @ts-expect-error - PacketDB is not assignable to DiagramDB
|
||||
parser: { yy: undefined },
|
||||
parse: async (input: string): Promise<void> => {
|
||||
const ast: Packet = await parse('packet', input);
|
||||
const db = parser.parser?.yy;
|
||||
if (!(db instanceof PacketDB)) {
|
||||
throw new Error(
|
||||
'parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||
);
|
||||
}
|
||||
log.debug(ast);
|
||||
populate(ast);
|
||||
populate(ast, db);
|
||||
},
|
||||
};
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
|
||||
import { isLabelStyle } from '../../rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
|
||||
|
||||
import type { TreemapDiagramConfig, TreemapNode } from './types.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
import { isLabelStyle } from '../../rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
@@ -14,99 +14,82 @@ import {
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { TreemapDB, TreemapData, TreemapDiagramConfig, TreemapNode } from './types.js';
|
||||
export class TreeMapDB implements DiagramDB {
|
||||
private nodes: TreemapNode[] = [];
|
||||
private levels: Map<TreemapNode, number> = new Map<TreemapNode, number>();
|
||||
private outerNodes: TreemapNode[] = [];
|
||||
private classes: Map<string, DiagramStyleClassDef> = new Map<string, DiagramStyleClassDef>();
|
||||
private root?: TreemapNode;
|
||||
|
||||
const defaultTreemapData: TreemapData = {
|
||||
nodes: [],
|
||||
levels: new Map(),
|
||||
outerNodes: [],
|
||||
classes: new Map(),
|
||||
};
|
||||
|
||||
const state = new ImperativeState<TreemapData>(() => structuredClone(defaultTreemapData));
|
||||
|
||||
const getConfig = (): Required<TreemapDiagramConfig> => {
|
||||
// Use type assertion with unknown as intermediate step
|
||||
const defaultConfig = DEFAULT_CONFIG as unknown as { treemap: Required<TreemapDiagramConfig> };
|
||||
const userConfig = commonGetConfig() as unknown as { treemap?: Partial<TreemapDiagramConfig> };
|
||||
|
||||
return cleanAndMerge({
|
||||
...defaultConfig.treemap,
|
||||
...(userConfig.treemap ?? {}),
|
||||
}) as Required<TreemapDiagramConfig>;
|
||||
};
|
||||
|
||||
const getNodes = (): TreemapNode[] => state.records.nodes;
|
||||
|
||||
const addNode = (node: TreemapNode, level: number) => {
|
||||
const data = state.records;
|
||||
data.nodes.push(node);
|
||||
data.levels.set(node, level);
|
||||
|
||||
if (level === 0) {
|
||||
data.outerNodes.push(node);
|
||||
public getNodes() {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
// Set the root node if this is a level 0 node and we don't have a root yet
|
||||
if (level === 0 && !data.root) {
|
||||
data.root = node;
|
||||
public getConfig() {
|
||||
const defaultConfig = DEFAULT_CONFIG as unknown as { treemap: Required<TreemapDiagramConfig> };
|
||||
const userConfig = commonGetConfig() as unknown as { treemap?: Partial<TreemapDiagramConfig> };
|
||||
return cleanAndMerge({
|
||||
...defaultConfig.treemap,
|
||||
...(userConfig.treemap ?? {}),
|
||||
}) as Required<TreemapDiagramConfig>;
|
||||
}
|
||||
};
|
||||
|
||||
const getRoot = (): TreemapNode | undefined => ({ name: '', children: state.records.outerNodes });
|
||||
public addNode(node: TreemapNode, level: number) {
|
||||
this.nodes.push(node);
|
||||
this.levels.set(node, level);
|
||||
if (level === 0) {
|
||||
this.outerNodes.push(node);
|
||||
this.root ??= node;
|
||||
}
|
||||
}
|
||||
|
||||
const addClass = (id: string, _style: string) => {
|
||||
const classes = state.records.classes;
|
||||
const styleClass = classes.get(id) ?? { id, styles: [], textStyles: [] };
|
||||
classes.set(id, styleClass);
|
||||
public getRoot() {
|
||||
return { name: '', children: this.outerNodes };
|
||||
}
|
||||
|
||||
const styles = _style.replace(/\\,/g, '§§§').replace(/,/g, ';').replace(/§§§/g, ',').split(';');
|
||||
|
||||
if (styles) {
|
||||
styles.forEach((s) => {
|
||||
if (isLabelStyle(s)) {
|
||||
if (styleClass?.textStyles) {
|
||||
styleClass.textStyles.push(s);
|
||||
} else {
|
||||
styleClass.textStyles = [s];
|
||||
public addClass(id: string, _style: string) {
|
||||
const styleClass = this.classes.get(id) ?? { id, styles: [], textStyles: [] };
|
||||
const styles = _style.replace(/\\,/g, '§§§').replace(/,/g, ';').replace(/§§§/g, ',').split(';');
|
||||
if (styles) {
|
||||
styles.forEach((s) => {
|
||||
if (isLabelStyle(s)) {
|
||||
if (styleClass?.textStyles) {
|
||||
styleClass.textStyles.push(s);
|
||||
} else {
|
||||
styleClass.textStyles = [s];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (styleClass?.styles) {
|
||||
styleClass.styles.push(s);
|
||||
} else {
|
||||
styleClass.styles = [s];
|
||||
}
|
||||
});
|
||||
if (styleClass?.styles) {
|
||||
styleClass.styles.push(s);
|
||||
} else {
|
||||
styleClass.styles = [s];
|
||||
}
|
||||
});
|
||||
}
|
||||
this.classes.set(id, styleClass);
|
||||
}
|
||||
|
||||
classes.set(id, styleClass);
|
||||
};
|
||||
const getClasses = (): Map<string, DiagramStyleClassDef> => {
|
||||
return state.records.classes;
|
||||
};
|
||||
public getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
const getStylesForClass = (classSelector: string): string[] => {
|
||||
return state.records.classes.get(classSelector)?.styles ?? [];
|
||||
};
|
||||
public getStylesForClass(classSelector: string): string[] {
|
||||
return this.classes.get(classSelector)?.styles ?? [];
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
commonClear();
|
||||
state.reset();
|
||||
};
|
||||
public clear() {
|
||||
commonClear();
|
||||
this.nodes = [];
|
||||
this.levels = new Map();
|
||||
this.outerNodes = [];
|
||||
this.classes = new Map();
|
||||
this.root = undefined;
|
||||
}
|
||||
|
||||
export const db: TreemapDB = {
|
||||
getNodes,
|
||||
addNode,
|
||||
getRoot,
|
||||
getConfig,
|
||||
clear,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
addClass,
|
||||
getClasses,
|
||||
getStylesForClass,
|
||||
};
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setAccDescription = setAccDescription;
|
||||
}
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { db } from './db.js';
|
||||
import { TreeMapDB } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
import { renderer } from './renderer.js';
|
||||
import styles from './styles.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new TreeMapDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
@@ -2,15 +2,15 @@ import { parse } from '@mermaid-js/parser';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import { db } from './db.js';
|
||||
import type { TreemapNode, TreemapAst } from './types.js';
|
||||
import type { TreemapNode, TreemapAst, TreemapDB } from './types.js';
|
||||
import { buildHierarchy } from './utils.js';
|
||||
import { TreeMapDB } from './db.js';
|
||||
|
||||
/**
|
||||
* Populates the database with data from the Treemap AST
|
||||
* @param ast - The Treemap AST
|
||||
*/
|
||||
const populate = (ast: TreemapAst) => {
|
||||
const populate = (ast: TreemapAst, db: TreemapDB) => {
|
||||
// We need to bypass the type checking for populateCommonDb
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
populateCommonDb(ast as any, db);
|
||||
@@ -84,6 +84,8 @@ const getItemName = (item: { name?: string | number }): string => {
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
// @ts-expect-error - TreeMapDB is not assignable to DiagramDB
|
||||
parser: { yy: undefined },
|
||||
parse: async (text: string): Promise<void> => {
|
||||
try {
|
||||
// Use a generic parse that accepts any diagram type
|
||||
@@ -91,7 +93,13 @@ export const parser: ParserDefinition = {
|
||||
const parseFunc = parse as (diagramType: string, text: string) => Promise<TreemapAst>;
|
||||
const ast = await parseFunc('treemap', text);
|
||||
log.debug('Treemap AST:', ast);
|
||||
populate(ast);
|
||||
const db = parser.parser?.yy;
|
||||
if (!(db instanceof TreeMapDB)) {
|
||||
throw new Error(
|
||||
'parser.parser?.yy was not a TreemapDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||
);
|
||||
}
|
||||
populate(ast, db);
|
||||
} catch (error) {
|
||||
log.error('Error parsing treemap:', error);
|
||||
throw error;
|
||||
|
38
packages/mermaid/src/diagrams/usecase/db.ts
Normal file
38
packages/mermaid/src/diagrams/usecase/db.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { UsecaseDiagramConfig, UsecaseNode } from './types.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
|
||||
export class UsecaseDiagramDB implements DiagramDB {
|
||||
public getNodes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
public getConfig() {
|
||||
return cleanAndMerge({}) as Required<UsecaseDiagramConfig>;
|
||||
}
|
||||
|
||||
public addNode(node: UsecaseNode, level: number) {
|
||||
if (level === 0) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
public getRoot() {
|
||||
return { name: '', children: [] };
|
||||
}
|
||||
|
||||
public addClass(_id: string, _style: string) {
|
||||
// TODO
|
||||
}
|
||||
public getClasses() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public getStylesForClass(_classSelector: string) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public clear() {
|
||||
// commonClear();
|
||||
}
|
||||
}
|
22
packages/mermaid/src/diagrams/usecase/detector.ts
Normal file
22
packages/mermaid/src/diagrams/usecase/detector.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'usecase';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*usecase/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./diagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
export const usecase: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
14
packages/mermaid/src/diagrams/usecase/diagram.ts
Normal file
14
packages/mermaid/src/diagrams/usecase/diagram.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { UsecaseDiagramDB } 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 UsecaseDiagramDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
40
packages/mermaid/src/diagrams/usecase/parser.ts
Normal file
40
packages/mermaid/src/diagrams/usecase/parser.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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 { UsecaseAst } from './types.js';
|
||||
import { UsecaseDiagramDB } from './db.js';
|
||||
|
||||
/**
|
||||
* Populates the database with data from the Usecase AST
|
||||
* @param ast - The Usecase AST
|
||||
*/
|
||||
const populate = (ast: UsecaseAst, db: UsecaseDiagramDB) => {
|
||||
// We need to bypass the type checking for populateCommonDb
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
populateCommonDb(ast as any, db);
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
// @ts-expect-error - UsecaseDB is not assignable to DiagramDB
|
||||
parser: { yy: undefined },
|
||||
parse: async (text: string): Promise<void> => {
|
||||
try {
|
||||
// Use a generic parse that accepts any diagram type
|
||||
|
||||
const parseFunc = parse as (diagramType: string, text: string) => Promise<UsecaseAst>;
|
||||
const ast = await parseFunc('usecase', text);
|
||||
log.debug('Usecase AST:', ast);
|
||||
const db = parser.parser?.yy;
|
||||
if (!(db instanceof UsecaseDiagramDB)) {
|
||||
throw new Error(
|
||||
'parser.parser?.yy was not a UsecaseDiagramDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||
);
|
||||
}
|
||||
populate(ast, db);
|
||||
} catch (error) {
|
||||
log.error('Error parsing usecase:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
21
packages/mermaid/src/diagrams/usecase/renderer.ts
Normal file
21
packages/mermaid/src/diagrams/usecase/renderer.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import type {
|
||||
DiagramRenderer,
|
||||
DiagramStyleClassDef,
|
||||
DrawDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
/**
|
||||
* Draws the Usecase diagram
|
||||
*/
|
||||
const draw: DrawDefinition = (_text, _id, _version, _diagram: Diagram) => {
|
||||
// TODO: Implement the draw function for the usecase diagram
|
||||
};
|
||||
|
||||
const getClasses = function (
|
||||
_text: string,
|
||||
_diagramObj: Pick<Diagram, 'db'>
|
||||
): Map<string, DiagramStyleClassDef> {
|
||||
return new Map<string, DiagramStyleClassDef>();
|
||||
};
|
||||
export const renderer: DiagramRenderer = { draw, getClasses };
|
51
packages/mermaid/src/diagrams/usecase/styles.ts
Normal file
51
packages/mermaid/src/diagrams/usecase/styles.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import type { UsecaseStyleOptions } from './types.js';
|
||||
|
||||
const defaultUsecaseStyleOptions: UsecaseStyleOptions = {
|
||||
sectionStrokeColor: 'black',
|
||||
sectionStrokeWidth: '1',
|
||||
sectionFillColor: '#efefef',
|
||||
leafStrokeColor: 'black',
|
||||
leafStrokeWidth: '1',
|
||||
leafFillColor: '#efefef',
|
||||
labelColor: 'black',
|
||||
labelFontSize: '12px',
|
||||
valueFontSize: '10px',
|
||||
valueColor: 'black',
|
||||
titleColor: 'black',
|
||||
titleFontSize: '14px',
|
||||
};
|
||||
|
||||
export const getStyles: DiagramStylesProvider = ({
|
||||
usecase,
|
||||
}: { usecase?: UsecaseStyleOptions } = {}) => {
|
||||
const options = cleanAndMerge(defaultUsecaseStyleOptions, usecase);
|
||||
|
||||
return `
|
||||
.usecaseNode.section {
|
||||
stroke: ${options.sectionStrokeColor};
|
||||
stroke-width: ${options.sectionStrokeWidth};
|
||||
fill: ${options.sectionFillColor};
|
||||
}
|
||||
.usecaseNode.leaf {
|
||||
stroke: ${options.leafStrokeColor};
|
||||
stroke-width: ${options.leafStrokeWidth};
|
||||
fill: ${options.leafFillColor};
|
||||
}
|
||||
.usecaseLabel {
|
||||
fill: ${options.labelColor};
|
||||
font-size: ${options.labelFontSize};
|
||||
}
|
||||
.usecaseValue {
|
||||
fill: ${options.valueColor};
|
||||
font-size: ${options.valueFontSize};
|
||||
}
|
||||
.usecaseTitle {
|
||||
fill: ${options.titleColor};
|
||||
font-size: ${options.titleFontSize};
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
export default getStyles;
|
68
packages/mermaid/src/diagrams/usecase/types.ts
Normal file
68
packages/mermaid/src/diagrams/usecase/types.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { DiagramDBBase, DiagramStyleClassDef } from '../../diagram-api/types.js';
|
||||
import type { BaseDiagramConfig } from '../../config.type.js';
|
||||
|
||||
export interface UsecaseNode {
|
||||
name: string;
|
||||
children?: UsecaseNode[];
|
||||
value?: number;
|
||||
parent?: UsecaseNode;
|
||||
classSelector?: string;
|
||||
cssCompiledStyles?: string[];
|
||||
}
|
||||
|
||||
export interface UsecaseDiagramDB extends DiagramDBBase<UsecaseDiagramConfig> {
|
||||
getNodes: () => UsecaseNode[];
|
||||
addNode: (node: UsecaseNode, level: number) => void;
|
||||
getRoot: () => UsecaseNode | undefined;
|
||||
getClasses: () => Map<string, DiagramStyleClassDef>;
|
||||
addClass: (className: string, style: string) => void;
|
||||
getStylesForClass: (classSelector: string) => string[];
|
||||
// Update
|
||||
}
|
||||
|
||||
export interface UsecaseStyleOptions {
|
||||
sectionStrokeColor?: string;
|
||||
sectionStrokeWidth?: string;
|
||||
sectionFillColor?: string;
|
||||
leafStrokeColor?: string;
|
||||
leafStrokeWidth?: string;
|
||||
leafFillColor?: string;
|
||||
labelColor?: string;
|
||||
labelFontSize?: string;
|
||||
valueFontSize?: string;
|
||||
valueColor?: string;
|
||||
titleColor?: string;
|
||||
titleFontSize?: string;
|
||||
}
|
||||
|
||||
export interface UsecaseData {
|
||||
nodes: UsecaseNode[];
|
||||
levels: Map<UsecaseNode, number>;
|
||||
root?: UsecaseNode;
|
||||
outerNodes: UsecaseNode[];
|
||||
}
|
||||
|
||||
export interface UsecaseItem {
|
||||
$type: string;
|
||||
name: string;
|
||||
value?: number;
|
||||
classSelector?: string;
|
||||
}
|
||||
|
||||
export interface UsecaseAst {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// Define the UsecaseDiagramConfig interface
|
||||
export interface UsecaseDiagramConfig extends BaseDiagramConfig {
|
||||
padding?: number;
|
||||
diagramPadding?: number;
|
||||
showValues?: boolean;
|
||||
nodeWidth?: number;
|
||||
nodeHeight?: number;
|
||||
borderWidth?: number;
|
||||
valueFontSize?: number;
|
||||
labelFontSize?: number;
|
||||
valueFormat?: string;
|
||||
}
|
@@ -302,7 +302,7 @@ If you are adding a feature, you will definitely need to add tests. Depending on
|
||||
|
||||
Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run.
|
||||
|
||||
Unit tests are mandatory for all code except the renderers. (The renderers are tested with integration tests.)
|
||||
Unit tests are mandatory for all code except the layout tests. (The layouts are tested with integration tests.)
|
||||
|
||||
We use [Vitest](https://vitest.dev) to run unit tests.
|
||||
|
||||
@@ -328,6 +328,30 @@ When using Docker prepend your command with `./run`:
|
||||
./run pnpm test
|
||||
```
|
||||
|
||||
##### Testing the DOM
|
||||
|
||||
One can use `jsdomIt` to test any part of Mermaid that interacts with the DOM, as long as it is not related to the layout.
|
||||
|
||||
The function `jsdomIt` ([developed in utils.ts](../../tests/util.ts)) overrides `it` from `vitest`, and creates a pseudo-browser environment that works almost like the real deal for the duration of the test. It uses JSDOM to create a DOM, and adds objects `window` and `document` to `global` to mock the browser environment.
|
||||
|
||||
> [!NOTE]
|
||||
> The layout cannot work in `jsdomIt` tests because JSDOM has no rendering engine, hence the pseudo-browser environment.
|
||||
|
||||
Example :
|
||||
|
||||
```typescript
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
|
||||
jsdomIt('should add element "thing" in the SVG', ({ svg }) => {
|
||||
// Code in this block runs in a pseudo-browser environment
|
||||
addThing(svg); // The svg item is the D3 selection of the SVG node
|
||||
const svgNode = ensureNodeFromSelector('svg'); // Retrieve the DOM node using the DOM API
|
||||
expect(svgNode.querySelector('thing')).not.toBeNull(); // Test the structure of the SVG
|
||||
});
|
||||
```
|
||||
|
||||
They can be used to test any method that interacts with the DOM, including for testing renderers. For renderers, additional integration testing is necessary to test the layout though.
|
||||
|
||||
#### Integration / End-to-End (E2E) Tests
|
||||
|
||||
These test the rendering and visual appearance of the diagrams.
|
||||
|
189
packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd
Normal file
189
packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDiagram.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDb.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/styles.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/types.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/flowChartShapes.js"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/flowchart/elk/detector.ts"
|
||||
generationTime: 2025-07-23T10:31:53.266Z
|
||||
---
|
||||
flowchart TD
|
||||
%% Entry Points and Detection
|
||||
Input["User Input Text"] --> Detection{Detection Phase}
|
||||
|
||||
Detection --> flowDetector["flowDetector.ts<br/>detector(txt, config)"]
|
||||
Detection --> flowDetectorV2["flowDetector-v2.ts<br/>detector(txt, config)"]
|
||||
Detection --> elkDetector["elk/detector.ts<br/>detector(txt, config)"]
|
||||
|
||||
flowDetector --> |"Checks /^\s*graph/"| DetectLegacy{Legacy Flowchart?}
|
||||
flowDetectorV2 --> |"Checks /^\s*flowchart/"| DetectNew{New Flowchart?}
|
||||
elkDetector --> |"Checks /^\s*flowchart-elk/"| DetectElk{ELK Layout?}
|
||||
|
||||
DetectLegacy --> |Yes| LoadDiagram
|
||||
DetectNew --> |Yes| LoadDiagram
|
||||
DetectElk --> |Yes| LoadDiagram
|
||||
|
||||
%% Loading Phase
|
||||
LoadDiagram["loader() function"] --> flowDiagram["flowDiagram.ts<br/>diagram object"]
|
||||
|
||||
flowDiagram --> DiagramStructure{Diagram Components}
|
||||
DiagramStructure --> Parser["parser: flowParser"]
|
||||
DiagramStructure --> Database["db: new FlowDB()"]
|
||||
DiagramStructure --> Renderer["renderer: flowRenderer-v3-unified"]
|
||||
DiagramStructure --> Styles["styles: flowStyles"]
|
||||
DiagramStructure --> Init["init: (cnf: MermaidConfig)"]
|
||||
|
||||
%% Parser Phase
|
||||
Parser --> flowParser["parser/flowParser.ts<br/>newParser.parse(src)"]
|
||||
flowParser --> |"Preprocesses src"| RemoveWhitespace["Remove trailing whitespace<br/>src.replace(/}\s*\n/g, '}\n')"]
|
||||
RemoveWhitespace --> flowJison["parser/flow.jison<br/>flowJisonParser.parse(newSrc)"]
|
||||
|
||||
flowJison --> ParseGraph["Parse Graph Structure"]
|
||||
ParseGraph --> ParseVertices["Parse Vertices"]
|
||||
ParseGraph --> ParseEdges["Parse Edges"]
|
||||
ParseGraph --> ParseSubgraphs["Parse Subgraphs"]
|
||||
ParseGraph --> ParseClasses["Parse Classes"]
|
||||
ParseGraph --> ParseStyles["Parse Styles"]
|
||||
|
||||
%% Database Phase - FlowDB Class
|
||||
Database --> FlowDBClass["flowDb.ts<br/>FlowDB class"]
|
||||
|
||||
FlowDBClass --> DBInit["constructor()<br/>- Initialize counters<br/>- Bind methods<br/>- Setup toolTips<br/>- Call clear()"]
|
||||
|
||||
DBInit --> DBMethods{FlowDB Methods}
|
||||
|
||||
DBMethods --> addVertex["addVertex(id, textObj, type, style,<br/>classes, dir, props, metadata)"]
|
||||
DBMethods --> addLink["addLink(_start[], _end[], linkData)"]
|
||||
DBMethods --> addSingleLink["addSingleLink(_start, _end, type, id)"]
|
||||
DBMethods --> setDirection["setDirection(dir)"]
|
||||
DBMethods --> addSubGraph["addSubGraph(nodes[], id, title)"]
|
||||
DBMethods --> addClass["addClass(id, style)"]
|
||||
DBMethods --> setClass["setClass(ids, className)"]
|
||||
DBMethods --> setTooltip["setTooltip(ids, tooltip)"]
|
||||
DBMethods --> setClickEvent["setClickEvent(id, functionName, args)"]
|
||||
DBMethods --> setClickFun["setClickFun(id, functionName, args)"]
|
||||
|
||||
%% Vertex Processing
|
||||
addVertex --> VertexProcess{Vertex Processing}
|
||||
VertexProcess --> CreateVertex["Create FlowVertex object<br/>- id, labelType, domId<br/>- styles[], classes[]"]
|
||||
VertexProcess --> SanitizeText["sanitizeText(textObj.text)"]
|
||||
VertexProcess --> ParseMetadata["Parse YAML metadata<br/>yaml.load(yamlData)"]
|
||||
VertexProcess --> SetVertexProps["Set vertex properties<br/>- shape, label, icon, form<br/>- pos, img, constraint, w, h"]
|
||||
|
||||
%% Edge Processing
|
||||
addSingleLink --> EdgeProcess{Edge Processing}
|
||||
EdgeProcess --> CreateEdge["Create FlowEdge object<br/>- start, end, type, text<br/>- labelType, classes[]"]
|
||||
EdgeProcess --> ProcessLinkText["Process link text<br/>- sanitizeText()<br/>- strip quotes"]
|
||||
EdgeProcess --> SetEdgeProps["Set edge properties<br/>- type, stroke, length"]
|
||||
EdgeProcess --> GenerateEdgeId["Generate edge ID<br/>getEdgeId(start, end, counter)"]
|
||||
EdgeProcess --> ValidateEdgeLimit["Validate edge limit<br/>maxEdges check"]
|
||||
|
||||
%% Data Collection
|
||||
DBMethods --> GetData["getData()"]
|
||||
GetData --> CollectNodes["Collect nodes[] from vertices"]
|
||||
GetData --> CollectEdges["Collect edges[] from edges"]
|
||||
GetData --> ProcessSubGraphs["Process subgraphs<br/>- parentDB Map<br/>- subGraphDB Map"]
|
||||
GetData --> AddNodeFromVertex["addNodeFromVertex()<br/>for each vertex"]
|
||||
GetData --> ProcessEdgeTypes["destructEdgeType()<br/>arrowTypeStart, arrowTypeEnd"]
|
||||
|
||||
%% Node Creation
|
||||
AddNodeFromVertex --> NodeCreation{Node Creation}
|
||||
NodeCreation --> FindExistingNode["findNode(nodes, vertex.id)"]
|
||||
NodeCreation --> CreateBaseNode["Create base node<br/>- id, label, parentId<br/>- cssStyles, cssClasses<br/>- shape, domId, tooltip"]
|
||||
NodeCreation --> GetCompiledStyles["getCompiledStyles(classDefs)"]
|
||||
NodeCreation --> GetTypeFromVertex["getTypeFromVertex(vertex)"]
|
||||
|
||||
%% Rendering Phase
|
||||
Renderer --> flowRendererV3["flowRenderer-v3-unified.ts<br/>draw(text, id, version, diag)"]
|
||||
|
||||
flowRendererV3 --> RenderInit["Initialize rendering<br/>- getConfig()<br/>- handle securityLevel<br/>- getDiagramElement()"]
|
||||
|
||||
RenderInit --> GetLayoutData["diag.db.getData()<br/>as LayoutData"]
|
||||
GetLayoutData --> SetupLayoutData["Setup layout data<br/>- type, layoutAlgorithm<br/>- direction, spacing<br/>- markers, diagramId"]
|
||||
|
||||
SetupLayoutData --> CallRender["render(data4Layout, svg)"]
|
||||
CallRender --> SetupViewPort["setupViewPortForSVG(svg, padding)"]
|
||||
SetupViewPort --> ProcessLinks["Process vertex links<br/>- create anchor elements<br/>- handle click events"]
|
||||
|
||||
%% Shape Rendering
|
||||
CallRender --> ShapeSystem["flowChartShapes.js<br/>Shape Functions"]
|
||||
|
||||
ShapeSystem --> ShapeFunctions{Shape Functions}
|
||||
ShapeFunctions --> question["question(parent, bbox, node)"]
|
||||
ShapeFunctions --> hexagon["hexagon(parent, bbox, node)"]
|
||||
ShapeFunctions --> rect_left_inv_arrow["rect_left_inv_arrow(parent, bbox, node)"]
|
||||
ShapeFunctions --> lean_right["lean_right(parent, bbox, node)"]
|
||||
ShapeFunctions --> lean_left["lean_left(parent, bbox, node)"]
|
||||
|
||||
ShapeFunctions --> insertPolygonShape["insertPolygonShape(parent, w, h, points)"]
|
||||
ShapeFunctions --> intersectPolygon["intersectPolygon(node, points, point)"]
|
||||
ShapeFunctions --> intersectRect["intersectRect(node, point)"]
|
||||
|
||||
%% Styling System
|
||||
Styles --> stylesTS["styles.ts<br/>getStyles(options)"]
|
||||
stylesTS --> StyleOptions["FlowChartStyleOptions<br/>- arrowheadColor, border2<br/>- clusterBkg, mainBkg<br/>- fontFamily, textColor"]
|
||||
|
||||
StyleOptions --> GenerateCSS["Generate CSS styles<br/>- .label, .cluster-label<br/>- .node, .edgePath<br/>- .flowchart-link, .edgeLabel"]
|
||||
GenerateCSS --> GetIconStyles["getIconStyles()"]
|
||||
|
||||
%% Type System
|
||||
Parser --> TypeSystem["types.ts<br/>Type Definitions"]
|
||||
TypeSystem --> FlowVertex["FlowVertex interface"]
|
||||
TypeSystem --> FlowEdge["FlowEdge interface"]
|
||||
TypeSystem --> FlowClass["FlowClass interface"]
|
||||
TypeSystem --> FlowSubGraph["FlowSubGraph interface"]
|
||||
TypeSystem --> FlowVertexTypeParam["FlowVertexTypeParam<br/>Shape types"]
|
||||
|
||||
%% Utility Functions
|
||||
DBMethods --> UtilityFunctions{Utility Functions}
|
||||
UtilityFunctions --> lookUpDomId["lookUpDomId(id)"]
|
||||
UtilityFunctions --> getClasses["getClasses()"]
|
||||
UtilityFunctions --> getDirection["getDirection()"]
|
||||
UtilityFunctions --> getVertices["getVertices()"]
|
||||
UtilityFunctions --> getEdges["getEdges()"]
|
||||
UtilityFunctions --> getSubGraphs["getSubGraphs()"]
|
||||
UtilityFunctions --> clear["clear()"]
|
||||
UtilityFunctions --> defaultConfig["defaultConfig()"]
|
||||
|
||||
%% Event Handling
|
||||
ProcessLinks --> EventHandling{Event Handling}
|
||||
EventHandling --> setupToolTips["setupToolTips(element)"]
|
||||
EventHandling --> bindFunctions["bindFunctions(element)"]
|
||||
EventHandling --> runFunc["utils.runFunc(functionName, args)"]
|
||||
|
||||
%% Common Database Functions
|
||||
DBMethods --> CommonDB["commonDb.js functions"]
|
||||
CommonDB --> setAccTitle["setAccTitle()"]
|
||||
CommonDB --> getAccTitle["getAccTitle()"]
|
||||
CommonDB --> setAccDescription["setAccDescription()"]
|
||||
CommonDB --> getAccDescription["getAccDescription()"]
|
||||
CommonDB --> setDiagramTitle["setDiagramTitle()"]
|
||||
CommonDB --> getDiagramTitle["getDiagramTitle()"]
|
||||
CommonDB --> commonClear["clear()"]
|
||||
|
||||
%% Final Output
|
||||
ProcessLinks --> FinalSVG["Final SVG Output"]
|
||||
|
||||
%% Layout Algorithm Selection
|
||||
SetupLayoutData --> LayoutAlgorithm{Layout Algorithm}
|
||||
LayoutAlgorithm --> Dagre["dagre<br/>(default)"]
|
||||
LayoutAlgorithm --> DagreWrapper["dagre-wrapper<br/>(v2 renderer)"]
|
||||
LayoutAlgorithm --> ELK["elk<br/>(external package)"]
|
||||
|
||||
%% Testing Components
|
||||
FlowDBClass --> TestFiles["Test Files"]
|
||||
TestFiles --> flowDbSpec["flowDb.spec.ts"]
|
||||
TestFiles --> flowChartShapesSpec["flowChartShapes.spec.js"]
|
||||
TestFiles --> ParserTests["parser/*.spec.js files<br/>- flow-text.spec.js<br/>- flow-edges.spec.js<br/>- flow-style.spec.js<br/>- subgraph.spec.js"]
|
||||
|
||||
%% Configuration
|
||||
Init --> ConfigSetup["Configuration Setup"]
|
||||
ConfigSetup --> FlowchartConfig["cnf.flowchart config"]
|
||||
ConfigSetup --> ArrowMarkers["arrowMarkerAbsolute"]
|
||||
ConfigSetup --> LayoutConfig["layout config"]
|
||||
ConfigSetup --> SetConfig["setConfig() calls"]
|
307
packages/mermaid/src/docs/diagrams/mermaid-api-sequence.mmd
Normal file
307
packages/mermaid/src/docs/diagrams/mermaid-api-sequence.mmd
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/mermaidAPI.ts"
|
||||
- "File: /packages/mermaid/src/mermaid.ts"
|
||||
generationTime: 2025-01-28T16:30:00.000Z
|
||||
---
|
||||
sequenceDiagram
|
||||
participant User as User/Browser
|
||||
participant Mermaid as mermaid.ts
|
||||
participant Queue as executionQueue
|
||||
participant API as mermaidAPI.ts
|
||||
participant Config as configApi
|
||||
participant Preprocessor as preprocessDiagram
|
||||
participant DiagramAPI as diagram-api
|
||||
participant Diagram as Diagram.fromText
|
||||
participant Renderer as diagram.renderer
|
||||
participant Styles as Styling System
|
||||
participant DOM as DOM/SVG
|
||||
|
||||
Note over User, DOM: Mermaid Complete API Flow
|
||||
|
||||
%% Initialization Phase
|
||||
User->>+Mermaid: mermaid.initialize(config)
|
||||
Mermaid->>+API: mermaidAPI.initialize(config)
|
||||
|
||||
API->>API: assignWithDepth({}, userOptions)
|
||||
API->>API: handle legacy fontFamily config
|
||||
API->>Config: saveConfigFromInitialize(options)
|
||||
|
||||
alt Theme Configuration Available
|
||||
API->>API: check if theme in theme object
|
||||
API->>API: set themeVariables from theme
|
||||
else Default Theme
|
||||
API->>API: use default theme variables
|
||||
end
|
||||
|
||||
API->>Config: setSiteConfig(options) or getSiteConfig()
|
||||
API->>API: setLogLevel(config.logLevel)
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
|
||||
API-->>-Mermaid: initialization complete
|
||||
Mermaid-->>-User: ready to render
|
||||
|
||||
%% Content Loaded Event
|
||||
User->>DOM: document.load event
|
||||
DOM->>+Mermaid: contentLoaded()
|
||||
|
||||
opt startOnLoad is true
|
||||
Mermaid->>Config: getConfig()
|
||||
Config-->>Mermaid: { startOnLoad: true }
|
||||
Mermaid->>Mermaid: run()
|
||||
end
|
||||
|
||||
Mermaid-->>-DOM: event handling complete
|
||||
|
||||
%% Main Run Function
|
||||
User->>+Mermaid: mermaid.run(options)
|
||||
|
||||
Mermaid->>Mermaid: runThrowsErrors(options)
|
||||
Mermaid->>Config: getConfig()
|
||||
Config-->>Mermaid: configuration object
|
||||
|
||||
alt nodes provided
|
||||
Mermaid->>Mermaid: use provided nodes
|
||||
else querySelector provided
|
||||
Mermaid->>DOM: document.querySelectorAll(querySelector)
|
||||
DOM-->>Mermaid: nodesToProcess
|
||||
end
|
||||
|
||||
Mermaid->>Mermaid: new InitIDGenerator(deterministicIds, seed)
|
||||
|
||||
loop For each diagram element
|
||||
Mermaid->>DOM: check element.getAttribute('data-processed')
|
||||
|
||||
opt not processed
|
||||
Mermaid->>DOM: element.setAttribute('data-processed', 'true')
|
||||
Mermaid->>Mermaid: generate unique id
|
||||
Mermaid->>DOM: get element.innerHTML
|
||||
Mermaid->>Mermaid: entityDecode and clean text
|
||||
Mermaid->>Mermaid: detectInit(txt)
|
||||
|
||||
Mermaid->>Queue: render(id, txt, element)
|
||||
end
|
||||
end
|
||||
|
||||
Mermaid-->>-User: processing initiated
|
||||
|
||||
%% Render Function (Queued)
|
||||
activate Queue
|
||||
Queue->>+API: mermaidAPI.render(id, text, container)
|
||||
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
API->>+Preprocessor: processAndSetConfigs(text)
|
||||
|
||||
Preprocessor->>Preprocessor: preprocessDiagram(text)
|
||||
Preprocessor->>Config: reset()
|
||||
Preprocessor->>Config: addDirective(processed.config)
|
||||
Preprocessor-->>-API: { code, config }
|
||||
|
||||
API->>Config: getConfig()
|
||||
Config-->>API: current configuration
|
||||
|
||||
opt text length > maxTextSize
|
||||
API->>API: text = MAX_TEXTLENGTH_EXCEEDED_MSG
|
||||
end
|
||||
|
||||
API->>API: setup id selectors and element IDs
|
||||
API->>API: determine security level (sandbox/loose)
|
||||
|
||||
%% DOM Setup
|
||||
alt svgContainingElement provided
|
||||
alt isSandboxed
|
||||
API->>DOM: sandboxedIframe(select(svgContainingElement), iFrameID)
|
||||
API->>DOM: select iframe contentDocument body
|
||||
else
|
||||
API->>DOM: select(svgContainingElement)
|
||||
end
|
||||
else no container
|
||||
API->>API: removeExistingElements(document, id, divId, iFrameId)
|
||||
alt isSandboxed
|
||||
API->>DOM: sandboxedIframe(select('body'), iFrameID)
|
||||
else
|
||||
API->>DOM: select('body')
|
||||
end
|
||||
end
|
||||
|
||||
API->>DOM: appendDivSvgG(root, id, enclosingDivID, fontFamily, XMLNS_XLINK_STD)
|
||||
|
||||
%% Diagram Creation
|
||||
API->>+Diagram: Diagram.fromText(text, { title: processed.title })
|
||||
|
||||
Diagram->>DiagramAPI: detect diagram type
|
||||
Diagram->>DiagramAPI: load appropriate diagram
|
||||
Diagram-->>-API: diagram instance
|
||||
|
||||
opt parsing error occurred
|
||||
API->>+Diagram: Diagram.fromText('error')
|
||||
Diagram-->>-API: error diagram
|
||||
API->>API: store parseEncounteredException
|
||||
end
|
||||
|
||||
%% Style Generation
|
||||
API->>DOM: get svg element and firstChild
|
||||
API->>Renderer: diag.renderer.getClasses(text, diag)
|
||||
Renderer-->>API: diagramClassDefs
|
||||
|
||||
API->>+Styles: createUserStyles(config, diagramType, diagramClassDefs, idSelector)
|
||||
|
||||
Styles->>Styles: createCssStyles(config, classDefs)
|
||||
|
||||
opt config.themeCSS defined
|
||||
Styles->>Styles: append themeCSS
|
||||
end
|
||||
|
||||
opt fontFamily configured
|
||||
Styles->>Styles: add CSS variables for fonts
|
||||
end
|
||||
|
||||
opt classDefs exist
|
||||
loop For each styleClassDef
|
||||
opt has styles
|
||||
Styles->>Styles: cssImportantStyles for each CSS element
|
||||
end
|
||||
opt has textStyles
|
||||
Styles->>Styles: cssImportantStyles for tspan elements
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Styles->>Styles: getStyles(graphType, userCSSstyles, themeVariables)
|
||||
Styles->>Styles: serialize(compile(svgId{allStyles}), stringify)
|
||||
Styles-->>-API: compiled CSS rules
|
||||
|
||||
API->>DOM: create style element
|
||||
API->>DOM: style.innerHTML = rules
|
||||
API->>DOM: svg.insertBefore(style, firstChild)
|
||||
|
||||
%% Diagram Rendering
|
||||
API->>+Renderer: diag.renderer.draw(text, id, version, diag)
|
||||
|
||||
Renderer->>Renderer: diagram-specific rendering logic
|
||||
Renderer->>DOM: create SVG elements
|
||||
Renderer->>DOM: apply positioning and styling
|
||||
Renderer-->>-API: rendered diagram
|
||||
|
||||
opt rendering error
|
||||
alt suppressErrorRendering
|
||||
API->>API: removeTempElements()
|
||||
API->>Mermaid: throw error
|
||||
else
|
||||
API->>Renderer: errorRenderer.draw(text, id, version)
|
||||
end
|
||||
end
|
||||
|
||||
%% Accessibility and Cleanup
|
||||
API->>DOM: select svg element
|
||||
API->>Diagram: diag.db.getAccTitle()
|
||||
API->>Diagram: diag.db.getAccDescription()
|
||||
API->>API: addA11yInfo(diagramType, svgNode, a11yTitle, a11yDescr)
|
||||
|
||||
API->>DOM: set xmlns for foreignobject elements
|
||||
API->>DOM: get innerHTML from enclosing div
|
||||
|
||||
API->>+API: cleanUpSvgCode(svgCode, isSandboxed, arrowMarkerAbsolute)
|
||||
|
||||
opt not useArrowMarkerUrls and not sandboxed
|
||||
API->>API: replace marker-end URLs with anchors
|
||||
end
|
||||
|
||||
API->>API: decodeEntities(svgCode)
|
||||
API->>API: replace <br> with <br/>
|
||||
API-->>-API: cleaned SVG code
|
||||
|
||||
alt isSandboxed
|
||||
API->>+API: putIntoIFrame(svgCode, svgEl)
|
||||
API->>API: calculate iframe height
|
||||
API->>API: toBase64 encode SVG content
|
||||
API->>API: create iframe with sandbox attributes
|
||||
API-->>-API: iframe HTML
|
||||
else not loose security
|
||||
API->>API: DOMPurify.sanitize(svgCode, options)
|
||||
end
|
||||
|
||||
API->>API: attachFunctions()
|
||||
API->>API: removeTempElements()
|
||||
|
||||
opt parseEncounteredException
|
||||
API->>Mermaid: throw parseEncounteredException
|
||||
end
|
||||
|
||||
API-->>-Queue: { diagramType, svg: svgCode, bindFunctions }
|
||||
|
||||
%% Return to Web Integration
|
||||
activate Mermaid
|
||||
Queue-->>Mermaid: render result
|
||||
Mermaid->>DOM: element.innerHTML = svg
|
||||
|
||||
opt postRenderCallback
|
||||
Mermaid->>User: postRenderCallback(id)
|
||||
end
|
||||
|
||||
opt bindFunctions exist
|
||||
Mermaid->>DOM: bindFunctions(element)
|
||||
end
|
||||
deactivate Mermaid
|
||||
|
||||
%% Parse Function Flow
|
||||
User->>+Mermaid: mermaid.parse(text, parseOptions)
|
||||
activate Queue
|
||||
|
||||
Queue->>+API: mermaidAPI.parse(text, parseOptions)
|
||||
|
||||
API->>DiagramAPI: addDiagrams()
|
||||
API->>Preprocessor: processAndSetConfigs(text)
|
||||
Preprocessor-->>API: { code, config }
|
||||
API->>Diagram: getDiagramFromText(code)
|
||||
Diagram-->>API: diagram instance
|
||||
API-->>-Queue: { diagramType: diagram.type, config }
|
||||
|
||||
Queue-->>-Mermaid: parse result
|
||||
Mermaid-->>-User: ParseResult or false
|
||||
|
||||
%% External Diagram Registration
|
||||
User->>+Mermaid: registerExternalDiagrams(diagrams, options)
|
||||
|
||||
Mermaid->>DiagramAPI: addDiagrams()
|
||||
Mermaid->>DiagramAPI: registerLazyLoadedDiagrams(...diagrams)
|
||||
|
||||
opt lazyLoad is false
|
||||
Mermaid->>DiagramAPI: loadRegisteredDiagrams()
|
||||
end
|
||||
|
||||
Mermaid-->>-User: registration complete
|
||||
|
||||
%% Error Handling
|
||||
Note over Mermaid, API: Error Handling Throughout
|
||||
alt Error occurs
|
||||
API->>Mermaid: throw error
|
||||
Mermaid->>+Mermaid: handleError(error, errors, parseError)
|
||||
|
||||
Mermaid->>Mermaid: log.warn(error)
|
||||
|
||||
alt isDetailedError
|
||||
Mermaid->>User: parseError(error.str, error.hash)
|
||||
else
|
||||
Mermaid->>User: parseError(error)
|
||||
end
|
||||
|
||||
opt not suppressErrors
|
||||
Mermaid->>User: throw error
|
||||
end
|
||||
|
||||
Mermaid-->>-User: error handled
|
||||
end
|
||||
|
||||
%% Configuration Details
|
||||
Note over Config: Configuration Functions
|
||||
Note right of Config: Functions:<br/>- reset()<br/>- getConfig()<br/>- setConfig()<br/>- getSiteConfig()<br/>- updateSiteConfig()<br/>- saveConfigFromInitialize()
|
||||
|
||||
Note over Styles: CSS Generation
|
||||
Note right of Styles: Features:<br/>- createCssStyles()<br/>- createUserStyles()<br/>- cssImportantStyles()<br/>- Theme integration<br/>- Class definitions
|
||||
|
||||
Note over API: Security Levels
|
||||
Note right of API: Modes:<br/>- sandbox: iframe isolation<br/>- loose: minimal sanitization<br/>- default: DOMPurify sanitization
|
||||
|
||||
Note over API: Helper Functions
|
||||
Note right of API: Utilities:<br/>- cleanUpSvgCode()<br/>- putIntoIFrame()<br/>- appendDivSvgG()<br/>- removeExistingElements()
|
@@ -0,0 +1,180 @@
|
||||
---
|
||||
references:
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapDb.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/detector.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapTypes.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/styles.ts"
|
||||
- "File: /packages/mermaid/src/diagrams/mindmap/svgDraw.ts"
|
||||
generationTime: 2025-01-28T16:00:00.000Z
|
||||
---
|
||||
sequenceDiagram
|
||||
participant User as User Input Text
|
||||
participant Detector as detector.ts
|
||||
participant Loader as DiagramLoader
|
||||
participant Definition as mindmap-definition.ts
|
||||
participant Parser as parser/mindmap.jison
|
||||
participant DB as MindmapDB
|
||||
participant Renderer as mindmapRenderer.ts
|
||||
participant Cytoscape as cytoscape.js
|
||||
participant SVGDraw as svgDraw.ts
|
||||
participant Styles as styles.ts
|
||||
participant Output as Final SVG
|
||||
|
||||
Note over User, Output: Mindmap Implementation Flow
|
||||
|
||||
%% Detection Phase
|
||||
User->>Detector: /^\s*mindmap/ text input
|
||||
activate Detector
|
||||
Detector->>Detector: detector(txt) validates pattern
|
||||
Detector->>Loader: loader() function called
|
||||
deactivate Detector
|
||||
|
||||
activate Loader
|
||||
Loader->>Definition: import mindmap-definition.js
|
||||
deactivate Loader
|
||||
|
||||
%% Core Structure Setup
|
||||
activate Definition
|
||||
Definition->>DB: get db() → new MindmapDB()
|
||||
Definition->>Renderer: setup renderer
|
||||
Definition->>Parser: setup parser
|
||||
Definition->>Styles: setup styles
|
||||
deactivate Definition
|
||||
|
||||
%% Database Initialization
|
||||
activate DB
|
||||
Note over DB: MindmapDB Constructor
|
||||
DB->>DB: initialize nodes array
|
||||
DB->>DB: setup nodeType constants
|
||||
DB->>DB: bind methods
|
||||
DB->>DB: clear() state
|
||||
|
||||
%% Parsing Phase
|
||||
activate Parser
|
||||
User->>Parser: mindmap syntax text
|
||||
|
||||
loop For each node in hierarchy
|
||||
Parser->>DB: addNode(level, id, descr, type)
|
||||
activate DB
|
||||
DB->>DB: sanitizeText(id, descr)
|
||||
DB->>DB: getType(startStr, endStr)
|
||||
Note right of DB: Shape Detection:<br/>[ → RECT<br/>( → ROUNDED_RECT<br/>(( → CIRCLE<br/>)) → BANG<br/>{{ → HEXAGON
|
||||
DB->>DB: getParent(level)
|
||||
DB->>DB: create MindmapNode
|
||||
DB->>DB: add to hierarchy
|
||||
deactivate DB
|
||||
end
|
||||
|
||||
opt Icon/Class Decoration
|
||||
Parser->>DB: decorateNode(decoration)
|
||||
DB->>DB: set icon/class properties
|
||||
end
|
||||
deactivate Parser
|
||||
|
||||
%% Data Preparation
|
||||
Renderer->>DB: getData() for layout
|
||||
activate DB
|
||||
DB->>DB: collect all nodes
|
||||
DB->>DB: build parent-child relationships
|
||||
DB-->>Renderer: return node hierarchy
|
||||
deactivate DB
|
||||
|
||||
%% Rendering Pipeline
|
||||
activate Renderer
|
||||
Note over Renderer: Rendering Phase
|
||||
|
||||
Renderer->>Cytoscape: initialize cytoscape
|
||||
activate Cytoscape
|
||||
|
||||
loop For each node in mindmap
|
||||
Renderer->>Cytoscape: addNodes(mindmap, cy, conf, level)
|
||||
Cytoscape->>Cytoscape: create node data
|
||||
Cytoscape->>Cytoscape: set position (x, y)
|
||||
end
|
||||
|
||||
loop For parent-child relationships
|
||||
Renderer->>Cytoscape: add edges
|
||||
Cytoscape->>Cytoscape: create edge data
|
||||
end
|
||||
|
||||
Renderer->>Cytoscape: configure cose-bilkent layout
|
||||
Cytoscape->>Cytoscape: calculate optimal positions
|
||||
Cytoscape-->>Renderer: return positioned graph
|
||||
deactivate Cytoscape
|
||||
|
||||
%% SVG Generation
|
||||
Renderer->>SVGDraw: drawNodes(db, svg, mindmap, section, conf)
|
||||
activate SVGDraw
|
||||
|
||||
loop For each node recursively
|
||||
SVGDraw->>SVGDraw: select shape function
|
||||
|
||||
alt Default Shape
|
||||
SVGDraw->>SVGDraw: defaultBkg() - rounded rectangle
|
||||
else Rectangle Shape
|
||||
SVGDraw->>SVGDraw: rectBkg() - sharp corners
|
||||
else Circle Shape
|
||||
SVGDraw->>SVGDraw: circleBkg() - perfect circle
|
||||
else Cloud Shape
|
||||
SVGDraw->>SVGDraw: cloudBkg() - organic curves
|
||||
else Bang Shape
|
||||
SVGDraw->>SVGDraw: bangBkg() - explosion style
|
||||
else Hexagon Shape
|
||||
SVGDraw->>SVGDraw: hexagonBkg() - six sides
|
||||
end
|
||||
|
||||
SVGDraw->>SVGDraw: create SVG elements
|
||||
SVGDraw->>SVGDraw: add text labels
|
||||
SVGDraw->>SVGDraw: position node
|
||||
|
||||
opt Node has children
|
||||
SVGDraw->>SVGDraw: drawNodes() recursive call
|
||||
end
|
||||
end
|
||||
deactivate SVGDraw
|
||||
|
||||
%% Edge Rendering
|
||||
Renderer->>Renderer: drawEdges(edgesEl, cy)
|
||||
loop For each edge
|
||||
Renderer->>Renderer: extract edge bounds
|
||||
Renderer->>Renderer: draw SVG path
|
||||
end
|
||||
|
||||
%% Styling Application
|
||||
Renderer->>Styles: getStyles(options)
|
||||
activate Styles
|
||||
|
||||
Styles->>Styles: genSections(options)
|
||||
loop For THEME_COLOR_LIMIT sections
|
||||
Styles->>Styles: generate color scale
|
||||
Styles->>Styles: create CSS rules
|
||||
Note right of Styles: .section-X fills<br/>.edge-depth-X widths<br/>.node-icon-X colors
|
||||
end
|
||||
|
||||
Styles->>Styles: apply theme integration
|
||||
Styles-->>Renderer: return compiled CSS
|
||||
deactivate Styles
|
||||
|
||||
%% Final Assembly
|
||||
Renderer->>Output: selectSvgElement()
|
||||
Renderer->>Output: setupGraphViewbox()
|
||||
Renderer->>Output: apply styles
|
||||
Renderer->>Output: add interactive elements
|
||||
deactivate Renderer
|
||||
|
||||
activate Output
|
||||
Note over Output: Final Mindmap Features
|
||||
Output->>Output: responsive layout
|
||||
Output->>Output: accessibility attributes
|
||||
Output->>Output: hover effects
|
||||
Output->>Output: click handling
|
||||
Output-->>User: rendered mindmap
|
||||
deactivate Output
|
||||
|
||||
%% Configuration Details
|
||||
Note over DB, Styles: Configuration Options
|
||||
Note right of DB: Padding Calculations:<br/>Base padding from config<br/>RECT: ×2 padding<br/>ROUNDED_RECT: ×2 padding<br/>HEXAGON: ×2 padding
|
||||
Note right of Styles: Section Management:<br/>MAX_SECTIONS = 12<br/>Dynamic color generation<br/>Git theme integration
|
||||
Note right of Renderer: Layout Parameters:<br/>Cytoscape configuration<br/>coseBilkent settings<br/>Node spacing rules
|
@@ -79,6 +79,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
LLM integrations to create mermaid diagrams using AI from text descriptions.
|
||||
|
||||
- [HueHive - Create mermaid diagrams with text](https://huehive.co/tools/diagrams)
|
||||
- [MCP Server Mermaid](https://github.com/hustcc/mcp-mermaid) - Generate mermaid diagram and chart with AI MCP dynamically.
|
||||
|
||||
### CRM/ERP
|
||||
|
||||
@@ -98,6 +99,7 @@ Blogging frameworks and platforms
|
||||
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
||||
- [WordPress](https://wordpress.org)
|
||||
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
||||
- [WP Documentation](https://wordpress.org/themes/wp-documentation/)
|
||||
|
||||
### CMS/ECM
|
||||
|
||||
|
@@ -10,9 +10,7 @@ Applications that support Mermaid files [SHOULD](https://datatracker.ietf.org/do
|
||||
|
||||
### MIME Type
|
||||
|
||||
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is `text/vnd.mermaid`.
|
||||
|
||||
Currently pending [IANA](https://www.iana.org/) recognition.
|
||||
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is [`text/vnd.mermaid`](https://www.iana.org/assignments/media-types/application/vnd.mermaid).
|
||||
|
||||
## Showcase
|
||||
|
||||
|
@@ -1135,15 +1135,46 @@ It is possible to style the type of curve used for lines between items, if the d
|
||||
Available curve styles include `basis`, `bumpX`, `bumpY`, `cardinal`, `catmullRom`, `linear`, `monotoneX`, `monotoneY`,
|
||||
`natural`, `step`, `stepAfter`, and `stepBefore`.
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
|
||||
Line styling can be achieved in two ways:
|
||||
|
||||
1. Change the curve style of all the lines
|
||||
2. Change the curve style of a particular line
|
||||
|
||||
#### Diagram level curve style
|
||||
|
||||
In this example, a left-to-right graph uses the `stepBefore` curve style:
|
||||
|
||||
```
|
||||
%%{ init: { 'flowchart': { 'curve': 'stepBefore' } } }%%
|
||||
---
|
||||
config:
|
||||
flowchart:
|
||||
curve: stepBefore
|
||||
---
|
||||
graph LR
|
||||
```
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
#### Edge level curve style using Edge IDs (v<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
You can assign IDs to [edges](#attaching-an-id-to-edges). After assigning an ID you can modify the line style by modifying the edge's `curve` property using the following syntax:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
A e2@--> C
|
||||
e1@{ curve: linear }
|
||||
e2@{ curve: natural }
|
||||
```
|
||||
|
||||
```info
|
||||
Any edge curve style modified at the edge level overrides the diagram level style.
|
||||
```
|
||||
|
||||
```info
|
||||
If the same edge is modified multiple times the last modification will be rendered.
|
||||
```
|
||||
|
||||
### Styling a node
|
||||
|
||||
|
@@ -1,40 +1,5 @@
|
||||
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// -------------------------------------
|
||||
// Mocks and mocking
|
||||
|
||||
import { MockedD3 } from './tests/MockedD3.js';
|
||||
|
||||
// Note: If running this directly from within an IDE, the mocks directory must be at packages/mermaid/mocks
|
||||
vi.mock('d3');
|
||||
vi.mock('dagre-d3');
|
||||
|
||||
// mermaidAPI.spec.ts:
|
||||
import * as accessibility from './accessibility.js'; // Import it this way so we can use spyOn(accessibility,...)
|
||||
vi.mock('./accessibility.js', () => ({
|
||||
setA11yDiagramInfo: vi.fn(),
|
||||
addSVGa11yTitleDescription: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the renderers specifically so we can test render(). Need to mock draw() for each renderer
|
||||
vi.mock('./diagrams/c4/c4Renderer.js');
|
||||
vi.mock('./diagrams/class/classRenderer.js');
|
||||
vi.mock('./diagrams/class/classRenderer-v2.js');
|
||||
vi.mock('./diagrams/er/erRenderer.js');
|
||||
vi.mock('./diagrams/flowchart/flowRenderer-v2.js');
|
||||
vi.mock('./diagrams/git/gitGraphRenderer.js');
|
||||
vi.mock('./diagrams/gantt/ganttRenderer.js');
|
||||
vi.mock('./diagrams/user-journey/journeyRenderer.js');
|
||||
vi.mock('./diagrams/pie/pieRenderer.js');
|
||||
vi.mock('./diagrams/packet/renderer.js');
|
||||
vi.mock('./diagrams/xychart/xychartRenderer.js');
|
||||
vi.mock('./diagrams/requirement/requirementRenderer.js');
|
||||
vi.mock('./diagrams/sequence/sequenceRenderer.js');
|
||||
vi.mock('./diagrams/radar/renderer.js');
|
||||
vi.mock('./diagrams/architecture/architectureRenderer.js');
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
import assignWithDepth from './assignWithDepth.js';
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
import mermaid from './mermaid.js';
|
||||
@@ -75,6 +40,9 @@ import { SequenceDB } from './diagrams/sequence/sequenceDb.js';
|
||||
import { decodeEntities, encodeEntities } from './utils.js';
|
||||
import { toBase64 } from './utils/base64.js';
|
||||
import { StateDB } from './diagrams/state/stateDb.js';
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
import { select } from 'd3';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
/**
|
||||
* @see https://vitest.dev/guide/mocking.html Mock part of a module
|
||||
@@ -225,63 +193,49 @@ describe('mermaidAPI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const fauxParentNode = new MockedD3();
|
||||
const fauxEnclosingDiv = new MockedD3();
|
||||
const fauxSvgNode = new MockedD3();
|
||||
|
||||
describe('appendDivSvgG', () => {
|
||||
const fauxGNode = new MockedD3();
|
||||
const parent_append_spy = vi.spyOn(fauxParentNode, 'append').mockReturnValue(fauxEnclosingDiv);
|
||||
const div_append_spy = vi.spyOn(fauxEnclosingDiv, 'append').mockReturnValue(fauxSvgNode);
|
||||
// @ts-ignore @todo TODO why is this getting a type error?
|
||||
const div_attr_spy = vi.spyOn(fauxEnclosingDiv, 'attr').mockReturnValue(fauxEnclosingDiv);
|
||||
const svg_append_spy = vi.spyOn(fauxSvgNode, 'append').mockReturnValue(fauxGNode);
|
||||
// @ts-ignore @todo TODO why is this getting a type error?
|
||||
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
|
||||
// cspell:ignore dthe
|
||||
|
||||
it('appends a div node', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(parent_append_spy).toHaveBeenCalledWith('div');
|
||||
expect(div_append_spy).toHaveBeenCalledWith('svg');
|
||||
jsdomIt('appends a div node', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const divNode = ensureNodeFromSelector('div');
|
||||
const svgNode = ensureNodeFromSelector('svg', divNode);
|
||||
ensureNodeFromSelector('g', svgNode);
|
||||
});
|
||||
it('the id for the div is "d" with the id appended', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
|
||||
jsdomIt('the id for the div is "d" with the id appended', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const divNode = ensureNodeFromSelector('div');
|
||||
expect(divNode?.getAttribute('id')).toBe('dtheId');
|
||||
});
|
||||
|
||||
it('sets the style for the div if one is given', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'given div style', 'given x link');
|
||||
expect(div_attr_spy).toHaveBeenCalledWith('style', 'given div style');
|
||||
jsdomIt('sets the style for the div if one is given', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId', 'given div style', 'given x link');
|
||||
const divNode = ensureNodeFromSelector('div');
|
||||
expect(divNode?.getAttribute('style')).toBe('given div style');
|
||||
});
|
||||
|
||||
it('appends a svg node to the div node', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
|
||||
jsdomIt('sets the svg width to 100%', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const svgNode = ensureNodeFromSelector('div > svg');
|
||||
expect(svgNode.getAttribute('width')).toBe('100%');
|
||||
});
|
||||
it('sets the svg width to 100%', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%');
|
||||
jsdomIt('the svg id is the id', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const svgNode = ensureNodeFromSelector('div > svg');
|
||||
expect(svgNode.getAttribute('id')).toBe('theId');
|
||||
});
|
||||
it('the svg id is the id', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId');
|
||||
jsdomIt('the svg xml namespace is the 2000 standard', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId');
|
||||
const svgNode = ensureNodeFromSelector('div > svg');
|
||||
expect(svgNode.getAttribute('xmlns')).toBe('http://www.w3.org/2000/svg');
|
||||
});
|
||||
it('the svg xml namespace is the 2000 standard', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg');
|
||||
jsdomIt('sets the svg xlink if one is given', ({ body }) => {
|
||||
appendDivSvgG(body, 'theId', 'dtheId', 'div style', 'given x link');
|
||||
const svgNode = ensureNodeFromSelector('div > svg');
|
||||
expect(svgNode.getAttribute('xmlns:xlink')).toBe('given x link');
|
||||
});
|
||||
it('sets the svg xlink if one is given', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'div style', 'given x link');
|
||||
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns:xlink', 'given x link');
|
||||
});
|
||||
it('appends a g (group) node to the svg node', () => {
|
||||
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||
expect(svg_append_spy).toHaveBeenCalledWith('g');
|
||||
});
|
||||
it('returns the given parentRoot d3 nodes', () => {
|
||||
expect(appendDivSvgG(fauxParentNode, 'theId', 'dtheId')).toEqual(fauxParentNode);
|
||||
jsdomIt('returns the given parentRoot d3 nodes', ({ body }) => {
|
||||
expect(appendDivSvgG(body, 'theId', 'dtheId')).toEqual(body);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -782,9 +736,9 @@ graph TD;A--x|text including URL space|B;`)
|
||||
// render(id, text, cb?, svgContainingElement?)
|
||||
|
||||
// Test all diagram types. Note that old flowchart 'graph' type will invoke the flowRenderer-v2. (See the flowchart v2 detector.)
|
||||
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different than was is put in the diagram text (ex: in -v2 diagrams)
|
||||
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different from what is put in the diagram text (ex: in -v2 diagrams)
|
||||
const diagramTypesAndExpectations = [
|
||||
{ textDiagramType: 'C4Context', expectedType: 'c4' },
|
||||
// { textDiagramType: 'C4Context', expectedType: 'c4' }, TODO : setAccTitle not called in C4 jison parser
|
||||
{ textDiagramType: 'classDiagram', expectedType: 'class' },
|
||||
{ textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' },
|
||||
{ textDiagramType: 'erDiagram', expectedType: 'er' },
|
||||
@@ -796,7 +750,11 @@ graph TD;A--x|text including URL space|B;`)
|
||||
{ textDiagramType: 'pie', expectedType: 'pie' },
|
||||
{ textDiagramType: 'packet', expectedType: 'packet' },
|
||||
{ textDiagramType: 'packet-beta', expectedType: 'packet' },
|
||||
{ textDiagramType: 'xychart-beta', expectedType: 'xychart' },
|
||||
{
|
||||
textDiagramType: 'xychart-beta',
|
||||
expectedType: 'xychart',
|
||||
content: 'x-axis "Attempts" 10000 --> 10000\ny-axis "Passing tests" 1 --> 1\nbar [1]',
|
||||
},
|
||||
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
|
||||
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
|
||||
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
|
||||
@@ -812,20 +770,25 @@ graph TD;A--x|text including URL space|B;`)
|
||||
diagramTypesAndExpectations.forEach((testedDiagram) => {
|
||||
describe(`${testedDiagram.textDiagramType}`, () => {
|
||||
const diagramType = testedDiagram.textDiagramType;
|
||||
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`;
|
||||
const content = testedDiagram.content || '';
|
||||
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n ${content}`;
|
||||
const expectedDiagramType = testedDiagram.expectedType;
|
||||
|
||||
it('should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription', async () => {
|
||||
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
|
||||
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
|
||||
const result = await mermaidAPI.render(id, diagramText);
|
||||
expect(result.diagramType).toBe(expectedDiagramType);
|
||||
expect(a11yDiagramInfo_spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expectedDiagramType
|
||||
);
|
||||
expect(a11yTitleDesc_spy).toHaveBeenCalled();
|
||||
});
|
||||
jsdomIt(
|
||||
'should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription',
|
||||
async () => {
|
||||
const { svg } = await mermaidAPI.render(id, diagramText);
|
||||
const dom = new JSDOM(svg);
|
||||
const svgNode = ensureNodeFromSelector('svg', dom.window.document);
|
||||
const descNode = ensureNodeFromSelector('desc', svgNode);
|
||||
const titleNode = ensureNodeFromSelector('title', svgNode);
|
||||
expect(svgNode.getAttribute('aria-roledescription')).toBe(expectedDiagramType);
|
||||
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${id}`);
|
||||
expect(descNode.getAttribute('id')).toBe(`chart-desc-${id}`);
|
||||
expect(descNode.innerHTML).toBe(a11yDescr);
|
||||
expect(titleNode.innerHTML).toBe(a11yTitle);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -54,7 +54,7 @@ export async function requirementBox<T extends SVGGraphicsElement>(
|
||||
if (isRequirementNode) {
|
||||
const idHeight = await addText(
|
||||
shapeSvg,
|
||||
`${requirementNode.requirementId ? `id: ${requirementNode.requirementId}` : ''}`,
|
||||
`${requirementNode.requirementId ? `ID: ${requirementNode.requirementId}` : ''}`,
|
||||
accumulativeHeight,
|
||||
node.labelStyle
|
||||
);
|
||||
|
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* This is a mocked/stubbed version of the d3 Selection type. Each of the main functions are all
|
||||
* mocked (via vi.fn()) so you can track if they have been called, etc.
|
||||
*
|
||||
* Note that node() returns a HTML Element with tag 'svg'. It is an empty element (no innerHTML, no children, etc).
|
||||
* This potentially allows testing of mermaidAPI render().
|
||||
*/
|
||||
export class MockedD3 {
|
||||
public attribs = new Map<string, string>();
|
||||
public id: string | undefined = '';
|
||||
_children: MockedD3[] = [];
|
||||
|
||||
_containingHTMLdoc = new Document();
|
||||
|
||||
constructor(givenId = 'mock-id') {
|
||||
this.id = givenId;
|
||||
}
|
||||
|
||||
/** Helpful utility during development/debugging. This is not a real d3 function */
|
||||
public listChildren(): string {
|
||||
return this._children
|
||||
.map((child) => {
|
||||
return child.id;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
select = vi.fn().mockImplementation(({ select_str = '' }): MockedD3 => {
|
||||
// Get the id from an argument string. if it is of the form [id='some-id'], strip off the
|
||||
// surrounding id[..]
|
||||
const stripSurroundRegexp = /\[id='(.*)']/;
|
||||
const matchedSurrounds = select_str.match(stripSurroundRegexp);
|
||||
const cleanId = matchedSurrounds ? matchedSurrounds[1] : select_str;
|
||||
return new MockedD3(cleanId);
|
||||
});
|
||||
|
||||
// This has the same implementation as select(). (It calls it.)
|
||||
selectAll = vi.fn().mockImplementation(({ select_str = '' }): MockedD3 => {
|
||||
return this.select(select_str);
|
||||
});
|
||||
|
||||
append = vi.fn().mockImplementation(function (
|
||||
this: MockedD3,
|
||||
type: string,
|
||||
id = '' + '-appended'
|
||||
): MockedD3 {
|
||||
const newMock = new MockedD3(id);
|
||||
newMock.attribs.set('type', type);
|
||||
this._children.push(newMock);
|
||||
return newMock;
|
||||
});
|
||||
|
||||
// NOTE: The d3 implementation allows for a selector ('beforeSelector' arg below).
|
||||
// With this mocked implementation, we assume it will always refer to a node id
|
||||
// and will always be of the form "#[id of the node to insert before]".
|
||||
// To keep this simple, any leading '#' is removed and the resulting string is the node id searched.
|
||||
insert = (type: string, beforeSelector?: string, id = this.id + '-inserted'): MockedD3 => {
|
||||
const newMock = new MockedD3(id);
|
||||
newMock.attribs.set('type', type);
|
||||
if (beforeSelector === undefined) {
|
||||
this._children.push(newMock);
|
||||
} else {
|
||||
const idOnly = beforeSelector.startsWith('#') ? beforeSelector.substring(1) : beforeSelector;
|
||||
const foundIndex = this._children.findIndex((child) => child.id === idOnly);
|
||||
if (foundIndex < 0) {
|
||||
this._children.push(newMock);
|
||||
} else {
|
||||
this._children.splice(foundIndex, 0, newMock);
|
||||
}
|
||||
}
|
||||
return newMock;
|
||||
};
|
||||
|
||||
attr(attrName: string): undefined | string;
|
||||
attr(attrName: string, attrValue: string): MockedD3;
|
||||
attr(attrName: string, attrValue?: string): undefined | string | MockedD3 {
|
||||
if (arguments.length === 1) {
|
||||
return this.attribs.get(attrName);
|
||||
} else {
|
||||
if (attrName === 'id') {
|
||||
this.id = attrValue; // also set the id explicitly
|
||||
}
|
||||
if (attrValue !== undefined) {
|
||||
this.attribs.set(attrName, attrValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public lower(attrValue = '') {
|
||||
this.attribs.set('lower', attrValue);
|
||||
return this;
|
||||
}
|
||||
public style(attrValue = '') {
|
||||
this.attribs.set('style', attrValue);
|
||||
return this;
|
||||
}
|
||||
public text(attrValue = '') {
|
||||
this.attribs.set('text', attrValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
// NOTE: Returns a HTML Element with tag 'svg' that has _another_ 'svg' element child.
|
||||
// This allows different tests to succeed -- some need a top level 'svg' and some need a 'svg' element to be the firstChild
|
||||
// Real implementation returns an HTML Element
|
||||
public node = vi.fn().mockImplementation(() => {
|
||||
//create a top level svg element
|
||||
const topElem = this._containingHTMLdoc.createElement('svg');
|
||||
//@ts-ignore - this is a mock SVG element
|
||||
topElem.getBBox = this.getBBox;
|
||||
const elem_svgChild = this._containingHTMLdoc.createElement('svg'); // another svg element
|
||||
topElem.appendChild(elem_svgChild);
|
||||
return topElem;
|
||||
});
|
||||
|
||||
// TODO Is this correct? shouldn't it return a list of HTML Elements?
|
||||
nodes = vi.fn().mockImplementation(function (this: MockedD3): MockedD3[] {
|
||||
return this._children;
|
||||
});
|
||||
|
||||
// This will try to use attrs that have been set.
|
||||
getBBox = () => {
|
||||
const x = this.attribs.has('x') ? this.attribs.get('x') : 20;
|
||||
const y = this.attribs.has('y') ? this.attribs.get('y') : 30;
|
||||
const width = this.attribs.has('width') ? this.attribs.get('width') : 140;
|
||||
const height = this.attribs.has('height') ? this.attribs.get('height') : 250;
|
||||
return {
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// The following functions are here for completeness. They simply return a vi.fn()
|
||||
|
||||
insertBefore = vi.fn();
|
||||
curveBasis = vi.fn();
|
||||
curveBasisClosed = vi.fn();
|
||||
curveBasisOpen = vi.fn();
|
||||
curveLinear = vi.fn();
|
||||
curveLinearClosed = vi.fn();
|
||||
curveMonotoneX = vi.fn();
|
||||
curveMonotoneY = vi.fn();
|
||||
curveNatural = vi.fn();
|
||||
curveStep = vi.fn();
|
||||
curveStepAfter = vi.fn();
|
||||
curveStepBefore = vi.fn();
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
import { vi } from 'vitest';
|
||||
vi.mock('d3');
|
||||
vi.mock('dagre-d3-es');
|
@@ -26,6 +26,10 @@ ${'2w'} | ${dayjs.duration(2, 'w')}
|
||||
```
|
||||
*/
|
||||
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { expect, it } from 'vitest';
|
||||
import { select, type Selection } from 'd3';
|
||||
|
||||
export const convert = (template: TemplateStringsArray, ...params: unknown[]) => {
|
||||
const header = template[0]
|
||||
.trim()
|
||||
@@ -42,3 +46,83 @@ export const convert = (template: TemplateStringsArray, ...params: unknown[]) =>
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Getting rid of linter issues to make {@link jsdomIt} work.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function setOnProtectedConstant(object: any, key: string, value: unknown): void {
|
||||
object[key] = value;
|
||||
}
|
||||
|
||||
export const MOCKED_BBOX = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 666,
|
||||
height: 666,
|
||||
};
|
||||
|
||||
interface JsdomItInput {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
body: Selection<HTMLBodyElement, never, HTMLElement, any>; // The `any` here comes from D3'as API.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
svg: Selection<SVGSVGElement, never, HTMLElement, any>; // The `any` here comes from D3'as API.
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method borrowed from d3 : https://github.com/d3/d3-selection/blob/v3.0.0/test/jsdom.js
|
||||
*
|
||||
* Fools d3 into thinking it's working in a browser with a real DOM.
|
||||
*
|
||||
* The DOM is actually an instance of JSDom with monkey-patches for DOM methods that require a
|
||||
* rendering engine.
|
||||
*
|
||||
* The resulting environment is capable of rendering SVGs with the caveat that layouts are
|
||||
* completely screwed.
|
||||
*
|
||||
* This makes it possible to make structural tests instead of mocking everything.
|
||||
*/
|
||||
export function jsdomIt(message: string, run: (input: JsdomItInput) => void | Promise<void>) {
|
||||
return it(message, async (): Promise<void> => {
|
||||
const oldWindow = global.window;
|
||||
const oldDocument = global.document;
|
||||
|
||||
try {
|
||||
const baseHtml = `
|
||||
<html lang="en">
|
||||
<body id="cy">
|
||||
<svg id="svg"/>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const dom = new JSDOM(baseHtml, {
|
||||
resources: 'usable',
|
||||
beforeParse(_window) {
|
||||
// Mocks DOM functions that require rendering, JSDOM doesn't
|
||||
setOnProtectedConstant(_window.Element.prototype, 'getBBox', () => MOCKED_BBOX);
|
||||
setOnProtectedConstant(_window.Element.prototype, 'getComputedTextLength', () => 200);
|
||||
},
|
||||
});
|
||||
setOnProtectedConstant(global, 'window', dom.window); // Fool D3 into thinking it's in a browser
|
||||
setOnProtectedConstant(global, 'document', dom.window.document); // Fool D3 into thinking it's in a browser
|
||||
setOnProtectedConstant(global, 'MutationObserver', undefined); // JSDOM doesn't like cytoscape elements
|
||||
|
||||
const body = select<HTMLBodyElement, never>('body');
|
||||
const svg = select<SVGSVGElement, never>('svg');
|
||||
await run({ body, svg });
|
||||
} finally {
|
||||
setOnProtectedConstant(global, 'window', oldWindow);
|
||||
setOnProtectedConstant(global, 'document', oldDocument);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the node from its parent with ParentNode#querySelector,
|
||||
* then checks that it exists before returning it.
|
||||
*/
|
||||
export function ensureNodeFromSelector(selector: string, parent: ParentNode = document): Element {
|
||||
const node = parent.querySelector(selector);
|
||||
expect(node).not.toBeNull();
|
||||
return node!;
|
||||
}
|
||||
|
@@ -16,6 +16,19 @@ export interface NodeMetaData {
|
||||
export interface EdgeMetaData {
|
||||
animation?: 'fast' | 'slow';
|
||||
animate?: boolean;
|
||||
curve?:
|
||||
| 'basis'
|
||||
| 'bumpX'
|
||||
| 'bumpY'
|
||||
| 'cardinal'
|
||||
| 'catmullRom'
|
||||
| 'linear'
|
||||
| 'monotoneX'
|
||||
| 'monotoneY'
|
||||
| 'natural'
|
||||
| 'step'
|
||||
| 'stepAfter'
|
||||
| 'stepBefore';
|
||||
}
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user