Merge branch 'develop' of https://github.com/mermaid-js/mermaid into 4459-remove-beta-suffix

This commit is contained in:
darshanr0107
2025-07-16 13:38:28 +05:30
103 changed files with 4049 additions and 2102 deletions

View File

@@ -33,4 +33,9 @@ export const packageOptions = {
packageName: 'mermaid-layout-elk', packageName: 'mermaid-layout-elk',
file: 'layouts.ts', file: 'layouts.ts',
}, },
examples: {
name: 'mermaid-examples',
packageName: 'examples',
file: 'index.ts',
},
} as const satisfies Record<string, PackageOptions>; } as const satisfies Record<string, PackageOptions>;

View File

@@ -10,13 +10,16 @@ const buildType = (packageName: string) => {
console.log(out.toString()); console.log(out.toString());
} }
} catch (e) { } catch (e) {
console.error(e);
if (e.stdout.length > 0) { if (e.stdout.length > 0) {
console.error(e.stdout.toString()); console.error(e.stdout.toString());
} }
if (e.stderr.length > 0) { if (e.stderr.length > 0) {
console.error(e.stderr.toString()); console.error(e.stderr.toString());
} }
// Exit the build process if we are in CI
if (process.env.CI) {
throw new Error(`Failed to build types for ${packageName}`);
}
} }
}; };

View File

@@ -1,6 +0,0 @@
---
'mermaid': minor
'@mermaid-js/parser': minor
---
Adding support for the new diagram type nested treemap

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
chore: Updated TreeMapDB to use class based approach

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
chore: Update MindmapDB to use class based approach

View File

@@ -0,0 +1,7 @@
---
'mermaid': patch
---
fix(timeline): fix loading `leftMargin` from config
The `timeline.leftMargin` config value should now correctly control the size of the left margin, instead of being ignored.

6
.github/lychee.toml vendored
View File

@@ -46,9 +46,15 @@ exclude = [
# Drupal 403 # Drupal 403
"https://(www.)?drupal.org", "https://(www.)?drupal.org",
# Phbpp 403
"https://(www.)?phpbb.com",
# Swimm returns 404, even though the link is valid # Swimm returns 404, even though the link is valid
"https://docs.swimm.io", "https://docs.swimm.io",
# Certificate Error
"https://noteshub.app",
# Timeout # Timeout
"https://huehive.co", "https://huehive.co",
"https://foswiki.org", "https://foswiki.org",

View File

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

View File

@@ -29,3 +29,29 @@ jobs:
disable-releaser: true disable-releaser: true
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Add "Sponsored by MermaidChart" label
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.payload.pull_request.number;
const { data: commits } = await github.rest.pulls.listCommits({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const isSponsored = commits.every(
(c) => c.commit.author.email?.endsWith('@mermaidchart.com')
);
if (isSponsored) {
console.log('PR is sponsored. Adding label.');
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['Sponsored by MermaidChart'],
});
}

View File

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

View File

@@ -26,7 +26,10 @@ export default eyesPlugin(
config.env.useArgos = process.env.RUN_VISUAL_TEST === 'true'; config.env.useArgos = process.env.RUN_VISUAL_TEST === 'true';
if (config.env.useArgos) { if (config.env.useArgos) {
registerArgosTask(on, config); registerArgosTask(on, config, {
// Enable upload to Argos only when it runs on CI.
uploadToArgos: !!process.env.CI,
});
} else { } else {
addMatchImageSnapshotPlugin(on, config); addMatchImageSnapshotPlugin(on, config);
} }

View File

@@ -1,7 +1,7 @@
import { imgSnapshotTest } from '../../helpers/util'; import { imgSnapshotTest } from '../../helpers/util';
describe('packet structure', () => { describe('packet structure', () => {
it('should render a simple packet diagram', () => { it('should render a simple packet-beta diagram', () => {
imgSnapshotTest( imgSnapshotTest(
`packet-beta `packet-beta
title Hello world title Hello world
@@ -10,9 +10,18 @@ describe('packet structure', () => {
); );
}); });
it('should render a simple packet diagram', () => {
imgSnapshotTest(
`packet
title Hello world
0-10: "hello"
`
);
});
it('should render a simple packet diagram without ranges', () => { it('should render a simple packet diagram without ranges', () => {
imgSnapshotTest( imgSnapshotTest(
`packet-beta `packet
0: "h" 0: "h"
1: "i" 1: "i"
` `
@@ -21,7 +30,7 @@ describe('packet structure', () => {
it('should render a complex packet diagram', () => { it('should render a complex packet diagram', () => {
imgSnapshotTest( imgSnapshotTest(
`packet-beta `packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"
@@ -52,7 +61,7 @@ describe('packet structure', () => {
packet: packet:
showBits: false showBits: false
--- ---
packet-beta packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"

View File

@@ -2,35 +2,35 @@
"durations": [ "durations": [
{ {
"spec": "cypress/integration/other/configuration.spec.js", "spec": "cypress/integration/other/configuration.spec.js",
"duration": 6130 "duration": 5672
}, },
{ {
"spec": "cypress/integration/other/external-diagrams.spec.js", "spec": "cypress/integration/other/external-diagrams.spec.js",
"duration": 1974 "duration": 1990
}, },
{ {
"spec": "cypress/integration/other/ghsa.spec.js", "spec": "cypress/integration/other/ghsa.spec.js",
"duration": 3308 "duration": 3186
}, },
{ {
"spec": "cypress/integration/other/iife.spec.js", "spec": "cypress/integration/other/iife.spec.js",
"duration": 1877 "duration": 1948
}, },
{ {
"spec": "cypress/integration/other/interaction.spec.js", "spec": "cypress/integration/other/interaction.spec.js",
"duration": 10902 "duration": 11938
}, },
{ {
"spec": "cypress/integration/other/rerender.spec.js", "spec": "cypress/integration/other/rerender.spec.js",
"duration": 1836 "duration": 1932
}, },
{ {
"spec": "cypress/integration/other/xss.spec.js", "spec": "cypress/integration/other/xss.spec.js",
"duration": 26467 "duration": 27237
}, },
{ {
"spec": "cypress/integration/rendering/appli.spec.js", "spec": "cypress/integration/rendering/appli.spec.js",
"duration": 3129 "duration": 3170
}, },
{ {
"spec": "cypress/integration/rendering/architecture.spec.ts", "spec": "cypress/integration/rendering/architecture.spec.ts",
@@ -38,147 +38,147 @@
}, },
{ {
"spec": "cypress/integration/rendering/block.spec.js", "spec": "cypress/integration/rendering/block.spec.js",
"duration": 16230 "duration": 17390
}, },
{ {
"spec": "cypress/integration/rendering/c4.spec.js", "spec": "cypress/integration/rendering/c4.spec.js",
"duration": 5231 "duration": 5296
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js", "spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
"duration": 38113 "duration": 39004
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js", "spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
"duration": 36423 "duration": 37653
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js", "spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
"duration": 22509 "duration": 23278
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js", "spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
"duration": 34933 "duration": 36645
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram.spec.js", "spec": "cypress/integration/rendering/classDiagram.spec.js",
"duration": 14681 "duration": 15418
}, },
{ {
"spec": "cypress/integration/rendering/conf-and-directives.spec.js", "spec": "cypress/integration/rendering/conf-and-directives.spec.js",
"duration": 8877 "duration": 9684
}, },
{ {
"spec": "cypress/integration/rendering/current.spec.js", "spec": "cypress/integration/rendering/current.spec.js",
"duration": 2517 "duration": 2570
}, },
{ {
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js", "spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
"duration": 81226 "duration": 84687
}, },
{ {
"spec": "cypress/integration/rendering/erDiagram.spec.js", "spec": "cypress/integration/rendering/erDiagram.spec.js",
"duration": 14211 "duration": 14819
}, },
{ {
"spec": "cypress/integration/rendering/errorDiagram.spec.js", "spec": "cypress/integration/rendering/errorDiagram.spec.js",
"duration": 3355 "duration": 3371
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-elk.spec.js", "spec": "cypress/integration/rendering/flowchart-elk.spec.js",
"duration": 38857 "duration": 39925
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js", "spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
"duration": 28570 "duration": 34694
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-icon.spec.js", "spec": "cypress/integration/rendering/flowchart-icon.spec.js",
"duration": 6902 "duration": 7137
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts", "spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
"duration": 23075 "duration": 24740
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-v2.spec.js", "spec": "cypress/integration/rendering/flowchart-v2.spec.js",
"duration": 40514 "duration": 42077
}, },
{ {
"spec": "cypress/integration/rendering/flowchart.spec.js", "spec": "cypress/integration/rendering/flowchart.spec.js",
"duration": 28611 "duration": 30642
}, },
{ {
"spec": "cypress/integration/rendering/gantt.spec.js", "spec": "cypress/integration/rendering/gantt.spec.js",
"duration": 16605 "duration": 18085
}, },
{ {
"spec": "cypress/integration/rendering/gitGraph.spec.js", "spec": "cypress/integration/rendering/gitGraph.spec.js",
"duration": 47636 "duration": 50107
}, },
{ {
"spec": "cypress/integration/rendering/iconShape.spec.ts", "spec": "cypress/integration/rendering/iconShape.spec.ts",
"duration": 262219 "duration": 276279
}, },
{ {
"spec": "cypress/integration/rendering/imageShape.spec.ts", "spec": "cypress/integration/rendering/imageShape.spec.ts",
"duration": 54111 "duration": 56505
}, },
{ {
"spec": "cypress/integration/rendering/info.spec.ts", "spec": "cypress/integration/rendering/info.spec.ts",
"duration": 3006 "duration": 3036
}, },
{ {
"spec": "cypress/integration/rendering/journey.spec.js", "spec": "cypress/integration/rendering/journey.spec.js",
"duration": 6858 "duration": 6889
}, },
{ {
"spec": "cypress/integration/rendering/kanban.spec.ts", "spec": "cypress/integration/rendering/kanban.spec.ts",
"duration": 7281 "duration": 7353
}, },
{ {
"spec": "cypress/integration/rendering/katex.spec.js", "spec": "cypress/integration/rendering/katex.spec.js",
"duration": 3579 "duration": 3580
}, },
{ {
"spec": "cypress/integration/rendering/marker_unique_id.spec.js", "spec": "cypress/integration/rendering/marker_unique_id.spec.js",
"duration": 2448 "duration": 2508
}, },
{ {
"spec": "cypress/integration/rendering/mindmap.spec.ts", "spec": "cypress/integration/rendering/mindmap.spec.ts",
"duration": 10618 "duration": 10939
}, },
{ {
"spec": "cypress/integration/rendering/newShapes.spec.ts", "spec": "cypress/integration/rendering/newShapes.spec.ts",
"duration": 140874 "duration": 149102
}, },
{ {
"spec": "cypress/integration/rendering/oldShapes.spec.ts", "spec": "cypress/integration/rendering/oldShapes.spec.ts",
"duration": 108015 "duration": 113987
}, },
{ {
"spec": "cypress/integration/rendering/packet.spec.ts", "spec": "cypress/integration/rendering/packet.spec.ts",
"duration": 4241 "duration": 4060
}, },
{ {
"spec": "cypress/integration/rendering/pie.spec.ts", "spec": "cypress/integration/rendering/pie.spec.ts",
"duration": 5645 "duration": 5715
}, },
{ {
"spec": "cypress/integration/rendering/quadrantChart.spec.js", "spec": "cypress/integration/rendering/quadrantChart.spec.js",
"duration": 8524 "duration": 8945
}, },
{ {
"spec": "cypress/integration/rendering/radar.spec.js", "spec": "cypress/integration/rendering/radar.spec.js",
"duration": 5203 "duration": 5337
}, },
{ {
"spec": "cypress/integration/rendering/requirement.spec.js", "spec": "cypress/integration/rendering/requirement.spec.js",
"duration": 2635 "duration": 2643
}, },
{ {
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js", "spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
"duration": 50512 "duration": 52072
}, },
{ {
"spec": "cypress/integration/rendering/sankey.spec.ts", "spec": "cypress/integration/rendering/sankey.spec.ts",
@@ -186,31 +186,35 @@
}, },
{ {
"spec": "cypress/integration/rendering/sequencediagram.spec.js", "spec": "cypress/integration/rendering/sequencediagram.spec.js",
"duration": 34559 "duration": 35721
}, },
{ {
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js", "spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
"duration": 24421 "duration": 26030
}, },
{ {
"spec": "cypress/integration/rendering/stateDiagram.spec.js", "spec": "cypress/integration/rendering/stateDiagram.spec.js",
"duration": 15316 "duration": 16333
}, },
{ {
"spec": "cypress/integration/rendering/theme.spec.js", "spec": "cypress/integration/rendering/theme.spec.js",
"duration": 28240 "duration": 29287
}, },
{ {
"spec": "cypress/integration/rendering/timeline.spec.ts", "spec": "cypress/integration/rendering/timeline.spec.ts",
"duration": 6808 "duration": 8491
},
{
"spec": "cypress/integration/rendering/treemap.spec.ts",
"duration": 12291
}, },
{ {
"spec": "cypress/integration/rendering/xyChart.spec.js", "spec": "cypress/integration/rendering/xyChart.spec.js",
"duration": 19359 "duration": 20651
}, },
{ {
"spec": "cypress/integration/rendering/zenuml.spec.js", "spec": "cypress/integration/rendering/zenuml.spec.js",
"duration": 3164 "duration": 3218
} }
] ]
} }

View File

@@ -4,7 +4,7 @@
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" /> <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/font-awesome.min.css"
/> />
<link <link
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"

View File

@@ -17,7 +17,7 @@
<div class="diagrams"> <div class="diagrams">
<pre class="mermaid"> <pre class="mermaid">
packet-beta packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"
@@ -44,7 +44,7 @@
packet: packet:
showBits: false showBits: false
--- ---
packet-beta packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"
@@ -70,7 +70,7 @@
config: config:
theme: forest theme: forest
--- ---
packet-beta packet
title Forest theme title Forest theme
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
@@ -97,7 +97,7 @@
config: config:
theme: dark theme: dark
--- ---
packet-beta packet
title Dark theme title Dark theme
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"

View File

@@ -301,7 +301,7 @@ If you are adding a feature, you will definitely need to add tests. Depending on
Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run. Unit tests are 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. 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 ./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 #### Integration / End-to-End (E2E) Tests
These test the rendering and visual appearance of the diagrams. These test the rendering and visual appearance of the diagrams.

View File

@@ -111,3 +111,13 @@ const themes = {
``` ```
The actual options and values for the colors are defined in **src/theme/theme-\[xyz].js**. If you provide the options your diagram needs in the existing theme files then the theming will work smoothly without hiccups. The actual options and values for the colors are defined in **src/theme/theme-\[xyz].js**. If you provide the options your diagram needs in the existing theme files then the theming will work smoothly without hiccups.
## Examples
The `@mermaid-js/examples` package contains a collection of examples that are used by tools like mermaid.live to help users get started with the new diagram.
You can duplicate an existing diagram example file, eg: `packages/examples/src/examples/flowchart.ts`, and modify it with details specific to your diagram.
Then you can 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, and that should be marked as default. It is good to add more examples to showcase different features of the diagram.

View File

@@ -10,7 +10,7 @@
# Interface: ExternalDiagramDefinition # Interface: ExternalDiagramDefinition
Defined in: [packages/mermaid/src/diagram-api/types.ts:99](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L99) Defined in: [packages/mermaid/src/diagram-api/types.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L94)
## Properties ## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:99](https://github.com/me
> **detector**: `DiagramDetector` > **detector**: `DiagramDetector`
Defined in: [packages/mermaid/src/diagram-api/types.ts:101](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L101) Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
--- ---
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:101](https://github.com/m
> **id**: `string` > **id**: `string`
Defined in: [packages/mermaid/src/diagram-api/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L100) Defined in: [packages/mermaid/src/diagram-api/types.ts:95](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L95)
--- ---
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:100](https://github.com/m
> **loader**: `DiagramLoader` > **loader**: `DiagramLoader`
Defined in: [packages/mermaid/src/diagram-api/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L102) Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)

View File

@@ -10,7 +10,7 @@
# Interface: Mermaid # Interface: Mermaid
Defined in: [packages/mermaid/src/mermaid.ts:418](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L418) Defined in: [packages/mermaid/src/mermaid.ts:429](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L429)
## Properties ## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:418](https://github.com/mermaid-js/
> **contentLoaded**: () => `void` > **contentLoaded**: () => `void`
Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436) Defined in: [packages/mermaid/src/mermaid.ts:447](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L447)
\##contentLoaded Callback function that is called when page is loaded. This functions fetches \##contentLoaded Callback function that is called when page is loaded. This functions fetches
configuration for mermaid rendering and calls init for rendering the mermaid diagrams on the configuration for mermaid rendering and calls init for rendering the mermaid diagrams on the
@@ -34,7 +34,7 @@ page.
> **detectType**: (`text`, `config`?) => `string` > **detectType**: (`text`, `config`?) => `string`
Defined in: [packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438) Defined in: [packages/mermaid/src/mermaid.ts:449](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L449)
Detects the type of the graph text. Detects the type of the graph text.
@@ -86,11 +86,28 @@ A graph definition key
--- ---
### getRegisteredDiagramsMetadata()
> **getRegisteredDiagramsMetadata**: () => `Pick`<[`ExternalDiagramDefinition`](ExternalDiagramDefinition.md), `"id"`>\[]
Defined in: [packages/mermaid/src/mermaid.ts:451](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L451)
Gets the metadata for all registered diagrams.
Currently only the id is returned.
#### Returns
`Pick`<[`ExternalDiagramDefinition`](ExternalDiagramDefinition.md), `"id"`>\[]
An array of objects with the id of the diagram.
---
### ~~init()~~ ### ~~init()~~
> **init**: (`config`?, `nodes`?, `callback`?) => `Promise`<`void`> > **init**: (`config`?, `nodes`?, `callback`?) => `Promise`<`void`>
Defined in: [packages/mermaid/src/mermaid.ts:431](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L431) Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442)
## init ## init
@@ -138,7 +155,7 @@ Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
> **initialize**: (`config`) => `void` > **initialize**: (`config`) => `void`
Defined in: [packages/mermaid/src/mermaid.ts:435](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L435) Defined in: [packages/mermaid/src/mermaid.ts:446](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L446)
Used to set configurations for mermaid. Used to set configurations for mermaid.
This function should be called before the run function. This function should be called before the run function.
@@ -161,7 +178,7 @@ Configuration object for mermaid.
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>; `render`: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }> > **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>; `render`: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
Defined in: [packages/mermaid/src/mermaid.ts:425](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L425) Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
**`Internal`** **`Internal`**
@@ -175,7 +192,7 @@ Use [parse](Mermaid.md#parse) and [render](Mermaid.md#render) instead. Please [o
> **parse**: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)> > **parse**: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>
Defined in: [packages/mermaid/src/mermaid.ts:426](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L426) Defined in: [packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437)
Parse the text and validate the syntax. Parse the text and validate the syntax.
@@ -243,7 +260,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not
> `optional` **parseError**: [`ParseErrorFunction`](../type-aliases/ParseErrorFunction.md) > `optional` **parseError**: [`ParseErrorFunction`](../type-aliases/ParseErrorFunction.md)
Defined in: [packages/mermaid/src/mermaid.ts:420](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L420) Defined in: [packages/mermaid/src/mermaid.ts:431](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L431)
--- ---
@@ -251,7 +268,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:420](https://github.com/mermaid-js/
> **registerExternalDiagrams**: (`diagrams`, `opts`) => `Promise`<`void`> > **registerExternalDiagrams**: (`diagrams`, `opts`) => `Promise`<`void`>
Defined in: [packages/mermaid/src/mermaid.ts:434](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L434) Defined in: [packages/mermaid/src/mermaid.ts:445](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L445)
Used to register external diagram types. Used to register external diagram types.
@@ -281,7 +298,7 @@ If opts.lazyLoad is false, the diagrams will be loaded immediately.
> **registerIconPacks**: (`iconLoaders`) => `void` > **registerIconPacks**: (`iconLoaders`) => `void`
Defined in: [packages/mermaid/src/mermaid.ts:439](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L439) Defined in: [packages/mermaid/src/mermaid.ts:450](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L450)
#### Parameters #### Parameters
@@ -299,7 +316,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:439](https://github.com/mermaid-js/
> **registerLayoutLoaders**: (`loaders`) => `void` > **registerLayoutLoaders**: (`loaders`) => `void`
Defined in: [packages/mermaid/src/mermaid.ts:433](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L433) Defined in: [packages/mermaid/src/mermaid.ts:444](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L444)
#### Parameters #### Parameters
@@ -317,7 +334,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:433](https://github.com/mermaid-js/
> **render**: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)> > **render**: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>
Defined in: [packages/mermaid/src/mermaid.ts:427](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L427) Defined in: [packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438)
#### Parameters #### Parameters
@@ -349,7 +366,7 @@ Deprecated for external use.
> **run**: (`options`) => `Promise`<`void`> > **run**: (`options`) => `Promise`<`void`>
Defined in: [packages/mermaid/src/mermaid.ts:432](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L432) Defined in: [packages/mermaid/src/mermaid.ts:443](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L443)
## run ## run
@@ -393,7 +410,7 @@ Optional runtime configs
> **setParseErrorHandler**: (`parseErrorHandler`) => `void` > **setParseErrorHandler**: (`parseErrorHandler`) => `void`
Defined in: [packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437) Defined in: [packages/mermaid/src/mermaid.ts:448](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L448)
## setParseErrorHandler Alternative to directly setting parseError using: ## setParseErrorHandler Alternative to directly setting parseError using:
@@ -424,4 +441,4 @@ New parseError() callback.
> **startOnLoad**: `boolean` > **startOnLoad**: `boolean`
Defined in: [packages/mermaid/src/mermaid.ts:419](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L419) Defined in: [packages/mermaid/src/mermaid.ts:430](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L430)

View File

@@ -12,4 +12,4 @@
> **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`> > **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
Defined in: [packages/mermaid/src/diagram-api/types.ts:130](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L130) Defined in: [packages/mermaid/src/diagram-api/types.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L126)

View File

@@ -12,4 +12,4 @@
> **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`> > **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
Defined in: [packages/mermaid/src/diagram-api/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L132) Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)

View File

@@ -12,4 +12,4 @@
> `const` **default**: [`Mermaid`](../interfaces/Mermaid.md) > `const` **default**: [`Mermaid`](../interfaces/Mermaid.md)
Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442) Defined in: [packages/mermaid/src/mermaid.ts:454](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L454)

View File

@@ -17,7 +17,7 @@ This diagram type is particularly useful for developers, network engineers, educ
## Syntax ## Syntax
``` ```
packet-beta packet
start: "Block name" %% Single-bit block start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks start-end: "Block name" %% Multi-bit blocks
... More Fields ... ... More Fields ...
@@ -28,7 +28,7 @@ start-end: "Block name" %% Multi-bit blocks
Using start and end bit counts can be difficult, especially when modifying a design. For this we add a bit count field, which starts from the end of the previous field automagically. Use `+<count>` to set the number of bits, thus: Using start and end bit counts can be difficult, especially when modifying a design. For this we add a bit count field, which starts from the end of the previous field automagically. Use `+<count>` to set the number of bits, thus:
``` ```
packet-beta packet
+1: "Block name" %% Single-bit block +1: "Block name" %% Single-bit block
+8: "Block name" %% 8-bit block +8: "Block name" %% 8-bit block
9-15: "Manually set start and end, it's fine to mix and match" 9-15: "Manually set start and end, it's fine to mix and match"
@@ -41,7 +41,7 @@ packet-beta
--- ---
title: "TCP Packet" title: "TCP Packet"
--- ---
packet-beta packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"
@@ -65,7 +65,7 @@ packet-beta
--- ---
title: "TCP Packet" title: "TCP Packet"
--- ---
packet-beta packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"
@@ -86,7 +86,7 @@ packet-beta
``` ```
```mermaid-example ```mermaid-example
packet-beta packet
title UDP Packet title UDP Packet
+16: "Source Port" +16: "Source Port"
+16: "Destination Port" +16: "Destination Port"
@@ -96,7 +96,7 @@ title UDP Packet
``` ```
```mermaid ```mermaid
packet-beta packet
title UDP Packet title UDP Packet
+16: "Source Port" +16: "Source Port"
+16: "Destination Port" +16: "Destination Port"
@@ -144,7 +144,7 @@ config:
packet: packet:
startByteColor: red startByteColor: red
--- ---
packet-beta packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"

View File

@@ -64,7 +64,7 @@
}, },
"devDependencies": { "devDependencies": {
"@applitools/eyes-cypress": "^3.44.9", "@applitools/eyes-cypress": "^3.44.9",
"@argos-ci/cypress": "^4.0.3", "@argos-ci/cypress": "^5.0.2",
"@changesets/changelog-github": "^0.5.1", "@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.27.12", "@changesets/cli": "^2.27.12",
"@cspell/eslint-plugin": "^8.19.3", "@cspell/eslint-plugin": "^8.19.3",
@@ -88,8 +88,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cpy-cli": "^5.0.0", "cpy-cli": "^5.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cspell": "^8.6.1", "cspell": "^9.1.3",
"cypress": "^14.0.3", "cypress": "^14.5.1",
"cypress-image-snapshot": "^4.0.1", "cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.14", "cypress-split": "^1.24.14",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
@@ -109,25 +109,25 @@
"globals": "^16.0.0", "globals": "^16.0.0",
"globby": "^14.0.2", "globby": "^14.0.2",
"husky": "^9.1.7", "husky": "^9.1.7",
"jest": "^29.7.0", "jest": "^30.0.4",
"jison": "^0.4.18", "jison": "^0.4.18",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsdom": "^26.0.0", "jsdom": "^26.1.0",
"langium-cli": "3.3.0", "langium-cli": "3.3.0",
"lint-staged": "^15.2.11", "lint-staged": "^16.1.2",
"markdown-table": "^3.0.4", "markdown-table": "^3.0.4",
"nyc": "^17.1.0", "nyc": "^17.1.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"prettier": "^3.5.2", "prettier": "^3.5.2",
"prettier-plugin-jsdoc": "^1.3.2", "prettier-plugin-jsdoc": "^1.3.2",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^5.14.0", "rollup-plugin-visualizer": "^6.0.3",
"start-server-and-test": "^2.0.10", "start-server-and-test": "^2.0.10",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"tsx": "^4.7.3", "tsx": "^4.7.3",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"typescript-eslint": "^8.32.0", "typescript-eslint": "^8.32.0",
"vite": "^6.1.1", "vite": "^7.0.3",
"vite-plugin-istanbul": "^7.0.0", "vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.0.6" "vitest": "^3.0.6"
}, },
@@ -139,8 +139,13 @@
"roughjs": "patches/roughjs.patch" "roughjs": "patches/roughjs.patch"
}, },
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"canvas",
"cypress", "cypress",
"esbuild" "esbuild"
],
"ignoredBuiltDependencies": [
"sharp",
"vue-demi"
] ]
} }
} }

View File

@@ -0,0 +1,32 @@
{
"name": "@mermaid-js/examples",
"version": "0.0.1-beta.1",
"description": "Mermaid examples package",
"author": "Sidharth Vinod",
"type": "module",
"module": "./dist/mermaid-examples.core.mjs",
"types": "./dist/mermaid.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/mermaid-examples.core.mjs",
"default": "./dist/mermaid-examples.core.mjs"
}
},
"files": [
"dist"
],
"scripts": {
"clean": "rimraf dist"
},
"dependencies": {},
"devDependencies": {
"mermaid": "workspace:*"
},
"peerDependencies": {
"mermaid": "workspace:~"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,34 @@
import mermaid from 'mermaid';
import { diagramData } from './index.js';
describe('examples', () => {
beforeAll(async () => {
// To trigger the diagram registration
await mermaid.registerExternalDiagrams([]);
});
it('should have examples for each diagrams', () => {
const skippedDiagrams = [
// These diagrams have no examples
'error',
'info',
'---',
// These diagrams have v2 versions, with examples
'class',
'graph',
'flowchart-elk',
'flowchart',
'state',
];
const diagrams = mermaid
.getRegisteredDiagramsMetadata()
.filter((d) => !skippedDiagrams.includes(d.id));
expect(diagrams.length).toBeGreaterThan(0);
for (const diagram of diagrams) {
const data = diagramData.find((d) => d.id === diagram.id)!;
expect(data, `Example for ${diagram.id} is not defined`).toBeDefined();
expect(data.examples.length).toBeGreaterThan(0);
expect(data.examples.filter((e) => e.isDefault).length).toBe(1);
}
});
});

View File

@@ -0,0 +1,24 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'architecture',
name: 'Architecture Diagram',
description: 'Visualize system architecture and components',
examples: [
{
title: 'Basic System Architecture',
isDefault: true,
code: `architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,27 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'block',
name: 'Block Diagram',
description: 'Create block-based visualizations with beta styling',
examples: [
{
title: 'Basic Block Layout',
isDefault: true,
code: `block-beta
columns 1
db(("DB"))
blockArrowId6<["&nbsp;&nbsp;&nbsp;"]>(down)
block:ID
A
B["A wide one in the middle"]
C
end
space
D
ID --> D
C --> D
style B fill:#969,stroke:#333,stroke-width:4px`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,47 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'c4',
name: 'C4 Diagram',
description:
'Visualize software architecture using the C4 model (Context, Container, Component, Code)',
examples: [
{
title: 'Internet Banking System Context',
isDefault: true,
code: `C4Context
title System Context diagram for Internet Banking System
Enterprise_Boundary(b0, "BankBoundary0") {
Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.")
Person(customerB, "Banking Customer B")
Person_Ext(customerC, "Banking Customer C", "desc")
Person(customerD, "Banking Customer D", "A customer of the bank, <br/> with personal bank accounts.")
System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.")
Enterprise_Boundary(b1, "BankBoundary") {
SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.")
System_Boundary(b2, "BankBoundary2") {
System(SystemA, "Banking System A")
System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts. next line.")
}
System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.")
SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.")
Boundary(b3, "BankBoundary3", "boundary") {
SystemQueue(SystemF, "Banking System F Queue", "A system of the bank.")
SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.")
}
}
}
BiRel(customerA, SystemAA, "Uses")
BiRel(SystemAA, SystemE, "Uses")
Rel(SystemAA, SystemC, "Sends e-mails", "SMTP")
Rel(SystemC, customerA, "Sends e-mails to")`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,34 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'classDiagram',
name: 'Class Diagram',
description: 'Visualize class structures and relationships in object-oriented programming',
examples: [
{
title: 'Basic Class Inheritance',
isDefault: true,
code: `classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,36 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'er',
name: 'Entity Relationship Diagram',
description: 'Visualize database schemas and relationships between entities',
examples: [
{
title: 'Basic ER Schema',
isDefault: true,
code: `erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_ITEM : contains
PRODUCT ||--o{ ORDER_ITEM : includes
CUSTOMER {
string id
string name
string email
}
ORDER {
string id
date orderDate
string status
}
PRODUCT {
string id
string name
float price
}
ORDER_ITEM {
int quantity
float price
}`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,19 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'flowchart-v2',
name: 'Flowchart',
description: 'Visualize flowcharts and directed graphs',
examples: [
{
title: 'Basic Flowchart',
isDefault: true,
code: `flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,22 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'gantt',
name: 'Gantt Chart',
description: 'Visualize project schedules and timelines',
examples: [
{
title: 'Basic Project Timeline',
isDefault: true,
code: `gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,28 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'gitGraph',
name: 'Git Graph',
description: 'Visualize Git repository history and branch relationships',
examples: [
{
title: 'Basic Git Flow',
isDefault: true,
code: `gitGraph
commit
branch develop
checkout develop
commit
commit
checkout main
merge develop
commit
branch feature
checkout feature
commit
commit
checkout main
merge feature`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,37 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'kanban',
name: 'Kanban Diagram',
description: 'Visualize work items in a Kanban board',
examples: [
{
title: 'Kanban Diagram',
isDefault: true,
code: `---
config:
kanban:
ticketBaseUrl: 'https://github.com/mermaid-js/mermaid/issues/#TICKET#'
---
kanban
Todo
[Create Documentation]
docs[Create Blog about the new diagram]
[In progress]
id6[Create renderer so that it works in all cases. We also add some extra text here for testing purposes. And some more just for the extra flare.]
id9[Ready for deploy]
id8[Design grammar]@{ assigned: 'knsv' }
id10[Ready for test]
id4[Create parsing tests]@{ ticket: 2038, assigned: 'K.Sveidqvist', priority: 'High' }
id66[last item]@{ priority: 'Very Low', assigned: 'knsv' }
id11[Done]
id5[define getData]
id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char]@{ ticket: 2036, priority: 'Very High'}
id3[Update DB function]@{ ticket: 2037, assigned: knsv, priority: 'High' }
id12[Can't reproduce]
id3[Weird flickering in Firefox]
`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,32 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'mindmap',
name: 'Mindmap',
description: 'Visualize ideas and concepts in a tree-like structure',
examples: [
{
title: 'Basic Mindmap',
isDefault: true,
code: `mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid`,
},
],
} satisfies DiagramMetadata;
// cspell:ignore Buzan

View File

@@ -0,0 +1,34 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'packet',
name: 'Packet Diagram',
description: 'Visualize packet data and network traffic',
examples: [
{
title: 'TCP Packet',
isDefault: true,
code: `---
title: "TCP Packet"
---
packet
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-255: "Data (variable length)"`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,17 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'pie',
name: 'Pie Chart',
description: 'Visualize data as proportional segments of a circle',
examples: [
{
title: 'Basic Pie Chart',
isDefault: true,
code: `pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 15`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,27 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'quadrantChart',
name: 'Quadrant Chart',
description: 'Visualize items in a 2x2 matrix based on two variables',
examples: [
{
title: 'Product Positioning',
isDefault: true,
code: `quadrantChart
title Reach and engagement of campaigns
x-axis Low Reach --> High Reach
y-axis Low Engagement --> High Engagement
quadrant-1 We should expand
quadrant-2 Need to promote
quadrant-3 Re-evaluate
quadrant-4 May be improved
Campaign A: [0.3, 0.6]
Campaign B: [0.45, 0.23]
Campaign C: [0.57, 0.69]
Campaign D: [0.78, 0.34]
Campaign E: [0.40, 0.34]
Campaign F: [0.35, 0.78]`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,25 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'radar',
name: 'Radar Diagram',
description: 'Visualize data in a radial format',
examples: [
{
title: 'Student Grades',
isDefault: true,
code: `---
title: "Grades"
---
radar-beta
axis m["Math"], s["Science"], e["English"]
axis h["History"], g["Geography"], a["Art"]
curve a["Alice"]{85, 90, 80, 70, 75, 90}
curve b["Bob"]{70, 75, 85, 80, 90, 85}
max 100
min 0
`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,27 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'requirement',
name: 'Requirement Diagram',
description: 'Visualize system requirements and their relationships',
examples: [
{
title: 'Basic Requirements',
code: `requirementDiagram
requirement test_req {
id: 1
text: the test text.
risk: high
verifymethod: test
}
element test_entity {
type: simulation
}
test_entity - satisfies -> test_req`,
isDefault: true,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,88 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'sankey',
name: 'Sankey Diagram',
description: 'Visualize flow quantities between different stages or processes',
examples: [
{
title: 'Energy Flow',
isDefault: true,
code: `---
config:
sankey:
showValues: false
---
sankey-beta
Agricultural 'waste',Bio-conversion,124.729
Bio-conversion,Liquid,0.597
Bio-conversion,Losses,26.862
Bio-conversion,Solid,280.322
Bio-conversion,Gas,81.144
Biofuel imports,Liquid,35
Biomass imports,Solid,35
Coal imports,Coal,11.606
Coal reserves,Coal,63.965
Coal,Solid,75.571
District heating,Industry,10.639
District heating,Heating and cooling - commercial,22.505
District heating,Heating and cooling - homes,46.184
Electricity grid,Over generation / exports,104.453
Electricity grid,Heating and cooling - homes,113.726
Electricity grid,H2 conversion,27.14
Electricity grid,Industry,342.165
Electricity grid,Road transport,37.797
Electricity grid,Agriculture,4.412
Electricity grid,Heating and cooling - commercial,40.858
Electricity grid,Losses,56.691
Electricity grid,Rail transport,7.863
Electricity grid,Lighting & appliances - commercial,90.008
Electricity grid,Lighting & appliances - homes,93.494
Gas imports,NGas,40.719
Gas reserves,NGas,82.233
Gas,Heating and cooling - commercial,0.129
Gas,Losses,1.401
Gas,Thermal generation,151.891
Gas,Agriculture,2.096
Gas,Industry,48.58
Geothermal,Electricity grid,7.013
H2 conversion,H2,20.897
H2 conversion,Losses,6.242
H2,Road transport,20.897
Hydro,Electricity grid,6.995
Liquid,Industry,121.066
Liquid,International shipping,128.69
Liquid,Road transport,135.835
Liquid,Domestic aviation,14.458
Liquid,International aviation,206.267
Liquid,Agriculture,3.64
Liquid,National navigation,33.218
Liquid,Rail transport,4.413
Marine algae,Bio-conversion,4.375
NGas,Gas,122.952
Nuclear,Thermal generation,839.978
Oil imports,Oil,504.287
Oil reserves,Oil,107.703
Oil,Liquid,611.99
Other waste,Solid,56.587
Other waste,Bio-conversion,77.81
Pumped heat,Heating and cooling - homes,193.026
Pumped heat,Heating and cooling - commercial,70.672
Solar PV,Electricity grid,59.901
Solar Thermal,Heating and cooling - homes,19.263
Solar,Solar Thermal,19.263
Solar,Solar PV,59.901
Solid,Agriculture,0.882
Solid,Thermal generation,400.12
Solid,Industry,46.477
Thermal generation,Electricity grid,525.531
Thermal generation,Losses,787.129
Thermal generation,District heating,79.329
Tidal,Electricity grid,9.452
UK land based bioenergy,Bio-conversion,182.01
Wave,Electricity grid,19.013
Wind,Electricity grid,289.366`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,18 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'sequence',
name: 'Sequence Diagram',
description: 'Visualize interactions between objects over time',
examples: [
{
title: 'Basic Sequence',
isDefault: true,
code: `sequenceDiagram
Alice->>+John: Hello John, how are you?
Alice->>+John: John, can you hear me?
John-->>-Alice: Hi Alice, I can hear you!
John-->>-Alice: I feel great!`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,20 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'stateDiagram',
name: 'State Diagram',
description: 'Visualize the states and transitions of a system',
examples: [
{
title: 'Basic State Diagram',
code: `stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]`,
isDefault: true,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,20 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'timeline',
name: 'Timeline Diagram',
description: 'Visualize events and milestones in chronological order',
examples: [
{
title: 'Project Timeline',
isDefault: true,
code: `timeline
title History of Social Media Platform
2002 : LinkedIn
2004 : Facebook
: Google
2005 : YouTube
2006 : Twitter`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,21 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'treemap',
name: 'Treemap',
description: 'Visualize hierarchical data as nested rectangles',
examples: [
{
title: 'Treemap',
isDefault: true,
code: `treemap-beta
"Section 1"
"Leaf 1.1": 12
"Section 1.2"
"Leaf 1.2.1": 12
"Section 2"
"Leaf 2.1": 20
"Leaf 2.2": 25`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,22 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'journey',
name: 'User Journey Diagram',
description: 'Visualize user interactions and experiences with a system',
examples: [
{
title: 'My Working Day',
isDefault: true,
code: `journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
Do work: 1: Me, Cat
section Go home
Go downstairs: 5: Me
Sit down: 5: Me`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,19 @@
import type { DiagramMetadata } from '../types.js';
export default {
id: 'xychart',
name: 'XY Chart',
description: 'Create scatter plots and line charts with customizable axes',
examples: [
{
title: 'Sales Revenue',
isDefault: true,
code: `xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]`,
},
],
} satisfies DiagramMetadata;

View File

@@ -0,0 +1,48 @@
import type { DiagramMetadata } from './types.js';
import flowChart from './examples/flowchart.js';
import c4 from './examples/c4.js';
import kanban from './examples/kanban.js';
import classDiagram from './examples/class.js';
import sequenceDiagram from './examples/sequence.js';
import pieDiagram from './examples/pie.js';
import userJourneyDiagram from './examples/user-journey.js';
import mindmapDiagram from './examples/mindmap.js';
import requirementDiagram from './examples/requirement.js';
import radarDiagram from './examples/radar.js';
import stateDiagram from './examples/state.js';
import erDiagram from './examples/er.js';
import gitDiagram from './examples/git.js';
import architectureDiagram from './examples/architecture.js';
import xychartDiagram from './examples/xychart.js';
import sankeyDiagram from './examples/sankey.js';
import ganttDiagram from './examples/gantt.js';
import timelineDiagram from './examples/timeline.js';
import quadrantChart from './examples/quadrant-chart.js';
import packetDiagram from './examples/packet.js';
import blockDiagram from './examples/block.js';
import treemapDiagram from './examples/treemap.js';
export const diagramData: DiagramMetadata[] = [
flowChart,
c4,
kanban,
classDiagram,
sequenceDiagram,
pieDiagram,
userJourneyDiagram,
mindmapDiagram,
requirementDiagram,
radarDiagram,
stateDiagram,
erDiagram,
gitDiagram,
architectureDiagram,
xychartDiagram,
sankeyDiagram,
ganttDiagram,
timelineDiagram,
quadrantChart,
packetDiagram,
blockDiagram,
treemapDiagram,
];

View File

@@ -0,0 +1,12 @@
export interface Example {
title: string;
code: string;
isDefault?: boolean;
}
export interface DiagramMetadata {
id: string;
name: string;
description: string;
examples: Example[];
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"module": "Node16",
"moduleResolution": "Node16"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,25 @@
# mermaid # mermaid
## 11.8.1
### Patch Changes
- Updated dependencies [[`0da2922`](https://github.com/mermaid-js/mermaid/commit/0da2922ee7f47959e324ec10d3d21ee70594f557)]:
- @mermaid-js/parser@0.6.1
## 11.8.0
### Minor Changes
- [#6590](https://github.com/mermaid-js/mermaid/pull/6590) [`f338802`](https://github.com/mermaid-js/mermaid/commit/f338802642cdecf5b7ed6c19a20cf2a81effbbee) Thanks [@knsv](https://github.com/knsv)! - Adding support for the new diagram type nested treemap
### Patch Changes
- [#6707](https://github.com/mermaid-js/mermaid/pull/6707) [`592c5bb`](https://github.com/mermaid-js/mermaid/commit/592c5bb880c3b942710a2878d386bcb3eb35c137) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Log a warning when duplicate commit IDs are encountered in gitGraph to help identify and debug rendering issues caused by non-unique IDs.
- Updated dependencies [[`f338802`](https://github.com/mermaid-js/mermaid/commit/f338802642cdecf5b7ed6c19a20cf2a81effbbee)]:
- @mermaid-js/parser@0.6.0
## 11.7.0 ## 11.7.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "mermaid", "name": "mermaid",
"version": "11.7.0", "version": "11.8.1",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.", "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module", "type": "module",
"module": "./dist/mermaid.core.mjs", "module": "./dist/mermaid.core.mjs",
@@ -79,10 +79,10 @@
"dagre-d3-es": "7.0.11", "dagre-d3-es": "7.0.11",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.2.5", "dompurify": "^3.2.5",
"katex": "^0.16.9", "katex": "^0.16.22",
"khroma": "^2.1.0", "khroma": "^2.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.7", "marked": "^16.0.0",
"roughjs": "^4.6.6", "roughjs": "^4.6.6",
"stylis": "^4.3.6", "stylis": "^4.3.6",
"ts-dedent": "^2.2.0", "ts-dedent": "^2.2.0",
@@ -105,13 +105,14 @@
"@types/stylis": "^4.2.7", "@types/stylis": "^4.2.7",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"canvas": "^3.1.0",
"chokidar": "3.6.0", "chokidar": "3.6.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"csstree-validator": "^4.0.1", "csstree-validator": "^4.0.1",
"globby": "^14.0.2", "globby": "^14.0.2",
"jison": "^0.4.18", "jison": "^0.4.18",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"jsdom": "^26.0.0", "jsdom": "^26.1.0",
"json-schema-to-typescript": "^15.0.4", "json-schema-to-typescript": "^15.0.4",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",

View File

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

View File

@@ -7,7 +7,9 @@ export const loadRegisteredDiagrams = async () => {
// Load all lazy loaded diagrams in parallel // Load all lazy loaded diagrams in parallel
const results = await Promise.allSettled( const results = await Promise.allSettled(
Object.entries(detectors).map(async ([key, { detector, loader }]) => { Object.entries(detectors).map(async ([key, { detector, loader }]) => {
if (loader) { if (!loader) {
return;
}
try { try {
getDiagram(key); getDiagram(key);
} catch { } catch {
@@ -22,7 +24,6 @@ export const loadRegisteredDiagrams = async () => {
throw err; throw err;
} }
} }
}
}) })
); );
const failed = results.filter((result) => result.status === 'rejected'); const failed = results.filter((result) => result.status === 'rejected');

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type * as d3 from 'd3'; import type * as d3 from 'd3';
import type { SetRequired } from 'type-fest'; import type { SetOptional, SetRequired } from 'type-fest';
import type { Diagram } from '../Diagram.js'; import type { Diagram } from '../Diagram.js';
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js'; import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
@@ -91,17 +91,13 @@ export interface DiagramDefinition {
) => void; ) => void;
} }
export interface DetectorRecord {
detector: DiagramDetector;
loader?: DiagramLoader;
}
export interface ExternalDiagramDefinition { export interface ExternalDiagramDefinition {
id: string; id: string;
detector: DiagramDetector; detector: DiagramDetector;
loader: DiagramLoader; loader: DiagramLoader;
} }
export type DetectorRecord = SetOptional<Omit<ExternalDiagramDefinition, 'id'>, 'loader'>;
export type DiagramDetector = (text: string, config?: MermaidConfig) => boolean; export type DiagramDetector = (text: string, config?: MermaidConfig) => boolean;
export type DiagramLoader = () => Promise<{ id: string; diagram: DiagramDefinition }>; export type DiagramLoader = () => Promise<{ id: string; diagram: DiagramDefinition }>;

View File

@@ -379,6 +379,15 @@ function layoutArchitecture(
}, },
}, },
], ],
layout: {
name: 'grid',
boundingBox: {
x1: 0,
x2: 100,
y1: 0,
y2: 100,
},
},
}); });
// Remove element after layout // Remove element after layout
renderEl.remove(); renderEl.remove();

View File

@@ -287,7 +287,7 @@ const setBlock = (block: Block) => {
blockDatabase.set(block.id, block); blockDatabase.set(block.id, block);
}; };
const getLogger = () => console; const getLogger = () => log;
/** /**
* Return all of the style classes * Return all of the style classes

View File

@@ -1,4 +1,4 @@
import { rejects } from 'assert'; import { log } from '../../logger.js';
import { db } from './gitGraphAst.js'; import { db } from './gitGraphAst.js';
import { parser } from './gitGraphParser.js'; import { parser } from './gitGraphParser.js';
@@ -1319,4 +1319,42 @@ describe('when parsing a gitGraph', function () {
} }
}); });
}); });
it('should log a warning when two commits have the same ID', async () => {
const str = `gitGraph
commit id:"initial commit"
commit id:"work on first release"
commit id:"design freeze from here"
branch v1-rc
checkout v1-rc
commit id:"bugfix 1"
commit id:"bigfix 2" tag:"v1.0.1"
branch FORK-v1.0-MDR
checkout FORK-v1.0-MDR
commit id:"working on MDR"
checkout v1-rc
commit id:"minor design changes for MDR" tag:"v1.0.2"
checkout FORK-v1.0-MDR
merge v1-rc
checkout main
commit id:"new feature for v1.1…"
checkout FORK-v1.0-MDR
commit id:"working on MDR"
commit id:"finishing MDR"
branch v1.0-MDR
checkout v1.0-MDR
commit id:"brush up release" tag:"v1.0.2-MDR"
checkout v1-rc
commit id:"bugfix without MDR"
checkout main
commit id:"work on v1.1"
`;
const logWarnSpy = vi.spyOn(log, 'warn').mockImplementation(() => undefined);
await parser.parse(str);
expect(logWarnSpy).toHaveBeenCalledWith('Commit ID working on MDR already exists');
logWarnSpy.mockRestore();
});
}); });

View File

@@ -125,6 +125,9 @@ export const commit = function (commitDB: CommitDB) {
}; };
state.records.head = newCommit; state.records.head = newCommit;
log.info('main branch', config.mainBranchName); log.info('main branch', config.mainBranchName);
if (state.records.commits.has(newCommit.id)) {
log.warn(`Commit ID ${newCommit.id} already exists`);
}
state.records.commits.set(newCommit.id, newCommit); state.records.commits.set(newCommit.id, newCommit);
state.records.branches.set(state.records.currBranch, newCommit.id); state.records.branches.set(state.records.currBranch, newCommit.id);
log.debug('in pushCommit ' + newCommit.id); log.debug('in pushCommit ' + newCommit.id);

View File

@@ -1,12 +1,14 @@
// @ts-ignore: JISON doesn't support types // @ts-ignore: JISON doesn't support types
import parser from './parser/mindmap.jison'; import parser from './parser/mindmap.jison';
import db from './mindmapDb.js'; import { MindmapDB } from './mindmapDb.js';
import renderer from './mindmapRenderer.js'; import renderer from './mindmapRenderer.js';
import styles from './styles.js'; import styles from './styles.js';
import type { DiagramDefinition } from '../../diagram-api/types.js'; import type { DiagramDefinition } from '../../diagram-api/types.js';
export const diagram: DiagramDefinition = { export const diagram: DiagramDefinition = {
db, get db() {
return new MindmapDB();
},
renderer, renderer,
parser, parser,
styles, styles,

View File

@@ -1,12 +1,12 @@
// @ts-expect-error No types available for JISON // @ts-expect-error No types available for JISON
import { parser as mindmap } from './parser/mindmap.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 // Todo fix utils functions for tests
import { setLogLevel } from '../../diagram-api/diagramAPI.js'; import { setLogLevel } from '../../diagram-api/diagramAPI.js';
describe('when parsing a mindmap ', function () { describe('when parsing a mindmap ', function () {
beforeEach(function () { beforeEach(function () {
mindmap.yy = mindmapDB; mindmap.yy = new MindmapDB();
mindmap.yy.clear(); mindmap.yy.clear();
setLogLevel('trace'); setLogLevel('trace');
}); });

View File

@@ -5,70 +5,6 @@ import { log } from '../../logger.js';
import type { MindmapNode } from './mindmapTypes.js'; import type { MindmapNode } from './mindmapTypes.js';
import defaultConfig from '../../defaultConfig.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 = { const nodeType = {
DEFAULT: 0, DEFAULT: 0,
NO_BORDER: 0, NO_BORDER: 0,
@@ -78,82 +14,149 @@ const nodeType = {
CLOUD: 4, CLOUD: 4,
BANG: 5, BANG: 5,
HEXAGON: 6, HEXAGON: 6,
}; } as const;
const getType = (startStr: string, endStr: string): number => { 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); log.debug('In get type', startStr, endStr);
switch (startStr) { switch (startStr) {
case '[': case '[':
return nodeType.RECT; return this.nodeType.RECT;
case '(': case '(':
return endStr === ')' ? nodeType.ROUNDED_RECT : nodeType.CLOUD; return endStr === ')' ? this.nodeType.ROUNDED_RECT : this.nodeType.CLOUD;
case '((': case '((':
return nodeType.CIRCLE; return this.nodeType.CIRCLE;
case ')': case ')':
return nodeType.CLOUD; return this.nodeType.CLOUD;
case '))': case '))':
return nodeType.BANG; return this.nodeType.BANG;
case '{{': case '{{':
return nodeType.HEXAGON; return this.nodeType.HEXAGON;
default: default:
return nodeType.DEFAULT; return this.nodeType.DEFAULT;
}
} }
};
const setElementForId = (id: number, element: D3Element) => { public setElementForId(id: number, element: D3Element): void {
elements[id] = element; this.elements[id] = element;
}; }
public getElementById(id: number) {
return this.elements[id];
}
const decorateNode = (decoration?: { class?: string; icon?: string }) => { public decorateNode(decoration?: { class?: string; icon?: string }): void {
if (!decoration) { if (!decoration) {
return; return;
} }
const config = getConfig(); const config = getConfig();
const node = nodes[nodes.length - 1]; const node = this.nodes[this.nodes.length - 1];
if (decoration.icon) { if (decoration.icon) {
node.icon = sanitizeText(decoration.icon, config); node.icon = sanitizeText(decoration.icon, config);
} }
if (decoration.class) { if (decoration.class) {
node.class = sanitizeText(decoration.class, config); node.class = sanitizeText(decoration.class, config);
} }
}; }
const type2Str = (type: number) => { type2Str(type: number): string {
switch (type) { switch (type) {
case nodeType.DEFAULT: case this.nodeType.DEFAULT:
return 'no-border'; return 'no-border';
case nodeType.RECT: case this.nodeType.RECT:
return 'rect'; return 'rect';
case nodeType.ROUNDED_RECT: case this.nodeType.ROUNDED_RECT:
return 'rounded-rect'; return 'rounded-rect';
case nodeType.CIRCLE: case this.nodeType.CIRCLE:
return 'circle'; return 'circle';
case nodeType.CLOUD: case this.nodeType.CLOUD:
return 'cloud'; return 'cloud';
case nodeType.BANG: case this.nodeType.BANG:
return 'bang'; return 'bang';
case nodeType.HEXAGON: case this.nodeType.HEXAGON:
return 'hexgon'; // cspell: disable-line return 'hexgon'; // cspell: disable-line
default: default:
return 'no-border'; return 'no-border';
} }
}; }
// Expose logger to grammar public getLogger() {
const getLogger = () => log; return log;
const getElementById = (id: number) => elements[id]; }
}
const db = {
clear,
addNode,
getMindmap,
nodeType,
getType,
setElementForId,
decorateNode,
type2Str,
getLogger,
getElementById,
} as const;
export default db;

View File

@@ -9,10 +9,10 @@ import { log } from '../../logger.js';
import type { D3Element } from '../../types.js'; import type { D3Element } from '../../types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.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 { drawNode, positionNode } from './svgDraw.js';
import defaultConfig from '../../defaultConfig.js'; import defaultConfig from '../../defaultConfig.js';
import type { MindmapDB } from './mindmapDb.js';
// Inject the layout algorithm into cytoscape // Inject the layout algorithm into cytoscape
cytoscape.use(coseBilkent); cytoscape.use(coseBilkent);

View File

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

View File

@@ -1,8 +1,9 @@
import { createText } from '../../rendering-util/createText.js'; 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 type { Point, D3Element } from '../../types.js';
import { parseFontSize } from '../../utils.js'; import { parseFontSize } from '../../utils.js';
import type { MermaidConfig } from '../../config.type.js'; import type { MermaidConfig } from '../../config.type.js';
import type { MindmapDB } from './mindmapDb.js';
const MAX_SECTIONS = 12; const MAX_SECTIONS = 12;

View File

@@ -7,7 +7,7 @@ import type {
const id = 'packet'; const id = 'packet';
const detector: DiagramDetector = (txt) => { const detector: DiagramDetector = (txt) => {
return /^\s*packet-beta/.test(txt); return /^\s*packet(-beta)?/.test(txt);
}; };
const loader: DiagramLoader = async () => { const loader: DiagramLoader = async () => {

View File

@@ -15,8 +15,14 @@ describe('packet diagrams', () => {
expect(getPacket()).toMatchInlineSnapshot('[]'); expect(getPacket()).toMatchInlineSnapshot('[]');
}); });
it('should handle a packet definition', async () => {
const str = `packet`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle diagram with data and title', async () => { it('should handle diagram with data and title', async () => {
const str = `packet-beta const str = `packet
title Packet diagram title Packet diagram
accTitle: Packet accTitle accTitle: Packet accTitle
accDescr: Packet accDescription accDescr: Packet accDescription
@@ -41,7 +47,7 @@ describe('packet diagrams', () => {
}); });
it('should handle single bits', async () => { it('should handle single bits', async () => {
const str = `packet-beta const str = `packet
0-10: "test" 0-10: "test"
11: "single" 11: "single"
`; `;
@@ -67,7 +73,7 @@ describe('packet diagrams', () => {
}); });
it('should handle bit counts', async () => { it('should handle bit counts', async () => {
const str = `packet-beta const str = `packet
+8: "byte" +8: "byte"
+16: "word" +16: "word"
`; `;
@@ -93,7 +99,7 @@ describe('packet diagrams', () => {
}); });
it('should handle bit counts with bit or bits', async () => { it('should handle bit counts with bit or bits', async () => {
const str = `packet-beta const str = `packet
+8: "byte" +8: "byte"
+16: "word" +16: "word"
`; `;
@@ -119,7 +125,7 @@ describe('packet diagrams', () => {
}); });
it('should split into multiple rows', async () => { it('should split into multiple rows', async () => {
const str = `packet-beta const str = `packet
0-10: "test" 0-10: "test"
11-90: "multiple" 11-90: "multiple"
`; `;
@@ -161,7 +167,7 @@ describe('packet diagrams', () => {
}); });
it('should split into multiple rows when cut at exact length', async () => { it('should split into multiple rows when cut at exact length', async () => {
const str = `packet-beta const str = `packet
0-16: "test" 0-16: "test"
17-63: "multiple" 17-63: "multiple"
`; `;
@@ -195,7 +201,7 @@ describe('packet diagrams', () => {
}); });
it('should throw error if numbers are not continuous', async () => { it('should throw error if numbers are not continuous', async () => {
const str = `packet-beta const str = `packet
0-16: "test" 0-16: "test"
18-20: "error" 18-20: "error"
`; `;
@@ -205,7 +211,7 @@ describe('packet diagrams', () => {
}); });
it('should throw error if numbers are not continuous with bit counts', async () => { it('should throw error if numbers are not continuous with bit counts', async () => {
const str = `packet-beta const str = `packet
+16: "test" +16: "test"
18-20: "error" 18-20: "error"
`; `;
@@ -215,7 +221,7 @@ describe('packet diagrams', () => {
}); });
it('should throw error if numbers are not continuous for single packets', async () => { it('should throw error if numbers are not continuous for single packets', async () => {
const str = `packet-beta const str = `packet
0-16: "test" 0-16: "test"
18: "error" 18: "error"
`; `;
@@ -225,7 +231,7 @@ describe('packet diagrams', () => {
}); });
it('should throw error if numbers are not continuous for single packets with bit counts', async () => { it('should throw error if numbers are not continuous for single packets with bit counts', async () => {
const str = `packet-beta const str = `packet
+16: "test" +16: "test"
18: "error" 18: "error"
`; `;
@@ -235,7 +241,7 @@ describe('packet diagrams', () => {
}); });
it('should throw error if numbers are not continuous for single packets - 2', async () => { it('should throw error if numbers are not continuous for single packets - 2', async () => {
const str = `packet-beta const str = `packet
0-16: "test" 0-16: "test"
17: "good" 17: "good"
19: "error" 19: "error"
@@ -246,7 +252,7 @@ describe('packet diagrams', () => {
}); });
it('should throw error if end is less than start', async () => { it('should throw error if end is less than start', async () => {
const str = `packet-beta const str = `packet
0-16: "test" 0-16: "test"
25-20: "error" 25-20: "error"
`; `;
@@ -256,7 +262,7 @@ describe('packet diagrams', () => {
}); });
it('should throw error if bit count is 0', async () => { it('should throw error if bit count is 0', async () => {
const str = `packet-beta const str = `packet
+0: "test" +0: "test"
`; `;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot( await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(

View File

@@ -524,7 +524,7 @@ export const drawBox = function (elem, box, conf) {
box.name, box.name,
g, g,
box.x, box.x,
box.y + (box.textMaxHeight || 0) / 2, box.y + conf.boxTextMargin + (box.textMaxHeight || 0) / 2,
box.width, box.width,
0, 0,
{ class: 'text' }, { class: 'text' },

View File

@@ -28,8 +28,7 @@ interface TimelineTask {
export const draw = function (text: string, id: string, version: string, diagObj: Diagram) { export const draw = function (text: string, id: string, version: string, diagObj: Diagram) {
//1. Fetch the configuration //1. Fetch the configuration
const conf = getConfig(); const conf = getConfig();
// @ts-expect-error - wrong config? const LEFT_MARGIN = conf.timeline?.leftMargin ?? 50;
const LEFT_MARGIN = conf.leftMargin ?? 50;
log.debug('timeline', diagObj.db); log.debug('timeline', diagObj.db);

View File

@@ -1,10 +1,10 @@
import { getConfig as commonGetConfig } from '../../config.js'; import type { DiagramDB } from '../../diagram-api/types.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import type { DiagramStyleClassDef } 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 { cleanAndMerge } from '../../utils.js';
import { ImperativeState } from '../../utils/imperativeState.js'; import { isLabelStyle } from '../../rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import { import {
clear as commonClear, clear as commonClear,
getAccDescription, getAccDescription,
@@ -14,54 +14,42 @@ import {
setAccTitle, setAccTitle,
setDiagramTitle, setDiagramTitle,
} from '../common/commonDb.js'; } 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 = { public getNodes() {
nodes: [], return this.nodes;
levels: new Map(), }
outerNodes: [],
classes: new Map(),
};
const state = new ImperativeState<TreemapData>(() => structuredClone(defaultTreemapData)); public getConfig() {
const getConfig = (): Required<TreemapDiagramConfig> => {
// Use type assertion with unknown as intermediate step
const defaultConfig = DEFAULT_CONFIG as unknown as { treemap: Required<TreemapDiagramConfig> }; const defaultConfig = DEFAULT_CONFIG as unknown as { treemap: Required<TreemapDiagramConfig> };
const userConfig = commonGetConfig() as unknown as { treemap?: Partial<TreemapDiagramConfig> }; const userConfig = commonGetConfig() as unknown as { treemap?: Partial<TreemapDiagramConfig> };
return cleanAndMerge({ return cleanAndMerge({
...defaultConfig.treemap, ...defaultConfig.treemap,
...(userConfig.treemap ?? {}), ...(userConfig.treemap ?? {}),
}) as Required<TreemapDiagramConfig>; }) 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);
public addNode(node: TreemapNode, level: number) {
this.nodes.push(node);
this.levels.set(node, level);
if (level === 0) { if (level === 0) {
data.outerNodes.push(node); this.outerNodes.push(node);
this.root ??= node;
}
} }
// Set the root node if this is a level 0 node and we don't have a root yet public getRoot() {
if (level === 0 && !data.root) { return { name: '', children: this.outerNodes };
data.root = node;
} }
};
const getRoot = (): TreemapNode | undefined => ({ name: '', children: state.records.outerNodes });
const addClass = (id: string, _style: string) => {
const classes = state.records.classes;
const styleClass = classes.get(id) ?? { id, styles: [], textStyles: [] };
classes.set(id, styleClass);
public addClass(id: string, _style: string) {
const styleClass = this.classes.get(id) ?? { id, styles: [], textStyles: [] };
const styles = _style.replace(/\\,/g, '§§§').replace(/,/g, ';').replace(/§§§/g, ',').split(';'); const styles = _style.replace(/\\,/g, '§§§').replace(/,/g, ';').replace(/§§§/g, ',').split(';');
if (styles) { if (styles) {
styles.forEach((s) => { styles.forEach((s) => {
if (isLabelStyle(s)) { if (isLabelStyle(s)) {
@@ -78,35 +66,30 @@ const addClass = (id: string, _style: string) => {
} }
}); });
} }
this.classes.set(id, styleClass);
}
classes.set(id, styleClass); public getClasses() {
}; return this.classes;
const getClasses = (): Map<string, DiagramStyleClassDef> => { }
return state.records.classes;
};
const getStylesForClass = (classSelector: string): string[] => { public getStylesForClass(classSelector: string): string[] {
return state.records.classes.get(classSelector)?.styles ?? []; return this.classes.get(classSelector)?.styles ?? [];
}; }
const clear = () => { public clear() {
commonClear(); commonClear();
state.reset(); this.nodes = [];
}; this.levels = new Map();
this.outerNodes = [];
this.classes = new Map();
this.root = undefined;
}
export const db: TreemapDB = { public setAccTitle = setAccTitle;
getNodes, public getAccTitle = getAccTitle;
addNode, public setDiagramTitle = setDiagramTitle;
getRoot, public getDiagramTitle = getDiagramTitle;
getConfig, public getAccDescription = getAccDescription;
clear, public setAccDescription = setAccDescription;
setAccTitle, }
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
addClass,
getClasses,
getStylesForClass,
};

View File

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

View File

@@ -2,15 +2,15 @@ import { parse } from '@mermaid-js/parser';
import type { ParserDefinition } from '../../diagram-api/types.js'; import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js'; import { populateCommonDb } from '../common/populateCommonDb.js';
import { db } from './db.js'; import type { TreemapNode, TreemapAst, TreemapDB } from './types.js';
import type { TreemapNode, TreemapAst } from './types.js';
import { buildHierarchy } from './utils.js'; import { buildHierarchy } from './utils.js';
import { TreeMapDB } from './db.js';
/** /**
* Populates the database with data from the Treemap AST * Populates the database with data from the Treemap AST
* @param ast - 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 // We need to bypass the type checking for populateCommonDb
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
populateCommonDb(ast as any, db); populateCommonDb(ast as any, db);
@@ -84,6 +84,8 @@ const getItemName = (item: { name?: string | number }): string => {
}; };
export const parser: ParserDefinition = { export const parser: ParserDefinition = {
// @ts-expect-error - TreeMapDB is not assignable to DiagramDB
parser: { yy: undefined },
parse: async (text: string): Promise<void> => { parse: async (text: string): Promise<void> => {
try { try {
// Use a generic parse that accepts any diagram type // 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 parseFunc = parse as (diagramType: string, text: string) => Promise<TreemapAst>;
const ast = await parseFunc('treemap', text); const ast = await parseFunc('treemap', text);
log.debug('Treemap AST:', ast); 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) { } catch (error) {
log.error('Error parsing treemap:', error); log.error('Error parsing treemap:', error);
throw error; throw error;

View File

@@ -302,7 +302,7 @@ If you are adding a feature, you will definitely need to add tests. Depending on
Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run. Unit tests are 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. 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 ./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 #### Integration / End-to-End (E2E) Tests
These test the rendering and visual appearance of the diagrams. These test the rendering and visual appearance of the diagrams.

View File

@@ -106,3 +106,13 @@ const themes = {
``` ```
The actual options and values for the colors are defined in **src/theme/theme-[xyz].js**. If you provide the options your diagram needs in the existing theme files then the theming will work smoothly without hiccups. The actual options and values for the colors are defined in **src/theme/theme-[xyz].js**. If you provide the options your diagram needs in the existing theme files then the theming will work smoothly without hiccups.
## Examples
The `@mermaid-js/examples` package contains a collection of examples that are used by tools like mermaid.live to help users get started with the new diagram.
You can duplicate an existing diagram example file, eg: `packages/examples/src/examples/flowchart.ts`, and modify it with details specific to your diagram.
Then you can 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, and that should be marked as default. It is good to add more examples to showcase different features of the diagram.

View File

@@ -27,7 +27,7 @@
"@iconify-json/carbon": "^1.1.37", "@iconify-json/carbon": "^1.1.37",
"@unocss/reset": "^66.0.0", "@unocss/reset": "^66.0.0",
"@vite-pwa/vitepress": "^1.0.0", "@vite-pwa/vitepress": "^1.0.0",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^6.0.0",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"https-localhost": "^4.7.1", "https-localhost": "^4.7.1",
"pathe": "^2.0.3", "pathe": "^2.0.3",

View File

@@ -11,7 +11,7 @@ This diagram type is particularly useful for developers, network engineers, educ
## Syntax ## Syntax
``` ```
packet-beta packet
start: "Block name" %% Single-bit block start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks start-end: "Block name" %% Multi-bit blocks
... More Fields ... ... More Fields ...
@@ -22,7 +22,7 @@ start-end: "Block name" %% Multi-bit blocks
Using start and end bit counts can be difficult, especially when modifying a design. For this we add a bit count field, which starts from the end of the previous field automagically. Use `+<count>` to set the number of bits, thus: Using start and end bit counts can be difficult, especially when modifying a design. For this we add a bit count field, which starts from the end of the previous field automagically. Use `+<count>` to set the number of bits, thus:
``` ```
packet-beta packet
+1: "Block name" %% Single-bit block +1: "Block name" %% Single-bit block
+8: "Block name" %% 8-bit block +8: "Block name" %% 8-bit block
9-15: "Manually set start and end, it's fine to mix and match" 9-15: "Manually set start and end, it's fine to mix and match"
@@ -35,7 +35,7 @@ packet-beta
--- ---
title: "TCP Packet" title: "TCP Packet"
--- ---
packet-beta packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"
@@ -56,7 +56,7 @@ packet-beta
``` ```
```mermaid-example ```mermaid-example
packet-beta packet
title UDP Packet title UDP Packet
+16: "Source Port" +16: "Source Port"
+16: "Destination Port" +16: "Destination Port"
@@ -104,7 +104,7 @@ config:
packet: packet:
startByteColor: red startByteColor: red
--- ---
packet-beta packet
0-15: "Source Port" 0-15: "Source Port"
16-31: "Destination Port" 16-31: "Destination Port"
32-63: "Sequence Number" 32-63: "Sequence Number"

View File

@@ -5,7 +5,7 @@
import { registerIconPacks } from './rendering-util/icons.js'; import { registerIconPacks } from './rendering-util/icons.js';
import { dedent } from 'ts-dedent'; import { dedent } from 'ts-dedent';
import type { MermaidConfig } from './config.type.js'; import type { MermaidConfig } from './config.type.js';
import { detectType, registerLazyLoadedDiagrams } from './diagram-api/detectType.js'; import { detectType, detectors, registerLazyLoadedDiagrams } from './diagram-api/detectType.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js'; import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js';
import type { ExternalDiagramDefinition, SVG, SVGGroup } from './diagram-api/types.js'; import type { ExternalDiagramDefinition, SVG, SVGGroup } from './diagram-api/types.js';
@@ -415,6 +415,17 @@ const render: typeof mermaidAPI.render = (id, text, container) => {
}); });
}; };
/**
* Gets the metadata for all registered diagrams.
* Currently only the id is returned.
* @returns An array of objects with the id of the diagram.
*/
const getRegisteredDiagramsMetadata = (): Pick<ExternalDiagramDefinition, 'id'>[] => {
return Object.keys(detectors).map((id) => ({
id,
}));
};
export interface Mermaid { export interface Mermaid {
startOnLoad: boolean; startOnLoad: boolean;
parseError?: ParseErrorFunction; parseError?: ParseErrorFunction;
@@ -437,6 +448,7 @@ export interface Mermaid {
setParseErrorHandler: typeof setParseErrorHandler; setParseErrorHandler: typeof setParseErrorHandler;
detectType: typeof detectType; detectType: typeof detectType;
registerIconPacks: typeof registerIconPacks; registerIconPacks: typeof registerIconPacks;
getRegisteredDiagramsMetadata: typeof getRegisteredDiagramsMetadata;
} }
const mermaid: Mermaid = { const mermaid: Mermaid = {
@@ -454,6 +466,7 @@ const mermaid: Mermaid = {
setParseErrorHandler, setParseErrorHandler,
detectType, detectType,
registerIconPacks, registerIconPacks,
getRegisteredDiagramsMetadata,
}; };
export default mermaid; export default mermaid;

View File

@@ -1,40 +1,5 @@
import { assert, beforeEach, describe, expect, it, vi } from 'vitest'; 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 assignWithDepth from './assignWithDepth.js';
import type { MermaidConfig } from './config.type.js'; import type { MermaidConfig } from './config.type.js';
import mermaid from './mermaid.js'; import mermaid from './mermaid.js';
@@ -75,6 +40,9 @@ import { SequenceDB } from './diagrams/sequence/sequenceDb.js';
import { decodeEntities, encodeEntities } from './utils.js'; import { decodeEntities, encodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js'; import { toBase64 } from './utils/base64.js';
import { StateDB } from './diagrams/state/stateDb.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 * @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', () => { 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 // cspell:ignore dthe
it('appends a div node', () => { jsdomIt('appends a div node', ({ body }) => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); appendDivSvgG(body, 'theId', 'dtheId');
expect(parent_append_spy).toHaveBeenCalledWith('div'); const divNode = ensureNodeFromSelector('div');
expect(div_append_spy).toHaveBeenCalledWith('svg'); const svgNode = ensureNodeFromSelector('svg', divNode);
ensureNodeFromSelector('g', svgNode);
}); });
it('the id for the div is "d" with the id appended', () => { jsdomIt('the id for the div is "d" with the id appended', ({ body }) => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); appendDivSvgG(body, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId'); const divNode = ensureNodeFromSelector('div');
expect(divNode?.getAttribute('id')).toBe('dtheId');
}); });
it('sets the style for the div if one is given', () => { jsdomIt('sets the style for the div if one is given', ({ body }) => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'given div style', 'given x link'); appendDivSvgG(body, 'theId', 'dtheId', 'given div style', 'given x link');
expect(div_attr_spy).toHaveBeenCalledWith('style', 'given div style'); const divNode = ensureNodeFromSelector('div');
expect(divNode?.getAttribute('style')).toBe('given div style');
}); });
it('appends a svg node to the div node', () => { jsdomIt('sets the svg width to 100%', ({ body }) => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); appendDivSvgG(body, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId'); const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('width')).toBe('100%');
}); });
it('sets the svg width to 100%', () => { jsdomIt('the svg id is the id', ({ body }) => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); appendDivSvgG(body, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%'); const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('id')).toBe('theId');
}); });
it('the svg id is the id', () => { jsdomIt('the svg xml namespace is the 2000 standard', ({ body }) => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); appendDivSvgG(body, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId'); 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', () => { jsdomIt('sets the svg xlink if one is given', ({ body }) => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); appendDivSvgG(body, 'theId', 'dtheId', 'div style', 'given x link');
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg'); const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('xmlns:xlink')).toBe('given x link');
}); });
it('sets the svg xlink if one is given', () => { jsdomIt('returns the given parentRoot d3 nodes', ({ body }) => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'div style', 'given x link'); expect(appendDivSvgG(body, 'theId', 'dtheId')).toEqual(body);
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);
}); });
}); });
@@ -782,9 +736,9 @@ graph TD;A--x|text including URL space|B;`)
// render(id, text, cb?, svgContainingElement?) // 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.) // 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 = [ const diagramTypesAndExpectations = [
{ textDiagramType: 'C4Context', expectedType: 'c4' }, // { textDiagramType: 'C4Context', expectedType: 'c4' }, TODO : setAccTitle not called in C4 jison parser
{ textDiagramType: 'classDiagram', expectedType: 'class' }, { textDiagramType: 'classDiagram', expectedType: 'class' },
{ textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' }, { textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' },
{ textDiagramType: 'erDiagram', expectedType: 'er' }, { textDiagramType: 'erDiagram', expectedType: 'er' },
@@ -794,8 +748,13 @@ graph TD;A--x|text including URL space|B;`)
{ textDiagramType: 'gantt', expectedType: 'gantt' }, { textDiagramType: 'gantt', expectedType: 'gantt' },
{ textDiagramType: 'journey', expectedType: 'journey' }, { textDiagramType: 'journey', expectedType: 'journey' },
{ textDiagramType: 'pie', expectedType: 'pie' }, { textDiagramType: 'pie', expectedType: 'pie' },
{ textDiagramType: 'packet', expectedType: 'packet' },
{ textDiagramType: 'packet-beta', 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: 'xychart', expectedType: 'xychart' }, { textDiagramType: 'xychart', expectedType: 'xychart' },
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' }, { textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' }, { textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
@@ -812,20 +771,25 @@ graph TD;A--x|text including URL space|B;`)
diagramTypesAndExpectations.forEach((testedDiagram) => { diagramTypesAndExpectations.forEach((testedDiagram) => {
describe(`${testedDiagram.textDiagramType}`, () => { describe(`${testedDiagram.textDiagramType}`, () => {
const diagramType = 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; const expectedDiagramType = testedDiagram.expectedType;
it('should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription', async () => { jsdomIt(
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo'); 'should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription',
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription'); async () => {
const result = await mermaidAPI.render(id, diagramText); const { svg } = await mermaidAPI.render(id, diagramText);
expect(result.diagramType).toBe(expectedDiagramType); const dom = new JSDOM(svg);
expect(a11yDiagramInfo_spy).toHaveBeenCalledWith( const svgNode = ensureNodeFromSelector('svg', dom.window.document);
expect.anything(), const descNode = ensureNodeFromSelector('desc', svgNode);
expectedDiagramType 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);
}
); );
expect(a11yTitleDesc_spy).toHaveBeenCalled();
});
}); });
}); });
}); });

View File

@@ -17,7 +17,7 @@ export async function note<T extends SVGGraphicsElement>(
if (!useHtmlLabels) { if (!useHtmlLabels) {
node.centerLabel = true; node.centerLabel = true;
} }
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const totalWidth = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); const totalWidth = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const totalHeight = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); const totalHeight = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const x = -totalWidth / 2; const x = -totalWidth / 2;
@@ -50,6 +50,11 @@ export async function note<T extends SVGGraphicsElement>(
rect.selectAll('path').attr('style', nodeStyles); rect.selectAll('path').attr('style', nodeStyles);
} }
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, rect); updateNodeBounds(node, rect);
node.intersect = function (point) { node.intersect = function (point) {

View File

@@ -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();
}

View File

@@ -1,3 +0,0 @@
import { vi } from 'vitest';
vi.mock('d3');
vi.mock('dagre-d3-es');

View File

@@ -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[]) => { export const convert = (template: TemplateStringsArray, ...params: unknown[]) => {
const header = template[0] const header = template[0]
.trim() .trim()
@@ -42,3 +46,83 @@ export const convert = (template: TemplateStringsArray, ...params: unknown[]) =>
} }
return out; 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!;
}

View File

@@ -1,11 +1,11 @@
import { vi } from 'vitest'; import { expect, vi } from 'vitest';
import utils, { calculatePoint, cleanAndMerge, detectDirective } from './utils.js'; import utils, { calculatePoint, cleanAndMerge, detectDirective } from './utils.js';
import assignWithDepth from './assignWithDepth.js'; import assignWithDepth from './assignWithDepth.js';
import { detectType } from './diagram-api/detectType.js'; import { detectType } from './diagram-api/detectType.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import memoize from 'lodash-es/memoize.js'; import memoize from 'lodash-es/memoize.js';
import { MockedD3 } from './tests/MockedD3.js';
import { preprocessDiagram } from './preprocess.js'; import { preprocessDiagram } from './preprocess.js';
import { MOCKED_BBOX, ensureNodeFromSelector, jsdomIt } from './tests/util.js';
addDiagrams(); addDiagrams();
@@ -369,53 +369,34 @@ describe('when initializing the id generator', function () {
}); });
describe('when inserting titles', function () { describe('when inserting titles', function () {
const svg = new MockedD3('svg'); jsdomIt('does nothing if the title is empty', function ({ svg }) {
const mockedElement = {
getBBox: vi.fn().mockReturnValue({ x: 10, y: 11, width: 100, height: 200 }),
};
const fauxTitle = new MockedD3('title');
beforeEach(() => {
svg.node = vi.fn().mockReturnValue(mockedElement);
});
it('does nothing if the title is empty', function () {
const svgAppendSpy = vi.spyOn(svg, 'append');
utils.insertTitle(svg, 'testClass', 0, ''); utils.insertTitle(svg, 'testClass', 0, '');
expect(svgAppendSpy).not.toHaveBeenCalled(); const titleNode = document.querySelector('svg > text');
expect(titleNode).toBeNull();
}); });
it('appends the title as a text item with the given title text', function () { jsdomIt('appends the title as a text item with the given title text', function ({ svg }) {
const svgAppendSpy = vi.spyOn(svg, 'append').mockReturnValue(fauxTitle);
const titleTextSpy = vi.spyOn(fauxTitle, 'text');
utils.insertTitle(svg, 'testClass', 5, 'test title'); utils.insertTitle(svg, 'testClass', 5, 'test title');
expect(svgAppendSpy).toHaveBeenCalled(); const titleNode = ensureNodeFromSelector('svg > text');
expect(titleTextSpy).toHaveBeenCalledWith('test title'); expect(titleNode.innerHTML).toBe('test title');
}); });
it('x value is the bounds x position + half of the bounds width', () => { jsdomIt('x value is the bounds x position + half of the bounds width', ({ svg }) => {
vi.spyOn(svg, 'append').mockReturnValue(fauxTitle);
const titleAttrSpy = vi.spyOn(fauxTitle, 'attr');
utils.insertTitle(svg, 'testClass', 5, 'test title'); utils.insertTitle(svg, 'testClass', 5, 'test title');
expect(titleAttrSpy).toHaveBeenCalledWith('x', 10 + 100 / 2); const titleNode = ensureNodeFromSelector('svg > text');
expect(titleNode.getAttribute('x')).toBe(`${MOCKED_BBOX.x + MOCKED_BBOX.width / 2}`);
}); });
it('y value is the negative of given title top margin', () => { jsdomIt('y value is the negative of given title top margin', ({ svg }) => {
vi.spyOn(svg, 'append').mockReturnValue(fauxTitle);
const titleAttrSpy = vi.spyOn(fauxTitle, 'attr');
utils.insertTitle(svg, 'testClass', 5, 'test title'); utils.insertTitle(svg, 'testClass', 5, 'test title');
expect(titleAttrSpy).toHaveBeenCalledWith('y', -5); const titleNode = ensureNodeFromSelector('svg > text');
expect(titleNode.getAttribute('y')).toBe(`${MOCKED_BBOX.y - 5}`);
}); });
it('class is the given css class', () => { jsdomIt('class is the given css class', ({ svg }) => {
vi.spyOn(svg, 'append').mockReturnValue(fauxTitle);
const titleAttrSpy = vi.spyOn(fauxTitle, 'attr');
utils.insertTitle(svg, 'testClass', 5, 'test title'); utils.insertTitle(svg, 'testClass', 5, 'test title');
expect(titleAttrSpy).toHaveBeenCalledWith('class', 'testClass'); const titleNode = ensureNodeFromSelector('svg > text');
expect(titleNode.getAttribute('class')).toBe('testClass');
}); });
}); });

View File

@@ -1,5 +1,17 @@
# @mermaid-js/parser # @mermaid-js/parser
## 0.6.1
### Patch Changes
- [#6725](https://github.com/mermaid-js/mermaid/pull/6725) [`0da2922`](https://github.com/mermaid-js/mermaid/commit/0da2922ee7f47959e324ec10d3d21ee70594f557) Thanks [@shubham-mermaid](https://github.com/shubham-mermaid)! - chore: use Treemap instead of TreemapDoc in parser.
## 0.6.0
### Minor Changes
- [#6590](https://github.com/mermaid-js/mermaid/pull/6590) [`f338802`](https://github.com/mermaid-js/mermaid/commit/f338802642cdecf5b7ed6c19a20cf2a81effbbee) Thanks [@knsv](https://github.com/knsv)! - Adding support for the new diagram type nested treemap
## 0.5.0 ## 0.5.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mermaid-js/parser", "name": "@mermaid-js/parser",
"version": "0.5.0", "version": "0.6.1",
"description": "MermaidJS parser", "description": "MermaidJS parser",
"author": "Yokozuna59", "author": "Yokozuna59",
"contributors": [ "contributors": [

View File

@@ -8,7 +8,7 @@ export {
Architecture, Architecture,
GitGraph, GitGraph,
Radar, Radar,
TreemapDoc, Treemap,
Branch, Branch,
Commit, Commit,
Merge, Merge,
@@ -20,7 +20,7 @@ export {
isPieSection, isPieSection,
isArchitecture, isArchitecture,
isGitGraph, isGitGraph,
isTreemapDoc, isTreemap,
isBranch, isBranch,
isCommit, isCommit,
isMerge, isMerge,

View File

@@ -3,7 +3,7 @@ import "../common/common";
entry Packet: entry Packet:
NEWLINE* NEWLINE*
"packet-beta" ("packet"| "packet-beta")
( (
TitleAndAccessibilities TitleAndAccessibilities
| blocks+=PacketBlock | blocks+=PacketBlock

View File

@@ -2,6 +2,6 @@ import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class PacketTokenBuilder extends AbstractMermaidTokenBuilder { export class PacketTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() { public constructor() {
super(['packet-beta']); super(['packet']);
} }
} }

View File

@@ -1,5 +1,5 @@
import type { ValidationAcceptor, ValidationChecks } from 'langium'; import type { ValidationAcceptor, ValidationChecks } from 'langium';
import type { MermaidAstType, TreemapDoc } from '../generated/ast.js'; import type { MermaidAstType, Treemap } from '../generated/ast.js';
import type { TreemapServices } from './module.js'; import type { TreemapServices } from './module.js';
/** /**
@@ -9,10 +9,10 @@ export function registerValidationChecks(services: TreemapServices) {
const validator = services.validation.TreemapValidator; const validator = services.validation.TreemapValidator;
const registry = services.validation.ValidationRegistry; const registry = services.validation.ValidationRegistry;
if (registry) { if (registry) {
// Use any to bypass type checking since we know TreemapDoc is part of the AST // Use any to bypass type checking since we know Treemap is part of the AST
// but the type system is having trouble with it // but the type system is having trouble with it
const checks: ValidationChecks<MermaidAstType> = { const checks: ValidationChecks<MermaidAstType> = {
TreemapDoc: validator.checkSingleRoot.bind(validator), Treemap: validator.checkSingleRoot.bind(validator),
// Remove unused validation for TreemapRow // Remove unused validation for TreemapRow
}; };
registry.register(checks, validator); registry.register(checks, validator);
@@ -27,7 +27,7 @@ export class TreemapValidator {
* Validates that a treemap has only one root node. * Validates that a treemap has only one root node.
* A root node is defined as a node that has no indentation. * A root node is defined as a node that has no indentation.
*/ */
checkSingleRoot(doc: TreemapDoc, accept: ValidationAcceptor): void { checkSingleRoot(doc: Treemap, accept: ValidationAcceptor): void {
let rootNodeIndentation; let rootNodeIndentation;
for (const row of doc.TreemapRows) { for (const row of doc.TreemapRows) {

View File

@@ -34,14 +34,14 @@ interface ClassDefStatement {
className: string className: string
styleText: string // Optional style text styleText: string // Optional style text
} }
interface TreemapDoc { interface Treemap {
TreemapRows: TreemapRow[] TreemapRows: TreemapRow[]
title?: string title?: string
accTitle?: string accTitle?: string
accDescr?: string accDescr?: string
} }
entry TreemapDoc returns TreemapDoc: entry Treemap returns Treemap:
TREEMAP_KEYWORD TREEMAP_KEYWORD
( (
TitleAndAccessibilities TitleAndAccessibilities

View File

@@ -11,6 +11,12 @@ describe('packet', () => {
` `
\tpacket-beta \tpacket-beta
`, `,
`packet`,
` packet `,
`\tpacket\t`,
`
\tpacket
`,
])('should handle regular packet', (context: string) => { ])('should handle regular packet', (context: string) => {
const result = parse(context); const result = parse(context);
expectNoErrorsOrAlternatives(result); expectNoErrorsOrAlternatives(result);

View File

@@ -32,15 +32,10 @@ const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined)
* @param result - the result `parse` function. * @param result - the result `parse` function.
*/ */
export function expectNoErrorsOrAlternatives(result: ParseResult) { export function expectNoErrorsOrAlternatives(result: ParseResult) {
if (result.lexerErrors.length > 0) { expect.soft(result.lexerErrors).toHaveLength(0);
// console.debug(result.lexerErrors); expect.soft(result.parserErrors).toHaveLength(0);
} // To see what the error is, in the logs.
if (result.parserErrors.length > 0) { expect(result.lexerErrors[0]).toBeUndefined();
// console.debug(result.parserErrors);
}
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
expect(consoleMock).not.toHaveBeenCalled(); expect(consoleMock).not.toHaveBeenCalled();
consoleMock.mockReset(); consoleMock.mockReset();
} }

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { expectNoErrorsOrAlternatives } from './test-util.js'; import { expectNoErrorsOrAlternatives } from './test-util.js';
import type { TreemapDoc, Section, Leaf, TreemapRow } from '../src/language/generated/ast.js'; import type { Treemap, Section, Leaf, TreemapRow } from '../src/language/generated/ast.js';
import type { LangiumParser } from 'langium'; import type { LangiumParser } from 'langium';
import { createTreemapServices } from '../src/language/treemap/module.js'; import { createTreemapServices } from '../src/language/treemap/module.js';
@@ -9,21 +9,21 @@ describe('Treemap Parser', () => {
const parser: LangiumParser = services.parser.LangiumParser; const parser: LangiumParser = services.parser.LangiumParser;
const parse = (input: string) => { const parse = (input: string) => {
return parser.parse<TreemapDoc>(input); return parser.parse<Treemap>(input);
}; };
describe('Basic Parsing', () => { describe('Basic Parsing', () => {
it('should parse empty treemap', () => { it('should parse empty treemap', () => {
const result = parse('treemap'); const result = parse('treemap');
expectNoErrorsOrAlternatives(result); expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe('TreemapDoc'); expect(result.value.$type).toBe('Treemap');
expect(result.value.TreemapRows).toHaveLength(0); expect(result.value.TreemapRows).toHaveLength(0);
}); });
it('should parse a section node', () => { it('should parse a section node', () => {
const result = parse('treemap\n"Root"'); const result = parse('treemap\n"Root"');
expectNoErrorsOrAlternatives(result); expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe('TreemapDoc'); expect(result.value.$type).toBe('Treemap');
expect(result.value.TreemapRows).toHaveLength(1); expect(result.value.TreemapRows).toHaveLength(1);
if (result.value.TreemapRows[0].item) { if (result.value.TreemapRows[0].item) {
expect(result.value.TreemapRows[0].item.$type).toBe('Section'); expect(result.value.TreemapRows[0].item.$type).toBe('Section');
@@ -39,7 +39,7 @@ describe('Treemap Parser', () => {
"Child2" : 200 "Child2" : 200
`); `);
expectNoErrorsOrAlternatives(result); expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe('TreemapDoc'); expect(result.value.$type).toBe('Treemap');
expect(result.value.TreemapRows).toHaveLength(3); expect(result.value.TreemapRows).toHaveLength(3);
if (result.value.TreemapRows[0].item) { if (result.value.TreemapRows[0].item) {
@@ -95,7 +95,7 @@ describe('Treemap Parser', () => {
// We're only checking that the multiple root nodes parse successfully // We're only checking that the multiple root nodes parse successfully
// The validation errors would be reported by the validator during validation // The validation errors would be reported by the validator during validation
expect(result.value.$type).toBe('TreemapDoc'); expect(result.value.$type).toBe('Treemap');
expect(result.value.TreemapRows).toHaveLength(2); expect(result.value.TreemapRows).toHaveLength(2);
}); });
}); });
@@ -104,7 +104,7 @@ describe('Treemap Parser', () => {
it('should parse a treemap with title', () => { it('should parse a treemap with title', () => {
const result = parse('treemap\ntitle My Treemap Diagram\n"Root"\n "Child": 100'); const result = parse('treemap\ntitle My Treemap Diagram\n"Root"\n "Child": 100');
expectNoErrorsOrAlternatives(result); expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe('TreemapDoc'); expect(result.value.$type).toBe('Treemap');
// We can't directly test the title property due to how Langium processes TitleAndAccessibilities // We can't directly test the title property due to how Langium processes TitleAndAccessibilities
// but we can verify the TreemapRows are parsed correctly // but we can verify the TreemapRows are parsed correctly
expect(result.value.TreemapRows).toHaveLength(2); expect(result.value.TreemapRows).toHaveLength(2);
@@ -113,7 +113,7 @@ describe('Treemap Parser', () => {
it('should parse a treemap with accTitle', () => { it('should parse a treemap with accTitle', () => {
const result = parse('treemap\naccTitle: Accessible Title\n"Root"\n "Child": 100'); const result = parse('treemap\naccTitle: Accessible Title\n"Root"\n "Child": 100');
expectNoErrorsOrAlternatives(result); expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe('TreemapDoc'); expect(result.value.$type).toBe('Treemap');
// We can't directly test the accTitle property due to how Langium processes TitleAndAccessibilities // We can't directly test the accTitle property due to how Langium processes TitleAndAccessibilities
expect(result.value.TreemapRows).toHaveLength(2); expect(result.value.TreemapRows).toHaveLength(2);
}); });
@@ -123,7 +123,7 @@ describe('Treemap Parser', () => {
'treemap\naccDescr: This is an accessible description\n"Root"\n "Child": 100' 'treemap\naccDescr: This is an accessible description\n"Root"\n "Child": 100'
); );
expectNoErrorsOrAlternatives(result); expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe('TreemapDoc'); expect(result.value.$type).toBe('Treemap');
// We can't directly test the accDescr property due to how Langium processes TitleAndAccessibilities // We can't directly test the accDescr property due to how Langium processes TitleAndAccessibilities
expect(result.value.TreemapRows).toHaveLength(2); expect(result.value.TreemapRows).toHaveLength(2);
}); });
@@ -136,7 +136,7 @@ accDescr: This is an accessible description
"Root" "Root"
"Child": 100`); "Child": 100`);
expectNoErrorsOrAlternatives(result); expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe('TreemapDoc'); expect(result.value.$type).toBe('Treemap');
// We can't directly test these properties due to how Langium processes TitleAndAccessibilities // We can't directly test these properties due to how Langium processes TitleAndAccessibilities
expect(result.value.TreemapRows).toHaveLength(2); expect(result.value.TreemapRows).toHaveLength(2);
}); });

View File

@@ -1,5 +1,25 @@
# mermaid # mermaid
## 11.8.1
### Patch Changes
- Updated dependencies [[`0da2922`](https://github.com/mermaid-js/mermaid/commit/0da2922ee7f47959e324ec10d3d21ee70594f557)]:
- @mermaid-js/parser@0.6.1
## 11.8.0
### Minor Changes
- [#6590](https://github.com/mermaid-js/mermaid/pull/6590) [`f338802`](https://github.com/mermaid-js/mermaid/commit/f338802642cdecf5b7ed6c19a20cf2a81effbbee) Thanks [@knsv](https://github.com/knsv)! - Adding support for the new diagram type nested treemap
### Patch Changes
- [#6707](https://github.com/mermaid-js/mermaid/pull/6707) [`592c5bb`](https://github.com/mermaid-js/mermaid/commit/592c5bb880c3b942710a2878d386bcb3eb35c137) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Log a warning when duplicate commit IDs are encountered in gitGraph to help identify and debug rendering issues caused by non-unique IDs.
- Updated dependencies [[`f338802`](https://github.com/mermaid-js/mermaid/commit/f338802642cdecf5b7ed6c19a20cf2a81effbbee)]:
- @mermaid-js/parser@0.6.0
## 11.7.0 ## 11.7.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mermaid-js/tiny", "name": "@mermaid-js/tiny",
"version": "11.7.0", "version": "11.8.1",
"description": "Tiny version of mermaid", "description": "Tiny version of mermaid",
"type": "commonjs", "type": "commonjs",
"main": "./dist/mermaid.tiny.js", "main": "./dist/mermaid.tiny.js",

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