diff --git a/.changeset/fix-7609-numeric-subgraph-id.md b/.changeset/fix-7609-numeric-subgraph-id.md new file mode 100644 index 00000000000..301a9014265 --- /dev/null +++ b/.changeset/fix-7609-numeric-subgraph-id.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix(flowchart): render nested subgraphs with numeric ids without dagre rank crash (#7609) diff --git a/cypress/integration/rendering/flowchart/flowchart.spec.js b/cypress/integration/rendering/flowchart/flowchart.spec.js index 55cf02a7454..268ca74264d 100644 --- a/cypress/integration/rendering/flowchart/flowchart.spec.js +++ b/cypress/integration/rendering/flowchart/flowchart.spec.js @@ -1003,6 +1003,23 @@ graph TD } ); }); + it('#7609: should render nested subgraph with numeric id without crashing', () => { + imgSnapshotTest( + ` + graph LR + subgraph outer + subgraph 1 ["inner"] + external + subgraph sub + internal + end + sub-->external + end + end + `, + { fontFamily: 'courier' } + ); + }); }); it('#5824: should be able to render string and markdown labels', () => { imgSnapshotTest( diff --git a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js index 7e5c5f582ff..1752e2446a2 100644 --- a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js +++ b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js @@ -363,6 +363,19 @@ export const extractor = (graph, depth) => { // containing the nodes and edges in the cluster in a new graph // for (let i = 0;) let nodes = graph.nodes(); + // graphlib backs nodes with a plain object, so integer-like string ids + // ("1") sort ahead of others ("outer") and reorder iteration. Process + // outer clusters first so nested extraction is order-independent. (#7609) + const nodeDepth = (v) => { + let d = 0; + let cur = graph.parent(v); + while (cur != null) { + d++; + cur = graph.parent(cur); + } + return d; + }; + nodes = [...nodes].sort((a, b) => nodeDepth(a) - nodeDepth(b)); let hasChildren = false; for (const node of nodes) { const children = graph.children(node); diff --git a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.spec.js b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.spec.js index d44e5439110..b76041f309a 100644 --- a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.spec.js +++ b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.spec.js @@ -373,6 +373,40 @@ describe('Graphlib decorations', () => { expect(bGraph.edges().length).toBe(0); }); }); + it('adjustClustersAndEdges should extract nested clusters when a subgraph has a numeric id (issue #7609)', function () { + /* + graph LR + subgraph outer + subgraph 1 ["inner"] + external + subgraph sub + internal + end + sub-->external + end + end + */ + g.setNode('outer', { id: 'outer' }); + g.setNode('1', { id: '1' }); + g.setNode('sub', { id: 'sub' }); + g.setNode('external', { id: 'external' }); + g.setNode('internal', { id: 'internal' }); + g.setParent('1', 'outer'); + g.setParent('sub', '1'); + g.setParent('external', '1'); + g.setParent('internal', 'sub'); + g.setEdge('sub', 'external', { data: 'sub-external' }); + + adjustClustersAndEdges(g); + + const outerGraph = g.node('outer').graph; + const innerGraph = outerGraph.node('1').graph; + expect(innerGraph.node('sub')?.clusterNode).toBe(true); + const subGraph = innerGraph.node('sub').graph; + expect(subGraph.nodes()).toEqual(['internal']); + expect(innerGraph.children('sub')).toEqual([]); + }); + it('adjustClustersAndEdges should handle nesting GLB77', function () { /* flowchart TB 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..48feed70f66 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 @@ -336,6 +336,19 @@ export const extractor = (graph, depth) => { return; } let nodes = graph.nodes(); + // graphlib backs nodes with a plain object, so integer-like string ids + // ("1") sort ahead of others ("outer") and reorder iteration. Process + // outer clusters first so nested extraction is order-independent. (#7609) + const nodeDepth = (v) => { + let d = 0; + let cur = graph.parent(v); + while (cur != null) { + d++; + cur = graph.parent(cur); + } + return d; + }; + nodes = [...nodes].sort((a, b) => nodeDepth(a) - nodeDepth(b)); let hasChildren = false; for (const node of nodes) { const children = graph.children(node); 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..12cf2e9562b 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 @@ -373,6 +373,40 @@ describe('Graphlib decorations', () => { expect(bGraph.edges().length).toBe(0); }); }); + it('adjustClustersAndEdges should extract nested clusters when a subgraph has a numeric id (issue #7609)', function () { + /* + graph LR + subgraph outer + subgraph 1 ["inner"] + external + subgraph sub + internal + end + sub-->external + end + end + */ + g.setNode('outer', { id: 'outer' }); + g.setNode('1', { id: '1' }); + g.setNode('sub', { id: 'sub' }); + g.setNode('external', { id: 'external' }); + g.setNode('internal', { id: 'internal' }); + g.setParent('1', 'outer'); + g.setParent('sub', '1'); + g.setParent('external', '1'); + g.setParent('internal', 'sub'); + g.setEdge('sub', 'external', { data: 'sub-external' }); + + adjustClustersAndEdges(g); + + const outerGraph = g.node('outer').graph; + const innerGraph = outerGraph.node('1').graph; + expect(innerGraph.node('sub')?.clusterNode).toBe(true); + const subGraph = innerGraph.node('sub').graph; + expect(subGraph.nodes()).toEqual(['internal']); + expect(innerGraph.children('sub')).toEqual([]); + }); + it('adjustClustersAndEdges should handle nesting GLB77', function () { /* flowchart TB