diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts
index e71a9c7f5..9d0a82a87 100644
--- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts
+++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts
@@ -3,8 +3,8 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
import parser from './parser/stateDiagram.jison';
import db from './stateDb.js';
import styles from './styles.js';
-import renderer from './stateRenderer-v2.js';
-// import renderer from './stateRenderer-v3-unified.js';
+// import renderer from './stateRenderer-v2.js';
+import renderer from './stateRenderer-v3-unified.js';
export const diagram: DiagramDefinition = {
parser,
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js
similarity index 80%
rename from packages/mermaid/src/rendering-util/layout-algorithms/dagre.js
rename to packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js
index 76685dd7b..f6760f96f 100644
--- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre.js
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js
@@ -1,7 +1,8 @@
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
-import insertMarkers from './markers.js';
-import { updateNodeBounds } from './shapes/util.js';
+import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
+import insertMarkers from '../../rendering-elements/markers.js';
+import { updateNodeBounds } from '../../rendering-elements/shapes/util.js';
import {
clear as clearGraphlib,
clusterDb,
@@ -9,12 +10,22 @@ import {
findNonClusterChild,
sortNodesByHierarchy,
} from './mermaid-graphlib.js';
-import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes.js';
-import { insertCluster, clear as clearClusters } from './clusters.js';
-import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js';
-import { log } from '../logger.js';
-import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
-import { getConfig } from '../diagram-api/diagramAPI.js';
+import {
+ insertNode,
+ positionNode,
+ clear as clearNodes,
+ setNodeElem,
+} from '../../rendering-elements/nodes.js';
+import { insertCluster, clear as clearClusters } from '../../rendering-elements/clusters.js';
+import {
+ insertEdgeLabel,
+ positionEdgeLabel,
+ insertEdge,
+ clear as clearEdges,
+} from '../../rendering-elements/edges.js';
+import { log } from '$root/logger.js';
+import { getSubGraphTitleMargins } from '../../../utils/subGraphTitleMargins.js';
+import { getConfig } from '../../../diagram-api/diagramAPI.js';
const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, siteConfig) => {
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
@@ -161,19 +172,49 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, sit
return { elem, diff };
};
-export const render = async (elem, graph, markers, diagramtype, id) => {
- insertMarkers(elem, markers, diagramtype, id);
+export const render = async (data4Layout, svg, element) => {
+ console.warn('HERERERERERER');
+ // Create the input mermaid.graph
+ const graph = new graphlib.Graph({
+ multigraph: true,
+ compound: true,
+ })
+ .setGraph({
+ rankdir: data4Layout.direction,
+ nodesep: data4Layout.nodeSpacing,
+ ranksep: data4Layout.rankSpacing,
+ marginx: 8,
+ marginy: 8,
+ })
+ .setDefaultEdgeLabel(function () {
+ return {};
+ });
+
+ // Org
+
+ insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
clearNodes();
clearEdges();
clearClusters();
clearGraphlib();
+ // Add the nodes and edges to the graph
+ data4Layout.nodes.forEach((node) => {
+ graph.setNode(node.id, { ...node });
+ });
+
log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph)));
adjustClustersAndEdges(graph);
log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph)));
- // log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
const siteConfig = getConfig();
- await recursiveRender(elem, graph, diagramtype, id, undefined, siteConfig);
+ await recursiveRender(
+ element,
+ graph,
+ data4Layout.type,
+ data4Layout.diagramId,
+ undefined,
+ siteConfig
+ );
};
// const shapeDefinitions = {};
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
new file mode 100644
index 000000000..ee2df03c8
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.js
@@ -0,0 +1,474 @@
+/** Decorates with functions required by mermaids dagre-wrapper. */
+import { log } from '$root/logger.js';
+import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
+import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
+
+export let clusterDb = {};
+let descendants = {};
+let parents = {};
+
+export const clear = () => {
+ descendants = {};
+ parents = {};
+ clusterDb = {};
+};
+
+const isDescendant = (id, ancenstorId) => {
+ // if (id === ancenstorId) return true;
+
+ log.trace('In isDecendant', ancenstorId, ' ', id, ' = ', descendants[ancenstorId].includes(id));
+ if (descendants[ancenstorId].includes(id)) {
+ return true;
+ }
+
+ return false;
+};
+
+const edgeInCluster = (edge, clusterId) => {
+ log.info('Decendants of ', clusterId, ' is ', descendants[clusterId]);
+ log.info('Edge is ', edge);
+ // Edges to/from the cluster is not in the cluster, they are in the parent
+ if (edge.v === clusterId) {
+ return false;
+ }
+ if (edge.w === clusterId) {
+ return false;
+ }
+
+ if (!descendants[clusterId]) {
+ log.debug('Tilt, ', clusterId, ',not in decendants');
+ return false;
+ }
+ return (
+ descendants[clusterId].includes(edge.v) ||
+ isDescendant(edge.v, clusterId) ||
+ isDescendant(edge.w, clusterId) ||
+ descendants[clusterId].includes(edge.w)
+ );
+};
+
+const copy = (clusterId, graph, newGraph, rootId) => {
+ log.warn(
+ 'Copying children of ',
+ clusterId,
+ 'root',
+ rootId,
+ 'data',
+ graph.node(clusterId),
+ rootId
+ );
+ const nodes = graph.children(clusterId) || [];
+
+ // Include cluster node if it is not the root
+ if (clusterId !== rootId) {
+ nodes.push(clusterId);
+ }
+
+ log.warn('Copying (nodes) clusterId', clusterId, 'nodes', nodes);
+
+ nodes.forEach((node) => {
+ if (graph.children(node).length > 0) {
+ copy(node, graph, newGraph, rootId);
+ } else {
+ const data = graph.node(node);
+ log.info('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId);
+ newGraph.setNode(node, data);
+ if (rootId !== graph.parent(node)) {
+ log.warn('Setting parent', node, graph.parent(node));
+ newGraph.setParent(node, graph.parent(node));
+ }
+
+ if (clusterId !== rootId && node !== clusterId) {
+ log.debug('Setting parent', node, clusterId);
+ newGraph.setParent(node, clusterId);
+ } else {
+ log.info('In copy ', clusterId, 'root', rootId, 'data', graph.node(clusterId), rootId);
+ log.debug(
+ 'Not Setting parent for node=',
+ node,
+ 'cluster!==rootId',
+ clusterId !== rootId,
+ 'node!==clusterId',
+ node !== clusterId
+ );
+ }
+ const edges = graph.edges(node);
+ log.debug('Copying Edges', edges);
+ edges.forEach((edge) => {
+ log.info('Edge', edge);
+ const data = graph.edge(edge.v, edge.w, edge.name);
+ log.info('Edge data', data, rootId);
+ try {
+ // Do not copy edges in and out of the root cluster, they belong to the parent graph
+ if (edgeInCluster(edge, rootId)) {
+ log.info('Copying as ', edge.v, edge.w, data, edge.name);
+ newGraph.setEdge(edge.v, edge.w, data, edge.name);
+ log.info('newGraph edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0]));
+ } else {
+ log.info(
+ 'Skipping copy of edge ',
+ edge.v,
+ '-->',
+ edge.w,
+ ' rootId: ',
+ rootId,
+ ' clusterId:',
+ clusterId
+ );
+ }
+ } catch (e) {
+ log.error(e);
+ }
+ });
+ }
+ log.debug('Removing node', node);
+ graph.removeNode(node);
+ });
+};
+export const extractDescendants = (id, graph) => {
+ // log.debug('Extracting ', id);
+ const children = graph.children(id);
+ let res = [...children];
+
+ for (const child of children) {
+ parents[child] = id;
+ res = [...res, ...extractDescendants(child, graph)];
+ }
+
+ return res;
+};
+
+/**
+ * Validates the graph, checking that all parent child relation points to existing nodes and that
+ * edges between nodes also ia correct. When not correct the function logs the discrepancies.
+ *
+ * @param graph
+ */
+export const validate = (graph) => {
+ const edges = graph.edges();
+ log.trace('Edges: ', edges);
+ for (const edge of edges) {
+ if (graph.children(edge.v).length > 0) {
+ log.trace('The node ', edge.v, ' is part of and edge even though it has children');
+ return false;
+ }
+ if (graph.children(edge.w).length > 0) {
+ log.trace('The node ', edge.w, ' is part of and edge even though it has children');
+ return false;
+ }
+ }
+ return true;
+};
+
+/**
+ * Finds a child that is not a cluster. When faking an edge between a node and a cluster.
+ *
+ * @param id
+ * @param {any} graph
+ */
+export const findNonClusterChild = (id, graph) => {
+ // const node = graph.node(id);
+ log.trace('Searching', id);
+ // const children = graph.children(id).reverse();
+ const children = graph.children(id); //.reverse();
+ log.trace('Searching children of id ', id, children);
+ if (children.length < 1) {
+ log.trace('This is a valid node', id);
+ return id;
+ }
+ for (const child of children) {
+ const _id = findNonClusterChild(child, graph);
+ if (_id) {
+ log.trace('Found replacement for', id, ' => ', _id);
+ return _id;
+ }
+ }
+};
+
+const getAnchorId = (id) => {
+ if (!clusterDb[id]) {
+ return id;
+ }
+ // If the cluster has no external connections
+ if (!clusterDb[id].externalConnections) {
+ return id;
+ }
+
+ // Return the replacement node
+ if (clusterDb[id]) {
+ return clusterDb[id].id;
+ }
+ return id;
+};
+
+export const adjustClustersAndEdges = (graph, depth) => {
+ if (!graph || depth > 10) {
+ log.debug('Opting out, no graph ');
+ return;
+ } else {
+ log.debug('Opting in, graph ');
+ }
+ // Go through the nodes and for each cluster found, save a replacement node, this can be used when
+ // faking a link to a cluster
+ graph.nodes().forEach(function (id) {
+ const children = graph.children(id);
+ if (children.length > 0) {
+ log.warn(
+ 'Cluster identified',
+ id,
+ ' Replacement id in edges: ',
+ findNonClusterChild(id, graph)
+ );
+ descendants[id] = extractDescendants(id, graph);
+ clusterDb[id] = { id: findNonClusterChild(id, graph), clusterData: graph.node(id) };
+ }
+ });
+
+ // Check incoming and outgoing edges for each cluster
+ graph.nodes().forEach(function (id) {
+ const children = graph.children(id);
+ const edges = graph.edges();
+ if (children.length > 0) {
+ log.debug('Cluster identified', id, descendants);
+ edges.forEach((edge) => {
+ // log.debug('Edge, descendants: ', edge, descendants[id]);
+
+ // Check if any edge leaves the cluster (not the actual cluster, that's a link from the box)
+ if (edge.v !== id && edge.w !== id) {
+ // Any edge where either the one of the nodes is descending to the cluster but not the other
+ // if (descendants[id].indexOf(edge.v) < 0 && descendants[id].indexOf(edge.w) < 0) {
+
+ const d1 = isDescendant(edge.v, id);
+ const d2 = isDescendant(edge.w, id);
+
+ // d1 xor d2 - if either d1 is true and d2 is false or the other way around
+ if (d1 ^ d2) {
+ log.warn('Edge: ', edge, ' leaves cluster ', id);
+ log.warn('Decendants of XXX ', id, ': ', descendants[id]);
+ clusterDb[id].externalConnections = true;
+ }
+ }
+ });
+ } else {
+ log.debug('Not a cluster ', id, descendants);
+ }
+ });
+
+ for (let id of Object.keys(clusterDb)) {
+ const nonClusterChild = clusterDb[id].id;
+ const parent = graph.parent(nonClusterChild);
+
+ // Change replacement node of id to parent of current replacement node if valid
+ if (parent !== id && clusterDb[parent] && !clusterDb[parent].externalConnections) {
+ clusterDb[id].id = parent;
+ }
+ }
+
+ // For clusters with incoming and/or outgoing edges translate those edges to a real node
+ // in the cluster in order to fake the edge
+ graph.edges().forEach(function (e) {
+ const edge = graph.edge(e);
+ log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
+ log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
+
+ let v = e.v;
+ let w = e.w;
+ // Check if link is either from or to a cluster
+ log.warn(
+ 'Fix XXX',
+ clusterDb,
+ 'ids:',
+ e.v,
+ e.w,
+ 'Translating: ',
+ clusterDb[e.v],
+ ' --- ',
+ clusterDb[e.w]
+ );
+ if (clusterDb[e.v] && clusterDb[e.w] && clusterDb[e.v] === clusterDb[e.w]) {
+ log.warn('Fixing and trixing link to self - removing XXX', e.v, e.w, e.name);
+ log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name);
+ v = getAnchorId(e.v);
+ w = getAnchorId(e.w);
+ graph.removeEdge(e.v, e.w, e.name);
+ const specialId = e.w + '---' + e.v;
+ graph.setNode(specialId, {
+ domId: specialId,
+ id: specialId,
+ labelStyle: '',
+ labelText: edge.label,
+ padding: 0,
+ shape: 'labelRect',
+ style: '',
+ });
+ const edge1 = structuredClone(edge);
+ const edge2 = structuredClone(edge);
+ edge1.label = '';
+ edge1.arrowTypeEnd = 'none';
+ edge2.label = '';
+ edge1.fromCluster = e.v;
+ edge2.toCluster = e.v;
+
+ graph.setEdge(v, specialId, edge1, e.name + '-cyclic-special');
+ graph.setEdge(specialId, w, edge2, e.name + '-cyclic-special');
+ } else if (clusterDb[e.v] || clusterDb[e.w]) {
+ log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name);
+ v = getAnchorId(e.v);
+ w = getAnchorId(e.w);
+ graph.removeEdge(e.v, e.w, e.name);
+ if (v !== e.v) {
+ const parent = graph.parent(v);
+ clusterDb[parent].externalConnections = true;
+ edge.fromCluster = e.v;
+ }
+ if (w !== e.w) {
+ const parent = graph.parent(w);
+ clusterDb[parent].externalConnections = true;
+ edge.toCluster = e.w;
+ }
+ log.warn('Fix Replacing with XXX', v, w, e.name);
+ graph.setEdge(v, w, edge, e.name);
+ }
+ });
+ log.warn('Adjusted Graph', graphlibJson.write(graph));
+ extractor(graph, 0);
+
+ log.trace(clusterDb);
+
+ // Remove references to extracted cluster
+ // graph.edges().forEach(edge => {
+ // if (isDecendant(edge.v, clusterId) || isDecendant(edge.w, clusterId)) {
+ // graph.removeEdge(edge);
+ // }
+ // });
+};
+
+export const extractor = (graph, depth) => {
+ log.warn('extractor - ', depth, graphlibJson.write(graph), graph.children('D'));
+ if (depth > 10) {
+ log.error('Bailing out');
+ return;
+ }
+ // For clusters without incoming and/or outgoing edges, create a new cluster-node
+ // containing the nodes and edges in the custer in a new graph
+ // for (let i = 0;)
+ let nodes = graph.nodes();
+ let hasChildren = false;
+ for (const node of nodes) {
+ const children = graph.children(node);
+ hasChildren = hasChildren || children.length > 0;
+ }
+
+ if (!hasChildren) {
+ log.debug('Done, no node has children', graph.nodes());
+ return;
+ }
+ // const clusters = Object.keys(clusterDb);
+ // clusters.forEach(clusterId => {
+ log.debug('Nodes = ', nodes, depth);
+ for (const node of nodes) {
+ log.debug(
+ 'Extracting node',
+ node,
+ clusterDb,
+ clusterDb[node] && !clusterDb[node].externalConnections,
+ !graph.parent(node),
+ graph.node(node),
+ graph.children('D'),
+ ' Depth ',
+ depth
+ );
+ // Note that the node might have been removed after the Object.keys call so better check
+ // that it still is in the game
+ if (!clusterDb[node]) {
+ // Skip if the node is not a cluster
+ log.debug('Not a cluster', node, depth);
+ // break;
+ } else if (
+ !clusterDb[node].externalConnections &&
+ // !graph.parent(node) &&
+ graph.children(node) &&
+ graph.children(node).length > 0
+ ) {
+ log.warn(
+ 'Cluster without external connections, without a parent and with children',
+ node,
+ depth
+ );
+
+ const graphSettings = graph.graph();
+ let dir = graphSettings.rankdir === 'TB' ? 'LR' : 'TB';
+ if (clusterDb[node] && clusterDb[node].clusterData && clusterDb[node].clusterData.dir) {
+ dir = clusterDb[node].clusterData.dir;
+ log.warn('Fixing dir', clusterDb[node].clusterData.dir, dir);
+ }
+
+ const clusterGraph = new graphlib.Graph({
+ multigraph: true,
+ compound: true,
+ })
+ .setGraph({
+ rankdir: dir, // Todo: set proper spacing
+ nodesep: 50,
+ ranksep: 50,
+ marginx: 8,
+ marginy: 8,
+ })
+ .setDefaultEdgeLabel(function () {
+ return {};
+ });
+
+ log.warn('Old graph before copy', graphlibJson.write(graph));
+ copy(node, graph, clusterGraph, node);
+ graph.setNode(node, {
+ clusterNode: true,
+ id: node,
+ clusterData: clusterDb[node].clusterData,
+ labelText: clusterDb[node].labelText,
+ graph: clusterGraph,
+ });
+ log.warn('New graph after copy node: (', node, ')', graphlibJson.write(clusterGraph));
+ log.debug('Old graph after copy', graphlibJson.write(graph));
+ } else {
+ log.warn(
+ 'Cluster ** ',
+ node,
+ ' **not meeting the criteria !externalConnections:',
+ !clusterDb[node].externalConnections,
+ ' no parent: ',
+ !graph.parent(node),
+ ' children ',
+ graph.children(node) && graph.children(node).length > 0,
+ graph.children('D'),
+ depth
+ );
+ log.debug(clusterDb);
+ }
+ }
+
+ nodes = graph.nodes();
+ log.warn('New list of nodes', nodes);
+ for (const node of nodes) {
+ const data = graph.node(node);
+ log.warn(' Now next level', node, data);
+ if (data.clusterNode) {
+ extractor(data.graph, depth + 1);
+ }
+ }
+};
+
+const sorter = (graph, nodes) => {
+ if (nodes.length === 0) {
+ return [];
+ }
+ let result = Object.assign(nodes);
+ nodes.forEach((node) => {
+ const children = graph.children(node);
+ const sorted = sorter(graph, children);
+ result = [...result, ...sorted];
+ });
+
+ return result;
+};
+
+export const sortNodesByHierarchy = (graph) => sorter(graph, graph.children());
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
new file mode 100644
index 000000000..d44e54391
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js
@@ -0,0 +1,508 @@
+import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
+import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
+import {
+ validate,
+ adjustClustersAndEdges,
+ extractDescendants,
+ sortNodesByHierarchy,
+} from './mermaid-graphlib.js';
+import { setLogLevel, log } from '../logger.js';
+
+describe('Graphlib decorations', () => {
+ let g;
+ beforeEach(function () {
+ setLogLevel(1);
+ g = new graphlib.Graph({
+ multigraph: true,
+ compound: true,
+ });
+ g.setGraph({
+ rankdir: 'TB',
+ nodesep: 10,
+ ranksep: 10,
+ marginx: 8,
+ marginy: 8,
+ });
+ g.setDefaultEdgeLabel(function () {
+ return {};
+ });
+ });
+
+ describe('validate', function () {
+ it('Validate should detect edges between clusters', function () {
+ /*
+ subgraph C1
+ a --> b
+ end
+ subgraph C2
+ c
+ end
+ C1 --> C2
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setNode('c', { data: 3 });
+ g.setParent('a', 'C1');
+ g.setParent('b', 'C1');
+ g.setParent('c', 'C2');
+ g.setEdge('a', 'b');
+ g.setEdge('C1', 'C2');
+
+ expect(validate(g)).toBe(false);
+ });
+ it('Validate should not detect edges between clusters after adjustment', function () {
+ /*
+ subgraph C1
+ a --> b
+ end
+ subgraph C2
+ c
+ end
+ C1 --> C2
+ */
+ g.setNode('a', {});
+ g.setNode('b', {});
+ g.setNode('c', {});
+ g.setParent('a', 'C1');
+ g.setParent('b', 'C1');
+ g.setParent('c', 'C2');
+ g.setEdge('a', 'b');
+ g.setEdge('C1', 'C2');
+
+ adjustClustersAndEdges(g);
+ log.info(g.edges());
+ expect(validate(g)).toBe(true);
+ });
+
+ it('Validate should detect edges between clusters and transform clusters GLB4', function () {
+ /*
+ a --> b
+ subgraph C1
+ subgraph C2
+ a
+ end
+ b
+ end
+ C1 --> c
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setNode('c', { data: 3 });
+ g.setNode('C1', { data: 4 });
+ g.setNode('C2', { data: 5 });
+ g.setParent('a', 'C2');
+ g.setParent('b', 'C1');
+ g.setParent('C2', 'C1');
+ g.setEdge('a', 'b', { name: 'C1-internal-link' });
+ g.setEdge('C1', 'c', { name: 'C1-external-link' });
+
+ adjustClustersAndEdges(g);
+ log.info(g.nodes());
+ expect(g.nodes().length).toBe(2);
+ expect(validate(g)).toBe(true);
+ });
+ it('Validate should detect edges between clusters and transform clusters GLB5', function () {
+ /*
+ a --> b
+ subgraph C1
+ a
+ end
+ subgraph C2
+ b
+ end
+ C1 -->
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setParent('a', 'C1');
+ g.setParent('b', 'C2');
+ // g.setEdge('a', 'b', { name: 'C1-internal-link' });
+ g.setEdge('C1', 'C2', { name: 'C1-external-link' });
+
+ log.info(g.nodes());
+ adjustClustersAndEdges(g);
+ log.info(g.nodes());
+ expect(g.nodes().length).toBe(2);
+ expect(validate(g)).toBe(true);
+ });
+ it('adjustClustersAndEdges GLB6', function () {
+ /*
+ subgraph C1
+ a
+ end
+ C1 --> b
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setNode('C1', { data: 3 });
+ g.setParent('a', 'C1');
+ g.setEdge('C1', 'b', { data: 'link1' }, '1');
+
+ // log.info(g.edges())
+ adjustClustersAndEdges(g);
+ log.info(g.edges());
+ expect(g.nodes()).toEqual(['b', 'C1']);
+ expect(g.edges().length).toBe(1);
+ expect(validate(g)).toBe(true);
+ expect(g.node('C1').clusterNode).toBe(true);
+
+ const C1Graph = g.node('C1').graph;
+ expect(C1Graph.nodes()).toEqual(['a']);
+ });
+ it('adjustClustersAndEdges GLB7', function () {
+ /*
+ subgraph C1
+ a
+ end
+ C1 --> b
+ C1 --> c
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setNode('c', { data: 3 });
+ g.setParent('a', 'C1');
+ g.setNode('C1', { data: 4 });
+ g.setEdge('C1', 'b', { data: 'link1' }, '1');
+ g.setEdge('C1', 'c', { data: 'link2' }, '2');
+
+ log.info(g.node('C1'));
+ adjustClustersAndEdges(g);
+ log.info(g.edges());
+ expect(g.nodes()).toEqual(['b', 'c', 'C1']);
+ expect(g.nodes().length).toBe(3);
+ expect(g.edges().length).toBe(2);
+
+ expect(g.edges().length).toBe(2);
+ const edgeData = g.edge(g.edges()[1]);
+ expect(edgeData.data).toBe('link2');
+ expect(validate(g)).toBe(true);
+
+ const C1Graph = g.node('C1').graph;
+ expect(C1Graph.nodes()).toEqual(['a']);
+ });
+ it('adjustClustersAndEdges GLB8', function () {
+ /*
+ subgraph A
+ a
+ end
+ subgraph B
+ b
+ end
+ subgraph C
+ c
+ end
+ A --> B
+ A --> C
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setNode('c', { data: 3 });
+ g.setParent('a', 'A');
+ g.setParent('b', 'B');
+ g.setParent('c', 'C');
+ g.setEdge('A', 'B', { data: 'link1' }, '1');
+ g.setEdge('A', 'C', { data: 'link2' }, '2');
+
+ // log.info(g.edges())
+ adjustClustersAndEdges(g);
+ expect(g.nodes()).toEqual(['A', 'B', 'C']);
+ expect(g.edges().length).toBe(2);
+
+ expect(g.edges().length).toBe(2);
+ const edgeData = g.edge(g.edges()[1]);
+ expect(edgeData.data).toBe('link2');
+ expect(validate(g)).toBe(true);
+
+ const CGraph = g.node('C').graph;
+ expect(CGraph.nodes()).toEqual(['c']);
+ });
+
+ it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB10', function () {
+ /*
+ subgraph C
+ subgraph D
+ d
+ end
+ end
+ */
+
+ g.setNode('C', { data: 1 });
+ g.setNode('D', { data: 2 });
+ g.setNode('d', { data: 3 });
+ g.setParent('d', 'D');
+ g.setParent('D', 'C');
+
+ // log.info('Graph before', g.node('D'))
+ // log.info('Graph before', graphlibJson.write(g))
+ adjustClustersAndEdges(g);
+ // log.info('Graph after', graphlibJson.write(g), g.node('C').graph)
+
+ const CGraph = g.node('C').graph;
+ const DGraph = CGraph.node('D').graph;
+
+ expect(CGraph.nodes()).toEqual(['D']);
+ expect(DGraph.nodes()).toEqual(['d']);
+
+ expect(g.nodes()).toEqual(['C']);
+ expect(g.nodes().length).toBe(1);
+ });
+
+ it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB11', function () {
+ /*
+ subgraph A
+ a
+ end
+ subgraph B
+ b
+ end
+ subgraph C
+ subgraph D
+ d
+ end
+ end
+ A --> B
+ A --> C
+ */
+
+ g.setNode('C', { data: 1 });
+ g.setNode('D', { data: 2 });
+ g.setNode('d', { data: 3 });
+ g.setNode('B', { data: 4 });
+ g.setNode('b', { data: 5 });
+ g.setNode('A', { data: 6 });
+ g.setNode('a', { data: 7 });
+ g.setParent('a', 'A');
+ g.setParent('b', 'B');
+ g.setParent('d', 'D');
+ g.setParent('D', 'C');
+ g.setEdge('A', 'B', { data: 'link1' }, '1');
+ g.setEdge('A', 'C', { data: 'link2' }, '2');
+
+ log.info('Graph before', g.node('D'));
+ log.info('Graph before', graphlibJson.write(g));
+ adjustClustersAndEdges(g);
+ log.trace('Graph after', graphlibJson.write(g));
+ expect(g.nodes()).toEqual(['C', 'B', 'A']);
+ expect(g.nodes().length).toBe(3);
+ expect(g.edges().length).toBe(2);
+
+ const AGraph = g.node('A').graph;
+ const BGraph = g.node('B').graph;
+ const CGraph = g.node('C').graph;
+ // log.info(CGraph.nodes());
+ const DGraph = CGraph.node('D').graph;
+ // log.info('DG', CGraph.children('D'));
+
+ log.info('A', AGraph.nodes());
+ expect(AGraph.nodes().length).toBe(1);
+ expect(AGraph.nodes()).toEqual(['a']);
+ log.trace('Nodes', BGraph.nodes());
+ expect(BGraph.nodes().length).toBe(1);
+ expect(BGraph.nodes()).toEqual(['b']);
+ expect(CGraph.nodes()).toEqual(['D']);
+ expect(CGraph.nodes().length).toEqual(1);
+
+ expect(AGraph.edges().length).toBe(0);
+ expect(BGraph.edges().length).toBe(0);
+ expect(CGraph.edges().length).toBe(0);
+ expect(DGraph.nodes()).toEqual(['d']);
+ expect(DGraph.edges().length).toBe(0);
+ // expect(CGraph.node('D')).toEqual({ data: 2 });
+ expect(g.edges().length).toBe(2);
+
+ // expect(g.edges().length).toBe(2);
+ // const edgeData = g.edge(g.edges()[1]);
+ // expect(edgeData.data).toBe('link2');
+ // expect(validate(g)).toBe(true);
+ });
+ it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB20', function () {
+ /*
+ a --> b
+ subgraph b [Test]
+ c --> d -->e
+ end
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setNode('c', { data: 3 });
+ g.setNode('d', { data: 3 });
+ g.setNode('e', { data: 3 });
+ g.setParent('c', 'b');
+ g.setParent('d', 'b');
+ g.setParent('e', 'b');
+ g.setEdge('a', 'b', { data: 'link1' }, '1');
+ g.setEdge('c', 'd', { data: 'link2' }, '2');
+ g.setEdge('d', 'e', { data: 'link2' }, '2');
+
+ log.info('Graph before', graphlibJson.write(g));
+ adjustClustersAndEdges(g);
+ const bGraph = g.node('b').graph;
+ // log.trace('Graph after', graphlibJson.write(g))
+ log.info('Graph after', graphlibJson.write(bGraph));
+ expect(bGraph.nodes().length).toBe(3);
+ expect(bGraph.edges().length).toBe(2);
+ });
+ it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB21', function () {
+ /*
+ state a {
+ state b {
+ state c {
+ e
+ }
+ }
+ }
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setNode('c', { data: 3 });
+ g.setNode('e', { data: 3 });
+ g.setParent('b', 'a');
+ g.setParent('c', 'b');
+ g.setParent('e', 'c');
+
+ log.info('Graph before', graphlibJson.write(g));
+ adjustClustersAndEdges(g);
+ const aGraph = g.node('a').graph;
+ const bGraph = aGraph.node('b').graph;
+ log.info('Graph after', graphlibJson.write(aGraph));
+ const cGraph = bGraph.node('c').graph;
+ // log.trace('Graph after', graphlibJson.write(g))
+ expect(aGraph.nodes().length).toBe(1);
+ expect(bGraph.nodes().length).toBe(1);
+ expect(cGraph.nodes().length).toBe(1);
+ expect(bGraph.edges().length).toBe(0);
+ });
+ });
+ it('adjustClustersAndEdges should handle nesting GLB77', function () {
+ /*
+flowchart TB
+ subgraph A
+ b-->B
+ a-->c
+ end
+ subgraph B
+ c
+ end
+ */
+
+ const exportedGraph = JSON.parse(
+ '{"options":{"directed":true,"multigraph":true,"compound":true},"nodes":[{"v":"A","value":{"labelStyle":"","shape":"rect","labelText":"A","rx":0,"ry":0,"class":"default","style":"","id":"A","width":500,"type":"group","padding":15}},{"v":"B","value":{"labelStyle":"","shape":"rect","labelText":"B","rx":0,"ry":0,"class":"default","style":"","id":"B","width":500,"type":"group","padding":15},"parent":"A"},{"v":"b","value":{"labelStyle":"","shape":"rect","labelText":"b","rx":0,"ry":0,"class":"default","style":"","id":"b","padding":15},"parent":"A"},{"v":"c","value":{"labelStyle":"","shape":"rect","labelText":"c","rx":0,"ry":0,"class":"default","style":"","id":"c","padding":15},"parent":"B"},{"v":"a","value":{"labelStyle":"","shape":"rect","labelText":"a","rx":0,"ry":0,"class":"default","style":"","id":"a","padding":15},"parent":"A"}],"edges":[{"v":"b","w":"B","name":"1","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-b-B","classes":"flowchart-link LS-b LE-B"}},{"v":"a","w":"c","name":"2","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-a-c","classes":"flowchart-link LS-a LE-c"}}],"value":{"rankdir":"TB","nodesep":50,"ranksep":50,"marginx":8,"marginy":8}}'
+ );
+ const gr = graphlibJson.read(exportedGraph);
+
+ log.info('Graph before', graphlibJson.write(gr));
+ adjustClustersAndEdges(gr);
+ const aGraph = gr.node('A').graph;
+ const bGraph = aGraph.node('B').graph;
+ log.info('Graph after', graphlibJson.write(aGraph));
+ // log.trace('Graph after', graphlibJson.write(g))
+ expect(aGraph.parent('c')).toBe('B');
+ expect(aGraph.parent('B')).toBe(undefined);
+ });
+});
+describe('extractDescendants', function () {
+ let g;
+ beforeEach(function () {
+ setLogLevel(1);
+ g = new graphlib.Graph({
+ multigraph: true,
+ compound: true,
+ });
+ g.setGraph({
+ rankdir: 'TB',
+ nodesep: 10,
+ ranksep: 10,
+ marginx: 8,
+ marginy: 8,
+ });
+ g.setDefaultEdgeLabel(function () {
+ return {};
+ });
+ });
+ it('Simple case of one level descendants GLB9', function () {
+ /*
+ subgraph A
+ a
+ end
+ subgraph B
+ b
+ end
+ subgraph C
+ c
+ end
+ A --> B
+ A --> C
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setNode('c', { data: 3 });
+ g.setParent('a', 'A');
+ g.setParent('b', 'B');
+ g.setParent('c', 'C');
+ g.setEdge('A', 'B', { data: 'link1' }, '1');
+ g.setEdge('A', 'C', { data: 'link2' }, '2');
+
+ // log.info(g.edges())
+ const d1 = extractDescendants('A', g);
+ const d2 = extractDescendants('B', g);
+ const d3 = extractDescendants('C', g);
+
+ expect(d1).toEqual(['a']);
+ expect(d2).toEqual(['b']);
+ expect(d3).toEqual(['c']);
+ });
+});
+describe('sortNodesByHierarchy', function () {
+ let g;
+ beforeEach(function () {
+ setLogLevel(1);
+ g = new graphlib.Graph({
+ multigraph: true,
+ compound: true,
+ });
+ g.setGraph({
+ rankdir: 'TB',
+ nodesep: 10,
+ ranksep: 10,
+ marginx: 8,
+ marginy: 8,
+ });
+ g.setDefaultEdgeLabel(function () {
+ return {};
+ });
+ });
+ it('should sort proper en nodes are in reverse order', function () {
+ /*
+ a -->b
+ subgraph B
+ b
+ end
+ subgraph A
+ B
+ end
+ */
+ g.setNode('a', { data: 1 });
+ g.setNode('b', { data: 2 });
+ g.setParent('b', 'B');
+ g.setParent('B', 'A');
+ g.setEdge('a', 'b', '1');
+ expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']);
+ });
+ it('should sort proper en nodes are in correct order', function () {
+ /*
+ a -->b
+ subgraph B
+ b
+ end
+ subgraph A
+ B
+ end
+ */
+ g.setNode('a', { data: 1 });
+ g.setParent('B', 'A');
+ g.setParent('b', 'B');
+ g.setNode('b', { data: 2 });
+ g.setEdge('a', 'b', '1');
+ expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']);
+ });
+});
diff --git a/packages/mermaid/src/rendering-util/render.js b/packages/mermaid/src/rendering-util/render.js
index a235c31ff..f6fa82f51 100644
--- a/packages/mermaid/src/rendering-util/render.js
+++ b/packages/mermaid/src/rendering-util/render.js
@@ -1,6 +1,9 @@
export const render = async (data4Layout, svg, element) => {
if (data4Layout.layoutAlgorithm === 'dagre-wrapper') {
- const layoutRenderer = await import('../dagre-wrapper/index-refactored.js');
+ console.warn('THERERERERERER');
+ // const layoutRenderer = await import('../dagre-wrapper/index-refactored.js');
+
+ const layoutRenderer = await import('./layout-algorithms/dagre/index.js');
return layoutRenderer.render(data4Layout, svg, element);
}
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/clusters.js b/packages/mermaid/src/rendering-util/rendering-elements/clusters.js
new file mode 100644
index 000000000..0b1ecd572
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/rendering-elements/clusters.js
@@ -0,0 +1,261 @@
+import intersectRect from '../rendering-elements/intersect/intersect-rect.js';
+import { log } from '$root/logger.js';
+import createLabel from './createLabel.js';
+import { createText } from '../createText.ts';
+import { select } from 'd3';
+import { getConfig } from '$root/diagram-api/diagramAPI.js';
+import { evaluate } from '$root/diagrams/common/common.js';
+import { getSubGraphTitleMargins } from '$root/utils/subGraphTitleMargins.js';
+
+const rect = (parent, node) => {
+ log.info('Creating subgraph rect for ', node.id, node);
+ const siteConfig = getConfig();
+
+ // Add outer g element
+ const shapeSvg = parent
+ .insert('g')
+ .attr('class', 'cluster' + (node.class ? ' ' + node.class : ''))
+ .attr('id', node.id);
+
+ // add the rect
+ const rect = shapeSvg.insert('rect', ':first-child');
+
+ const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
+
+ // Create the label and insert it after the rect
+ const label = shapeSvg.insert('g').attr('class', 'cluster-label');
+
+ // const text = label
+ // .node()
+ // .appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
+ const text =
+ node.labelType === 'markdown'
+ ? createText(label, node.labelText, { style: node.labelStyle, useHtmlLabels })
+ : label.node().appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
+
+ // Get the size of the label
+ let bbox = text.getBBox();
+
+ if (evaluate(siteConfig.flowchart.htmlLabels)) {
+ const div = text.children[0];
+ const dv = select(text);
+ bbox = div.getBoundingClientRect();
+ dv.attr('width', bbox.width);
+ dv.attr('height', bbox.height);
+ }
+
+ const padding = 0 * node.padding;
+ const halfPadding = padding / 2;
+
+ const width = node.width <= bbox.width + padding ? bbox.width + padding : node.width;
+ if (node.width <= bbox.width + padding) {
+ node.diff = (bbox.width - node.width) / 2 - node.padding / 2;
+ } else {
+ node.diff = -node.padding / 2;
+ }
+
+ log.trace('Data ', node, JSON.stringify(node));
+ // center the rect around its coordinate
+ rect
+ .attr('style', node.style)
+ .attr('rx', node.rx)
+ .attr('ry', node.ry)
+ .attr('x', node.x - width / 2)
+ .attr('y', node.y - node.height / 2 - halfPadding)
+ .attr('width', width)
+ .attr('height', node.height + padding);
+
+ const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
+ if (useHtmlLabels) {
+ label.attr(
+ 'transform',
+ // This puts the labal on top of the box instead of inside it
+ `translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
+ );
+ } else {
+ label.attr(
+ 'transform',
+ // This puts the labal on top of the box instead of inside it
+ `translate(${node.x}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
+ );
+ }
+ // Center the label
+
+ const rectBox = rect.node().getBBox();
+ node.width = rectBox.width;
+ node.height = rectBox.height;
+
+ node.intersect = function (point) {
+ return intersectRect(node, point);
+ };
+
+ return shapeSvg;
+};
+
+/**
+ * Non visible cluster where the note is group with its
+ *
+ * @param {any} parent
+ * @param {any} node
+ * @returns {any} ShapeSvg
+ */
+const noteGroup = (parent, node) => {
+ // Add outer g element
+ const shapeSvg = parent.insert('g').attr('class', 'note-cluster').attr('id', node.id);
+
+ // add the rect
+ const rect = shapeSvg.insert('rect', ':first-child');
+
+ const padding = 0 * node.padding;
+ const halfPadding = padding / 2;
+
+ // center the rect around its coordinate
+ rect
+ .attr('rx', node.rx)
+ .attr('ry', node.ry)
+ .attr('x', node.x - node.width / 2 - halfPadding)
+ .attr('y', node.y - node.height / 2 - halfPadding)
+ .attr('width', node.width + padding)
+ .attr('height', node.height + padding)
+ .attr('fill', 'none');
+
+ const rectBox = rect.node().getBBox();
+ node.width = rectBox.width;
+ node.height = rectBox.height;
+
+ node.intersect = function (point) {
+ return intersectRect(node, point);
+ };
+
+ return shapeSvg;
+};
+const roundedWithTitle = (parent, node) => {
+ const siteConfig = getConfig();
+
+ // Add outer g element
+ const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id);
+
+ // add the rect
+ const rect = shapeSvg.insert('rect', ':first-child');
+
+ // Create the label and insert it after the rect
+ const label = shapeSvg.insert('g').attr('class', 'cluster-label');
+ const innerRect = shapeSvg.append('rect');
+
+ const text = label
+ .node()
+ .appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
+
+ // Get the size of the label
+ let bbox = text.getBBox();
+ if (evaluate(siteConfig.flowchart.htmlLabels)) {
+ const div = text.children[0];
+ const dv = select(text);
+ bbox = div.getBoundingClientRect();
+ dv.attr('width', bbox.width);
+ dv.attr('height', bbox.height);
+ }
+ bbox = text.getBBox();
+ const padding = 0 * node.padding;
+ const halfPadding = padding / 2;
+
+ const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;
+ if (node.width <= bbox.width + node.padding) {
+ node.diff = (bbox.width + node.padding * 0 - node.width) / 2;
+ } else {
+ node.diff = -node.padding / 2;
+ }
+
+ // center the rect around its coordinate
+ rect
+ .attr('class', 'outer')
+ .attr('x', node.x - width / 2 - halfPadding)
+ .attr('y', node.y - node.height / 2 - halfPadding)
+ .attr('width', width + padding)
+ .attr('height', node.height + padding);
+ innerRect
+ .attr('class', 'inner')
+ .attr('x', node.x - width / 2 - halfPadding)
+ .attr('y', node.y - node.height / 2 - halfPadding + bbox.height - 1)
+ .attr('width', width + padding)
+ .attr('height', node.height + padding - bbox.height - 3);
+
+ const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
+ // Center the label
+ label.attr(
+ 'transform',
+ `translate(${node.x - bbox.width / 2}, ${
+ node.y -
+ node.height / 2 -
+ node.padding / 3 +
+ (evaluate(siteConfig.flowchart.htmlLabels) ? 5 : 3) +
+ subGraphTitleTopMargin
+ })`
+ );
+
+ const rectBox = rect.node().getBBox();
+ node.height = rectBox.height;
+
+ node.intersect = function (point) {
+ return intersectRect(node, point);
+ };
+
+ return shapeSvg;
+};
+
+const divider = (parent, node) => {
+ // Add outer g element
+ const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id);
+
+ // add the rect
+ const rect = shapeSvg.insert('rect', ':first-child');
+
+ const padding = 0 * node.padding;
+ const halfPadding = padding / 2;
+
+ // center the rect around its coordinate
+ rect
+ .attr('class', 'divider')
+ .attr('x', node.x - node.width / 2 - halfPadding)
+ .attr('y', node.y - node.height / 2)
+ .attr('width', node.width + padding)
+ .attr('height', node.height + padding);
+
+ const rectBox = rect.node().getBBox();
+ node.width = rectBox.width;
+ node.height = rectBox.height;
+ node.diff = -node.padding / 2;
+ node.intersect = function (point) {
+ return intersectRect(node, point);
+ };
+
+ return shapeSvg;
+};
+
+const shapes = { rect, roundedWithTitle, noteGroup, divider };
+
+let clusterElems = {};
+
+export const insertCluster = (elem, node) => {
+ log.trace('Inserting cluster');
+ const shape = node.shape || 'rect';
+ clusterElems[node.id] = shapes[shape](elem, node);
+};
+export const getClusterTitleWidth = (elem, node) => {
+ const label = createLabel(node.labelText, node.labelStyle, undefined, true);
+ elem.node().appendChild(label);
+ const width = label.getBBox().width;
+ elem.node().removeChild(label);
+ return width;
+};
+
+export const clear = () => {
+ clusterElems = {};
+};
+
+export const positionCluster = (node) => {
+ log.info('Position cluster (' + node.id + ', ' + node.x + ', ' + node.y + ')');
+ const el = clusterElems[node.id];
+
+ el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
+};
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/createLabel.js b/packages/mermaid/src/rendering-util/rendering-elements/createLabel.js
new file mode 100644
index 000000000..d62c1fc8c
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/rendering-elements/createLabel.js
@@ -0,0 +1,100 @@
+import { select } from 'd3';
+import { log } from '$root/logger.js';
+import { getConfig } from '$root/diagram-api/diagramAPI.js';
+import { evaluate } from '$root/diagrams/common/common.js';
+import { decodeEntities } from '$root/utils.js';
+
+/**
+ * @param dom
+ * @param styleFn
+ */
+function applyStyle(dom, styleFn) {
+ if (styleFn) {
+ dom.attr('style', styleFn);
+ }
+}
+
+/**
+ * @param {any} node
+ * @returns {SVGForeignObjectElement} Node
+ */
+function addHtmlLabel(node) {
+ const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));
+ const div = fo.append('xhtml:div');
+
+ const label = node.label;
+ const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
+ div.html(
+ '' +
+ label +
+ ''
+ );
+
+ applyStyle(div, node.labelStyle);
+ div.style('display', 'inline-block');
+ // Fix for firefox
+ div.style('white-space', 'nowrap');
+ div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
+ return fo.node();
+}
+/**
+ * @param _vertexText
+ * @param style
+ * @param isTitle
+ * @param isNode
+ * @deprecated svg-util/createText instead
+ */
+const createLabel = (_vertexText, style, isTitle, isNode) => {
+ let vertexText = _vertexText || '';
+ if (typeof vertexText === 'object') {
+ vertexText = vertexText[0];
+ }
+ if (evaluate(getConfig().flowchart.htmlLabels)) {
+ // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
+ vertexText = vertexText.replace(/\\n|\n/g, '
');
+ log.info('vertexText' + vertexText);
+ const node = {
+ isNode,
+ label: decodeEntities(vertexText).replace(
+ /fa[blrs]?:fa-[\w-]+/g,
+ (s) => ``
+ ),
+ labelStyle: style.replace('fill:', 'color:'),
+ };
+ let vertexNode = addHtmlLabel(node);
+ // vertexNode.parentNode.removeChild(vertexNode);
+ return vertexNode;
+ } else {
+ const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
+ let rows = [];
+ if (typeof vertexText === 'string') {
+ rows = vertexText.split(/\\n|\n|
/gi);
+ } else if (Array.isArray(vertexText)) {
+ rows = vertexText;
+ } else {
+ rows = [];
+ }
+
+ for (const row of rows) {
+ const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
+ tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
+ tspan.setAttribute('dy', '1em');
+ tspan.setAttribute('x', '0');
+ if (isTitle) {
+ tspan.setAttribute('class', 'title-row');
+ } else {
+ tspan.setAttribute('class', 'row');
+ }
+ tspan.textContent = row.trim();
+ svgLabel.appendChild(tspan);
+ }
+ return svgLabel;
+ }
+};
+
+export default createLabel;
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.spec.ts b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.spec.ts
new file mode 100644
index 000000000..6cfb59fab
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.spec.ts
@@ -0,0 +1,79 @@
+import type { Mocked } from 'vitest';
+import type { SVG } from '../diagram-api/types.js';
+import { addEdgeMarkers } from './edgeMarker.js';
+
+describe('addEdgeMarker', () => {
+ const svgPath = {
+ attr: vitest.fn(),
+ } as unknown as Mocked