Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-radios-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mermaid': patch
---

fix: Prevent crash when flowchart node shape is undefined
33 changes: 32 additions & 1 deletion cypress/integration/rendering/flowchart/flowchart-v2.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
};
Loading