Compare commits

..

55 Commits

Author SHA1 Message Date
Knut Sveidqvist
a716a525c3 Merge remote-tracking branch 'origin/develop' into mindmaps-and-elk-updates 2025-08-14 13:53:33 +02:00
Knut Sveidqvist
11abfc9ae5 Refactor code structure for improved readability and maintainability 2025-08-12 16:08:19 +02:00
Knut Sveidqvist
81b0ffb92a Merge branch '6088-fix-for-diamond-intersections' into mindmaps-and-elk-updates 2025-08-12 11:11:51 +02:00
darshanr0107
dd36046e23 docs: update tidy-tree Readme
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-11 14:22:08 +05:30
darshanr0107
1507435e15 chore: remove unwanted logs
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-11 13:51:10 +05:30
darshanr0107
68c01b76bf fix: failing unit tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 14:32:25 +05:30
darshanr0107
28717e108d chore: remove duplicate code and throw generic error if nodes are not present
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 13:52:22 +05:30
darshanr0107
688d9b383d chore: update changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 13:16:46 +05:30
darshanr0107
e68424d748 fix: updated changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 12:45:16 +05:30
darshanr0107
204a9a338f fix: fix failing sequence diagram
unit test

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 18:54:45 +05:30
darshanr0107
6a6a39ff33 chore: remove disallowed path and regenerate dependencies
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 18:00:10 +05:30
autofix-ci[bot]
b296db9a33 [autofix.ci] apply automated fixes 2025-08-07 11:39:39 +00:00
darshanr0107
01ce84d8ee fix: lint and build issues
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 17:04:01 +05:30
darshanr0107
f48e663d4c Merge branch 'develop' into knsv/mindmap-refactoring 2025-08-07 13:58:19 +05:30
darshanr0107
a4aa2bd355 fix: failing test case caused by invalid expected value
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 13:52:41 +05:30
darshanr0107
b51b9d50c2 fix: failing e2e for mindmaps
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 13:10:28 +05:30
darshanr0107
b61780f735 fix: failing unit tests
Some optional description over here if you need to add more info

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-06 16:35:54 +05:30
darshanr0107
074f18dfb8 fix: build issues
Some optional description over here if you need to add more info

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-05 20:22:17 +05:30
darshanr0107
d7308b0f43 fix: build issues for cose - bilkent
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-05 19:16:02 +05:30
darshanr0107
2f1860386a fix: build for tidy-tree layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-05 15:35:42 +05:30
darshanr0107
f0bca7da55 Merge branch 'develop' into knsv/mindmap-refactoring 2025-08-04 20:07:15 +05:30
Sidharth Vinod
6fcdf5bfcc Merge pull request #6807 from mermaid-js/refactor/layout-algorithms-to-packages
Extract tidy-tree layout into separate package
2025-08-04 19:44:55 +05:30
darshanr0107
e2ce0450c1 fix:separate out tidy-tree and elk from deafult loaders
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-04 19:40:34 +05:30
darshanr0107
c95c64139d Decouple layout loaders from core package, register tidy-tree and elk loaders separately
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-04 14:25:08 +05:30
shubhamparikh2704
a7f12f1baa fix: fixed workflow
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-01 16:28:30 +05:30
darshanr0107
2a8653de2b move tidy-tree-layout to separate package
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-01 14:05:28 +05:30
darshanr0107
a92c3bb251 extract tidy-tree layout into separate package
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-01 13:36:35 +05:30
darshanr0107
3677abe9e5 Merge branch 'develop' into knsv/mindmap-refactoring 2025-07-30 19:53:22 +05:30
darshanr0107
95847ad236 clean up unused files, types, and commented code; remove unnecessary d3 mock test
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-30 18:48:23 +05:30
darshanr0107
e0152fb873 fix: resolve duplicate keys in lockfile and fix test failures
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-30 15:33:03 +05:30
darshanr0107
2298b96d8e Merge branch 'develop' into knsv/mindmap-refactoring 2025-07-30 15:07:32 +05:30
darshanr0107
5db83365b6 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into knsv/mindmap-refactoring 2025-07-30 14:59:39 +05:30
darshanr0107
341a81a113 add sample mind maps rendered with all layouts for baseline validation
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-29 12:51:39 +05:30
darshanr0107
8a62b4cace fix: apply intersection logic for edges in tidy-tree layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-29 12:21:42 +05:30
darshanr0107
ccafc20917 add support for cloud and bang shape
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-24 18:07:25 +05:30
darshanr0107
d5cb4eaa59 add support for elk layout in mindmap
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-23 20:11:38 +05:30
darshanr0107
425fb7ee33 fix: handle undefined edge list in insertEdge to prevent runtime error
Fixes a runtime error in cose-bilkent layout caused by insertEdge calling .filter() on an undefined edge list. Now defaults to an empty array to prevent the crash.

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-23 17:18:48 +05:30
darshanr0107
cd6f8e5a24 tidy-tree as the default layout for mindmaps
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-21 18:41:18 +05:30
Knut Sveidqvist
8314554eb5 Merge branch 'test-merge' into 6088-fix-for-diamond-intersections 2025-06-25 13:00:03 +02:00
Knut Sveidqvist
b7c03dc27e Some cleanup 2025-06-25 12:58:54 +02:00
Knut Sveidqvist
c7f2f609a9 Intersections ok 2025-06-24 20:30:50 +02:00
Knut Sveidqvist
4c3de3a1ec Merge remote-tracking branch 'origin/develop' into test-merge 2025-06-24 10:58:28 +02:00
Knut Sveidqvist
f0445b74d1 #6646 Tidy-tree layout in place for Mindmaps 2025-06-12 13:33:05 +02:00
Knut Sveidqvist
ba52eef257 Tidy-tree getting closer handling of multi point edges 2025-06-12 11:21:35 +02:00
Knut Sveidqvist
c13ce2a5c0 Tidy-tree getting closer, now algorithm works 2025-06-11 20:58:54 +02:00
Knut Sveidqvist
d2463f41b5 Tidy-tree WIP 2025-06-11 18:04:21 +02:00
Knut Sveidqvist
eadb343292 Tidy-tree POC 2025-06-11 16:55:58 +02:00
Knut Sveidqvist
e7208622f7 Merge branch '6088-fix-for-diamond-intersections 2025-06-06 20:14:02 +02:00
Knut Sveidqvist
fbae611406 Refresh 2025-06-06 20:10:19 +02:00
Knut Sveidqvist
34027bc589 Repackaging cose-bilkins 2025-06-06 20:03:34 +02:00
Knut Sveidqvist
f2eef37599 Updated renderinig flow for mindmaps 2025-06-06 11:20:31 +02:00
Knut Sveidqvist
1e3ea13323 Fix for when last point is on the intersection 2024-11-29 09:07:47 +01:00
Knut Sveidqvist
4c8c48cde9 Generic solution for intersection of shapes with elk 2024-11-28 14:31:54 +01:00
Knut Sveidqvist
c8e50276e8 Added changeset 2024-11-27 15:54:05 +01:00
Knut Sveidqvist
1e6419a63f #6088 Updated offset calculations 2024-11-27 15:52:24 +01:00
153 changed files with 9370 additions and 8906 deletions

View File

@@ -33,6 +33,11 @@ export const packageOptions = {
packageName: 'mermaid-layout-elk',
file: 'layouts.ts',
},
'mermaid-layout-tidy-tree': {
name: 'mermaid-layout-tidy-tree',
packageName: 'mermaid-layout-tidy-tree',
file: 'index.ts',
},
examples: {
name: 'mermaid-examples',
packageName: 'examples',

View File

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

View File

@@ -0,0 +1,6 @@
---
'@mermaid-js/layout-elk': patch
'mermaid': patch
---
feat: Exposing elk configuration forceNodeModelOrder and considerModelOrder to the mermaid configuration

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
chore: Remove the "-beta" suffix from the XYChart, Block, Sankey diagrams to reflect their stable status

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Position the edge label in state diagram correctly relative to the edge

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Handle arrows correctly when auto number is enabled

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Apply correct dateFormat in Gantt chart to show only day when specified

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: handle exclude dates properly in Gantt charts when using dateFormat: 'YYYY-MM-DD HH:mm:ss'

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: fixed connection gaps in flowchart for roundedRect, stadium and diamond shape

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.

View File

@@ -0,0 +1,7 @@
---
'mermaid': minor
'@mermaid-js/layout-tidy-tree': minor
'@mermaid-js/layout-elk': minor
---
feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
---
'mermaid': patch
---
fix(block): overflowing blocks no longer affect later lines
This may change the layout of block diagrams that have overflowing lines
(i.e. block diagrams that use up more columns that the `columns` specifier).

View File

@@ -0,0 +1,7 @@
---
'mermaid': patch
---
fix: log warning for blocks exceeding column width
This update adds a validation check that logs a warning message when a block's width exceeds the defined column layout.

View File

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

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Allow equals sign in sequenceDiagram labels

View File

@@ -0,0 +1,9 @@
---
'mermaid': patch
---
Add validation for negative values in pie charts:
Prevents crashes during parsing by validating values post-parsing.
Provides clearer, user-friendly error messages for invalid negative inputs.

View File

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

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: node border style for handdrawn shapes

View File

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

View File

@@ -0,0 +1,5 @@
---
'@mermaid-js/layout-elk': patch
---
Make elk not force node model order, but strongly consider it instead

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: correctly render non-directional lines for '---' in block diagrams

View File

@@ -0,0 +1,9 @@
---
'mermaid': patch
---
fix: fallback to raw text instead of rendering _Unsupported markdown_ or empty blocks
Instead of printing **Unsupported markdown: XXX**, or empty blocks when using a markdown feature
that Mermaid does not yet support when `htmlLabels: true`(default) or `htmlLabels: false`,
fallback to the raw markdown text.

View File

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

View File

@@ -2,7 +2,6 @@
Ashish Jain
cpettitt
Dong Cai
fourcube
knsv
Knut Sveidqvist
Nikolay Rozhkov

View File

@@ -7,6 +7,7 @@ catmull
compositTitleSize
curv
doublecircle
elem
elems
gantt
gitgraph

1
.github/lychee.toml vendored
View File

@@ -59,7 +59,6 @@ exclude = [
"https://huehive.co",
"https://foswiki.org",
"https://www.gnu.org",
"https://redmine.org",
"https://mermaid-preview.com"
]

View File

@@ -23,6 +23,9 @@ env:
jobs:
e2e-applitools:
runs-on: ubuntu-latest
container:
image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1
options: --user 1001
steps:
- if: ${{ ! env.USE_APPLI }}
name: Warn if not using Applitools

View File

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

View File

@@ -38,8 +38,6 @@ jobs:
options: --user 1001
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
@@ -57,7 +55,6 @@ jobs:
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
ref: ${{ env.targetHash }}
- name: Install dependencies
@@ -86,8 +83,6 @@ jobs:
containers: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# uses version from "packageManager" field in package.json
@@ -142,118 +137,13 @@ jobs:
SPLIT_INDEX: ${{ strategy.job-index }}
SPLIT_FILE: 'cypress/timings.json'
VITEST_COVERAGE: true
- name: Debug coverage generation
if: ${{ steps.cypress.conclusion == 'success' }}
run: |
echo "Checking if coverage files were generated:"
ls -la coverage/ || echo "No coverage directory"
ls -la coverage/cypress/ || echo "No coverage/cypress directory"
echo "Looking for any .info files:"
find . -name "*.info" -type f | head -10 || echo "No .info files found"
- name: Prepare coverage artifacts
if: ${{ steps.cypress.conclusion == 'success' }}
run: |
mkdir -p coverage/cypress
if [ -f coverage/cypress/coverage-final.json ]; then
cp coverage/cypress/coverage-final.json coverage/cypress/coverage-final-${{ matrix.containers }}.json
echo "Created coverage-final-${{ matrix.containers }}.json"
ls -la coverage/cypress/coverage-final-${{ matrix.containers }}.json
else
echo "Error: coverage/cypress/coverage-final.json not found"
exit 1
fi
- name: Upload e2e coverage artifact
if: ${{ steps.cypress.conclusion == 'success' }}
uses: actions/upload-artifact@v4
with:
name: e2e-coverage-${{ matrix.containers }}
path: coverage/cypress/coverage-final-${{ matrix.containers }}.json
coverage-merge:
runs-on: ubuntu-latest
needs: e2e
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
- name: Download e2e coverage shards
uses: actions/download-artifact@v4
with:
pattern: e2e-coverage-*
path: coverage/e2e-shards
merge-multiple: true
- name: Debug downloaded artifacts
run: |
echo "Contents of coverage/e2e-shards:"
find coverage/e2e-shards -type f -name "*.info" | head -20
echo "All files in coverage/e2e-shards:"
ls -la coverage/e2e-shards/
echo "Directory structure:"
find coverage/e2e-shards -type f | head -20
echo "Looking for coverage-final.json files:"
find coverage/e2e-shards -name "coverage-final.json" | head -20
- name: Install dependencies for merging
run: pnpm install --frozen-lockfile
env:
CYPRESS_CACHE_FOLDER: .cache/Cypress
- name: Prepare coverage files for merge script
run: |
mkdir -p coverage/vitest coverage/cypress
# Copy E2E coverage-final.json files to the structure expected by scripts/coverage.ts
for i in {1..5}; do
if [ -f "coverage/e2e-shards/coverage-final-$i.json" ]; then
cp "coverage/e2e-shards/coverage-final-$i.json" "coverage/cypress/coverage-final.json"
echo "Copied coverage-final-$i.json to cypress/"
break
fi
done
# Create a minimal but valid vitest coverage-final.json
echo '{"type":"Coverage","version":"1.1","data":{}}' > coverage/vitest/coverage-final.json
echo "Prepared coverage files:"
ls -la coverage/vitest/
ls -la coverage/cypress/
echo "Checking file contents:"
echo "Vitest coverage file:"
cat coverage/vitest/coverage-final.json
echo "Cypress coverage file:"
cat coverage/cypress/coverage-final.json
echo "Validating JSON files:"
if jq . coverage/vitest/coverage-final.json > /dev/null; then
echo "✓ Vitest coverage file is valid JSON"
else
echo "✗ Vitest coverage file is invalid JSON"
exit 1
fi
if jq . coverage/cypress/coverage-final.json > /dev/null; then
echo "✓ Cypress coverage file is valid JSON"
else
echo "✗ Cypress coverage file is invalid JSON"
exit 1
fi
- name: Generate LCOV from coverage data
run: |
mkdir -p coverage/combined
# Convert coverage-final.json to LCOV format using nyc
if [ -f coverage/cypress/coverage-final.json ]; then
echo "Converting Cypress coverage to LCOV..."
npx nyc report --reporter=lcov --report-dir=coverage/combined --cwd=. --temp-dir=coverage/cypress
echo "LCOV generation completed"
ls -la coverage/combined/
else
echo "No Cypress coverage file found"
exit 1
fi
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
# Run step only pushes to develop and pull_requests
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop'}}
if: ${{ steps.cypress.conclusion == 'success' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop')}}
with:
files: coverage/combined/lcov.info
files: coverage/cypress/lcov.info
flags: e2e
name: mermaid-codecov
fail_ci_if_error: false

View File

@@ -10,8 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# uses version from "packageManager" field in package.json
@@ -43,6 +41,7 @@ jobs:
- name: Verify out-of-tree build with TypeScript
run: |
pnpm test:check:tsc
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
# Run step only pushes to develop and pull_requests

View File

@@ -35,7 +35,7 @@ jobs:
# 2) No unwanted vitepress paths
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run `rm -rf packages/mermaid/src/vitepress && pnpm install` to regenerate.")
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run \`rm -rf packages/mermaid/src/vitepress && pnpm install\` to regenerate.")
fi
# 3) Lockfile only changes when package.json changes

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ node_modules/
coverage/
.idea/
.pnpm-store/
.instructions/
dist
v8-compile-cache-0

View File

@@ -7,5 +7,6 @@ export default {
'prettier --write',
],
'.cspell/*.txt': ['tsx scripts/fixCSpell.ts'],
'**/*.md': ['pnpm cspell'],
'**/*.jison': ['pnpm -w run lint:jison'],
};

14
.nycrc
View File

@@ -1,14 +0,0 @@
{
"reporter": ["text", "lcov", "json", "html"],
"exclude": [
"node_modules/**/*",
"cypress/**/*",
"coverage/**/*",
"**/*.spec.js",
"**/*.spec.ts",
"**/*.test.js",
"**/*.test.ts"
],
"all": true,
"check-coverage": false
}

View File

@@ -1,27 +0,0 @@
coverage:
status:
project:
default:
target: auto
threshold: 1%
patch:
default:
target: auto
threshold: 1%
comment:
layout: 'reach,diff,flags,tree'
behavior: default
require_changes: false
flags:
unit:
paths:
- packages/
e2e:
paths:
- packages/
# Wait for both unit and e2e coverage uploads before finalizing
notify:
after_n_builds: 2

View File

@@ -15,13 +15,6 @@ export default eyesPlugin(
setupNodeEvents(on, config) {
coverage(on, config);
cypressSplit(on, config);
// Ensure coverage generates LCOV format
on('task', {
coverage: () => {
return null;
},
});
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push('--window-size=1440,1024', '--force-device-scale-factor=1');

View File

@@ -14,7 +14,7 @@ interface CodeObject {
mermaid: CypressMermaidConfig;
}
export const utf8ToB64 = (str: string): string => {
const utf8ToB64 = (str: string): string => {
return Buffer.from(decodeURIComponent(encodeURIComponent(str))).toString('base64');
};
@@ -22,7 +22,7 @@ const batchId: string =
'mermaid-batch-' +
(Cypress.env('useAppli')
? Date.now().toString()
: (Cypress.env('CYPRESS_COMMIT') ?? Date.now().toString()));
: Cypress.env('CYPRESS_COMMIT') || Date.now().toString());
export const mermaidUrl = (
graphStr: string | string[],
@@ -61,7 +61,9 @@ export const imgSnapshotTest = (
sequence: {
...(_options.sequence ?? {}),
actorFontFamily: 'courier',
noteFontFamily: _options.sequence?.noteFontFamily ?? 'courier',
noteFontFamily: _options.sequence?.noteFontFamily
? _options.sequence.noteFontFamily
: 'courier',
messageFontFamily: 'courier',
},
};

View File

@@ -1,4 +1,4 @@
import { imgSnapshotTest, mermaidUrl, utf8ToB64 } from '../../helpers/util.ts';
import { mermaidUrl } from '../../helpers/util.ts';
describe('XSS', () => {
it('should handle xss in tags', () => {
const str =
@@ -141,37 +141,4 @@ describe('XSS', () => {
cy.wait(1000);
cy.get('#the-malware').should('not.exist');
});
it('should sanitize icon labels in architecture diagrams', () => {
const str = JSON.stringify({
code: `architecture-beta
group api(cloud)[API]
service db "<img src=x onerror=\\"xssAttack()\\">" [Database] in api`,
});
imgSnapshotTest(utf8ToB64(str), {}, true);
cy.wait(1000);
cy.get('#the-malware').should('not.exist');
});
it('should sanitize katex blocks', () => {
const str = JSON.stringify({
code: `sequenceDiagram
participant A as Alice<img src="x" onerror="xssAttack()">$$\\text{Alice}$$
A->>John: Hello John, how are you?`,
});
imgSnapshotTest(utf8ToB64(str), {}, true);
cy.wait(1000);
cy.get('#the-malware').should('not.exist');
});
it('should sanitize labels', () => {
const str = JSON.stringify({
code: `erDiagram
"<img src=x onerror=xssAttack()>" ||--|| ENTITY2 : "<img src=x onerror=xssAttack()>"
`,
});
imgSnapshotTest(utf8ToB64(str), {}, true);
cy.wait(1000);
cy.get('#the-malware').should('not.exist');
});
});

View File

@@ -1118,7 +1118,7 @@ end
imgSnapshotTest(
`flowchart TB
A(["Start"]) --> n1["Untitled Node"]
A --> n2["Untitled Node"]
A --> n2["Untitled Node"]
`,
{}
);
@@ -1127,7 +1127,7 @@ end
imgSnapshotTest(
`flowchart BT
n2["Untitled Node"] --> n1["Diamond"]
n1@{ shape: diam}
n1@{ shape: diam}
`,
{}
);
@@ -1138,7 +1138,7 @@ end
n2["Untitled Node"] --> n1["Rounded Rectangle"]
n3["Untitled Node"] --> n1
n1@{ shape: rounded}
n3@{ shape: rect}
n3@{ shape: rect}
`,
{}
);

View File

@@ -159,12 +159,10 @@ root
});
it('square shape', () => {
imgSnapshotTest(
`
mindmap
`mindmap
root[
The root
]
`,
]`,
{},
undefined,
shouldHaveRoot
@@ -172,12 +170,10 @@ mindmap
});
it('rounded rect shape', () => {
imgSnapshotTest(
`
mindmap
`mindmap
root((
The root
))
`,
))`,
{},
undefined,
shouldHaveRoot
@@ -185,12 +181,10 @@ mindmap
});
it('circle shape', () => {
imgSnapshotTest(
`
mindmap
`mindmap
root(
The root
)
`,
)`,
{},
undefined,
shouldHaveRoot
@@ -198,10 +192,8 @@ mindmap
});
it('default shape', () => {
imgSnapshotTest(
`
mindmap
The root
`,
`mindmap
The root`,
{},
undefined,
shouldHaveRoot
@@ -209,12 +201,10 @@ mindmap
});
it('adding children', () => {
imgSnapshotTest(
`
mindmap
`mindmap
The root
child1
child2
`,
child2`,
{},
undefined,
shouldHaveRoot
@@ -222,13 +212,11 @@ mindmap
});
it('adding grand children', () => {
imgSnapshotTest(
`
mindmap
`mindmap
The root
child1
child2
child3
`,
child3`,
{},
undefined,
shouldHaveRoot

View File

@@ -1,659 +0,0 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
const looks = ['classic'];
const participantTypes = [
{ type: 'participant', display: 'participant' },
{ type: 'actor', display: 'actor' },
{ type: 'boundary', display: 'boundary' },
{ type: 'control', display: 'control' },
{ type: 'entity', display: 'entity' },
{ type: 'database', display: 'database' },
{ type: 'collections', display: 'collections' },
{ type: 'queue', display: 'queue' },
];
const restrictedTypes = ['boundary', 'control', 'entity', 'database', 'collections', 'queue'];
const interactionTypes = ['->>', '-->>', '->', '-->', '-x', '--x', '->>+', '-->>+'];
const notePositions = ['left of', 'right of', 'over'];
function getParticipantLine(name, type, alias) {
if (restrictedTypes.includes(type)) {
return ` participant ${name}@{ "type" : "${type}" }\n`;
} else if (alias) {
return ` participant ${name}@{ "type" : "${type}" } \n`;
} else {
return ` participant ${name}@{ "type" : "${type}" }\n`;
}
}
looks.forEach((look) => {
describe(`Sequence Diagram Tests - ${look} look`, () => {
it('should render all participant types', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.forEach((pt, index) => {
const name = `${pt.display}${index}`;
diagramCode += getParticipantLine(name, pt.type);
});
for (let i = 0; i < participantTypes.length - 1; i++) {
diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
}
imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } });
});
it('should render all interaction types', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += getParticipantLine('A', 'actor');
diagramCode += getParticipantLine('B', 'boundary');
interactionTypes.forEach((interaction, index) => {
diagramCode += ` A ${interaction} B: ${interaction} message ${index}\n`;
});
imgSnapshotTest(diagramCode, { look });
});
it('should render participant creation and destruction', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.forEach((pt, index) => {
const name = `${pt.display}${index}`;
diagramCode += getParticipantLine('A', pt.type);
diagramCode += getParticipantLine('B', pt.type);
diagramCode += ` create participant ${name}@{ "type" : "${pt.type}" }\n`;
diagramCode += ` A ->> ${name}: Hello ${pt.display}\n`;
if (index % 2 === 0) {
diagramCode += ` destroy ${name}\n`;
}
});
imgSnapshotTest(diagramCode, { look });
});
it('should render notes in all positions', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += getParticipantLine('A', 'actor');
diagramCode += getParticipantLine('B', 'boundary');
notePositions.forEach((position, index) => {
diagramCode += ` Note ${position} A: Note ${position} ${index}\n`;
});
diagramCode += ` A ->> B: Message with notes\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render parallel interactions', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.slice(0, 4).forEach((pt, index) => {
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
});
diagramCode += ` par Parallel actions\n`;
for (let i = 0; i < 3; i += 2) {
diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
if (i < participantTypes.length - 2) {
diagramCode += ` and\n`;
}
}
diagramCode += ` end\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render alternative flows', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += getParticipantLine('A', 'actor');
diagramCode += getParticipantLine('B', 'boundary');
diagramCode += ` alt Successful case\n`;
diagramCode += ` A ->> B: Request\n`;
diagramCode += ` B -->> A: Success\n`;
diagramCode += ` else Failure case\n`;
diagramCode += ` A ->> B: Request\n`;
diagramCode += ` B --x A: Failure\n`;
diagramCode += ` end\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render loops', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.slice(0, 3).forEach((pt, index) => {
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
});
diagramCode += ` loop For each participant\n`;
for (let i = 0; i < 3; i++) {
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Message ${i}\n`;
}
diagramCode += ` end\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render boxes around groups', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += ` box Group 1\n`;
participantTypes.slice(0, 3).forEach((pt, index) => {
diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
});
diagramCode += ` end\n`;
diagramCode += ` box rgb(200,220,255) Group 2\n`;
participantTypes.slice(3, 6).forEach((pt, index) => {
diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
});
diagramCode += ` end\n`;
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[3].display}0: Cross-group message\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render with different font settings', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.slice(0, 3).forEach((pt, index) => {
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
});
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Regular message\n`;
diagramCode += ` Note right of ${participantTypes[1].display}1: Regular note\n`;
imgSnapshotTest(diagramCode, {
look,
sequence: {
actorFontFamily: 'courier',
actorFontSize: 14,
messageFontFamily: 'Arial',
messageFontSize: 12,
noteFontFamily: 'times',
noteFontSize: 16,
noteAlign: 'left',
},
});
});
});
});
// Additional tests for specific combinations
describe('Sequence Diagram Special Cases', () => {
it('should render complex sequence with all features', () => {
const diagramCode = `
sequenceDiagram
box rgb(200,220,255) Authentication
actor User
participant LoginUI@{ "type": "boundary" }
participant AuthService@{ "type": "control" }
participant UserDB@{ "type": "database" }
end
box rgb(200,255,220) Order Processing
participant Order@{ "type": "entity" }
participant OrderQueue@{ "type": "queue" }
participant AuditLogs@{ "type": "collections" }
end
User ->> LoginUI: Enter credentials
LoginUI ->> AuthService: Validate
AuthService ->> UserDB: Query user
UserDB -->> AuthService: User data
alt Valid credentials
AuthService -->> LoginUI: Success
LoginUI -->> User: Welcome
par Place order
User ->> Order: New order
Order ->> OrderQueue: Process
and
Order ->> AuditLogs: Record
end
loop Until confirmed
OrderQueue ->> Order: Update status
Order -->> User: Notification
end
else Invalid credentials
AuthService --x LoginUI: Failure
LoginUI --x User: Retry
end
`;
imgSnapshotTest(diagramCode, {});
});
it('should render with wrapped messages and notes', () => {
const diagramCode = `
sequenceDiagram
participant A
participant B
A ->> B: This is a very long message that should wrap properly in the diagram rendering
Note over A,B: This is a very long note that should also wrap properly when rendered in the diagram
par Wrapped parallel
A ->> B: Parallel message 1<br>with explicit line break
and
B ->> A: Parallel message 2<br>with explicit line break
end
loop Wrapped loop
Note right of B: This is a long note<br>in a loop
A ->> B: Message in loop
end
`;
imgSnapshotTest(diagramCode, { sequence: { wrap: true } });
});
describe('Sequence Diagram Rendering with Different Participant Types', () => {
it('should render a sequence diagram with various participant types', () => {
imgSnapshotTest(
`
sequenceDiagram
participant User@{ "type": "actor" }
participant AuthService@{ "type": "control" }
participant UI@{ "type": "boundary" }
participant OrderController@{ "type": "control" }
participant Product@{ "type": "entity" }
participant MongoDB@{ "type": "database" }
participant Products@{ "type": "collections" }
participant OrderQueue@{ "type": "queue" }
User ->> UI: Login request
UI ->> AuthService: Validate credentials
AuthService -->> UI: Authentication token
UI ->> OrderController: Place order
OrderController ->> Product: Check availability
Product -->> OrderController: Available
OrderController ->> MongoDB: Save order
MongoDB -->> OrderController: Order saved
OrderController ->> OrderQueue: Process payment
OrderQueue -->> User: Order confirmation
`
);
});
it('should render participant creation and destruction with different types', () => {
imgSnapshotTest(`
sequenceDiagram
participant Alice@{ "type" : "boundary" }
Alice->>Bob: Hello Bob, how are you ?
Bob->>Alice: Fine, thank you. And you?
create participant Carl@{ "type" : "control" }
Alice->>Carl: Hi Carl!
create actor D as Donald
Carl->>D: Hi!
destroy Carl
Alice-xCarl: We are too many
destroy Bob
Bob->>Alice: I agree
`);
});
it('should handle complex interactions between different participant types', () => {
imgSnapshotTest(
`
sequenceDiagram
box rgb(200,220,255) Authentication
participant User@{ "type": "actor" }
participant LoginUI@{ "type": "boundary" }
participant AuthService@{ "type": "control" }
participant UserDB@{ "type": "database" }
end
box rgb(200,255,220) Order Processing
participant Order@{ "type": "entity" }
participant OrderQueue@{ "type": "queue" }
participant AuditLogs@{ "type": "collections" }
end
User ->> LoginUI: Enter credentials
LoginUI ->> AuthService: Validate
AuthService ->> UserDB: Query user
UserDB -->> AuthService: User data
alt Valid credentials
AuthService -->> LoginUI: Success
LoginUI -->> User: Welcome
par Place order
User ->> Order: New order
Order ->> OrderQueue: Process
and
Order ->> AuditLogs: Record
end
loop Until confirmed
OrderQueue ->> Order: Update status
Order -->> User: Notification
end
else Invalid credentials
AuthService --x LoginUI: Failure
LoginUI --x User: Retry
end
`,
{ sequence: { useMaxWidth: false } }
);
});
it('should render parallel processes with different participant types', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Customer@{ "type": "actor" }
participant Frontend@{ "type": "participant" }
participant PaymentService@{ "type": "boundary" }
participant InventoryManager@{ "type": "control" }
participant Order@{ "type": "entity" }
participant OrdersDB@{ "type": "database" }
participant NotificationQueue@{ "type": "queue" }
Customer ->> Frontend: Place order
Frontend ->> Order: Create order
par Parallel Processing
Order ->> PaymentService: Process payment
and
Order ->> InventoryManager: Reserve items
end
PaymentService -->> Order: Payment confirmed
InventoryManager -->> Order: Items reserved
Order ->> OrdersDB: Save finalized order
OrdersDB -->> Order: Order saved
Order ->> NotificationQueue: Send confirmation
NotificationQueue -->> Customer: Order confirmation
`
);
});
});
it('should render different participant types with notes and loops', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Admin
participant Dashboard
participant AuthService@{ "type" : "boundary" }
participant UserManager@{ "type" : "control" }
participant UserProfile@{ "type" : "entity" }
participant UserDB@{ "type" : "database" }
participant Logs@{ "type" : "database" }
Admin ->> Dashboard: Open user management
loop Authentication check
Dashboard ->> AuthService: Verify admin rights
AuthService ->> Dashboard: Access granted
end
Dashboard ->> UserManager: List users
UserManager ->> UserDB: Query users
UserDB ->> UserManager: Return user data
Note right of UserDB: Encrypted data<br/>requires decryption
UserManager ->> UserProfile: Format profiles
UserProfile ->> UserManager: Formatted data
UserManager ->> Dashboard: Display users
Dashboard ->> Logs: Record access
Logs ->> Admin: Audit trail
`
);
});
it('should render different participant types with alternative flows', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Client
participant MobileApp
participant CloudService@{ "type" : "boundary" }
participant DataProcessor@{ "type" : "control" }
participant Transaction@{ "type" : "entity" }
participant TransactionsDB@{ "type" : "database" }
participant EventBus@{ "type" : "queue" }
Client ->> MobileApp: Initiate transaction
MobileApp ->> CloudService: Authenticate
alt Authentication successful
CloudService -->> MobileApp: Auth token
MobileApp ->> DataProcessor: Process data
DataProcessor ->> Transaction: Create transaction
Transaction ->> TransactionsDB: Save record
TransactionsDB -->> Transaction: Confirmation
Transaction ->> EventBus: Publish event
EventBus -->> Client: Notification
else Authentication failed
CloudService -->> MobileApp: Error
MobileApp -->> Client: Show error
end
`
);
});
it('should render different participant types with wrapping text', () => {
imgSnapshotTest(
`
sequenceDiagram
participant B@{ "type" : "boundary" }
participant C@{ "type" : "control" }
participant E@{ "type" : "entity" }
participant DB@{ "type" : "database" }
participant COL@{ "type" : "collections" }
participant Q@{ "type" : "queue" }
FE ->> B: Another long message<br/>with explicit<br/>line breaks
B -->> FE: Response message that is also quite long and needs to wrap
FE ->> C: Process data
C ->> E: Validate
E -->> C: Validation result
C ->> DB: Save
DB -->> C: Save result
C ->> COL: Log
COL -->> Q: Forward
Q -->> LongNameUser: Final response with confirmation of all actions taken
`,
{ sequence: { wrap: true } }
);
});
describe('Sequence Diagram - New Participant Types with Long Notes and Messages', () => {
it('should render long notes left of boundary', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "boundary" }
actor Bob
Alice->>Bob: Hola
Note left of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render wrapped long notes left of control', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "control" }
actor Bob
Alice->>Bob: Hola
Note left of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long notes right of entity', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "entity" }
actor Bob
Alice->>Bob: Hola
Note right of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render wrapped long notes right of database', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "database" }
actor Bob
Alice->>Bob: Hola
Note right of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long notes over collections', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "collections" }
actor Bob
Alice->>Bob: Hola
Note over Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render wrapped long notes over queue', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "queue" }
actor Bob
Alice->>Bob: Hola
Note over Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render notes over actor and boundary', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Alice
participant Charlie@{ "type" : "boundary" }
note over Alice: Some note
note over Charlie: Other note
`,
{}
);
});
it('should render long messages from database to collections', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "database" }
participant Bob@{ "type" : "collections" }
Alice->>Bob: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render wrapped long messages from control to entity', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "control" }
participant Bob@{ "type" : "entity" }
Alice->>Bob:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long messages from queue to boundary', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "queue" }
participant Bob@{ "type" : "boundary" }
Alice->>Bob: I'm short
Bob->>Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
`,
{}
);
});
it('should render wrapped long messages from actor to database', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Alice
participant Bob@{ "type" : "database" }
Alice->>Bob: I'm short
Bob->>Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
`,
{}
);
});
});
describe('svg size', () => {
it('should render a sequence diagram when useMaxWidth is true (default)', () => {
renderGraph(
`
sequenceDiagram
actor Alice
participant Bob@{ "type" : "boundary" }
participant John@{ "type" : "control" }
Alice ->> Bob: Hello Bob, how are you?
Bob-->>John: How about you John?
Bob--x Alice: I am good thanks!
Bob-x John: I am good thanks!
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
Bob-->Alice: Checking with John...
alt either this
Alice->>John: Yes
else or this
Alice->>John: No
else or this will happen
Alice->John: Maybe
end
par this happens in parallel
Alice -->> Bob: Parallel message 1
and
Alice -->> John: Parallel message 2
end
`,
{ sequence: { useMaxWidth: true } }
);
cy.get('svg').should((svg) => {
expect(svg).to.have.attr('width', '100%');
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
expect(maxWidthValue).to.be.within(820 * 0.95, 820 * 1.05);
});
});
it('should render a sequence diagram when useMaxWidth is false', () => {
renderGraph(
`
sequenceDiagram
actor Alice
participant Bob@{ "type" : "boundary" }
participant John@{ "type" : "control" }
Alice ->> Bob: Hello Bob, how are you?
Bob-->>John: How about you John?
Bob--x Alice: I am good thanks!
Bob-x John: I am good thanks!
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
Bob-->Alice: Checking with John...
alt either this
Alice->>John: Yes
else or this
Alice->>John: No
else or this will happen
Alice->John: Maybe
end
par this happens in parallel
Alice -->> Bob: Parallel message 1
and
Alice -->> John: Parallel message 2
end
`,
{ sequence: { useMaxWidth: false } }
);
cy.get('svg').should((svg) => {
const width = parseFloat(svg.attr('width'));
expect(width).to.be.within(820 * 0.95, 820 * 1.05);
expect(svg).to.not.have.attr('style');
});
});
});
});

View File

@@ -893,17 +893,6 @@ describe('Sequence diagram', () => {
}
);
});
it('should handle bidirectional arrows with autonumber', () => {
imgSnapshotTest(`
sequenceDiagram
autonumber
participant A
participant B
A<<->>B: This is a bidirectional message
A->B: This is a normal message`);
});
it('should support actor links and properties when not mirrored EXPERIMENTAL: USE WITH CAUTION', () => {
//Be aware that the syntax for "properties" is likely to be changed.
imgSnapshotTest(

View File

@@ -131,6 +131,19 @@
<body>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
curve: linear
---
flowchart LR
D["Use<br/>the<br/>editor"] -- Mermaid js --> I["fa:fa-code Text"]
I --> D & D
D@{ shape: question}
I@{ shape: question}
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@@ -157,84 +170,149 @@ treemap
"Leaf 2.2": 25
"Leaf 2.3": 12
classDef class1 fill:red,color:blue,stroke:#FFD600;
</pre>
<pre id="diagram5" class="mermaid">
---
config:
layout: elk
flowchart:
curve: rounded
---
flowchart LR
I["fa:fa-code Text"] -- Mermaid js --> D["Use<br/>the<br/>editor!"]
I --> D & D
D@{ shape: question}
I@{ shape: question}
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: tidy-tree
---
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
</pre
>
<pre id="diagram4" class="mermaid2">
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
curve: linear
---
flowchart LR
A[A] --> B[B]
A[A] --- B([C])
A@{ shape: diamond}
%%B@{ shape: diamond}
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
curve: linear
---
flowchart LR
A[A] -- Mermaid js --> B[B]
A[A] -- Mermaid js --- B[B]
A@{ shape: diamond}
B@{ shape: diamond}
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
curve: rounded
---
flowchart LR
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
I --> D & D
D@{ shape: question}
I@{ shape: question}
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
curve: rounded
elk:
nodePlacementStrategy: NETWORK_SIMPLEX
---
flowchart LR
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
D --> I & I
a["a"]
D@{ shape: trap-b}
I@{ shape: lean-l}
</pre>
<pre id="diagram4" class="mermaid">
---
config:
treemap:
valueFormat: '$0,0'
layout: elk
---
treemap
"Budget"
"Operations"
"Salaries": 7000
"Equipment": 2000
"Supplies": 1000
"Marketing"
"Advertising": 4000
"Events": 1000
flowchart LR
%% subgraph s1["Untitled subgraph"]
C["Evaluate"]
%% end
</pre
>
B --> C
</pre>
<pre id="diagram4" class="mermaid">
treemap
title Accessible Treemap Title
"Category A"
"Item A1": 10
"Item A2": 20
"Category B"
"Item B1": 15
"Item B2": 25
</pre>
<pre id="diagram4" class="mermaid2">
flowchart LR
AB["apa@apa@"] --> B(("`apa@apa`"))
</pre>
<pre id="diagram4" class="mermaid2">
flowchart
D(("for D"))
</pre>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@==> B
e1@{ animate: true}
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
flowchart:
//curve: linear
---
flowchart LR
A e1@--> B
classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
class e1 animate
</pre>
<h2>infinite</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
</pre>
<h2>Mermaid - edge-animation-slow</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
e1@{ animation: fast}
</pre>
<h2>Mermaid - edge-animation-fast</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear;
class e1 edge-animation-fast
</pre>
%% A ==> B
%% A2 --> B2
A{A} --> B((Bo boo)) & B & B & B
<pre id="diagram4" class="mermaid2">
info </pre
>
<pre id="diagram4" class="mermaid2">
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
theme: default
look: classic
---
flowchart LR
subgraph s1["APA"]
D{"Use the editor"}
end
subgraph S2["S2"]
s1
I>"fa:fa-code Text"]
E["E"]
end
D -- Mermaid js --> I
D --> I & E
E --> I
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -259,7 +337,7 @@ config:
end
end
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -272,7 +350,7 @@ config:
D-->I
D-->I
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -311,7 +389,7 @@ flowchart LR
n8@{ shape: rect}
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -327,7 +405,7 @@ flowchart LR
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -336,7 +414,7 @@ flowchart LR
A{A} --> B & C
</pre
>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -348,7 +426,7 @@ flowchart LR
end
</pre
>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -366,7 +444,7 @@ flowchart LR
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
kanban:
@@ -385,81 +463,81 @@ kanban
task3[💻 Develop login feature]@{ ticket: 103 }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
kanban
id2[In progress]
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
kanban:
@@ -488,6 +566,7 @@ kanban
<script type="module">
import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
import tidyTreeLayouts from './mermaid-layout-tidy-tree.esm.mjs';
const staticBellIconPack = {
prefix: 'fa6-regular',
@@ -513,6 +592,7 @@ kanban
},
]);
mermaid.registerLayoutLoaders(layouts);
mermaid.registerLayoutLoaders(tidyTreeLayouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
@@ -530,7 +610,7 @@ kanban
// look: 'handDrawn',
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
// layout: 'dagre',
// layout: 'elk',
layout: 'elk',
// layout: 'fixed',
// htmlLabels: false,
flowchart: { titleTopMargin: 10 },

View File

@@ -0,0 +1,379 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
B
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
root((mindmap))
A
B
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap))
A
B
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap))
A
B
</pre>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
</pre>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
</pre>
<hr />
<script type="module">
import mermaid from '/mermaid.esm.mjs';
mermaid.initialize({
theme: 'default',
logLevel: 3,
securityLevel: 'loose',
});
</script>
</body>
</html>

View File

@@ -41,6 +41,10 @@ graph TB
const { svg } = await mermaid.render('d22', value);
console.log(svg);
el.innerHTML = svg;
// mermaid.test1('first_slow', 1200).then((r) => console.info(r));
// mermaid.test1('second_fast', 200).then((r) => console.info(r));
// mermaid.test1('third_fast', 200).then((r) => console.info(r));
// mermaid.test1('forth_slow', 1200).then((r) => console.info(r));
</script>
</body>
</html>

View File

@@ -182,7 +182,7 @@ const contentLoadedApi = async function () {
for (let i = 0; i < numCodes; i++) {
const { svg, bindFunctions } = await mermaid.render('newid' + i, graphObj.code[i], divs[i]);
div.innerHTML = svg;
bindFunctions?.(div);
bindFunctions(div);
}
} else {
const div = document.createElement('div');
@@ -194,7 +194,7 @@ const contentLoadedApi = async function () {
const { svg, bindFunctions } = await mermaid.render('newid', graphObj.code, div);
div.innerHTML = svg;
console.log(div.innerHTML);
bindFunctions?.(div);
bindFunctions(div);
}
}
};

View File

@@ -2,223 +2,219 @@
"durations": [
{
"spec": "cypress/integration/other/configuration.spec.js",
"duration": 6162
"duration": 5815
},
{
"spec": "cypress/integration/other/external-diagrams.spec.js",
"duration": 2148
"duration": 2035
},
{
"spec": "cypress/integration/other/ghsa.spec.js",
"duration": 3585
"duration": 3386
},
{
"spec": "cypress/integration/other/iife.spec.js",
"duration": 2099
"duration": 2089
},
{
"spec": "cypress/integration/other/interaction.spec.js",
"duration": 12119
"duration": 11578
},
{
"spec": "cypress/integration/other/rerender.spec.js",
"duration": 2063
"duration": 2119
},
{
"spec": "cypress/integration/other/xss.spec.js",
"duration": 31921
"duration": 27282
},
{
"spec": "cypress/integration/rendering/appli.spec.js",
"duration": 3385
"duration": 3377
},
{
"spec": "cypress/integration/rendering/architecture.spec.ts",
"duration": 108
"duration": 97
},
{
"spec": "cypress/integration/rendering/block.spec.js",
"duration": 18063
"duration": 18137
},
{
"spec": "cypress/integration/rendering/c4.spec.js",
"duration": 5519
"duration": 5455
},
{
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
"duration": 40040
"duration": 40850
},
{
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
"duration": 38665
"duration": 37964
},
{
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
"duration": 22836
"duration": 23446
},
{
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
"duration": 37096
"duration": 37207
},
{
"spec": "cypress/integration/rendering/classDiagram.spec.js",
"duration": 16452
"duration": 16531
},
{
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
"duration": 10387
"duration": 9385
},
{
"spec": "cypress/integration/rendering/current.spec.js",
"duration": 2803
"duration": 2697
},
{
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
"duration": 86891
"duration": 88648
},
{
"spec": "cypress/integration/rendering/erDiagram.spec.js",
"duration": 15206
"duration": 15094
},
{
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
"duration": 3540
"duration": 3548
},
{
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
"duration": 41975
"duration": 44889
},
{
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
"duration": 30909
"duration": 30487
},
{
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
"duration": 7881
"duration": 7375
},
{
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
"duration": 24294
"duration": 24913
},
{
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
"duration": 47652
"duration": 51927
},
{
"spec": "cypress/integration/rendering/flowchart.spec.js",
"duration": 32049
"duration": 31676
},
{
"spec": "cypress/integration/rendering/gantt.spec.js",
"duration": 20248
"duration": 19897
},
{
"spec": "cypress/integration/rendering/gitGraph.spec.js",
"duration": 51202
"duration": 53450
},
{
"spec": "cypress/integration/rendering/iconShape.spec.ts",
"duration": 283546
"duration": 287035
},
{
"spec": "cypress/integration/rendering/imageShape.spec.ts",
"duration": 57257
"duration": 58555
},
{
"spec": "cypress/integration/rendering/info.spec.ts",
"duration": 3352
"duration": 3179
},
{
"spec": "cypress/integration/rendering/journey.spec.js",
"duration": 7423
"duration": 7099
},
{
"spec": "cypress/integration/rendering/kanban.spec.ts",
"duration": 7804
"duration": 7628
},
{
"spec": "cypress/integration/rendering/katex.spec.js",
"duration": 3847
"duration": 3764
},
{
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
"duration": 2637
"duration": 2573
},
{
"spec": "cypress/integration/rendering/mindmap.spec.ts",
"duration": 11658
"duration": 11269
},
{
"spec": "cypress/integration/rendering/newShapes.spec.ts",
"duration": 149500
"duration": 148389
},
{
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
"duration": 115427
"duration": 113395
},
{
"spec": "cypress/integration/rendering/packet.spec.ts",
"duration": 4801
"duration": 4714
},
{
"spec": "cypress/integration/rendering/pie.spec.ts",
"duration": 6786
"duration": 6446
},
{
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
"duration": 9422
"duration": 9133
},
{
"spec": "cypress/integration/rendering/radar.spec.js",
"duration": 5652
"duration": 5544
},
{
"spec": "cypress/integration/rendering/requirement.spec.js",
"duration": 2787
"duration": 2709
},
{
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
"duration": 53631
"duration": 55647
},
{
"spec": "cypress/integration/rendering/sankey.spec.ts",
"duration": 7075
},
{
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
"duration": 20446
"duration": 6751
},
{
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
"duration": 37326
"duration": 36618
},
{
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
"duration": 29208
"duration": 29642
},
{
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
"duration": 16328
"duration": 16037
},
{
"spec": "cypress/integration/rendering/theme.spec.js",
"duration": 30541
"duration": 30006
},
{
"spec": "cypress/integration/rendering/timeline.spec.ts",
"duration": 8611
"duration": 8451
},
{
"spec": "cypress/integration/rendering/treemap.spec.ts",
"duration": 11878
"duration": 11996
},
{
"spec": "cypress/integration/rendering/xyChart.spec.js",
"duration": 20400
"duration": 20627
},
{
"spec": "cypress/integration/rendering/zenuml.spec.js",
"duration": 3528
"duration": 3472
}
]
}

View File

@@ -20,14 +20,14 @@
width: 800
nodeAlignment: left
---
sankey
sankey
a,b,8
b,c,8
c,d,8
d,e,8
x,c,4
c,y,4
c,y,4
</pre>
<h2>Energy flow</h2>

View File

@@ -19,7 +19,6 @@
- [addDirective](functions/addDirective.md)
- [getConfig](functions/getConfig.md)
- [getSiteConfig](functions/getSiteConfig.md)
- [getUserDefinedConfig](functions/getUserDefinedConfig.md)
- [reset](functions/reset.md)
- [sanitize](functions/sanitize.md)
- [saveConfigFromInitialize](functions/saveConfigFromInitialize.md)

View File

@@ -1,19 +0,0 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md).
[**mermaid**](../../README.md)
---
# Function: getUserDefinedConfig()
> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
Defined in: [packages/mermaid/src/config.ts:252](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L252)
## Returns
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)

View File

@@ -10,7 +10,7 @@
# Interface: LayoutData
Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145)
Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L170)
## Indexable
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.co
> **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L148)
Defined in: [packages/mermaid/src/rendering-util/types.ts:173](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L173)
---
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.co
> **edges**: `Edge`\[]
Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147)
Defined in: [packages/mermaid/src/rendering-util/types.ts:172](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L172)
---
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.co
> **nodes**: `Node`\[]
Defined in: [packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146)
Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L171)

View File

@@ -10,7 +10,7 @@
# Interface: LayoutLoaderDefinition
Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L21)
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.co
> `optional` **algorithm**: `string`
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
Defined in: [packages/mermaid/src/rendering-util/render.ts:27](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L27)
---
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.co
> **loader**: `LayoutLoader`
Defined in: [packages/mermaid/src/rendering-util/render.ts:23](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L23)
Defined in: [packages/mermaid/src/rendering-util/render.ts:26](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L26)
---
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:23](https://github.co
> **name**: `string`
Defined in: [packages/mermaid/src/rendering-util/render.ts:22](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L22)
Defined in: [packages/mermaid/src/rendering-util/render.ts:25](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L25)

View File

@@ -10,7 +10,7 @@
# Interface: ParseOptions
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
Defined in: [packages/mermaid/src/types.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L76)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mer
> `optional` **suppressErrors**: `boolean`
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
Defined in: [packages/mermaid/src/types.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L81)
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
The `parseError` function will not be called.

View File

@@ -10,7 +10,7 @@
# Interface: ParseResult
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mer
> **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
The config passed as YAML frontmatter or directives
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
The diagram type, e.g. 'flowchart', 'sequence', etc.

View File

@@ -10,7 +10,7 @@
# Interface: RenderOptions
Defined in: [packages/mermaid/src/rendering-util/render.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L7)
Defined in: [packages/mermaid/src/rendering-util/render.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L10)
## Properties
@@ -18,4 +18,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:7](https://github.com
> `optional` **algorithm**: `string`
Defined in: [packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8)
Defined in: [packages/mermaid/src/rendering-util/render.ts:11](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L11)

View File

@@ -10,7 +10,7 @@
# Interface: RenderResult
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/me
> `optional` **bindFunctions**: (`element`) => `void`
Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
Defined in: [packages/mermaid/src/types.ts:120](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L120)
Bind function to be called after the svg has been inserted into the DOM.
This is necessary for adding event listeners to the elements in the svg.
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
The diagram type, e.g. 'flowchart', 'sequence', etc.
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
> **svg**: `string`
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
The svg code for the rendered graph.

View File

@@ -73,7 +73,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [Obsidian](https://help.obsidian.md/Editing+and+formatting/Advanced+formatting+syntax#Diagram) ✅
- [Outline](https://docs.getoutline.com/s/guide/doc/diagrams-KQiKoT4wzK) ✅
- [Redmine](https://redmine.org)
- [Mermaid Macro](https://redmine.org/plugins/redmine_mermaid_macro)
- [Mermaid Macro](https://www.redmine.org/plugins/redmine_mermaid_macro)
- [Markdown for mermaid plugin](https://github.com/jamieh-mongolian/markdown-for-mermaid-plugin)
- [redmine-mermaid](https://github.com/styz/redmine_mermaid)
- Visual Studio Code [Polyglot Interactive Notebooks](https://github.com/dotnet/interactive#net-interactive)
@@ -117,7 +117,7 @@ Content Management Systems/Enterprise Content Management
- [Grav CMS](https://getgrav.org/)
- [Mermaid Diagrams Plugin](https://github.com/DanielFlaum/grav-plugin-mermaid-diagrams)
- [GitLab Markdown Adapter](https://github.com/Goutte/grav-plugin-gitlab-markdown-adapter)
- [Tiki Wiki CMS Groupware](https://tiki.org)
- [Tiki](https://tiki.org)
- [Tracker Entity Relationship Diagram](https://doc.tiki.org/Tracker-Entity-Relationship-Diagram)
- [VitePress](https://vitepress.vuejs.org/)
- [Plugin for Mermaid.js](https://emersonbottero.github.io/vitepress-plugin-mermaid/)

View File

@@ -6,66 +6,6 @@
# Blog
## [Mermaid introduces the Visual Editor for Entity Relationship diagrams](https://docs.mermaidchart.com/blog/posts/mermaid-introduces-the-visual-editor-for-entity-relationship-diagrams)
7/15/2025 • 7 mins
Mermaid just introduced a Visual Editor for Entity Relationship diagrams, letting anyone map database structures through a simple point-and-click interface instead of code. This no-code ER builder now sits alongside Mermaids editors for flowcharts, sequence, and class diagrams, enabling teams to craft and share polished data models for apps, AI, and business processes.
## [Mermaid supports Treemap Diagrams now!!!](https://docs.mermaidchart.com/blog/posts/mermaid-have-treemap-diagrams-now)
7/3/2025 • 4 mins
Mermaid has introduced Treemap diagrams, currently in beta, enhancing hierarchical data visualization. Treemap diagrams use nested rectangles to represent data relationships, focusing on size and proportions. They offer various applications, including budget visualization and market analysis. With simple syntax and customization options, users can effectively present complex data hierarchies.
## [AI Diagram Generators and Data Visualization: Best Practices](https://docs.mermaidchart.com/blog/posts/ai-diagram-generators-and-data-visualization-best-practices)
7/2/2025 • 6 mins
AI diagram generators transform complex data into clear, interactive visuals enabling faster analysis, better decisions, and stronger collaboration across teams. By combining automation with manual refinement, these tools empower anyone to communicate insights effectively, regardless of technical skill level.
## [How to Choose the Best AI Diagram Generator for Your Needs (2025)](https://docs.mermaidchart.com/blog/posts/how-to-choose-the-best-ai-diagram-generator-for-your-needs-2025)
6/26/2025 • 14 mins
AI diagram generators are transforming how developers visualize and communicate complex systems, reducing hours of manual work into minutes. With tools like Mermaid AI, users benefit from both code-based and visual editing, enabling seamless collaboration and precision. Whether youre diagramming workflows, software architecture, or data relationships, the right AI tool can significantly boost productivity and streamline communication.
## [5 Time-Saving Tips for Using Mermaids AI Diagram Generator Effectively](https://docs.mermaidchart.com/blog/posts/5-time-saving-tips-for-using-mermaids-ai-diagram-generator-effectively)
6/11/2025 • 10 mins
See how developers can save time and boost productivity using Mermaid Charts AI diagram generator. Learn five practical tips that help turn plain language into powerful, professional diagrams.
## [Enhancing Team Collaboration with AI-Powered Diagrams](https://docs.mermaidchart.com/blog/posts/enhancing-team-collaboration-with-ai-powered-diagrams)
5/27/2025 • 6 mins
Software teams move fast, but old-school diagramming tools cant keep up. Mermaid Chart replaces static slides and whiteboards with real-time, AI-generated visuals that evolve with your code and ideas. Just describe a process in plain English, and watch it come to life.
## [What is an AI Diagram Generator? Benefits and Use Cases](https://docs.mermaidchart.com/blog/posts/what-is-an-ai-diagram-generator-benefits-and-use-cases)
5/22/2025 • 6 mins
Discover how AI diagram generators like Mermaid Chart transform developer workflows. Instantly turn text into flowcharts, ERDs, and system diagrams, no manual drag-and-drop needed. Learn how it works, key benefits, and real-world use cases.
## [How to Use Mermaid Chart as an AI Diagram Generator for Developers](https://docs.mermaidchart.com/blog/posts/how-to-use-mermaid-chart-as-an-ai-diagram-generator)
5/21/2025 • 9 mins
Would an AI diagram generator make your life easier? We think it would!
## [Mermaid Chart VS Code Plugin: Create and Edit Mermaid.js Diagrams in Visual Studio Code](https://docs.mermaidchart.com/blog/posts/mermaid-chart-vs-code-plugin-create-and-edit-mermaid-js-diagrams-in-visual-studio-code)
3/21/2025 • 5 mins
The Mermaid Chart VS Code Plugin is a powerful developer diagramming tool that brings Mermaid.js diagramming directly into your Visual Studio Code environment. Whether youre visualizing software architecture, documenting API flows, fixing bad documentation, or managing flowcharts and sequence diagrams, this plugin integrates seamlessly into your workflow. Key Features of the Mermaid Chart VS Code \[…]
## [Mermaid Chart: The Evolution of Mermaid](https://docs.mermaidchart.com/blog/posts/mermaid-chart-the-evolution-of-mermaid)
1/30/2025 • 3 mins
Mermaid revolutionized diagramming with its simple, markdown-style syntax, empowering millions of developers worldwide. Now, Mermaid Chart takes it further with AI-powered visuals, a GUI for seamless editing, real-time collaboration, and advanced design tools. Experience the next generation of diagramming—faster, smarter, and built for modern teams. Try Mermaid Chart today!
## [GUI for editing Mermaid Class Diagrams](https://docs.mermaidchart.com/blog/posts/gui-for-editing-mermaid-class-diagrams)
1/17/2025 • 5 mins

View File

@@ -326,7 +326,9 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
| Bang | Bang | `bang` | Bang | `bang` |
| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
| Cloud | Cloud | `cloud` | cloud | `cloud` |
| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |
@@ -983,23 +985,11 @@ flowchart TD
- `b`
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
- `on`
- `off`
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
```mermaid-example
flowchart TD
%% My image with a constrained aspect ratio
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
```
```mermaid
flowchart TD
%% My image with a constrained aspect ratio
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
```
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
## Links between nodes
@@ -1828,7 +1818,7 @@ config:
graph LR
```
#### Edge level curve style using Edge IDs (v11.10.0+)
#### Edge level curve style using Edge IDs (v\<MERMAID_RELEASE_VERSION>+)
You can assign IDs to [edges](#attaching-an-id-to-edges). After assigning an ID you can modify the line style by modifying the edge's `curve` property using the following syntax:

View File

@@ -39,7 +39,7 @@ Drawing a pie chart is really simple in mermaid.
**Note:**
> Pie chart values must be **positive numbers greater than zero**.
> Pie chart values must be **positive numbers greater than zero**.\
> **Negative values are not allowed** and will result in an error.
\[pie] \[showData] (OPTIONAL)

View File

@@ -74,126 +74,6 @@ sequenceDiagram
Bob->>Alice: Hi Alice
```
### Boundary
If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "boundary" }
participant Bob
Alice->>Bob: Request from boundary
Bob->>Alice: Response to boundary
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "boundary" }
participant Bob
Alice->>Bob: Request from boundary
Bob->>Alice: Response to boundary
```
### Control
If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "control" }
participant Bob
Alice->>Bob: Control request
Bob->>Alice: Control response
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "control" }
participant Bob
Alice->>Bob: Control request
Bob->>Alice: Control response
```
### Entity
If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "entity" }
participant Bob
Alice->>Bob: Entity request
Bob->>Alice: Entity response
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "entity" }
participant Bob
Alice->>Bob: Entity request
Bob->>Alice: Entity response
```
### Database
If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "database" }
participant Bob
Alice->>Bob: DB query
Bob->>Alice: DB result
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "database" }
participant Bob
Alice->>Bob: DB query
Bob->>Alice: DB result
```
### Collections
If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "collections" }
participant Bob
Alice->>Bob: Collections request
Bob->>Alice: Collections response
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "collections" }
participant Bob
Alice->>Bob: Collections request
Bob->>Alice: Collections response
```
### Queue
If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "queue" }
participant Bob
Alice->>Bob: Queue message
Bob->>Alice: Queue response
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "queue" }
participant Bob
Alice->>Bob: Queue message
Bob->>Alice: Queue response
```
### Aliases
The actor can have a convenient identifier and a descriptive label.

View File

@@ -17,6 +17,7 @@ export default tseslint.config(
...tseslint.configs.stylisticTypeChecked,
{
ignores: [
'**/*.d.ts',
'**/dist/',
'**/node_modules/',
'.git/',

145
instructions.md Normal file
View File

@@ -0,0 +1,145 @@
Please help me to implement the tidy-tree algorithm.
- I have added a placeholder with the correct signature in `packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/`.
Replace the cytoscape layout with one using tidy-tree.
This is the API for tidy-tree:
```
# non-layered-tidy-tree-layout
Draw non-layered tidy trees in linear time.
> This a JavaScript port from the project [cwi-swat/non-layered-tidy-trees](https://github.com/cwi-swat/non-layered-tidy-trees), which is written in Java. The algorithm used in that project is from the paper by _A.J. van der Ploeg_, [Drawing Non-layered Tidy Trees in Linear Time](http://oai.cwi.nl/oai/asset/21856/21856B.pdf). There is another JavaScript port from that project [d3-flextree](https://github.com/Klortho/d3-flextree), which depends on _d3-hierarchy_. This project is dependency free.
## Getting started
### Installation
```
npm install non-layered-tidy-tree-layout
```
Or
```
yarn add non-layered-tidy-tree-layout
````
There's also a built verison: `dist/non-layered-tidy-tree-layout.js` for use with browser `<script>` tag, or as a Javascript module.
### Usage
```js
import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout'
// BoundingBox(gap, bottomPadding)
const bb = new BoundingBox(10, 20)
const layout = new Layout(bb)
const treeData = {
id: 0,
width: 40,
height: 40,
children: [
{
id: 1,
width: 40,
height: 40,
children: [{ id: 6, width: 400, height: 40 }]
},
{ id: 2, width: 40, height: 40 },
{ id: 3, width: 40, height: 40 },
{ id: 4, width: 40, height: 40 },
{ id: 5, width: 40, height: 80 }
]
}
const { result, boundingBox } = layout.layout(treeData)
// result:
// {
// id: 0,
// x: 300,
// y: 0,
// width: 40,
// height: 40,
// children: [
// {
// id: 1,
// x: 185,
// y: 60,
// width: 40,
// height: 40,
// children: [
// { id: 6, x: 5, y: 120, width: 400, height: 40 }
// ]
// },
// { id: 2, x: 242.5, y: 60, width: 40, height: 40 },
// { id: 3, x: 300, y: 60, width: 40, height: 40 },
// { id: 4, x: 357.5, y: 60, width: 40, height: 40 },
// { id: 5, x: 415, y: 60, width: 40, height: 80 }
// ]
// }
//
// boundingBox:
// {
// left: 5,
// right: 455,
// top: 0,
// bottom: 160
// }
````
The method `Layout.layout` modifies `treeData` inplace. It returns an object like `{ result: treeData, boundingBox: {left: num, right: num, top: num, bottom: num} }`. `result` is the same object `treeData` with calculated coordinates, `boundingBox` are the coordinates for the whole tree:
![](./screenshots/1.png)
The red dashed lines are the bounding boxes for each node. `Layout.layout()` produces coordinates to draw nodes, which are the grey boxes with black border.
The library also provides a class `Tree` and a method `layout`.
```js
/**
* Constructor for Tree.
* @param {number} width - width of bounding box
* @param {number} height - height of bounding box
* @param {number} y - veritcal coordinate of bounding box
* @param {array} children - a list of Tree instances
*/
new Tree(width, height, y, children);
/**
* Calculate x, y coordindates and assign them to tree.
* @param {Object} tree - a Tree object
*/
layout(tree);
```
In case your data structure are not the same as provided by the example above, you can refer to `src/helpers.js` to implement a `Layout` class that converts your data to a `Tree`, then call `layout` to calculate the coordinates for drawing.
## License
[MIT](./LICENSE)
## Changelog
### [2.0.1]
- Fixed bounding box calculation in `Layout.getSize` and `Layout.assignLayout` and `Layout.layout`
### [2.0.0]
- Added `Layout.layout`
- Removed `Layout.layoutTreeData`
### [1.0.0]
- Added `Layout`, `BoundingBox`, `layout`, `Tree`
```
```

View File

@@ -27,6 +27,9 @@
"devDependencies": {
"mermaid": "workspace:*"
},
"peerDependencies": {
"mermaid": "workspace:~"
},
"publishConfig": {
"access": "public"
}

View File

@@ -1,16 +1,5 @@
# @mermaid-js/layout-elk
## 0.1.9
### Patch Changes
- [#6857](https://github.com/mermaid-js/mermaid/pull/6857) [`b9ef683`](https://github.com/mermaid-js/mermaid/commit/b9ef683fb67b8959abc455d6cc5266c37ba435f6) Thanks [@knsv](https://github.com/knsv)! - feat: Exposing elk configuration forceNodeModelOrder and considerModelOrder to the mermaid configuration
- [#6849](https://github.com/mermaid-js/mermaid/pull/6849) [`2260948`](https://github.com/mermaid-js/mermaid/commit/2260948b7bda08f00616c2ce678bed1da69eb96c) Thanks [@anderium](https://github.com/anderium)! - Make elk not force node model order, but strongly consider it instead
- Updated dependencies [[`b9ef683`](https://github.com/mermaid-js/mermaid/commit/b9ef683fb67b8959abc455d6cc5266c37ba435f6), [`2c0931d`](https://github.com/mermaid-js/mermaid/commit/2c0931da46794b49d2523211e25f782900c34e94), [`33e08da`](https://github.com/mermaid-js/mermaid/commit/33e08daf175125295a06b1b80279437004a4e865), [`814b68b`](https://github.com/mermaid-js/mermaid/commit/814b68b4a94813f7c6b3d7fb4559532a7bab2652), [`fce7cab`](https://github.com/mermaid-js/mermaid/commit/fce7cabb71d68a20a66246fe23d066512126a412), [`fc07f0d`](https://github.com/mermaid-js/mermaid/commit/fc07f0d8abca49e4f887d7457b7b94fb07d1e3da), [`12e01bd`](https://github.com/mermaid-js/mermaid/commit/12e01bdb5cacf3569133979a5a4f1d8973e9aec1), [`01aaef3`](https://github.com/mermaid-js/mermaid/commit/01aaef39b4a1ec8bc5a0c6bfa3a20b712d67f4dc), [`daf8d8d`](https://github.com/mermaid-js/mermaid/commit/daf8d8d3befcd600618a629977b76463b38d0ad9), [`c36cd05`](https://github.com/mermaid-js/mermaid/commit/c36cd05c45ac3090181152b4dae41f8d7b569bd6), [`8bb29fc`](https://github.com/mermaid-js/mermaid/commit/8bb29fc879329ad109898e4025b4f4eba2ab0649), [`71b04f9`](https://github.com/mermaid-js/mermaid/commit/71b04f93b07f876df2b30656ef36036c1d0e4e4f), [`c99bce6`](https://github.com/mermaid-js/mermaid/commit/c99bce6bab4c7ce0b81b66d44f44853ce4aeb1c3), [`6cc1926`](https://github.com/mermaid-js/mermaid/commit/6cc192680a2531cab28f87a8061a53b786e010f3), [`9da6fb3`](https://github.com/mermaid-js/mermaid/commit/9da6fb39ae278401771943ac85d6d1b875f78cf1), [`e48b0ba`](https://github.com/mermaid-js/mermaid/commit/e48b0ba61dab7f95aa02da603b5b7d383b894932), [`4d62d59`](https://github.com/mermaid-js/mermaid/commit/4d62d5963238400270e9314c6e4d506f48147074), [`e9ce8cf`](https://github.com/mermaid-js/mermaid/commit/e9ce8cf4da9062d85098042044822100889bb0dd), [`9258b29`](https://github.com/mermaid-js/mermaid/commit/9258b2933bbe1ef41087345ffea3731673671c49), [`da90f67`](https://github.com/mermaid-js/mermaid/commit/da90f6760b6efb0da998bcb63b75eecc29e06c08), [`0133f1c`](https://github.com/mermaid-js/mermaid/commit/0133f1c0c5cff4fc4c8e0b99e9cf0b3d49dcbe71), [`895f9d4`](https://github.com/mermaid-js/mermaid/commit/895f9d43ff98ca05ebfba530789f677f31a011ff)]:
- mermaid@11.10.0
## 0.1.8
### Patch Changes

View File

@@ -2,7 +2,7 @@
This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine.
> [!NOTE]
> [!NOTE]
> The ELK Layout engine will not be available in all providers that support mermaid by default.
> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine.
@@ -69,4 +69,4 @@ mermaid.registerLayoutLoaders(elkLayouts);
- `elk.mrtree`: Multi-root tree layout
- `elk.sporeOverlap`: Spore overlap layout
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively -->
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively. -->

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/layout-elk",
"version": "0.1.9",
"version": "0.1.8",
"description": "ELK layout engine for mermaid",
"module": "dist/mermaid-layout-elk.core.mjs",
"types": "dist/layouts.d.ts",

View File

@@ -0,0 +1,9 @@
export interface TreeData {
parentById: Record<string, string>;
childrenById: Record<string, string[]>;
}
export declare const findCommonAncestor: (
id1: string,
id2: string,
{ parentById }: TreeData
) => string;

View File

@@ -0,0 +1,19 @@
import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
export declare const render: (
data4Layout: LayoutData,
svg: SVG,
{
common,
getConfig,
insertCluster,
insertEdge,
insertEdgeLabel,
insertMarkers,
insertNode,
interpolateToCurve,
labelHelper,
log,
positionEdgeLabel,
}: InternalHelpers,
{ algorithm }: RenderOptions
) => Promise<void>;

View File

@@ -1,10 +1,23 @@
import type {
InternalHelpers,
LayoutData,
RenderOptions,
SVG,
SVGGroup,
} from '@mermaid-chart/mermaid';
// @ts-ignore TODO: Investigate D3 issue
import { curveLinear } from 'd3';
import ELK from 'elkjs/lib/elk.bundled.js';
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
type Node = LayoutData['nodes'][number];
// Minimal structural type to avoid depending on d3 Selection typings
interface D3Selection<T extends Element> {
node(): T | null;
attr(name: string, value: string): D3Selection<T>;
}
interface LabelData {
width: number;
height: number;
@@ -13,9 +26,9 @@ interface LabelData {
}
interface NodeWithVertex extends Omit<Node, 'domId'> {
children?: unknown[];
children?: LayoutData['nodes'];
labelData?: LabelData;
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>;
domId?: D3Selection<SVGAElement | SVGGElement>;
}
export const render = async (
@@ -51,14 +64,13 @@ export const render = async (
// Add the element to the DOM
if (!node.isGroup) {
const child: NodeWithVertex = {
...node,
};
const child = node as NodeWithVertex;
graph.children.push(child);
nodeDb[node.id] = child;
nodeDb[node.id] = node;
const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
const boundingBox = childNodeEl.node()!.getBBox();
// Store the domId separately for rendering, not in the ELK graph
child.domId = childNodeEl;
child.width = boundingBox.width;
child.height = boundingBox.height;
@@ -68,7 +80,9 @@ export const render = async (
...node,
children: [],
};
// Let elk render with the copy
graph.children.push(child);
// Save the original containing the intersection function
nodeDb[node.id] = child;
await addVertices(nodeEl, nodeArr, child, node.id);
@@ -143,7 +157,7 @@ export const render = async (
height: node.height,
};
if (node.isGroup) {
log.debug('id abc88 subgraph = ', node.id, node.x, node.y, node.labelData);
log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData);
const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph');
// TODO use faster way of cloning
const clusterNode = JSON.parse(JSON.stringify(node));
@@ -152,10 +166,10 @@ export const render = async (
clusterNode.width = Math.max(clusterNode.width, node.labelData.width);
await insertCluster(subgraphEl, clusterNode);
log.debug('id (UIO)= ', node.id, node.width, node.shape, node.labels);
log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels);
} else {
log.info(
'id NODE = ',
'Id NODE = ',
node.id,
node.x,
node.y,
@@ -197,25 +211,19 @@ export const render = async (
});
});
subgraphs.forEach(function (subgraph: { id: string | number }) {
const data: any = { id: subgraph.id };
if (parentLookupDb.parentById[subgraph.id] !== undefined) {
data.parent = parentLookupDb.parentById[subgraph.id];
}
});
return parentLookupDb;
};
const getEdgeStartEndPoint = (edge: any) => {
const source: any = edge.start;
const target: any = edge.end;
// edge.start and edge.end are IDs (string/number) in our layout data
const sourceId: string | number = edge.start;
const targetId: string | number = edge.end;
// Save the original source and target
const sourceId = source;
const targetId = target;
const source = sourceId;
const target = targetId;
const startNode = nodeDb[edge.start.id];
const endNode = nodeDb[edge.end.id];
const startNode = nodeDb[sourceId];
const endNode = nodeDb[targetId];
if (!startNode || !endNode) {
return { source, target };
@@ -259,7 +267,6 @@ export const render = async (
const edges = dataForLayout.edges;
const labelsEl = svg.insert('g').attr('class', 'edgeLabels');
const linkIdCnt: any = {};
const dir = dataForLayout.direction || 'DOWN';
let defaultStyle: string | undefined;
let defaultLabelStyle: string | undefined;
@@ -289,7 +296,7 @@ export const render = async (
linkIdCnt[linkIdBase]++;
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
}
const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase];
const linkId = linkIdBase; // + '_' + linkIdCnt[linkIdBase];
edge.id = linkId;
log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
const linkNameStart = 'LS_' + edge.start;
@@ -358,8 +365,8 @@ export const render = async (
break;
}
edgeData.style = edgeData.style += style;
edgeData.labelStyle = edgeData.labelStyle += labelStyle;
edgeData.style += style;
edgeData.labelStyle += labelStyle;
const conf = getConfig();
if (edge.interpolate !== undefined) {
@@ -396,13 +403,11 @@ export const render = async (
// calculate start and end points of the edge, note that the source and target
// can be modified for shapes that have ports
// @ts-ignore TODO: fix this
const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir);
const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge);
log.debug('abc78 source and target', source, target);
// Add the edge to the graph
graph.edges.push({
// @ts-ignore TODO: fix this
id: 'e' + edge.start + edge.end,
...edge,
sources: [source],
targets: [target],
@@ -436,6 +441,7 @@ export const render = async (
case 'RL':
return 'LEFT';
case 'TB':
case 'TD': // TD is an alias for TB in Mermaid
return 'DOWN';
case 'BT':
return 'UP';
@@ -459,155 +465,6 @@ export const render = async (
}
}
function intersectLine(
p1: { y: number; x: number },
p2: { y: number; x: number },
q1: { x: any; y: any },
q2: { x: any; y: any }
) {
log.debug('UIO intersectLine', p1, p2, q1, q2);
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
// p7 and p473.
// let a1, a2, b1, b2, c1, c2;
// let r1, r2, r3, r4;
// let denom, offset, num;
// let x, y;
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
// b1 y + c1 = 0.
const a1 = p2.y - p1.y;
const b1 = p1.x - p2.x;
const c1 = p2.x * p1.y - p1.x * p2.y;
// Compute r3 and r4.
const r3 = a1 * q1.x + b1 * q1.y + c1;
const r4 = a1 * q2.x + b1 * q2.y + c1;
const epsilon = 1e-6;
// Check signs of r3 and r4. If both point 3 and point 4 lie on
// same side of line 1, the line segments do not intersect.
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
return /*DON'T_INTERSECT*/;
}
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
const a2 = q2.y - q1.y;
const b2 = q1.x - q2.x;
const c2 = q2.x * q1.y - q1.x * q2.y;
// Compute r1 and r2
const r1 = a2 * p1.x + b2 * p1.y + c2;
const r2 = a2 * p2.x + b2 * p2.y + c2;
// Check signs of r1 and r2. If both point 1 and point 2 lie
// on same side of second line segment, the line segments do
// not intersect.
if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
return /*DON'T_INTERSECT*/;
}
// Line segments intersect: compute intersection point.
const denom = a1 * b2 - a2 * b1;
if (denom === 0) {
return /*COLLINEAR*/;
}
const offset = Math.abs(denom / 2);
// The denom/2 is to get rounding instead of truncating. It
// is added or subtracted to the numerator, depending upon the
// sign of the numerator.
let num = b1 * c2 - b2 * c1;
const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
num = a2 * c1 - a1 * c2;
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
return { x: x, y: y };
}
function sameSign(r1: number, r2: number) {
return r1 * r2 > 0;
}
const diamondIntersection = (
bounds: { x: any; y: any; width: any; height: any },
outsidePoint: { x: number; y: number },
insidePoint: any
) => {
const x1 = bounds.x;
const y1 = bounds.y;
const w = bounds.width; //+ bounds.padding;
const h = bounds.height; // + bounds.padding;
const polyPoints = [
{ x: x1, y: y1 - h / 2 },
{ x: x1 + w / 2, y: y1 },
{ x: x1, y: y1 + h / 2 },
{ x: x1 - w / 2, y: y1 },
];
log.debug(
`APA16 diamondIntersection calc abc89:
outsidePoint: ${JSON.stringify(outsidePoint)}
insidePoint : ${JSON.stringify(insidePoint)}
node-bounds : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`,
JSON.stringify(polyPoints)
);
const intersections = [];
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
polyPoints.forEach(function (entry) {
minX = Math.min(minX, entry.x);
minY = Math.min(minY, entry.y);
});
const left = x1 - w / 2 - minX;
const top = y1 - h / 2 - minY;
for (let i = 0; i < polyPoints.length; i++) {
const p1 = polyPoints[i];
const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
const intersect = intersectLine(
bounds,
outsidePoint,
{ x: left + p1.x, y: top + p1.y },
{ x: left + p2.x, y: top + p2.y }
);
if (intersect) {
intersections.push(intersect);
}
}
if (!intersections.length) {
return bounds;
}
log.debug('UIO intersections', intersections);
if (intersections.length > 1) {
// More intersections, find the one nearest to edge end point
intersections.sort(function (p, q) {
const pdx = p.x - outsidePoint.x;
const pdy = p.y - outsidePoint.y;
const distp = Math.sqrt(pdx * pdx + pdy * pdy);
const qdx = q.x - outsidePoint.x;
const qdy = q.y - outsidePoint.y;
const distq = Math.sqrt(qdx * qdx + qdy * qdy);
return distp < distq ? -1 : distp === distq ? 0 : 1;
});
}
return intersections[0];
};
const intersection = (
node: { x: any; y: any; width: number; height: number },
outsidePoint: { x: number; y: number },
@@ -653,7 +510,7 @@ export const render = async (
return res;
} else {
// Intersection on sides of rect
// Intersection onn sides of rect
if (insidePoint.x < outsidePoint.x) {
r = outsidePoint.x - w - x;
} else {
@@ -696,62 +553,298 @@ export const render = async (
}
return false;
};
/**
* This function will page a path and node where the last point(s) in the path is inside the node
* and return an update path ending by the border of the node.
*/
const cutPathAtIntersect = (
_points: any[],
bounds: { x: any; y: any; width: any; height: any; padding: any },
isDiamond: boolean
) => {
log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
const points: any[] = [];
let lastPointOutside = _points[0];
let isInside = false;
_points.forEach((point: any) => {
// check if point is inside the boundary rect
if (!outsideNode(bounds, point) && !isInside) {
// First point inside the rect found
// Calc the intersection coord between the point and the last point outside the rect
let inter;
if (isDiamond) {
const inter2 = diamondIntersection(bounds, lastPointOutside, point);
const distance = Math.sqrt(
(lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2
);
if (distance > 1) {
inter = inter2;
}
}
if (!inter) {
inter = intersection(bounds, lastPointOutside, point);
}
const cutter2 = (startNode: any, endNode: any, _points: any[]) => {
const startBounds = {
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
width: startNode.width,
height: startNode.height,
padding: startNode.padding,
};
const endBounds = {
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
width: endNode.width,
height: endNode.height,
padding: endNode.padding,
};
// Check case where the intersection is the same as the last point
let pointPresent = false;
points.forEach((p) => {
pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y);
});
// if (!pointPresent) {
if (!points.some((e) => e.x === inter.x && e.y === inter.y)) {
points.push(inter);
} else {
log.debug('abc88 no intersect', inter, points);
}
// points.push(inter);
isInside = true;
} else {
// Outside
log.debug('abc88 outside', point, lastPointOutside, points);
lastPointOutside = point;
// points.push(point);
if (!isInside) {
points.push(point);
if (_points.length === 0) {
return [];
}
// Copy the original points array
const points = [..._points];
// The first point is the center of sNode, the last point is the center of eNode
const startCenter = points[0];
const endCenter = points[points.length - 1];
log.debug('UIO cutter2: startCenter:', startCenter);
log.debug('UIO cutter2: endCenter:', endCenter);
let firstOutsideStartIndex = -1;
let lastOutsideEndIndex = -1;
// Single iteration through the array
for (const [i, point] of points.entries()) {
// Check if this is the first point outside the start node
if (firstOutsideStartIndex === -1 && outsideNode(startBounds, point)) {
firstOutsideStartIndex = i;
log.debug('UIO cutter2: First point outside start node at index', i, point);
}
// Check if this point is outside the end node (keep updating to find the last one)
if (outsideNode(endBounds, point)) {
lastOutsideEndIndex = i;
log.debug('UIO cutter2: Point outside end node at index', i, point);
}
}
log.debug(
'UIO cutter2: firstOutsideStartIndex:',
firstOutsideStartIndex,
'lastOutsideEndIndex:',
lastOutsideEndIndex
);
log.debug('UIO cutter2: startBounds:', startBounds);
log.debug('UIO cutter2: endBounds:', endBounds);
log.debug('UIO cutter2: original points:', _points);
// Calculate intersection with start node if we found a point outside it
if (firstOutsideStartIndex !== -1) {
const outsidePoint = points[firstOutsideStartIndex];
let startIntersection;
// Try using the node's intersect method first
if (startNode.intersect) {
startIntersection = startNode.intersect(outsidePoint);
// Check if the intersection is valid (distance > 1)
const distance = Math.sqrt(
(startCenter.x - startIntersection.x) ** 2 + (startCenter.y - startIntersection.y) ** 2
);
if (distance <= 1) {
startIntersection = null;
}
}
});
// Fallback to intersection function
if (!startIntersection) {
startIntersection = intersection(startBounds, startCenter, outsidePoint);
}
// Replace the first point with the intersection
if (startIntersection) {
// Check if the intersection is the same as any existing point
const isDuplicate = points.some(
(p, index) =>
index > 0 &&
Math.abs(p.x - startIntersection.x) < 0.1 &&
Math.abs(p.y - startIntersection.y) < 0.1
);
if (isDuplicate) {
log.debug(
'UIO cutter2: Start intersection is duplicate of existing point, removing first point instead'
);
points.shift(); // Remove the first point instead of replacing it
} else {
log.debug(
'UIO cutter2: Replacing first point',
points[0],
'with intersection',
startIntersection
);
points[0] = startIntersection;
}
}
}
// Calculate intersection with end node
// Need to recalculate indices since we may have removed the first point
let outsidePointForEnd = null;
let outsideIndexForEnd = -1;
// Find the last point that's outside the end node in the current points array
for (let i = points.length - 1; i >= 0; i--) {
if (outsideNode(endBounds, points[i])) {
outsidePointForEnd = points[i];
outsideIndexForEnd = i;
log.debug('UIO cutter2: Found point outside end node at current index:', i, points[i]);
break;
}
}
if (!outsidePointForEnd && points.length > 1) {
// No points outside end node, try using the second-to-last point
log.debug('UIO cutter2: No points outside end node, trying second-to-last point');
outsidePointForEnd = points[points.length - 2];
outsideIndexForEnd = points.length - 2;
}
if (outsidePointForEnd) {
// Check if the outside point is actually on the boundary (distance = 0 from intersection)
// If so, we need to create a truly outside point
let actualOutsidePoint = outsidePointForEnd;
// Quick check: if the point is very close to the node boundary, move it further out
const dx = Math.abs(outsidePointForEnd.x - endBounds.x);
const dy = Math.abs(outsidePointForEnd.y - endBounds.y);
const w = endBounds.width / 2;
const h = endBounds.height / 2;
log.debug('UIO cutter2: Checking if outside point is truly outside:', {
outsidePoint: outsidePointForEnd,
dx,
dy,
w,
h,
isOnBoundary: Math.abs(dx - w) < 1 || Math.abs(dy - h) < 1,
});
// If the point is on or very close to the boundary, move it further out
if (Math.abs(dx - w) < 1 || Math.abs(dy - h) < 1) {
log.debug('UIO cutter2: Outside point is on boundary, creating truly outside point');
// Move the point further away from the node center
const directionX = outsidePointForEnd.x - endBounds.x;
const directionY = outsidePointForEnd.y - endBounds.y;
const length = Math.sqrt(directionX * directionX + directionY * directionY);
if (length > 0) {
// Move the point 10 pixels further out in the same direction
actualOutsidePoint = {
x: endBounds.x + (directionX / length) * (length + 10),
y: endBounds.y + (directionY / length) * (length + 10),
};
log.debug('UIO cutter2: Created truly outside point:', actualOutsidePoint);
}
}
let endIntersection;
// Try using the node's intersect method first
if (endNode.intersect) {
endIntersection = endNode.intersect(actualOutsidePoint);
log.debug('UIO cutter2: endNode.intersect result:', endIntersection);
// Check if the intersection is on the wrong side of the node
const isWrongSide =
(actualOutsidePoint.x < endBounds.x && endIntersection.x > endBounds.x) ||
(actualOutsidePoint.x > endBounds.x && endIntersection.x < endBounds.x);
if (isWrongSide) {
log.debug('UIO cutter2: endNode.intersect returned wrong side, setting to null');
endIntersection = null;
} else {
// Check if the intersection is valid (distance > 1)
const distance = Math.sqrt(
(actualOutsidePoint.x - endIntersection.x) ** 2 +
(actualOutsidePoint.y - endIntersection.y) ** 2
);
log.debug('UIO cutter2: Distance from outside point to intersection:', distance);
if (distance <= 1) {
log.debug('UIO cutter2: endNode.intersect distance too small, setting to null');
endIntersection = null;
}
}
} else {
log.debug('UIO cutter2: endNode.intersect method not available');
}
// Fallback to intersection function
if (!endIntersection) {
// Create a proper inside point that's on the correct side of the node
// The inside point should be between the outside point and the far edge
const insidePoint = {
x:
actualOutsidePoint.x < endBounds.x
? endBounds.x - endBounds.width / 4
: endBounds.x + endBounds.width / 4,
y: endCenter.y,
};
log.debug('UIO cutter2: Using fallback intersection function with:', {
endBounds,
actualOutsidePoint,
insidePoint,
endCenter,
});
endIntersection = intersection(endBounds, actualOutsidePoint, insidePoint);
log.debug('UIO cutter2: Fallback intersection result:', endIntersection);
}
// Replace the last point with the intersection
if (endIntersection) {
// Check if the intersection is the same as any existing point
const isDuplicate = points.some(
(p, index) =>
index < points.length - 1 &&
Math.abs(p.x - endIntersection.x) < 0.1 &&
Math.abs(p.y - endIntersection.y) < 0.1
);
if (isDuplicate) {
log.debug(
'UIO cutter2: End intersection is duplicate of existing point, removing last point instead'
);
points.pop(); // Remove the last point instead of replacing it
} else {
log.debug(
'UIO cutter2: Replacing last point',
points[points.length - 1],
'with intersection',
endIntersection,
'using outside point at index',
outsideIndexForEnd
);
points[points.length - 1] = endIntersection;
}
}
} else {
log.debug('UIO cutter2: No suitable outside point found for end node intersection');
}
// Final cleanup: Check if the last point is too close to the previous point
if (points.length > 1) {
const lastPoint = points[points.length - 1];
const secondLastPoint = points[points.length - 2];
const distance = Math.sqrt(
(lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2
);
// If the distance is very small (less than 2 pixels), remove the last point
if (distance < 2) {
log.debug(
'UIO cutter2: Last point too close to previous point, removing it. Distance:',
distance
);
log.debug('UIO cutter2: Removing last point:', lastPoint, 'keeping:', secondLastPoint);
points.pop();
}
}
log.debug('UIO cutter2: Final points:', points);
// Debug: Check which side of the end node we're ending at
if (points.length > 0) {
const finalPoint = points[points.length - 1];
const endNodeCenter = endBounds.x;
const endNodeLeftEdge = endNodeCenter - endBounds.width / 2;
const endNodeRightEdge = endNodeCenter + endBounds.width / 2;
log.debug('UIO cutter2: End node analysis:', {
finalPoint,
endNodeCenter,
endNodeLeftEdge,
endNodeRightEdge,
endingSide: finalPoint.x < endNodeCenter ? 'LEFT' : 'RIGHT',
distanceFromLeftEdge: Math.abs(finalPoint.x - endNodeLeftEdge),
distanceFromRightEdge: Math.abs(finalPoint.x - endNodeRightEdge),
});
}
return points;
};
@@ -774,9 +867,11 @@ export const render = async (
'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy,
'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges,
'elk.direction': 'DOWN',
'spacing.baseValue': 35,
'elk.layered.unnecessaryBendpoints': true,
'elk.layered.cycleBreaking.strategy': data4Layout.config.elk?.cycleBreakingStrategy,
'spacing.baseValue': 25,
// 'elk.layered.unnecessaryBendpoints': true,
// 'elk.layered.cycleBreaking.strategy': data4Layout.config.elk?.cycleBreakingStrategy,
// 'elk.layered.cycleBreaking.strategy': 'GREEDY_MODEL_ORDER',
// 'elk.layered.cycleBreaking.strategy': 'MODEL_ORDER',
// 'spacing.nodeNode': 20,
// 'spacing.nodeNodeBetweenLayers': 25,
// 'spacing.edgeNode': 20,
@@ -784,23 +879,29 @@ export const render = async (
// 'spacing.edgeEdge': 10,
// 'spacing.edgeEdgeBetweenLayers': 20,
// 'spacing.nodeSelfLoop': 20,
// Tweaking options
// 'elk.layered.nodePlacement.favorStraightEdges': true,
// 'nodePlacement.feedbackEdges': true,
// 'elk.layered.wrapping.multiEdge.improveCuts': true,
// 'elk.layered.wrapping.multiEdge.improveWrappedEdges': true,
// 'elk.layered.wrapping.strategy': 'MULTI_EDGE',
// 'elk.layered.edgeRouting.selfLoopDistribution': 'EQUALLY',
// 'elk.layered.mergeHierarchyEdges': true,
// 'elk.layered.feedbackEdges': true,
// 'elk.layered.crossingMinimization.semiInteractive': true,
// 'elk.layered.edgeRouting.splines.sloppy.layerSpacingFactor': 1,
// 'elk.layered.edgeRouting.polyline.slopedEdgeZoneWidth': 4.0,
// 'elk.layered.wrapping.validify.strategy': 'LOOK_BACK',
// 'elk.insideSelfLoops.activate': true,
// 'elk.alg.layered.options.EdgeStraighteningStrategy': 'NONE',
'nodePlacement.favorStraightEdges': true,
'elk.layered.nodePlacement.favorStraightEdges': true,
'nodePlacement.feedbackEdges': true,
'elk.layered.wrapping.multiEdge.improveCuts': true,
'elk.layered.wrapping.multiEdge.improveWrappedEdges': true,
'elk.layered.wrapping.strategy': 'MULTI_EDGE',
// 'elk.layered.wrapping.strategy': 'SINGLE_EDGE',
'elk.layered.edgeRouting.selfLoopDistribution': 'EQUALLY',
'elk.layered.mergeHierarchyEdges': true,
'elk.layered.feedbackEdges': true,
'elk.layered.crossingMinimization.semiInteractive': true,
'elk.layered.edgeRouting.splines.sloppy.layerSpacingFactor': 1,
'elk.layered.edgeRouting.polyline.slopedEdgeZoneWidth': 4.0,
'elk.layered.wrapping.validify.strategy': 'LOOK_BACK',
'elk.insideSelfLoops.activate': true,
'elk.separateConnectedComponents': true,
'elk.alg.layered.options.EdgeStraighteningStrategy': 'NONE',
// 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', // NODES_AND_EDGES
// 'elk.layered.wrapping.cutting.strategy': 'ARD', // NODES_AND_EDGES
'elk.layered.considerModelOrder.strategy': 'EDGES', // NODES_AND_EDGES
'elk.layered.wrapping.cutting.strategy': 'ARD', // NODES_AND_EDGES
},
children: [],
edges: [],
@@ -840,15 +941,16 @@ export const render = async (
// Subgraph
if (parentLookupDb.childrenById[node.id] !== undefined) {
// Set label and adjust node width separately (avoid side effects in labels array)
node.labels = [
{
text: node.label,
width: node?.labelData?.width || 50,
height: node?.labelData?.height || 50,
width: node?.labelData?.width ?? 50,
height: node?.labelData?.height ?? 50,
},
(node.width = node.width + 2 * node.padding),
log.debug('UIO node label', node?.labelData?.width, node.padding),
];
node.width = node.width + 2 * node.padding;
log.debug('UIO node label', node?.labelData?.width, node.padding);
node.layoutOptions = {
'spacing.baseValue': 30,
'nodeLabels.placement': '[H_CENTER V_TOP, INSIDE]',
@@ -869,11 +971,16 @@ export const render = async (
delete node.height;
}
});
elkGraph.edges.forEach((edge: any) => {
log.debug('APA01 processing edges, count:', elkGraph.edges.length);
elkGraph.edges.forEach((edge: any, index: number) => {
log.debug('APA01 processing edge', index, ':', edge);
const source = edge.sources[0];
const target = edge.targets[0];
log.debug('APA01 source:', source, 'target:', target);
log.debug('APA01 nodeDb[source]:', nodeDb[source]);
log.debug('APA01 nodeDb[target]:', nodeDb[target]);
if (nodeDb[source].parentId !== nodeDb[target].parentId) {
if (nodeDb[source] && nodeDb[target] && nodeDb[source].parentId !== nodeDb[target].parentId) {
const ancestorId = findCommonAncestor(source, target, parentLookupDb);
// an edge that breaks a subgraph has been identified, set configuration accordingly
setIncludeChildrenPolicy(source, ancestorId);
@@ -881,7 +988,37 @@ export const render = async (
}
});
const g = await elk.layout(elkGraph);
log.debug('APA01 before');
log.debug('APA01 elkGraph structure:', JSON.stringify(elkGraph, null, 2));
log.debug('APA01 elkGraph.children length:', elkGraph.children?.length);
log.debug('APA01 elkGraph.edges length:', elkGraph.edges?.length);
// Validate that all edge references exist as nodes
elkGraph.edges?.forEach((edge: any, index: number) => {
log.debug(`APA01 validating edge ${index}:`, edge);
if (edge.sources) {
edge.sources.forEach((sourceId: any) => {
const sourceExists = elkGraph.children?.some((child: any) => child.id === sourceId);
log.debug(`APA01 source ${sourceId} exists:`, sourceExists);
});
}
if (edge.targets) {
edge.targets.forEach((targetId: any) => {
const targetExists = elkGraph.children?.some((child: any) => child.id === targetId);
log.debug(`APA01 target ${targetId} exists:`, targetExists);
});
}
});
let g;
try {
g = await elk.layout(elkGraph);
log.debug('APA01 after - success');
log.info('APA01 layout result:', JSON.stringify(g, null, 2));
} catch (error) {
log.error('APA01 ELK layout error:', error);
throw error;
}
// debugger;
await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
@@ -941,7 +1078,7 @@ export const render = async (
log.debug(
'UIO width',
startNode.id,
startNode.with,
startNode.width,
'bbox.width=',
bbox.width,
'lw=',
@@ -961,7 +1098,7 @@ export const render = async (
log.debug(
'UIO width',
startNode.id,
startNode.with,
startNode.width,
bbox.width,
'EW = ',
ew,
@@ -969,43 +1106,26 @@ export const render = async (
startNode.innerHTML
);
}
if (startNode.shape === 'diamond' || startNode.shape === 'diam') {
startNode.x = startNode.offset.posX + startNode.width / 2;
startNode.y = startNode.offset.posY + startNode.height / 2;
endNode.x = endNode.offset.posX + endNode.width / 2;
endNode.y = endNode.offset.posY + endNode.height / 2;
if (startNode.shape !== 'rect33') {
edge.points.unshift({
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
x: startNode.x,
y: startNode.y,
});
}
if (endNode.shape === 'diamond' || endNode.shape === 'diam') {
if (endNode.shape !== 'rect33') {
edge.points.push({
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
x: endNode.x,
y: endNode.y,
});
}
edge.points = cutPathAtIntersect(
edge.points.reverse(),
{
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
width: sw,
height: startNode.height,
padding: startNode.padding,
},
startNode.shape === 'diamond' || startNode.shape === 'diam'
).reverse();
edge.points = cutPathAtIntersect(
edge.points,
{
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
width: ew,
height: endNode.height,
padding: endNode.padding,
},
endNode.shape === 'diamond' || endNode.shape === 'diam'
);
log.debug('UIO cutter2: Points before cutter2:', edge.points);
edge.points = cutter2(startNode, endNode, edge.points);
log.debug('UIO cutter2: Points after cutter2:', edge.points);
const paths = insertEdge(
edgesEl,
edge,
@@ -1013,7 +1133,8 @@ export const render = async (
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
data4Layout.diagramId,
true
);
log.info('APA12 edge points after insert', JSON.stringify(edge.points));

View File

@@ -5,6 +5,6 @@
"outDir": "./dist",
"types": ["vitest/importMeta", "vitest/globals"]
},
"include": ["./src/**/*.ts"],
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
"typeRoots": ["./src/types"]
}

View File

@@ -0,0 +1,59 @@
# @mermaid-js/layout-tidy-tree
This package provides a bidirectional tidy tree layout engine for Mermaid based on the non-layered-tidy-tree-layout algorithm.
> [!NOTE]
> The Tidy Tree Layout engine will not be available in all providers that support mermaid by default.
> The websites will have to install the @mermaid-js/layout-tidy-tree package to use the Tidy Tree layout engine.
## Usage
```
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
B
```
### With bundlers
```sh
npm install @mermaid-js/layout-tidy-tree
```
```ts
import mermaid from 'mermaid';
import tidyTreeLayouts from '@mermaid-js/layout-tidy-tree';
mermaid.registerLayoutLoaders(tidyTreeLayouts);
```
### With CDN
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import tidyTreeLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-tidy-tree@0/dist/mermaid-layout-tidy-tree.esm.min.mjs';
mermaid.registerLayoutLoaders(tidyTreeLayouts);
</script>
```
## Tidy Tree Layout Overview
tidy-tree: The bidirectional tidy tree layout
The bidirectional tidy tree layout algorithm creates two separate trees that grow horizontally in opposite directions from a central root node:
Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
This creates a balanced, symmetric layout that is ideal for mindmaps, organizational charts, and other tree-based diagrams.
Layout Structure:
[Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
↓ ↓ ↓ ↓
[GrandChild] [GrandChild] [GrandChild] [GrandChild]

View File

@@ -0,0 +1,46 @@
{
"name": "@mermaid-js/layout-tidy-tree",
"version": "0.1.0",
"description": "Tidy-tree layout engine for mermaid",
"module": "dist/mermaid-layout-tidy-tree.core.mjs",
"types": "dist/layouts.d.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/mermaid-layout-tidy-tree.core.mjs",
"types": "./dist/layouts.d.ts"
},
"./": "./"
},
"keywords": [
"diagram",
"markdown",
"tidy-tree",
"mermaid",
"layout"
],
"scripts": {},
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid"
},
"contributors": [
"Knut Sveidqvist",
"Sidharth Vinod"
],
"license": "MIT",
"dependencies": {
"d3": "^7.9.0",
"non-layered-tidy-tree-layout": "^2.0.2"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"mermaid": "workspace:^"
},
"peerDependencies": {
"mermaid": "^11.0.2"
},
"files": [
"dist"
]
}

View File

@@ -0,0 +1,50 @@
/**
* Bidirectional Tidy-Tree Layout Algorithm for Generic Diagrams
*
* This module provides a layout algorithm implementation using the
* non-layered-tidy-tree-layout algorithm for positioning nodes and edges
* in tree structures with a bidirectional approach.
*
* The algorithm creates two separate trees that grow horizontally in opposite
* directions from a central root node:
* - Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
* - Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
*
* This creates a balanced, symmetric layout that is ideal for mindmaps,
* organizational charts, and other tree-based diagrams.
*
* The algorithm follows the unified rendering pattern and can be used
* by any diagram type that provides compatible LayoutData.
*/
/**
* Render function for the bidirectional tidy-tree layout algorithm
*
* This function follows the unified rendering pattern used by all layout algorithms.
* It takes LayoutData, inserts nodes into DOM, runs the bidirectional tidy-tree layout algorithm,
* and renders the positioned elements to the SVG.
*
* Features:
* - Alternates root children between left and right trees
* - Left tree grows horizontally to the left (rotated 90° counterclockwise)
* - Right tree grows horizontally to the right (rotated 90° clockwise)
* - Uses tidy-tree algorithm for optimal spacing within each tree
* - Creates symmetric, balanced layouts
* - Maintains proper edge connections between all nodes
*
* Layout Structure:
* ```
* [Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
* ↓ ↓ ↓ ↓
* [GrandChild] [GrandChild] [GrandChild] [GrandChild]
* ```
*
* @param layoutData - Layout data containing nodes, edges, and configuration
* @param svg - SVG element to render to
* @param helpers - Internal helper functions for rendering
* @param options - Rendering options
*/
export { default } from './layouts.js';
export * from './types.js';
export * from './layout.js';
export { render } from './render.js';

View File

@@ -0,0 +1,410 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock non-layered-tidy-tree-layout
vi.mock('non-layered-tidy-tree-layout', () => ({
BoundingBox: vi.fn().mockImplementation(() => ({})),
Layout: vi.fn().mockImplementation(() => ({
layout: vi.fn().mockImplementation((treeData) => {
const result = { ...treeData };
if (result.id?.toString().startsWith('virtual-root')) {
result.x = 0;
result.y = 0;
} else {
result.x = 100;
result.y = 50;
}
if (result.children) {
result.children.forEach((child: any, index: number) => {
child.x = 50 + index * 100;
child.y = 100;
if (child.children) {
child.children.forEach((grandchild: any, gIndex: number) => {
grandchild.x = 25 + gIndex * 50;
grandchild.y = 200;
});
}
});
}
return {
result,
boundingBox: {
left: 0,
right: 200,
top: 0,
bottom: 250,
},
};
}),
})),
}));
import { executeTidyTreeLayout, validateLayoutData } from './layout.js';
import type { LayoutResult } from './types.js';
import type { LayoutData, MermaidConfig } from 'mermaid';
describe('Tidy-Tree Layout Algorithm', () => {
let mockConfig: MermaidConfig;
let mockLayoutData: LayoutData;
beforeEach(() => {
mockConfig = {
theme: 'default',
} as MermaidConfig;
mockLayoutData = {
nodes: [
{
id: 'root',
label: 'Root',
isGroup: false,
shape: 'rect',
width: 100,
height: 50,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'child1',
label: 'Child 1',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'child2',
label: 'Child 2',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'child3',
label: 'Child 3',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'child4',
label: 'Child 4',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
],
edges: [
{
id: 'root_child1',
start: 'root',
end: 'child1',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
{
id: 'root_child2',
start: 'root',
end: 'child2',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
{
id: 'root_child3',
start: 'root',
end: 'child3',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
{
id: 'root_child4',
start: 'root',
end: 'child4',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
],
config: mockConfig,
direction: 'TB',
type: 'test',
diagramId: 'test-diagram',
markers: [],
};
});
describe('validateLayoutData', () => {
it('should validate correct layout data', () => {
expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
});
it('should throw error for missing data', () => {
expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
});
it('should throw error for missing config', () => {
const invalidData = { ...mockLayoutData, config: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
});
it('should throw error for invalid nodes array', () => {
const invalidData = { ...mockLayoutData, nodes: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required');
});
it('should throw error for invalid edges array', () => {
const invalidData = { ...mockLayoutData, edges: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
});
});
describe('executeTidyTreeLayout function', () => {
it('should execute layout algorithm successfully', async () => {
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData, mockConfig);
expect(result).toBeDefined();
expect(result.nodes).toBeDefined();
expect(result.edges).toBeDefined();
expect(Array.isArray(result.nodes)).toBe(true);
expect(Array.isArray(result.edges)).toBe(true);
});
it('should return positioned nodes with coordinates', async () => {
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData, mockConfig);
expect(result.nodes.length).toBeGreaterThan(0);
result.nodes.forEach((node) => {
expect(node.x).toBeDefined();
expect(node.y).toBeDefined();
expect(typeof node.x).toBe('number');
expect(typeof node.y).toBe('number');
});
});
it('should return positioned edges with coordinates', async () => {
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData, mockConfig);
expect(result.edges.length).toBeGreaterThan(0);
result.edges.forEach((edge) => {
expect(edge.startX).toBeDefined();
expect(edge.startY).toBeDefined();
expect(edge.midX).toBeDefined();
expect(edge.midY).toBeDefined();
expect(edge.endX).toBeDefined();
expect(edge.endY).toBeDefined();
});
});
it('should handle empty layout data gracefully', async () => {
const emptyData: LayoutData = {
...mockLayoutData,
nodes: [],
edges: [],
};
await expect(executeTidyTreeLayout(emptyData, mockConfig)).rejects.toThrow(
'No nodes found in layout data'
);
});
it('should throw error for missing nodes', async () => {
const invalidData = { ...mockLayoutData, nodes: [] };
await expect(executeTidyTreeLayout(invalidData, mockConfig)).rejects.toThrow(
'No nodes found in layout data'
);
});
it('should handle empty edges (single node tree)', async () => {
const singleNodeData = {
...mockLayoutData,
edges: [],
nodes: [mockLayoutData.nodes[0]],
};
const result = await executeTidyTreeLayout(singleNodeData, mockConfig);
expect(result).toBeDefined();
expect(result.nodes).toHaveLength(1);
expect(result.edges).toHaveLength(0);
});
it('should create bidirectional dual-tree layout with alternating left/right children', async () => {
const result = await executeTidyTreeLayout(mockLayoutData, mockConfig);
expect(result).toBeDefined();
expect(result.nodes).toHaveLength(5);
const rootNode = result.nodes.find((node) => node.id === 'root');
expect(rootNode).toBeDefined();
expect(rootNode!.x).toBe(0);
expect(rootNode!.y).toBe(20);
const child1 = result.nodes.find((node) => node.id === 'child1');
const child2 = result.nodes.find((node) => node.id === 'child2');
const child3 = result.nodes.find((node) => node.id === 'child3');
const child4 = result.nodes.find((node) => node.id === 'child4');
expect(child1).toBeDefined();
expect(child2).toBeDefined();
expect(child3).toBeDefined();
expect(child4).toBeDefined();
expect(child1!.x).toBeLessThan(rootNode!.x);
expect(child2!.x).toBeGreaterThan(rootNode!.x);
expect(child3!.x).toBeLessThan(rootNode!.x);
expect(child4!.x).toBeGreaterThan(rootNode!.x);
expect(child1!.x).toBeLessThan(-100);
expect(child3!.x).toBeLessThan(-100);
expect(child2!.x).toBeGreaterThan(100);
expect(child4!.x).toBeGreaterThan(100);
});
it('should correctly transpose coordinates to prevent high nodes from covering nodes above them', async () => {
const testData = {
...mockLayoutData,
nodes: [
{
id: 'root',
label: 'Root',
isGroup: false,
shape: 'rect' as const,
width: 100,
height: 50,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'tall-child',
label: 'Tall Child',
isGroup: false,
shape: 'rect' as const,
width: 80,
height: 120,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'short-child',
label: 'Short Child',
isGroup: false,
shape: 'rect' as const,
width: 80,
height: 30,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
],
edges: [
{
id: 'root_tall',
start: 'root',
end: 'tall-child',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
{
id: 'root_short',
start: 'root',
end: 'short-child',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
],
};
const result = await executeTidyTreeLayout(testData, mockConfig);
expect(result).toBeDefined();
expect(result.nodes).toHaveLength(3);
const rootNode = result.nodes.find((node) => node.id === 'root');
const tallChild = result.nodes.find((node) => node.id === 'tall-child');
const shortChild = result.nodes.find((node) => node.id === 'short-child');
expect(rootNode).toBeDefined();
expect(tallChild).toBeDefined();
expect(shortChild).toBeDefined();
expect(tallChild!.x).not.toBe(shortChild!.x);
expect(tallChild!.width).toBe(80);
expect(tallChild!.height).toBe(120);
expect(shortChild!.width).toBe(80);
expect(shortChild!.height).toBe(30);
const yDifference = Math.abs(tallChild!.y - shortChild!.y);
expect(yDifference).toBeGreaterThanOrEqual(0);
});
});
});

View File

@@ -0,0 +1,638 @@
import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout';
import type { MermaidConfig, LayoutData } from 'mermaid';
import type {
LayoutResult,
TidyTreeNode,
PositionedNode,
PositionedEdge,
Node,
Edge,
} from './types.js';
/**
* Execute the tidy-tree layout algorithm on generic layout data
*
* This function takes layout data and uses the non-layered-tidy-tree-layout
* algorithm to calculate optimal node positions for tree structures.
*
* @param data - The layout data containing nodes, edges, and configuration
* @param config - Mermaid configuration object
* @returns Promise resolving to layout result with positioned nodes and edges
*/
export function executeTidyTreeLayout(
data: LayoutData,
_config: MermaidConfig
): Promise<LayoutResult> {
let intersectionShift = 50;
return new Promise((resolve, reject) => {
try {
if (!data.nodes || !Array.isArray(data.nodes) || data.nodes.length === 0) {
throw new Error('No nodes found in layout data');
}
if (!data.edges || !Array.isArray(data.edges)) {
data.edges = [];
}
const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
const gap = 20;
const bottomPadding = 40;
intersectionShift = 30;
const bb = new BoundingBox(gap, bottomPadding);
const layout = new Layout(bb);
let leftResult = null;
let rightResult = null;
let leftBoundingBox = null;
let rightBoundingBox = null;
if (leftTree) {
const leftLayoutResult = layout.layout(leftTree);
leftResult = leftLayoutResult.result;
leftBoundingBox = leftLayoutResult.boundingBox;
}
if (rightTree) {
const rightLayoutResult = layout.layout(rightTree);
rightResult = rightLayoutResult.result;
rightBoundingBox = rightLayoutResult.boundingBox;
}
const positionedNodes = combineAndPositionTrees(
rootNode,
leftResult,
rightResult,
leftBoundingBox,
rightBoundingBox,
data
);
const positionedEdges = calculateEdgePositions(
data.edges,
positionedNodes,
intersectionShift
);
resolve({
nodes: positionedNodes,
edges: positionedEdges,
});
} catch (error) {
reject(error);
}
});
}
/**
* Convert LayoutData to dual-tree format (left and right trees)
*
* This function builds two separate tree structures from the nodes and edges,
* alternating children between left and right trees.
*/
function convertToDualTreeFormat(data: LayoutData): {
leftTree: TidyTreeNode | null;
rightTree: TidyTreeNode | null;
rootNode: TidyTreeNode;
} {
const { nodes, edges } = data;
const nodeMap = new Map<string, Node>();
nodes.forEach((node) => nodeMap.set(node.id, node));
const children = new Map<string, string[]>();
const parents = new Map<string, string>();
edges.forEach((edge) => {
const parentId = edge.start;
const childId = edge.end;
if (parentId && childId) {
if (!children.has(parentId)) {
children.set(parentId, []);
}
children.get(parentId)!.push(childId);
parents.set(childId, parentId);
}
});
const rootNodeData = nodes.find((node) => !parents.has(node.id));
if (!rootNodeData && nodes.length === 0) {
throw new Error('No nodes available to create tree');
}
const actualRoot = rootNodeData ?? nodes[0];
const rootNode: TidyTreeNode = {
id: actualRoot.id,
width: actualRoot.width ?? 100,
height: actualRoot.height ?? 50,
_originalNode: actualRoot,
};
const rootChildren = children.get(actualRoot.id) ?? [];
const leftChildren: string[] = [];
const rightChildren: string[] = [];
rootChildren.forEach((childId, index) => {
if (index % 2 === 0) {
leftChildren.push(childId);
} else {
rightChildren.push(childId);
}
});
const leftTree = leftChildren.length > 0 ? buildSubTree(leftChildren, children, nodeMap) : null;
const rightTree =
rightChildren.length > 0 ? buildSubTree(rightChildren, children, nodeMap) : null;
return { leftTree, rightTree, rootNode };
}
/**
* Build a subtree from a list of root children
* For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
*/
function buildSubTree(
rootChildren: string[],
children: Map<string, string[]>,
nodeMap: Map<string, Node>
): TidyTreeNode {
const virtualRoot: TidyTreeNode = {
id: `virtual-root-${Math.random()}`,
width: 1,
height: 1,
children: rootChildren
.map((childId) => nodeMap.get(childId))
.filter((child): child is Node => child !== undefined)
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
};
return virtualRoot;
}
/**
* Recursively convert a node and its children to tidy-tree format
* This version transposes width/height for horizontal tree layout
*/
function convertNodeToTidyTreeTransposed(
node: Node,
children: Map<string, string[]>,
nodeMap: Map<string, Node>
): TidyTreeNode {
const childIds = children.get(node.id) ?? [];
const childNodes = childIds
.map((childId) => nodeMap.get(childId))
.filter((child): child is Node => child !== undefined)
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
return {
id: node.id,
width: node.height ?? 50,
height: node.width ?? 100,
children: childNodes.length > 0 ? childNodes : undefined,
_originalNode: node,
};
}
/**
* Combine and position the left and right trees around the root node
* Creates a bidirectional layout where left tree grows left and right tree grows right
*/
function combineAndPositionTrees(
rootNode: TidyTreeNode,
leftResult: TidyTreeNode | null,
rightResult: TidyTreeNode | null,
_leftBoundingBox: any,
_rightBoundingBox: any,
_data: LayoutData
): PositionedNode[] {
const positionedNodes: PositionedNode[] = [];
const rootX = 0;
const rootY = 0;
const treeSpacing = rootNode.width / 2 + 30;
const leftTreeNodes: PositionedNode[] = [];
const rightTreeNodes: PositionedNode[] = [];
if (leftResult?.children) {
positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
}
if (rightResult?.children) {
positionRightTreeBidirectional(
rightResult.children,
rightTreeNodes,
rootX + treeSpacing,
rootY
);
}
let leftTreeCenterY = 0;
let rightTreeCenterY = 0;
if (leftTreeNodes.length > 0) {
const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort(
(a, b) => b - a
);
const firstLevelLeftX = leftTreeXPositions[0];
const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX);
if (firstLevelLeftNodes.length > 0) {
const leftMinY = Math.min(
...firstLevelLeftNodes.map((node) => node.y - (node.height ?? 50) / 2)
);
const leftMaxY = Math.max(
...firstLevelLeftNodes.map((node) => node.y + (node.height ?? 50) / 2)
);
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
}
}
if (rightTreeNodes.length > 0) {
const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort(
(a, b) => a - b
);
const firstLevelRightX = rightTreeXPositions[0];
const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX);
if (firstLevelRightNodes.length > 0) {
const rightMinY = Math.min(
...firstLevelRightNodes.map((node) => node.y - (node.height ?? 50) / 2)
);
const rightMaxY = Math.max(
...firstLevelRightNodes.map((node) => node.y + (node.height ?? 50) / 2)
);
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
}
}
const leftTreeOffset = -leftTreeCenterY;
const rightTreeOffset = -rightTreeCenterY;
positionedNodes.push({
id: String(rootNode.id),
x: rootX,
y: rootY + 20,
section: 'root',
width: rootNode._originalNode?.width ?? rootNode.width,
height: rootNode._originalNode?.height ?? rootNode.height,
originalNode: rootNode._originalNode,
});
const leftTreeNodesWithOffset = leftTreeNodes.map((node) => ({
id: node.id,
x: node.x - (node.width ?? 0) / 2,
y: node.y + leftTreeOffset + (node.height ?? 0) / 2,
section: 'left' as const,
width: node.width,
height: node.height,
originalNode: node.originalNode,
}));
const rightTreeNodesWithOffset = rightTreeNodes.map((node) => ({
id: node.id,
x: node.x + (node.width ?? 0) / 2,
y: node.y + rightTreeOffset + (node.height ?? 0) / 2,
section: 'right' as const,
width: node.width,
height: node.height,
originalNode: node.originalNode,
}));
positionedNodes.push(...leftTreeNodesWithOffset);
positionedNodes.push(...rightTreeNodesWithOffset);
return positionedNodes;
}
/**
* Position nodes from the left tree in a bidirectional layout (grows to the left)
* Rotates the tree 90 degrees counterclockwise so it grows horizontally to the left
*/
function positionLeftTreeBidirectional(
nodes: TidyTreeNode[],
positionedNodes: PositionedNode[],
offsetX: number,
offsetY: number
): void {
nodes.forEach((node) => {
const distanceFromRoot = node.y ?? 0;
const verticalPosition = node.x ?? 0;
const originalWidth = node._originalNode?.width ?? 100;
const originalHeight = node._originalNode?.height ?? 50;
const adjustedY = offsetY + verticalPosition;
positionedNodes.push({
id: String(node.id),
x: offsetX - distanceFromRoot,
y: adjustedY,
width: originalWidth,
height: originalHeight,
originalNode: node._originalNode,
});
if (node.children) {
positionLeftTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
}
});
}
/**
* Position nodes from the right tree in a bidirectional layout (grows to the right)
* Rotates the tree 90 degrees clockwise so it grows horizontally to the right
*/
function positionRightTreeBidirectional(
nodes: TidyTreeNode[],
positionedNodes: PositionedNode[],
offsetX: number,
offsetY: number
): void {
nodes.forEach((node) => {
const distanceFromRoot = node.y ?? 0;
const verticalPosition = node.x ?? 0;
const originalWidth = node._originalNode?.width ?? 100;
const originalHeight = node._originalNode?.height ?? 50;
const adjustedY = offsetY + verticalPosition;
positionedNodes.push({
id: String(node.id),
x: offsetX + distanceFromRoot,
y: adjustedY,
width: originalWidth,
height: originalHeight,
originalNode: node._originalNode,
});
if (node.children) {
positionRightTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
}
});
}
/**
* Calculate the intersection point of a line with a circle
* @param circle - Circle coordinates and radius
* @param lineStart - Starting point of the line
* @param lineEnd - Ending point of the line
* @returns The intersection point
*/
function computeCircleEdgeIntersection(
circle: { x: number; y: number; width: number; height: number },
lineStart: { x: number; y: number },
lineEnd: { x: number; y: number }
): { x: number; y: number } {
const radius = Math.min(circle.width, circle.height) / 2;
const dx = lineEnd.x - lineStart.x;
const dy = lineEnd.y - lineStart.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) {
return lineStart;
}
const nx = dx / length;
const ny = dy / length;
return {
x: circle.x - nx * radius,
y: circle.y - ny * radius,
};
}
/**
* Calculate intersection point of a line with a rectangle
* This is a simplified version that we'll use instead of importing from mermaid
*/
function intersection(
node: { x: number; y: number; width?: number; height?: number },
point1: { x: number; y: number },
point2: { x: number; y: number }
): { x: number; y: number } {
const nodeWidth = node.width ?? 100;
const nodeHeight = node.height ?? 50;
const centerX = node.x;
const centerY = node.y;
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
if (dx === 0 && dy === 0) {
return { x: centerX, y: centerY };
}
const halfWidth = nodeWidth / 2;
const halfHeight = nodeHeight / 2;
let intersectionX = centerX;
let intersectionY = centerY;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 0) {
intersectionX = centerX + halfWidth;
intersectionY = centerY + (halfWidth * dy) / dx;
} else {
intersectionX = centerX - halfWidth;
intersectionY = centerY - (halfWidth * dy) / dx;
}
} else {
if (dy > 0) {
intersectionY = centerY + halfHeight;
intersectionX = centerX + (halfHeight * dx) / dy;
} else {
intersectionY = centerY - halfHeight;
intersectionX = centerX - (halfHeight * dx) / dy;
}
}
return { x: intersectionX, y: intersectionY };
}
/**
* Calculate edge positions based on positioned nodes
* Now includes tree membership and node dimensions for precise edge calculations
* Edges now stop at shape boundaries instead of extending to centers
*/
function calculateEdgePositions(
edges: Edge[],
positionedNodes: PositionedNode[],
intersectionShift: number
): PositionedEdge[] {
const nodeInfo = new Map<string, PositionedNode>();
positionedNodes.forEach((node) => {
nodeInfo.set(node.id, node);
});
return edges.map((edge) => {
const sourceNode = nodeInfo.get(edge.start ?? '');
const targetNode = nodeInfo.get(edge.end ?? '');
if (!sourceNode || !targetNode) {
return {
id: edge.id,
source: edge.start ?? '',
target: edge.end ?? '',
startX: 0,
startY: 0,
midX: 0,
midY: 0,
endX: 0,
endY: 0,
points: [{ x: 0, y: 0 }],
sourceSection: undefined,
targetSection: undefined,
sourceWidth: undefined,
sourceHeight: undefined,
targetWidth: undefined,
targetHeight: undefined,
};
}
const sourceCenter = { x: sourceNode.x, y: sourceNode.y };
const targetCenter = { x: targetNode.x, y: targetNode.y };
const isSourceRound = ['circle', 'cloud', 'bang'].includes(
sourceNode.originalNode?.shape ?? ''
);
const isTargetRound = ['circle', 'cloud', 'bang'].includes(
targetNode.originalNode?.shape ?? ''
);
let startPos = isSourceRound
? computeCircleEdgeIntersection(
{
x: sourceNode.x,
y: sourceNode.y,
width: sourceNode.width ?? 100,
height: sourceNode.height ?? 100,
},
targetCenter,
sourceCenter
)
: intersection(sourceNode, sourceCenter, targetCenter);
let endPos = isTargetRound
? computeCircleEdgeIntersection(
{
x: targetNode.x,
y: targetNode.y,
width: targetNode.width ?? 100,
height: targetNode.height ?? 100,
},
sourceCenter,
targetCenter
)
: intersection(targetNode, targetCenter, sourceCenter);
const midX = (startPos.x + endPos.x) / 2;
const midY = (startPos.y + endPos.y) / 2;
const points = [startPos];
if (sourceNode.section === 'left') {
points.push({
x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift,
y: sourceNode.y,
});
} else if (sourceNode.section === 'right') {
points.push({
x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift,
y: sourceNode.y,
});
}
if (targetNode.section === 'left') {
points.push({
x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift,
y: targetNode.y,
});
} else if (targetNode.section === 'right') {
points.push({
x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift,
y: targetNode.y,
});
}
points.push(endPos);
const secondPoint = points.length > 1 ? points[1] : targetCenter;
startPos = isSourceRound
? computeCircleEdgeIntersection(
{
x: sourceNode.x,
y: sourceNode.y,
width: sourceNode.width ?? 100,
height: sourceNode.height ?? 100,
},
secondPoint,
sourceCenter
)
: intersection(sourceNode, secondPoint, sourceCenter);
points[0] = startPos;
const secondLastPoint = points.length > 1 ? points[points.length - 2] : sourceCenter;
endPos = isTargetRound
? computeCircleEdgeIntersection(
{
x: targetNode.x,
y: targetNode.y,
width: targetNode.width ?? 100,
height: targetNode.height ?? 100,
},
secondLastPoint,
targetCenter
)
: intersection(targetNode, secondLastPoint, targetCenter);
points[points.length - 1] = endPos;
return {
id: edge.id,
source: edge.start ?? '',
target: edge.end ?? '',
startX: startPos.x,
startY: startPos.y,
midX,
midY,
endX: endPos.x,
endY: endPos.y,
points,
sourceSection: sourceNode?.section,
targetSection: targetNode?.section,
sourceWidth: sourceNode?.width,
sourceHeight: sourceNode?.height,
targetWidth: targetNode?.width,
targetHeight: targetNode?.height,
};
});
}
/**
* Validate layout data structure
* @param data - The data to validate
* @returns True if data is valid, throws error otherwise
*/
export function validateLayoutData(data: LayoutData): boolean {
if (!data) {
throw new Error('Layout data is required');
}
if (!data.config) {
throw new Error('Configuration is required in layout data');
}
if (!Array.isArray(data.nodes)) {
throw new Error('Nodes array is required in layout data');
}
if (!Array.isArray(data.edges)) {
throw new Error('Edges array is required in layout data');
}
return true;
}

View File

@@ -0,0 +1,13 @@
import type { LayoutLoaderDefinition } from 'mermaid';
const loader = async () => await import(`./render.js`);
const tidyTreeLayout: LayoutLoaderDefinition[] = [
{
name: 'tidy-tree',
loader,
algorithm: 'tidy-tree',
},
];
export default tidyTreeLayout;

View File

@@ -0,0 +1,18 @@
declare module 'non-layered-tidy-tree-layout' {
export class BoundingBox {
constructor(gap: number, bottomPadding: number);
}
export class Layout {
constructor(boundingBox: BoundingBox);
layout(data: any): {
result: any;
boundingBox: {
left: number;
right: number;
top: number;
bottom: number;
};
};
}
}

View File

@@ -0,0 +1,180 @@
import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
import { executeTidyTreeLayout } from './layout.js';
interface NodeWithPosition {
id: string;
x?: number;
y?: number;
width?: number;
height?: number;
domId?: any;
[key: string]: any;
}
/**
* Render function for bidirectional tidy-tree layout algorithm
*
* This follows the same pattern as ELK and dagre renderers:
* 1. Insert nodes into DOM to get their actual dimensions
* 2. Run the bidirectional tidy-tree layout algorithm to calculate positions
* 3. Position the nodes and edges based on layout results
*
* The bidirectional layout creates two trees that grow horizontally in opposite
* directions from a central root node:
* - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...)
* - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...)
*/
export const render = async (
data4Layout: LayoutData,
svg: SVG,
{
insertCluster,
insertEdge,
insertEdgeLabel,
insertMarkers,
insertNode,
log,
positionEdgeLabel,
}: InternalHelpers,
{ algorithm: _algorithm }: RenderOptions
) => {
const nodeDb: Record<string, NodeWithPosition> = {};
const clusterDb: Record<string, any> = {};
const element = svg.select('g');
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
const edgePaths = element.insert('g').attr('class', 'edgePaths');
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
const nodes = element.insert('g').attr('class', 'nodes');
// Step 1: Insert nodes into DOM to get their actual dimensions
log.debug('Inserting nodes into DOM for dimension calculation');
await Promise.all(
data4Layout.nodes.map(async (node) => {
if (node.isGroup) {
const clusterNode: NodeWithPosition = {
...node,
id: node.id,
width: node.width,
height: node.height,
};
clusterDb[node.id] = clusterNode;
nodeDb[node.id] = clusterNode;
await insertCluster(subGraphsEl, node);
} else {
const nodeWithPosition: NodeWithPosition = {
...node,
id: node.id,
width: node.width,
height: node.height,
};
nodeDb[node.id] = nodeWithPosition;
const nodeEl = await insertNode(nodes, node, {
config: data4Layout.config,
dir: data4Layout.direction || 'TB',
});
const boundingBox = nodeEl.node()!.getBBox();
nodeWithPosition.width = boundingBox.width;
nodeWithPosition.height = boundingBox.height;
nodeWithPosition.domId = nodeEl;
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
}
})
);
// Step 2: Run the bidirectional tidy-tree layout algorithm
log.debug('Running bidirectional tidy-tree layout algorithm');
const updatedLayoutData = {
...data4Layout,
nodes: data4Layout.nodes.map((node) => {
const nodeWithDimensions = nodeDb[node.id];
return {
...node,
width: nodeWithDimensions.width ?? node.width ?? 100,
height: nodeWithDimensions.height ?? node.height ?? 50,
};
}),
};
const layoutResult = await executeTidyTreeLayout(updatedLayoutData, data4Layout.config);
// Step 3: Position the nodes based on bidirectional layout results
log.debug('Positioning nodes based on bidirectional layout results');
layoutResult.nodes.forEach((positionedNode) => {
const node = nodeDb[positionedNode.id];
if (node?.domId) {
// Position the node at the calculated coordinates from bidirectional layout
// The layout algorithm has already calculated positions for:
// - Root node at center (0, 0)
// - Left tree nodes with negative x coordinates (growing left)
// - Right tree nodes with positive x coordinates (growing right)
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
// Store the final position
node.x = positionedNode.x;
node.y = positionedNode.y;
// Step 3: Position the nodes based on bidirectional layout results
log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`);
}
});
log.debug('Inserting and positioning edges');
await Promise.all(
data4Layout.edges.map(async (edge) => {
await insertEdgeLabel(edgeLabels, edge);
const startNode = nodeDb[edge.start ?? ''];
const endNode = nodeDb[edge.end ?? ''];
if (startNode && endNode) {
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
if (positionedEdge) {
log.debug('APA01 positionedEdge', positionedEdge);
const edgeWithPath = {
...edge,
points: positionedEdge.points,
};
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
positionEdgeLabel(edgeWithPath, paths);
} else {
const edgeWithPath = {
...edge,
points: [
{ x: startNode.x ?? 0, y: startNode.y ?? 0 },
{ x: endNode.x ?? 0, y: endNode.y ?? 0 },
],
};
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
positionEdgeLabel(edgeWithPath, paths);
}
}
})
);
log.debug('Bidirectional tidy-tree rendering completed');
};

View File

@@ -0,0 +1,69 @@
import type { LayoutData } from 'mermaid';
export type Node = LayoutData['nodes'][number];
export type Edge = LayoutData['edges'][number];
/**
* Positioned node after layout calculation
*/
export interface PositionedNode {
id: string;
x: number;
y: number;
section?: 'root' | 'left' | 'right';
width?: number;
height?: number;
originalNode?: Node;
[key: string]: unknown;
}
/**
* Positioned edge after layout calculation
*/
export interface PositionedEdge {
id: string;
source: string;
target: string;
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
sourceSection?: 'root' | 'left' | 'right';
targetSection?: 'root' | 'left' | 'right';
sourceWidth?: number;
sourceHeight?: number;
targetWidth?: number;
targetHeight?: number;
[key: string]: unknown;
}
/**
* Result of layout algorithm execution
*/
export interface LayoutResult {
nodes: PositionedNode[];
edges: PositionedEdge[];
}
/**
* Tidy-tree node structure compatible with non-layered-tidy-tree-layout
*/
export interface TidyTreeNode {
id: string | number;
width: number;
height: number;
x?: number;
y?: number;
children?: TidyTreeNode[];
_originalNode?: Node;
}
/**
* Tidy-tree layout configuration
*/
export interface TidyTreeLayoutConfig {
gap: number;
bottomPadding: number;
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"types": ["vitest/importMeta", "vitest/globals"]
},
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
"typeRoots": ["./src/types"]
}

View File

@@ -1,14 +1,5 @@
# @mermaid-js/mermaid-zenuml
## 0.2.2
### Patch Changes
- [#6798](https://github.com/mermaid-js/mermaid/pull/6798) [`3ffe961`](https://github.com/mermaid-js/mermaid/commit/3ffe9618aebc9ac96de6e3c826481f542f18c2a9) Thanks [@MrCoder](https://github.com/MrCoder)! - Fixed a critical bug that the ZenUML diagram is not rendered.
- Updated dependencies [[`b9ef683`](https://github.com/mermaid-js/mermaid/commit/b9ef683fb67b8959abc455d6cc5266c37ba435f6), [`2c0931d`](https://github.com/mermaid-js/mermaid/commit/2c0931da46794b49d2523211e25f782900c34e94), [`33e08da`](https://github.com/mermaid-js/mermaid/commit/33e08daf175125295a06b1b80279437004a4e865), [`814b68b`](https://github.com/mermaid-js/mermaid/commit/814b68b4a94813f7c6b3d7fb4559532a7bab2652), [`fce7cab`](https://github.com/mermaid-js/mermaid/commit/fce7cabb71d68a20a66246fe23d066512126a412), [`fc07f0d`](https://github.com/mermaid-js/mermaid/commit/fc07f0d8abca49e4f887d7457b7b94fb07d1e3da), [`12e01bd`](https://github.com/mermaid-js/mermaid/commit/12e01bdb5cacf3569133979a5a4f1d8973e9aec1), [`01aaef3`](https://github.com/mermaid-js/mermaid/commit/01aaef39b4a1ec8bc5a0c6bfa3a20b712d67f4dc), [`daf8d8d`](https://github.com/mermaid-js/mermaid/commit/daf8d8d3befcd600618a629977b76463b38d0ad9), [`c36cd05`](https://github.com/mermaid-js/mermaid/commit/c36cd05c45ac3090181152b4dae41f8d7b569bd6), [`8bb29fc`](https://github.com/mermaid-js/mermaid/commit/8bb29fc879329ad109898e4025b4f4eba2ab0649), [`71b04f9`](https://github.com/mermaid-js/mermaid/commit/71b04f93b07f876df2b30656ef36036c1d0e4e4f), [`c99bce6`](https://github.com/mermaid-js/mermaid/commit/c99bce6bab4c7ce0b81b66d44f44853ce4aeb1c3), [`6cc1926`](https://github.com/mermaid-js/mermaid/commit/6cc192680a2531cab28f87a8061a53b786e010f3), [`9da6fb3`](https://github.com/mermaid-js/mermaid/commit/9da6fb39ae278401771943ac85d6d1b875f78cf1), [`e48b0ba`](https://github.com/mermaid-js/mermaid/commit/e48b0ba61dab7f95aa02da603b5b7d383b894932), [`4d62d59`](https://github.com/mermaid-js/mermaid/commit/4d62d5963238400270e9314c6e4d506f48147074), [`e9ce8cf`](https://github.com/mermaid-js/mermaid/commit/e9ce8cf4da9062d85098042044822100889bb0dd), [`9258b29`](https://github.com/mermaid-js/mermaid/commit/9258b2933bbe1ef41087345ffea3731673671c49), [`da90f67`](https://github.com/mermaid-js/mermaid/commit/da90f6760b6efb0da998bcb63b75eecc29e06c08), [`0133f1c`](https://github.com/mermaid-js/mermaid/commit/0133f1c0c5cff4fc4c8e0b99e9cf0b3d49dcbe71), [`895f9d4`](https://github.com/mermaid-js/mermaid/commit/895f9d43ff98ca05ebfba530789f677f31a011ff)]:
- mermaid@11.10.0
## 0.2.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/mermaid-zenuml",
"version": "0.2.2",
"version": "0.2.1",
"description": "MermaidJS plugin for ZenUML integration",
"module": "dist/mermaid-zenuml.core.mjs",
"types": "dist/detector.d.ts",

View File

@@ -1,72 +1,5 @@
# mermaid
## 11.10.0
### Minor Changes
- [#6744](https://github.com/mermaid-js/mermaid/pull/6744) [`daf8d8d`](https://github.com/mermaid-js/mermaid/commit/daf8d8d3befcd600618a629977b76463b38d0ad9) Thanks [@SpecularAura](https://github.com/SpecularAura)! - feat: Added support for per link curve styling in flowchart diagram using edge ids
### Patch Changes
- [#6857](https://github.com/mermaid-js/mermaid/pull/6857) [`b9ef683`](https://github.com/mermaid-js/mermaid/commit/b9ef683fb67b8959abc455d6cc5266c37ba435f6) Thanks [@knsv](https://github.com/knsv)! - feat: Exposing elk configuration forceNodeModelOrder and considerModelOrder to the mermaid configuration
- [#6653](https://github.com/mermaid-js/mermaid/pull/6653) [`2c0931d`](https://github.com/mermaid-js/mermaid/commit/2c0931da46794b49d2523211e25f782900c34e94) Thanks [@darshanr0107](https://github.com/darshanr0107)! - chore: Remove the "-beta" suffix from the XYChart, Block, Sankey diagrams to reflect their stable status
- [#6683](https://github.com/mermaid-js/mermaid/pull/6683) [`33e08da`](https://github.com/mermaid-js/mermaid/commit/33e08daf175125295a06b1b80279437004a4e865) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Position the edge label in state diagram correctly relative to the edge
- [#6693](https://github.com/mermaid-js/mermaid/pull/6693) [`814b68b`](https://github.com/mermaid-js/mermaid/commit/814b68b4a94813f7c6b3d7fb4559532a7bab2652) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Apply correct dateFormat in Gantt chart to show only day when specified
- [#6734](https://github.com/mermaid-js/mermaid/pull/6734) [`fce7cab`](https://github.com/mermaid-js/mermaid/commit/fce7cabb71d68a20a66246fe23d066512126a412) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: handle exclude dates properly in Gantt charts when using dateFormat: 'YYYY-MM-DD HH:mm:ss'
- [#6733](https://github.com/mermaid-js/mermaid/pull/6733) [`fc07f0d`](https://github.com/mermaid-js/mermaid/commit/fc07f0d8abca49e4f887d7457b7b94fb07d1e3da) Thanks [@omkarht](https://github.com/omkarht)! - fix: fixed connection gaps in flowchart for roundedRect, stadium and diamond shape
- [#6876](https://github.com/mermaid-js/mermaid/pull/6876) [`12e01bd`](https://github.com/mermaid-js/mermaid/commit/12e01bdb5cacf3569133979a5a4f1d8973e9aec1) Thanks [@sidharthv96](https://github.com/sidharthv96)! - fix: sanitize icon labels and icon SVGs
Resolves CVE-2025-54880 reported by @fourcube
- [#6801](https://github.com/mermaid-js/mermaid/pull/6801) [`01aaef3`](https://github.com/mermaid-js/mermaid/commit/01aaef39b4a1ec8bc5a0c6bfa3a20b712d67f4dc) Thanks [@sidharthv96](https://github.com/sidharthv96)! - fix: Update casing of ID in requirement diagram
- [#6796](https://github.com/mermaid-js/mermaid/pull/6796) [`c36cd05`](https://github.com/mermaid-js/mermaid/commit/c36cd05c45ac3090181152b4dae41f8d7b569bd6) Thanks [@HashanCP](https://github.com/HashanCP)! - fix: Make flowchart elk detector regex match less greedy
- [#6702](https://github.com/mermaid-js/mermaid/pull/6702) [`8bb29fc`](https://github.com/mermaid-js/mermaid/commit/8bb29fc879329ad109898e4025b4f4eba2ab0649) Thanks [@qraqras](https://github.com/qraqras)! - fix(block): overflowing blocks no longer affect later lines
This may change the layout of block diagrams that have overflowing lines
(i.e. block diagrams that use up more columns that the `columns` specifier).
- [#6717](https://github.com/mermaid-js/mermaid/pull/6717) [`71b04f9`](https://github.com/mermaid-js/mermaid/commit/71b04f93b07f876df2b30656ef36036c1d0e4e4f) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: log warning for blocks exceeding column width
This update adds a validation check that logs a warning message when a block's width exceeds the defined column layout.
- [#6820](https://github.com/mermaid-js/mermaid/pull/6820) [`c99bce6`](https://github.com/mermaid-js/mermaid/commit/c99bce6bab4c7ce0b81b66d44f44853ce4aeb1c3) Thanks [@kriss-u](https://github.com/kriss-u)! - fix: Add escaped class literal name on namespace
- [#6332](https://github.com/mermaid-js/mermaid/pull/6332) [`6cc1926`](https://github.com/mermaid-js/mermaid/commit/6cc192680a2531cab28f87a8061a53b786e010f3) Thanks [@ajuckel](https://github.com/ajuckel)! - fix: Allow equals sign in sequenceDiagram labels
- [#6651](https://github.com/mermaid-js/mermaid/pull/6651) [`9da6fb3`](https://github.com/mermaid-js/mermaid/commit/9da6fb39ae278401771943ac85d6d1b875f78cf1) Thanks [@darshanr0107](https://github.com/darshanr0107)! - Add validation for negative values in pie charts:
Prevents crashes during parsing by validating values post-parsing.
Provides clearer, user-friendly error messages for invalid negative inputs.
- [#6803](https://github.com/mermaid-js/mermaid/pull/6803) [`e48b0ba`](https://github.com/mermaid-js/mermaid/commit/e48b0ba61dab7f95aa02da603b5b7d383b894932) Thanks [@omkarht](https://github.com/omkarht)! - chore: migrate to class-based ArchitectureDB implementation
- [#6838](https://github.com/mermaid-js/mermaid/pull/6838) [`4d62d59`](https://github.com/mermaid-js/mermaid/commit/4d62d5963238400270e9314c6e4d506f48147074) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: node border style for handdrawn shapes
- [#6739](https://github.com/mermaid-js/mermaid/pull/6739) [`e9ce8cf`](https://github.com/mermaid-js/mermaid/commit/e9ce8cf4da9062d85098042044822100889bb0dd) Thanks [@kriss-u](https://github.com/kriss-u)! - fix: Update flowchart direction TD's behavior to be the same as TB
- [#6833](https://github.com/mermaid-js/mermaid/pull/6833) [`9258b29`](https://github.com/mermaid-js/mermaid/commit/9258b2933bbe1ef41087345ffea3731673671c49) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: correctly render non-directional lines for '---' in block diagrams
- [#6855](https://github.com/mermaid-js/mermaid/pull/6855) [`da90f67`](https://github.com/mermaid-js/mermaid/commit/da90f6760b6efb0da998bcb63b75eecc29e06c08) Thanks [@sidharthv96](https://github.com/sidharthv96)! - fix: fallback to raw text instead of rendering _Unsupported markdown_ or empty blocks
Instead of printing **Unsupported markdown: XXX**, or empty blocks when using a markdown feature
that Mermaid does not yet support when `htmlLabels: true`(default) or `htmlLabels: false`,
fallback to the raw markdown text.
- [#6876](https://github.com/mermaid-js/mermaid/pull/6876) [`0133f1c`](https://github.com/mermaid-js/mermaid/commit/0133f1c0c5cff4fc4c8e0b99e9cf0b3d49dcbe71) Thanks [@sidharthv96](https://github.com/sidharthv96)! - fix: sanitize KATEX blocks
Resolves CVE-2025-54881 reported by @fourcube
- [#6804](https://github.com/mermaid-js/mermaid/pull/6804) [`895f9d4`](https://github.com/mermaid-js/mermaid/commit/895f9d43ff98ca05ebfba530789f677f31a011ff) Thanks [@omkarht](https://github.com/omkarht)! - chore: Update packet diagram to use new class-based database structure
## 11.9.0
### Minor Changes
@@ -154,7 +87,6 @@
### Minor Changes
- [#6408](https://github.com/mermaid-js/mermaid/pull/6408) [`ad65313`](https://github.com/mermaid-js/mermaid/commit/ad653138e16765d095613a6e5de86dc5e52ac8f0) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix: restore curve type configuration functionality for flowcharts. This fixes the issue where curve type settings were not being applied when configured through any of the following methods:
- Config
- Init directive (%%{ init: { 'flowchart': { 'curve': '...' } } }%%)
- LinkStyle command (linkStyle default interpolate ...)
@@ -173,14 +105,12 @@
### Minor Changes
- [#6187](https://github.com/mermaid-js/mermaid/pull/6187) [`7809b5a`](https://github.com/mermaid-js/mermaid/commit/7809b5a93fae127f45727071f5ff14325222c518) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Flowchart new syntax for node metadata bugs
- Incorrect label mapping for nodes when using `&`
- Syntax error when `}` with trailing spaces before new line
- [#6136](https://github.com/mermaid-js/mermaid/pull/6136) [`ec0d9c3`](https://github.com/mermaid-js/mermaid/commit/ec0d9c389aa6018043187654044c1e0b5aa4f600) Thanks [@knsv](https://github.com/knsv)! - Adding support for animation of flowchart edges
- [#6373](https://github.com/mermaid-js/mermaid/pull/6373) [`05bdf0e`](https://github.com/mermaid-js/mermaid/commit/05bdf0e20e2629fe77513218fbd4e28e65f75882) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Upgrade Requirement and ER diagram to use the common renderer flow
- Added support for directions
- Added support for hand drawn look
@@ -229,7 +159,6 @@
- [#5999](https://github.com/mermaid-js/mermaid/pull/5999) [`742ad7c`](https://github.com/mermaid-js/mermaid/commit/742ad7c130964df1fb5544e909d9556081285f68) Thanks [@knsv](https://github.com/knsv)! - Adding Kanban board, a new diagram type
- [#5880](https://github.com/mermaid-js/mermaid/pull/5880) [`bdf145f`](https://github.com/mermaid-js/mermaid/commit/bdf145ffe362462176d9c1e68d5f3ff5c9d962b0) Thanks [@yari-dewalt](https://github.com/yari-dewalt)! - Class diagram changes:
- Updates the class diagram to the new unified way of rendering.
- Includes a new "classBox" shape to be used in diagrams
- Other updates such as:

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "11.10.0",
"version": "11.9.0",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -68,7 +68,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "^7.0.4",
"@iconify/utils": "^3.0.1",
"@iconify/utils": "^2.1.33",
"@mermaid-js/parser": "workspace:^",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.3",
@@ -82,7 +82,7 @@
"katex": "^0.16.22",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^16.0.0",
"marked": "^15.0.7",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
@@ -123,8 +123,8 @@
"rimraf": "^6.0.1",
"start-server-and-test": "^2.0.10",
"type-fest": "^4.35.0",
"typedoc": "^0.27.8",
"typedoc-plugin-markdown": "^4.4.2",
"typedoc": "^0.28.9",
"typedoc-plugin-markdown": "^4.8.0",
"typescript": "~5.7.3",
"unist-util-flatmap": "^1.0.0",
"unist-util-visit": "^5.0.0",

View File

@@ -171,7 +171,9 @@ This Markdown should be kept.
expect(buildShapeDoc()).toMatchInlineSnapshot(`
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
| Bang | Bang | \`bang\` | Bang | \`bang\` |
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
| Cloud | Cloud | \`cloud\` | cloud | \`cloud\` |
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |

View File

@@ -78,187 +78,3 @@ describe('when working with site config', () => {
expect(config_4.altFontFamily).toBeUndefined();
});
});
describe('getUserDefinedConfig', () => {
beforeEach(() => {
configApi.reset();
});
it('should return empty object when no user config is defined', () => {
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toEqual({});
});
it('should return config from initialize only', () => {
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial' };
configApi.saveConfigFromInitialize(initConfig);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toEqual(initConfig);
});
it('should return config from directives only', () => {
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
const directive2: MermaidConfig = { theme: 'forest' };
configApi.addDirective(directive1);
configApi.addDirective(directive2);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"fontFamily": "Arial",
"fontSize": 14,
"layout": "elk",
"theme": "forest",
}
`);
});
it('should combine initialize config and directives', () => {
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial', layout: 'dagre' };
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
const directive2: MermaidConfig = { theme: 'forest' };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive1);
configApi.addDirective(directive2);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"fontFamily": "Arial",
"fontSize": 14,
"layout": "elk",
"theme": "forest",
}
`);
});
it('should handle nested config objects properly', () => {
const initConfig: MermaidConfig = {
flowchart: { nodeSpacing: 50, rankSpacing: 100 },
theme: 'default',
};
const directive: MermaidConfig = {
flowchart: { nodeSpacing: 75, curve: 'basis' },
mindmap: { padding: 20 },
};
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"flowchart": {
"curve": "basis",
"nodeSpacing": 75,
"rankSpacing": 100,
},
"mindmap": {
"padding": 20,
},
"theme": "default",
}
`);
});
it('should handle complex nested overrides', () => {
const initConfig: MermaidConfig = {
flowchart: {
nodeSpacing: 50,
rankSpacing: 100,
curve: 'linear',
},
theme: 'default',
};
const directive1: MermaidConfig = {
flowchart: {
nodeSpacing: 75,
},
fontSize: 12,
};
const directive2: MermaidConfig = {
flowchart: {
curve: 'basis',
nodeSpacing: 100,
},
mindmap: {
padding: 15,
},
};
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive1);
configApi.addDirective(directive2);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"flowchart": {
"curve": "basis",
"nodeSpacing": 100,
"rankSpacing": 100,
},
"fontSize": 12,
"mindmap": {
"padding": 15,
},
"theme": "default",
}
`);
});
it('should return independent copies (not references)', () => {
const initConfig: MermaidConfig = { theme: 'dark', flowchart: { nodeSpacing: 50 } };
configApi.saveConfigFromInitialize(initConfig);
const userConfig1 = configApi.getUserDefinedConfig();
const userConfig2 = configApi.getUserDefinedConfig();
userConfig1.theme = 'neutral';
userConfig1.flowchart!.nodeSpacing = 999;
expect(userConfig2).toMatchInlineSnapshot(`
{
"flowchart": {
"nodeSpacing": 50,
},
"theme": "dark",
}
`);
});
it('should handle edge cases with undefined values', () => {
const initConfig: MermaidConfig = { theme: 'dark', layout: undefined };
const directive: MermaidConfig = { fontSize: 14, fontFamily: undefined };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"fontSize": 14,
"layout": undefined,
"theme": "dark",
}
`);
});
it('should retain config from initialize after reset', () => {
const initConfig: MermaidConfig = { theme: 'dark' };
const directive: MermaidConfig = { layout: 'elk' };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"layout": "elk",
"theme": "dark",
}
`);
configApi.reset();
});
});

View File

@@ -248,17 +248,3 @@ const checkConfig = (config: MermaidConfig) => {
issueWarning('LAZY_LOAD_DEPRECATED');
}
};
export const getUserDefinedConfig = (): MermaidConfig => {
let userConfig: MermaidConfig = {};
if (configFromInitialize) {
userConfig = assignWithDepth(userConfig, configFromInitialize);
}
for (const d of directives) {
userConfig = assignWithDepth(userConfig, d);
}
return userConfig;
};

View File

@@ -1075,6 +1075,10 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
export interface MindmapDiagramConfig extends BaseDiagramConfig {
padding?: number;
maxNodeWidth?: number;
/**
* Layout algorithm to use for positioning mindmap nodes
*/
layoutAlgorithm?: string;
}
/**
* The object containing configurations specific for kanban diagrams

View File

@@ -1,9 +1,9 @@
import { select } from 'd3';
import { getConfig } from '../diagram-api/diagramAPI.js';
import { evaluate, sanitizeText } from '../diagrams/common/common.js';
import { log } from '../logger.js';
import { replaceIconSubstring } from '../rendering-util/createText.js';
import { getConfig } from '../diagram-api/diagramAPI.js';
import { evaluate } from '../diagrams/common/common.js';
import { decodeEntities } from '../utils.js';
import { replaceIconSubstring } from '../rendering-util/createText.js';
/**
* @param dom
@@ -19,14 +19,14 @@ function applyStyle(dom, styleFn) {
* @param {any} node
* @returns {SVGForeignObjectElement} Node
*/
function addHtmlLabel(node, config) {
function addHtmlLabel(node) {
const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));
const div = fo.append('xhtml:div');
const label = node.label;
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
const span = div.append('span');
span.html(sanitizeText(label, config));
span.html(label);
applyStyle(span, node.labelStyle);
span.attr('class', labelClass);
@@ -49,8 +49,7 @@ const createLabel = async (_vertexText, style, isTitle, isNode) => {
if (typeof vertexText === 'object') {
vertexText = vertexText[0];
}
const config = getConfig();
if (evaluate(config.flowchart.htmlLabels)) {
if (evaluate(getConfig().flowchart.htmlLabels)) {
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
vertexText = vertexText.replace(/\\n|\n/g, '<br />');
log.debug('vertexText' + vertexText);
@@ -60,7 +59,7 @@ const createLabel = async (_vertexText, style, isTitle, isNode) => {
label,
labelStyle: style.replace('fill:', 'color:'),
};
let vertexNode = addHtmlLabel(node, config);
let vertexNode = addHtmlLabel(node);
// vertexNode.parentNode.removeChild(vertexNode);
return vertexNode;
} else {

View File

@@ -1,5 +1,3 @@
// tests to check that comments are removed
import { cleanupComments } from './comments.js';
import { describe, it, expect } from 'vitest';
@@ -10,12 +8,12 @@ describe('comments', () => {
%% This is a comment
%% This is another comment
graph TD
A-->B
A-->B
%% This is a comment
`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
"graph TD
A-->B
A-->B
"
`);
});
@@ -29,9 +27,9 @@ graph TD
%%{ init: {'theme': 'space before init'}}%%
%%{init: {'theme': 'space after ending'}}%%
graph TD
A-->B
A-->B
B-->C
B-->C
%% This is a comment
`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
@@ -39,9 +37,9 @@ graph TD
%%{ init: {'theme': 'space before init'}}%%
%%{init: {'theme': 'space after ending'}}%%
graph TD
A-->B
A-->B
B-->C
B-->C
"
`);
});
@@ -50,14 +48,14 @@ graph TD
const text = `
%% This is a comment
graph TD
A-->B
%% This is a comment
C-->D
A-->B
%% This is a comment
C-->D
`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
"graph TD
A-->B
C-->D
A-->B
C-->D
"
`);
});
@@ -70,11 +68,11 @@ graph TD
%% This is a comment
graph TD
A-->B
A-->B
`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
"graph TD
A-->B
A-->B
"
`);
});
@@ -82,12 +80,12 @@ graph TD
it('should remove comments at end of text with no newline', () => {
const text = `
graph TD
A-->B
A-->B
%% This is a comment`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
"graph TD
A-->B
A-->B
"
`);
});

View File

@@ -4,5 +4,6 @@
* @returns cleaned text
*/
export const cleanupComments = (text: string): string => {
return text.replace(/^\s*%%(?!{)[^\n]+\n?/gm, '').trimStart();
const cleaned = text.replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
return cleaned.trimStart();
};

View File

@@ -1,6 +1,5 @@
import type { Position } from 'cytoscape';
import type { LayoutOptions, Position } from 'cytoscape';
import cytoscape from 'cytoscape';
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
import fcose from 'cytoscape-fcose';
import { select } from 'd3';
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
@@ -41,7 +40,7 @@ registerIconPacks([
icons: architectureIcons,
},
]);
cytoscape.use(fcose);
cytoscape.use(fcose as any);
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
services.forEach((service) => {
@@ -429,7 +428,7 @@ function layoutArchitecture(
},
alignmentConstraint,
relativePlacementConstraint,
} as FcoseLayoutOptions);
} as LayoutOptions);
// Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend
layout.one('layoutstop', () => {

View File

@@ -3,7 +3,6 @@ import { getConfig } from '../../diagram-api/diagramAPI.js';
import { createText } from '../../rendering-util/createText.js';
import { getIconSVG } from '../../rendering-util/icons.js';
import type { D3Element } from '../../types.js';
import { sanitizeText } from '../common/common.js';
import type { ArchitectureDB } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js';
import {
@@ -272,7 +271,6 @@ export const drawServices = async function (
elem: D3Element,
services: ArchitectureService[]
): Promise<number> {
const config = getConfig();
for (const service of services) {
const serviceElem = elem.append('g');
const iconSize = db.getConfigField('iconSize');
@@ -287,7 +285,7 @@ export const drawServices = async function (
width: iconSize * 1.5,
classes: 'architecture-service-label',
},
config
getConfig()
);
textElem
@@ -322,7 +320,7 @@ export const drawServices = async function (
.attr('class', 'node-icon-text')
.attr('style', `height: ${iconSize}px;`)
.append('div')
.html(sanitizeText(service.iconText, config));
.html(service.iconText);
const fontSize =
parseInt(
window

View File

@@ -311,8 +311,9 @@ export const hasKatex = (text: string): boolean => (text.match(katexRegex)?.leng
* @returns Object containing \{width, height\}
*/
export const calculateMathMLDimensions = async (text: string, config: MermaidConfig) => {
text = await renderKatex(text, config);
const divElem = document.createElement('div');
divElem.innerHTML = await renderKatexSanitized(text, config);
divElem.innerHTML = text;
divElem.id = 'katex-temp';
divElem.style.visibility = 'hidden';
divElem.style.position = 'absolute';
@@ -324,7 +325,14 @@ export const calculateMathMLDimensions = async (text: string, config: MermaidCon
return dim;
};
const renderKatexUnsanitized = async (text: string, config: MermaidConfig): Promise<string> => {
/**
* Attempts to render and return the KaTeX portion of a string with MathML
*
* @param text - The text to test
* @param config - Configuration for Mermaid
* @returns String containing MathML if KaTeX is supported, or an error message if it is not and stylesheets aren't present
*/
export const renderKatex = async (text: string, config: MermaidConfig): Promise<string> => {
if (!hasKatex(text)) {
return text;
}
@@ -365,20 +373,6 @@ const renderKatexUnsanitized = async (text: string, config: MermaidConfig): Prom
);
};
/**
* Attempts to render and return the KaTeX portion of a string with MathML
*
* @param text - The text to test
* @param config - Configuration for Mermaid
* @returns String containing MathML if KaTeX is supported, or an error message if it is not and stylesheets aren't present
*/
export const renderKatexSanitized = async (
text: string,
config: MermaidConfig
): Promise<string> => {
return sanitizeText(await renderKatexUnsanitized(text, config), config);
};
export default {
getRows,
sanitizeText,

View File

@@ -0,0 +1,297 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MindmapDB } from './mindmapDb.js';
import type { MindmapLayoutNode, MindmapLayoutEdge } from './mindmapDb.js';
import type { Edge } from '../../rendering-util/types.js';
// Mock the getConfig function
vi.mock('../../diagram-api/diagramAPI.js', () => ({
getConfig: vi.fn(() => ({
mindmap: {
layoutAlgorithm: 'cose-bilkent',
padding: 10,
maxNodeWidth: 200,
useMaxWidth: true,
},
})),
}));
describe('MindmapDb getData function', () => {
let db: MindmapDB;
beforeEach(() => {
db = new MindmapDB();
// Clear the database before each test
db.clear();
});
describe('getData', () => {
it('should return empty data when no mindmap is set', () => {
const result = db.getData();
expect(result.nodes).toEqual([]);
expect(result.edges).toEqual([]);
expect(result.config).toBeDefined();
expect(result.rootNode).toBeUndefined();
});
it('should return structured data for simple mindmap', () => {
// Create a simple mindmap structure
db.addNode(0, 'root', 'Root Node', 0);
db.addNode(1, 'child1', 'Child 1', 0);
db.addNode(1, 'child2', 'Child 2', 0);
const result = db.getData();
expect(result.nodes).toHaveLength(3);
expect(result.edges).toHaveLength(2);
expect(result.config).toBeDefined();
expect(result.rootNode).toBeDefined();
// Check root node
const rootNode = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '0');
expect(rootNode).toBeDefined();
expect(rootNode?.label).toBe('Root Node');
expect(rootNode?.level).toBe(0);
// Check child nodes
const child1 = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '1');
expect(child1).toBeDefined();
expect(child1?.label).toBe('Child 1');
expect(child1?.level).toBe(1);
// Check edges
expect(result.edges).toContainEqual(
expect.objectContaining({
start: '0',
end: '1',
depth: 0,
})
);
});
it('should return structured data for hierarchical mindmap', () => {
// Create a hierarchical mindmap structure
db.addNode(0, 'root', 'Root Node', 0);
db.addNode(1, 'child1', 'Child 1', 0);
db.addNode(2, 'grandchild1', 'Grandchild 1', 0);
db.addNode(2, 'grandchild2', 'Grandchild 2', 0);
db.addNode(1, 'child2', 'Child 2', 0);
const result = db.getData();
expect(result.nodes).toHaveLength(5);
expect(result.edges).toHaveLength(4);
// Check that all levels are represented
const levels = result.nodes.map((n) => (n as MindmapLayoutNode).level);
expect(levels).toContain(0); // root
expect(levels).toContain(1); // children
expect(levels).toContain(2); // grandchildren
// Check edge relationships
const edgeRelations = result.edges.map(
(e) => `${(e as MindmapLayoutEdge).start}->${(e as MindmapLayoutEdge).end}`
);
expect(edgeRelations).toContain('0->1'); // root to child1
expect(edgeRelations).toContain('1->2'); // child1 to grandchild1
expect(edgeRelations).toContain('1->3'); // child1 to grandchild2
expect(edgeRelations).toContain('0->4'); // root to child2
});
it('should preserve node properties in processed data', () => {
// Add a node with specific properties
db.addNode(0, 'root', 'Root Node', 2); // type 2 = rectangle
// Set additional properties
const mindmap = db.getMindmap();
if (mindmap) {
mindmap.width = 150;
mindmap.height = 75;
mindmap.padding = 15;
mindmap.section = 1;
mindmap.class = 'custom-class';
mindmap.icon = 'star';
}
const result = db.getData();
expect(result.nodes).toHaveLength(1);
const node = result.nodes[0] as MindmapLayoutNode;
expect(node.type).toBe(2);
expect(node.width).toBe(150);
expect(node.height).toBe(75);
expect(node.padding).toBe(15);
expect(node.section).toBeUndefined(); // Root node has undefined section
expect(node.cssClasses).toBe('mindmap-node section-root section--1 custom-class');
expect(node.icon).toBe('star');
});
it('should generate unique edge IDs', () => {
db.addNode(0, 'root', 'Root Node', 0);
db.addNode(1, 'child1', 'Child 1', 0);
db.addNode(1, 'child2', 'Child 2', 0);
db.addNode(1, 'child3', 'Child 3', 0);
const result = db.getData();
const edgeIds = result.edges.map((e: Edge) => e.id);
const uniqueIds = new Set(edgeIds);
expect(edgeIds).toHaveLength(3);
expect(uniqueIds.size).toBe(3); // All IDs should be unique
});
it('should handle nodes with missing optional properties', () => {
db.addNode(0, 'root', 'Root Node', 0);
const result = db.getData();
const node = result.nodes[0] as MindmapLayoutNode;
// Should handle undefined/missing properties gracefully
expect(node.section).toBeUndefined(); // Root node has undefined section
expect(node.cssClasses).toBe('mindmap-node section-root section--1'); // Root node gets special classes
expect(node.icon).toBeUndefined();
expect(node.x).toBeUndefined();
expect(node.y).toBeUndefined();
});
it('should assign correct section classes based on sibling position', () => {
// Create the example mindmap structure:
// A
// a0
// aa0
// a1
// aaa
// a2
db.addNode(0, 'A', 'A', 0); // Root
db.addNode(1, 'a0', 'a0', 0); // First child of root
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
db.addNode(1, 'a1', 'a1', 0); // Second child of root
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
db.addNode(1, 'a2', 'a2', 0); // Third child of root
const result = db.getData();
// Find nodes by their labels
const nodeA = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
const nodeA0 = result.nodes.find((n) => n.label === 'a0') as MindmapLayoutNode;
const nodeAa0 = result.nodes.find((n) => n.label === 'aa0') as MindmapLayoutNode;
const nodeA1 = result.nodes.find((n) => n.label === 'a1') as MindmapLayoutNode;
const nodeAaa = result.nodes.find((n) => n.label === 'aaa') as MindmapLayoutNode;
const nodeA2 = result.nodes.find((n) => n.label === 'a2') as MindmapLayoutNode;
// Check section assignments
expect(nodeA.section).toBeUndefined(); // Root has undefined section
expect(nodeA0.section).toBe(0); // First child of root
expect(nodeAa0.section).toBe(0); // Inherits from parent a0
expect(nodeA1.section).toBe(1); // Second child of root
expect(nodeAaa.section).toBe(1); // Inherits from parent a1
expect(nodeA2.section).toBe(2); // Third child of root
// Check CSS classes
expect(nodeA.cssClasses).toBe('mindmap-node section-root section--1');
expect(nodeA0.cssClasses).toBe('mindmap-node section-0');
expect(nodeAa0.cssClasses).toBe('mindmap-node section-0');
expect(nodeA1.cssClasses).toBe('mindmap-node section-1');
expect(nodeAaa.cssClasses).toBe('mindmap-node section-1');
expect(nodeA2.cssClasses).toBe('mindmap-node section-2');
});
it('should preserve custom classes while adding section classes', () => {
db.addNode(0, 'root', 'Root Node', 0);
db.addNode(1, 'child', 'Child Node', 0);
// Add custom classes to nodes
const mindmap = db.getMindmap();
if (mindmap) {
mindmap.class = 'custom-root-class';
if (mindmap.children?.[0]) {
mindmap.children[0].class = 'custom-child-class';
}
}
const result = db.getData();
const rootNode = result.nodes.find((n) => n.label === 'Root Node') as MindmapLayoutNode;
const childNode = result.nodes.find((n) => n.label === 'Child Node') as MindmapLayoutNode;
// Should include both section classes and custom classes
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1 custom-root-class');
expect(childNode.cssClasses).toBe('mindmap-node section-0 custom-child-class');
});
it('should not create any fake root nodes', () => {
// Create a simple mindmap
db.addNode(0, 'A', 'A', 0);
db.addNode(1, 'a0', 'a0', 0);
db.addNode(1, 'a1', 'a1', 0);
const result = db.getData();
// Check that we only have the expected nodes
expect(result.nodes).toHaveLength(3);
expect(result.nodes.map((n) => n.label)).toEqual(['A', 'a0', 'a1']);
// Check that there's no node with label "mindmap" or any other fake root
const mindmapNode = result.nodes.find((n) => n.label === 'mindmap');
expect(mindmapNode).toBeUndefined();
// Verify the root node has the correct classes
const rootNode = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1');
expect(rootNode.level).toBe(0);
});
it('should assign correct section classes to edges', () => {
// Create the example mindmap structure:
// A
// a0
// aa0
// a1
// aaa
// a2
db.addNode(0, 'A', 'A', 0); // Root
db.addNode(1, 'a0', 'a0', 0); // First child of root
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
db.addNode(1, 'a1', 'a1', 0); // Second child of root
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
db.addNode(1, 'a2', 'a2', 0); // Third child of root
const result = db.getData();
// Should have 5 edges: A->a0, a0->aa0, A->a1, a1->aaa, A->a2
expect(result.edges).toHaveLength(5);
// Find edges by their start and end nodes
const edgeA_a0 = result.edges.find(
(e) => e.start === '0' && e.end === '1'
) as MindmapLayoutEdge;
const edgeA0_aa0 = result.edges.find(
(e) => e.start === '1' && e.end === '2'
) as MindmapLayoutEdge;
const edgeA_a1 = result.edges.find(
(e) => e.start === '0' && e.end === '3'
) as MindmapLayoutEdge;
const edgeA1_aaa = result.edges.find(
(e) => e.start === '3' && e.end === '4'
) as MindmapLayoutEdge;
const edgeA_a2 = result.edges.find(
(e) => e.start === '0' && e.end === '5'
) as MindmapLayoutEdge;
// Check edge classes
expect(edgeA_a0.classes).toBe('edge section-edge-0 edge-depth-1'); // A->a0: section-0, depth-1
expect(edgeA0_aa0.classes).toBe('edge section-edge-0 edge-depth-2'); // a0->aa0: section-0, depth-2
expect(edgeA_a1.classes).toBe('edge section-edge-1 edge-depth-1'); // A->a1: section-1, depth-1
expect(edgeA1_aaa.classes).toBe('edge section-edge-1 edge-depth-2'); // a1->aaa: section-1, depth-2
expect(edgeA_a2.classes).toBe('edge section-edge-2 edge-depth-1'); // A->a2: section-2, depth-1
// Check section assignments match the child nodes
expect(edgeA_a0.section).toBe(0);
expect(edgeA0_aa0.section).toBe(0);
expect(edgeA_a1.section).toBe(1);
expect(edgeA1_aaa.section).toBe(1);
expect(edgeA_a2.section).toBe(2);
});
});
});

View File

@@ -5,6 +5,22 @@ import { log } from '../../logger.js';
import type { MindmapNode } from './mindmapTypes.js';
import defaultConfig from '../../defaultConfig.js';
import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
// Extend Node type for mindmap-specific properties
export type MindmapLayoutNode = Node & {
level: number;
nodeId: string;
type: number;
section?: number;
};
// Extend Edge type for mindmap-specific properties
export type MindmapLayoutEdge = Edge & {
depth: number;
section?: number;
};
const nodeType = {
DEFAULT: 0,
NO_BORDER: 0,
@@ -27,7 +43,6 @@ export class MindmapDB {
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);
@@ -156,6 +171,214 @@ export class MindmapDB {
}
}
/**
* Assign section numbers to nodes based on their position relative to root
* @param node - The mindmap node to process
* @param sectionNumber - The section number to assign (undefined for root)
*/
public assignSections(node: MindmapNode, sectionNumber?: number): void {
// For root node, section should be undefined (not -1)
if (node.level === 0) {
node.section = undefined;
} else {
// For non-root nodes, assign the section number
node.section = sectionNumber;
}
// For root node's children, assign section numbers based on their index
// For other nodes, inherit parent's section number
if (node.children) {
for (const [index, child] of node.children.entries()) {
const childSectionNumber = node.level === 0 ? index : sectionNumber;
this.assignSections(child, childSectionNumber);
}
}
}
/**
* Convert mindmap tree structure to flat array of nodes
* @param node - The mindmap node to process
* @param processedNodes - Array to collect processed nodes
*/
public flattenNodes(node: MindmapNode, processedNodes: MindmapLayoutNode[]): void {
// Build CSS classes for the node
const cssClasses = ['mindmap-node'];
// Add section-specific classes
if (node.level === 0) {
// Root node gets special classes
cssClasses.push('section-root', 'section--1');
} else if (node.section !== undefined) {
// Child nodes get section class based on their section number
cssClasses.push(`section-${node.section}`);
}
// Add any custom classes from the node
if (node.class) {
cssClasses.push(node.class);
}
const classes = cssClasses.join(' ');
// Map mindmap node type to valid shape name
const getShapeFromType = (type: number) => {
switch (type) {
case nodeType.CIRCLE:
return 'circle';
case nodeType.RECT:
return 'rect';
case nodeType.ROUNDED_RECT:
return 'rounded';
case nodeType.CLOUD:
return 'cloud';
case nodeType.BANG:
return 'bang';
case nodeType.HEXAGON:
return 'hexagon';
case nodeType.DEFAULT:
case nodeType.NO_BORDER:
default:
return 'rect';
}
};
const processedNode: MindmapLayoutNode = {
id: node.id.toString(),
domId: 'node_' + node.id.toString(),
label: node.descr,
isGroup: false,
shape: getShapeFromType(node.type),
width: node.width,
height: node.height ?? 0,
padding: node.padding,
cssClasses: classes,
cssStyles: [],
look: 'default',
icon: node.icon,
x: node.x,
y: node.y,
// Mindmap-specific properties
level: node.level,
nodeId: node.nodeId,
type: node.type,
section: node.section,
};
processedNodes.push(processedNode);
// Recursively process children
if (node.children) {
for (const child of node.children) {
this.flattenNodes(child, processedNodes);
}
}
}
/**
* Generate edges from parent-child relationships in mindmap tree
* @param node - The mindmap node to process
* @param edges - Array to collect edges
*/
public generateEdges(node: MindmapNode, edges: MindmapLayoutEdge[]): void {
if (!node.children) {
return;
}
for (const child of node.children) {
// Build CSS classes for the edge
let edgeClasses = 'edge';
// Add section-specific classes based on the child's section
if (child.section !== undefined) {
edgeClasses += ` section-edge-${child.section}`;
}
// Add depth class based on the parent's level + 1 (depth of the edge)
const edgeDepth = node.level + 1;
edgeClasses += ` edge-depth-${edgeDepth}`;
const edge: MindmapLayoutEdge = {
id: `edge_${node.id}_${child.id}`,
start: node.id.toString(),
end: child.id.toString(),
type: 'normal',
curve: 'basis',
thickness: 'normal',
look: 'default',
classes: edgeClasses,
// Store mindmap-specific data
depth: node.level,
section: child.section,
};
edges.push(edge);
// Recursively process child edges
this.generateEdges(child, edges);
}
}
/**
* Get structured data for layout algorithms
* Following the pattern established by ER diagrams
* @returns Structured data containing nodes, edges, and config
*/
public getData(): LayoutData {
const mindmapRoot = this.getMindmap();
const config = getConfig();
if (!mindmapRoot) {
return {
nodes: [],
edges: [],
config,
};
}
log.debug('getData: mindmapRoot', mindmapRoot, config);
// Assign section numbers to all nodes based on their position relative to root
this.assignSections(mindmapRoot);
// Convert tree structure to flat arrays
const processedNodes: MindmapLayoutNode[] = [];
const processedEdges: MindmapLayoutEdge[] = [];
this.flattenNodes(mindmapRoot, processedNodes);
this.generateEdges(mindmapRoot, processedEdges);
log.debug(
`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`
);
// Create shapes map for ELK compatibility
const shapes = new Map<string, any>();
for (const node of processedNodes) {
shapes.set(node.id, {
shape: node.shape,
width: node.width,
height: node.height,
padding: node.padding,
});
}
return {
nodes: processedNodes,
edges: processedEdges,
config,
// Store the root node for mindmap-specific layout algorithms
rootNode: mindmapRoot,
// Properties required by dagre layout algorithm
markers: [], // Mindmaps don't use markers
direction: 'TB', // Top-to-bottom direction for mindmaps
nodeSpacing: 50, // Default spacing between nodes
rankSpacing: 50, // Default spacing between ranks
// Add shapes for ELK compatibility
shapes: Object.fromEntries(shapes),
// Additional properties that layout algorithms might expect
type: 'mindmap',
diagramId: 'mindmap-' + Date.now(),
};
}
// Expose logger to grammar
public getLogger() {
return log;
}

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