From 9b00f1f2fb6843a5ff94b13974bcb646640a3b4d Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Tue, 17 Dec 2024 14:56:18 +0100 Subject: [PATCH 01/57] #5574 Adding support for edge ids and animations --- cypress/platform/knsv2.html | 81 +++++++++++----- .../mermaid/src/diagrams/flowchart/flowDb.ts | 96 ++++++++++++++----- .../flowchart/parser/flow-edges.spec.js | 89 +++++++++++++++++ .../src/diagrams/flowchart/parser/flow.jison | 11 ++- .../mermaid/src/diagrams/flowchart/types.ts | 4 + .../rendering-elements/edges.js | 28 +++++- .../shapes/handDrawnShapeStyles.ts | 44 +++++---- packages/mermaid/src/rendering-util/types.ts | 1 + packages/mermaid/src/styles.ts | 23 ++++- packages/mermaid/src/types.ts | 5 + packages/mermaid/src/utils.ts | 6 +- 11 files changed, 314 insertions(+), 74 deletions(-) diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 1c7bda8e7..dfcd8aa4f 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -84,29 +84,66 @@ /* tspan { font-size: 6px !important; } */ + /* .flowchart-link { + stroke-dasharray: 4, 4 !important; + animation: flow 1s linear infinite; + animation: dashdraw 4.93282s linear infinite; + stroke-width: 2px !important; + } */ + + @keyframes dashdraw { + from { + stroke-dashoffset: 0; + } + } + + /*stroke-width:2;stroke-dasharray:10.000000,9.865639;stroke-dashoffset:-198.656393;animation: 4.932820s linear infinite;*/ + /* stroke-width:2;stroke-dasharray:10.000000,9.865639;stroke-dashoffset:-198.656393;animation: dashdraw 4.932820s linear infinite;*/ -
----
-config:
-  layout: elk
----
+    
       flowchart LR
-      subgraph S2
-      subgraph s1["APA"]
-      D{"Use the editor"}
-      end
-
-
-      D -- Mermaid js --> I{"fa:fa-code Text"}
-            D --> I
-            D --> I
-
-      end
+        A --> B
     
+      flowchart LR
+        A e1@==> B
+        e1@{ animate: true}
+    
+
+flowchart LR
+  A e1@--> B
+  classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
+  class e1 animate
+    
+

infinite

+
+flowchart LR
+  A e1@--> B
+  classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
+  class e1 animate
+    
+

Mermaid - edge-animation-slow

+
+flowchart LR
+  A e1@--> B
+e1@{ animation: fast}
+    
+

Mermaid - edge-animation-fast

+
+flowchart LR
+  A e1@--> B
+  classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear;
+  class e1 edge-animation-fast
+    
+ +
+
+info    
+
 ---
 config:
   layout: elk
@@ -131,7 +168,7 @@ config:
       end
       end
     
-
+    
 ---
 config:
   layout: elk
@@ -144,7 +181,7 @@ config:
       D-->I
       D-->I
     
-
+    
 ---
 config:
   layout: elk
@@ -183,7 +220,7 @@ flowchart LR
     n8@{ shape: rect}
 
     
-
+    
 ---
 config:
   layout: elk
@@ -199,7 +236,7 @@ flowchart LR
 
 
     
-
+    
 ---
 config:
   layout: elk
@@ -208,7 +245,7 @@ flowchart LR
     A{A} --> B & C
 
-
+    
 ---
 config:
   layout: elk
@@ -220,7 +257,7 @@ flowchart LR
     end
 
-
+    
 ---
 config:
   layout: elk
diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts
index 1dbc789c9..ccb8a8e94 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts
+++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts
@@ -24,7 +24,7 @@ import type {
   FlowLink,
   FlowVertexTypeParam,
 } from './types.js';
-import type { NodeMetaData } from '../../types.js';
+import type { NodeMetaData, EdgeMetaData } from '../../types.js';
 
 const MERMAID_DOM_ID_PREFIX = 'flowchart-';
 let vertexCounter = 0;
@@ -71,12 +71,38 @@ export const addVertex = function (
   classes: string[],
   dir: string,
   props = {},
-  shapeData: any
+  metadata: any
 ) {
-  // console.log('addVertex', id, shapeData);
   if (!id || id.trim().length === 0) {
     return;
   }
+  // Extract the metadata from the shapeData, the syntax for adding metadata for nodes and edges is the same
+  // so at this point we don't know if it's a node or an edge, but we can still extract the metadata
+  let doc;
+  if (metadata !== undefined) {
+    let yamlData;
+    // detect if shapeData contains a newline character
+    if (!metadata.includes('\n')) {
+      yamlData = '{\n' + metadata + '\n}';
+    } else {
+      yamlData = metadata + '\n';
+    }
+    doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
+  }
+
+  // Check if this is an edge
+  const edge = edges.find((e) => e.id === id);
+  if (edge) {
+    const edgeDoc = doc as EdgeMetaData;
+    if (edgeDoc?.animate) {
+      edge.animate = edgeDoc.animate;
+    }
+    if (edgeDoc?.animation) {
+      edge.animation = edgeDoc.animation;
+    }
+    return;
+  }
+
   let txt;
 
   let vertex = vertices.get(id);
@@ -128,19 +154,7 @@ export const addVertex = function (
     Object.assign(vertex.props, props);
   }
 
-  if (shapeData !== undefined) {
-    let yamlData;
-    // detect if shapeData contains a newline character
-    // console.log('shapeData', shapeData);
-    if (!shapeData.includes('\n')) {
-      // console.log('yamlData shapeData has no new lines', shapeData);
-      yamlData = '{\n' + shapeData + '\n}';
-    } else {
-      // console.log('yamlData shapeData has new lines', shapeData);
-      yamlData = shapeData + '\n';
-    }
-    // console.log('yamlData', yamlData);
-    const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
+  if (doc !== undefined) {
     if (doc.shape) {
       if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) {
         throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
@@ -187,11 +201,18 @@ export const addVertex = function (
  * Function called by parser when a link/edge definition has been found
  *
  */
-export const addSingleLink = function (_start: string, _end: string, type: any) {
+export const addSingleLink = function (_start: string, _end: string, type: any, id?: string) {
   const start = _start;
   const end = _end;
 
-  const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' };
+  const edge: FlowEdge = {
+    start: start,
+    end: end,
+    type: undefined,
+    text: '',
+    labelType: 'text',
+    classes: [],
+  };
   log.info('abc78 Got edge...', edge);
   const linkTextObj = type.text;
 
@@ -210,6 +231,9 @@ export const addSingleLink = function (_start: string, _end: string, type: any)
     edge.stroke = type.stroke;
     edge.length = type.length > 10 ? 10 : type.length;
   }
+  if (id) {
+    edge.id = id;
+  }
 
   if (edges.length < (config.maxEdges ?? 500)) {
     log.info('Pushing edge...');
@@ -225,11 +249,17 @@ You have to call mermaid.initialize.`
   }
 };
 
-export const addLink = function (_start: string[], _end: string[], type: unknown) {
-  log.info('addLink', _start, _end, type);
+export const addLink = function (_start: string[], _end: string[], linkData: unknown) {
+  const id =
+    linkData && typeof linkData === 'object' && 'id' in linkData
+      ? linkData.id?.replace('@', '')
+      : undefined;
+
+  log.info('addLink', _start, _end, id);
+
   for (const start of _start) {
     for (const end of _end) {
-      addSingleLink(start, end, type);
+      addSingleLink(start, end, linkData, id);
     }
   }
 };
@@ -282,7 +312,13 @@ export const updateLink = function (positions: ('default' | number)[], style: st
   });
 };
 
-export const addClass = function (ids: string, style: string[]) {
+export const addClass = function (ids: string, _style: string[]) {
+  const style = _style
+    .join()
+    .replace(/\\,/g, '§§§')
+    .replace(/,/g, ';')
+    .replace(/§§§/g, ',')
+    .split(';');
   ids.split(',').forEach(function (id) {
     let classNode = classes.get(id);
     if (classNode === undefined) {
@@ -337,6 +373,10 @@ export const setClass = function (ids: string, className: string) {
     if (vertex) {
       vertex.classes.push(className);
     }
+    const edge = edges.find((e) => e.id === id);
+    if (edge) {
+      edge.classes.push(className);
+    }
     const subGraph = subGraphLookup.get(id);
     if (subGraph) {
       subGraph.classes.push(className);
@@ -997,7 +1037,7 @@ export const getData = () => {
       styles.push(...rawEdge.style);
     }
     const edge: Edge = {
-      id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }),
+      id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }, rawEdge.id),
       start: rawEdge.start,
       end: rawEdge.end,
       type: rawEdge.type ?? 'normal',
@@ -1009,14 +1049,20 @@ export const getData = () => {
         rawEdge?.stroke === 'invisible'
           ? ''
           : 'edge-thickness-normal edge-pattern-solid flowchart-link',
-      arrowTypeStart: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeStart,
-      arrowTypeEnd: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeEnd,
+      arrowTypeStart:
+        rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open' ? 'none' : arrowTypeStart,
+      arrowTypeEnd:
+        rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open' ? 'none' : arrowTypeEnd,
       arrowheadStyle: 'fill: #333',
+      cssCompiledStyles: getCompiledStyles(rawEdge.classes),
       labelStyle: styles,
       style: styles,
       pattern: rawEdge.stroke,
       look: config.look,
+      animate: rawEdge.animate,
+      animation: rawEdge.animation,
     };
+
     edges.push(edge);
   });
 
diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js
index 4ae289bad..5682c9bed 100644
--- a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js
+++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js
@@ -39,6 +39,27 @@ const doubleEndedEdges = [
   { edgeStart: '<==', edgeEnd: '==>', stroke: 'thick', type: 'double_arrow_point' },
   { edgeStart: '<-.', edgeEnd: '.->', stroke: 'dotted', type: 'double_arrow_point' },
 ];
+const regularEdges = [
+  { edgeStart: '--', edgeEnd: '--x', stroke: 'normal', type: 'arrow_cross' },
+  { edgeStart: '==', edgeEnd: '==x', stroke: 'thick', type: 'arrow_cross' },
+  { edgeStart: '-.', edgeEnd: '.-x', stroke: 'dotted', type: 'arrow_cross' },
+  { edgeStart: '--', edgeEnd: '--o', stroke: 'normal', type: 'arrow_circle' },
+  { edgeStart: '==', edgeEnd: '==o', stroke: 'thick', type: 'arrow_circle' },
+  { edgeStart: '-.', edgeEnd: '.-o', stroke: 'dotted', type: 'arrow_circle' },
+  { edgeStart: '--', edgeEnd: '-->', stroke: 'normal', type: 'arrow_point' },
+  { edgeStart: '==', edgeEnd: '==>', stroke: 'thick', type: 'arrow_point' },
+  { edgeStart: '-.', edgeEnd: '.->', stroke: 'dotted', type: 'arrow_point' },
+
+  { edgeStart: '--', edgeEnd: '----x', stroke: 'normal', type: 'arrow_cross' },
+  { edgeStart: '==', edgeEnd: '====x', stroke: 'thick', type: 'arrow_cross' },
+  { edgeStart: '-.', edgeEnd: '...-x', stroke: 'dotted', type: 'arrow_cross' },
+  { edgeStart: '--', edgeEnd: '----o', stroke: 'normal', type: 'arrow_circle' },
+  { edgeStart: '==', edgeEnd: '====o', stroke: 'thick', type: 'arrow_circle' },
+  { edgeStart: '-.', edgeEnd: '...-o', stroke: 'dotted', type: 'arrow_circle' },
+  { edgeStart: '--', edgeEnd: '---->', stroke: 'normal', type: 'arrow_point' },
+  { edgeStart: '==', edgeEnd: '====>', stroke: 'thick', type: 'arrow_point' },
+  { edgeStart: '-.', edgeEnd: '...->', stroke: 'dotted', type: 'arrow_point' },
+];
 
 describe('[Edges] when parsing', () => {
   beforeEach(function () {
@@ -67,6 +88,74 @@ describe('[Edges] when parsing', () => {
     expect(edges[0].type).toBe('arrow_circle');
   });
 
+  describe('edges with ids', function () {
+    describe('open ended edges with ids and labels', function () {
+      regularEdges.forEach((edgeType) => {
+        it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {
+          const res = flow.parser.parse(
+            `flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;`
+          );
+          const vert = flow.parser.yy.getVertices();
+          const edges = flow.parser.yy.getEdges();
+          expect(vert.get('A').id).toBe('A');
+          expect(vert.get('B').id).toBe('B');
+          expect(edges.length).toBe(1);
+          expect(edges[0].id).toBe('e1');
+          expect(edges[0].start).toBe('A');
+          expect(edges[0].end).toBe('B');
+          expect(edges[0].type).toBe(`${edgeType.type}`);
+          expect(edges[0].text).toBe('');
+          expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
+        });
+        it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () {
+          const res = flow.parser.parse(
+            `flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;`
+          );
+          const vert = flow.parser.yy.getVertices();
+          const edges = flow.parser.yy.getEdges();
+          expect(vert.get('A').id).toBe('A');
+          expect(vert.get('B').id).toBe('B');
+          expect(edges.length).toBe(1);
+          expect(edges[0].id).toBe('e1');
+          expect(edges[0].start).toBe('A');
+          expect(edges[0].end).toBe('B');
+          expect(edges[0].type).toBe(`${edgeType.type}`);
+          expect(edges[0].text).toBe('');
+          expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
+        });
+      });
+      it('should handle normal edges where you also have a node with metadata', function () {
+        const res = flow.parser.parse(`flowchart LR
+A id1@-->B
+A@{ shape: 'rect' }
+`);
+        const edges = flow.parser.yy.getEdges();
+
+        expect(edges[0].id).toBe('id1');
+      });
+    });
+    describe('double ended edges with ids and labels', function () {
+      doubleEndedEdges.forEach((edgeType) => {
+        it(`should handle ${edgeType.stroke} ${edgeType.type} with  text`, function () {
+          const res = flow.parser.parse(
+            `flowchart TD;\nA e1@${edgeType.edgeStart} label ${edgeType.edgeEnd} B;`
+          );
+          const vert = flow.parser.yy.getVertices();
+          const edges = flow.parser.yy.getEdges();
+          expect(vert.get('A').id).toBe('A');
+          expect(vert.get('B').id).toBe('B');
+          expect(edges.length).toBe(1);
+          expect(edges[0].id).toBe('e1');
+          expect(edges[0].start).toBe('A');
+          expect(edges[0].end).toBe('B');
+          expect(edges[0].type).toBe(`${edgeType.type}`);
+          expect(edges[0].text).toBe('label');
+          expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
+        });
+      });
+    });
+  });
+
   describe('edges', function () {
     doubleEndedEdges.forEach((edgeType) => {
       it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {
diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison
index b3df82fa5..fbd30fa9e 100644
--- a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison
+++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison
@@ -141,6 +141,7 @@ that id.
 .*direction\s+RL[^\n]*       return 'direction_rl';
 .*direction\s+LR[^\n]*       return 'direction_lr';
 
+[^\s]+\@(?=[^\{])               { return 'LINK_ID'; }
 [0-9]+                       return 'NUM';
 \#                           return 'BRKT';
 ":::"                        return 'STYLE_SEPARATOR';
@@ -201,7 +202,9 @@ that id.
 "*"                   return 'MULT';
 "#"                   return 'BRKT';
 "&"                   return 'AMP';
-([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+  return 'NODE_STRING';
+([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+  {
+    return 'NODE_STRING';
+}
 "-"                   return 'MINUS'
 [\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
 [\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
@@ -361,7 +364,7 @@ spaceList
 
 statement
     : vertexStatement separator
-    { /* console.warn('finat vs', $vertexStatement.nodes); */ $$=$vertexStatement.nodes}
+    { $$=$vertexStatement.nodes}
     | styleStatement separator
     {$$=[];}
     | linkStyleStatement separator
@@ -472,6 +475,8 @@ link: linkStatement arrowText
     {$$ = $linkStatement;}
     | START_LINK edgeText LINK
         {var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText};}
+    | LINK_ID START_LINK edgeText LINK
+        {var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText, "id": $LINK_ID};}
     ;
 
 edgeText: edgeTextToken
@@ -487,6 +492,8 @@ edgeText: edgeTextToken
 
 linkStatement: LINK
         {var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};}
+    | LINK_ID LINK
+        {var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length, "id": $LINK_ID};}
         ;
 
 arrowText:
diff --git a/packages/mermaid/src/diagrams/flowchart/types.ts b/packages/mermaid/src/diagrams/flowchart/types.ts
index b2c5cf620..00acb6751 100644
--- a/packages/mermaid/src/diagrams/flowchart/types.ts
+++ b/packages/mermaid/src/diagrams/flowchart/types.ts
@@ -62,6 +62,10 @@ export interface FlowEdge {
   length?: number;
   text: string;
   labelType: 'text';
+  classes: string[];
+  id?: string;
+  animation?: 'fast' | 'slow';
+  animate?: boolean;
 }
 
 export interface FlowClass {
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
index a6a7a55f7..2581d342f 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js
+++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
@@ -9,6 +9,7 @@ import { curveBasis, line, select } from 'd3';
 import rough from 'roughjs';
 import createLabel from './createLabel.js';
 import { addEdgeMarkers } from './edgeMarker.ts';
+import { isLabelStyle } from './shapes/handDrawnShapeStyles.js';
 
 const edgeLabels = new Map();
 const terminalLabels = new Map();
@@ -429,6 +430,14 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
   const tail = startNode;
   var head = endNode;
 
+  const edgeClassStyles = [];
+  for (const key in edge.cssCompiledStyles) {
+    if (isLabelStyle(key)) {
+      continue;
+    }
+    edgeClassStyles.push(edge.cssCompiledStyles[key]);
+  }
+
   if (head.intersect && tail.intersect) {
     points = points.slice(1, edge.points.length - 1);
     points.unshift(tail.intersect(points[0]));
@@ -521,12 +530,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
     svgPath.attr('d', d);
     elem.node().appendChild(svgPath.node());
   } else {
+    const stylesFromClasses = edgeClassStyles.join(';');
+    const styles = edge.edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '';
+    let animationClass = '';
+    if (edge.animate) {
+      animationClass = ' edge-animation-fast';
+    }
+    if (edge.animation) {
+      animationClass = ' edge-animation-' + edge.animation;
+    }
     svgPath = elem
       .append('path')
       .attr('d', linePath)
       .attr('id', edge.id)
-      .attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
-      .attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
+      .attr(
+        'class',
+        ' ' +
+          strokeClasses +
+          (edge.classes ? ' ' + edge.classes : '') +
+          (animationClass ? animationClass : '')
+      )
+      .attr('style', stylesFromClasses + ';' + styles);
   }
 
   // DEBUG code, DO NOT REMOVE
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts
index 80e2a4423..4ac6b2ddd 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts
+++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts
@@ -32,7 +32,28 @@ export const styles2Map = (styles: string[]) => {
   });
   return styleMap;
 };
-
+export const isLabelStyle = (key: string) => {
+  return (
+    key === 'color' ||
+    key === 'font-size' ||
+    key === 'font-family' ||
+    key === 'font-weight' ||
+    key === 'font-style' ||
+    key === 'text-decoration' ||
+    key === 'text-align' ||
+    key === 'text-transform' ||
+    key === 'line-height' ||
+    key === 'letter-spacing' ||
+    key === 'word-spacing' ||
+    key === 'text-shadow' ||
+    key === 'text-overflow' ||
+    key === 'white-space' ||
+    key === 'word-wrap' ||
+    key === 'word-break' ||
+    key === 'overflow-wrap' ||
+    key === 'hyphens'
+  );
+};
 export const styles2String = (node: Node) => {
   const { stylesArray } = compileStyles(node);
   const labelStyles: string[] = [];
@@ -42,26 +63,7 @@ export const styles2String = (node: Node) => {
 
   stylesArray.forEach((style) => {
     const key = style[0];
-    if (
-      key === 'color' ||
-      key === 'font-size' ||
-      key === 'font-family' ||
-      key === 'font-weight' ||
-      key === 'font-style' ||
-      key === 'text-decoration' ||
-      key === 'text-align' ||
-      key === 'text-transform' ||
-      key === 'line-height' ||
-      key === 'letter-spacing' ||
-      key === 'word-spacing' ||
-      key === 'text-shadow' ||
-      key === 'text-overflow' ||
-      key === 'white-space' ||
-      key === 'word-wrap' ||
-      key === 'word-break' ||
-      key === 'overflow-wrap' ||
-      key === 'hyphens'
-    ) {
+    if (isLabelStyle(key)) {
       labelStyles.push(style.join(':') + ' !important');
     } else {
       nodeStyles.push(style.join(':') + ' !important');
diff --git a/packages/mermaid/src/rendering-util/types.ts b/packages/mermaid/src/rendering-util/types.ts
index 86cfd50b3..d64594218 100644
--- a/packages/mermaid/src/rendering-util/types.ts
+++ b/packages/mermaid/src/rendering-util/types.ts
@@ -101,6 +101,7 @@ export interface Edge {
   arrowheadStyle?: string;
   arrowTypeEnd?: string;
   arrowTypeStart?: string;
+  cssCompiledStyles?: string[];
   // Flowchart specific properties
   defaultInterpolate?: string;
   end?: string;
diff --git a/packages/mermaid/src/styles.ts b/packages/mermaid/src/styles.ts
index 78b514c40..2cb11f146 100644
--- a/packages/mermaid/src/styles.ts
+++ b/packages/mermaid/src/styles.ts
@@ -27,7 +27,28 @@ const getStyles = (
     font-size: ${options.fontSize};
     fill: ${options.textColor}
   }
-
+  @keyframes edge-animation-frame {
+    from {
+      stroke-dashoffset: 0;
+    }
+  }
+  @keyframes dash {
+    to {
+      stroke-dashoffset: 0;
+    }
+  }
+  & .edge-animation-slow {
+    stroke-dasharray: 9,5 !important;
+    stroke-dashoffset: 900;
+    animation: dash 50s linear infinite;
+    stroke-linecap: round;
+  }
+  & .edge-animation-fast {
+    stroke-dasharray: 9,5 !important;
+    stroke-dashoffset: 900;
+    animation: dash 20s linear infinite;
+    stroke-linecap: round;
+  }
   /* Classes common for multiple diagrams */
 
   & .error-icon {
diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts
index 5587ca3f4..fdccae677 100644
--- a/packages/mermaid/src/types.ts
+++ b/packages/mermaid/src/types.ts
@@ -12,6 +12,11 @@ export interface NodeMetaData {
   assigned?: string;
   ticket?: string;
 }
+
+export interface EdgeMetaData {
+  animation?: 'fast' | 'slow';
+  animate?: boolean;
+}
 import type { MermaidConfig } from './config.type.js';
 
 export interface Point {
diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts
index c1d674834..68b5e2889 100644
--- a/packages/mermaid/src/utils.ts
+++ b/packages/mermaid/src/utils.ts
@@ -937,8 +937,12 @@ export const getEdgeId = (
     counter?: number;
     prefix?: string;
     suffix?: string;
-  }
+  },
+  id?: string
 ) => {
+  if (id) {
+    return id;
+  }
   return `${prefix ? `${prefix}_` : ''}${from}_${to}_${counter}${suffix ? `_${suffix}` : ''}`;
 };
 

From c153d0455fdc89abd49f61d117a64aac1f3748b0 Mon Sep 17 00:00:00 2001
From: Knut Sveidqvist 
Date: Tue, 17 Dec 2024 16:28:38 +0100
Subject: [PATCH 02/57] #5574 Fixed issue linkStyles

---
 packages/mermaid/src/diagrams/flowchart/flowDb.ts           | 3 ---
 .../mermaid/src/rendering-util/rendering-elements/edges.js  | 6 +++---
 2 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts
index ccb8a8e94..ffe46d398 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts
+++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts
@@ -297,9 +297,6 @@ export const updateLink = function (positions: ('default' | number)[], style: st
     if (pos === 'default') {
       edges.defaultStyle = style;
     } else {
-      // if (utils.isSubstringInArray('fill', style) === -1) {
-      //   style.push('fill:none');
-      // }
       edges[pos].style = style;
       // if edges[pos].style does have fill not set, set it to none
       if (
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
index 2581d342f..649686c0c 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js
+++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
@@ -429,7 +429,6 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
   let pointsHasChanged = false;
   const tail = startNode;
   var head = endNode;
-
   const edgeClassStyles = [];
   for (const key in edge.cssCompiledStyles) {
     if (isLabelStyle(key)) {
@@ -510,6 +509,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
   let svgPath;
   let linePath = lineFunction(lineData);
   const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
+
   if (edge.look === 'handDrawn') {
     const rc = rough.svg(elem);
     Object.assign([], lineData);
@@ -531,7 +531,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
     elem.node().appendChild(svgPath.node());
   } else {
     const stylesFromClasses = edgeClassStyles.join(';');
-    const styles = edge.edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '';
+    const styles = edgeStyles ? edgeStyles.reduce((acc, style) => acc + style + ';', '') : '';
     let animationClass = '';
     if (edge.animate) {
       animationClass = ' edge-animation-fast';
@@ -550,7 +550,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
           (edge.classes ? ' ' + edge.classes : '') +
           (animationClass ? animationClass : '')
       )
-      .attr('style', stylesFromClasses + ';' + styles);
+      .attr('style', stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles);
   }
 
   // DEBUG code, DO NOT REMOVE

From 323b07a2e4255b8339a29a919634ce2e7b7322bf Mon Sep 17 00:00:00 2001
From: Knut Sveidqvist 
Date: Wed, 18 Dec 2024 11:56:48 +0100
Subject: [PATCH 03/57] Typescript fix and updating documentation

---
 .../setup/interfaces/mermaid.LayoutData.md    |  6 +-
 .../setup/interfaces/mermaid.ParseOptions.md  |  2 +-
 .../setup/interfaces/mermaid.ParseResult.md   |  4 +-
 .../setup/interfaces/mermaid.RenderResult.md  |  6 +-
 docs/syntax/flowchart.md                      | 85 +++++++++++++++++++
 .../mermaid/src/diagrams/flowchart/flowDb.ts  | 18 +++-
 packages/mermaid/src/docs/syntax/flowchart.md | 61 +++++++++++++
 packages/mermaid/src/rendering-util/types.ts  |  2 +
 8 files changed, 171 insertions(+), 13 deletions(-)

diff --git a/docs/config/setup/interfaces/mermaid.LayoutData.md b/docs/config/setup/interfaces/mermaid.LayoutData.md
index 5616e1c9a..552a16a8d 100644
--- a/docs/config/setup/interfaces/mermaid.LayoutData.md
+++ b/docs/config/setup/interfaces/mermaid.LayoutData.md
@@ -20,7 +20,7 @@
 
 #### Defined in
 
-[packages/mermaid/src/rendering-util/types.ts:144](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L144)
+[packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147)
 
 ---
 
@@ -30,7 +30,7 @@
 
 #### Defined in
 
-[packages/mermaid/src/rendering-util/types.ts:143](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L143)
+[packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146)
 
 ---
 
@@ -40,4 +40,4 @@
 
 #### Defined in
 
-[packages/mermaid/src/rendering-util/types.ts:142](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L142)
+[packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145)
diff --git a/docs/config/setup/interfaces/mermaid.ParseOptions.md b/docs/config/setup/interfaces/mermaid.ParseOptions.md
index 717e35565..bac54b8ca 100644
--- a/docs/config/setup/interfaces/mermaid.ParseOptions.md
+++ b/docs/config/setup/interfaces/mermaid.ParseOptions.md
@@ -19,4 +19,4 @@ The `parseError` function will not be called.
 
 #### Defined in
 
-[packages/mermaid/src/types.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L59)
+[packages/mermaid/src/types.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L64)
diff --git a/docs/config/setup/interfaces/mermaid.ParseResult.md b/docs/config/setup/interfaces/mermaid.ParseResult.md
index 9f90b6dd4..e2eb5df50 100644
--- a/docs/config/setup/interfaces/mermaid.ParseResult.md
+++ b/docs/config/setup/interfaces/mermaid.ParseResult.md
@@ -18,7 +18,7 @@ The config passed as YAML frontmatter or directives
 
 #### Defined in
 
-[packages/mermaid/src/types.ts:70](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L70)
+[packages/mermaid/src/types.ts:75](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L75)
 
 ---
 
@@ -30,4 +30,4 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
 
 #### Defined in
 
-[packages/mermaid/src/types.ts:66](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L66)
+[packages/mermaid/src/types.ts:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L71)
diff --git a/docs/config/setup/interfaces/mermaid.RenderResult.md b/docs/config/setup/interfaces/mermaid.RenderResult.md
index f882b7af4..cce7f6928 100644
--- a/docs/config/setup/interfaces/mermaid.RenderResult.md
+++ b/docs/config/setup/interfaces/mermaid.RenderResult.md
@@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
 
 #### Defined in
 
-[packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
+[packages/mermaid/src/types.ts:103](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L103)
 
 ---
 
@@ -51,7 +51,7 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
 
 #### Defined in
 
-[packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
+[packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
 
 ---
 
@@ -63,4 +63,4 @@ The svg code for the rendered graph.
 
 #### Defined in
 
-[packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
+[packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
diff --git a/docs/syntax/flowchart.md b/docs/syntax/flowchart.md
index 3837e77de..fefa12e02 100644
--- a/docs/syntax/flowchart.md
+++ b/docs/syntax/flowchart.md
@@ -1183,6 +1183,91 @@ flowchart TB
     B --> D
 ```
 
+### Attaching an ID to Edges
+
+Mermaid now supports assigning IDs to edges, similar to how IDs and metadata can be attached to nodes. This feature lays the groundwork for more advanced styling, classes, and animation capabilities on edges.
+
+**Syntax:**
+
+To give an edge an ID, prepend the edge syntax with the ID followed by an `@` character. For example:
+
+```mermaid-example
+flowchart LR
+  A e1@–> B
+```
+
+```mermaid
+flowchart LR
+  A e1@–> B
+```
+
+In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
+
+### Turning an Animation On
+
+Once you have assigned an ID to an edge, you can turn on animations for that edge by defining the edge’s properties:
+
+```mermaid-example
+flowchart LR
+  A e1@==> B
+  e1@{ animate: true }
+```
+
+```mermaid
+flowchart LR
+  A e1@==> B
+  e1@{ animate: true }
+```
+
+This tells Mermaid that the edge `e1` should be animated.
+
+### Selecting Type of Animation
+
+In the initial version, two animation speeds are supported: `fast` and `slow`. Selecting a specific animation type is a shorthand for enabling animation and setting the animation speed in one go.
+
+**Examples:**
+
+```mermaid-example
+flowchart LR
+  A e1@–> B
+  e1@{ animation: fast }
+```
+
+```mermaid
+flowchart LR
+  A e1@–> B
+  e1@{ animation: fast }
+```
+
+This is equivalent to `{ animate: true, animation: fast }`.
+
+### Using classDef Statements for Animations
+
+You can also animate edges by assigning a class to them and then defining animation properties in a `classDef` statement. For example:
+
+```mermaid-example
+flowchart LR
+  A e1@–> B
+  classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
+  class e1 animate
+```
+
+```mermaid
+flowchart LR
+  A e1@–> B
+  classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
+  class e1 animate
+```
+
+In this snippet:
+
+- `e1@-->` creates an edge with ID `e1`.
+- `classDef animate` defines a class named `animate` with styling and animation properties.
+- `class e1 animate` applies the `animate` class to the edge `e1`.
+
+**Note on Escaping Commas:**
+When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaid’s style definitions.
+
 ## New arrow types
 
 There are new types of arrows supported:
diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts
index ffe46d398..931347a4d 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts
+++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts
@@ -249,11 +249,21 @@ You have to call mermaid.initialize.`
   }
 };
 
+interface LinkData {
+  id: string;
+}
+
+function isLinkData(value: unknown): value is LinkData {
+  return (
+    value !== null &&
+    typeof value === 'object' &&
+    'id' in value &&
+    typeof (value as LinkData).id === 'string'
+  );
+}
+
 export const addLink = function (_start: string[], _end: string[], linkData: unknown) {
-  const id =
-    linkData && typeof linkData === 'object' && 'id' in linkData
-      ? linkData.id?.replace('@', '')
-      : undefined;
+  const id = isLinkData(linkData) ? linkData.id.replace('@', '') : undefined;
 
   log.info('addLink', _start, _end, id);
 
diff --git a/packages/mermaid/src/docs/syntax/flowchart.md b/packages/mermaid/src/docs/syntax/flowchart.md
index 829b71c2d..6c9db197a 100644
--- a/packages/mermaid/src/docs/syntax/flowchart.md
+++ b/packages/mermaid/src/docs/syntax/flowchart.md
@@ -711,6 +711,67 @@ flowchart TB
     B --> D
 ```
 
+### Attaching an ID to Edges
+
+Mermaid now supports assigning IDs to edges, similar to how IDs and metadata can be attached to nodes. This feature lays the groundwork for more advanced styling, classes, and animation capabilities on edges.
+
+**Syntax:**
+
+To give an edge an ID, prepend the edge syntax with the ID followed by an `@` character. For example:
+
+```mermaid
+flowchart LR
+  A e1@–> B
+```
+
+In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
+
+### Turning an Animation On
+
+Once you have assigned an ID to an edge, you can turn on animations for that edge by defining the edge’s properties:
+
+```mermaid
+flowchart LR
+  A e1@==> B
+  e1@{ animate: true }
+```
+
+This tells Mermaid that the edge `e1` should be animated.
+
+### Selecting Type of Animation
+
+In the initial version, two animation speeds are supported: `fast` and `slow`. Selecting a specific animation type is a shorthand for enabling animation and setting the animation speed in one go.
+
+**Examples:**
+
+```mermaid
+flowchart LR
+  A e1@–> B
+  e1@{ animation: fast }
+```
+
+This is equivalent to `{ animate: true, animation: fast }`.
+
+### Using classDef Statements for Animations
+
+You can also animate edges by assigning a class to them and then defining animation properties in a `classDef` statement. For example:
+
+```mermaid
+flowchart LR
+  A e1@–> B
+  classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
+  class e1 animate
+```
+
+In this snippet:
+
+- `e1@-->` creates an edge with ID `e1`.
+- `classDef animate` defines a class named `animate` with styling and animation properties.
+- `class e1 animate` applies the `animate` class to the edge `e1`.
+
+**Note on Escaping Commas:**
+When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaid’s style definitions.
+
 ## New arrow types
 
 There are new types of arrows supported:
diff --git a/packages/mermaid/src/rendering-util/types.ts b/packages/mermaid/src/rendering-util/types.ts
index d64594218..1f84c66c3 100644
--- a/packages/mermaid/src/rendering-util/types.ts
+++ b/packages/mermaid/src/rendering-util/types.ts
@@ -96,6 +96,8 @@ export interface Edge {
   label?: string;
   classes?: string;
   style?: string[];
+  animate?: boolean;
+  animation?: 'fast' | 'slow';
   // Properties common to both Flowchart and State Diagram edges
   arrowhead?: string;
   arrowheadStyle?: string;

From ec0d9c389aa6018043187654044c1e0b5aa4f600 Mon Sep 17 00:00:00 2001
From: Knut Sveidqvist 
Date: Wed, 18 Dec 2024 11:59:39 +0100
Subject: [PATCH 04/57] Adding changeset

---
 .changeset/many-brooms-promise.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/many-brooms-promise.md

diff --git a/.changeset/many-brooms-promise.md b/.changeset/many-brooms-promise.md
new file mode 100644
index 000000000..fec442b34
--- /dev/null
+++ b/.changeset/many-brooms-promise.md
@@ -0,0 +1,5 @@
+---
+'mermaid': minor
+---
+
+Adding support for animation of flowchart edges

From 9ef6090c8c70832e747b60b88aff9e7c557138fd Mon Sep 17 00:00:00 2001
From: Saurabh Gore 
Date: Tue, 31 Dec 2024 17:40:52 +0530
Subject: [PATCH 05/57] convert flowDb to class.

---
 .../src/diagrams/flowchart/flowDb.spec.ts     |    7 +-
 .../mermaid/src/diagrams/flowchart/flowDb.ts  | 1921 ++++++++---------
 .../src/diagrams/flowchart/flowDiagram.ts     |    8 +-
 .../flowchart/flowRenderer-v3-unified.ts      |    3 +-
 .../flowchart/parser/flow-arrows.spec.js      |    4 +-
 .../flowchart/parser/flow-comments.spec.js    |    4 +-
 .../flowchart/parser/flow-direction.spec.js   |    4 +-
 .../flowchart/parser/flow-edges.spec.js       |    4 +-
 .../flowchart/parser/flow-huge.spec.js        |    4 +-
 .../parser/flow-interactions.spec.js          |    4 +-
 .../flowchart/parser/flow-lines.spec.js       |    4 +-
 .../flowchart/parser/flow-md-string.spec.js   |    4 +-
 .../flowchart/parser/flow-node-data.spec.js   |    4 +-
 .../flowchart/parser/flow-singlenode.spec.js  |    4 +-
 .../flowchart/parser/flow-style.spec.js       |    4 +-
 .../flowchart/parser/flow-text.spec.js        |    4 +-
 .../parser/flow-vertice-chaining.spec.js      |    4 +-
 .../diagrams/flowchart/parser/flow.spec.js    |    4 +-
 .../flowchart/parser/subgraph.spec.js         |    4 +-
 19 files changed, 991 insertions(+), 1008 deletions(-)

diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts
index 5983bf04c..841a9d9fe 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts
+++ b/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts
@@ -1,9 +1,11 @@
-import flowDb from './flowDb.js';
+import { FlowDb } from './flowDb.js';
 import type { FlowSubGraph } from './types.js';
 
 describe('flow db subgraphs', () => {
+  let flowDb: FlowDb;
   let subgraphs: FlowSubGraph[];
   beforeEach(() => {
+    flowDb = new FlowDb();
     subgraphs = [
       { nodes: ['a', 'b', 'c', 'e'] },
       { nodes: ['f', 'g', 'h'] },
@@ -44,8 +46,9 @@ describe('flow db subgraphs', () => {
 });
 
 describe('flow db addClass', () => {
+  let flowDb: FlowDb;
   beforeEach(() => {
-    flowDb.clear();
+    flowDb = new FlowDb();
   });
   it('should detect many classes', () => {
     flowDb.addClass('a,b', ['stroke-width: 8px']);
diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts
index 1dbc789c9..8b88fc723 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts
+++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts
@@ -27,1037 +27,1016 @@ import type {
 import type { NodeMetaData } from '../../types.js';
 
 const MERMAID_DOM_ID_PREFIX = 'flowchart-';
-let vertexCounter = 0;
-let config = getConfig();
-let vertices = new Map();
-let edges: FlowEdge[] & { defaultInterpolate?: string; defaultStyle?: string[] } = [];
-let classes = new Map();
-let subGraphs: FlowSubGraph[] = [];
-let subGraphLookup = new Map();
-let tooltips = new Map();
-let subCount = 0;
-let firstGraphFlag = true;
-let direction: string;
 
-let version: string; // As in graph
+export class FlowDb {
+  private vertexCounter = 0;
+  private config = getConfig();
+  private vertices = new Map();
+  private edges: FlowEdge[] & { defaultInterpolate?: string; defaultStyle?: string[] } = [];
+  private classes = new Map();
+  private subGraphs: FlowSubGraph[] = [];
+  private subGraphLookup = new Map();
+  private tooltips = new Map();
+  private subCount = 0;
+  private firstGraphFlag = true;
+  private direction: string | undefined;
 
-// Functions to be run after graph rendering
-let funs: ((element: Element) => void)[] = []; // cspell:ignore funs
+  private version: string | undefined; // As in graph
 
-const sanitizeText = (txt: string) => common.sanitizeText(txt, config);
+  private secCount = -1;
+  private posCrossRef: number[] = [];
 
-/**
- * Function to lookup domId from id in the graph definition.
- *
- * @param id - id of the node
- */
-export const lookUpDomId = function (id: string) {
-  for (const vertex of vertices.values()) {
-    if (vertex.id === id) {
-      return vertex.domId;
+  // Functions to be run after graph rendering
+  private funs: ((element: Element) => void)[] = []; // cspell:ignore funs
+
+  constructor() {
+    this.funs.push(this.setupToolTips);
+    this.clear();
+    this.setGen('gen-2');
+  }
+
+  private sanitizeText = (txt: string) => common.sanitizeText(txt, this.config);
+
+  /**
+   * Function to lookup domId from id in the graph definition.
+   *
+   * @param id - id of the node
+   */
+  public lookUpDomId = (id: string) => {
+    for (const vertex of this.vertices.values()) {
+      if (vertex.id === id) {
+        return vertex.domId;
+      }
     }
-  }
-  return id;
-};
+    return id;
+  };
 
-/**
- * Function called by parser when a node definition has been found
- */
-export const addVertex = function (
-  id: string,
-  textObj: FlowText,
-  type: FlowVertexTypeParam,
-  style: string[],
-  classes: string[],
-  dir: string,
-  props = {},
-  shapeData: any
-) {
-  // console.log('addVertex', id, shapeData);
-  if (!id || id.trim().length === 0) {
-    return;
-  }
-  let txt;
-
-  let vertex = vertices.get(id);
-  if (vertex === undefined) {
-    vertex = {
-      id,
-      labelType: 'text',
-      domId: MERMAID_DOM_ID_PREFIX + id + '-' + vertexCounter,
-      styles: [],
-      classes: [],
-    };
-    vertices.set(id, vertex);
-  }
-  vertexCounter++;
-
-  if (textObj !== undefined) {
-    config = getConfig();
-    txt = sanitizeText(textObj.text.trim());
-    vertex.labelType = textObj.type;
-    // strip quotes if string starts and ends with a quote
-    if (txt.startsWith('"') && txt.endsWith('"')) {
-      txt = txt.substring(1, txt.length - 1);
+  /**
+   * Function called by parser when a node definition has been found
+   */
+  public addVertex = (
+    id: string,
+    textObj: FlowText,
+    type: FlowVertexTypeParam,
+    style: string[],
+    classes: string[],
+    dir: string,
+    props = {},
+    shapeData: any
+  ) => {
+    // console.log('addVertex', id, shapeData);
+    if (!id || id.trim().length === 0) {
+      return;
     }
-    vertex.text = txt;
-  } else {
-    if (vertex.text === undefined) {
-      vertex.text = id;
-    }
-  }
-  if (type !== undefined) {
-    vertex.type = type;
-  }
-  if (style !== undefined && style !== null) {
-    style.forEach(function (s) {
-      vertex.styles.push(s);
-    });
-  }
-  if (classes !== undefined && classes !== null) {
-    classes.forEach(function (s) {
-      vertex.classes.push(s);
-    });
-  }
-  if (dir !== undefined) {
-    vertex.dir = dir;
-  }
-  if (vertex.props === undefined) {
-    vertex.props = props;
-  } else if (props !== undefined) {
-    Object.assign(vertex.props, props);
-  }
+    let txt;
 
-  if (shapeData !== undefined) {
-    let yamlData;
-    // detect if shapeData contains a newline character
-    // console.log('shapeData', shapeData);
-    if (!shapeData.includes('\n')) {
-      // console.log('yamlData shapeData has no new lines', shapeData);
-      yamlData = '{\n' + shapeData + '\n}';
+    let vertex = this.vertices.get(id);
+    if (vertex === undefined) {
+      vertex = {
+        id,
+        labelType: 'text',
+        domId: MERMAID_DOM_ID_PREFIX + id + '-' + this.vertexCounter,
+        styles: [],
+        classes: [],
+      };
+      this.vertices.set(id, vertex);
+    }
+    this.vertexCounter++;
+
+    if (textObj !== undefined) {
+      this.config = getConfig();
+      txt = this.sanitizeText(textObj.text.trim());
+      vertex.labelType = textObj.type;
+      // strip quotes if string starts and ends with a quote
+      if (txt.startsWith('"') && txt.endsWith('"')) {
+        txt = txt.substring(1, txt.length - 1);
+      }
+      vertex.text = txt;
     } else {
-      // console.log('yamlData shapeData has new lines', shapeData);
-      yamlData = shapeData + '\n';
-    }
-    // console.log('yamlData', yamlData);
-    const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
-    if (doc.shape) {
-      if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) {
-        throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
-      } else if (!isValidShape(doc.shape)) {
-        throw new Error(`No such shape: ${doc.shape}.`);
-      }
-      vertex.type = doc?.shape;
-    }
-
-    if (doc?.label) {
-      vertex.text = doc?.label;
-    }
-    if (doc?.icon) {
-      vertex.icon = doc?.icon;
-      if (!doc.label?.trim() && vertex.text === id) {
-        vertex.text = '';
+      if (vertex.text === undefined) {
+        vertex.text = id;
       }
     }
-    if (doc?.form) {
-      vertex.form = doc?.form;
+    if (type !== undefined) {
+      vertex.type = type;
     }
-    if (doc?.pos) {
-      vertex.pos = doc?.pos;
+    if (style !== undefined && style !== null) {
+      style.forEach((s) => {
+        vertex.styles.push(s);
+      });
     }
-    if (doc?.img) {
-      vertex.img = doc?.img;
-      if (!doc.label?.trim() && vertex.text === id) {
-        vertex.text = '';
+    if (classes !== undefined && classes !== null) {
+      classes.forEach((s) => {
+        vertex.classes.push(s);
+      });
+    }
+    if (dir !== undefined) {
+      vertex.dir = dir;
+    }
+    if (vertex.props === undefined) {
+      vertex.props = props;
+    } else if (props !== undefined) {
+      Object.assign(vertex.props, props);
+    }
+
+    if (shapeData !== undefined) {
+      let yamlData;
+      // detect if shapeData contains a newline character
+      // console.log('shapeData', shapeData);
+      if (!shapeData.includes('\n')) {
+        // console.log('yamlData shapeData has no new lines', shapeData);
+        yamlData = '{\n' + shapeData + '\n}';
+      } else {
+        // console.log('yamlData shapeData has new lines', shapeData);
+        yamlData = shapeData + '\n';
+      }
+      // console.log('yamlData', yamlData);
+      const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
+      if (doc.shape) {
+        if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) {
+          throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
+        } else if (!isValidShape(doc.shape)) {
+          throw new Error(`No such shape: ${doc.shape}.`);
+        }
+        vertex.type = doc?.shape;
+      }
+
+      if (doc?.label) {
+        vertex.text = doc?.label;
+      }
+      if (doc?.icon) {
+        vertex.icon = doc?.icon;
+        if (!doc.label?.trim() && vertex.text === id) {
+          vertex.text = '';
+        }
+      }
+      if (doc?.form) {
+        vertex.form = doc?.form;
+      }
+      if (doc?.pos) {
+        vertex.pos = doc?.pos;
+      }
+      if (doc?.img) {
+        vertex.img = doc?.img;
+        if (!doc.label?.trim() && vertex.text === id) {
+          vertex.text = '';
+        }
+      }
+      if (doc?.constraint) {
+        vertex.constraint = doc.constraint;
+      }
+      if (doc.w) {
+        vertex.assetWidth = Number(doc.w);
+      }
+      if (doc.h) {
+        vertex.assetHeight = Number(doc.h);
       }
     }
-    if (doc?.constraint) {
-      vertex.constraint = doc.constraint;
+  };
+
+  /**
+   * Function called by parser when a link/edge definition has been found
+   *
+   */
+  private addSingleLink = (_start: string, _end: string, type: any) => {
+    const start = _start;
+    const end = _end;
+
+    const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' };
+    log.info('abc78 Got edge...', edge);
+    const linkTextObj = type.text;
+
+    if (linkTextObj !== undefined) {
+      edge.text = this.sanitizeText(linkTextObj.text.trim());
+
+      // strip quotes if string starts and ends with a quote
+      if (edge.text.startsWith('"') && edge.text.endsWith('"')) {
+        edge.text = edge.text.substring(1, edge.text.length - 1);
+      }
+      edge.labelType = linkTextObj.type;
     }
-    if (doc.w) {
-      vertex.assetWidth = Number(doc.w);
+
+    if (type !== undefined) {
+      edge.type = type.type;
+      edge.stroke = type.stroke;
+      edge.length = type.length > 10 ? 10 : type.length;
     }
-    if (doc.h) {
-      vertex.assetHeight = Number(doc.h);
-    }
-  }
-};
 
-/**
- * Function called by parser when a link/edge definition has been found
- *
- */
-export const addSingleLink = function (_start: string, _end: string, type: any) {
-  const start = _start;
-  const end = _end;
-
-  const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' };
-  log.info('abc78 Got edge...', edge);
-  const linkTextObj = type.text;
-
-  if (linkTextObj !== undefined) {
-    edge.text = sanitizeText(linkTextObj.text.trim());
-
-    // strip quotes if string starts and ends with a quote
-    if (edge.text.startsWith('"') && edge.text.endsWith('"')) {
-      edge.text = edge.text.substring(1, edge.text.length - 1);
-    }
-    edge.labelType = linkTextObj.type;
-  }
-
-  if (type !== undefined) {
-    edge.type = type.type;
-    edge.stroke = type.stroke;
-    edge.length = type.length > 10 ? 10 : type.length;
-  }
-
-  if (edges.length < (config.maxEdges ?? 500)) {
-    log.info('Pushing edge...');
-    edges.push(edge);
-  } else {
-    throw new Error(
-      `Edge limit exceeded. ${edges.length} edges found, but the limit is ${config.maxEdges}.
+    if (this.edges.length < (this.config.maxEdges ?? 500)) {
+      log.info('Pushing edge...');
+      this.edges.push(edge);
+    } else {
+      throw new Error(
+        `Edge limit exceeded. ${this.edges.length} edges found, but the limit is ${this.config.maxEdges}.
 
 Initialize mermaid with maxEdges set to a higher number to allow more edges.
 You cannot set this config via configuration inside the diagram as it is a secure config.
 You have to call mermaid.initialize.`
-    );
-  }
-};
-
-export const addLink = function (_start: string[], _end: string[], type: unknown) {
-  log.info('addLink', _start, _end, type);
-  for (const start of _start) {
-    for (const end of _end) {
-      addSingleLink(start, end, type);
-    }
-  }
-};
-
-/**
- * Updates a link's line interpolation algorithm
- *
- */
-export const updateLinkInterpolate = function (
-  positions: ('default' | number)[],
-  interpolate: string
-) {
-  positions.forEach(function (pos) {
-    if (pos === 'default') {
-      edges.defaultInterpolate = interpolate;
-    } else {
-      edges[pos].interpolate = interpolate;
-    }
-  });
-};
-
-/**
- * Updates a link with a style
- *
- */
-export const updateLink = function (positions: ('default' | number)[], style: string[]) {
-  positions.forEach(function (pos) {
-    if (typeof pos === 'number' && pos >= edges.length) {
-      throw new Error(
-        `The index ${pos} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${
-          edges.length - 1
-        }. (Help: Ensure that the index is within the range of existing edges.)`
       );
     }
-    if (pos === 'default') {
-      edges.defaultStyle = style;
-    } else {
-      // if (utils.isSubstringInArray('fill', style) === -1) {
-      //   style.push('fill:none');
-      // }
-      edges[pos].style = style;
-      // if edges[pos].style does have fill not set, set it to none
-      if (
-        (edges[pos]?.style?.length ?? 0) > 0 &&
-        !edges[pos]?.style?.some((s) => s?.startsWith('fill'))
-      ) {
-        edges[pos]?.style?.push('fill:none');
+  };
+
+  public addLink = (_start: string[], _end: string[], type: unknown) => {
+    log.info('addLink', _start, _end, type);
+    for (const start of _start) {
+      for (const end of _end) {
+        this.addSingleLink(start, end, type);
       }
     }
-  });
-};
+  };
 
-export const addClass = function (ids: string, style: string[]) {
-  ids.split(',').forEach(function (id) {
-    let classNode = classes.get(id);
-    if (classNode === undefined) {
-      classNode = { id, styles: [], textStyles: [] };
-      classes.set(id, classNode);
-    }
-
-    if (style !== undefined && style !== null) {
-      style.forEach(function (s) {
-        if (/color/.exec(s)) {
-          const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill');
-          classNode.textStyles.push(newStyle);
-        }
-        classNode.styles.push(s);
-      });
-    }
-  });
-};
-
-/**
- * Called by parser when a graph definition is found, stores the direction of the chart.
- *
- */
-export const setDirection = function (dir: string) {
-  direction = dir;
-  if (/.*/.exec(direction)) {
-    direction = 'LR';
-  }
-  if (/.*v/.exec(direction)) {
-    direction = 'TB';
-  }
-  if (direction === 'TD') {
-    direction = 'TB';
-  }
-};
-
-/**
- * Called by parser when a special node is found, e.g. a clickable element.
- *
- * @param ids - Comma separated list of ids
- * @param className - Class to add
- */
-export const setClass = function (ids: string, className: string) {
-  for (const id of ids.split(',')) {
-    const vertex = vertices.get(id);
-    if (vertex) {
-      vertex.classes.push(className);
-    }
-    const subGraph = subGraphLookup.get(id);
-    if (subGraph) {
-      subGraph.classes.push(className);
-    }
-  }
-};
-
-const setTooltip = function (ids: string, tooltip: string) {
-  if (tooltip === undefined) {
-    return;
-  }
-  tooltip = sanitizeText(tooltip);
-  for (const id of ids.split(',')) {
-    tooltips.set(version === 'gen-1' ? lookUpDomId(id) : id, tooltip);
-  }
-};
-
-const setClickFun = function (id: string, functionName: string, functionArgs: string) {
-  const domId = lookUpDomId(id);
-  // if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
-  if (getConfig().securityLevel !== 'loose') {
-    return;
-  }
-  if (functionName === undefined) {
-    return;
-  }
-  let argList: string[] = [];
-  if (typeof functionArgs === 'string') {
-    /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
-    argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
-    for (let i = 0; i < argList.length; i++) {
-      let item = argList[i].trim();
-      /* Removes all double quotes at the start and end of an argument */
-      /* This preserves all starting and ending whitespace inside */
-      if (item.startsWith('"') && item.endsWith('"')) {
-        item = item.substr(1, item.length - 2);
+  /**
+   * Updates a link's line interpolation algorithm
+   *
+   */
+  public updateLinkInterpolate = (positions: ('default' | number)[], interpolate: string) => {
+    positions.forEach((pos) => {
+      if (pos === 'default') {
+        this.edges.defaultInterpolate = interpolate;
+      } else {
+        this.edges[pos].interpolate = interpolate;
       }
-      argList[i] = item;
-    }
-  }
+    });
+  };
 
-  /* if no arguments passed into callback, default to passing in id */
-  if (argList.length === 0) {
-    argList.push(id);
-  }
-
-  const vertex = vertices.get(id);
-  if (vertex) {
-    vertex.haveCallback = true;
-    funs.push(function () {
-      const elem = document.querySelector(`[id="${domId}"]`);
-      if (elem !== null) {
-        elem.addEventListener(
-          'click',
-          function () {
-            utils.runFunc(functionName, ...argList);
-          },
-          false
+  /**
+   * Updates a link with a style
+   *
+   */
+  public updateLink = (positions: ('default' | number)[], style: string[]) => {
+    positions.forEach((pos) => {
+      if (typeof pos === 'number' && pos >= this.edges.length) {
+        throw new Error(
+          `The index ${pos} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${
+            this.edges.length - 1
+          }. (Help: Ensure that the index is within the range of existing edges.)`
         );
       }
-    });
-  }
-};
-
-/**
- * Called by parser when a link is found. Adds the URL to the vertex data.
- *
- * @param ids - Comma separated list of ids
- * @param linkStr - URL to create a link for
- * @param target - Target attribute for the link
- */
-export const setLink = function (ids: string, linkStr: string, target: string) {
-  ids.split(',').forEach(function (id) {
-    const vertex = vertices.get(id);
-    if (vertex !== undefined) {
-      vertex.link = utils.formatUrl(linkStr, config);
-      vertex.linkTarget = target;
-    }
-  });
-  setClass(ids, 'clickable');
-};
-
-export const getTooltip = function (id: string) {
-  return tooltips.get(id);
-};
-
-/**
- * Called by parser when a click definition is found. Registers an event handler.
- *
- * @param ids - Comma separated list of ids
- * @param functionName - Function to be called on click
- * @param functionArgs - Arguments to be passed to the function
- */
-export const setClickEvent = function (ids: string, functionName: string, functionArgs: string) {
-  ids.split(',').forEach(function (id) {
-    setClickFun(id, functionName, functionArgs);
-  });
-  setClass(ids, 'clickable');
-};
-
-export const bindFunctions = function (element: Element) {
-  funs.forEach(function (fun) {
-    fun(element);
-  });
-};
-export const getDirection = function () {
-  return direction.trim();
-};
-/**
- * Retrieval function for fetching the found nodes after parsing has completed.
- *
- */
-export const getVertices = function () {
-  return vertices;
-};
-
-/**
- * Retrieval function for fetching the found links after parsing has completed.
- *
- */
-export const getEdges = function () {
-  return edges;
-};
-
-/**
- * Retrieval function for fetching the found class definitions after parsing has completed.
- *
- */
-export const getClasses = function () {
-  return classes;
-};
-
-const setupToolTips = function (element: Element) {
-  let tooltipElem = select('.mermaidTooltip');
-  // @ts-ignore TODO: fix this
-  if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
-    // @ts-ignore TODO: fix this
-    tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0);
-  }
-
-  const svg = select(element).select('svg');
-
-  const nodes = svg.selectAll('g.node');
-  nodes
-    .on('mouseover', function () {
-      const el = select(this);
-      const title = el.attr('title');
-
-      // Don't try to draw a tooltip if no data is provided
-      if (title === null) {
-        return;
-      }
-      const rect = (this as Element)?.getBoundingClientRect();
-
-      tooltipElem.transition().duration(200).style('opacity', '.9');
-      tooltipElem
-        .text(el.attr('title'))
-        .style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
-        .style('top', window.scrollY + rect.bottom + 'px');
-      tooltipElem.html(tooltipElem.html().replace(/<br\/>/g, '
')); - el.classed('hover', true); - }) - .on('mouseout', function () { - tooltipElem.transition().duration(500).style('opacity', 0); - const el = select(this); - el.classed('hover', false); - }); -}; -funs.push(setupToolTips); - -/** - * Clears the internal graph db so that a new graph can be parsed. - * - */ -export const clear = function (ver = 'gen-1') { - vertices = new Map(); - classes = new Map(); - edges = []; - funs = [setupToolTips]; - subGraphs = []; - subGraphLookup = new Map(); - subCount = 0; - tooltips = new Map(); - firstGraphFlag = true; - version = ver; - config = getConfig(); - commonClear(); -}; - -export const setGen = (ver: string) => { - version = ver || 'gen-2'; -}; - -export const defaultStyle = function () { - return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;'; -}; - -export const addSubGraph = function ( - _id: { text: string }, - list: string[], - _title: { text: string; type: string } -) { - let id: string | undefined = _id.text.trim(); - let title = _title.text; - if (_id === _title && /\s/.exec(_title.text)) { - id = undefined; - } - - function uniq(a: any[]) { - const prims: any = { boolean: {}, number: {}, string: {} }; - const objs: any[] = []; - - let dir; // = undefined; direction.trim(); - const nodeList = a.filter(function (item) { - const type = typeof item; - if (item.stmt && item.stmt === 'dir') { - dir = item.value; - return false; - } - if (item.trim() === '') { - return false; - } - if (type in prims) { - return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true); + if (pos === 'default') { + this.edges.defaultStyle = style; } else { - return objs.includes(item) ? false : objs.push(item); + // if (utils.isSubstringInArray('fill', style) === -1) { + // style.push('fill:none'); + // } + this.edges[pos].style = style; + // if edges[pos].style does have fill not set, set it to none + if ( + (this.edges[pos]?.style?.length ?? 0) > 0 && + !this.edges[pos]?.style?.some((s) => s?.startsWith('fill')) + ) { + this.edges[pos]?.style?.push('fill:none'); + } } }); - return { nodeList, dir }; - } - - const { nodeList, dir } = uniq(list.flat()); - if (version === 'gen-1') { - for (let i = 0; i < nodeList.length; i++) { - nodeList[i] = lookUpDomId(nodeList[i]); - } - } - - id = id ?? 'subGraph' + subCount; - title = title || ''; - title = sanitizeText(title); - subCount = subCount + 1; - const subGraph = { - id: id, - nodes: nodeList, - title: title.trim(), - classes: [], - dir, - labelType: _title.type, }; - log.info('Adding', subGraph.id, subGraph.nodes, subGraph.dir); + public addClass = (ids: string, style: string[]) => { + ids.split(',').forEach((id) => { + let classNode = this.classes.get(id); + if (classNode === undefined) { + classNode = { id, styles: [], textStyles: [] }; + this.classes.set(id, classNode); + } - // Remove the members in the new subgraph if they already belong to another subgraph - subGraph.nodes = makeUniq(subGraph, subGraphs).nodes; - subGraphs.push(subGraph); - subGraphLookup.set(id, subGraph); - return id; -}; + if (style !== undefined && style !== null) { + style.forEach((s) => { + if (/color/.exec(s)) { + const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill'); + classNode.textStyles.push(newStyle); + } + classNode.styles.push(s); + }); + } + }); + }; -const getPosForId = function (id: string) { - for (const [i, subGraph] of subGraphs.entries()) { - if (subGraph.id === id) { - return i; + /** + * Called by parser when a graph definition is found, stores the direction of the chart. + * + */ + public setDirection = (dir: string) => { + this.direction = dir; + if (/.* 2000) { + if (/.*\^/.exec(this.direction)) { + this.direction = 'BT'; + } + if (/.*>/.exec(this.direction)) { + this.direction = 'LR'; + } + if (/.*v/.exec(this.direction)) { + this.direction = 'TB'; + } + if (this.direction === 'TD') { + this.direction = 'TB'; + } + }; + + /** + * Called by parser when a special node is found, e.g. a clickable element. + * + * @param ids - Comma separated list of ids + * @param className - Class to add + */ + public setClass = (ids: string, className: string) => { + for (const id of ids.split(',')) { + const vertex = this.vertices.get(id); + if (vertex) { + vertex.classes.push(className); + } + const subGraph = this.subGraphLookup.get(id); + if (subGraph) { + subGraph.classes.push(className); + } + } + }; + + public setTooltip = (ids: string, tooltip: string) => { + if (tooltip === undefined) { + return; + } + tooltip = this.sanitizeText(tooltip); + for (const id of ids.split(',')) { + this.tooltips.set(this.version === 'gen-1' ? this.lookUpDomId(id) : id, tooltip); + } + }; + + private setClickFun = (id: string, functionName: string, functionArgs: string) => { + const domId = this.lookUpDomId(id); + // if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + if (getConfig().securityLevel !== 'loose') { + return; + } + if (functionName === undefined) { + return; + } + let argList: string[] = []; + if (typeof functionArgs === 'string') { + /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */ + argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/); + for (let i = 0; i < argList.length; i++) { + let item = argList[i].trim(); + /* Removes all double quotes at the start and end of an argument */ + /* This preserves all starting and ending whitespace inside */ + if (item.startsWith('"') && item.endsWith('"')) { + item = item.substr(1, item.length - 2); + } + argList[i] = item; + } + } + + /* if no arguments passed into callback, default to passing in id */ + if (argList.length === 0) { + argList.push(id); + } + + const vertex = this.vertices.get(id); + if (vertex) { + vertex.haveCallback = true; + this.funs.push(() => { + const elem = document.querySelector(`[id="${domId}"]`); + if (elem !== null) { + elem.addEventListener( + 'click', + () => { + utils.runFunc(functionName, ...argList); + }, + false + ); + } + }); + } + }; + + /** + * Called by parser when a link is found. Adds the URL to the vertex data. + * + * @param ids - Comma separated list of ids + * @param linkStr - URL to create a link for + * @param target - Target attribute for the link + */ + public setLink = (ids: string, linkStr: string, target: string) => { + ids.split(',').forEach((id) => { + const vertex = this.vertices.get(id); + if (vertex !== undefined) { + vertex.link = utils.formatUrl(linkStr, this.config); + vertex.linkTarget = target; + } + }); + this.setClass(ids, 'clickable'); + }; + + public getTooltip = (id: string) => { + return this.tooltips.get(id); + }; + + /** + * Called by parser when a click definition is found. Registers an event handler. + * + * @param ids - Comma separated list of ids + * @param functionName - Function to be called on click + * @param functionArgs - Arguments to be passed to the function + */ + public setClickEvent = (ids: string, functionName: string, functionArgs: string) => { + ids.split(',').forEach((id) => { + this.setClickFun(id, functionName, functionArgs); + }); + this.setClass(ids, 'clickable'); + }; + + public bindFunctions = (element: Element) => { + this.funs.forEach((fun) => { + fun(element); + }); + }; + public getDirection = () => { + return this.direction?.trim(); + }; + /** + * Retrieval function for fetching the found nodes after parsing has completed. + * + */ + public getVertices = () => { + return this.vertices; + }; + + /** + * Retrieval function for fetching the found links after parsing has completed. + * + */ + public getEdges = () => { + return this.edges; + }; + + /** + * Retrieval function for fetching the found class definitions after parsing has completed. + * + */ + public getClasses = () => { + return this.classes; + }; + + private setupToolTips = (element: Element) => { + let tooltipElem = select('.mermaidTooltip'); + // @ts-ignore TODO: fix this + if ((tooltipElem._groups || tooltipElem)[0][0] === null) { + // @ts-ignore TODO: fix this + tooltipElem = select('body') + .append('div') + .attr('class', 'mermaidTooltip') + .style('opacity', 0); + } + + const svg = select(element).select('svg'); + + const nodes = svg.selectAll('g.node'); + nodes + .on('mouseover', (e: MouseEvent) => { + const el = select(e.currentTarget as Element); + const title = el.attr('title'); + + // Don't try to draw a tooltip if no data is provided + if (title === null) { + return; + } + const rect = (e.currentTarget as Element)?.getBoundingClientRect(); + + tooltipElem.transition().duration(200).style('opacity', '.9'); + tooltipElem + .text(el.attr('title')) + .style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px') + .style('top', window.scrollY + rect.bottom + 'px'); + tooltipElem.html(tooltipElem.html().replace(/<br\/>/g, '
')); + el.classed('hover', true); + }) + .on('mouseout', (e: MouseEvent) => { + tooltipElem.transition().duration(500).style('opacity', 0); + const el = select(e.currentTarget as Element); + el.classed('hover', false); + }); + }; + + /** + * Clears the internal graph db so that a new graph can be parsed. + * + */ + public clear = (ver = 'gen-1') => { + this.vertices = new Map(); + this.classes = new Map(); + this.edges = []; + this.funs = [this.setupToolTips]; + this.subGraphs = []; + this.subGraphLookup = new Map(); + this.subCount = 0; + this.tooltips = new Map(); + this.firstGraphFlag = true; + this.version = ver; + this.config = getConfig(); + commonClear(); + }; + + public setGen = (ver: string) => { + this.version = ver || 'gen-2'; + }; + + public defaultStyle = () => { + return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;'; + }; + + public addSubGraph = ( + _id: { text: string }, + list: string[], + _title: { text: string; type: string } + ) => { + let id: string | undefined = _id.text.trim(); + let title = _title.text; + if (_id === _title && /\s/.exec(_title.text)) { + id = undefined; + } + + const uniq = (a: any[]) => { + const prims: any = { boolean: {}, number: {}, string: {} }; + const objs: any[] = []; + + let dir; // = undefined; direction.trim(); + const nodeList = a.filter(function (item) { + const type = typeof item; + if (item.stmt && item.stmt === 'dir') { + dir = item.value; + return false; + } + if (item.trim() === '') { + return false; + } + if (type in prims) { + return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true); + } else { + return objs.includes(item) ? false : objs.push(item); + } + }); + return { nodeList, dir }; + }; + + const { nodeList, dir } = uniq(list.flat()); + if (this.version === 'gen-1') { + for (let i = 0; i < nodeList.length; i++) { + nodeList[i] = this.lookUpDomId(nodeList[i]); + } + } + + id = id ?? 'subGraph' + this.subCount; + title = title || ''; + title = this.sanitizeText(title); + this.subCount = this.subCount + 1; + const subGraph = { + id: id, + nodes: nodeList, + title: title.trim(), + classes: [], + dir, + labelType: _title.type, + }; + + log.info('Adding', subGraph.id, subGraph.nodes, subGraph.dir); + + // Remove the members in the new subgraph if they already belong to another subgraph + subGraph.nodes = this.makeUniq(subGraph, this.subGraphs).nodes; + this.subGraphs.push(subGraph); + this.subGraphLookup.set(id, subGraph); + return id; + }; + + private getPosForId = (id: string) => { + for (const [i, subGraph] of this.subGraphs.entries()) { + if (subGraph.id === id) { + return i; + } + } + return -1; + }; + + private indexNodes2 = (id: string, pos: number): { result: boolean; count: number } => { + const nodes = this.subGraphs[pos].nodes; + this.secCount = this.secCount + 1; + if (this.secCount > 2000) { + return { + result: false, + count: 0, + }; + } + this.posCrossRef[this.secCount] = pos; + // Check if match + if (this.subGraphs[pos].id === id) { + return { + result: true, + count: 0, + }; + } + + let count = 0; + let posCount = 1; + while (count < nodes.length) { + const childPos = this.getPosForId(nodes[count]); + // Ignore regular nodes (pos will be -1) + if (childPos >= 0) { + const res = this.indexNodes2(id, childPos); + if (res.result) { + return { + result: true, + count: posCount + res.count, + }; + } else { + posCount = posCount + res.count; + } + } + count = count + 1; + } + return { result: false, - count: 0, + count: posCount, }; - } - posCrossRef[secCount] = pos; - // Check if match - if (subGraphs[pos].id === id) { - return { - result: true, - count: 0, - }; - } - - let count = 0; - let posCount = 1; - while (count < nodes.length) { - const childPos = getPosForId(nodes[count]); - // Ignore regular nodes (pos will be -1) - if (childPos >= 0) { - const res = indexNodes2(id, childPos); - if (res.result) { - return { - result: true, - count: posCount + res.count, - }; - } else { - posCount = posCount + res.count; - } - } - count = count + 1; - } - - return { - result: false, - count: posCount, }; -}; -export const getDepthFirstPos = function (pos: number) { - return posCrossRef[pos]; -}; -export const indexNodes = function () { - secCount = -1; - if (subGraphs.length > 0) { - indexNodes2('none', subGraphs.length - 1); - } -}; - -export const getSubGraphs = function () { - return subGraphs; -}; - -export const firstGraph = () => { - if (firstGraphFlag) { - firstGraphFlag = false; - return true; - } - return false; -}; - -const destructStartLink = (_str: string): FlowLink => { - let str = _str.trim(); - let type = 'arrow_open'; - - switch (str[0]) { - case '<': - type = 'arrow_point'; - str = str.slice(1); - break; - case 'x': - type = 'arrow_cross'; - str = str.slice(1); - break; - case 'o': - type = 'arrow_circle'; - str = str.slice(1); - break; - } - - let stroke = 'normal'; - - if (str.includes('=')) { - stroke = 'thick'; - } - - if (str.includes('.')) { - stroke = 'dotted'; - } - - return { type, stroke }; -}; - -const countChar = (char: string, str: string) => { - const length = str.length; - let count = 0; - for (let i = 0; i < length; ++i) { - if (str[i] === char) { - ++count; + public getDepthFirstPos = (pos: number) => { + return this.posCrossRef[pos]; + }; + public indexNodes = () => { + this.secCount = -1; + if (this.subGraphs.length > 0) { + this.indexNodes2('none', this.subGraphs.length - 1); } - } - return count; -}; + }; -const destructEndLink = (_str: string) => { - const str = _str.trim(); - let line = str.slice(0, -1); - let type = 'arrow_open'; + public getSubGraphs = () => { + return this.subGraphs; + }; - switch (str.slice(-1)) { - case 'x': - type = 'arrow_cross'; - if (str.startsWith('x')) { - type = 'double_' + type; - line = line.slice(1); - } - break; - case '>': - type = 'arrow_point'; - if (str.startsWith('<')) { - type = 'double_' + type; - line = line.slice(1); - } - break; - case 'o': - type = 'arrow_circle'; - if (str.startsWith('o')) { - type = 'double_' + type; - line = line.slice(1); - } - break; - } + public firstGraph = () => { + if (this.firstGraphFlag) { + this.firstGraphFlag = false; + return true; + } + return false; + }; - let stroke = 'normal'; - let length = line.length - 1; + private destructStartLink = (_str: string): FlowLink => { + let str = _str.trim(); + let type = 'arrow_open'; - if (line.startsWith('=')) { - stroke = 'thick'; - } - - if (line.startsWith('~')) { - stroke = 'invisible'; - } - - const dots = countChar('.', line); - - if (dots) { - stroke = 'dotted'; - length = dots; - } - - return { type, stroke, length }; -}; - -export const destructLink = (_str: string, _startStr: string) => { - const info = destructEndLink(_str); - let startInfo; - if (_startStr) { - startInfo = destructStartLink(_startStr); - - if (startInfo.stroke !== info.stroke) { - return { type: 'INVALID', stroke: 'INVALID' }; + switch (str[0]) { + case '<': + type = 'arrow_point'; + str = str.slice(1); + break; + case 'x': + type = 'arrow_cross'; + str = str.slice(1); + break; + case 'o': + type = 'arrow_circle'; + str = str.slice(1); + break; } - if (startInfo.type === 'arrow_open') { - // -- xyz --> - take arrow type from ending - startInfo.type = info.type; - } else { - // x-- xyz --> - not supported - if (startInfo.type !== info.type) { + let stroke = 'normal'; + + if (str.includes('=')) { + stroke = 'thick'; + } + + if (str.includes('.')) { + stroke = 'dotted'; + } + + return { type, stroke }; + }; + + private countChar = (char: string, str: string) => { + const length = str.length; + let count = 0; + for (let i = 0; i < length; ++i) { + if (str[i] === char) { + ++count; + } + } + return count; + }; + + private destructEndLink = (_str: string) => { + const str = _str.trim(); + let line = str.slice(0, -1); + let type = 'arrow_open'; + + switch (str.slice(-1)) { + case 'x': + type = 'arrow_cross'; + if (str.startsWith('x')) { + type = 'double_' + type; + line = line.slice(1); + } + break; + case '>': + type = 'arrow_point'; + if (str.startsWith('<')) { + type = 'double_' + type; + line = line.slice(1); + } + break; + case 'o': + type = 'arrow_circle'; + if (str.startsWith('o')) { + type = 'double_' + type; + line = line.slice(1); + } + break; + } + + let stroke = 'normal'; + let length = line.length - 1; + + if (line.startsWith('=')) { + stroke = 'thick'; + } + + if (line.startsWith('~')) { + stroke = 'invisible'; + } + + const dots = this.countChar('.', line); + + if (dots) { + stroke = 'dotted'; + length = dots; + } + + return { type, stroke, length }; + }; + + public destructLink = (_str: string, _startStr: string) => { + const info = this.destructEndLink(_str); + let startInfo; + if (_startStr) { + startInfo = this.destructStartLink(_startStr); + + if (startInfo.stroke !== info.stroke) { return { type: 'INVALID', stroke: 'INVALID' }; } - startInfo.type = 'double_' + startInfo.type; + if (startInfo.type === 'arrow_open') { + // -- xyz --> - take arrow type from ending + startInfo.type = info.type; + } else { + // x-- xyz --> - not supported + if (startInfo.type !== info.type) { + return { type: 'INVALID', stroke: 'INVALID' }; + } + + startInfo.type = 'double_' + startInfo.type; + } + + if (startInfo.type === 'double_arrow') { + startInfo.type = 'double_arrow_point'; + } + + startInfo.length = info.length; + return startInfo; } - if (startInfo.type === 'double_arrow') { - startInfo.type = 'double_arrow_point'; + return info; + }; + + // Todo optimizer this by caching existing nodes + public exists = (allSgs: FlowSubGraph[], _id: string) => { + for (const sg of allSgs) { + if (sg.nodes.includes(_id)) { + return true; + } } - - startInfo.length = info.length; - return startInfo; - } - - return info; -}; - -// Todo optimizer this by caching existing nodes -const exists = (allSgs: FlowSubGraph[], _id: string) => { - for (const sg of allSgs) { - if (sg.nodes.includes(_id)) { - return true; - } - } - return false; -}; -/** - * Deletes an id from all subgraphs - * - */ -const makeUniq = (sg: FlowSubGraph, allSubgraphs: FlowSubGraph[]) => { - const res: string[] = []; - sg.nodes.forEach((_id, pos) => { - if (!exists(allSubgraphs, _id)) { - res.push(sg.nodes[pos]); - } - }); - return { nodes: res }; -}; - -export const lex = { - firstGraph, -}; - -const getTypeFromVertex = (vertex: FlowVertex): ShapeID => { - if (vertex.img) { - return 'imageSquare'; - } - if (vertex.icon) { - if (vertex.form === 'circle') { - return 'iconCircle'; - } - if (vertex.form === 'square') { - return 'iconSquare'; - } - if (vertex.form === 'rounded') { - return 'iconRounded'; - } - return 'icon'; - } - switch (vertex.type) { - case 'square': - case undefined: - return 'squareRect'; - case 'round': - return 'roundedRect'; - case 'ellipse': - // @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976 - return 'ellipse'; - default: - return vertex.type; - } -}; - -const findNode = (nodes: Node[], id: string) => nodes.find((node) => node.id === id); -const destructEdgeType = (type: string | undefined) => { - let arrowTypeStart = 'none'; - let arrowTypeEnd = 'arrow_point'; - switch (type) { - case 'arrow_point': - case 'arrow_circle': - case 'arrow_cross': - arrowTypeEnd = type; - break; - - case 'double_arrow_point': - case 'double_arrow_circle': - case 'double_arrow_cross': - arrowTypeStart = type.replace('double_', ''); - arrowTypeEnd = arrowTypeStart; - break; - } - return { arrowTypeStart, arrowTypeEnd }; -}; - -const addNodeFromVertex = ( - vertex: FlowVertex, - nodes: Node[], - parentDB: Map, - subGraphDB: Map, - config: any, - look: string -) => { - const parentId = parentDB.get(vertex.id); - const isGroup = subGraphDB.get(vertex.id) ?? false; - - const node = findNode(nodes, vertex.id); - if (node) { - node.cssStyles = vertex.styles; - node.cssCompiledStyles = getCompiledStyles(vertex.classes); - node.cssClasses = vertex.classes.join(' '); - } else { - const baseNode = { - id: vertex.id, - label: vertex.text, - labelStyle: '', - parentId, - padding: config.flowchart?.padding || 8, - cssStyles: vertex.styles, - cssCompiledStyles: getCompiledStyles(['default', 'node', ...vertex.classes]), - cssClasses: 'default ' + vertex.classes.join(' '), - dir: vertex.dir, - domId: vertex.domId, - look, - link: vertex.link, - linkTarget: vertex.linkTarget, - tooltip: getTooltip(vertex.id), - icon: vertex.icon, - pos: vertex.pos, - img: vertex.img, - assetWidth: vertex.assetWidth, - assetHeight: vertex.assetHeight, - constraint: vertex.constraint, - }; - if (isGroup) { - nodes.push({ - ...baseNode, - isGroup: true, - shape: 'rect', - }); - } else { - nodes.push({ - ...baseNode, - isGroup: false, - shape: getTypeFromVertex(vertex), - }); - } - } -}; - -function getCompiledStyles(classDefs: string[]) { - let compiledStyles: string[] = []; - for (const customClass of classDefs) { - const cssClass = classes.get(customClass); - if (cssClass?.styles) { - compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim()); - } - if (cssClass?.textStyles) { - compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim()); - } - } - return compiledStyles; -} - -export const getData = () => { - const config = getConfig(); - const nodes: Node[] = []; - const edges: Edge[] = []; - - const subGraphs = getSubGraphs(); - const parentDB = new Map(); - const subGraphDB = new Map(); - - // Setup the subgraph data for adding nodes - for (let i = subGraphs.length - 1; i >= 0; i--) { - const subGraph = subGraphs[i]; - if (subGraph.nodes.length > 0) { - subGraphDB.set(subGraph.id, true); - } - for (const id of subGraph.nodes) { - parentDB.set(id, subGraph.id); - } - } - - // Data is setup, add the nodes - for (let i = subGraphs.length - 1; i >= 0; i--) { - const subGraph = subGraphs[i]; - nodes.push({ - id: subGraph.id, - label: subGraph.title, - labelStyle: '', - parentId: parentDB.get(subGraph.id), - padding: 8, - cssCompiledStyles: getCompiledStyles(subGraph.classes), - cssClasses: subGraph.classes.join(' '), - shape: 'rect', - dir: subGraph.dir, - isGroup: true, - look: config.look, + return false; + }; + /** + * Deletes an id from all subgraphs + * + */ + public makeUniq = (sg: FlowSubGraph, allSubgraphs: FlowSubGraph[]) => { + const res: string[] = []; + sg.nodes.forEach((_id, pos) => { + if (!this.exists(allSubgraphs, _id)) { + res.push(sg.nodes[pos]); + } }); - } + return { nodes: res }; + }; - const n = getVertices(); - n.forEach((vertex) => { - addNodeFromVertex(vertex, nodes, parentDB, subGraphDB, config, config.look || 'classic'); - }); + public lex = { + firstGraph: this.firstGraph, + }; - const e = getEdges(); - e.forEach((rawEdge, index) => { - const { arrowTypeStart, arrowTypeEnd } = destructEdgeType(rawEdge.type); - const styles = [...(e.defaultStyle ?? [])]; - - if (rawEdge.style) { - styles.push(...rawEdge.style); + private getTypeFromVertex = (vertex: FlowVertex): ShapeID => { + if (vertex.img) { + return 'imageSquare'; } - const edge: Edge = { - id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }), - start: rawEdge.start, - end: rawEdge.end, - type: rawEdge.type ?? 'normal', - label: rawEdge.text, - labelpos: 'c', - thickness: rawEdge.stroke, - minlen: rawEdge.length, - classes: - rawEdge?.stroke === 'invisible' - ? '' - : 'edge-thickness-normal edge-pattern-solid flowchart-link', - arrowTypeStart: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeStart, - arrowTypeEnd: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeEnd, - arrowheadStyle: 'fill: #333', - labelStyle: styles, - style: styles, - pattern: rawEdge.stroke, - look: config.look, - }; - edges.push(edge); - }); + if (vertex.icon) { + if (vertex.form === 'circle') { + return 'iconCircle'; + } + if (vertex.form === 'square') { + return 'iconSquare'; + } + if (vertex.form === 'rounded') { + return 'iconRounded'; + } + return 'icon'; + } + switch (vertex.type) { + case 'square': + case undefined: + return 'squareRect'; + case 'round': + return 'roundedRect'; + case 'ellipse': + // @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976 + return 'ellipse'; + default: + return vertex.type; + } + }; - return { nodes, edges, other: {}, config }; -}; + private findNode = (nodes: Node[], id: string) => nodes.find((node) => node.id === id); + private destructEdgeType = (type: string | undefined) => { + let arrowTypeStart = 'none'; + let arrowTypeEnd = 'arrow_point'; + switch (type) { + case 'arrow_point': + case 'arrow_circle': + case 'arrow_cross': + arrowTypeEnd = type; + break; -export default { - defaultConfig: () => defaultConfig.flowchart, - setAccTitle, - getAccTitle, - getAccDescription, - getData, - setAccDescription, - addVertex, - lookUpDomId, - addLink, - updateLinkInterpolate, - updateLink, - addClass, - setDirection, - setClass, - setTooltip, - getTooltip, - setClickEvent, - setLink, - bindFunctions, - getDirection, - getVertices, - getEdges, - getClasses, - clear, - setGen, - defaultStyle, - addSubGraph, - getDepthFirstPos, - indexNodes, - getSubGraphs, - destructLink, - lex, - exists, - makeUniq, - setDiagramTitle, - getDiagramTitle, -}; + case 'double_arrow_point': + case 'double_arrow_circle': + case 'double_arrow_cross': + arrowTypeStart = type.replace('double_', ''); + arrowTypeEnd = arrowTypeStart; + break; + } + return { arrowTypeStart, arrowTypeEnd }; + }; + + private addNodeFromVertex = ( + vertex: FlowVertex, + nodes: Node[], + parentDB: Map, + subGraphDB: Map, + config: any, + look: string + ) => { + const parentId = parentDB.get(vertex.id); + const isGroup = subGraphDB.get(vertex.id) ?? false; + + const node = this.findNode(nodes, vertex.id); + if (node) { + node.cssStyles = vertex.styles; + node.cssCompiledStyles = this.getCompiledStyles(vertex.classes); + node.cssClasses = vertex.classes.join(' '); + } else { + const baseNode = { + id: vertex.id, + label: vertex.text, + labelStyle: '', + parentId, + padding: config.flowchart?.padding || 8, + cssStyles: vertex.styles, + cssCompiledStyles: this.getCompiledStyles(['default', 'node', ...vertex.classes]), + cssClasses: 'default ' + vertex.classes.join(' '), + dir: vertex.dir, + domId: vertex.domId, + look, + link: vertex.link, + linkTarget: vertex.linkTarget, + tooltip: this.getTooltip(vertex.id), + icon: vertex.icon, + pos: vertex.pos, + img: vertex.img, + assetWidth: vertex.assetWidth, + assetHeight: vertex.assetHeight, + constraint: vertex.constraint, + }; + if (isGroup) { + nodes.push({ + ...baseNode, + isGroup: true, + shape: 'rect', + }); + } else { + nodes.push({ + ...baseNode, + isGroup: false, + shape: this.getTypeFromVertex(vertex), + }); + } + } + }; + + private getCompiledStyles = (classDefs: string[]) => { + let compiledStyles: string[] = []; + for (const customClass of classDefs) { + const cssClass = this.classes.get(customClass); + if (cssClass?.styles) { + compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim()); + } + if (cssClass?.textStyles) { + compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim()); + } + } + return compiledStyles; + }; + + public getData = () => { + const config = getConfig(); + const nodes: Node[] = []; + const edges: Edge[] = []; + + const subGraphs = this.getSubGraphs(); + const parentDB = new Map(); + const subGraphDB = new Map(); + + // Setup the subgraph data for adding nodes + for (let i = subGraphs.length - 1; i >= 0; i--) { + const subGraph = subGraphs[i]; + if (subGraph.nodes.length > 0) { + subGraphDB.set(subGraph.id, true); + } + for (const id of subGraph.nodes) { + parentDB.set(id, subGraph.id); + } + } + + // Data is setup, add the nodes + for (let i = subGraphs.length - 1; i >= 0; i--) { + const subGraph = subGraphs[i]; + nodes.push({ + id: subGraph.id, + label: subGraph.title, + labelStyle: '', + parentId: parentDB.get(subGraph.id), + padding: 8, + cssCompiledStyles: this.getCompiledStyles(subGraph.classes), + cssClasses: subGraph.classes.join(' '), + shape: 'rect', + dir: subGraph.dir, + isGroup: true, + look: config.look, + }); + } + + const n = this.getVertices(); + n.forEach((vertex) => { + this.addNodeFromVertex(vertex, nodes, parentDB, subGraphDB, config, config.look || 'classic'); + }); + + const e = this.getEdges(); + e.forEach((rawEdge, index) => { + const { arrowTypeStart, arrowTypeEnd } = this.destructEdgeType(rawEdge.type); + const styles = [...(e.defaultStyle ?? [])]; + + if (rawEdge.style) { + styles.push(...rawEdge.style); + } + const edge: Edge = { + id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }), + start: rawEdge.start, + end: rawEdge.end, + type: rawEdge.type ?? 'normal', + label: rawEdge.text, + labelpos: 'c', + thickness: rawEdge.stroke, + minlen: rawEdge.length, + classes: + rawEdge?.stroke === 'invisible' + ? '' + : 'edge-thickness-normal edge-pattern-solid flowchart-link', + arrowTypeStart: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeStart, + arrowTypeEnd: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeEnd, + arrowheadStyle: 'fill: #333', + labelStyle: styles, + style: styles, + pattern: rawEdge.stroke, + look: config.look, + }; + edges.push(edge); + }); + + return { nodes, edges, other: {}, config }; + }; + + public defaultConfig = () => defaultConfig.flowchart; + public setAccTitle = setAccTitle; + public setAccDescription = setAccDescription; + public setDiagramTitle = setDiagramTitle; + public getAccTitle = getAccTitle; + public getAccDescription = getAccDescription; + public getDiagramTitle = getDiagramTitle; +} diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts index 67cdf918f..059125201 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts @@ -1,6 +1,6 @@ import type { MermaidConfig } from '../../config.type.js'; import { setConfig } from '../../diagram-api/diagramAPI.js'; -import flowDb from './flowDb.js'; +import { FlowDb } from './flowDb.js'; import renderer from './flowRenderer-v3-unified.js'; // @ts-ignore: JISON doesn't support types import flowParser from './parser/flow.jison'; @@ -8,7 +8,9 @@ import flowStyles from './styles.js'; export const diagram = { parser: flowParser, - db: flowDb, + get db() { + return new FlowDb(); + }, renderer, styles: flowStyles, init: (cnf: MermaidConfig) => { @@ -20,7 +22,5 @@ export const diagram = { } cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); - flowDb.clear(); - flowDb.setGen('gen-2'); }, }; diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts index 6cc15258d..f4c0b9e0e 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts @@ -7,7 +7,6 @@ import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/rende import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import type { LayoutData } from '../../rendering-util/types.js'; import utils from '../../utils.js'; -import { getDirection } from './flowDb.js'; export const getClasses = function ( text: string, @@ -37,7 +36,7 @@ export const draw = async function (text: string, id: string, _version: string, log.debug('Data: ', data4Layout); // Create the root SVG const svg = getDiagramElement(id, securityLevel); - const direction = getDirection(); + const direction = diag.db.getDirection(); data4Layout.type = diag.type; data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-arrows.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-arrows.spec.js index e89398ab4..e4fec241f 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-arrows.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-arrows.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Arrows] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-comments.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-comments.spec.js index 9c2a740af..673f11f89 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-comments.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-comments.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; import { cleanupComments } from '../../../diagram-api/comments.js'; @@ -9,7 +9,7 @@ setConfig({ describe('[Comments] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-direction.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-direction.spec.js index ce6b0b0c4..9598e7303 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-direction.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-direction.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('when parsing directions', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js index 4ae289bad..ed6f53276 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -42,7 +42,7 @@ const doubleEndedEdges = [ describe('[Edges] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-huge.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-huge.spec.js index 8931c6ee1..6f2ce0fd7 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-huge.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-huge.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Text] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-interactions.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-interactions.spec.js index cb3f48cca..9e16ff837 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-interactions.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-interactions.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; import { vi } from 'vitest'; @@ -9,7 +9,9 @@ setConfig({ }); describe('[Interactions] when parsing', () => { + let flowDb; beforeEach(function () { + flowDb = new FlowDb(); flow.parser.yy = flowDb; flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-lines.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-lines.spec.js index ec157e646..25f248532 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-lines.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-lines.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Lines] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js index 55e749a22..0b3f18e40 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('parsing a flow chart with markdown strings', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js index 1669cfada..74eb64e59 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('when parsing directions', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js index f6ed123d7..550f9c9fd 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -31,7 +31,7 @@ const specialChars = ['#', ':', '0', '&', ',', '*', '.', '\\', 'v', '-', '/', '_ describe('[Singlenodes] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js index 22fd48a33..775fb01f3 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Style] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js index 3754766f4..a2d069f90 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Text] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-vertice-chaining.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-vertice-chaining.spec.js index a5b6a2b6d..6e855a3d5 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-vertice-chaining.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-vertice-chaining.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('when parsing flowcharts', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow.spec.js index 8081c8fe4..41ed5ee52 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { cleanupComments } from '../../../diagram-api/comments.js'; import { setConfig } from '../../../config.js'; @@ -9,7 +9,7 @@ setConfig({ describe('parsing a flow chart', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/subgraph.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/subgraph.spec.js index 12b2e4a39..a3a65ecbb 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/subgraph.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/subgraph.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('when parsing subgraphs', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); From 0695893e30d2dc21725e7d10a49e5a7ddea8d346 Mon Sep 17 00:00:00 2001 From: Saurabh Gore Date: Fri, 3 Jan 2025 11:30:21 +0530 Subject: [PATCH 06/57] Added test cases --- packages/mermaid/src/mermaidAPI.spec.ts | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 5bd1b1dfc..54db4ed17 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -832,5 +832,41 @@ graph TD;A--x|text including URL space|B;`) expect(diagram).toBeInstanceOf(Diagram); expect(diagram.type).toBe('flowchart-v2'); }); + + it('should not fuckup db when rendering different diagrams', async () => { + const flwoDiagram1 = await mermaidAPI.getDiagramFromText( + `flowchart LR + %% This is a comment A -- text --> B{node} + A -- text --> B -- text2 --> C` + ); + const flwoDiagram2 = await mermaidAPI.getDiagramFromText( + `flowchart LR + %% This is a comment A -- text --> B{node} + A -- text --> B -- text2 --> C` + ); + // Since flowDiagram will return new Db object each time, we can compare the db to be different. + expect(flwoDiagram1.db).not.toBe(flwoDiagram2.db); + + const classDiagram1 = await mermaidAPI.getDiagramFromText( + `stateDiagram + [*] --> Still + Still --> [*] + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]` + ); + const classDiagram2 = await mermaidAPI.getDiagramFromText( + `stateDiagram + [*] --> Still + Still --> [*] + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]` + ); + // Since sequenceDiagram will return same Db object each time, we can compare the db to be same. + expect(classDiagram1.db).toBe(classDiagram2.db); + }); }); }); From f5bae98098de984bb177b7fbd4cf802447155ae7 Mon Sep 17 00:00:00 2001 From: Saurabh Gore Date: Fri, 3 Jan 2025 14:55:20 +0530 Subject: [PATCH 07/57] Set generation to gen-2 in clear --- cypress/platform/saurabh.html | 63 +++++-------------- .../mermaid/src/diagrams/flowchart/flowDb.ts | 2 +- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/cypress/platform/saurabh.html b/cypress/platform/saurabh.html index 89b314e68..cdb711fb7 100644 --- a/cypress/platform/saurabh.html +++ b/cypress/platform/saurabh.html @@ -62,56 +62,23 @@
-      flowchart LR
-      A@{ icon: "fa:window-minimize", form: circle }
-      E@{ icon: "fa:window-minimize", form: circle }
-      B@{ icon: "fa:bell", form: circle }
-      B2@{ icon: "fa:bell", form: circle }
-      C@{ icon: "fa:address-book",  form: square  }
-      D@{ icon: "fa:star-half",  form: square  }
-      A --> E
-      B --> B2
-
+      flowchart
+          A --> A
+          subgraph B
+            B1 --> B1
+          end
+          subgraph C
+            subgraph C1
+              C2 --> C2
+              subgraph D
+                D1 --> D1
+              end
+              D --> D
+            end
+            C1 --> C1
+          end
 
     
-
-      flowchart TB
-       A --test2--> B2@{ icon: "fa:bell", form: "rounded", label: "B2 aiduaid uyawduad uaduabd uyduadb", pos: "b" }
-       B2 --test--> C
-       D --> B2 --> E
-       style B2 fill:#f9f,stroke:#333,stroke-width:4px
-  
-
-      flowchart BT
-       A --test2--> B2@{ icon: "fa:bell", form: "square", label: "B2", pos: "t", h: 40, w: 30 }
-       B2 --test--> C
-       D --> B2 --> E
-  
-
-      flowchart BT
-       A --test2--> B2@{ icon: "fa:bell", label: "B2 awiugdawu uydgayuiwd wuydguy", pos: "b", h: 40, w: 30 }
-       B2 --test--> C
-  
-
-      flowchart BT
-       A --test2--> B2@{ icon: "fa:bell", label: "B2 dawuygd ayuwgd uy", pos: "t", h: 40, w: 30 }
-       B2 --test--> C
-  
-
-      flowchart TB
-       A --> B2@{ icon: "fa:bell", form: "circle", label: "test augfuyfavf ydvaubfuac", pos: "t", w: 200, h: 100 } --> C
-  
-
-      flowchart TB
-       A --> B2@{ icon: "fa:bell", form: "circle", label: "test augfuyfavf ydvaubfuac", pos: "b", w: 200, h: 100 } --> C
-       D --> B2 --> E
-