diff --git a/.build/jsonSchema.ts b/.build/jsonSchema.ts
index 7a700c1e2..48a9883de 100644
--- a/.build/jsonSchema.ts
+++ b/.build/jsonSchema.ts
@@ -27,6 +27,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'block',
'packet',
'architecture',
+ 'radar',
] as const;
/**
diff --git a/.changeset/angry-bags-brake.md b/.changeset/angry-bags-brake.md
deleted file mode 100644
index 472e486ec..000000000
--- a/.changeset/angry-bags-brake.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments
diff --git a/.changeset/bright-ads-exist.md b/.changeset/bright-ads-exist.md
deleted file mode 100644
index ef2f76f4c..000000000
--- a/.changeset/bright-ads-exist.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-Fixes for consistent edge id creation & handling edge cases for animate edge feature
diff --git a/.changeset/chatty-elephants-warn.md b/.changeset/chatty-elephants-warn.md
deleted file mode 100644
index 225047ece..000000000
--- a/.changeset/chatty-elephants-warn.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-Fix for issue #6195 - allowing @ signs inside node labels
diff --git a/.changeset/chilly-years-cheat.md b/.changeset/chilly-years-cheat.md
deleted file mode 100644
index e665af75b..000000000
--- a/.changeset/chilly-years-cheat.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram
diff --git a/.changeset/dull-tips-cough.md b/.changeset/dull-tips-cough.md
deleted file mode 100644
index 1f5179417..000000000
--- a/.changeset/dull-tips-cough.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-fix: revert state db to resolve getData returning empty nodes and edges
diff --git a/.changeset/gold-shoes-camp.md b/.changeset/gold-shoes-camp.md
new file mode 100644
index 000000000..3018e7381
--- /dev/null
+++ b/.changeset/gold-shoes-camp.md
@@ -0,0 +1,5 @@
+---
+'mermaid': patch
+---
+
+fix: Remove incorrect `style="undefined;"` attributes in some Mermaid diagrams
diff --git a/.changeset/great-ghosts-rule.md b/.changeset/great-ghosts-rule.md
deleted file mode 100644
index f11c6e2a9..000000000
--- a/.changeset/great-ghosts-rule.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-'mermaid': minor
----
-
-Flowchart new syntax for node metadata bugs
-
-- Incorrect label mapping for nodes when using `&`
-- Syntax error when `}` with trailing spaces before new line
diff --git a/.changeset/grumpy-cheetahs-deliver.md b/.changeset/grumpy-cheetahs-deliver.md
deleted file mode 100644
index fa6736d42..000000000
--- a/.changeset/grumpy-cheetahs-deliver.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-`mermaidAPI.getDiagramFromText()` now returns a new db instance on each call for state diagrams
diff --git a/.changeset/heavy-moose-mix.md b/.changeset/heavy-moose-mix.md
deleted file mode 100644
index c02d62446..000000000
--- a/.changeset/heavy-moose-mix.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-Added versioning to StateDB and updated tests and diagrams to use it.
diff --git a/.changeset/honest-trees-dress.md b/.changeset/honest-trees-dress.md
new file mode 100644
index 000000000..054f1bedb
--- /dev/null
+++ b/.changeset/honest-trees-dress.md
@@ -0,0 +1,7 @@
+---
+'@mermaid-js/mermaid-zenuml': patch
+---
+
+chore: bump minimum ZenUML version to 3.23.28
+
+commit: 9d06d8f31e7f12af9e9e092214f907f2dc93ad75
diff --git a/.changeset/many-brooms-promise.md b/.changeset/many-brooms-promise.md
deleted file mode 100644
index fec442b34..000000000
--- a/.changeset/many-brooms-promise.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': minor
----
-
-Adding support for animation of flowchart edges
diff --git a/.changeset/new-kiwis-listen.md b/.changeset/new-kiwis-listen.md
deleted file mode 100644
index 24306573c..000000000
--- a/.changeset/new-kiwis-listen.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each flowchart
diff --git a/.changeset/sad-mails-accept.md b/.changeset/sad-mails-accept.md
new file mode 100644
index 000000000..11dd69d8d
--- /dev/null
+++ b/.changeset/sad-mails-accept.md
@@ -0,0 +1,6 @@
+---
+'mermaid': patch
+'@mermaid-js/parser': patch
+---
+
+Refactor grammar so that title don't break Architecture Diagrams
diff --git a/.changeset/silver-olives-marry.md b/.changeset/silver-olives-marry.md
deleted file mode 100644
index d709b17ba..000000000
--- a/.changeset/silver-olives-marry.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages.
diff --git a/.changeset/stupid-dots-do.md b/.changeset/stupid-dots-do.md
deleted file mode 100644
index 594fa9536..000000000
--- a/.changeset/stupid-dots-do.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-fix: Gantt, Sankey and User Journey diagram are now able to pick font-family from mermaid config.
diff --git a/.changeset/weak-trees-perform.md b/.changeset/weak-trees-perform.md
deleted file mode 100644
index 17175301d..000000000
--- a/.changeset/weak-trees-perform.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-fix: `getDirection` and `setDirection` in `stateDb` refactored to return and set actual direction
diff --git a/.changeset/witty-crews-smell.md b/.changeset/witty-crews-smell.md
deleted file mode 100644
index 4213083f2..000000000
--- a/.changeset/witty-crews-smell.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'mermaid': patch
----
-
-`mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram
diff --git a/.changeset/yellow-mirrors-change.md b/.changeset/yellow-mirrors-change.md
new file mode 100644
index 000000000..09a766104
--- /dev/null
+++ b/.changeset/yellow-mirrors-change.md
@@ -0,0 +1,7 @@
+---
+'@mermaid-js/mermaid-zenuml': patch
+---
+
+fix(zenuml): limit `peerDependencies` to Mermaid v10 and v11
+
+commit: 0ad44c12feead9d20c6a870a49327ada58d6e657
diff --git a/.esbuild/build.ts b/.esbuild/build.ts
index 423e8f047..05002cb16 100644
--- a/.esbuild/build.ts
+++ b/.esbuild/build.ts
@@ -34,6 +34,19 @@ const buildPackage = async (entryName: keyof typeof packageOptions) => {
{ ...iifeOptions, minify: true, metafile: shouldVisualize }
);
}
+ if (entryName === 'mermaid-zenuml') {
+ const iifeOptions: MermaidBuildOptions = {
+ ...commonOptions,
+ format: 'iife',
+ globalName: 'mermaid-zenuml',
+ };
+ buildConfigs.push(
+ // mermaid-zenuml.js
+ { ...iifeOptions },
+ // mermaid-zenuml.min.js
+ { ...iifeOptions, minify: true, metafile: shouldVisualize }
+ );
+ }
const results = await Promise.all(buildConfigs.map((option) => build(getBuildConfig(option))));
diff --git a/.esbuild/util.ts b/.esbuild/util.ts
index 6d6d1d59b..dde0352af 100644
--- a/.esbuild/util.ts
+++ b/.esbuild/util.ts
@@ -58,6 +58,7 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
format,
minify,
options: { name, file, packageName },
+ globalName = 'mermaid',
} = options;
const external: string[] = ['require', 'fs', 'path'];
const outFileName = getFileName(name, options);
@@ -68,6 +69,7 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
},
metafile,
minify,
+ globalName,
logLevel: 'info',
chunkNames: `chunks/${outFileName}/[name]-[hash]`,
define: {
@@ -89,11 +91,12 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
if (format === 'iife') {
output.format = 'iife';
output.splitting = false;
- output.globalName = '__esbuild_esm_mermaid';
+ const originalGlobalName = output.globalName ?? 'mermaid';
+ output.globalName = `__esbuild_esm_mermaid_nm[${JSON.stringify(originalGlobalName)}]`;
// Workaround for removing the .default access in esbuild IIFE.
// https://github.com/mermaid-js/mermaid/pull/4109#discussion_r1292317396
output.footer = {
- js: 'globalThis.mermaid = globalThis.__esbuild_esm_mermaid.default;',
+ js: `globalThis[${JSON.stringify(originalGlobalName)}] = globalThis.${output.globalName}.default;`,
};
output.outExtension = { '.js': '.js' };
} else {
diff --git a/.github/workflows/e2e-timings.yml b/.github/workflows/e2e-timings.yml
index b51557b69..f45551988 100644
--- a/.github/workflows/e2e-timings.yml
+++ b/.github/workflows/e2e-timings.yml
@@ -11,6 +11,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
+ pull-requests: write
jobs:
timings:
@@ -29,6 +30,7 @@ jobs:
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
with:
runTests: false
+
- name: Cypress run
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
id: cypress
@@ -44,15 +46,17 @@ jobs:
SPLIT: 1
SPLIT_INDEX: 0
SPLIT_FILE: 'cypress/timings.json'
- - name: Commit changes
- uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
+
+ - name: Compare timings
+ run: pnpm tsx scripts/compare-timings.ts
+
+ - name: Commit and create pull request
+ uses: peter-evans/create-pull-request@a7b20e1da215b3ef3ccddb48ff65120256ed6226
with:
- add: 'cypress/timings.json'
- author_name: 'github-actions[bot]'
- author_email: '41898282+github-actions[bot]@users.noreply.github.com'
- message: 'chore: update E2E timings'
- - name: Create Pull Request
- uses: peter-evans/create-pull-request@v5
- with:
- branch: release-promotion
+ add-paths: |
+ cypress/timings.json
+ commit-message: 'chore: update E2E timings'
+ branch: update-timings
title: Update E2E Timings
+ delete-branch: true
+ sign-commits: true
diff --git a/cypress.config.ts b/cypress.config.ts
index d077ba915..50ea940e9 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -1,8 +1,8 @@
import eyesPlugin from '@applitools/eyes-cypress';
import { registerArgosTask } from '@argos-ci/cypress/task';
-import coverage from '@cypress/code-coverage/task';
+import coverage from '@cypress/code-coverage/task.js';
import { defineConfig } from 'cypress';
-import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin';
+import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin.js';
import cypressSplit from 'cypress-split';
export default eyesPlugin(
diff --git a/cypress/integration/rendering/architecture.spec.ts b/cypress/integration/rendering/architecture.spec.ts
index 25326ff80..ec74a5dd5 100644
--- a/cypress/integration/rendering/architecture.spec.ts
+++ b/cypress/integration/rendering/architecture.spec.ts
@@ -19,6 +19,25 @@ describe.skip('architecture diagram', () => {
`
);
});
+ it('should render a simple architecture diagram with titleAndAccessabilities', () => {
+ imgSnapshotTest(
+ `architecture-beta
+ title Simple Architecture Diagram
+ accTitle: Accessibility Title
+ accDescr: Accessibility Description
+ group api(cloud)[API]
+
+ service db(database)[Database] in api
+ service disk1(disk)[Storage] in api
+ service disk2(disk)[Storage] in api
+ service server(server)[Server] in api
+
+ db:L -- R:server
+ disk1:T -- B:server
+ disk2:T -- B:db
+ `
+ );
+ });
it('should render an architecture diagram with groups within groups', () => {
imgSnapshotTest(
`architecture-beta
@@ -172,7 +191,7 @@ describe.skip('architecture diagram', () => {
);
});
- it('should render an architecture diagram with a resonable height', () => {
+ it('should render an architecture diagram with a reasonable height', () => {
imgSnapshotTest(
`architecture-beta
group federated(cloud)[Federated Environment]
diff --git a/cypress/integration/rendering/erDiagram-unified.spec.js b/cypress/integration/rendering/erDiagram-unified.spec.js
new file mode 100644
index 000000000..8cecba21d
--- /dev/null
+++ b/cypress/integration/rendering/erDiagram-unified.spec.js
@@ -0,0 +1,652 @@
+import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
+
+const testOptions = [
+ { description: '', options: { logLevel: 1 } },
+ { description: 'ELK: ', options: { logLevel: 1, layout: 'elk' } },
+ { description: 'HD: ', options: { logLevel: 1, look: 'handDrawn' } },
+];
+
+describe('Entity Relationship Diagram Unified', () => {
+ testOptions.forEach(({ description, options }) => {
+ it(`${description}should render a simple ER diagram`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ CUSTOMER ||--o{ ORDER : places
+ ORDER ||--|{ LINE-ITEM : contains
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render a simple ER diagram without htmlLabels`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ CUSTOMER ||--o{ ORDER : places
+ ORDER ||--|{ LINE-ITEM : contains
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render an ER diagram with a recursive relationship`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ CUSTOMER ||..o{ CUSTOMER : refers
+ CUSTOMER ||--o{ ORDER : places
+ ORDER ||--|{ LINE-ITEM : contains
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render an ER diagram with multiple relationships between the same two entities`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ CUSTOMER ||--|{ ADDRESS : "invoiced at"
+ CUSTOMER ||--|{ ADDRESS : "receives goods at"
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render a cyclical ER diagram`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ A ||--|{ B : likes
+ B ||--|{ C : likes
+ C ||--|{ A : likes
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render a not-so-simple ER diagram`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ CUSTOMER }|..|{ DELIVERY-ADDRESS : has
+ CUSTOMER ||--o{ ORDER : places
+ CUSTOMER ||--o{ INVOICE : "liable for"
+ DELIVERY-ADDRESS ||--o{ ORDER : receives
+ INVOICE ||--|{ ORDER : covers
+ ORDER ||--|{ ORDER-ITEM : includes
+ PRODUCT-CATEGORY ||--|{ PRODUCT : contains
+ PRODUCT ||--o{ ORDER-ITEM : "ordered in"
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render a not-so-simple ER diagram without htmlLabels`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ CUSTOMER }|..|{ DELIVERY-ADDRESS : has
+ CUSTOMER ||--o{ ORDER : places
+ CUSTOMER ||--o{ INVOICE : "liable for"
+ DELIVERY-ADDRESS ||--o{ ORDER : receives
+ INVOICE ||--|{ ORDER : covers
+ ORDER ||--|{ ORDER-ITEM : includes
+ PRODUCT-CATEGORY ||--|{ PRODUCT : contains
+ PRODUCT ||--o{ ORDER-ITEM : "ordered in"
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render multiple ER diagrams`, () => {
+ imgSnapshotTest(
+ [
+ `
+ erDiagram
+ CUSTOMER ||--o{ ORDER : places
+ ORDER ||--|{ LINE-ITEM : contains
+ `,
+ `
+ erDiagram
+ CUSTOMER ||--o{ ORDER : places
+ ORDER ||--|{ LINE-ITEM : contains
+ `,
+ ],
+ options
+ );
+ });
+
+ it(`${description}should render an ER diagram with blank or empty labels`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ BOOK }|..|{ AUTHOR : ""
+ BOOK }|..|{ GENRE : " "
+ AUTHOR }|..|{ GENRE : " "
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities that have no relationships`, () => {
+ renderGraph(
+ `
+ erDiagram
+ DEAD_PARROT
+ HERMIT
+ RECLUSE
+ SOCIALITE }o--o{ SOCIALITE : "interacts with"
+ RECLUSE }o--o{ SOCIALITE : avoids
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with and without attributes`, () => {
+ renderGraph(
+ `
+ erDiagram
+ BOOK { string title }
+ AUTHOR }|..|{ BOOK : writes
+ BOOK { float price }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with generic and array attributes`, () => {
+ renderGraph(
+ `
+ erDiagram
+ BOOK {
+ string title
+ string[] authors
+ type~T~ type
+ }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with generic and array attributes without htmlLabels`, () => {
+ renderGraph(
+ `
+ erDiagram
+ BOOK {
+ string title
+ string[] authors
+ type~T~ type
+ }
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities with length in attributes type`, () => {
+ renderGraph(
+ `
+ erDiagram
+ CLUSTER {
+ varchar(99) name
+ string(255) description
+ }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with length in attributes type without htmlLabels`, () => {
+ renderGraph(
+ `
+ erDiagram
+ CLUSTER {
+ varchar(99) name
+ string(255) description
+ }
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities and attributes with big and small entity names`, () => {
+ renderGraph(
+ `
+ erDiagram
+ PRIVATE_FINANCIAL_INSTITUTION {
+ string name
+ int turnover
+ }
+ PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs
+ EMPLOYEE { bool officer_of_firm }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities and attributes with big and small entity names without htmlLabels`, () => {
+ renderGraph(
+ `
+ erDiagram
+ PRIVATE_FINANCIAL_INSTITUTION {
+ string name
+ int turnover
+ }
+ PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs
+ EMPLOYEE { bool officer_of_firm }
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities with attributes that begin with asterisk`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ BOOK {
+ int *id
+ string name
+ varchar(99) summary
+ }
+ BOOK }o..o{ STORE : soldBy
+ STORE {
+ int *id
+ string name
+ varchar(50) address
+ }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with attributes that begin with asterisk without htmlLabels`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ BOOK {
+ int *id
+ string name
+ varchar(99) summary
+ }
+ BOOK }o..o{ STORE : soldBy
+ STORE {
+ int *id
+ string name
+ varchar(50) address
+ }
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities with keys`, () => {
+ renderGraph(
+ `
+ erDiagram
+ AUTHOR_WITH_LONG_ENTITY_NAME {
+ string name PK
+ }
+ AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
+ BOOK {
+ float price
+ string author FK
+ string title PK
+ }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with keys without htmlLabels`, () => {
+ renderGraph(
+ `
+ erDiagram
+ AUTHOR_WITH_LONG_ENTITY_NAME {
+ string name PK
+ }
+ AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
+ BOOK {
+ float price
+ string author FK
+ string title PK
+ }
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities with comments`, () => {
+ renderGraph(
+ `
+ erDiagram
+ AUTHOR_WITH_LONG_ENTITY_NAME {
+ string name "comment"
+ }
+ AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
+ BOOK {
+ string author
+ string title "author comment"
+ float price "price comment"
+ }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with comments without htmlLabels`, () => {
+ renderGraph(
+ `
+ erDiagram
+ AUTHOR_WITH_LONG_ENTITY_NAME {
+ string name "comment"
+ }
+ AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
+ BOOK {
+ string author
+ string title "author comment"
+ float price "price comment"
+ }
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities with keys and comments`, () => {
+ renderGraph(
+ `
+ erDiagram
+ AUTHOR_WITH_LONG_ENTITY_NAME {
+ string name PK "comment"
+ }
+ AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
+ BOOK {
+ string description
+ float price "price comment"
+ string title PK "title comment"
+ string author FK
+ }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with keys and comments without htmlLabels`, () => {
+ renderGraph(
+ `
+ erDiagram
+ AUTHOR_WITH_LONG_ENTITY_NAME {
+ string name PK "comment"
+ }
+ AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
+ BOOK {
+ string description
+ float price "price comment"
+ string title PK "title comment"
+ string author FK
+ }
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities with aliases`, () => {
+ renderGraph(
+ `
+ erDiagram
+ T1 one or zero to one or more T2 : test
+ T2 one or many optionally to zero or one T3 : test
+ T3 zero or more to zero or many T4 : test
+ T4 many(0) to many(1) T5 : test
+ T5 many optionally to one T6 : test
+ T6 only one optionally to only one T1 : test
+ T4 0+ to 1+ T6 : test
+ T1 1 to 1 T3 : test
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render a simple ER diagram with a title`, () => {
+ imgSnapshotTest(
+ `---
+ title: simple ER diagram
+ ---
+ erDiagram
+ CUSTOMER ||--o{ ORDER : places
+ ORDER ||--|{ LINE-ITEM : contains
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with entity name aliases`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ p[Person] {
+ varchar(64) firstName
+ varchar(64) lastName
+ }
+ c["Customer Account"] {
+ varchar(128) email
+ }
+ p ||--o| c : has
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render relationship labels with line breaks`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ p[Person] {
+ string firstName
+ string lastName
+ }
+ a["Customer Account"] {
+ string email
+ }
+
+ b["Customer Account Secondary"] {
+ string email
+ }
+
+ c["Customer Account Tertiary"] {
+ string email
+ }
+
+ d["Customer Account Nth"] {
+ string email
+ }
+
+ p ||--o| a : "has
one"
+ p ||--o| b : "has
one
two"
+ p ||--o| c : "has
one
two
three"
+ p ||--o| d : "has
one
two
three
...
Nth"
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render an ER diagram with unicode text`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ _**testẽζ➕Ø😀㌕ぼ**_ {
+ *__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***"
+ *string(99)~T~~~~~~* firstName "Only __99__
characters are a
llowed dsfsdfsdfsdfs"
+ string last*Name*
+ string __phone__ UK
+ int _age_
+ }
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render an ER diagram with unicode text without htmlLabels`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ _**testẽζ➕Ø😀㌕ぼ**_ {
+ *__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***"
+ *string(99)~T~~~~~~* firstName "Only __99__
characters are a
llowed dsfsdfsdfsdfs"
+ string last*Name*
+ string __phone__ UK
+ int _age_
+ }
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render an ER diagram with relationships with unicode text`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ person[😀] {
+ string *first*Name
+ string _**last**Name_
+ }
+ a["*Customer Account*"] {
+ **string** ema*i*l
+ }
+ person ||--o| a : __hẽ😀__
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render an ER diagram with relationships with unicode text without htmlLabels`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ person[😀] {
+ string *first*Name
+ string _**last**Name_
+ }
+ a["*Customer Account*"] {
+ **string** ema*i*l
+ }
+ person ||--o| a : __hẽ😀__
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render an ER diagram with TB direction`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ direction TB
+ CAR ||--|{ NAMED-DRIVER : allows
+ PERSON ||..o{ NAMED-DRIVER : is
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render an ER diagram with BT direction`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ direction BT
+ CAR ||--|{ NAMED-DRIVER : allows
+ PERSON ||..o{ NAMED-DRIVER : is
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render an ER diagram with LR direction`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ direction LR
+ CAR ||--|{ NAMED-DRIVER : allows
+ PERSON ||..o{ NAMED-DRIVER : is
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render an ER diagram with RL direction`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ direction RL
+ CAR ||--|{ NAMED-DRIVER : allows
+ PERSON ||..o{ NAMED-DRIVER : is
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with styles applied from style statement`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ c[CUSTOMER]
+ p[PERSON]
+ style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with styles applied from style statement without htmlLabels`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ c[CUSTOMER]
+ p[PERSON]
+ style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities with styles applied from class statement`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ c[CUSTOMER]
+ p[PERSON]:::blue
+ classDef bold font-size:24px, font-weight: bold
+ classDef blue stroke:lightblue, color: #0000FF
+ class c,p bold
+ `,
+ options
+ );
+ });
+
+ it(`${description}should render entities with styles applied from class statement without htmlLabels`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ c[CUSTOMER]
+ p[PERSON]:::blue
+ classDef bold font-size:24px, font-weight: bold
+ classDef blue stroke:lightblue, color: #0000FF
+ class c,p bold
+ `,
+ { ...options, htmlLabels: false }
+ );
+ });
+
+ it(`${description}should render entities with styles applied from the default class and other styles`, () => {
+ imgSnapshotTest(
+ `
+ erDiagram
+ c[CUSTOMER]
+ p[PERSON]:::blue
+ classDef blue stroke:lightblue, color: #0000FF
+ classDef default fill:pink
+ style c color:green
+ `,
+ { ...options }
+ );
+ });
+ });
+});
diff --git a/cypress/integration/rendering/erDiagram.spec.js b/cypress/integration/rendering/erDiagram.spec.js
index aad9b1cf7..cbfec8218 100644
--- a/cypress/integration/rendering/erDiagram.spec.js
+++ b/cypress/integration/rendering/erDiagram.spec.js
@@ -109,8 +109,8 @@ describe('Entity Relationship Diagram', () => {
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
- // use within because the absolute value can be slightly different depending on the environment ±5%
- expect(maxWidthValue).to.be.within(140 * 0.95, 140 * 1.05);
+ // use within because the absolute value can be slightly different depending on the environment ±6%
+ expect(maxWidthValue).to.be.within(140 * 0.96, 140 * 1.06);
});
});
@@ -125,8 +125,8 @@ describe('Entity Relationship Diagram', () => {
);
cy.get('svg').should((svg) => {
const width = parseFloat(svg.attr('width'));
- // use within because the absolute value can be slightly different depending on the environment ±5%
- expect(width).to.be.within(140 * 0.95, 140 * 1.05);
+ // use within because the absolute value can be slightly different depending on the environment ±6%
+ expect(width).to.be.within(140 * 0.96, 140 * 1.06);
// expect(svg).to.have.attr('height', '465');
expect(svg).to.not.have.attr('style');
});
diff --git a/cypress/integration/rendering/flowchart.spec.js b/cypress/integration/rendering/flowchart.spec.js
index d3a83ae5f..7b986cd2f 100644
--- a/cypress/integration/rendering/flowchart.spec.js
+++ b/cypress/integration/rendering/flowchart.spec.js
@@ -895,7 +895,7 @@ graph TD
imgSnapshotTest(
`
graph TD
- classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff
+ classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff
hello --> default
`,
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
@@ -917,4 +917,21 @@ graph TD
}
);
});
+ it('#6369: edge color should affect arrow head', () => {
+ imgSnapshotTest(
+ `
+ flowchart LR
+ A --> B
+ A --> C
+ C --> D
+
+ linkStyle 0 stroke:#D50000
+ linkStyle 2 stroke:#D50000
+ `,
+ {
+ flowchart: { htmlLabels: true },
+ securityLevel: 'loose',
+ }
+ );
+ });
});
diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js
index d8bef6d1b..2d6c14c9d 100644
--- a/cypress/integration/rendering/journey.spec.js
+++ b/cypress/integration/rendering/journey.spec.js
@@ -63,4 +63,165 @@ section Checkout from website
{ journey: { useMaxWidth: false } }
);
});
+
+ it('should initialize with a left margin of 150px for user journeys', () => {
+ renderGraph(
+ `
+ ---
+ config:
+ journey:
+ maxLabelWidth: 320
+ ---
+ journey
+ title User Journey Example
+ section Onboarding
+ Sign Up: 5:
+ Browse Features: 3:
+ Use Core Functionality: 4:
+ section Engagement
+ Browse Features: 3
+ Use Core Functionality: 4
+ `,
+ { journey: { useMaxWidth: true } }
+ );
+
+ let diagramStartX;
+
+ cy.contains('foreignobject', 'Sign Up').then(($diagram) => {
+ diagramStartX = parseFloat($diagram.attr('x'));
+ expect(diagramStartX).to.be.closeTo(150, 2);
+ });
+ });
+
+ it('should maintain sufficient space between legend and diagram when legend labels are longer', () => {
+ renderGraph(
+ `journey
+ title Web hook life cycle
+ section Darkoob
+ Make preBuilt:5: Darkoob user
+ register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained
+ Map slug to a Prebuilt Job:5: Darkoob user
+ section External Service
+ set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty
+ listen to the events : 5 : External Service
+ call darkoob endpoint : 5 : External Service
+ section Darkoob
+ check for inputs : 5 : DarkoobAPI
+ run the prebuilt job : 5 : DarkoobAPI
+ `,
+ { journey: { useMaxWidth: true } }
+ );
+
+ let LabelEndX, diagramStartX;
+
+ // Get right edge of the legend
+ cy.contains('tspan', 'Darkoob userf').then((textBox) => {
+ const bbox = textBox[0].getBBox();
+ LabelEndX = bbox.x + bbox.width;
+ });
+
+ // Get left edge of the diagram
+ cy.contains('foreignobject', 'Make preBuilt').then((rect) => {
+ diagramStartX = parseFloat(rect.attr('x'));
+ });
+
+ // Assert right edge of the diagram is greater than or equal to the right edge of the label
+ cy.then(() => {
+ expect(diagramStartX).to.be.gte(LabelEndX);
+ });
+ });
+
+ it('should wrap a single long word with hyphenation', () => {
+ renderGraph(
+ `
+ ---
+ config:
+ journey:
+ maxLabelWidth: 100
+ ---
+ journey
+ title Long Word Test
+ section Test
+ VeryLongWord: 5: Supercalifragilisticexpialidocious
+ `,
+ { journey: { useMaxWidth: true } }
+ );
+
+ // Verify that the line ends with a hyphen, indicating proper hyphenation for words exceeding maxLabelWidth.
+ cy.get('tspan').then((tspans) => {
+ const hasHyphen = [...tspans].some((t) => t.textContent.trim().endsWith('-'));
+ return expect(hasHyphen).to.be.true;
+ });
+ });
+
+ it('should wrap text on whitespace without adding hyphens', () => {
+ renderGraph(
+ `
+ ---
+ config:
+ journey:
+ maxLabelWidth: 200
+ ---
+ journey
+ title Whitespace Test
+ section Test
+ TextWithSpaces: 5: Gustavo Fring is played by Giancarlo Esposito and is a character in Breaking Bad.
+ `,
+ { journey: { useMaxWidth: true } }
+ );
+
+ // Verify that none of the text spans end with a hyphen.
+ cy.get('tspan').each(($el) => {
+ const text = $el.text();
+ expect(text.trim()).not.to.match(/-$/);
+ });
+ });
+
+ it('should wrap long labels into multiple lines, keep them under max width, and maintain margins', () => {
+ renderGraph(
+ `
+ ---
+ config:
+ journey:
+ maxLabelWidth: 320
+ ---
+ journey
+ title User Journey Example
+ section Onboarding
+ Sign Up: 5: This is a long label that will be split into multiple lines to test the wrapping functionality
+ Browse Features: 3: This is another long label that will be split into multiple lines to test the wrapping functionality
+ Use Core Functionality: 4: This is yet another long label that will be split into multiple lines to test the wrapping functionality
+ section Engagement
+ Browse Features: 3
+ Use Core Functionality: 4
+ `,
+ { journey: { useMaxWidth: true } }
+ );
+
+ let diagramStartX, maxLineWidth;
+
+ // Get the diagram's left edge x-coordinate
+ cy.contains('foreignobject', 'Sign Up')
+ .then(($diagram) => {
+ diagramStartX = parseFloat($diagram.attr('x'));
+ })
+ .then(() => {
+ cy.get('text.legend').then(($lines) => {
+ // Check that there are multiple lines
+ expect($lines.length).to.be.equal(9);
+
+ // Check that all lines are under the maxLabelWidth
+ $lines.each((index, el) => {
+ const bbox = el.getBBox();
+ expect(bbox.width).to.be.lte(320);
+ maxLineWidth = Math.max(maxLineWidth || 0, bbox.width);
+ });
+
+ /** The expected margin between the diagram and the legend is 150px, as defined by
+ * conf.leftMargin in user-journey-config.js
+ */
+ expect(diagramStartX - maxLineWidth).to.be.closeTo(150, 2);
+ });
+ });
+ });
});
diff --git a/cypress/integration/rendering/radar.spec.js b/cypress/integration/rendering/radar.spec.js
new file mode 100644
index 000000000..b0bc3f6e0
--- /dev/null
+++ b/cypress/integration/rendering/radar.spec.js
@@ -0,0 +1,79 @@
+import { imgSnapshotTest } from '../../helpers/util';
+
+describe('radar structure', () => {
+ it('should render a simple radar diagram', () => {
+ imgSnapshotTest(
+ `radar-beta
+ title Best Radar Ever
+ axis A, B, C
+ curve c1{1, 2, 3}
+ `
+ );
+ });
+
+ it('should render a radar diagram with multiple curves', () => {
+ imgSnapshotTest(
+ `radar-beta
+ title Best Radar Ever
+ axis A, B, C
+ curve c1{1, 2, 3}
+ curve c2{2, 3, 1}
+ `
+ );
+ });
+
+ it('should render a complex radar diagram', () => {
+ imgSnapshotTest(
+ `radar-beta
+ title My favorite ninjas
+ axis Agility, Speed, Strength
+ axis Stam["Stamina"] , Intel["Intelligence"]
+
+ curve Ninja1["Naruto Uzumaki"]{
+ Agility 2, Speed 2,
+ Strength 3, Stam 5,
+ Intel 0
+ }
+ curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
+ curve Ninja3 {3, 2, 1, 5, 4}
+
+ showLegend true
+ ticks 3
+ max 8
+ min 0
+ graticule polygon
+ `
+ );
+ cy.get('svg').should((svg) => {
+ expect(svg).to.have.length(1);
+ });
+ });
+
+ it('should render radar diagram with config override', () => {
+ imgSnapshotTest(
+ `radar-beta
+ title Best Radar Ever
+ axis A,B,C
+ curve mycurve{1,2,3}`,
+ { radar: { marginTop: 100, axisScaleFactor: 0.5 } }
+ );
+ });
+
+ it('should parse radar diagram with theme override', () => {
+ imgSnapshotTest(
+ `radar-beta
+ axis A,B,C
+ curve mycurve{1,2,3}`,
+ { theme: 'base', themeVariables: { fontSize: 80, cScale0: '#FF0000' } }
+ );
+ });
+
+ it('should handle radar diagram with radar style override', () => {
+ imgSnapshotTest(
+ `radar-beta
+ axis A,B,C
+ curve mycurve{1,2,3}`,
+ { theme: 'base', themeVariables: { radar: { axisColor: '#FF0000' } } }
+ );
+ });
+});
diff --git a/cypress/platform/yari2.html b/cypress/platform/yari2.html
new file mode 100644
index 000000000..bd5ddffc2
--- /dev/null
+++ b/cypress/platform/yari2.html
@@ -0,0 +1,337 @@
+
+
+ --- + config: + htmlLabels: false + look: handDrawn + theme: forest + --- + erDiagram + _**hiØ**_[*test*] { + *__List~List~int~~sdfds__* __driversLicense__ PK "***The l😀icense #***" + *string(99)~T~~~~~~* firstName "Only 99+
characters are a
llowed dsfsdfsdfsdfs" + ~str ing~ lastName + string phone UK + int age + } + style PERSON color:red, stroke:blue,fill:#f9f + classDef test,test2 stroke:red + class PERSON test,test2 +
+ erDiagram + CAR { + string registrationNumber + string make + string model + } + PERSON { + string firstName + string lastName + int age + } + + CAR:::someclass + PERSON:::anotherclass,someclass + + classDef someclass fill:#f96 + classDef anotherclass color:blue ++
+ --- + config: + htmlLabels: false + layout: elk + look: handDrawn + theme: forest + --- + erDiagram + "hi" }o..o{ ORDER : places + style hi fill:lightblue ++
+ --- + config: + htmlLabels: false + look: handDrawn + layout: elk + --- + erDiagram + CAR ||--|{ NAMED-DRIVER : allows + PERSON ||..o{ NAMED-DRIVER : is ++
+ --- + config: + htmlLabels: true + look: handDrawn + theme: forest + --- + erDiagram + CAR ||--o{ NAMED-DRIVER : allows + CAR { + test test PK "comment" + string make + string model + string[] parts + } + PERSON ||--o{ NAMED-DRIVER : is + PERSON ||--o{ CAR : is + PERSON { + string driversLicense PK "The license #" + string(99) firstName "Only 99 characters are allowed" + string lastName + string phone UK + int age + } + NAMED-DRIVER { + string carRegistrationNumber PK, FK + string driverLicence PK, FK + } + MANUFACTURER only one to zero or more CAR : makes ++
+ --- + title: simple ER diagram + config: + theme: forest + --- + erDiagram + direction TB + p[Pers😀on] { + string firstName + string lastName + } + a["Customer Account"] { + string email + } + p ||--o| a : has + ++
+ --- + config: + layout: elk + --- + erDiagram + CUSTOMER }|..|{ DELIVERY-ADDRESS : has + CUSTOMER ||--o{ ORDER : places + CUSTOMER ||--o{ INVOICE : "liable for" + DELIVERY-ADDRESS ||--o{ ORDER : receives + INVOICE ||--|{ ORDER : covers + ORDER ||--|{ ORDER-ITEM : includes + PRODUCT-CATEGORY ||--|{ PRODUCT : contains + PRODUCT ||--o{ ORDER-ITEM : "ordered in" ++
+--- + config: + layout: elk +--- + erDiagram + rental{ + ~timestamp with time zone~ rental_date "NN" + ~integer~ inventory_id "NN" + ~integer~ customer_id "NN" + ~timestamp with time zone~ return_date + ~integer~ staff_id "NN" + ~integer~ rental_id "NN" + ~timestamp with time zone~ last_update "NN" + } + film_actor{ + ~integer~ actor_id "NN" + ~integer~ film_id "NN" + ~timestamp with time zone~ last_update "NN" + } + film{ + ~text~ title "NN" + ~text~ description + ~public.year~ release_year + ~integer~ language_id "NN" + ~integer~ original_language_id + ~smallint~ length + ~text[]~ special_features + ~tsvector~ fulltext "NN" + ~integer~ film_id "NN" + ~smallint~ rental_duration "NN" + ~numeric(4,2)~ rental_rate "NN" + ~numeric(5,2)~ replacement_cost "NN" + ~public.mpaa_rating~ rating + ~timestamp with time zone~ last_update "NN" + } + customer{ + ~integer~ store_id "NN" + ~text~ first_name "NN" + ~text~ last_name "NN" + ~text~ email + ~integer~ address_id "NN" + ~integer~ active + ~integer~ customer_id "NN" + ~boolean~ activebool "NN" + ~date~ create_date "NN" + ~timestamp with time zone~ last_update + } + film_category{ + ~integer~ film_id "NN" + ~integer~ category_id "NN" + ~timestamp with time zone~ last_update "NN" + } + actor{ + ~text~ first_name "NN" + ~text~ last_name "NN" + ~integer~ actor_id "NN" + ~timestamp with time zone~ last_update "NN" + } + store{ + ~integer~ manager_staff_id "NN" + ~integer~ address_id "NN" + ~integer~ store_id "NN" + ~timestamp with time zone~ last_update "NN" + } + city{ + ~text~ city "NN" + ~integer~ country_id "NN" + ~integer~ city_id "NN" + ~timestamp with time zone~ last_update "NN" + } + language{ + ~character(20)~ name "NN" + ~integer~ language_id "NN" + ~timestamp with time zone~ last_update "NN" + } + payment{ + ~integer~ customer_id "NN" + ~integer~ staff_id "NN" + ~integer~ rental_id "NN" + ~numeric(5,2)~ amount "NN" + ~timestamp with time zone~ payment_date "NN" + ~integer~ payment_id "NN" + } + category{ + ~text~ name "NN" + ~integer~ category_id "NN" + ~timestamp with time zone~ last_update "NN" + } + inventory{ + ~integer~ film_id "NN" + ~integer~ store_id "NN" + ~integer~ inventory_id "NN" + ~timestamp with time zone~ last_update "NN" + } + address{ + ~text~ address "NN" + ~text~ address2 + ~text~ district "NN" + ~integer~ city_id "NN" + ~text~ postal_code + ~text~ phone "NN" + ~integer~ address_id "NN" + ~timestamp with time zone~ last_update "NN" + } + staff{ + ~text~ first_name "NN" + ~text~ last_name "NN" + ~integer~ address_id "NN" + ~text~ email + ~integer~ store_id "NN" + ~text~ username "NN" + ~text~ password + ~bytea~ picture + ~integer~ staff_id "NN" + ~boolean~ active "NN" + ~timestamp with time zone~ last_update "NN" + } + country{ + ~text~ country "NN" + ~integer~ country_id "NN" + ~timestamp with time zone~ last_update "NN" + } + film_actor }|..|| film : film_actor_film_id_fkey + film_actor }|..|| actor : film_actor_actor_id_fkey + address }|..|| city : address_city_id_fkey + city }|..|| country : city_country_id_fkey + customer }|..|| store : customer_store_id_fkey + customer }|..|| address : customer_address_id_fkey + film }|..|| language : film_original_language_id_fkey + film }|..|| language : film_language_id_fkey + film_category }|..|| film : film_category_film_id_fkey + film_category }|..|| category : film_category_category_id_fkey + inventory }|..|| store : inventory_store_id_fkey ++