Skip to content
Open
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
2 changes: 1 addition & 1 deletion cypress/platform/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const contentLoaded = async function () {
pos = pos + 7;
const graphBase64 = document.location.href.substr(pos);
const graphObj = JSON.parse(b64ToUtf8(graphBase64));
if (graphObj.mermaid && graphObj.mermaid.theme === 'dark') {
if (graphObj.mermaid?.theme === 'dark') {
document.body.style.background = '#3f3f3f';
}
console.log(graphObj);
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/src/diagrams/git/gitGraphAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ function prettyPrintCommitHistory(commitArr: Commit[]) {
}
}
log.debug(label.join(' '));
if (commit.parents && commit.parents.length == 2 && commit.parents[0] && commit.parents[1]) {
if (commit.parents?.length == 2 && commit.parents?.[0] && commit.parents?.[1]) {
const newCommit = state.records.commits.get(commit.parents[0]);
upsert(commitArr, commit, newCommit);
if (commit.parents[1]) {
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/src/diagrams/sequence/sequenceDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export class SequenceDB implements DiagramDB {
old.box = assignedBox;

// Don't allow description nulling
if (old && name === old.name && description == null) {
if (old?.name === name && description == null) {
return;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/src/docs/.vitepress/homepageHeroCopy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const LOG_PREFIX = '[MMD_DOCS_HERO]';
* Note: index.md frontmatter is static, so this is applied via VitePress transformPageData.
*/
export function applyHomePageHeroCopy(pageData: any, hostname: string): void {
if (!pageData || pageData.relativePath !== 'index.md') {
if (pageData?.relativePath !== 'index.md') {
return;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/mermaid/src/docs/.vitepress/theme/mermaid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import tidyTreeLayout from '../../../../../mermaid-layout-tidy-tree/dist/mermaid
import layouts from '../../../../../mermaid-layout-elk/dist/mermaid-layout-elk.core.mjs';

const init = Promise.all([
mermaid.registerExternalDiagrams([zenuml]),
mermaid.registerLayoutLoaders(layouts),
mermaid.registerLayoutLoaders(tidyTreeLayout),
Promise.resolve(mermaid.registerExternalDiagrams([zenuml])),
Promise.resolve(mermaid.registerLayoutLoaders(layouts)),
Promise.resolve(mermaid.registerLayoutLoaders(tidyTreeLayout)),
]);
mermaid.registerIconPacks([
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit

dagreLayout(graph);

// Strip out any virtual ordering edges that were injected for edge-less
// clusters (see mermaid-graphlib.js). They must be removed before rendering
// so that no SVG edge path is drawn for them.
graph.edges().forEach(function (e) {
if (graph.edge(e)?._virtual) {
graph.removeEdge(e.v, e.w, e.name);
}
});

log.info('Graph after layout:', JSON.stringify(graphlibJson.write(graph)));
// Move the nodes to the correct place
let diff = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,16 @@ export const adjustClustersAndEdges = (graph, depth) => {
graph.removeEdge(e.v, e.w, e.name);
if (v !== e.v) {
const parent = graph.parent(v);
clusterDb.get(parent).externalConnections = true;
if (parent && clusterDb.has(parent)) {
clusterDb.get(parent).externalConnections = true;
}
edge.fromCluster = e.v;
}
if (w !== e.w) {
const parent = graph.parent(w);
clusterDb.get(parent).externalConnections = true;
if (parent && clusterDb.has(parent)) {
clusterDb.get(parent).externalConnections = true;
}
edge.toCluster = e.w;
}
log.warn('Fix Replacing with XXX', v, w, e.name);
Expand Down Expand Up @@ -359,11 +363,35 @@ export const extractor = (graph, depth) => {

log.warn('Old graph before copy', graphlibJson.write(graph));
copy(node, graph, clusterGraph, node);

// When a subgraph cluster that has an explicitly declared direction
// contains no edges, Dagre places all nodes in a single rank and lays
// them out along the cross-axis of that rankdir (e.g. `direction LR`
// produces a vertical column). To honour the user's declared direction
// we inject temporary, layout-only ordering edges (A → B → C → D …) that
// force Dagre to distribute the nodes across separate ranks. The edges
// are marked with `_virtual: true` so they can be stripped out in the
// rendering phase before any SVG paths are drawn.
const clusterMeta = clusterDb.get(node);
if (clusterMeta?.clusterData?.dir && clusterGraph.edges().length === 0) {
const clusterNodes = clusterGraph.nodes();
log.warn('Cluster has no edges, adding virtual ordering edges for layout', clusterNodes);
for (let i = 0; i < clusterNodes.length - 1; i++) {
clusterGraph.setEdge(clusterNodes[i], clusterNodes[i + 1], {
_virtual: true,
weight: 0,
minlen: 1,
});
}
}

const existingNodeData = graph.node(node) || {};
graph.setNode(node, {
...existingNodeData,
clusterNode: true,
id: node,
clusterData: clusterDb.get(node).clusterData,
label: clusterDb.get(node).label,
clusterData: clusterDb.get(node).clusterData,
graph: clusterGraph,
});
log.warn('New graph after copy node: (', node, ')', graphlibJson.write(clusterGraph));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,88 @@ flowchart TB
expect(aGraph.parent('B')).toBe(undefined);
});
});

describe('Virtual ordering edges for edge-less clusters', () => {
let g;
beforeEach(function () {
setLogLevel(1);
g = new graphlib.Graph({
multigraph: true,
compound: true,
});
g.setDefaultEdgeLabel(function () {
return {};
});
});

// Build a graph where cluster Y has no external edges (so it gets extracted
// into a clusterNode) and no internal edges (so virtual ordering edges are injected).
// The outer node X has no edge to Y; only Y's children are present.
const makeEdgelessCluster = (rankdir) => {
g.setGraph({
rankdir,
nodesep: 50,
ranksep: 50,
marginx: 8,
marginy: 8,
});
g.setNode('A', { data: 1 });
g.setNode('B', { data: 2 });
g.setNode('C', { data: 3 });
g.setNode('D', { data: 4 });
g.setNode('Y', { data: 5, dir: rankdir });
g.setParent('A', 'Y');
g.setParent('B', 'Y');
g.setParent('C', 'Y');
g.setParent('D', 'Y');
// NO edges to/from Y — ensures externalConnections stays false
// so Y gets extracted as a clusterNode
};

for (const dir of ['TB', 'BT', 'LR', 'RL']) {
it(`should inject virtual ordering edges for an edge-less cluster with direction ${dir}`, () => {
makeEdgelessCluster(dir);
adjustClustersAndEdges(g);

const yClusterNode = g.node('Y');
expect(yClusterNode).toBeDefined();
expect(yClusterNode.clusterNode).toBe(true);

const yGraph = yClusterNode.graph;
expect(yGraph.nodes().length).toBe(4);

// There should be exactly 3 virtual ordering edges (A→B, B→C, C→D)
const edges = yGraph.edges();
expect(edges.length).toBe(3);
edges.forEach((e) => {
expect(yGraph.edge(e)._virtual).toBe(true);
});
});
}

it('should NOT inject virtual edges when the cluster already has internal edges', () => {
g.setGraph({ rankdir: 'LR', nodesep: 50, ranksep: 50, marginx: 8, marginy: 8 });
g.setNode('A', { data: 1 });
g.setNode('B', { data: 2 });
g.setNode('Y', { data: 3, dir: 'LR' });
g.setParent('A', 'Y');
g.setParent('B', 'Y');
// One real internal edge — should prevent virtual edge injection
g.setEdge('A', 'B', { data: 'real' });

adjustClustersAndEdges(g);

const yClusterNode = g.node('Y');
expect(yClusterNode?.clusterNode).toBe(true);

const yGraph = yClusterNode.graph;
const edges = yGraph.edges();
expect(edges.length).toBe(1);
expect(yGraph.edge(edges[0]).data).toBe('real');
// No _virtual marker on the real edge
expect(yGraph.edge(edges[0])._virtual).toBeUndefined();
});
});
describe('extractDescendants', function () {
let g;
beforeEach(function () {
Expand Down
1 change: 1 addition & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
testTimeout: 30000,
// TODO: should we move this to a mermaid-core package?
coverage: {
provider: 'v8',
Expand Down
Loading