diff --git a/.build/types.ts b/.build/types.ts index 9dec05a68..1d439d87c 100644 --- a/.build/types.ts +++ b/.build/types.ts @@ -10,13 +10,16 @@ const buildType = (packageName: string) => { console.log(out.toString()); } } catch (e) { - console.error(e); if (e.stdout.length > 0) { console.error(e.stdout.toString()); } if (e.stderr.length > 0) { console.error(e.stderr.toString()); } + // Exit the build process if we are in CI + if (process.env.CI) { + throw new Error(`Failed to build types for ${packageName}`); + } } }; diff --git a/.changeset/add-vert-tag-bar-chart.md b/.changeset/add-vert-tag-bar-chart.md deleted file mode 100644 index 4ab74059c..000000000 --- a/.changeset/add-vert-tag-bar-chart.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': minor ---- - -feat: Add Vertical Line To Gantt Plot At Specified Time diff --git a/.changeset/eleven-wolves-deny.md b/.changeset/eleven-wolves-deny.md deleted file mode 100644 index 76bb69ec5..000000000 --- a/.changeset/eleven-wolves-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -chore: Convert StateDB into TypeScript diff --git a/.changeset/gold-shoes-camp.md b/.changeset/gold-shoes-camp.md deleted file mode 100644 index 3018e7381..000000000 --- a/.changeset/gold-shoes-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: Remove incorrect `style="undefined;"` attributes in some Mermaid diagrams diff --git a/.changeset/honest-trees-dress.md b/.changeset/honest-trees-dress.md deleted file mode 100644 index 054f1bedb..000000000 --- a/.changeset/honest-trees-dress.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@mermaid-js/mermaid-zenuml': patch ---- - -chore: bump minimum ZenUML version to 3.23.28 - -commit: 9d06d8f31e7f12af9e9e092214f907f2dc93ad75 diff --git a/.changeset/neat-moose-compare.md b/.changeset/neat-moose-compare.md deleted file mode 100644 index 98a064789..000000000 --- a/.changeset/neat-moose-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': minor ---- - -feat: Add support for styling Journey Diagram title (color, font-family, and font-size) diff --git a/.changeset/proud-seahorses-wash.md b/.changeset/proud-seahorses-wash.md deleted file mode 100644 index 8b844b679..000000000 --- a/.changeset/proud-seahorses-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -FontAwesome icons can now be embedded as SVGs in flowcharts if they are registered via `mermaid.registerIconPacks`. diff --git a/.changeset/sad-mails-accept.md b/.changeset/sad-mails-accept.md deleted file mode 100644 index 11dd69d8d..000000000 --- a/.changeset/sad-mails-accept.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'mermaid': patch -'@mermaid-js/parser': patch ---- - -Refactor grammar so that title don't break Architecture Diagrams diff --git a/.changeset/sixty-deer-tell.md b/.changeset/sixty-deer-tell.md deleted file mode 100644 index fd48d2aea..000000000 --- a/.changeset/sixty-deer-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': major ---- - -fix: allow sequence diagram arrows with a trailing colon but no message diff --git a/.changeset/soft-readers-tan.md b/.changeset/soft-readers-tan.md deleted file mode 100644 index ec3fa97af..000000000 --- a/.changeset/soft-readers-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': minor ---- - -feat: Dynamically Render Data Labels Within Bar Charts diff --git a/.changeset/ten-lamps-trade.md b/.changeset/ten-lamps-trade.md deleted file mode 100644 index 991a70daf..000000000 --- a/.changeset/ten-lamps-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: allow colons in events diff --git a/.changeset/yellow-mirrors-change.md b/.changeset/yellow-mirrors-change.md deleted file mode 100644 index 09a766104..000000000 --- a/.changeset/yellow-mirrors-change.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@mermaid-js/mermaid-zenuml': patch ---- - -fix(zenuml): limit `peerDependencies` to Mermaid v10 and v11 - -commit: 0ad44c12feead9d20c6a870a49327ada58d6e657 diff --git a/.cspell/code-terms.txt b/.cspell/code-terms.txt index 285b66365..8b549f888 100644 --- a/.cspell/code-terms.txt +++ b/.cspell/code-terms.txt @@ -47,6 +47,7 @@ edgesep EMPTYSTR enddate ERDIAGRAM +eslint flatmap forwardable frontmatter @@ -87,6 +88,7 @@ NODIR NSTR outdir Qcontrolx +QSTR reinit rels reqs diff --git a/.cspell/contributors.txt b/.cspell/contributors.txt index b7f52f8d0..80f4df22a 100644 --- a/.cspell/contributors.txt +++ b/.cspell/contributors.txt @@ -2,8 +2,10 @@ Ashish Jain cpettitt Dong Cai +knsv +Knut Sveidqvist Nikolay Rozhkov Peng Xiao Per Brolin +Sidharth Vinod subhash-halder -Vinod Sidharth diff --git a/.cspell/mermaid-terms.txt b/.cspell/mermaid-terms.txt index cb6db41de..b0cfa0a1d 100644 --- a/.cspell/mermaid-terms.txt +++ b/.cspell/mermaid-terms.txt @@ -13,11 +13,10 @@ gitgraph gzipped handDrawn kanban -knsv -Knut marginx marginy Markdownish +mermaidchart mermaidjs mindmap mindmaps @@ -35,7 +34,6 @@ sandboxed siebling statediagram substate -Sveidqvist unfixable Viewbox viewports diff --git a/.esbuild/build.ts b/.esbuild/build.ts index 05002cb16..72c0af869 100644 --- a/.esbuild/build.ts +++ b/.esbuild/build.ts @@ -1,5 +1,5 @@ import { build } from 'esbuild'; -import { mkdir, writeFile } from 'node:fs/promises'; +import { cp, mkdir, readFile, rename, writeFile } from 'node:fs/promises'; import { packageOptions } from '../.build/common.js'; import { generateLangium } from '../.build/generateLangium.js'; import type { MermaidBuildOptions } from './util.js'; @@ -31,7 +31,15 @@ const buildPackage = async (entryName: keyof typeof packageOptions) => { // mermaid.js { ...iifeOptions }, // mermaid.min.js - { ...iifeOptions, minify: true, metafile: shouldVisualize } + { ...iifeOptions, minify: true, metafile: shouldVisualize }, + // mermaid.tiny.min.js + { + ...iifeOptions, + minify: true, + includeLargeFeatures: false, + metafile: shouldVisualize, + sourcemap: false, + } ); } if (entryName === 'mermaid-zenuml') { @@ -70,6 +78,21 @@ const handler = (e) => { process.exit(1); }; +const buildTinyMermaid = async () => { + await mkdir('./packages/tiny/dist', { recursive: true }); + await rename( + './packages/mermaid/dist/mermaid.tiny.min.js', + './packages/tiny/dist/mermaid.tiny.js' + ); + // Copy version from mermaid's package.json to tiny's package.json + const mermaidPkg = JSON.parse(await readFile('./packages/mermaid/package.json', 'utf8')); + const tinyPkg = JSON.parse(await readFile('./packages/tiny/package.json', 'utf8')); + tinyPkg.version = mermaidPkg.version; + + await writeFile('./packages/tiny/package.json', JSON.stringify(tinyPkg, null, 2) + '\n'); + await cp('./packages/mermaid/CHANGELOG.md', './packages/tiny/CHANGELOG.md'); +}; + const main = async () => { await generateLangium(); await mkdir('stats', { recursive: true }); @@ -78,6 +101,7 @@ const main = async () => { for (const pkg of packageNames) { await buildPackage(pkg).catch(handler); } + await buildTinyMermaid(); }; void main(); diff --git a/.esbuild/util.ts b/.esbuild/util.ts index dde0352af..3a0ec6b41 100644 --- a/.esbuild/util.ts +++ b/.esbuild/util.ts @@ -14,6 +14,7 @@ export interface MermaidBuildOptions extends BuildOptions { metafile: boolean; format: 'esm' | 'iife'; options: PackageOptions; + includeLargeFeatures: boolean; } export const defaultOptions: Omit = { @@ -21,6 +22,7 @@ export const defaultOptions: Omit metafile: false, core: false, format: 'esm', + includeLargeFeatures: true, } as const; const buildOptions = (override: BuildOptions): BuildOptions => { @@ -39,12 +41,18 @@ const buildOptions = (override: BuildOptions): BuildOptions => { }; }; -const getFileName = (fileName: string, { core, format, minify }: MermaidBuildOptions) => { +const getFileName = ( + fileName: string, + { core, format, minify, includeLargeFeatures }: MermaidBuildOptions +) => { if (core) { fileName += '.core'; } else if (format === 'esm') { fileName += '.esm'; } + if (!includeLargeFeatures) { + fileName += '.tiny'; + } if (minify) { fileName += '.min'; } @@ -54,25 +62,27 @@ const getFileName = (fileName: string, { core, format, minify }: MermaidBuildOpt export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => { const { core, - metafile, format, - minify, options: { name, file, packageName }, globalName = 'mermaid', + includeLargeFeatures, + ...rest } = options; + const external: string[] = ['require', 'fs', 'path']; const outFileName = getFileName(name, options); const output: BuildOptions = buildOptions({ + ...rest, absWorkingDir: resolve(__dirname, `../packages/${packageName}`), entryPoints: { [outFileName]: `src/${file}`, }, - metafile, - minify, globalName, logLevel: 'info', chunkNames: `chunks/${outFileName}/[name]-[hash]`, define: { + // This needs to be stringified for esbuild + includeLargeFeatures: `${includeLargeFeatures}`, 'import.meta.vitest': 'undefined', }, }); diff --git a/.github/lychee.toml b/.github/lychee.toml index b4e8ba0fb..03dc0c5e5 100644 --- a/.github/lychee.toml +++ b/.github/lychee.toml @@ -46,6 +46,9 @@ exclude = [ # Drupal 403 "https://(www.)?drupal.org", +# Phbpp 403 +"https://(www.)?phpbb.com", + # Swimm returns 404, even though the link is valid "https://docs.swimm.io", @@ -53,6 +56,7 @@ exclude = [ "https://huehive.co", "https://foswiki.org", "https://www.gnu.org", +"https://mermaid-preview.com" ] # Exclude all private IPs from checking. diff --git a/.github/workflows/e2e-timings.yml b/.github/workflows/e2e-timings.yml index ca78d0b3a..00e733c48 100644 --- a/.github/workflows/e2e-timings.yml +++ b/.github/workflows/e2e-timings.yml @@ -58,7 +58,7 @@ jobs: echo "EOF" >> $GITHUB_OUTPUT - name: Commit and create pull request - uses: peter-evans/create-pull-request@3b1f4bffdc97d7b055dd96732d7348e585ad2c4e + uses: peter-evans/create-pull-request@889dce9eaba7900ce30494f5e1ac7220b27e5c81 with: add-paths: | cypress/timings.json diff --git a/.github/workflows/validate-lockfile.yml b/.github/workflows/validate-lockfile.yml new file mode 100644 index 000000000..6eb0a63ca --- /dev/null +++ b/.github/workflows/validate-lockfile.yml @@ -0,0 +1,70 @@ +name: Validate pnpm-lock.yaml + +on: + pull_request: + paths: + - 'pnpm-lock.yaml' + - '**/package.json' + - '.github/workflows/validate-lockfile.yml' + +jobs: + validate-lockfile: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + + - name: Validate pnpm-lock.yaml entries + id: validate # give this step an ID so we can reference its outputs + run: | + issues=() + + # 1) No tarball references + if grep -qF 'tarball:' pnpm-lock.yaml; then + issues+=("• Tarball references found (forbidden)") + fi + + # 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.") + fi + + # 3) Lockfile only changes when package.json changes + git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} > changed.txt + if grep -q '^pnpm-lock.yaml$' changed.txt && ! grep -q 'package.json' changed.txt; then + issues+=("• pnpm-lock.yaml changed without any package.json modification") + fi + + # If any issues, output them and fail + if [ ${#issues[@]} -gt 0 ]; then + # Use the new GITHUB_OUTPUT approach to set a multiline output + { + echo "errors<> $GITHUB_OUTPUT + exit 1 + fi + + - name: Comment on PR if validation failed + if: failure() + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body: | + The following issue(s) were detected: + ${{ steps.validate.outputs.errors }} + + Please address these and push an update. + + _Posted automatically by GitHub Actions_ diff --git a/.vite/build.ts b/.vite/build.ts index 486d59452..480dd6b30 100644 --- a/.vite/build.ts +++ b/.vite/build.ts @@ -94,6 +94,10 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions) }), ...visualizerOptions(packageName, core), ], + define: { + // Needs to be string + includeLargeFeatures: 'true', + }, }; if (watch && config.build) { diff --git a/README.md b/README.md index 760ce0f25..3f26a75f2 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,6 @@ In our release process we rely heavily on visual regression tests using [applito -## Mermaid AI Bot - -[Mermaid](https://codeparrot.ai/oracle?owner=mermaid-js&repo=mermaid) Bot will help you understand this repository better. You can ask for code examples, installation guide, debugging help and much more. - ## Examples **The following are some examples of the diagrams, charts and graphs that can be made using Mermaid. Click here to jump into the [text syntax](https://mermaid.js.org/intro/syntax-reference.html).** diff --git a/cypress/integration/other/configuration.spec.js b/cypress/integration/other/configuration.spec.js index ad6b21e29..b48a197a4 100644 --- a/cypress/integration/other/configuration.spec.js +++ b/cypress/integration/other/configuration.spec.js @@ -69,7 +69,9 @@ describe('Configuration', () => { .and('include', 'url(#'); }); }); - it('should handle arrowMarkerAbsolute explicitly set to "false" as false', () => { + // This has been broken for a long time, but something about the Cypress environment was + // rewriting the URL to be relative, causing the test to incorrectly pass. + it.skip('should handle arrowMarkerAbsolute explicitly set to "false" as false', () => { renderGraph( `graph TD A[Christmas] -->|Get money| B(Go shopping) @@ -112,7 +114,7 @@ describe('Configuration', () => { .first() .should('have.attr', 'marker-end') .should('exist') - .and('include', 'url(http://localhost'); + .and('include', 'url(http\\:\\/\\/localhost'); }); }); it('should not taint the initial configuration when using multiple directives', () => { diff --git a/cypress/integration/rendering/flowchart.spec.js b/cypress/integration/rendering/flowchart.spec.js index 7b986cd2f..40713ac4e 100644 --- a/cypress/integration/rendering/flowchart.spec.js +++ b/cypress/integration/rendering/flowchart.spec.js @@ -934,4 +934,43 @@ graph TD } ); }); + it('68: should honor subgraph direction when inheritDir is false', () => { + imgSnapshotTest( + ` + %%{init: {"flowchart": { "inheritDir": false }}}%% + flowchart TB + direction LR + subgraph A + direction TB + a --> b + end + subgraph B + c --> d + end + `, + { + fontFamily: 'courier', + } + ); + }); + + it('69: should inherit global direction when inheritDir is true', () => { + imgSnapshotTest( + ` + %%{init: {"flowchart": { "inheritDir": true }}}%% + flowchart TB + direction LR + subgraph A + direction TB + a --> b + end + subgraph B + c --> d + end + `, + { + fontFamily: 'courier', + } + ); + }); }); diff --git a/cypress/integration/rendering/timeline.spec.ts b/cypress/integration/rendering/timeline.spec.ts index dc6fab364..3785f5fcc 100644 --- a/cypress/integration/rendering/timeline.spec.ts +++ b/cypress/integration/rendering/timeline.spec.ts @@ -161,4 +161,68 @@ describe('Timeline diagram', () => { {} ); }); + + it('11: should render timeline with many stacked events and proper timeline line length', () => { + imgSnapshotTest( + `timeline + title Medical Device Lifecycle + section Pre-Development + Quality Management System : Regulatory Compliance : Risk Management + section Development + Management Responsibility : Planning Activities : Human Resources + Resource Management : Management Reviews : Infrastructure + section Post-Development + Product Realization Activities : Planning Activities : Customer-related Processes + Post-Production Activities : Feedback : Complaints : Adverse Events + : Research and Development : Purchasing Activities + : Production Activities : Installation Activities + : Servicing Activities : Post-Market Surveillance + `, + {} + ); + }); + + it('12: should render timeline with proper vertical line lengths for all columns', () => { + imgSnapshotTest( + `--- +config: + theme: base + themeVariables: + fontFamily: Fira Sans + fontSize: 17px + cScale0: '#b3cde0' + cScale1: '#f49090' + cScale2: '#85d5b8' +--- + +timeline + title Medical Device Lifecycle + section Planning + Quality Management System (4): Regulatory Compliance (4.1.1) + : Risk Management (4.1.2) + Management Resposibility (5): Planning Activities (5.4) + : Management Reviews (5.6) + Resource Management (6): Human Resources (6.2) + : Infrastructure (6.3) + section Realization + Research and Development (7.3): RnD Planning (7.3.2) + : Inputs (7.3.3) + : Outputs (7.3.4) + : Review (7.3.5) + : Verification (7.3.6) + : Validation (7.3.7) + Purchasing (7.4): Purchasing Process (7.4.1) + : Purchasing Information (7.4.2) + Production (7.5): Production Activities (7.5.1) + : Production Feedback (8.2.1) + Installation (7.5.3): Installation Activities (7.5.3) + Servicing (7.5.4): Servicing Activities (7.5.4) + section Post-Production + Post-Market Activities (8): Feedback (8.2.1) + : Complaints (8.2.2) + : Adverse Events (8.2.3) + `, + {} + ); + }); }); diff --git a/cypress/integration/rendering/treemap.spec.ts b/cypress/integration/rendering/treemap.spec.ts new file mode 100644 index 000000000..92fcb5808 --- /dev/null +++ b/cypress/integration/rendering/treemap.spec.ts @@ -0,0 +1,382 @@ +import { imgSnapshotTest } from '../../helpers/util.ts'; + +describe('Treemap Diagram', () => { + it('1: should render a basic treemap', () => { + imgSnapshotTest( + `treemap-beta +"Category A" + "Item A1": 10 + "Item A2": 20 +"Category B" + "Item B1": 15 + "Item B2": 25 + `, + {} + ); + }); + + it('2: should render a hierarchical treemap', () => { + imgSnapshotTest( + `treemap-beta +"Products" + "Electronics" + "Phones": 50 + "Computers": 30 + "Accessories": 20 + "Clothing" + "Men's" + "Shirts": 10 + "Pants": 15 + "Women's" + "Dresses": 20 + "Skirts": 10 + `, + {} + ); + }); + + it('3: should render a treemap with styling using classDef', () => { + imgSnapshotTest( + `treemap-beta +"Section 1" + "Leaf 1.1": 12 + "Section 1.2":::class1 + "Leaf 1.2.1": 12 +"Section 2" + "Leaf 2.1": 20:::class1 + "Leaf 2.2": 25 + "Leaf 2.3": 12 + +classDef class1 fill:red,color:blue,stroke:#FFD600; + `, + {} + ); + }); + + it('4: should handle long text that wraps', () => { + imgSnapshotTest( + `treemap-beta +"Main Category" + "This is a very long item name that should wrap to the next line when rendered in the treemap diagram": 50 + "Short item": 20 + `, + {} + ); + }); + + it('5: should render with a forest theme', () => { + imgSnapshotTest( + `--- +config: + theme: forest +--- +treemap-beta +"Category A" + "Item A1": 10 + "Item A2": 20 +"Category B" + "Item B1": 15 + "Item B2": 25 + `, + {} + ); + }); + + it('6: should handle multiple levels of nesting', () => { + imgSnapshotTest( + `treemap-beta +"Level 1" + "Level 2A" + "Level 3A": 10 + "Level 3B": 15 + "Level 2B" + "Level 3C": 20 + "Level 3D" + "Level 4A": 5 + "Level 4B": 5 + `, + {} + ); + }); + + it('7: should handle classDef with multiple styles', () => { + imgSnapshotTest( + `treemap-beta +"Main" + "A": 20 + "B":::important + "B1": 10 + "B2": 15 + "C": 5:::secondary + +classDef important fill:#f96,stroke:#333,stroke-width:2px; +classDef secondary fill:#6cf,stroke:#333,stroke-dasharray:5 5; + `, + {} + ); + }); + + it('8: should handle dollar value formatting with thousands separator', () => { + imgSnapshotTest( + `--- +config: + treemap: + valueFormat: "$0,0" +--- +treemap +"Budget" + "Operations" + "Salaries": 700000 + "Equipment": 200000 + "Supplies": 100000 + "Marketing" + "Advertising": 400000 + "Events": 100000 + `, + {} + ); + }); + + it('8a: should handle percentage formatting', () => { + imgSnapshotTest( + `--- +config: + treemap: + valueFormat: ".1%" +--- +treemap-beta +"Market Share" + "Company A": 0.35 + "Company B": 0.25 + "Company C": 0.15 + "Others": 0.25 + `, + {} + ); + }); + + it('8b: should handle decimal formatting', () => { + imgSnapshotTest( + `--- +config: + treemap: + valueFormat: ".2f" +--- +treemap-beta +"Metrics" + "Conversion Rate": 0.0567 + "Bounce Rate": 0.6723 + "Click-through Rate": 0.1289 + "Engagement": 0.4521 + `, + {} + ); + }); + + it('8c: should handle dollar sign with decimal places', () => { + imgSnapshotTest( + `--- +config: + treemap: + valueFormat: "$.2f" +--- +treemap-beta +"Product Prices" + "Basic": 19.99 + "Standard": 49.99 + "Premium": 99.99 + "Enterprise": 199.99 + `, + {} + ); + }); + + it('8d: should handle dollar sign with thousands separator and decimal places', () => { + imgSnapshotTest( + `--- +config: + treemap: + valueFormat: "$,.2f" +--- +treemap-beta +"Revenue" + "Q1": 1250345.75 + "Q2": 1645789.25 + "Q3": 1845123.50 + "Q4": 2145678.75 + `, + {} + ); + }); + + it('8e: should handle simple thousands separator', () => { + imgSnapshotTest( + `--- +config: + treemap: + valueFormat: "," +--- +treemap-beta +"User Counts" + "Active Users": 1250345 + "New Signups": 45789 + "Churned": 12350 + "Converted": 78975 + `, + {} + ); + }); + + it('8f: should handle valueFormat set via directive with dollar and thousands separator', () => { + imgSnapshotTest( + `--- +config: + treemap: + valueFormat: "$,.0f" +--- +treemap-beta +"Sales by Region" + "North": 1234567 + "South": 7654321 + "East": 4567890 + "West": 9876543 + `, + {} + ); + }); + + it('8g: should handle scientific notation format', () => { + imgSnapshotTest( + `--- +config: + treemap: + valueFormat: ".2e" +--- +treemap-beta +"Scientific Values" + "Value 1": 1234567 + "Value 2": 0.0000123 + "Value 3": 1000000000 + `, + {} + ); + }); + + it('9: should handle a complex example with multiple features', () => { + imgSnapshotTest( + `--- +config: + theme: dark + treemap: + valueFormat: "$0,0" +--- +treemap-beta +"Company Budget" + "Engineering":::engineering + "Frontend": 300000 + "Backend": 400000 + "DevOps": 200000 + "Marketing":::marketing + "Digital": 250000 + "Print": 100000 + "Events": 150000 + "Sales":::sales + "Direct": 500000 + "Channel": 300000 + +classDef engineering fill:#6b9bc3,stroke:#333; +classDef marketing fill:#c36b9b,stroke:#333; +classDef sales fill:#c3a66b,stroke:#333; + `, + {} + ); + }); + + it('10: should render the example from documentation', () => { + imgSnapshotTest( + ` + treemap-beta + "Section 1" + "Leaf 1.1": 12 + "Section 1.2":::class1 + "Leaf 1.2.1": 12 + "Section 2" + "Leaf 2.1": 20:::class1 + "Leaf 2.2": 25 + "Leaf 2.3": 12 + + classDef class1 fill:red,color:blue,stroke:#FFD600; + `, + {} + ); + }); + + it('11: should handle comments', () => { + imgSnapshotTest( + ` + treemap-beta + %% This is a comment + "Category A" + "Item A1": 10 + "Item A2": 20 + %% Another comment + "Category B" + "Item B1": 15 + "Item B2": 25 + `, + {} + ); + }); + /* + it.skip('12: should render a treemap with title', () => { + imgSnapshotTest( + ` + treemap-beta + title Treemap with Title + "Category A" + "Item A1": 10 + "Item A2": 20 + "Category B" + "Item B1": 15 + "Item B2": 25 + `, + {} + ); + }); + + it.skip('13: should render a treemap with accessibility attributes', () => { + imgSnapshotTest( + ` + treemap-beta + accTitle: Accessible Treemap Title + accDescr: This is a description of the treemap for accessibility purposes + "Category A" + "Item A1": 10 + "Item A2": 20 + "Category B" + "Item B1": 15 + "Item B2": 25 + `, + {} + ); + }); + + it.skip('14: should render a treemap with title and accessibility attributes', () => { + imgSnapshotTest( + ` + treemap + title Treemap with Title and Accessibility + accTitle: Accessible Treemap Title + accDescr: This is a description of the treemap for accessibility purposes + "Category A" + "Item A1": 10 + "Item A2": 20 + "Category B" + "Item B1": 15 + "Item B2": 25 + `, + {} + ); + }); + */ +}); diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 934d6f44c..ab7ded0c2 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -32,8 +32,26 @@ href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap" rel="stylesheet" /> + + + + + + +
+
+              erDiagram
+              CAR ||--o{ NAMED-DRIVER : allows
+              CAR ::: Pine {
+                  string registrationNumber PK "Primary Key
Unique registration number" + string make "Car make
e.g., Toyota" + string model "Model of the car
e.g., Corolla" + string[] parts "List of parts
Stored as array" + } + PERSON ||--o{ NAMED-DRIVER : is + PERSON ::: someclass { + string driversLicense PK "The license #
Primary Key" + string(99) firstName "Only 99 characters
are allowed
e.g., Smith" + string lastName "Last name of person
e.g., Smith" + string phone UK "Unique phone number
Used for contact" + int age "Age of the person
Must be numeric" + } + NAMED-DRIVER { + string carRegistrationNumber PK, FK, UK, PK "Foreign key to CAR
Also part of PK" + string driverLicence PK, FK "Foreign key to PERSON
Also part of PK" + } + MANUFACTURER only one to zero or more CAR : makesx +
+
+
+                  erDiagram
+                  _**testẽζ➕Ø😀㌕ぼ**_ {
+                    *__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***"
+                    string last*Name*
+                    string __phone__ UK
+                    *string(99)~T~~~~~~* firstName "Only __99__ 
characters are a
llowed dsfsdfsdfsdfs" + int _age_ + } +
+
+ + + + diff --git a/demos/timeline.html b/demos/timeline.html index 22ffecd97..b3b99a6dd 100644 --- a/demos/timeline.html +++ b/demos/timeline.html @@ -23,6 +23,23 @@ 1940 : fourth step : fifth step +

Medical Device Lifecycle Timeline

+
+        timeline
+        title Medical Device Lifecycle
+        section Planning
+          Quality Management System (4) : Regulatory Compliance (4.1) : Risk Management (4.1.3) : Management Review (5.6) : Infrastructure (6.3)
+          Management Responsibility (5) : Planning Activities (5.2) : Human Resources (6.2) : RnD Planning (7.3.2) : Purchasing Process (7.4.1) : Production Activities (7.5.1) : Installation Activities (7.5.3) : Servicing Activities (7.5.4)
+        section Realization
+          Research and Development (7.3) : Inputs (7.3.3) : Outputs (7.3.4) : Review (7.3.5) : Verification (7.3.6) : Validation (7.3.7)
+          Purchasing (7.4) : Purchasing Information (7.4.2) : Production Feedback (8.2.1)
+          Production (7.5) : Production Feedback (8.2.1)
+          Installation (7.5.3) : Installation Activities (7.5.3)
+          Servicing (7.5.4) : Servicing Activities (7.5.4)
+        section Post-Production
+          Post-Market Activities (8) : Feedback (8.2.1) : Complaints (8.2.2) : Adverse Events (8.2.3)
+    
+ + + diff --git a/demos/zenuml.html b/demos/zenuml.html index 3ce0ac085..8fe3ab3b9 100644 --- a/demos/zenuml.html +++ b/demos/zenuml.html @@ -10,16 +10,20 @@

Zenuml demos

 		zenuml
-      title Sync Messages (Design Pattern: Adapter)
-			@Starter(Client)
-      Adapter.interfaceMethod() {
-        translateParameter(parameter)
-
-        result = Implementation.implementationMethod()
-
-        translateResult()
-        return translatedResult
-      }
+BookLibService.Borrow(id) {
+  User = Session.GetUser()
+  if(User.isActive) {
+    try {
+      BookRepository.Update(id, onLoan, User)
+      receipt = new Receipt(id, dueDate)
+    } catch (BookNotFoundException) {
+      ErrorService.onException(BookNotFoundException)
+    } finally {
+      Connection.close()
+    }
+  }
+  return receipt
+}
     
 		zenuml
diff --git a/docs/community/questions-and-suggestions.md b/docs/community/questions-and-suggestions.md
index 8dbfae008..05c8d1410 100644
--- a/docs/community/questions-and-suggestions.md
+++ b/docs/community/questions-and-suggestions.md
@@ -22,4 +22,4 @@ This helps the team know the relative interest in something and helps them set p
 
 You have not found anything that already addresses your request, or maybe you have come up with the new idea? Feel free to open a new issue or discussion.
 
-Log in to [GitHub.com](https://www.github.com), and use [GitHub issue tracker of the mermaid-js repository](https://github.com/mermaid-js/mermaid/issues). Press \[] issue, select the appropriate template and describe your problem.
+Log in to [GitHub.com](https://www.github.com), and use [GitHub issue tracker of the mermaid-js repository](https://github.com/mermaid-js/mermaid/issues). Press [issue, select the appropriate template](https://github.com/mermaid-js/mermaid/issues/new/choose) and describe your problem.
diff --git a/docs/config/setup/defaultConfig/variables/configKeys.md b/docs/config/setup/defaultConfig/variables/configKeys.md
index 4687ad8bc..222111bd5 100644
--- a/docs/config/setup/defaultConfig/variables/configKeys.md
+++ b/docs/config/setup/defaultConfig/variables/configKeys.md
@@ -12,4 +12,4 @@
 
 > `const` **configKeys**: `Set`<`string`>
 
-Defined in: [packages/mermaid/src/defaultConfig.ts:274](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L274)
+Defined in: [packages/mermaid/src/defaultConfig.ts:290](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L290)
diff --git a/docs/config/usage.md b/docs/config/usage.md
index 2e207213c..e157d1827 100644
--- a/docs/config/usage.md
+++ b/docs/config/usage.md
@@ -98,6 +98,12 @@ Mermaid can load multiple diagrams, in the same page.
 > Try it out, save this code as HTML and load it using any browser.
 > (Except Internet Explorer, please don't use Internet Explorer.)
 
+## Tiny Mermaid
+
+We offer a smaller version of Mermaid that's approximately half the size of the full library. This tiny version doesn't support Mindmap Diagrams, Architecture Diagrams, KaTeX rendering, or lazy loading.
+
+If you need a more lightweight version without these features, you can use [Mermaid Tiny](https://github.com/mermaid-js/mermaid/tree/develop/packages/tiny).
+
 ## Enabling Click Event and Tags in Nodes
 
 A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduced in version 8.2 as a security improvement, aimed at preventing malicious use.
diff --git a/docs/ecosystem/integrations-community.md b/docs/ecosystem/integrations-community.md
index 27cf22ee1..463fcfae7 100644
--- a/docs/ecosystem/integrations-community.md
+++ b/docs/ecosystem/integrations-community.md
@@ -245,7 +245,7 @@ Communication tools and platforms
 | GitHub + Mermaid         | -                                                                                                           | [🦊🔗](https://addons.mozilla.org/firefox/addon/github-mermaid/)               | -                                                                              | -                                                                                                                            | [🐙🔗](https://github.com/BackMarket/github-mermaid-extension)                                       |
 | Asciidoctor Live Preview | [🎡🔗](https://chromewebstore.google.com/detail/asciidoctorjs-live-previe/iaalpfgpbocpdfblpnhhgllgbdbchmia) | -                                                                              | -                                                                              | [🌀🔗](https://microsoftedge.microsoft.com/addons/detail/asciidoctorjs-live-previ/pefkelkanablhjdekgdahplkccnbdggd?hl=en-US) | -                                                                                                    |
 | Diagram Tab              | -                                                                                                           | -                                                                              | -                                                                              | -                                                                                                                            | [🐙🔗](https://github.com/khafast/diagramtab)                                                        |
-| Markdown Diagrams        | [🎡🔗](https://chromewebstore.google.com/detail/markdown-diagrams/pmoglnmodacnbbofbgcagndelmgaclel)         | [🦊🔗](https://addons.mozilla.org/en-US/firefox/addon/markdown-diagrams/)      | [🔴🔗](https://addons.opera.com/en/extensions/details/markdown-diagrams/)      | [🌀🔗](https://microsoftedge.microsoft.com/addons/detail/markdown-diagrams/hceenoomhhdkjjijnmlclkpenkapfihe)                 | [🐙🔗](https://github.com/marcozaccari/markdown-diagrams-browser-extension/tree/master/doc/examples) |
+| Markdown Diagrams        | [🎡🔗](https://chromewebstore.google.com/detail/markdown-diagrams/pmoglnmodacnbbofbgcagndelmgaclel)         | [🦊🔗](https://addons.mozilla.org/en-US/firefox/addon/markdown-diagrams/)      | -                                                                              | [🌀🔗](https://microsoftedge.microsoft.com/addons/detail/markdown-diagrams/hceenoomhhdkjjijnmlclkpenkapfihe)                 | [🐙🔗](https://github.com/marcozaccari/markdown-diagrams-browser-extension/tree/master/doc/examples) |
 | Markdown Viewer          | -                                                                                                           | [🦊🔗](https://addons.mozilla.org/en-US/firefox/addon/markdown-viewer-chrome/) | -                                                                              | -                                                                                                                            | [🐙🔗](https://github.com/simov/markdown-viewer)                                                     |
 | Extensions for Mermaid   | -                                                                                                           | -                                                                              | [🔴🔗](https://addons.opera.com/en/extensions/details/extensions-for-mermaid/) | -                                                                                                                            | [🐙🔗](https://github.com/Stefan-S/mermaid-extension)                                                |
 | Chrome Diagrammer        | [🎡🔗](https://chromewebstore.google.com/detail/chrome-diagrammer/bkpbgjmkomfoakfklcjeoegkklgjnnpk)         | -                                                                              | -                                                                              | -                                                                                                                            | -                                                                                                    |
@@ -270,5 +270,6 @@ Communication tools and platforms
   - [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
 - [Reveal CK](https://github.com/jedcn/reveal-ck)
   - [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
+- [Vitepress Plugin](https://github.com/sametcn99/vitepress-mermaid-renderer)
 
 
diff --git a/docs/ecosystem/mermaid-chart.md b/docs/ecosystem/mermaid-chart.md
index 79fe3c788..8100a1846 100644
--- a/docs/ecosystem/mermaid-chart.md
+++ b/docs/ecosystem/mermaid-chart.md
@@ -30,7 +30,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
 
   Official Mermaid Chart plugins:
 
-  - [Mermaid Chart GPT](https://chat.openai.com/g/g-1IRFKwq4G-mermaid-chart)
+  - [Mermaid Chart GPT](https://chatgpt.com/g/g-684cc36f30208191b21383b88650a45d-mermaid-chart-diagrams-and-charts)
   - [Confluence](https://marketplace.atlassian.com/apps/1234056/mermaid-chart-for-confluence?hosting=cloud&tab=overview)
   - [Jira](https://marketplace.atlassian.com/apps/1234810/mermaid-chart-for-jira?tab=overview&hosting=cloud)
   - [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=MermaidChart.vscode-mermaid-chart)
diff --git a/docs/intro/index.md b/docs/intro/index.md
index 06a408268..51dd9f9d4 100644
--- a/docs/intro/index.md
+++ b/docs/intro/index.md
@@ -354,6 +354,7 @@ To Deploy Mermaid:
 
 - [Mermaid Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
 - [Mermaid CLI](https://github.com/mermaid-js/mermaid-cli)
+- [Mermaid Tiny](https://github.com/mermaid-js/mermaid/tree/develop/packages/tiny)
 - [Mermaid Webpack Demo](https://github.com/mermaidjs/mermaid-webpack-demo)
 - [Mermaid Parcel Demo](https://github.com/mermaidjs/mermaid-parcel-demo)
 
diff --git a/docs/public/1-Callout-Easy.svg b/docs/public/1-Callout-Easy.svg
new file mode 100644
index 000000000..a6e9251a0
--- /dev/null
+++ b/docs/public/1-Callout-Easy.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/2-Callout-Integrations.svg b/docs/public/2-Callout-Integrations.svg
new file mode 100644
index 000000000..b5ebdf055
--- /dev/null
+++ b/docs/public/2-Callout-Integrations.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/3-Callout-Awards.svg b/docs/public/3-Callout-Awards.svg
new file mode 100644
index 000000000..f10c0fc39
--- /dev/null
+++ b/docs/public/3-Callout-Awards.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/hero-chart-dark.svg b/docs/public/hero-chart-dark.svg
new file mode 100644
index 000000000..2beb9bdab
--- /dev/null
+++ b/docs/public/hero-chart-dark.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/hero-chart.svg b/docs/public/hero-chart.svg
new file mode 100644
index 000000000..fbc675f3a
--- /dev/null
+++ b/docs/public/hero-chart.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/icons/ai-diagram.svg b/docs/public/icons/ai-diagram.svg
new file mode 100644
index 000000000..d3ff002f6
--- /dev/null
+++ b/docs/public/icons/ai-diagram.svg
@@ -0,0 +1,19 @@
+
+  
+    
+  
+  
+    
+  
+
diff --git a/docs/public/icons/ai-repair.svg b/docs/public/icons/ai-repair.svg
new file mode 100644
index 000000000..1e255ac81
--- /dev/null
+++ b/docs/public/icons/ai-repair.svg
@@ -0,0 +1,19 @@
+
+  
+    
+  
+  
+    
+  
+
diff --git a/docs/public/icons/comment.svg b/docs/public/icons/comment.svg
new file mode 100644
index 000000000..626bfd265
--- /dev/null
+++ b/docs/public/icons/comment.svg
@@ -0,0 +1,19 @@
+
+  
+    
+  
+  
+    
+  
+
diff --git a/docs/public/icons/folder.svg b/docs/public/icons/folder.svg
new file mode 100644
index 000000000..a443f1699
--- /dev/null
+++ b/docs/public/icons/folder.svg
@@ -0,0 +1,11 @@
+
+  
+
diff --git a/docs/public/icons/group.svg b/docs/public/icons/group.svg
new file mode 100644
index 000000000..8a7443b6e
--- /dev/null
+++ b/docs/public/icons/group.svg
@@ -0,0 +1,11 @@
+
+  
+
diff --git a/docs/public/icons/groups.svg b/docs/public/icons/groups.svg
new file mode 100644
index 000000000..c827bebc6
--- /dev/null
+++ b/docs/public/icons/groups.svg
@@ -0,0 +1,19 @@
+
+  
+    
+  
+  
+    
+  
+
diff --git a/docs/public/icons/open-source.svg b/docs/public/icons/open-source.svg
new file mode 100644
index 000000000..d6c1f9843
--- /dev/null
+++ b/docs/public/icons/open-source.svg
@@ -0,0 +1,11 @@
+
+  
+  
+
diff --git a/docs/public/icons/plugins.svg b/docs/public/icons/plugins.svg
new file mode 100644
index 000000000..fbf4a4800
--- /dev/null
+++ b/docs/public/icons/plugins.svg
@@ -0,0 +1,19 @@
+
+  
+    
+  
+  
+    
+  
+
diff --git a/docs/public/icons/presentation.svg b/docs/public/icons/presentation.svg
new file mode 100644
index 000000000..4c679a19e
--- /dev/null
+++ b/docs/public/icons/presentation.svg
@@ -0,0 +1,19 @@
+
+  
+    
+  
+  
+    
+  
+
diff --git a/docs/public/icons/public.svg b/docs/public/icons/public.svg
new file mode 100644
index 000000000..3d563baa1
--- /dev/null
+++ b/docs/public/icons/public.svg
@@ -0,0 +1,11 @@
+
+  
+
diff --git a/docs/public/icons/terminal.svg b/docs/public/icons/terminal.svg
new file mode 100644
index 000000000..5af2408d4
--- /dev/null
+++ b/docs/public/icons/terminal.svg
@@ -0,0 +1,11 @@
+
+  
+
diff --git a/docs/public/icons/version-history.svg b/docs/public/icons/version-history.svg
new file mode 100644
index 000000000..bacb28629
--- /dev/null
+++ b/docs/public/icons/version-history.svg
@@ -0,0 +1,19 @@
+
+  
+    
+  
+  
+    
+  
+
diff --git a/docs/public/icons/whiteboard.svg b/docs/public/icons/whiteboard.svg
new file mode 100644
index 000000000..9ac13652a
--- /dev/null
+++ b/docs/public/icons/whiteboard.svg
@@ -0,0 +1,19 @@
+
+  
+    
+  
+  
+    
+  
+
diff --git a/docs/public/mermaid-logo-horizontal.svg b/docs/public/mermaid-logo-horizontal.svg
new file mode 100644
index 000000000..34b3bb124
--- /dev/null
+++ b/docs/public/mermaid-logo-horizontal.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/syntax/classDiagram.md b/docs/syntax/classDiagram.md
index 20fdef0ed..2067cc97c 100644
--- a/docs/syntax/classDiagram.md
+++ b/docs/syntax/classDiagram.md
@@ -545,6 +545,38 @@ It is possible to annotate classes with markers to provide additional metadata a
 
 Annotations are defined within the opening `<<` and closing `>>`. There are two ways to add an annotation to a class, and either way the output will be same:
 
+> **Tip:**\
+> In Mermaid class diagrams, annotations like `<>` can be attached in two ways:
+>
+> - **Inline with the class definition** (Recommended for consistency):
+>
+>   ```mermaid-example
+>   classDiagram
+>     class Shape <>
+>   ```
+>
+>   ```mermaid
+>   classDiagram
+>     class Shape <>
+>   ```
+>
+> - **Separate line after the class definition**:
+>
+>   ```mermaid-example
+>   classDiagram
+>     class Shape
+>     <> Shape
+>   ```
+>
+>   ```mermaid
+>   classDiagram
+>     class Shape
+>     <> Shape
+>   ```
+>
+> Both methods are fully supported and produce identical diagrams.\
+> However, it is recommended to use the **inline style** for better readability and consistent formatting across diagrams.
+
 - In a **_separate line_** after a class is defined:
 
 ```mermaid-example
diff --git a/docs/syntax/flowchart.md b/docs/syntax/flowchart.md
index 7daf7ca2b..243592515 100644
--- a/docs/syntax/flowchart.md
+++ b/docs/syntax/flowchart.md
@@ -969,12 +969,7 @@ flowchart TD
 
 You can use the `image` shape to include an image in your flowchart. The syntax for defining an image shape is as follows:
 
-```mermaid-example
-flowchart TD
-    A@{ img: "https://example.com/image.png", label: "Image Label", pos: "t", w: 60, h: 60, constraint: "off" }
 ```
-
-```mermaid
 flowchart TD
     A@{ img: "https://example.com/image.png", label: "Image Label", pos: "t", w: 60, h: 60, constraint: "off" }
 ```
@@ -1600,6 +1595,7 @@ flowchart LR
 The "Markdown Strings" feature enhances flowcharts and mind maps by offering a more versatile string type, which supports text formatting options such as bold and italics, and automatically wraps text within labels.
 
 ```mermaid-example
+---
 config:
   flowchart:
     htmlLabels: false
@@ -1616,6 +1612,7 @@ end
 ```
 
 ```mermaid
+---
 config:
   flowchart:
     htmlLabels: false
@@ -1952,7 +1949,7 @@ flowchart TD
 
 There are two ways to display these FontAwesome icons:
 
-### Register FontAwesome icon packs (v\+)
+### Register FontAwesome icon packs (v11.7.0+)
 
 You can register your own FontAwesome icon pack following the ["Registering icon packs" instructions](../config/icons.md).
 
diff --git a/docs/syntax/packet.md b/docs/syntax/packet.md
index 5eab81910..f789961d2 100644
--- a/docs/syntax/packet.md
+++ b/docs/syntax/packet.md
@@ -16,13 +16,25 @@ This diagram type is particularly useful for developers, network engineers, educ
 
 ## Syntax
 
-```md
+```
 packet-beta
 start: "Block name" %% Single-bit block
 start-end: "Block name" %% Multi-bit blocks
 ... More Fields ...
 ```
 
+### Bits Syntax (v11.7.0+)
+
+Using start and end bit counts can be difficult, especially when modifying a design. For this we add a bit count field, which starts from the end of the previous field automagically. Use `+` to set the number of bits, thus:
+
+```
+packet-beta
++1: "Block name" %% Single-bit block
++8: "Block name" %% 8-bit block
+9-15: "Manually set start and end, it's fine to mix and match"
+... More Fields ...
+```
+
 ## Examples
 
 ```mermaid-example
@@ -76,8 +88,8 @@ packet-beta
 ```mermaid-example
 packet-beta
 title UDP Packet
-0-15: "Source Port"
-16-31: "Destination Port"
++16: "Source Port"
++16: "Destination Port"
 32-47: "Length"
 48-63: "Checksum"
 64-95: "Data (variable length)"
@@ -86,8 +98,8 @@ title UDP Packet
 ```mermaid
 packet-beta
 title UDP Packet
-0-15: "Source Port"
-16-31: "Destination Port"
++16: "Source Port"
++16: "Destination Port"
 32-47: "Length"
 48-63: "Checksum"
 64-95: "Data (variable length)"
diff --git a/docs/syntax/treemap.md b/docs/syntax/treemap.md
new file mode 100644
index 000000000..1ee916ff9
--- /dev/null
+++ b/docs/syntax/treemap.md
@@ -0,0 +1,353 @@
+> **Warning**
+>
+> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
+>
+> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/treemap.md](../../packages/mermaid/src/docs/syntax/treemap.md).
+
+# Treemap Diagram
+
+> A treemap diagram displays hierarchical data as a set of nested rectangles. Each branch of the tree is represented by a rectangle, which is then tiled with smaller rectangles representing sub-branches.
+
+> **Warning**
+> This is a new diagram type in Mermaid. Its syntax may evolve in future versions.
+
+## Introduction
+
+Treemap diagrams are an effective way to visualize hierarchical data and show proportions between categories and subcategories. The size of each rectangle is proportional to the value it represents, making it easy to compare different parts of a hierarchy.
+
+Treemap diagrams are particularly useful for:
+
+- Visualizing hierarchical data structures
+- Comparing proportions between categories
+- Displaying large amounts of hierarchical data in a limited space
+- Identifying patterns and outliers in hierarchical data
+
+## Syntax
+
+```
+treemap-beta
+"Section 1"
+    "Leaf 1.1": 12
+    "Section 1.2"
+      "Leaf 1.2.1": 12
+"Section 2"
+    "Leaf 2.1": 20
+    "Leaf 2.2": 25
+```
+
+### Node Definition
+
+Nodes in a treemap are defined using the following syntax:
+
+- **Section/Parent nodes**: Defined with quoted text `"Section Name"`
+- **Leaf nodes with values**: Defined with quoted text followed by a colon and value `"Leaf Name": value`
+- **Hierarchy**: Created using indentation (spaces or tabs)
+- **Styling**: Nodes can be styled using the `:::class` syntax
+
+## Examples
+
+### Basic Treemap
+
+```mermaid-example
+treemap-beta
+"Category A"
+    "Item A1": 10
+    "Item A2": 20
+"Category B"
+    "Item B1": 15
+    "Item B2": 25
+```
+
+```mermaid
+treemap-beta
+"Category A"
+    "Item A1": 10
+    "Item A2": 20
+"Category B"
+    "Item B1": 15
+    "Item B2": 25
+```
+
+### Hierarchical Treemap
+
+```mermaid-example
+treemap-beta
+"Products"
+    "Electronics"
+        "Phones": 50
+        "Computers": 30
+        "Accessories": 20
+    "Clothing"
+        "Men's": 40
+        "Women's": 40
+```
+
+```mermaid
+treemap-beta
+"Products"
+    "Electronics"
+        "Phones": 50
+        "Computers": 30
+        "Accessories": 20
+    "Clothing"
+        "Men's": 40
+        "Women's": 40
+```
+
+### Treemap with Styling
+
+```mermaid-example
+treemap-beta
+"Section 1"
+    "Leaf 1.1": 12
+    "Section 1.2":::class1
+      "Leaf 1.2.1": 12
+"Section 2"
+    "Leaf 2.1": 20:::class1
+    "Leaf 2.2": 25
+    "Leaf 2.3": 12
+
+classDef class1 fill:red,color:blue,stroke:#FFD600;
+```
+
+```mermaid
+treemap-beta
+"Section 1"
+    "Leaf 1.1": 12
+    "Section 1.2":::class1
+      "Leaf 1.2.1": 12
+"Section 2"
+    "Leaf 2.1": 20:::class1
+    "Leaf 2.2": 25
+    "Leaf 2.3": 12
+
+classDef class1 fill:red,color:blue,stroke:#FFD600;
+```
+
+## Styling and Configuration
+
+Treemap diagrams can be customized using Mermaid's styling and configuration options.
+
+### Using classDef for Styling
+
+You can define custom styles for nodes using the `classDef` syntax, which is a standard feature across many Mermaid diagram types:
+
+```mermaid-example
+treemap-beta
+"Main"
+    "A": 20
+    "B":::important
+        "B1": 10
+        "B2": 15
+    "C": 5
+
+classDef important fill:#f96,stroke:#333,stroke-width:2px;
+```
+
+```mermaid
+treemap-beta
+"Main"
+    "A": 20
+    "B":::important
+        "B1": 10
+        "B2": 15
+    "C": 5
+
+classDef important fill:#f96,stroke:#333,stroke-width:2px;
+```
+
+### Theme Configuration
+
+You can customize the colors of your treemap using the theme configuration:
+
+```mermaid-example
+---
+config:
+    theme: 'forest'
+---
+treemap-beta
+"Category A"
+    "Item A1": 10
+    "Item A2": 20
+"Category B"
+    "Item B1": 15
+    "Item B2": 25
+```
+
+```mermaid
+---
+config:
+    theme: 'forest'
+---
+treemap-beta
+"Category A"
+    "Item A1": 10
+    "Item A2": 20
+"Category B"
+    "Item B1": 15
+    "Item B2": 25
+```
+
+### Diagram Padding
+
+You can adjust the padding around the treemap diagram using the `diagramPadding` configuration option:
+
+```mermaid-example
+---
+config:
+  treemap:
+    diagramPadding: 200
+---
+treemap-beta
+"Category A"
+    "Item A1": 10
+    "Item A2": 20
+"Category B"
+    "Item B1": 15
+    "Item B2": 25
+```
+
+```mermaid
+---
+config:
+  treemap:
+    diagramPadding: 200
+---
+treemap-beta
+"Category A"
+    "Item A1": 10
+    "Item A2": 20
+"Category B"
+    "Item B1": 15
+    "Item B2": 25
+```
+
+## Configuration Options
+
+The treemap diagram supports the following configuration options:
+
+| Option         | Description                                                                 | Default |
+| -------------- | --------------------------------------------------------------------------- | ------- |
+| useMaxWidth    | When true, the diagram width is set to 100% and scales with available space | true    |
+| padding        | Internal padding between nodes                                              | 10      |
+| diagramPadding | Padding around the entire diagram                                           | 8       |
+| showValues     | Whether to show values in the treemap                                       | true    |
+| nodeWidth      | Width of nodes                                                              | 100     |
+| nodeHeight     | Height of nodes                                                             | 40      |
+| borderWidth    | Width of borders                                                            | 1       |
+| valueFontSize  | Font size for values                                                        | 12      |
+| labelFontSize  | Font size for labels                                                        | 14      |
+| valueFormat    | Format for values (see Value Formatting section)                            | ','     |
+
+## Advanced Features
+
+### Value Formatting
+
+Values in treemap diagrams can be formatted to display in different ways using the `valueFormat` configuration option. This option primarily uses [D3's format specifiers](https://github.com/d3/d3-format#locale_format) to control how numbers are displayed, with some additional special cases for common formats.
+
+Some common format patterns:
+
+- `,` - Thousands separator (default)
+- `$` - Add dollar sign
+- `.1f` - Show one decimal place
+- `.1%` - Show as percentage with one decimal place
+- `$0,0` - Dollar sign with thousands separator
+- `$.2f` - Dollar sign with 2 decimal places
+- `$,.2f` - Dollar sign with thousands separator and 2 decimal places
+
+The treemap diagram supports both standard D3 format specifiers and some common currency formats that combine the dollar sign with other formatting options.
+
+Example with currency formatting:
+
+```mermaid-example
+---
+config:
+  treemap:
+    valueFormat: '$0,0'
+---
+treemap-beta
+"Budget"
+    "Operations"
+        "Salaries": 700000
+        "Equipment": 200000
+        "Supplies": 100000
+    "Marketing"
+        "Advertising": 400000
+        "Events": 100000
+```
+
+```mermaid
+---
+config:
+  treemap:
+    valueFormat: '$0,0'
+---
+treemap-beta
+"Budget"
+    "Operations"
+        "Salaries": 700000
+        "Equipment": 200000
+        "Supplies": 100000
+    "Marketing"
+        "Advertising": 400000
+        "Events": 100000
+```
+
+Example with percentage formatting:
+
+```mermaid-example
+---
+config:
+  treemap:
+    valueFormat: '$.1%'
+---
+treemap-beta
+"Market Share"
+    "Company A": 0.35
+    "Company B": 0.25
+    "Company C": 0.15
+    "Others": 0.25
+```
+
+```mermaid
+---
+config:
+  treemap:
+    valueFormat: '$.1%'
+---
+treemap-beta
+"Market Share"
+    "Company A": 0.35
+    "Company B": 0.25
+    "Company C": 0.15
+    "Others": 0.25
+```
+
+## Common Use Cases
+
+Treemap diagrams are commonly used for:
+
+1. **Financial Data**: Visualizing budget allocations, market shares, or portfolio compositions
+2. **File System Analysis**: Showing disk space usage by folders and files
+3. **Population Demographics**: Displaying population distribution across regions and subregions
+4. **Product Hierarchies**: Visualizing product categories and their sales volumes
+5. **Organizational Structures**: Representing departments and team sizes in a company
+
+## Limitations
+
+- Treemap diagrams work best when the data has a natural hierarchy
+- Very small values may be difficult to see or label in a treemap diagram
+- Deep hierarchies (many levels) can be challenging to represent clearly
+- Treemap diagrams are not well suited for representing data with negative values
+
+## Related Diagrams
+
+If treemap diagrams don't suit your needs, consider these alternatives:
+
+- [**Pie Charts**](./pie.md): For simple proportion comparisons without hierarchy
+- **Sunburst Diagrams**: For hierarchical data with a radial layout (yet to be released in Mermaid).
+- [**Sankey Diagrams**](./sankey.md): For flow-based hierarchical data
+
+## Notes
+
+The treemap diagram implementation in Mermaid is designed to be simple to use while providing powerful visualization capabilities. As this is a newer diagram type, feedback and feature requests are welcome through the Mermaid GitHub repository.
diff --git a/package.json b/package.json
index e5197c2eb..75d577b86 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
     "@changesets/cli": "^2.27.12",
     "@cspell/eslint-plugin": "^8.19.3",
     "@cypress/code-coverage": "^3.12.49",
-    "@eslint/js": "^9.25.1",
+    "@eslint/js": "^9.26.0",
     "@rollup/plugin-typescript": "^12.1.2",
     "@types/cors": "^2.8.17",
     "@types/express": "^5.0.0",
@@ -83,7 +83,7 @@
     "@vitest/spy": "^3.0.6",
     "@vitest/ui": "^3.0.6",
     "ajv": "^8.17.1",
-    "chokidar": "^4.0.3",
+    "chokidar": "3.6.0",
     "concurrently": "^9.1.2",
     "cors": "^2.8.5",
     "cpy-cli": "^5.0.0",
@@ -93,7 +93,7 @@
     "cypress-image-snapshot": "^4.0.1",
     "cypress-split": "^1.24.14",
     "esbuild": "^0.25.0",
-    "eslint": "^9.25.1",
+    "eslint": "^9.26.0",
     "eslint-config-prettier": "^10.1.1",
     "eslint-plugin-cypress": "^4.3.0",
     "eslint-plugin-html": "^8.1.2",
@@ -126,7 +126,7 @@
     "tslib": "^2.8.1",
     "tsx": "^4.7.3",
     "typescript": "~5.7.3",
-    "typescript-eslint": "^8.31.1",
+    "typescript-eslint": "^8.32.0",
     "vite": "^6.1.1",
     "vite-plugin-istanbul": "^7.0.0",
     "vitest": "^3.0.6"
diff --git a/packages/mermaid-layout-elk/CHANGELOG.md b/packages/mermaid-layout-elk/CHANGELOG.md
index 319d6b828..96b5d2fae 100644
--- a/packages/mermaid-layout-elk/CHANGELOG.md
+++ b/packages/mermaid-layout-elk/CHANGELOG.md
@@ -1,5 +1,14 @@
 # @mermaid-js/layout-elk
 
+## 0.1.8
+
+### Patch Changes
+
+- [#6648](https://github.com/mermaid-js/mermaid/pull/6648) [`85c5b9b`](https://github.com/mermaid-js/mermaid/commit/85c5b9b4c064e2edabf21757c8215a1018d4d288) Thanks [@knsv](https://github.com/knsv)! - Make elk respect the order of nodes based from the code
+
+- Updated dependencies [[`97b79c3`](https://github.com/mermaid-js/mermaid/commit/97b79c3578a2004c63fa32f6d5e17bd8a536e13a), [`b1cf291`](https://github.com/mermaid-js/mermaid/commit/b1cf29127348602137552405e3300dee1697f0de), [`a4754ad`](https://github.com/mermaid-js/mermaid/commit/a4754ad195e70d52fbd46ef44f40797d2d215e41), [`2b05d7e`](https://github.com/mermaid-js/mermaid/commit/2b05d7e1edef635e6c80cb383b10ea0a89279f41), [`41e84b7`](https://github.com/mermaid-js/mermaid/commit/41e84b726a1f2df002b77c4b0071e2c15e47838e), [`d63d3bf`](https://github.com/mermaid-js/mermaid/commit/d63d3bf1e7596ac7eeb24ba06cbc7a70f9c8b070), [`aa6cb86`](https://github.com/mermaid-js/mermaid/commit/aa6cb86899968c65561eebfc1d54dd086b1518a2), [`df9df9d`](https://github.com/mermaid-js/mermaid/commit/df9df9dc32b80a8c320cc0efd5483b9485f15bde), [`cdbd3e5`](https://github.com/mermaid-js/mermaid/commit/cdbd3e58a3a35d63a79258115dedca4a535c1038), [`c17277e`](https://github.com/mermaid-js/mermaid/commit/c17277e743b1c12e4134fba44c62a7d5885f2574), [`a1ba65c`](https://github.com/mermaid-js/mermaid/commit/a1ba65c0c08432ec36e772570c3a5899cb57c102), [`1ddaf10`](https://github.com/mermaid-js/mermaid/commit/1ddaf10b89d8c7311c5e10d466b42fa36b61210b), [`ca80f71`](https://github.com/mermaid-js/mermaid/commit/ca80f719eac86cf4c31392105d5d896f39b84bbc), [`bca6ed6`](https://github.com/mermaid-js/mermaid/commit/bca6ed67c3e0db910bf498fdd0fc0346c02d392b)]:
+  - mermaid@11.7.0
+
 ## 0.1.7
 
 ### Patch Changes
diff --git a/packages/mermaid-layout-elk/package.json b/packages/mermaid-layout-elk/package.json
index 2ce9bc278..023958c1f 100644
--- a/packages/mermaid-layout-elk/package.json
+++ b/packages/mermaid-layout-elk/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@mermaid-js/layout-elk",
-  "version": "0.1.7",
+  "version": "0.1.8",
   "description": "ELK layout engine for mermaid",
   "module": "dist/mermaid-layout-elk.core.mjs",
   "types": "dist/layouts.d.ts",
diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts
index 4d124c04f..9f361bf7b 100644
--- a/packages/mermaid-layout-elk/src/render.ts
+++ b/packages/mermaid-layout-elk/src/render.ts
@@ -766,6 +766,7 @@ export const render = async (
     id: 'root',
     layoutOptions: {
       'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
+      'elk.layered.crossingMinimization.forceNodeModelOrder': true,
       'elk.algorithm': algorithm,
       'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy,
       'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges,
@@ -780,7 +781,6 @@ export const render = async (
       // 'spacing.edgeEdge': 10,
       // 'spacing.edgeEdgeBetweenLayers': 20,
       // 'spacing.nodeSelfLoop': 20,
-
       // Tweaking options
       // 'elk.layered.nodePlacement.favorStraightEdges': true,
       // 'nodePlacement.feedbackEdges': true,
diff --git a/packages/mermaid-zenuml/CHANGELOG.md b/packages/mermaid-zenuml/CHANGELOG.md
new file mode 100644
index 000000000..57e8795a4
--- /dev/null
+++ b/packages/mermaid-zenuml/CHANGELOG.md
@@ -0,0 +1,14 @@
+# @mermaid-js/mermaid-zenuml
+
+## 0.2.1
+
+### Patch Changes
+
+- [#6581](https://github.com/mermaid-js/mermaid/pull/6581) [`941bc69`](https://github.com/mermaid-js/mermaid/commit/941bc698350bd103b2a431ed8fed0c7b0d92fff0) Thanks [@MrCoder](https://github.com/MrCoder)! - Upgraded the dependency @zenuml/core
+
+- [#6319](https://github.com/mermaid-js/mermaid/pull/6319) [`9d06d8f`](https://github.com/mermaid-js/mermaid/commit/9d06d8f31e7f12af9e9e092214f907f2dc93ad75) Thanks [@renovate](https://github.com/apps/renovate)! - chore: bump minimum ZenUML version to 3.23.28
+
+- [#5737](https://github.com/mermaid-js/mermaid/pull/5737) [`0ad44c1`](https://github.com/mermaid-js/mermaid/commit/0ad44c12feead9d20c6a870a49327ada58d6e657) Thanks [@sidharthv96](https://github.com/sidharthv96)! - fix(zenuml): limit `peerDependencies` to Mermaid v10 and v11
+
+- Updated dependencies [[`97b79c3`](https://github.com/mermaid-js/mermaid/commit/97b79c3578a2004c63fa32f6d5e17bd8a536e13a), [`b1cf291`](https://github.com/mermaid-js/mermaid/commit/b1cf29127348602137552405e3300dee1697f0de), [`a4754ad`](https://github.com/mermaid-js/mermaid/commit/a4754ad195e70d52fbd46ef44f40797d2d215e41), [`2b05d7e`](https://github.com/mermaid-js/mermaid/commit/2b05d7e1edef635e6c80cb383b10ea0a89279f41), [`41e84b7`](https://github.com/mermaid-js/mermaid/commit/41e84b726a1f2df002b77c4b0071e2c15e47838e), [`d63d3bf`](https://github.com/mermaid-js/mermaid/commit/d63d3bf1e7596ac7eeb24ba06cbc7a70f9c8b070), [`aa6cb86`](https://github.com/mermaid-js/mermaid/commit/aa6cb86899968c65561eebfc1d54dd086b1518a2), [`df9df9d`](https://github.com/mermaid-js/mermaid/commit/df9df9dc32b80a8c320cc0efd5483b9485f15bde), [`cdbd3e5`](https://github.com/mermaid-js/mermaid/commit/cdbd3e58a3a35d63a79258115dedca4a535c1038), [`c17277e`](https://github.com/mermaid-js/mermaid/commit/c17277e743b1c12e4134fba44c62a7d5885f2574), [`a1ba65c`](https://github.com/mermaid-js/mermaid/commit/a1ba65c0c08432ec36e772570c3a5899cb57c102), [`1ddaf10`](https://github.com/mermaid-js/mermaid/commit/1ddaf10b89d8c7311c5e10d466b42fa36b61210b), [`ca80f71`](https://github.com/mermaid-js/mermaid/commit/ca80f719eac86cf4c31392105d5d896f39b84bbc), [`bca6ed6`](https://github.com/mermaid-js/mermaid/commit/bca6ed67c3e0db910bf498fdd0fc0346c02d392b)]:
+  - mermaid@11.7.0
diff --git a/packages/mermaid-zenuml/package.json b/packages/mermaid-zenuml/package.json
index 7a419c433..d3a76e797 100644
--- a/packages/mermaid-zenuml/package.json
+++ b/packages/mermaid-zenuml/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@mermaid-js/mermaid-zenuml",
-  "version": "0.2.0",
+  "version": "0.2.1",
   "description": "MermaidJS plugin for ZenUML integration",
   "module": "dist/mermaid-zenuml.core.mjs",
   "types": "dist/detector.d.ts",
@@ -33,7 +33,7 @@
   ],
   "license": "MIT",
   "dependencies": {
-    "@zenuml/core": "^3.23.28"
+    "@zenuml/core": "^3.31.1"
   },
   "devDependencies": {
     "mermaid": "workspace:^"
diff --git a/packages/mermaid/CHANGELOG.md b/packages/mermaid/CHANGELOG.md
index b57b83b6a..c57e2a6db 100644
--- a/packages/mermaid/CHANGELOG.md
+++ b/packages/mermaid/CHANGELOG.md
@@ -1,5 +1,64 @@
 # mermaid
 
+## 11.8.1
+
+### Patch Changes
+
+- Updated dependencies [[`0da2922`](https://github.com/mermaid-js/mermaid/commit/0da2922ee7f47959e324ec10d3d21ee70594f557)]:
+  - @mermaid-js/parser@0.6.1
+
+## 11.8.0
+
+### Minor Changes
+
+- [#6590](https://github.com/mermaid-js/mermaid/pull/6590) [`f338802`](https://github.com/mermaid-js/mermaid/commit/f338802642cdecf5b7ed6c19a20cf2a81effbbee) Thanks [@knsv](https://github.com/knsv)! - Adding support for the new diagram type nested treemap
+
+### Patch Changes
+
+- [#6707](https://github.com/mermaid-js/mermaid/pull/6707) [`592c5bb`](https://github.com/mermaid-js/mermaid/commit/592c5bb880c3b942710a2878d386bcb3eb35c137) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Log a warning when duplicate commit IDs are encountered in gitGraph to help identify and debug rendering issues caused by non-unique IDs.
+
+- Updated dependencies [[`f338802`](https://github.com/mermaid-js/mermaid/commit/f338802642cdecf5b7ed6c19a20cf2a81effbbee)]:
+  - @mermaid-js/parser@0.6.0
+
+## 11.7.0
+
+### Minor Changes
+
+- [#6479](https://github.com/mermaid-js/mermaid/pull/6479) [`97b79c3`](https://github.com/mermaid-js/mermaid/commit/97b79c3578a2004c63fa32f6d5e17bd8a536e13a) Thanks [@monicanguyen25](https://github.com/monicanguyen25)! - feat: Add Vertical Line To Gantt Plot At Specified Time
+
+- [#6225](https://github.com/mermaid-js/mermaid/pull/6225) [`41e84b7`](https://github.com/mermaid-js/mermaid/commit/41e84b726a1f2df002b77c4b0071e2c15e47838e) Thanks [@Shahir-47](https://github.com/Shahir-47)! - feat: Add support for styling Journey Diagram title (color, font-family, and font-size)
+
+- [#6423](https://github.com/mermaid-js/mermaid/pull/6423) [`aa6cb86`](https://github.com/mermaid-js/mermaid/commit/aa6cb86899968c65561eebfc1d54dd086b1518a2) Thanks [@BambioGaming](https://github.com/BambioGaming)! - Added support for the click directive in stateDiagram syntax
+
+- [#5980](https://github.com/mermaid-js/mermaid/pull/5980) [`df9df9d`](https://github.com/mermaid-js/mermaid/commit/df9df9dc32b80a8c320cc0efd5483b9485f15bde) Thanks [@BryanCrotazGivEnergy](https://github.com/BryanCrotazGivEnergy)! - feat: Add shorter `+: Label` syntax in packet diagram
+
+- [#6523](https://github.com/mermaid-js/mermaid/pull/6523) [`c17277e`](https://github.com/mermaid-js/mermaid/commit/c17277e743b1c12e4134fba44c62a7d5885f2574) Thanks [@NourBenz](https://github.com/NourBenz)! - fix: allow sequence diagram arrows with a trailing colon but no message
+
+- [#6475](https://github.com/mermaid-js/mermaid/pull/6475) [`a1ba65c`](https://github.com/mermaid-js/mermaid/commit/a1ba65c0c08432ec36e772570c3a5899cb57c102) Thanks [@Shahir-47](https://github.com/Shahir-47)! - feat: Dynamically Render Data Labels Within Bar Charts
+
+### Patch Changes
+
+- [#6588](https://github.com/mermaid-js/mermaid/pull/6588) [`b1cf291`](https://github.com/mermaid-js/mermaid/commit/b1cf29127348602137552405e3300dee1697f0de) Thanks [@omkarht](https://github.com/omkarht)! - Fix stroke styles for ER diagram to correctly apply path and row-specific styles
+
+- [#6296](https://github.com/mermaid-js/mermaid/pull/6296) [`a4754ad`](https://github.com/mermaid-js/mermaid/commit/a4754ad195e70d52fbd46ef44f40797d2d215e41) Thanks [@sidharthv96](https://github.com/sidharthv96)! - chore: Convert StateDB into TypeScript
+
+- [#6463](https://github.com/mermaid-js/mermaid/pull/6463) [`2b05d7e`](https://github.com/mermaid-js/mermaid/commit/2b05d7e1edef635e6c80cb383b10ea0a89279f41) Thanks [@AaronMoat](https://github.com/AaronMoat)! - fix: Remove incorrect `style="undefined;"` attributes in some Mermaid diagrams
+
+- [#6282](https://github.com/mermaid-js/mermaid/pull/6282) [`d63d3bf`](https://github.com/mermaid-js/mermaid/commit/d63d3bf1e7596ac7eeb24ba06cbc7a70f9c8b070) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - FontAwesome icons can now be embedded as SVGs in flowcharts if they are registered via `mermaid.registerIconPacks`.
+
+- [#6407](https://github.com/mermaid-js/mermaid/pull/6407) [`cdbd3e5`](https://github.com/mermaid-js/mermaid/commit/cdbd3e58a3a35d63a79258115dedca4a535c1038) Thanks [@thomascizeron](https://github.com/thomascizeron)! - Refactor grammar so that title don't break Architecture Diagrams
+
+- [#6343](https://github.com/mermaid-js/mermaid/pull/6343) [`1ddaf10`](https://github.com/mermaid-js/mermaid/commit/1ddaf10b89d8c7311c5e10d466b42fa36b61210b) Thanks [@jeswr](https://github.com/jeswr)! - fix: allow colons in events
+
+- [#6616](https://github.com/mermaid-js/mermaid/pull/6616) [`ca80f71`](https://github.com/mermaid-js/mermaid/commit/ca80f719eac86cf4c31392105d5d896f39b84bbc) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix(timeline): ensure consistent vertical line lengths with visible arrowheads
+
+  Fixed timeline diagrams where vertical dashed lines from tasks had inconsistent lengths. All vertical lines now extend to the same depth regardless of the number of events in each column, with sufficient padding to clearly display both the dashed line pattern and complete arrowheads.
+
+- [#6566](https://github.com/mermaid-js/mermaid/pull/6566) [`bca6ed6`](https://github.com/mermaid-js/mermaid/commit/bca6ed67c3e0db910bf498fdd0fc0346c02d392b) Thanks [@arpitjain099](https://github.com/arpitjain099)! - fix: Fix incomplete string escaping in URL manipulation logic when `arrowMarkerAbsolute: true` by ensuring all unsafe characters are escaped.
+
+- Updated dependencies [[`df9df9d`](https://github.com/mermaid-js/mermaid/commit/df9df9dc32b80a8c320cc0efd5483b9485f15bde), [`cdbd3e5`](https://github.com/mermaid-js/mermaid/commit/cdbd3e58a3a35d63a79258115dedca4a535c1038)]:
+  - @mermaid-js/parser@0.5.0
+
 ## 11.6.0
 
 ### Minor Changes
diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json
index 7f8230229..80c83ec8e 100644
--- a/packages/mermaid/package.json
+++ b/packages/mermaid/package.json
@@ -1,6 +1,6 @@
 {
   "name": "mermaid",
-  "version": "11.6.0",
+  "version": "11.8.1",
   "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
   "type": "module",
   "module": "./dist/mermaid.core.mjs",
@@ -105,7 +105,7 @@
     "@types/stylis": "^4.2.7",
     "@types/uuid": "^10.0.0",
     "ajv": "^8.17.1",
-    "chokidar": "^4.0.3",
+    "chokidar": "3.6.0",
     "concurrently": "^9.1.2",
     "csstree-validator": "^4.0.1",
     "globby": "^14.0.2",
diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts
index cc2bdc432..8cd451c16 100644
--- a/packages/mermaid/src/config.type.ts
+++ b/packages/mermaid/src/config.type.ts
@@ -295,6 +295,12 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
    *
    */
   wrappingWidth?: number;
+  /**
+   * If true, subgraphs without explicit direction will inherit the global graph direction
+   * (e.g., LR, TB, RL, BT). Defaults to false to preserve legacy layout behavior.
+   *
+   */
+  inheritDir?: boolean;
 }
 /**
  * This interface was referenced by `MermaidConfig`'s JSON-Schema
diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js
index 5a97e5b63..f945caf94 100644
--- a/packages/mermaid/src/dagre-wrapper/edges.js
+++ b/packages/mermaid/src/dagre-wrapper/edges.js
@@ -4,7 +4,7 @@ import { createText } from '../rendering-util/createText.js';
 import { line, curveBasis, select } from 'd3';
 import { getConfig } from '../diagram-api/diagramAPI.js';
 import utils from '../utils.js';
-import { evaluate } from '../diagrams/common/common.js';
+import { evaluate, getUrl } from '../diagrams/common/common.js';
 import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js';
 import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
 import { addEdgeMarkers } from './edgeMarker.js';
@@ -440,14 +440,7 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
   let url = '';
   // // TODO: Can we load this config only from the rendered graph type?
   if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) {
-    url =
-      window.location.protocol +
-      '//' +
-      window.location.host +
-      window.location.pathname +
-      window.location.search;
-    url = url.replace(/\(/g, '\\(');
-    url = url.replace(/\)/g, '\\)');
+    url = getUrl(true);
   }
 
   addEdgeMarkers(svgPath, edge, url, id, diagramType);
diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts
index 2e4e20f50..468f8e192 100644
--- a/packages/mermaid/src/defaultConfig.ts
+++ b/packages/mermaid/src/defaultConfig.ts
@@ -71,6 +71,10 @@ const config: RequiredDeep = {
         fontWeight: this.personFontWeight,
       };
     },
+    flowchart: {
+      ...defaultConfigJson.flowchart,
+      inheritDir: false, // default to legacy behavior
+    },
 
     external_personFont: function () {
       return {
@@ -258,6 +262,18 @@ const config: RequiredDeep = {
   radar: {
     ...defaultConfigJson.radar,
   },
+  treemap: {
+    useMaxWidth: true,
+    padding: 10,
+    diagramPadding: 8,
+    showValues: true,
+    nodeWidth: 100,
+    nodeHeight: 40,
+    borderWidth: 1,
+    valueFontSize: 12,
+    labelFontSize: 14,
+    valueFormat: ',',
+  },
 };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
index 8f2b76abb..97b9852ff 100644
--- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts
+++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
@@ -27,6 +27,8 @@ import block from '../diagrams/block/blockDetector.js';
 import architecture from '../diagrams/architecture/architectureDetector.js';
 import { registerLazyLoadedDiagrams } from './detectType.js';
 import { registerDiagram } from './diagramAPI.js';
+import { treemap } from '../diagrams/treemap/detector.js';
+import '../type.d.ts';
 
 let hasLoadedDiagrams = false;
 export const addDiagrams = () => {
@@ -69,6 +71,11 @@ export const addDiagrams = () => {
       return text.toLowerCase().trimStart().startsWith('---');
     }
   );
+
+  if (includeLargeFeatures) {
+    registerLazyLoadedDiagrams(flowchartElk, mindmap, architecture);
+  }
+
   // Ordering of detectors is important. The first one to return true will be used.
   registerLazyLoadedDiagrams(
     c4,
@@ -81,10 +88,8 @@ export const addDiagrams = () => {
     pie,
     requirement,
     sequence,
-    flowchartElk,
     flowchartV2,
     flowchart,
-    mindmap,
     timeline,
     git,
     stateV2,
@@ -95,7 +100,7 @@ export const addDiagrams = () => {
     packet,
     xychart,
     block,
-    architecture,
-    radar
+    radar,
+    treemap
   );
 };
diff --git a/packages/mermaid/src/diagrams/class/svgDraw.js b/packages/mermaid/src/diagrams/class/svgDraw.js
index 73cf97aeb..ecea2b1de 100644
--- a/packages/mermaid/src/diagrams/class/svgDraw.js
+++ b/packages/mermaid/src/diagrams/class/svgDraw.js
@@ -1,7 +1,7 @@
 import { line, curveBasis } from 'd3';
 import utils from '../../utils.js';
 import { log } from '../../logger.js';
-import { parseGenericTypes } from '../common/common.js';
+import { parseGenericTypes, getUrl } from '../common/common.js';
 
 let edgeCount = 0;
 export const drawEdge = function (elem, path, relation, conf, diagObj) {
@@ -42,14 +42,7 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) {
     .attr('class', 'relation');
   let url = '';
   if (conf.arrowMarkerAbsolute) {
-    url =
-      window.location.protocol +
-      '//' +
-      window.location.host +
-      window.location.pathname +
-      window.location.search;
-    url = url.replace(/\(/g, '\\(');
-    url = url.replace(/\)/g, '\\)');
+    url = getUrl(true);
   }
 
   if (relation.relation.lineType == 1) {
diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts
index fd76d0a45..00c9b8313 100644
--- a/packages/mermaid/src/diagrams/common/common.ts
+++ b/packages/mermaid/src/diagrams/common/common.ts
@@ -149,7 +149,7 @@ const breakToPlaceholder = (s: string): string => {
  * @param useAbsolute - Whether to return the absolute URL or not
  * @returns The current URL
  */
-const getUrl = (useAbsolute: boolean): string => {
+export const getUrl = (useAbsolute: boolean): string => {
   let url = '';
   if (useAbsolute) {
     url =
@@ -158,8 +158,8 @@ const getUrl = (useAbsolute: boolean): string => {
       window.location.host +
       window.location.pathname +
       window.location.search;
-    url = url.replaceAll(/\(/g, '\\(');
-    url = url.replaceAll(/\)/g, '\\)');
+
+    url = CSS.escape(url);
   }
 
   return url;
@@ -341,29 +341,36 @@ export const renderKatex = async (text: string, config: MermaidConfig): Promise<
     return text.replace(katexRegex, 'MathML is unsupported in this environment.');
   }
 
-  const { default: katex } = await import('katex');
-  const outputMode =
-    config.forceLegacyMathML || (!isMathMLSupported() && config.legacyMathML)
-      ? 'htmlAndMathml'
-      : 'mathml';
-  return text
-    .split(lineBreakRegex)
-    .map((line) =>
-      hasKatex(line)
-        ? `
${line}
` - : `
${line}
` - ) - .join('') - .replace(katexRegex, (_, c) => - katex - .renderToString(c, { - throwOnError: true, - displayMode: true, - output: outputMode, - }) - .replace(/\n/g, ' ') - .replace(//g, '') - ); + if (includeLargeFeatures) { + const { default: katex } = await import('katex'); + const outputMode = + config.forceLegacyMathML || (!isMathMLSupported() && config.legacyMathML) + ? 'htmlAndMathml' + : 'mathml'; + return text + .split(lineBreakRegex) + .map((line) => + hasKatex(line) + ? `
${line}
` + : `
${line}
` + ) + .join('') + .replace(katexRegex, (_, c) => + katex + .renderToString(c, { + throwOnError: true, + displayMode: true, + output: outputMode, + }) + .replace(/\n/g, ' ') + .replace(//g, '') + ); + } + + return text.replace( + katexRegex, + 'Katex is not supported in @mermaid-js/tiny. Please use the full mermaid library.' + ); }; export default { diff --git a/packages/mermaid/src/diagrams/er/erRenderer.js b/packages/mermaid/src/diagrams/er/erRenderer.js index 0327bfc9d..a6f7745aa 100644 --- a/packages/mermaid/src/diagrams/er/erRenderer.js +++ b/packages/mermaid/src/diagrams/er/erRenderer.js @@ -6,7 +6,7 @@ import { log } from '../../logger.js'; import utils from '../../utils.js'; import erMarkers from './erMarkers.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; -import { parseGenericTypes } from '../common/common.js'; +import { parseGenericTypes, getUrl } from '../common/common.js'; import { v5 as uuid5 } from 'uuid'; /** Regex used to remove chars from the entity name so the result can be used in an id */ @@ -451,14 +451,7 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) { // TODO: Understand this better let url = ''; if (conf.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); + url = getUrl(true); } // Decide which start and end markers it needs. It may be possible to be more concise here diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts index 3eb1b13a7..65f8c4a05 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts @@ -651,7 +651,8 @@ You have to call mermaid.initialize.` const prims: any = { boolean: {}, number: {}, string: {} }; const objs: any[] = []; - let dir; // = undefined; direction.trim(); + let dir: string | undefined; + const nodeList = a.filter(function (item) { const type = typeof item; if (item.stmt && item.stmt === 'dir') { @@ -670,7 +671,16 @@ You have to call mermaid.initialize.` return { nodeList, dir }; }; - const { nodeList, dir } = uniq(list.flat()); + const result = uniq(list.flat()); + const nodeList = result.nodeList; + let dir = result.dir; + const flowchartConfig = getConfig().flowchart ?? {}; + dir = + dir ?? + (flowchartConfig.inheritDir + ? (this.getDirection() ?? (getConfig() as any).direction ?? undefined) + : undefined); + if (this.version === 'gen-1') { for (let i = 0; i < nodeList.length; i++) { nodeList[i] = this.lookUpDomId(nodeList[i]); @@ -681,6 +691,7 @@ You have to call mermaid.initialize.` title = title || ''; title = this.sanitizeText(title); this.subCount = this.subCount + 1; + const subGraph = { id: id, nodes: nodeList, diff --git a/packages/mermaid/src/diagrams/git/gitGraph.spec.ts b/packages/mermaid/src/diagrams/git/gitGraph.spec.ts index fed21dd19..ee20a9aaf 100644 --- a/packages/mermaid/src/diagrams/git/gitGraph.spec.ts +++ b/packages/mermaid/src/diagrams/git/gitGraph.spec.ts @@ -1,4 +1,4 @@ -import { rejects } from 'assert'; +import { log } from '../../logger.js'; import { db } from './gitGraphAst.js'; import { parser } from './gitGraphParser.js'; @@ -1319,4 +1319,42 @@ describe('when parsing a gitGraph', function () { } }); }); + it('should log a warning when two commits have the same ID', async () => { + const str = `gitGraph + commit id:"initial commit" + commit id:"work on first release" + commit id:"design freeze from here" + branch v1-rc + checkout v1-rc + commit id:"bugfix 1" + commit id:"bigfix 2" tag:"v1.0.1" + branch FORK-v1.0-MDR + checkout FORK-v1.0-MDR + commit id:"working on MDR" + checkout v1-rc + commit id:"minor design changes for MDR" tag:"v1.0.2" + checkout FORK-v1.0-MDR + merge v1-rc + checkout main + commit id:"new feature for v1.1…" + checkout FORK-v1.0-MDR + commit id:"working on MDR" + commit id:"finishing MDR" + branch v1.0-MDR + checkout v1.0-MDR + commit id:"brush up release" tag:"v1.0.2-MDR" + checkout v1-rc + commit id:"bugfix without MDR" + checkout main + commit id:"work on v1.1" + `; + + const logWarnSpy = vi.spyOn(log, 'warn').mockImplementation(() => undefined); + + await parser.parse(str); + + expect(logWarnSpy).toHaveBeenCalledWith('Commit ID working on MDR already exists'); + + logWarnSpy.mockRestore(); + }); }); diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 36595eb51..0dbc1ecb0 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -125,6 +125,9 @@ export const commit = function (commitDB: CommitDB) { }; state.records.head = newCommit; log.info('main branch', config.mainBranchName); + if (state.records.commits.has(newCommit.id)) { + log.warn(`Commit ID ${newCommit.id} already exists`); + } state.records.commits.set(newCommit.id, newCommit); state.records.branches.set(state.records.currBranch, newCommit.id); log.debug('in pushCommit ' + newCommit.id); diff --git a/packages/mermaid/src/diagrams/info/infoDb.ts b/packages/mermaid/src/diagrams/info/infoDb.ts index 5616a0ab9..169ceaf27 100644 --- a/packages/mermaid/src/diagrams/info/infoDb.ts +++ b/packages/mermaid/src/diagrams/info/infoDb.ts @@ -1,7 +1,9 @@ import type { InfoFields, InfoDB } from './infoTypes.js'; import packageJson from '../../../package.json' assert { type: 'json' }; -export const DEFAULT_INFO_DB: InfoFields = { version: packageJson.version } as const; +export const DEFAULT_INFO_DB: InfoFields = { + version: packageJson.version + (includeLargeFeatures ? '' : '-tiny'), +} as const; export const getVersion = (): string => DEFAULT_INFO_DB.version; diff --git a/packages/mermaid/src/diagrams/packet/packet.spec.ts b/packages/mermaid/src/diagrams/packet/packet.spec.ts index 2d7b278cd..bdd09acec 100644 --- a/packages/mermaid/src/diagrams/packet/packet.spec.ts +++ b/packages/mermaid/src/diagrams/packet/packet.spec.ts @@ -30,6 +30,7 @@ describe('packet diagrams', () => { [ [ { + "bits": 11, "end": 10, "label": "test", "start": 0, @@ -49,11 +50,13 @@ describe('packet diagrams', () => { [ [ { + "bits": 11, "end": 10, "label": "test", "start": 0, }, { + "bits": 1, "end": 11, "label": "single", "start": 11, @@ -63,6 +66,58 @@ describe('packet diagrams', () => { `); }); + it('should handle bit counts', async () => { + const str = `packet-beta + +8: "byte" + +16: "word" + `; + await expect(parser.parse(str)).resolves.not.toThrow(); + expect(getPacket()).toMatchInlineSnapshot(` + [ + [ + { + "bits": 8, + "end": 7, + "label": "byte", + "start": 0, + }, + { + "bits": 16, + "end": 23, + "label": "word", + "start": 8, + }, + ], + ] + `); + }); + + it('should handle bit counts with bit or bits', async () => { + const str = `packet-beta + +8: "byte" + +16: "word" + `; + await expect(parser.parse(str)).resolves.not.toThrow(); + expect(getPacket()).toMatchInlineSnapshot(` + [ + [ + { + "bits": 8, + "end": 7, + "label": "byte", + "start": 0, + }, + { + "bits": 16, + "end": 23, + "label": "word", + "start": 8, + }, + ], + ] + `); + }); + it('should split into multiple rows', async () => { const str = `packet-beta 0-10: "test" @@ -73,11 +128,13 @@ describe('packet diagrams', () => { [ [ { + "bits": 11, "end": 10, "label": "test", "start": 0, }, { + "bits": 20, "end": 31, "label": "multiple", "start": 11, @@ -85,6 +142,7 @@ describe('packet diagrams', () => { ], [ { + "bits": 31, "end": 63, "label": "multiple", "start": 32, @@ -92,6 +150,7 @@ describe('packet diagrams', () => { ], [ { + "bits": 26, "end": 90, "label": "multiple", "start": 64, @@ -111,11 +170,13 @@ describe('packet diagrams', () => { [ [ { + "bits": 17, "end": 16, "label": "test", "start": 0, }, { + "bits": 14, "end": 31, "label": "multiple", "start": 17, @@ -123,6 +184,7 @@ describe('packet diagrams', () => { ], [ { + "bits": 31, "end": 63, "label": "multiple", "start": 32, @@ -142,6 +204,16 @@ describe('packet diagrams', () => { ); }); + it('should throw error if numbers are not continuous with bit counts', async () => { + const str = `packet-beta + +16: "test" + 18-20: "error" + `; + await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Packet block 18 - 20 is not contiguous. It should start from 16.]` + ); + }); + it('should throw error if numbers are not continuous for single packets', async () => { const str = `packet-beta 0-16: "test" @@ -152,6 +224,16 @@ describe('packet diagrams', () => { ); }); + it('should throw error if numbers are not continuous for single packets with bit counts', async () => { + const str = `packet-beta + +16: "test" + 18: "error" + `; + await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Packet block 18 - 18 is not contiguous. It should start from 16.]` + ); + }); + it('should throw error if numbers are not continuous for single packets - 2', async () => { const str = `packet-beta 0-16: "test" @@ -172,4 +254,13 @@ describe('packet diagrams', () => { `[Error: Packet block 25 - 20 is invalid. End must be greater than start.]` ); }); + + it('should throw error if bit count is 0', async () => { + const str = `packet-beta + +0: "test" + `; + await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Packet block 0 is invalid. Cannot have a zero bit field.]` + ); + }); }); diff --git a/packages/mermaid/src/diagrams/packet/parser.ts b/packages/mermaid/src/diagrams/packet/parser.ts index 06d180dfd..1dcccd636 100644 --- a/packages/mermaid/src/diagrams/packet/parser.ts +++ b/packages/mermaid/src/diagrams/packet/parser.ts @@ -10,26 +10,33 @@ const maxPacketSize = 10_000; const populate = (ast: Packet) => { populateCommonDb(ast, db); - let lastByte = -1; + let lastBit = -1; let word: PacketWord = []; let row = 1; const { bitsPerRow } = db.getConfig(); - for (let { start, end, label } of ast.blocks) { - if (end && end < start) { + + for (let { start, end, bits, label } of ast.blocks) { + if (start !== undefined && end !== undefined && end < start) { throw new Error(`Packet block ${start} - ${end} is invalid. End must be greater than start.`); } - if (start !== lastByte + 1) { + start ??= lastBit + 1; + if (start !== lastBit + 1) { throw new Error( `Packet block ${start} - ${end ?? start} is not contiguous. It should start from ${ - lastByte + 1 + lastBit + 1 }.` ); } - lastByte = end ?? start; - log.debug(`Packet block ${start} - ${lastByte} with label ${label}`); + if (bits === 0) { + throw new Error(`Packet block ${start} is invalid. Cannot have a zero bit field.`); + } + end ??= start + (bits ?? 1) - 1; + bits ??= end - start + 1; + lastBit = end; + log.debug(`Packet block ${start} - ${lastBit} with label ${label}`); while (word.length <= bitsPerRow + 1 && db.getPacket().length < maxPacketSize) { - const [block, nextBlock] = getNextFittingBlock({ start, end, label }, row, bitsPerRow); + const [block, nextBlock] = getNextFittingBlock({ start, end, bits, label }, row, bitsPerRow); word.push(block); if (block.end + 1 === row * bitsPerRow) { db.pushWord(word); @@ -39,7 +46,7 @@ const populate = (ast: Packet) => { if (!nextBlock) { break; } - ({ start, end, label } = nextBlock); + ({ start, end, bits, label } = nextBlock); } } db.pushWord(word); @@ -50,8 +57,11 @@ const getNextFittingBlock = ( row: number, bitsPerRow: number ): [Required, PacketBlock | undefined] => { + if (block.start === undefined) { + throw new Error('start should have been set during first phase'); + } if (block.end === undefined) { - block.end = block.start; + throw new Error('end should have been set during first phase'); } if (block.start > block.end) { @@ -62,16 +72,20 @@ const getNextFittingBlock = ( return [block as Required, undefined]; } + const rowEnd = row * bitsPerRow - 1; + const rowStart = row * bitsPerRow; return [ { start: block.start, - end: row * bitsPerRow - 1, + end: rowEnd, label: block.label, + bits: rowEnd - block.start, }, { - start: row * bitsPerRow, + start: rowStart, end: block.end, label: block.label, + bits: block.end - rowStart, }, ]; }; diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 84bb15b15..cfba92b79 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -3,6 +3,7 @@ import { select } from 'd3'; import svgDraw, { drawKatex, ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js'; import { log } from '../../logger.js'; import common, { calculateMathMLDimensions, hasKatex } from '../common/common.js'; +import { getUrl } from '../common/common.js'; import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import assignWithDepth from '../../assignWithDepth.js'; @@ -449,14 +450,7 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO let url = ''; if (conf.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); + url = getUrl(true); } line.attr('stroke-width', 2); diff --git a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison index bfaf5a62a..ffc086157 100644 --- a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison +++ b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison @@ -46,6 +46,10 @@ %% +"click" return 'CLICK'; +"href" return 'HREF'; +\"[^"]*\" return 'STRING'; + "default" return 'DEFAULT'; .*direction\s+TB[^\n]* return 'direction_tb'; @@ -246,8 +250,26 @@ statement | direction | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } - | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } ; - + | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } + | CLICK idStatement STRING STRING NL + { + $$ = { + stmt: "click", + id: $2, + url: $3, + tooltip: $4 + }; + } + | CLICK idStatement HREF STRING NL + { + $$ = { + stmt: "click", + id: $2, + url: $4, + tooltip: "" + }; + } + ; classDefStatement : classDef CLASSDEF_ID CLASSDEF_STYLEOPTS { diff --git a/packages/mermaid/src/diagrams/state/shapes.js b/packages/mermaid/src/diagrams/state/shapes.js index 419d2a76e..f9b34506f 100644 --- a/packages/mermaid/src/diagrams/state/shapes.js +++ b/packages/mermaid/src/diagrams/state/shapes.js @@ -1,7 +1,7 @@ import { line, curveBasis } from 'd3'; import { StateDB } from './stateDb.js'; import utils from '../../utils.js'; -import common from '../common/common.js'; +import common, { getUrl } from '../common/common.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import { log } from '../../logger.js'; @@ -444,14 +444,7 @@ export const drawEdge = function (elem, path, relation) { .attr('class', 'transition'); let url = ''; if (getConfig().state.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); + url = getUrl(true); } svgPath.attr( diff --git a/packages/mermaid/src/diagrams/state/stateDb.ts b/packages/mermaid/src/diagrams/state/stateDb.ts index 853a0e22f..e40b06a9c 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.ts +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -39,7 +39,16 @@ const CONSTANTS = { } as const; interface BaseStmt { - stmt: 'applyClass' | 'classDef' | 'dir' | 'relation' | 'state' | 'style' | 'root' | 'default'; + stmt: + | 'applyClass' + | 'classDef' + | 'dir' + | 'relation' + | 'state' + | 'style' + | 'root' + | 'default' + | 'click'; } interface ApplyClassStmt extends BaseStmt { @@ -92,6 +101,13 @@ export interface RootStmt { doc?: Stmt[]; } +export interface ClickStmt extends BaseStmt { + stmt: 'click'; + id: string; + url: string; + tooltip: string; +} + interface Note { position?: 'left of' | 'right of'; text: string; @@ -104,7 +120,8 @@ export type Stmt = | RelationStmt | StateStmt | StyleStmt - | RootStmt; + | RootStmt + | ClickStmt; interface DiagramEdge { id1: string; @@ -185,6 +202,7 @@ export class StateDB { private currentDocument = this.documents.root; private startEndCount = 0; private dividerCnt = 0; + private links = new Map(); static readonly relationType = { AGGREGATION: 0, @@ -230,6 +248,9 @@ export class StateDB { case STMT_APPLYCLASS: this.setCssClass(item.id.trim(), item.styleClass); break; + case 'click': + this.addLink(item.id, item.url, item.tooltip); + break; } } const diagramStates = this.getStates(); @@ -438,6 +459,7 @@ export class StateDB { this.startEndCount = 0; this.classes = newClassesList(); if (!saveCommon) { + this.links = new Map(); // <-- add here commonClear(); } } @@ -458,6 +480,21 @@ export class StateDB { return this.currentDocument.relations; } + /** + * Adds a clickable link to a state. + */ + addLink(stateId: string, url: string, tooltip: string): void { + this.links.set(stateId, { url, tooltip }); + log.warn('Adding link', stateId, url, tooltip); + } + + /** + * Get all registered links. + */ + getLinks(): Map { + return this.links; + } + /** * If the id is a start node ( [*] ), then return a new id constructed from * the start node name and the current start node count. diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js index 362c86ccd..c33ed51d7 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js @@ -400,4 +400,30 @@ describe('state diagram, ', function () { parser.parse(str); }); }); + describe('click directive', function () { + let stateDb; + beforeEach(function () { + stateDb = new StateDB(1); + }); + + it('should store links from click statements manually passed to extract()', function () { + const clickStmt = { + stmt: 'click', + id: 'S1', + url: 'https://example.com', + tooltip: 'Go to Example', + }; + + // Add state explicitly + stateDb.addState('S1'); + // Simulate parser output + stateDb.extract([clickStmt]); + + const links = stateDb.getLinks(); + expect(links.has('S1')).toBe(true); + const link = links.get('S1'); + expect(link.url).toBe('https://example.com'); + expect(link.tooltip).toBe('Go to Example'); + }); + }); }); diff --git a/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts index 2998c8173..4fcd69706 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts @@ -68,6 +68,61 @@ export const draw = async function (text: string, id: string, _version: string, // console.log('REF1:', data4Layout); await render(data4Layout, svg); const padding = 8; + + // Inject clickable links after nodes are rendered + try { + const links: Map = + typeof diag.db.getLinks === 'function' ? diag.db.getLinks() : new Map(); + + type StateKey = string | { id: string }; + + links.forEach((linkInfo, key: StateKey) => { + const stateId = typeof key === 'string' ? key : typeof key?.id === 'string' ? key.id : ''; + + if (!stateId) { + log.warn('⚠️ Invalid or missing stateId from key:', JSON.stringify(key)); + return; + } + + const allNodes = svg.node()?.querySelectorAll('g'); + let matchedElem: SVGGElement | undefined; + + allNodes?.forEach((g: SVGGElement) => { + const text = g.textContent?.trim(); + if (text === stateId) { + matchedElem = g; + } + }); + + if (!matchedElem) { + log.warn('⚠️ Could not find node matching text:', stateId); + return; + } + + const parent = matchedElem.parentNode; + if (!parent) { + log.warn('⚠️ Node has no parent, cannot wrap:', stateId); + return; + } + + const a = document.createElementNS('http://www.w3.org/2000/svg', 'a'); + const cleanedUrl = linkInfo.url.replace(/^"+|"+$/g, ''); // remove leading/trailing quotes + a.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', cleanedUrl); + a.setAttribute('target', '_blank'); + if (linkInfo.tooltip) { + const tooltip = linkInfo.tooltip.replace(/^"+|"+$/g, ''); + a.setAttribute('title', tooltip); + } + + parent.replaceChild(a, matchedElem); + a.appendChild(matchedElem); + + log.info('🔗 Wrapped node in tag for:', stateId, linkInfo.url); + }); + } catch (err) { + log.error('❌ Error injecting clickable links:', err); + } + utils.insertTitle( svg, 'statediagramTitleText', diff --git a/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts b/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts index b3405bc1b..7f406b589 100644 --- a/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts +++ b/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts @@ -126,6 +126,10 @@ export const draw = function (text: string, id: string, version: string, diagObj }; maxEventLineLengthTemp += svgDraw.getVirtualNodeHeight(svg, eventNode, conf); } + // Add spacing between events (10px per event except the last one) + if (task.events.length > 0) { + maxEventLineLengthTemp += (task.events.length - 1) * 10; + } maxEventLineLength = Math.max(maxEventLineLength, maxEventLineLengthTemp); } @@ -285,16 +289,9 @@ export const drawTasks = function ( lineWrapper .append('line') .attr('x1', masterX + 190 / 2) - .attr('y1', masterY + maxTaskHeight) // One section head + one task + margins - .attr('x2', masterX + 190 / 2) // Subtract stroke width so arrow point is retained - .attr( - 'y2', - masterY + - maxTaskHeight + - (isWithoutSections ? maxTaskHeight : maxSectionHeight) + - maxEventLineLength + - 120 - ) + .attr('y1', masterY + maxTaskHeight) // Start from bottom of task box + .attr('x2', masterX + 190 / 2) // Same x coordinate for vertical line + .attr('y2', masterY + maxTaskHeight + 100 + maxEventLineLength + 100) // End at consistent depth with ample padding for visible dashed lines and arrowheads .attr('stroke-width', 2) .attr('stroke', 'black') .attr('marker-end', 'url(#arrowhead)') diff --git a/packages/mermaid/src/diagrams/treemap/db.ts b/packages/mermaid/src/diagrams/treemap/db.ts new file mode 100644 index 000000000..6a68857f7 --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/db.ts @@ -0,0 +1,112 @@ +import { getConfig as commonGetConfig } from '../../config.js'; +import DEFAULT_CONFIG from '../../defaultConfig.js'; +import type { DiagramStyleClassDef } from '../../diagram-api/types.js'; +import { isLabelStyle } from '../../rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js'; + +import { cleanAndMerge } from '../../utils.js'; +import { ImperativeState } from '../../utils/imperativeState.js'; +import { + clear as commonClear, + getAccDescription, + getAccTitle, + getDiagramTitle, + setAccDescription, + setAccTitle, + setDiagramTitle, +} from '../common/commonDb.js'; +import type { TreemapDB, TreemapData, TreemapDiagramConfig, TreemapNode } from './types.js'; + +const defaultTreemapData: TreemapData = { + nodes: [], + levels: new Map(), + outerNodes: [], + classes: new Map(), +}; + +const state = new ImperativeState(() => structuredClone(defaultTreemapData)); + +const getConfig = (): Required => { + // Use type assertion with unknown as intermediate step + const defaultConfig = DEFAULT_CONFIG as unknown as { treemap: Required }; + const userConfig = commonGetConfig() as unknown as { treemap?: Partial }; + + return cleanAndMerge({ + ...defaultConfig.treemap, + ...(userConfig.treemap ?? {}), + }) as Required; +}; + +const getNodes = (): TreemapNode[] => state.records.nodes; + +const addNode = (node: TreemapNode, level: number) => { + const data = state.records; + data.nodes.push(node); + data.levels.set(node, level); + + if (level === 0) { + data.outerNodes.push(node); + } + + // Set the root node if this is a level 0 node and we don't have a root yet + if (level === 0 && !data.root) { + data.root = node; + } +}; + +const getRoot = (): TreemapNode | undefined => ({ name: '', children: state.records.outerNodes }); + +const addClass = (id: string, _style: string) => { + const classes = state.records.classes; + const styleClass = classes.get(id) ?? { id, styles: [], textStyles: [] }; + classes.set(id, styleClass); + + const styles = _style.replace(/\\,/g, '§§§').replace(/,/g, ';').replace(/§§§/g, ',').split(';'); + + if (styles) { + styles.forEach((s) => { + if (isLabelStyle(s)) { + if (styleClass?.textStyles) { + styleClass.textStyles.push(s); + } else { + styleClass.textStyles = [s]; + } + } + if (styleClass?.styles) { + styleClass.styles.push(s); + } else { + styleClass.styles = [s]; + } + }); + } + + classes.set(id, styleClass); +}; +const getClasses = (): Map => { + return state.records.classes; +}; + +const getStylesForClass = (classSelector: string): string[] => { + return state.records.classes.get(classSelector)?.styles ?? []; +}; + +const clear = () => { + commonClear(); + state.reset(); +}; + +export const db: TreemapDB = { + getNodes, + addNode, + getRoot, + getConfig, + clear, + setAccTitle, + getAccTitle, + setDiagramTitle, + getDiagramTitle, + getAccDescription, + setAccDescription, + addClass, + getClasses, + getStylesForClass, +}; diff --git a/packages/mermaid/src/diagrams/treemap/detector.ts b/packages/mermaid/src/diagrams/treemap/detector.ts new file mode 100644 index 000000000..3b050f660 --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/detector.ts @@ -0,0 +1,22 @@ +import type { + DiagramDetector, + DiagramLoader, + ExternalDiagramDefinition, +} from '../../diagram-api/types.js'; + +const id = 'treemap'; + +const detector: DiagramDetector = (txt) => { + return /^\s*treemap/.test(txt); +}; + +const loader: DiagramLoader = async () => { + const { diagram } = await import('./diagram.js'); + return { id, diagram }; +}; + +export const treemap: ExternalDiagramDefinition = { + id, + detector, + loader, +}; diff --git a/packages/mermaid/src/diagrams/treemap/diagram.ts b/packages/mermaid/src/diagrams/treemap/diagram.ts new file mode 100644 index 000000000..dd599174e --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/diagram.ts @@ -0,0 +1,12 @@ +import type { DiagramDefinition } from '../../diagram-api/types.js'; +import { db } from './db.js'; +import { parser } from './parser.js'; +import { renderer } from './renderer.js'; +import styles from './styles.js'; + +export const diagram: DiagramDefinition = { + parser, + db, + renderer, + styles, +}; diff --git a/packages/mermaid/src/diagrams/treemap/parser.ts b/packages/mermaid/src/diagrams/treemap/parser.ts new file mode 100644 index 000000000..82efb5911 --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/parser.ts @@ -0,0 +1,100 @@ +import { parse } from '@mermaid-js/parser'; +import type { ParserDefinition } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; +import { populateCommonDb } from '../common/populateCommonDb.js'; +import { db } from './db.js'; +import type { TreemapNode, TreemapAst } from './types.js'; +import { buildHierarchy } from './utils.js'; + +/** + * Populates the database with data from the Treemap AST + * @param ast - The Treemap AST + */ +const populate = (ast: TreemapAst) => { + // We need to bypass the type checking for populateCommonDb + // eslint-disable-next-line @typescript-eslint/no-explicit-any + populateCommonDb(ast as any, db); + + const items: { + level: number; + name: string; + type: string; + value?: number; + classSelector?: string; + cssCompiledStyles?: string; + }[] = []; + + // Extract classes and styles from the treemap + for (const row of ast.TreemapRows ?? []) { + if (row.$type === 'ClassDefStatement') { + db.addClass(row.className ?? '', row.styleText ?? ''); + } + } + + // Extract data from each row in the treemap + for (const row of ast.TreemapRows ?? []) { + const item = row.item; + + if (!item) { + continue; + } + + const level = row.indent ? parseInt(row.indent) : 0; + const name = getItemName(item); + + // Get styles as a string if they exist + const styles = item.classSelector ? db.getStylesForClass(item.classSelector) : []; + const cssCompiledStyles = styles.length > 0 ? styles.join(';') : undefined; + + const itemData = { + level, + name, + type: item.$type, + value: item.value, + classSelector: item.classSelector, + cssCompiledStyles, + }; + + items.push(itemData); + } + + // Convert flat structure to hierarchical + const hierarchyNodes = buildHierarchy(items); + + // Add all nodes to the database + const addNodesRecursively = (nodes: TreemapNode[], level: number) => { + for (const node of nodes) { + db.addNode(node, level); + if (node.children && node.children.length > 0) { + addNodesRecursively(node.children, level + 1); + } + } + }; + + addNodesRecursively(hierarchyNodes, 0); +}; + +/** + * Gets the name of a treemap item + * @param item - The treemap item + * @returns The name of the item + */ +const getItemName = (item: { name?: string | number }): string => { + return item.name ? String(item.name) : ''; +}; + +export const parser: ParserDefinition = { + parse: async (text: string): Promise => { + try { + // Use a generic parse that accepts any diagram type + + const parseFunc = parse as (diagramType: string, text: string) => Promise; + const ast = await parseFunc('treemap', text); + log.debug('Treemap AST:', ast); + populate(ast); + } catch (error) { + log.error('Error parsing treemap:', error); + throw error; + } + }, +}; diff --git a/packages/mermaid/src/diagrams/treemap/renderer.ts b/packages/mermaid/src/diagrams/treemap/renderer.ts new file mode 100644 index 000000000..6c5f8efd2 --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/renderer.ts @@ -0,0 +1,526 @@ +import type { Diagram } from '../../Diagram.js'; +import type { + DiagramRenderer, + DiagramStyleClassDef, + DrawDefinition, +} from '../../diagram-api/types.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; +import type { TreemapDB, TreemapNode } from './types.js'; +import { scaleOrdinal, treemap, hierarchy, format, select } from 'd3'; +import { styles2String } from '../../rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js'; +import { getConfig } from '../../config.js'; +import { log } from '../../logger.js'; +import type { Node } from '../../rendering-util/types.js'; + +const DEFAULT_INNER_PADDING = 10; // Default for inner padding between cells/sections +const SECTION_INNER_PADDING = 10; // Default for inner padding between cells/sections +const SECTION_HEADER_HEIGHT = 25; + +/** + * Draws the treemap diagram + */ +const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { + const treemapDb = diagram.db as TreemapDB; + const config = treemapDb.getConfig(); + const treemapInnerPadding = config.padding ?? DEFAULT_INNER_PADDING; + const title = treemapDb.getDiagramTitle(); + const root = treemapDb.getRoot(); + const { themeVariables } = getConfig(); + if (!root) { + return; + } + + // Define dimensions + const titleHeight = title ? 30 : 0; + + const svg = selectSvgElement(id); + // Use config dimensions or defaults + const width = config.nodeWidth ? config.nodeWidth * SECTION_INNER_PADDING : 960; + const height = config.nodeHeight ? config.nodeHeight * SECTION_INNER_PADDING : 500; + + const svgWidth = width; + const svgHeight = height + titleHeight; + + // Set the SVG size + svg.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`); + configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth); + + // Format for displaying values + let valueFormat; + try { + // Handle special format patterns + const formatStr = config.valueFormat || ','; + + // Handle special cases that aren't directly supported by D3 format + if (formatStr === '$0,0') { + // Currency with thousands separator + valueFormat = (value: number) => '$' + format(',')(value); + } else if (formatStr.startsWith('$') && formatStr.includes(',')) { + // Other dollar formats with commas + const precision = /\.\d+/.exec(formatStr); + const precisionStr = precision ? precision[0] : ''; + valueFormat = (value: number) => '$' + format(',' + precisionStr)(value); + } else if (formatStr.startsWith('$')) { + // Simple dollar sign prefix + const restOfFormat = formatStr.substring(1); + valueFormat = (value: number) => '$' + format(restOfFormat || '')(value); + } else { + // Standard D3 format + valueFormat = format(formatStr); + } + } catch (error) { + log.error('Error creating format function:', error); + // Fallback to default format + valueFormat = format(','); + } + + // Create color scale + const colorScale = scaleOrdinal().range([ + 'transparent', + themeVariables.cScale0, + themeVariables.cScale1, + themeVariables.cScale2, + themeVariables.cScale3, + themeVariables.cScale4, + themeVariables.cScale5, + themeVariables.cScale6, + themeVariables.cScale7, + themeVariables.cScale8, + themeVariables.cScale9, + themeVariables.cScale10, + themeVariables.cScale11, + ]); + const colorScalePeer = scaleOrdinal().range([ + 'transparent', + themeVariables.cScalePeer0, + themeVariables.cScalePeer1, + themeVariables.cScalePeer2, + themeVariables.cScalePeer3, + themeVariables.cScalePeer4, + themeVariables.cScalePeer5, + themeVariables.cScalePeer6, + themeVariables.cScalePeer7, + themeVariables.cScalePeer8, + themeVariables.cScalePeer9, + themeVariables.cScalePeer10, + themeVariables.cScalePeer11, + ]); + const colorScaleLabel = scaleOrdinal().range([ + themeVariables.cScaleLabel0, + themeVariables.cScaleLabel1, + themeVariables.cScaleLabel2, + themeVariables.cScaleLabel3, + themeVariables.cScaleLabel4, + themeVariables.cScaleLabel5, + themeVariables.cScaleLabel6, + themeVariables.cScaleLabel7, + themeVariables.cScaleLabel8, + themeVariables.cScaleLabel9, + themeVariables.cScaleLabel10, + themeVariables.cScaleLabel11, + ]); + + // Draw the title if it exists + if (title) { + svg + .append('text') + .attr('x', svgWidth / 2) + .attr('y', titleHeight / 2) + .attr('class', 'treemapTitle') + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .text(title); + } + + // Create a main container for the treemap, translated below the title + const g = svg + .append('g') + .attr('transform', `translate(0, ${titleHeight})`) + .attr('class', 'treemapContainer'); + + // Create the hierarchical structure + const hierarchyRoot = hierarchy(root) + .sum((d) => d.value ?? 0) + .sort((a, b) => (b.value ?? 0) - (a.value ?? 0)); + + // Create treemap layout + const treemapLayout = treemap() + .size([width, height]) + .paddingTop((d) => + d.children && d.children.length > 0 ? SECTION_HEADER_HEIGHT + SECTION_INNER_PADDING : 0 + ) + .paddingInner(treemapInnerPadding) + .paddingLeft((d) => (d.children && d.children.length > 0 ? SECTION_INNER_PADDING : 0)) + .paddingRight((d) => (d.children && d.children.length > 0 ? SECTION_INNER_PADDING : 0)) + .paddingBottom((d) => (d.children && d.children.length > 0 ? SECTION_INNER_PADDING : 0)) + .round(true); + + // Apply the treemap layout to the hierarchy + const treemapData = treemapLayout(hierarchyRoot); + + // Draw section nodes (branches - nodes with children) + const branchNodes = treemapData.descendants().filter((d) => d.children && d.children.length > 0); + const sections = g + .selectAll('.treemapSection') + .data(branchNodes) + .enter() + .append('g') + .attr('class', 'treemapSection') + .attr('transform', (d) => `translate(${d.x0},${d.y0})`); + + // Add section header background + sections + .append('rect') + .attr('width', (d) => d.x1 - d.x0) + .attr('height', SECTION_HEADER_HEIGHT) + .attr('class', 'treemapSectionHeader') + .attr('fill', 'none') + .attr('fill-opacity', 0.6) + .attr('stroke-width', 0.6) + .attr('style', (d) => { + // Hide the label for the root section + if (d.depth === 0) { + return 'display: none;'; + } + return ''; + }); + + // Add clip paths for section headers to prevent text overflow + sections + .append('clipPath') + .attr('id', (_d, i) => `clip-section-${id}-${i}`) + .append('rect') + .attr('width', (d) => Math.max(0, d.x1 - d.x0 - 12)) // 6px padding on each side + .attr('height', SECTION_HEADER_HEIGHT); + + sections + .append('rect') + .attr('width', (d) => d.x1 - d.x0) + .attr('height', (d) => d.y1 - d.y0) + .attr('class', (_d, i) => { + return `treemapSection section${i}`; + }) + .attr('fill', (d) => colorScale(d.data.name)) + .attr('fill-opacity', 0.6) + .attr('stroke', (d) => colorScalePeer(d.data.name)) + .attr('stroke-width', 2.0) + .attr('stroke-opacity', 0.4) + .attr('style', (d) => { + // Hide the label for the root section + if (d.depth === 0) { + return 'display: none;'; + } + const styles = styles2String({ cssCompiledStyles: d.data.cssCompiledStyles } as Node); + return styles.nodeStyles + ';' + styles.borderStyles.join(';'); + }); + // Add section labels + sections + .append('text') + .attr('class', 'treemapSectionLabel') + .attr('x', 6) // Keep original left padding + .attr('y', SECTION_HEADER_HEIGHT / 2) + .attr('dominant-baseline', 'middle') + .text((d) => (d.depth === 0 ? '' : d.data.name)) // Skip label for root section + .attr('font-weight', 'bold') + .attr('style', (d) => { + // Hide the label for the root section + if (d.depth === 0) { + return 'display: none;'; + } + const labelStyles = + 'dominant-baseline: middle; font-size: 12px; fill:' + + colorScaleLabel(d.data.name) + + '; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;'; + const styles = styles2String({ cssCompiledStyles: d.data.cssCompiledStyles } as Node); + return labelStyles + styles.labelStyles.replace('color:', 'fill:'); + }) + .each(function (d) { + // Skip processing for root section + if (d.depth === 0) { + return; + } + const self = select(this); + const originalText = d.data.name; + self.text(originalText); + const totalHeaderWidth = d.x1 - d.x0; + const labelXPosition = 6; + let spaceForTextContent; + if (config.showValues !== false && d.value) { + const valueEndsAtXRelative = totalHeaderWidth - 10; + const estimatedValueTextActualWidth = 30; + const gapBetweenLabelAndValue = 10; + const labelMustEndBeforeX = + valueEndsAtXRelative - estimatedValueTextActualWidth - gapBetweenLabelAndValue; + spaceForTextContent = labelMustEndBeforeX - labelXPosition; + } else { + const labelOwnRightPadding = 6; + spaceForTextContent = totalHeaderWidth - labelXPosition - labelOwnRightPadding; + } + const minimumWidthToDisplay = 15; + const actualAvailableWidth = Math.max(minimumWidthToDisplay, spaceForTextContent); + const textNode = self.node()!; + const currentTextContentLength = textNode.getComputedTextLength(); + if (currentTextContentLength > actualAvailableWidth) { + const ellipsis = '...'; + let currentTruncatedText = originalText; + while (currentTruncatedText.length > 0) { + currentTruncatedText = originalText.substring(0, currentTruncatedText.length - 1); + if (currentTruncatedText.length === 0) { + self.text(ellipsis); + if (textNode.getComputedTextLength() > actualAvailableWidth) { + self.text(''); + } + break; + } + self.text(currentTruncatedText + ellipsis); + if (textNode.getComputedTextLength() <= actualAvailableWidth) { + break; + } + } + } + }); + + // Add section values if enabled + if (config.showValues !== false) { + sections + .append('text') + .attr('class', 'treemapSectionValue') + .attr('x', (d) => d.x1 - d.x0 - 10) + .attr('y', SECTION_HEADER_HEIGHT / 2) + .attr('text-anchor', 'end') + .attr('dominant-baseline', 'middle') + .text((d) => (d.value ? valueFormat(d.value) : '')) + .attr('font-style', 'italic') + .attr('style', (d) => { + // Hide the value for the root section + if (d.depth === 0) { + return 'display: none;'; + } + const labelStyles = + 'text-anchor: end; dominant-baseline: middle; font-size: 10px; fill:' + + colorScaleLabel(d.data.name) + + '; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;'; + const styles = styles2String({ cssCompiledStyles: d.data.cssCompiledStyles } as Node); + return labelStyles + styles.labelStyles.replace('color:', 'fill:'); + }); + } + + // Draw the leaf nodes + const leafNodes = treemapData.leaves(); + const cell = g + .selectAll('.treemapLeafGroup') + .data(leafNodes) + .enter() + .append('g') + .attr('class', (d, i) => { + return `treemapNode treemapLeafGroup leaf${i}${d.data.classSelector ? ` ${d.data.classSelector}` : ''}x`; + }) + .attr('transform', (d) => `translate(${d.x0},${d.y0})`); + + // Add rectangle for each leaf node + cell + .append('rect') + .attr('width', (d) => d.x1 - d.x0) + .attr('height', (d) => d.y1 - d.y0) + .attr('class', 'treemapLeaf') + .attr('fill', (d) => { + // Leaves inherit color from their immediate parent section's name. + // If a leaf is the root itself (no parent), it uses its own name. + return d.parent ? colorScale(d.parent.data.name) : colorScale(d.data.name); + }) + .attr('style', (d) => { + const styles = styles2String({ cssCompiledStyles: d.data.cssCompiledStyles } as Node); + return styles.nodeStyles; + }) + .attr('fill-opacity', 0.3) + .attr('stroke', (d) => { + // Leaves inherit color from their immediate parent section's name. + // If a leaf is the root itself (no parent), it uses its own name. + return d.parent ? colorScale(d.parent.data.name) : colorScale(d.data.name); + }) + .attr('stroke-width', 3.0); + + // Add clip paths to prevent text from extending outside nodes + cell + .append('clipPath') + .attr('id', (_d, i) => `clip-${id}-${i}`) + .append('rect') + .attr('width', (d) => Math.max(0, d.x1 - d.x0 - 4)) + .attr('height', (d) => Math.max(0, d.y1 - d.y0 - 4)); + + // Add node labels with clipping + const leafLabels = cell + .append('text') + .attr('class', 'treemapLabel') + .attr('x', (d) => (d.x1 - d.x0) / 2) + .attr('y', (d) => (d.y1 - d.y0) / 2) + // .style('fill', (d) => colorScaleLabel(d.data.name)) + .attr('style', (d) => { + const labelStyles = + 'text-anchor: middle; dominant-baseline: middle; font-size: 38px;fill:' + + colorScaleLabel(d.data.name) + + ';'; + const styles = styles2String({ cssCompiledStyles: d.data.cssCompiledStyles } as Node); + return labelStyles + styles.labelStyles.replace('color:', 'fill:'); + }) + .attr('clip-path', (_d, i) => `url(#clip-${id}-${i})`) + .text((d) => d.data.name); + + leafLabels.each(function (d) { + const self = select(this); + const nodeWidth = d.x1 - d.x0; + const nodeHeight = d.y1 - d.y0; + const textNode = self.node()!; + + const padding = 4; + const availableWidth = nodeWidth - 2 * padding; + const availableHeight = nodeHeight - 2 * padding; + + if (availableWidth < 10 || availableHeight < 10) { + self.style('display', 'none'); + return; + } + + let currentLabelFontSize = parseInt(self.style('font-size'), 10); + const minLabelFontSize = 8; + const originalValueRelFontSize = 28; // Original font size of value, for max cap + const valueScaleFactor = 0.6; // Value font size as a factor of label font size + const minValueFontSize = 6; + const spacingBetweenLabelAndValue = 2; + + // 1. Adjust label font size to fit width + while ( + textNode.getComputedTextLength() > availableWidth && + currentLabelFontSize > minLabelFontSize + ) { + currentLabelFontSize--; + self.style('font-size', `${currentLabelFontSize}px`); + } + + // 2. Adjust both label and prospective value font size to fit combined height + let prospectiveValueFontSize = Math.max( + minValueFontSize, + Math.min(originalValueRelFontSize, Math.round(currentLabelFontSize * valueScaleFactor)) + ); + let combinedHeight = + currentLabelFontSize + spacingBetweenLabelAndValue + prospectiveValueFontSize; + + while (combinedHeight > availableHeight && currentLabelFontSize > minLabelFontSize) { + currentLabelFontSize--; + prospectiveValueFontSize = Math.max( + minValueFontSize, + Math.min(originalValueRelFontSize, Math.round(currentLabelFontSize * valueScaleFactor)) + ); + if ( + prospectiveValueFontSize < minValueFontSize && + currentLabelFontSize === minLabelFontSize + ) { + break; + } // Avoid shrinking label if value is already at min + self.style('font-size', `${currentLabelFontSize}px`); + combinedHeight = + currentLabelFontSize + spacingBetweenLabelAndValue + prospectiveValueFontSize; + if (prospectiveValueFontSize <= minValueFontSize && combinedHeight > availableHeight) { + // If value is at min and still doesn't fit, label might need to shrink more alone + // This might lead to label being too small for its own text, checked next + } + } + + // Update label font size based on height adjustment + self.style('font-size', `${currentLabelFontSize}px`); + + // 3. Final visibility check for the label + if ( + textNode.getComputedTextLength() > availableWidth || + currentLabelFontSize < minLabelFontSize || + availableHeight < currentLabelFontSize + ) { + self.style('display', 'none'); + // If label is hidden, value will be hidden by its own .each() loop + } + }); + + // Add node values with clipping + if (config.showValues !== false) { + const leafValues = cell + .append('text') + .attr('class', 'treemapValue') + .attr('x', (d) => (d.x1 - d.x0) / 2) + .attr('y', function (d) { + // Y position calculated dynamically in leafValues.each based on final label metrics + return (d.y1 - d.y0) / 2; // Placeholder, will be overwritten + }) + .attr('style', (d) => { + const labelStyles = + 'text-anchor: middle; dominant-baseline: hanging; font-size: 28px;fill:' + + colorScaleLabel(d.data.name) + + ';'; + const styles = styles2String({ cssCompiledStyles: d.data.cssCompiledStyles } as Node); + return labelStyles + styles.labelStyles.replace('color:', 'fill:'); + }) + + .attr('clip-path', (_d, i) => `url(#clip-${id}-${i})`) + .text((d) => (d.value ? valueFormat(d.value) : '')); + + leafValues.each(function (d) { + const valueTextElement = select(this); + const parentCellNode = this.parentNode as SVGGElement | null; + + if (!parentCellNode) { + valueTextElement.style('display', 'none'); + return; + } + + const labelElement = select(parentCellNode).select('.treemapLabel'); + + if (labelElement.empty() || labelElement.style('display') === 'none') { + valueTextElement.style('display', 'none'); + return; + } + + const finalLabelFontSize = parseFloat(labelElement.style('font-size')); + const originalValueFontSize = 28; // From initial style setting + const valueScaleFactor = 0.6; + const minValueFontSize = 6; + const spacingBetweenLabelAndValue = 2; + + const actualValueFontSize = Math.max( + minValueFontSize, + Math.min(originalValueFontSize, Math.round(finalLabelFontSize * valueScaleFactor)) + ); + valueTextElement.style('font-size', `${actualValueFontSize}px`); + + const labelCenterY = (d.y1 - d.y0) / 2; + const valueTopActualY = labelCenterY + finalLabelFontSize / 2 + spacingBetweenLabelAndValue; + valueTextElement.attr('y', valueTopActualY); + + const nodeWidth = d.x1 - d.x0; + const nodeTotalHeight = d.y1 - d.y0; + const cellBottomPadding = 4; + const maxValueBottomY = nodeTotalHeight - cellBottomPadding; + const availableWidthForValue = nodeWidth - 2 * 4; // padding for value text + + if ( + valueTextElement.node()!.getComputedTextLength() > availableWidthForValue || + valueTopActualY + actualValueFontSize > maxValueBottomY || + actualValueFontSize < minValueFontSize + ) { + valueTextElement.style('display', 'none'); + } else { + valueTextElement.style('display', null); + } + }); + } + const diagramPadding = config.diagramPadding ?? 8; + setupViewPortForSVG(svg, diagramPadding, 'flowchart', config?.useMaxWidth || false); +}; + +const getClasses = function ( + _text: string, + diagramObj: Pick +): Map { + return (diagramObj.db as TreemapDB).getClasses(); +}; +export const renderer: DiagramRenderer = { draw, getClasses }; diff --git a/packages/mermaid/src/diagrams/treemap/styles.ts b/packages/mermaid/src/diagrams/treemap/styles.ts new file mode 100644 index 000000000..6021838c9 --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/styles.ts @@ -0,0 +1,51 @@ +import type { DiagramStylesProvider } from '../../diagram-api/types.js'; +import { cleanAndMerge } from '../../utils.js'; +import type { TreemapStyleOptions } from './types.js'; + +const defaultTreemapStyleOptions: TreemapStyleOptions = { + sectionStrokeColor: 'black', + sectionStrokeWidth: '1', + sectionFillColor: '#efefef', + leafStrokeColor: 'black', + leafStrokeWidth: '1', + leafFillColor: '#efefef', + labelColor: 'black', + labelFontSize: '12px', + valueFontSize: '10px', + valueColor: 'black', + titleColor: 'black', + titleFontSize: '14px', +}; + +export const getStyles: DiagramStylesProvider = ({ + treemap, +}: { treemap?: TreemapStyleOptions } = {}) => { + const options = cleanAndMerge(defaultTreemapStyleOptions, treemap); + + return ` + .treemapNode.section { + stroke: ${options.sectionStrokeColor}; + stroke-width: ${options.sectionStrokeWidth}; + fill: ${options.sectionFillColor}; + } + .treemapNode.leaf { + stroke: ${options.leafStrokeColor}; + stroke-width: ${options.leafStrokeWidth}; + fill: ${options.leafFillColor}; + } + .treemapLabel { + fill: ${options.labelColor}; + font-size: ${options.labelFontSize}; + } + .treemapValue { + fill: ${options.valueColor}; + font-size: ${options.valueFontSize}; + } + .treemapTitle { + fill: ${options.titleColor}; + font-size: ${options.titleFontSize}; + } + `; +}; + +export default getStyles; diff --git a/packages/mermaid/src/diagrams/treemap/types.ts b/packages/mermaid/src/diagrams/treemap/types.ts new file mode 100644 index 000000000..8ab973666 --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/types.ts @@ -0,0 +1,80 @@ +import type { DiagramDBBase, DiagramStyleClassDef } from '../../diagram-api/types.js'; +import type { BaseDiagramConfig } from '../../config.type.js'; + +export interface TreemapNode { + name: string; + children?: TreemapNode[]; + value?: number; + parent?: TreemapNode; + classSelector?: string; + cssCompiledStyles?: string[]; +} + +export interface TreemapDB extends DiagramDBBase { + getNodes: () => TreemapNode[]; + addNode: (node: TreemapNode, level: number) => void; + getRoot: () => TreemapNode | undefined; + getClasses: () => Map; + addClass: (className: string, style: string) => void; + getStylesForClass: (classSelector: string) => string[]; +} + +export interface TreemapStyleOptions { + sectionStrokeColor?: string; + sectionStrokeWidth?: string; + sectionFillColor?: string; + leafStrokeColor?: string; + leafStrokeWidth?: string; + leafFillColor?: string; + labelColor?: string; + labelFontSize?: string; + valueFontSize?: string; + valueColor?: string; + titleColor?: string; + titleFontSize?: string; +} + +export interface TreemapData { + nodes: TreemapNode[]; + levels: Map; + root?: TreemapNode; + outerNodes: TreemapNode[]; + classes: Map; +} + +export interface TreemapItem { + $type: string; + name: string; + value?: number; + classSelector?: string; +} + +export interface TreemapRow { + $type: string; + indent?: string; + item?: TreemapItem; + className?: string; + styleText?: string; +} + +export interface TreemapAst { + TreemapRows?: TreemapRow[]; + title?: string; + description?: string; + accDescription?: string; + accTitle?: string; + diagramTitle?: string; +} + +// Define the TreemapDiagramConfig interface +export interface TreemapDiagramConfig extends BaseDiagramConfig { + padding?: number; + diagramPadding?: number; + showValues?: boolean; + nodeWidth?: number; + nodeHeight?: number; + borderWidth?: number; + valueFontSize?: number; + labelFontSize?: number; + valueFormat?: string; +} diff --git a/packages/mermaid/src/diagrams/treemap/utils.test.ts b/packages/mermaid/src/diagrams/treemap/utils.test.ts new file mode 100644 index 000000000..bfbd74c59 --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/utils.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from 'vitest'; +import { buildHierarchy } from './utils.js'; +import type { TreemapNode } from './types.js'; + +describe('treemap utilities', () => { + describe('buildHierarchy', () => { + it('should convert a flat array into a hierarchical structure', () => { + // Input flat structure + const flatItems = [ + { level: 0, name: 'Root', type: 'Section' }, + { level: 4, name: 'Branch 1', type: 'Section' }, + { level: 8, name: 'Leaf 1.1', type: 'Leaf', value: 10 }, + { level: 8, name: 'Leaf 1.2', type: 'Leaf', value: 15 }, + { level: 4, name: 'Branch 2', type: 'Section' }, + { level: 8, name: 'Leaf 2.1', type: 'Leaf', value: 20 }, + { level: 8, name: 'Leaf 2.2', type: 'Leaf', value: 25 }, + { level: 8, name: 'Leaf 2.3', type: 'Leaf', value: 30 }, + ]; + + // Expected hierarchical structure + const expectedHierarchy: TreemapNode[] = [ + { + name: 'Root', + children: [ + { + name: 'Branch 1', + children: [ + { name: 'Leaf 1.1', value: 10 }, + { name: 'Leaf 1.2', value: 15 }, + ], + }, + { + name: 'Branch 2', + children: [ + { name: 'Leaf 2.1', value: 20 }, + { name: 'Leaf 2.2', value: 25 }, + { name: 'Leaf 2.3', value: 30 }, + ], + }, + ], + }, + ]; + + const result = buildHierarchy(flatItems); + expect(result).toEqual(expectedHierarchy); + }); + + it('should handle empty input', () => { + expect(buildHierarchy([])).toEqual([]); + }); + + it('should handle only root nodes', () => { + const flatItems = [ + { level: 0, name: 'Root 1', type: 'Section' }, + { level: 0, name: 'Root 2', type: 'Section' }, + ]; + + const expected = [ + { name: 'Root 1', children: [] }, + { name: 'Root 2', children: [] }, + ]; + + expect(buildHierarchy(flatItems)).toEqual(expected); + }); + + it('should handle complex nesting levels', () => { + const flatItems = [ + { level: 0, name: 'Root', type: 'Section' }, + { level: 2, name: 'Level 1', type: 'Section' }, + { level: 4, name: 'Level 2', type: 'Section' }, + { level: 6, name: 'Leaf 1', type: 'Leaf', value: 10 }, + { level: 4, name: 'Level 2 again', type: 'Section' }, + { level: 6, name: 'Leaf 2', type: 'Leaf', value: 20 }, + ]; + + const expected = [ + { + name: 'Root', + children: [ + { + name: 'Level 1', + children: [ + { + name: 'Level 2', + children: [{ name: 'Leaf 1', value: 10 }], + }, + { + name: 'Level 2 again', + children: [{ name: 'Leaf 2', value: 20 }], + }, + ], + }, + ], + }, + ]; + + expect(buildHierarchy(flatItems)).toEqual(expected); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/treemap/utils.ts b/packages/mermaid/src/diagrams/treemap/utils.ts new file mode 100644 index 000000000..e7e7fcc5c --- /dev/null +++ b/packages/mermaid/src/diagrams/treemap/utils.ts @@ -0,0 +1,64 @@ +import type { TreemapNode } from './types.js'; + +/** + * Converts a flat array of treemap items into a hierarchical structure + * @param items - Array of flat treemap items with level, name, type, and optional value + * @returns A hierarchical tree structure + */ +export function buildHierarchy( + items: { + level: number; + name: string; + type: string; + value?: number; + classSelector?: string; + cssCompiledStyles?: string; + }[] +): TreemapNode[] { + if (!items.length) { + return []; + } + + const root: TreemapNode[] = []; + const stack: { node: TreemapNode; level: number }[] = []; + + items.forEach((item) => { + const node: TreemapNode = { + name: item.name, + children: item.type === 'Leaf' ? undefined : [], + }; + node.classSelector = item?.classSelector; + if (item?.cssCompiledStyles) { + node.cssCompiledStyles = [item.cssCompiledStyles]; + } + + if (item.type === 'Leaf' && item.value !== undefined) { + node.value = item.value; + } + + // Find the right parent for this node + while (stack.length > 0 && stack[stack.length - 1].level >= item.level) { + stack.pop(); + } + + if (stack.length === 0) { + // This is a root node + root.push(node); + } else { + // Add as child to the parent + const parent = stack[stack.length - 1].node; + if (parent.children) { + parent.children.push(node); + } else { + parent.children = [node]; + } + } + + // Only add to stack if it can have children + if (item.type !== 'Leaf') { + stack.push({ node, level: item.level }); + } + }); + + return root; +} diff --git a/packages/mermaid/src/docs/.vitepress/canonical-config.ts b/packages/mermaid/src/docs/.vitepress/canonical-config.ts new file mode 100644 index 000000000..7c2b5aaeb --- /dev/null +++ b/packages/mermaid/src/docs/.vitepress/canonical-config.ts @@ -0,0 +1,149 @@ +import type { CanonicalUrlConfig } from './canonical-urls.js'; + +/** + * Canonical URL configuration for Mermaid documentation + * + * This file contains the configuration for generating canonical URLs + * for the Mermaid documentation site. + */ +export const canonicalConfig: CanonicalUrlConfig = { + // Base URL for the Mermaid documentation site + baseUrl: 'https://docs.mermaidchart.com', + + // Disable automatic generation - only use specificCanonicalUrls + autoGenerate: false, + + // Patterns for pages to exclude from automatic canonical URL generation + excludePatterns: [ + // Exclude the main index page (handled by VitePress home layout) + 'index.md', + + // Exclude any draft or temporary files + 'draft-*.md', + '**/draft-*.md', + 'temp-*.md', + '**/temp-*.md', + '*.draft.md', + '**/*.draft.md', + + // Exclude any test files + '*.test.md', + '**/*.test.md', + '*.spec.md', + '**/*.spec.md', + + // You can add more patterns here as needed + // Examples: + // '**/internal/**', // Exclude internal documentation + // '**/archive/**', // Exclude archived content + ], + + // URL transformation rules + transformations: { + // Remove index.md from URLs (e.g., /intro/index.md -> /intro/) + //removeIndex: true, + + // Remove .md extension from URLs (e.g., /syntax/flowchart.md -> /syntax/flowchart) + removeMarkdownExtension: true, + + // Custom path transformations + customTransforms: [ + // Example: Redirect old paths to new paths + // { pattern: /^old-syntax\//, replacement: 'syntax/' }, + // Example: Handle special cases + // { pattern: /^config\/setup\/README$/, replacement: 'config/setup/' }, + // Add your custom transformations here + ], + }, +}; + +/** + * Pages that should have specific canonical URLs + * + * Since autoGenerate is set to false, ONLY pages listed here will get canonical URLs. + * + * Usage: Add entries to this object where the key is the relative path + * of the markdown file and the value is the desired canonical URL. + * + * Examples: + * - 'intro/index.md': 'https://docs.mermaidchart.com/intro/index.html' + * - 'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html' + * - 'config/configuration.md': 'https://docs.mermaidchart.com/mermaid-oss/config/configuration.html' + */ +export const specificCanonicalUrls: Record = { + // Add your specific canonical URLs here + // Example: + // 'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html', + + // Intro section + 'intro/index.md': 'https://docs.mermaidchart.com/intro/index.html', + 'intro/getting-started.md': + 'https://docs.mermaidchart.com/mermaid-oss/intro/getting-started.html', + 'intro/syntax-reference.md': + 'https://docs.mermaidchart.com/mermaid-oss/intro/syntax-reference.html', + + // Syntax section + 'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html', + 'syntax/sequenceDiagram.md': + 'https://docs.mermaidchart.com/mermaid-oss/syntax/sequenceDiagram.html', + 'syntax/classDiagram.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/classDiagram.html', + 'syntax/stateDiagram.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/stateDiagram.html', + 'syntax/entityRelationshipDiagram.md': + 'https://docs.mermaidchart.com/mermaid-oss/syntax/entityRelationshipDiagram.html', + 'syntax/userJourney.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/userJourney.html', + 'syntax/gantt.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/gantt.html', + 'syntax/pie.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/pie.html', + 'syntax/quadrantChart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/quadrantChart.html', + 'syntax/requirementDiagram.md': + 'https://docs.mermaidchart.com/mermaid-oss/syntax/requirementDiagram.html', + 'syntax/mindmap.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/mindmap.html', + 'syntax/timeline.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/timeline.html', + 'syntax/gitgraph.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/gitgraph.html', + 'syntax/c4.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/c4.html', + 'syntax/sankey.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/sankey.html', + 'syntax/xyChart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/xyChart.html', + 'syntax/block.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/block.html', + 'syntax/packet.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/packet.html', + 'syntax/kanban.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/kanban.html', + 'syntax/architecture.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/architecture.html', + 'syntax/radar.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/radar.html', + 'syntax/examples.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/examples.html', + + // Config section + 'config/configuration.md': 'https://docs.mermaidchart.com/mermaid-oss/config/configuration.html', + 'config/usage.md': 'https://docs.mermaidchart.com/mermaid-oss/config/usage.html', + 'config/icons.md': 'https://docs.mermaidchart.com/mermaid-oss/config/icons.html', + 'config/directives.md': 'https://docs.mermaidchart.com/mermaid-oss/config/directives.html', + 'config/theming.md': 'https://docs.mermaidchart.com/mermaid-oss/config/theming.html', + 'config/math.md': 'https://docs.mermaidchart.com/mermaid-oss/config/math.html', + 'config/accessibility.md': 'https://docs.mermaidchart.com/mermaid-oss/config/accessibility.html', + 'config/mermaidCLI.md': 'https://docs.mermaidchart.com/mermaid-oss/config/mermaidCLI.html', + 'config/faq.md': 'https://docs.mermaidchart.com/mermaid-oss/config/faq.html', + + // Ecosystem section + 'ecosystem/mermaid-chart.md': + 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/mermaid-chart.html', + 'ecosystem/tutorials.md': 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/tutorials.html', + 'ecosystem/integrations-community.md': + 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/integrations-community.html', + 'ecosystem/integrations-create.md': + 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/integrations-create.html', + + // Community section + 'community/intro.md': 'https://docs.mermaidchart.com/mermaid-oss/community/intro.html', + 'community/contributing.md': + 'https://docs.mermaidchart.com/mermaid-oss/community/contributing.html', + 'community/new-diagram.md': + 'https://docs.mermaidchart.com/mermaid-oss/community/new-diagram.html', + 'community/questions-and-suggestions.md': + 'https://docs.mermaidchart.com/mermaid-oss/community/questions-and-suggestions.html', + 'community/security.md': 'https://docs.mermaidchart.com/mermaid-oss/community/security.html', +}; + +/** + * Helper function to get canonical URL for a specific page + * This can be used in frontmatter or for manual overrides + */ +export function getCanonicalUrl(relativePath: string): string | undefined { + return specificCanonicalUrls[relativePath]; +} diff --git a/packages/mermaid/src/docs/.vitepress/canonical-urls.ts b/packages/mermaid/src/docs/.vitepress/canonical-urls.ts new file mode 100644 index 000000000..6c04e1eb1 --- /dev/null +++ b/packages/mermaid/src/docs/.vitepress/canonical-urls.ts @@ -0,0 +1,198 @@ +import type { PageData } from 'vitepress'; +import { canonicalConfig, specificCanonicalUrls } from './canonical-config.js'; + +/** + * Configuration for canonical URL generation + */ +export interface CanonicalUrlConfig { + /** Base URL for the site (e.g., 'https://mermaid.js.org') */ + baseUrl: string; + /** Whether to automatically generate canonical URLs for pages without explicit ones */ + autoGenerate: boolean; + /** Pages to exclude from automatic canonical URL generation (glob patterns supported) */ + excludePatterns?: string[]; + /** Custom URL transformations */ + transformations?: { + /** Remove index.md from URLs */ + removeIndex?: boolean; + /** Remove .md extension from URLs */ + removeMarkdownExtension?: boolean; + /** Custom path transformations */ + customTransforms?: { + pattern: RegExp; + replacement: string; + }[]; + }; +} + +/** + * Default configuration for canonical URLs + */ +const defaultConfig: CanonicalUrlConfig = { + baseUrl: 'https://mermaid.js.org', + autoGenerate: true, + excludePatterns: [ + // Exclude the home page as it's handled separately + 'index.md', + // Exclude any temporary or draft files + '**/draft-*', + '**/temp-*', + ], + transformations: { + removeIndex: true, + removeMarkdownExtension: true, + customTransforms: [ + // Transform any special cases here + // Example: { pattern: /^old-path\//, replacement: 'new-path/' } + ], + }, +}; + +/** + * Check if a path matches any of the exclude patterns + */ +function shouldExcludePath(relativePath: string, excludePatterns: string[] = []): boolean { + return excludePatterns.some((pattern) => { + // Convert glob pattern to regex + const regexPattern = pattern + .replace(/\*\*/g, '.*') + .replace(/\*/g, '[^/]*') + .replace(/\?/g, '.') + .replace(/\./g, '\\.'); + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(relativePath); + }); +} + +/** + * Transform a relative path to a canonical URL path + */ +function transformPath(relativePath: string, config: CanonicalUrlConfig): string { + let transformedPath = relativePath; + + // Apply built-in transformations + if (config.transformations?.removeMarkdownExtension) { + transformedPath = transformedPath.replace(/\.md$/, ''); + } + + if (config.transformations?.removeIndex) { + transformedPath = transformedPath.replace(/\/index$/, '/'); + if (transformedPath === 'index') { + transformedPath = ''; + } + } + + // Apply custom transformations + if (config.transformations?.customTransforms) { + for (const transform of config.transformations.customTransforms) { + transformedPath = transformedPath.replace(transform.pattern, transform.replacement); + } + } + + // Ensure path starts with / + if (transformedPath && !transformedPath.startsWith('/')) { + transformedPath = '/' + transformedPath; + } + + // Handle root path + if (!transformedPath) { + transformedPath = '/'; + } + + return transformedPath; +} + +/** + * Generate a canonical URL for a page + */ +function generateCanonicalUrl(relativePath: string, config: CanonicalUrlConfig): string { + const transformedPath = transformPath(relativePath, config); + return config.baseUrl + transformedPath; +} + +/** + * VitePress transformPageData hook to add canonical URLs + */ +export function addCanonicalUrls(pageData: PageData): void { + const config = canonicalConfig; + + // Check for specific canonical URLs first + const specificUrl = specificCanonicalUrls[pageData.relativePath]; + if (specificUrl) { + addCanonicalToHead(pageData, specificUrl); + return; + } + + // Skip if canonical URL is already explicitly set in frontmatter + if (pageData.frontmatter.canonical) { + // If it's already a full URL, use as-is + if (pageData.frontmatter.canonical.startsWith('http')) { + addCanonicalToHead(pageData, pageData.frontmatter.canonical); + return; + } + // If it's a relative path, convert to absolute URL + const canonicalUrl = config.baseUrl + pageData.frontmatter.canonical; + addCanonicalToHead(pageData, canonicalUrl); + return; + } + + // Skip if canonicalPath is set in frontmatter + if (pageData.frontmatter.canonicalPath) { + const canonicalUrl = config.baseUrl + pageData.frontmatter.canonicalPath; + addCanonicalToHead(pageData, canonicalUrl); + return; + } + + // Skip if auto-generation is disabled + if (!config.autoGenerate) { + return; + } + + // Skip if path should be excluded + if (shouldExcludePath(pageData.relativePath, config.excludePatterns)) { + return; + } + + // Generate canonical URL + const canonicalUrl = generateCanonicalUrl(pageData.relativePath, config); + addCanonicalToHead(pageData, canonicalUrl); +} + +/** + * Add canonical URL to page head + */ +function addCanonicalToHead(pageData: PageData, canonicalUrl: string): void { + // Initialize head array if it doesn't exist + pageData.frontmatter.head = pageData.frontmatter.head || []; + + // Check if canonical link already exists + const hasCanonical = pageData.frontmatter.head.some( + (item: any) => Array.isArray(item) && item[0] === 'link' && item[1]?.rel === 'canonical' + ); + + // Add canonical link if it doesn't exist + if (!hasCanonical) { + pageData.frontmatter.head.push(['link', { rel: 'canonical', href: canonicalUrl }]); + } +} + +/** + * Utility function to create a custom configuration + * This can be used to override the default configuration + */ +export function createCanonicalUrlConfig( + customConfig: Partial +): CanonicalUrlConfig { + return { + ...defaultConfig, + ...customConfig, + transformations: { + ...defaultConfig.transformations, + ...customConfig.transformations, + customTransforms: [ + ...(defaultConfig.transformations?.customTransforms || []), + ...(customConfig.transformations?.customTransforms || []), + ], + }, + }; +} diff --git a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue new file mode 100644 index 000000000..e41a7096d --- /dev/null +++ b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue @@ -0,0 +1,138 @@ + + + diff --git a/packages/mermaid/src/docs/.vitepress/components/TopBar.vue b/packages/mermaid/src/docs/.vitepress/components/TopBar.vue index 0914d808e..c5b464bb6 100644 --- a/packages/mermaid/src/docs/.vitepress/components/TopBar.vue +++ b/packages/mermaid/src/docs/.vitepress/components/TopBar.vue @@ -1,134 +1,65 @@