diff --git a/.changeset/cold-mails-fly.md b/.changeset/cold-mails-fly.md new file mode 100644 index 00000000000..c9c87a65c1f --- /dev/null +++ b/.changeset/cold-mails-fly.md @@ -0,0 +1,5 @@ +--- +'@mermaid-js/parser': major +--- + +enforce eventmodeling connection invariants via Langium validator diff --git a/.changeset/cold-snakes-attend.md b/.changeset/cold-snakes-attend.md new file mode 100644 index 00000000000..ff17804c148 --- /dev/null +++ b/.changeset/cold-snakes-attend.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: updated the parser to require a new line boundary before the "end note" keyword to ensure it is only matched when used as an actual note terminator. diff --git a/.changeset/eager-cobras-slide.md b/.changeset/eager-cobras-slide.md new file mode 100644 index 00000000000..f0b240ad0ea --- /dev/null +++ b/.changeset/eager-cobras-slide.md @@ -0,0 +1,7 @@ +--- +'mermaid': minor +--- + +feat: add datastore shape for flowchart + +In Data flow diagrams, a datastore/warehouse/file/database is used to represent data persistence. It is denoted by a rectangle with top and bottom border. This change adds that as a shape in flowcharts. diff --git a/.changeset/feat-nested-namespaces.md b/.changeset/feat-nested-namespaces.md new file mode 100644 index 00000000000..e6efcf0645d --- /dev/null +++ b/.changeset/feat-nested-namespaces.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +feat: add nested namespace support for class diagrams via dot notation and syntactic nesting diff --git a/.changeset/fix-7560-self-referential-multiplicity.md b/.changeset/fix-7560-self-referential-multiplicity.md new file mode 100644 index 00000000000..d0974dc009a --- /dev/null +++ b/.changeset/fix-7560-self-referential-multiplicity.md @@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix: self-referential class multiplicity labels no longer rendered multiple times + +Fixes #7560. Resolves an issue where cardinality labels on self-referential class relationships were rendered three times due to edge splitting in the dagre layout. The fix ensures that each sub-edge only carries its relevant label positions. diff --git a/.changeset/fix-mindmap-tidy-tree-root-edges.md b/.changeset/fix-mindmap-tidy-tree-root-edges.md new file mode 100644 index 00000000000..d6c17e796bf --- /dev/null +++ b/.changeset/fix-mindmap-tidy-tree-root-edges.md @@ -0,0 +1,6 @@ +--- +'@mermaid-js/layout-tidy-tree': patch +'mermaid': patch +--- + +fix: keep mindmap edges connected to a non-circular root when using the tidy-tree layout diff --git a/.changeset/three-melons-argue.md b/.changeset/three-melons-argue.md new file mode 100644 index 00000000000..0ddaad9a713 --- /dev/null +++ b/.changeset/three-melons-argue.md @@ -0,0 +1,6 @@ +--- +'mermaid': major +'@mermaid-js/parser': major +--- + +fix: replace Event Modeling `screen` / `scn` terminology with `ui` diff --git a/.changeset/yummy-moose-wait.md b/.changeset/yummy-moose-wait.md new file mode 100644 index 00000000000..49b0bef8221 --- /dev/null +++ b/.changeset/yummy-moose-wait.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: updated the parser to only treat comments starting with %%. diff --git a/cypress/integration/rendering/architecture/architecture.spec.ts b/cypress/integration/rendering/architecture/architecture.spec.ts index dd52aa8cf9e..173965765e7 100644 --- a/cypress/integration/rendering/architecture/architecture.spec.ts +++ b/cypress/integration/rendering/architecture/architecture.spec.ts @@ -10,40 +10,6 @@ describe('architecture diagram', () => { service disk1(disk)[Storage] in api service disk2(disk)[Storage] in api service server(server)[Server] in api - service gateway(internet)[Gateway] - - db:L -- R:server - disk1:T -- B:server - disk2:T -- B:db - server:T -- B:gateway - ` - ); - }); - it('should render a simple architecture diagram with titleAndAccessibilities', () => { - 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 - group api[API] - group public[Public API] in api - group private[Private API] in api service serv1(server)[Server] in public @@ -73,6 +39,7 @@ describe('architecture diagram', () => { service serv1(server)[Server 1] service serv2(server)[Server 2] service disk(disk)[Disk] + db:L -- R:s3 serv1:L -- T:s3 @@ -89,11 +56,13 @@ describe('architecture diagram', () => { service servR(server)[Server 3] service servT(server)[Server 4] service servB(server)[Server 5] + servC:L --> R:servL servC:R --> L:servR servC:T --> B:servT servC:B --> T:servB + servL:T --> L:servT servL:B --> L:servB @@ -110,12 +79,14 @@ describe('architecture diagram', () => { group top_group(cloud)[Top] group bottom_group(cloud)[Bottom] group center_group(cloud)[Center] + service left_disk(disk)[Disk] in left_group service right_disk(disk)[Disk] in right_group service top_disk(disk)[Disk] in top_group service bottom_disk(disk)[Disk] in bottom_group service center_disk(disk)[Disk] in center_group + left_disk{group}:R --> L:center_disk{group} right_disk{group}:L --> R:center_disk{group} @@ -132,11 +103,13 @@ describe('architecture diagram', () => { service servR(server)[Server 3] service servT(server)[Server 4] service servB(server)[Server 5] + servC:L -[Label]- R:servL servC:R -[Label]- L:servR servC:T -[Label]- B:servT servC:B -[Label]- T:servB + servL:T -[Label]- L:servT servL:B -[Label]- L:servB @@ -155,6 +128,7 @@ describe('architecture diagram', () => { service bottom_gateway(internet)[Gateway] junction juncC junction juncR + left_disk:R -- L:juncC top_disk:B -- T:juncC @@ -280,6 +254,7 @@ describe('architecture diagram', () => { web:B --> T:pe2 pe2:R --> L:bus vm1:R --> L:pe2 + ` `, { architecture: { randomize: false } } ); diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index d44e3a35d55..b64ba5c9dde 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -1250,4 +1250,104 @@ flowchart TD } ); }); + + describe('flowchart numeric subgraph ids', () => { + beforeEach(() => { + mermaidAPI.globalReset(); + mermaid.initialize({ + startOnLoad: false, + deterministicIds: true, + deterministicIDSeed: '', + flowchart: { htmlLabels: false }, + }); + }); + + jsdomIt('renders a flowchart with a numeric subgraph id', async () => { + const diagramText = `flowchart LR + subgraph 1 ["inner"] + A + end`; + + await expect(mermaidAPI.render('numeric-subgraph-id', diagramText)).resolves.toMatchObject({ + svg: expect.any(String), + }); + }); + + jsdomIt('renders nested flowcharts with numeric subgraph ids', async () => { + const diagramText = `flowchart LR + subgraph 1 ["outer"] + subgraph 2 ["inner"] + A --> B + end + end + B --> C`; + + await expect( + mermaidAPI.render('nested-numeric-subgraph-id', diagramText) + ).resolves.toMatchObject({ + svg: expect.any(String), + }); + }); + + jsdomIt('renders a flowchart when a numeric subgraph id has an outgoing edge', async () => { + const diagramText = `flowchart LR + subgraph 1 ["outer"] + subgraph 2 ["inner"] + A --> B + end + end + 1 --> C`; + + await expect(mermaidAPI.render('numeric-subgraph-edge', diagramText)).resolves.toMatchObject({ + svg: expect.any(String), + }); + }); + + jsdomIt('renders the alphabetic equivalent of the numeric subgraph edge case', async () => { + const diagramText = `flowchart LR + subgraph a ["outer"] + subgraph b ["inner"] + A --> B + end + end + a --> C`; + + await expect( + mermaidAPI.render('alphabetic-subgraph-edge', diagramText) + ).resolves.toMatchObject({ + svg: expect.any(String), + }); + }); + + jsdomIt( + 'reproduces issue #7609 with the exact graph LR repro through the Dagre render path', + async () => { + mermaid.initialize({ + startOnLoad: false, + deterministicIds: true, + deterministicIDSeed: '', + flowchart: { htmlLabels: false, defaultRenderer: 'dagre-wrapper' }, + }); + + // Regression coverage only: this preserves the exact repro from #7609 and documents + // the current Dagre failure instead of claiming the numeric subgraph bug is fixed. + const diagramText = `graph LR + subgraph outer + subgraph 1 ["inner"] + external + subgraph sub + internal + end + sub-->external + end + end`; + + const { svg } = await mermaidAPI.render('numeric-subgraph-issue-7609', diagramText); + + expect(svg).toContain('class="cluster'); + expect(svg).toContain('>inner<'); + expect(svg).toContain('>external<'); + } + ); + }); }); diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.js index ffe9062154f..d5b08528f9e 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.js +++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.js @@ -40,6 +40,21 @@ const edgeInCluster = (edge, clusterId) => { ); }; +const edgeLeavesCluster = (edge, clusterId) => { + const fromDescendant = isDescendant(edge.v, clusterId); + const toDescendant = isDescendant(edge.w, clusterId); + + if (edge.v === clusterId) { + return !toDescendant; + } + + if (edge.w === clusterId) { + return !fromDescendant; + } + + return fromDescendant !== toDescendant; +}; + const copy = (clusterId, graph, newGraph, rootId) => { log.warn( 'Copying children of ', @@ -247,10 +262,7 @@ export const adjustClustersAndEdges = (graph, depth) => { if (children.length > 0) { log.debug('Cluster identified', id, descendants); edges.forEach((edge) => { - const d1 = isDescendant(edge.v, id); - const d2 = isDescendant(edge.w, id); - - if (d1 ^ d2) { + if (edgeLeavesCluster(edge, id)) { log.warn('Edge: ', edge, ' leaves cluster ', id); log.warn('Descendants of XXX ', id, ': ', descendants.get(id)); clusterDb.get(id).externalConnections = true; diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js index 2e21b8ec4d9..cf265c7f950 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js +++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js @@ -372,6 +372,38 @@ describe('Graphlib decorations', () => { expect(cGraph.nodes().length).toBe(1); expect(bGraph.edges().length).toBe(0); }); + + it('adjustClustersAndEdges should rewrite edges from nested cluster nodes to descendant anchors GLB78', function () { + /* + subgraph outer + subgraph 1 [inner] + external + subgraph sub + internal + end + sub --> external + end + end + */ + g.setNode('outer', { data: 1 }); + g.setNode('1', { data: 2 }); + g.setNode('sub', { data: 3 }); + g.setNode('external', { data: 4 }); + g.setNode('internal', { data: 5 }); + g.setParent('1', 'outer'); + g.setParent('external', '1'); + g.setParent('sub', '1'); + g.setParent('internal', 'sub'); + g.setEdge('sub', 'external', { data: 'link1' }, '1'); + + adjustClustersAndEdges(g); + + const outerGraph = g.node('outer').graph; + const innerGraph = outerGraph.node('1').graph; + + expect(innerGraph.edges()).toEqual([{ v: 'internal', w: 'external', name: '1' }]); + expect(validate(innerGraph)).toBe(true); + }); }); it('adjustClustersAndEdges should handle nesting GLB77', function () { /* diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e744d699096..4f4f4b658c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4766,6 +4766,10 @@ packages: resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -15549,6 +15553,13 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 diff --git a/scripts/e2e-diagram-scope.mjs b/scripts/e2e-diagram-scope.mjs index ffca13da093..43177a7c072 100644 --- a/scripts/e2e-diagram-scope.mjs +++ b/scripts/e2e-diagram-scope.mjs @@ -211,6 +211,7 @@ export function detectScope(files, options = {}) { continue; } + // Anything else (root config, CI YAML, docs, cypress/other, etc.) → full suite // Ignorable files (docs, changesets, AI config, etc.) → skip silently. // Guard: .md files inside a diagram source folder are NOT ignorable — they // may be samples or signal intent, and their diagram folder was already