diff --git a/.changeset/brave-radios-live.md b/.changeset/brave-radios-live.md new file mode 100644 index 00000000000..8f24a2324b1 --- /dev/null +++ b/.changeset/brave-radios-live.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Prevent crash when flowchart node shape is undefined diff --git a/cypress/integration/rendering/flowchart/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart/flowchart-v2.spec.js index 4cb54e5087a..9adb1f4be39 100644 --- a/cypress/integration/rendering/flowchart/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart/flowchart-v2.spec.js @@ -1290,6 +1290,37 @@ class link myClass ); }); + it('V2 - 18: should render nested subgraphs with edge from cluster containing extractable subgraph', () => { + imgSnapshotTest( + `flowchart TB + subgraph asub + aa + end + a + subgraph bsub + subgraph csub + subgraph dsub + da + end + end + subgraph esub + subgraph fsub + %%comment out subgraph below + subgraph gsub + ga + abc + abcd + end + end + end + end + bsub-->a + da-->a + `, + {} + ); + }); + describe('Edge label autowrapping', () => { it('should wrap edge labels', () => { imgSnapshotTest( @@ -1308,7 +1339,7 @@ config: ${JSON.stringify({ markdownAutoWrap, htmlLabels })} title: Testing with ${JSON.stringify({ markdownAutoWrap, htmlLabels })} --- flowchart TD - A["This is a really long line of plain text that will autowrap and support \\n newlines too."] + A["This is a really long line of plain text that will autowrap and support \\n newlines too."] B["\`This is a really long line of **markdown** text that will autowrap, unless markdownAutoWrap:false is set.\`"] A -- "Plain text **labels** in flowcharts will autowrap,like node labels. \\n Newline characters work too." --> B B -- "\`**Markdown** edge labels will autowrap, unless markdownAutoWrap: false is set\`" --> C 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 f0e5cd5ed9b..bab1c6c479f 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 @@ -247,6 +247,22 @@ export const adjustClustersAndEdges = (graph, depth) => { if (parent !== id && clusterDb.has(parent) && !clusterDb.get(parent).externalConnections) { clusterDb.get(id).id = parent; } + // When this cluster has a direct outgoing edge AND its current anchor sits inside + // a sibling subgraph that will be extracted (collapsed into a clusterNode), the + // anchor will disappear by render time and the edge endpoint becomes undefined. + // Re-anchor onto a node that survives extraction. + const hasDirectOutgoingEdge = graph.edges().some((edge) => edge.v === id); + if ( + nonClusterChild && + clusterDb.get(id)?.externalConnections && + hasDirectOutgoingEdge && + isNodeInExtractableCluster(graph, nonClusterChild, id) + ) { + const safeAnchor = findSafeAnchorNode(graph, id, graph.parent(nonClusterChild)); + if (safeAnchor) { + clusterDb.get(id).id = safeAnchor; + } + } } graph.edges().forEach(function (e) { @@ -411,3 +427,43 @@ const sorter = (graph, nodes) => { }; export const sortNodesByHierarchy = (graph) => sorter(graph, graph.children()); + +/** Checks if a node is inside a cluster that will be extracted (has no external connections). */ +const isNodeInExtractableCluster = (graph, node, rootId) => { + let parent = graph.parent(node); + + while (parent && parent !== rootId) { + const cluster = clusterDb.get(parent); + if (cluster && !cluster.externalConnections) { + return true; + } + parent = graph.parent(parent); + } + + return false; +}; + +/** Finds an alternative anchor node for a cluster that is not inside an extractable cluster. */ +const findSafeAnchorNode = (graph, clusterId, excludedCluster) => { + const children = graph.children(clusterId) ?? []; + + for (const child of children) { + if (child === excludedCluster || isDescendant(child, excludedCluster)) { + continue; + } + + // findNonClusterChild returns the leaf itself when child is a leaf, or drills + // into a subgraph to find a non-cluster descendant. A returned leaf sibling is + // a perfectly valid anchor — only skip when the lookup found nothing usable. + const candidate = findNonClusterChild(child, graph, clusterId); + if (!candidate) { + continue; + } + + if (!isNodeInExtractableCluster(graph, candidate, clusterId)) { + return candidate; + } + } + + return null; +};