From 1e6419a63f7af7b8048187b7f894e19d87f7628b Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 27 Nov 2024 15:52:24 +0100 Subject: [PATCH 001/314] #6088 Updated offset calculations --- .../rendering/flowchart-elk.spec.js | 147 ++++++++++++++++++ cypress/platform/knsv2.html | 88 ++++++++--- packages/mermaid-layout-elk/src/render.ts | 34 ++-- 3 files changed, 223 insertions(+), 46 deletions(-) diff --git a/cypress/integration/rendering/flowchart-elk.spec.js b/cypress/integration/rendering/flowchart-elk.spec.js index c3aba53ea..38bfe6440 100644 --- a/cypress/integration/rendering/flowchart-elk.spec.js +++ b/cypress/integration/rendering/flowchart-elk.spec.js @@ -900,6 +900,153 @@ flowchart LR n7@{ shape: rect} n8@{ shape: rect} +`, + { flowchart: { titleTopMargin: 0 } } + ); + }); + + it('6088-1: should handle diamond shape intersections', () => { + imgSnapshotTest( + `--- +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 +`, + { flowchart: { titleTopMargin: 0 } } + ); + }); + + it('6088-2: should handle diamond shape intersections', () => { + imgSnapshotTest( + `--- +config: + layout: elk +--- + flowchart LR + a + subgraph s0["APA"] + subgraph s8["APA"] + subgraph s1["APA"] + D{"X"} + E[Q] + end + subgraph s3["BAPA"] + F[Q] + I + end + D --> I + D --> I + D --> I + + I{"X"} + end + end + +`, + { flowchart: { titleTopMargin: 0 } } + ); + }); + + it('6088-3: should handle diamond shape intersections', () => { + imgSnapshotTest( + `--- +config: + layout: elk +--- + flowchart LR + a + D{"Use the editor"} + + D -- Mermaid js --> I{"fa:fa-code Text"} + D-->I + D-->I + +`, + { flowchart: { titleTopMargin: 0 } } + ); + }); + + it('6088-4: should handle diamond shape intersections', () => { + imgSnapshotTest( + `--- +config: + layout: elk +--- +flowchart LR + subgraph s1["Untitled subgraph"] + n1["Evaluate"] + n2["Option 1"] + n3["Option 2"] + n4["fa:fa-car Option 3"] + end + subgraph s2["Untitled subgraph"] + n5["Evaluate"] + n6["Option 1"] + n7["Option 2"] + n8["fa:fa-car Option 3"] + end + A["Start"] -- Some text --> B("Continue") + B --> C{"Evaluate"} + C -- One --> D["Option 1"] + C -- Two --> E["Option 2"] + C -- Three --> F["fa:fa-car Option 3"] + n1 -- One --> n2 + n1 -- Two --> n3 + n1 -- Three --> n4 + n5 -- One --> n6 + n5 -- Two --> n7 + n5 -- Three --> n8 + n1@{ shape: diam} + n2@{ shape: rect} + n3@{ shape: rect} + n4@{ shape: rect} + n5@{ shape: diam} + n6@{ shape: rect} + n7@{ shape: rect} + n8@{ shape: rect} + +`, + { flowchart: { titleTopMargin: 0 } } + ); + }); + + it('6088-5: should handle diamond shape intersections', () => { + imgSnapshotTest( + `--- +config: + layout: elk +--- +flowchart LR + A{A} --> B & C + +`, + { flowchart: { titleTopMargin: 0 } } + ); + }); + it('6088-6: should handle diamond shape intersections', () => { + imgSnapshotTest( + `--- +config: + layout: elk +--- +flowchart LR + A{A} --> B & C + subgraph "subbe" + A + end + `, { flowchart: { titleTopMargin: 0 } } ); diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 7ec666c1a..1c7bda8e7 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -88,33 +88,61 @@ -
+    
 ---
 config:
   layout: elk
 ---
-flowchart LR
- subgraph s1["Untitled subgraph"]
-        n1["Evaluate"]
-        n2["Option 1"]
-        n3["Option 2"]
-        n4["fa:fa-car Option 3"]
-  end
-    n1 -- One --> n2
-    n1 -- Two --> n3
-    n1 -- Three --> n4
-    n5
-    n1@{ shape: diam}
-    n2@{ shape: rect}
-    n3@{ shape: rect}
-    n4@{ shape: rect}
-    A["Start"] -- Some text --> B("Continue")
-    B --> C{"Evaluate"}
-    C -- One --> D["Option 1"]
-    C -- Two --> E["Option 2"]
-    C -- Three --> F["fa:fa-car Option 3"]
+      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
+    
+
+---
+config:
+  layout: elk
+---
+      flowchart LR
+      a
+      subgraph s0["APA"]
+      subgraph s8["APA"]
+      subgraph s1["APA"]
+        D{"X"}
+        E[Q]
+      end
+      subgraph s3["BAPA"]
+        F[Q]
+        I
+      end
+            D --> I
+            D --> I
+            D --> I
+
+      I{"X"}
+      end
+      end
+    
+
+---
+config:
+  layout: elk
+---
+      flowchart LR
+      a
+        D{"Use the editor"}
+
+      D -- Mermaid js --> I{"fa:fa-code Text"}
+      D-->I
+      D-->I
     
 ---
@@ -155,7 +183,7 @@ flowchart LR
     n8@{ shape: rect}
 
     
-
+    
 ---
 config:
   layout: elk
@@ -171,7 +199,7 @@ flowchart LR
 
 
     
-
+    
 ---
 config:
   layout: elk
@@ -180,7 +208,19 @@ flowchart LR
     A{A} --> B & C
 
-
+    
+---
+config:
+  layout: elk
+---
+flowchart LR
+    A{A} --> B & C
+    subgraph "subbe"
+      A
+    end
+
+
 ---
 config:
   layout: elk
diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts
index 1216b5dc8..59b97c557 100644
--- a/packages/mermaid-layout-elk/src/render.ts
+++ b/packages/mermaid-layout-elk/src/render.ts
@@ -705,14 +705,11 @@ export const render = async (
     bounds: { x: any; y: any; width: any; height: any; padding: any },
     isDiamond: boolean
   ) => {
-    log.debug('UIO cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
+    log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
     const points: any[] = [];
     let lastPointOutside = _points[0];
     let isInside = false;
     _points.forEach((point: any) => {
-      // const node = clusterDb[edge.toCluster].node;
-      log.debug(' checking point', point, bounds);
-
       // check if point is inside the boundary rect
       if (!outsideNode(bounds, point) && !isInside) {
         // First point inside the rect found
@@ -906,7 +903,7 @@ export const render = async (
 
       const offset = calcOffset(sourceId, targetId, parentLookupDb);
       log.debug(
-        'offset',
+        'APA18 offset',
         offset,
         sourceId,
         ' ==> ',
@@ -971,29 +968,22 @@ export const render = async (
         }
         if (startNode.shape === 'diamond' || startNode.shape === 'diam') {
           edge.points.unshift({
-            x: startNode.x + startNode.width / 2 + offset.x,
-            y: startNode.y + startNode.height / 2 + offset.y,
+            x: startNode.offset.posX + startNode.width / 2,
+            y: startNode.offset.posY + startNode.height / 2,
           });
         }
         if (endNode.shape === 'diamond' || endNode.shape === 'diam') {
-          const x = endNode.x + endNode.width / 2 + offset.x;
-          // Add a point at the center of the diamond
-          if (
-            Math.abs(edge.points[edge.points.length - 1].y - endNode.y - offset.y) > 0.01 ||
-            Math.abs(edge.points[edge.points.length - 1].x - x) > 0.001
-          ) {
-            edge.points.push({
-              x: endNode.x + endNode.width / 2 + offset.x,
-              y: endNode.y + endNode.height / 2 + offset.y,
-            });
-          }
+          edge.points.push({
+            x: endNode.offset.posX + endNode.width / 2,
+            y: endNode.offset.posY + endNode.height / 2,
+          });
         }
 
         edge.points = cutPathAtIntersect(
           edge.points.reverse(),
           {
-            x: startNode.x + startNode.width / 2 + offset.x,
-            y: startNode.y + startNode.height / 2 + offset.y,
+            x: startNode.offset.posX + startNode.width / 2,
+            y: startNode.offset.posY + startNode.height / 2,
             width: sw,
             height: startNode.height,
             padding: startNode.padding,
@@ -1004,8 +994,8 @@ export const render = async (
         edge.points = cutPathAtIntersect(
           edge.points,
           {
-            x: endNode.x + ew / 2 + endNode.offset.x,
-            y: endNode.y + endNode.height / 2 + endNode.offset.y,
+            x: endNode.offset.posX + endNode.width / 2,
+            y: endNode.offset.posY + endNode.height / 2,
             width: ew,
             height: endNode.height,
             padding: endNode.padding,

From c8e50276e877c4de7593a09ec458c99353e65af8 Mon Sep 17 00:00:00 2001
From: Knut Sveidqvist 
Date: Wed, 27 Nov 2024 15:54:05 +0100
Subject: [PATCH 002/314] Added changeset

---
 .changeset/hungry-guests-drive.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/hungry-guests-drive.md

diff --git a/.changeset/hungry-guests-drive.md b/.changeset/hungry-guests-drive.md
new file mode 100644
index 000000000..7a09fa1cc
--- /dev/null
+++ b/.changeset/hungry-guests-drive.md
@@ -0,0 +1,5 @@
+---
+'@mermaid-js/layout-elk': patch
+---
+
+fix: Updated offset calculations for diamond shape when handling intersections

From 4c8c48cde95c1ab9c53e147913cf4eabd6b83a93 Mon Sep 17 00:00:00 2001
From: Knut Sveidqvist 
Date: Thu, 28 Nov 2024 14:31:54 +0100
Subject: [PATCH 003/314] Generic solution for intersection of shapes with elk

---
 cypress/platform/knsv2.html                   |  59 +++-
 packages/mermaid-layout-elk/src/render.ts     | 308 +++---------------
 .../rendering-elements/edges.js               | 119 ++-----
 .../intersect/intersect-line.js               | 129 +++++---
 .../rendering-elements/shapes/circle.ts       |   6 +-
 .../rendering-elements/shapes/drawRect.ts     |   5 +
 .../rendering-elements/shapes/question.ts     |  31 +-
 packages/mermaid/src/rendering-util/types.ts  |   2 +
 packages/mermaid/src/types.ts                 |   6 +
 packages/mermaid/src/utils/lineWithOffset.ts  |   3 +-
 10 files changed, 234 insertions(+), 434 deletions(-)

diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html
index 1c7bda8e7..2ffaeae37 100644
--- a/cypress/platform/knsv2.html
+++ b/cypress/platform/knsv2.html
@@ -90,6 +90,29 @@
   
     
 ---
+config:
+  layout: elk
+---
+flowchart LR
+ %% subgraph s1["Untitled subgraph"]
+        C{"Evaluate"}
+ %% end
+
+    B --> C
+    
+
+---
+config:
+  layout: elk
+---
+flowchart LR
+%% A ==> B
+%% A2 --> B2
+      D --> I((I the Circle))
+            D --> I
+    
+
+---
 config:
   layout: elk
 ---
@@ -100,9 +123,9 @@ config:
       end
 
 
-      D -- Mermaid js --> I{"fa:fa-code Text"}
-            D --> I
+      D -- Mermaid js --> I(("fa:fa-code Text"))
             D --> I
+           D --> E --> I
 
       end
     
@@ -238,7 +261,7 @@ flowchart LR
-
+    
 ---
 config:
   kanban:
@@ -257,81 +280,81 @@ kanban
     task3[💻 Develop login feature]@{ ticket: 103 }
 
     
-
+    
 flowchart LR
 nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
 
     
-
+    
 flowchart LR
 nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
 style A fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
 nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
 A:::AClass
 classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
   nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
 
     
-
+    
 flowchart LR
 nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
 
     
-
+    
 flowchart LR
 nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
 style A fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
 nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
 A:::AClass
 classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
   nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
 
     
-
+    
 flowchart LR
 nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
 
     
-
+    
 flowchart LR
 nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
 style A fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
 nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
 A:::AClass
 classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
   nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
   A:::AClass
   classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
   nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
   style A fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 kanban
   id2[In progress]
     docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
     
-
+    
 ---
 config:
   kanban:
diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts
index 59b97c557..2e666ab8d 100644
--- a/packages/mermaid-layout-elk/src/render.ts
+++ b/packages/mermaid-layout-elk/src/render.ts
@@ -60,6 +60,7 @@ export const render = async (
       const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
       const boundingBox = childNodeEl.node()!.getBBox();
       child.domId = childNodeEl;
+      child.calcIntersect = node.calcIntersect;
       child.width = boundingBox.width;
       child.height = boundingBox.height;
     } else {
@@ -459,228 +460,6 @@ export const render = async (
     }
   }
 
-  function intersectLine(
-    p1: { y: number; x: number },
-    p2: { y: number; x: number },
-    q1: { x: any; y: any },
-    q2: { x: any; y: any }
-  ) {
-    log.debug('UIO intersectLine', p1, p2, q1, q2);
-    // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
-    // p7 and p473.
-
-    // let a1, a2, b1, b2, c1, c2;
-    // let r1, r2, r3, r4;
-    // let denom, offset, num;
-    // let x, y;
-
-    // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
-    // b1 y + c1 = 0.
-    const a1 = p2.y - p1.y;
-    const b1 = p1.x - p2.x;
-    const c1 = p2.x * p1.y - p1.x * p2.y;
-
-    // Compute r3 and r4.
-    const r3 = a1 * q1.x + b1 * q1.y + c1;
-    const r4 = a1 * q2.x + b1 * q2.y + c1;
-
-    const epsilon = 1e-6;
-
-    // Check signs of r3 and r4. If both point 3 and point 4 lie on
-    // same side of line 1, the line segments do not intersect.
-    if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
-      return /*DON'T_INTERSECT*/;
-    }
-
-    // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
-    const a2 = q2.y - q1.y;
-    const b2 = q1.x - q2.x;
-    const c2 = q2.x * q1.y - q1.x * q2.y;
-
-    // Compute r1 and r2
-    const r1 = a2 * p1.x + b2 * p1.y + c2;
-    const r2 = a2 * p2.x + b2 * p2.y + c2;
-
-    // Check signs of r1 and r2. If both point 1 and point 2 lie
-    // on same side of second line segment, the line segments do
-    // not intersect.
-    if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
-      return /*DON'T_INTERSECT*/;
-    }
-
-    // Line segments intersect: compute intersection point.
-    const denom = a1 * b2 - a2 * b1;
-    if (denom === 0) {
-      return /*COLLINEAR*/;
-    }
-
-    const offset = Math.abs(denom / 2);
-
-    // The denom/2 is to get rounding instead of truncating. It
-    // is added or subtracted to the numerator, depending upon the
-    // sign of the numerator.
-    let num = b1 * c2 - b2 * c1;
-    const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
-
-    num = a2 * c1 - a1 * c2;
-    const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
-
-    return { x: x, y: y };
-  }
-
-  function sameSign(r1: number, r2: number) {
-    return r1 * r2 > 0;
-  }
-  const diamondIntersection = (
-    bounds: { x: any; y: any; width: any; height: any },
-    outsidePoint: { x: number; y: number },
-    insidePoint: any
-  ) => {
-    const x1 = bounds.x;
-    const y1 = bounds.y;
-
-    const w = bounds.width; //+ bounds.padding;
-    const h = bounds.height; // + bounds.padding;
-
-    const polyPoints = [
-      { x: x1, y: y1 - h / 2 },
-      { x: x1 + w / 2, y: y1 },
-      { x: x1, y: y1 + h / 2 },
-      { x: x1 - w / 2, y: y1 },
-    ];
-    log.debug(
-      `APA16 diamondIntersection calc abc89:
-  outsidePoint: ${JSON.stringify(outsidePoint)}
-  insidePoint : ${JSON.stringify(insidePoint)}
-  node-bounds       : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`,
-      JSON.stringify(polyPoints)
-    );
-
-    const intersections = [];
-
-    let minX = Number.POSITIVE_INFINITY;
-    let minY = Number.POSITIVE_INFINITY;
-
-    polyPoints.forEach(function (entry) {
-      minX = Math.min(minX, entry.x);
-      minY = Math.min(minY, entry.y);
-    });
-
-    const left = x1 - w / 2 - minX;
-    const top = y1 - h / 2 - minY;
-
-    for (let i = 0; i < polyPoints.length; i++) {
-      const p1 = polyPoints[i];
-      const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
-      const intersect = intersectLine(
-        bounds,
-        outsidePoint,
-        { x: left + p1.x, y: top + p1.y },
-        { x: left + p2.x, y: top + p2.y }
-      );
-
-      if (intersect) {
-        intersections.push(intersect);
-      }
-    }
-
-    if (!intersections.length) {
-      return bounds;
-    }
-
-    log.debug('UIO intersections', intersections);
-
-    if (intersections.length > 1) {
-      // More intersections, find the one nearest to edge end point
-      intersections.sort(function (p, q) {
-        const pdx = p.x - outsidePoint.x;
-        const pdy = p.y - outsidePoint.y;
-        const distp = Math.sqrt(pdx * pdx + pdy * pdy);
-
-        const qdx = q.x - outsidePoint.x;
-        const qdy = q.y - outsidePoint.y;
-        const distq = Math.sqrt(qdx * qdx + qdy * qdy);
-
-        return distp < distq ? -1 : distp === distq ? 0 : 1;
-      });
-    }
-
-    return intersections[0];
-  };
-
-  const intersection = (
-    node: { x: any; y: any; width: number; height: number },
-    outsidePoint: { x: number; y: number },
-    insidePoint: { x: number; y: number }
-  ) => {
-    log.debug(`intersection calc abc89:
-  outsidePoint: ${JSON.stringify(outsidePoint)}
-  insidePoint : ${JSON.stringify(insidePoint)}
-  node        : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`);
-    const x = node.x;
-    const y = node.y;
-
-    const dx = Math.abs(x - insidePoint.x);
-    // const dy = Math.abs(y - insidePoint.y);
-    const w = node.width / 2;
-    let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
-    const h = node.height / 2;
-
-    const Q = Math.abs(outsidePoint.y - insidePoint.y);
-    const R = Math.abs(outsidePoint.x - insidePoint.x);
-
-    if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
-      // Intersection is top or bottom of rect.
-      const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
-      r = (R * q) / Q;
-      const res = {
-        x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
-        y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
-      };
-
-      if (r === 0) {
-        res.x = outsidePoint.x;
-        res.y = outsidePoint.y;
-      }
-      if (R === 0) {
-        res.x = outsidePoint.x;
-      }
-      if (Q === 0) {
-        res.y = outsidePoint.y;
-      }
-
-      log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line
-
-      return res;
-    } else {
-      // Intersection onn sides of rect
-      if (insidePoint.x < outsidePoint.x) {
-        r = outsidePoint.x - w - x;
-      } else {
-        // r = outsidePoint.x - w - x;
-        r = x - w - outsidePoint.x;
-      }
-      const q = (Q * r) / R;
-      //  OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w;
-      // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
-      let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
-      // let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
-      let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
-      log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y });
-      if (r === 0) {
-        _x = outsidePoint.x;
-        _y = outsidePoint.y;
-      }
-      if (R === 0) {
-        _x = outsidePoint.x;
-      }
-      if (Q === 0) {
-        _y = outsidePoint.y;
-      }
-
-      return { x: _x, y: _y };
-    }
-  };
   const outsideNode = (
     node: { x: any; y: any; width: number; height: number },
     point: { x: number; y: number }
@@ -703,9 +482,9 @@ export const render = async (
   const cutPathAtIntersect = (
     _points: any[],
     bounds: { x: any; y: any; width: any; height: any; padding: any },
-    isDiamond: boolean
+    calcIntersect: any
   ) => {
-    log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
+    log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds);
     const points: any[] = [];
     let lastPointOutside = _points[0];
     let isInside = false;
@@ -714,20 +493,20 @@ export const render = async (
       if (!outsideNode(bounds, point) && !isInside) {
         // First point inside the rect found
         // Calc the intersection coord between the point anf the last point outside the rect
-        let inter;
-
-        if (isDiamond) {
-          const inter2 = diamondIntersection(bounds, lastPointOutside, point);
-          const distance = Math.sqrt(
-            (lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2
-          );
-          if (distance > 1) {
-            inter = inter2;
-          }
-        }
-        if (!inter) {
-          inter = intersection(bounds, lastPointOutside, point);
-        }
+        const inter = calcIntersect({ ...bounds, ...point }, lastPointOutside);
+        // console.log(
+        //   'APA30 inside',
+        //   '\nbounds',
+        //   { ...bounds, ...point },
+        //   `\npoint`,
+        //   point,
+        //   '\no outside point',
+        //   lastPointOutside,
+        //   '\npoints',
+        //   ...points,
+        //   '\nIntersection',
+        //   inter
+        // );
 
         // Check case where the intersection is the same as the last point
         let pointPresent = false;
@@ -966,43 +745,44 @@ export const render = async (
             startNode.innerHTML
           );
         }
-        if (startNode.shape === 'diamond' || startNode.shape === 'diam') {
+
+        if (startNode.calcIntersect) {
           edge.points.unshift({
             x: startNode.offset.posX + startNode.width / 2,
             y: startNode.offset.posY + startNode.height / 2,
+            width: startNode.width,
+            height: startNode.height,
           });
+          edge.points = cutPathAtIntersect(
+            edge.points.reverse(),
+            {
+              x: startNode.offset.posX + startNode.width / 2,
+              y: startNode.offset.posY + startNode.height / 2,
+              width: sw,
+              height: startNode.height,
+              padding: startNode.padding,
+            },
+            startNode.calcIntersect
+          ).reverse();
         }
-        if (endNode.shape === 'diamond' || endNode.shape === 'diam') {
+        if (endNode.calcIntersect) {
           edge.points.push({
             x: endNode.offset.posX + endNode.width / 2,
             y: endNode.offset.posY + endNode.height / 2,
           });
+          edge.points = cutPathAtIntersect(
+            edge.points,
+            {
+              x: endNode.offset.posX + endNode.width / 2,
+              y: endNode.offset.posY + endNode.height / 2,
+              width: ew,
+              height: endNode.height,
+              padding: endNode.padding,
+            },
+            endNode.calcIntersect
+          );
         }
 
-        edge.points = cutPathAtIntersect(
-          edge.points.reverse(),
-          {
-            x: startNode.offset.posX + startNode.width / 2,
-            y: startNode.offset.posY + startNode.height / 2,
-            width: sw,
-            height: startNode.height,
-            padding: startNode.padding,
-          },
-          startNode.shape === 'diamond' || startNode.shape === 'diam'
-        ).reverse();
-
-        edge.points = cutPathAtIntersect(
-          edge.points,
-          {
-            x: endNode.offset.posX + endNode.width / 2,
-            y: endNode.offset.posY + endNode.height / 2,
-            width: ew,
-            height: endNode.height,
-            padding: endNode.padding,
-          },
-          endNode.shape === 'diamond' || endNode.shape === 'diam'
-        );
-
         const paths = insertEdge(
           edgesEl,
           edge,
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
index a6a7a55f7..22fa6c1bb 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js
+++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
@@ -335,90 +335,35 @@ const cutPathAtIntersect = (_points, boundaryNode) => {
   return points;
 };
 
-function extractCornerPoints(points) {
-  const cornerPoints = [];
-  const cornerPointPositions = [];
-  for (let i = 1; i < points.length - 1; i++) {
-    const prev = points[i - 1];
-    const curr = points[i];
-    const next = points[i + 1];
-    if (
-      prev.x === curr.x &&
-      curr.y === next.y &&
-      Math.abs(curr.x - next.x) > 5 &&
-      Math.abs(curr.y - prev.y) > 5
-    ) {
-      cornerPoints.push(curr);
-      cornerPointPositions.push(i);
-    } else if (
-      prev.y === curr.y &&
-      curr.x === next.x &&
-      Math.abs(curr.x - prev.x) > 5 &&
-      Math.abs(curr.y - next.y) > 5
-    ) {
-      cornerPoints.push(curr);
-      cornerPointPositions.push(i);
-    }
+const adjustForArrowHeads = function (lineData, size = 5) {
+  const newLineData = [...lineData];
+  const lastPoint = lineData[lineData.length - 1];
+  const secondLastPoint = lineData[lineData.length - 2];
+
+  const distanceBetweenLastPoints = Math.sqrt(
+    (lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2
+  );
+
+  if (distanceBetweenLastPoints < size) {
+    // Calculate the direction vector from the last point to the second last point
+    const directionX = secondLastPoint.x - lastPoint.x;
+    const directionY = secondLastPoint.y - lastPoint.y;
+
+    // Normalize the direction vector
+    const magnitude = Math.sqrt(directionX ** 2 + directionY ** 2);
+    const normalizedX = directionX / magnitude;
+    const normalizedY = directionY / magnitude;
+
+    // Calculate the new position for the second last point
+    const adjustedSecondLastPoint = {
+      x: lastPoint.x + normalizedX * size,
+      y: lastPoint.y + normalizedY * size,
+    };
+
+    // Replace the second last point in the new line data
+    newLineData[newLineData.length - 2] = adjustedSecondLastPoint;
   }
-  return { cornerPoints, cornerPointPositions };
-}
 
-const findAdjacentPoint = function (pointA, pointB, distance) {
-  const xDiff = pointB.x - pointA.x;
-  const yDiff = pointB.y - pointA.y;
-  const length = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
-  const ratio = distance / length;
-  return { x: pointB.x - ratio * xDiff, y: pointB.y - ratio * yDiff };
-};
-
-const fixCorners = function (lineData) {
-  const { cornerPointPositions } = extractCornerPoints(lineData);
-  const newLineData = [];
-  for (let i = 0; i < lineData.length; i++) {
-    if (cornerPointPositions.includes(i)) {
-      const prevPoint = lineData[i - 1];
-      const nextPoint = lineData[i + 1];
-      const cornerPoint = lineData[i];
-
-      const newPrevPoint = findAdjacentPoint(prevPoint, cornerPoint, 5);
-      const newNextPoint = findAdjacentPoint(nextPoint, cornerPoint, 5);
-
-      const xDiff = newNextPoint.x - newPrevPoint.x;
-      const yDiff = newNextPoint.y - newPrevPoint.y;
-      newLineData.push(newPrevPoint);
-
-      const a = Math.sqrt(2) * 2;
-      let newCornerPoint = { x: cornerPoint.x, y: cornerPoint.y };
-      if (Math.abs(nextPoint.x - prevPoint.x) > 10 && Math.abs(nextPoint.y - prevPoint.y) >= 10) {
-        log.debug(
-          'Corner point fixing',
-          Math.abs(nextPoint.x - prevPoint.x),
-          Math.abs(nextPoint.y - prevPoint.y)
-        );
-        const r = 5;
-        if (cornerPoint.x === newPrevPoint.x) {
-          newCornerPoint = {
-            x: xDiff < 0 ? newPrevPoint.x - r + a : newPrevPoint.x + r - a,
-            y: yDiff < 0 ? newPrevPoint.y - a : newPrevPoint.y + a,
-          };
-        } else {
-          newCornerPoint = {
-            x: xDiff < 0 ? newPrevPoint.x - a : newPrevPoint.x + a,
-            y: yDiff < 0 ? newPrevPoint.y - r + a : newPrevPoint.y + r - a,
-          };
-        }
-      } else {
-        log.debug(
-          'Corner point skipping fixing',
-          Math.abs(nextPoint.x - prevPoint.x),
-          Math.abs(nextPoint.y - prevPoint.y)
-        );
-      }
-      newLineData.push(newCornerPoint, newNextPoint);
-    } else {
-      newLineData.push(lineData[i]);
-    }
-  }
   return newLineData;
 };
 
@@ -462,8 +407,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
   }
 
   let lineData = points.filter((p) => !Number.isNaN(p.y));
-  lineData = fixCorners(lineData);
+  lineData = adjustForArrowHeads(lineData);
+  // lineData = fixCorners(lineData);
   let curve = curveBasis;
+  // let curve = curveLinear;
   if (edge.curve) {
     curve = edge.curve;
   }
@@ -543,9 +490,9 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
   // lineData.forEach((point) => {
   //   elem
   //     .append('circle')
-  //     .style('stroke', 'blue')
-  //     .style('fill', 'blue')
-  //     .attr('r', 3)
+  //     .style('stroke', 'red')
+  //     .style('fill', 'red')
+  //     .attr('r', 1)
   //     .attr('cx', point.x)
   //     .attr('cy', point.y);
   // });
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/intersect/intersect-line.js b/packages/mermaid/src/rendering-util/rendering-elements/intersect/intersect-line.js
index bd3eb497f..b12f8688e 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/intersect/intersect-line.js
+++ b/packages/mermaid/src/rendering-util/rendering-elements/intersect/intersect-line.js
@@ -2,64 +2,87 @@
  * Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.
  */
 function intersectLine(p1, p2, q1, q2) {
-  // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
-  // p7 and p473.
+  {
+    // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
+    // p7 and p473.
 
-  var a1, a2, b1, b2, c1, c2;
-  var r1, r2, r3, r4;
-  var denom, offset, num;
-  var x, y;
+    // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
+    // b1 y + c1 = 0.
+    const a1 = p2.y - p1.y;
+    const b1 = p1.x - p2.x;
+    const c1 = p2.x * p1.y - p1.x * p2.y;
 
-  // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
-  // b1 y + c1 = 0.
-  a1 = p2.y - p1.y;
-  b1 = p1.x - p2.x;
-  c1 = p2.x * p1.y - p1.x * p2.y;
+    // Compute r3 and r4.
+    const r3 = a1 * q1.x + b1 * q1.y + c1;
+    const r4 = a1 * q2.x + b1 * q2.y + c1;
 
-  // Compute r3 and r4.
-  r3 = a1 * q1.x + b1 * q1.y + c1;
-  r4 = a1 * q2.x + b1 * q2.y + c1;
+    const epsilon = 1e-6;
 
-  // Check signs of r3 and r4. If both point 3 and point 4 lie on
-  // same side of line 1, the line segments do not intersect.
-  if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
-    return /*DON'T_INTERSECT*/;
+    // Check signs of r3 and r4. If both point 3 and point 4 lie on
+    // same side of line 1, the line segments do not intersect.
+    if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
+      return /*DON'T_INTERSECT*/;
+    }
+
+    // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
+    const a2 = q2.y - q1.y;
+    const b2 = q1.x - q2.x;
+    const c2 = q2.x * q1.y - q1.x * q2.y;
+
+    // Compute r1 and r2
+    const r1 = a2 * p1.x + b2 * p1.y + c2;
+    const r2 = a2 * p2.x + b2 * p2.y + c2;
+
+    // Check signs of r1 and r2. If both point 1 and point 2 lie
+    // on same side of second line segment, the line segments do
+    // not intersect.
+    if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
+      return /*DON'T_INTERSECT*/;
+    }
+
+    // Line segments intersect: compute intersection point.
+    const denom = a1 * b2 - a2 * b1;
+    if (denom === 0) {
+      return /*COLLINEAR*/;
+    }
+
+    const offset = Math.abs(denom / 2);
+
+    // The denom/2 is to get rounding instead of truncating. It
+    // is added or subtracted to the numerator, depending upon the
+    // sign of the numerator.
+    let num = b1 * c2 - b2 * c1;
+    const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
+
+    num = a2 * c1 - a1 * c2;
+    const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
+    // console.log(
+    //   'APA30 intersectLine intersection',
+    //   '\np1: (',
+    //   p1.x,
+    //   p1.y,
+    //   ')',
+    //   '\np2: (',
+    //   p2.x,
+    //   p2.y,
+    //   ')',
+    //   '\nq1: (',
+    //   q1.x,
+    //   q1.y,
+    //   ')',
+    //   '\np1: (',
+    //   q2.x,
+    //   q2.y,
+    //   ')',
+    //   'offset:',
+    //   offset,
+    //   '\nintersection: (',
+    //   x,
+    //   y,
+    //   ')'
+    // );
+    return { x: x, y: y };
   }
-
-  // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
-  a2 = q2.y - q1.y;
-  b2 = q1.x - q2.x;
-  c2 = q2.x * q1.y - q1.x * q2.y;
-
-  // Compute r1 and r2
-  r1 = a2 * p1.x + b2 * p1.y + c2;
-  r2 = a2 * p2.x + b2 * p2.y + c2;
-
-  // Check signs of r1 and r2. If both point 1 and point 2 lie
-  // on same side of second line segment, the line segments do
-  // not intersect.
-  if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {
-    return /*DON'T_INTERSECT*/;
-  }
-
-  // Line segments intersect: compute intersection point.
-  denom = a1 * b2 - a2 * b1;
-  if (denom === 0) {
-    return /*COLLINEAR*/;
-  }
-
-  offset = Math.abs(denom / 2);
-
-  // The denom/2 is to get rounding instead of truncating. It
-  // is added or subtracted to the numerator, depending upon the
-  // sign of the numerator.
-  num = b1 * c2 - b2 * c1;
-  x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
-
-  num = a2 * c1 - a1 * c2;
-  y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
-
-  return { x: x, y: y };
 }
 
 function sameSign(r1, r2) {
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts
index 6b3be6765..80b59bdc1 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts
+++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts
@@ -6,6 +6,7 @@ import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
 import rough from 'roughjs';
 import type { D3Selection } from '../../../types.js';
 import { handleUndefinedAttr } from '../../../utils.js';
+import type { Bounds, Point } from '../../../types.js';
 
 export async function circle(parent: D3Selection, node: Node) {
   const { labelStyles, nodeStyles } = styles2String(node);
@@ -35,7 +36,10 @@ export async function circle(parent: D3Selection(
   parent: D3Selection,
@@ -62,6 +63,10 @@ export async function drawRect(
 
   updateNodeBounds(node, rect);
 
+  node.calcIntersect = function (bounds: Bounds, point: Point) {
+    return intersect.rect(bounds, point);
+  };
+
   node.intersect = function (point) {
     return intersect.rect(node, point);
   };
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts
index eef958169..48511a866 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts
+++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts
@@ -1,4 +1,3 @@
-import { log } from '../../../logger.js';
 import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
 import intersect from '../intersect/index.js';
 import type { Node } from '../../types.js';
@@ -6,6 +5,7 @@ import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
 import rough from 'roughjs';
 import { insertPolygonShape } from './insertPolygonShape.js';
 import type { D3Selection } from '../../../types.js';
+import type { Bounds, Point } from '../../../types.js';
 
 export const createDecisionBoxPathD = (x: number, y: number, size: number): string => {
   return [
@@ -59,17 +59,28 @@ export async function question(parent: D3Selection
   }
 
   updateNodeBounds(node, polygon);
+  node.calcIntersect = function (bounds: Bounds, point: Point) {
+    const w = bbox.width + node.padding;
+    const h = bbox.height + node.padding;
+    const s = w + h;
+
+    // Define polygon points
+    const points = [
+      { x: s / 2, y: 0 },
+      { x: s, y: -s / 2 },
+      { x: s / 2, y: -s },
+      { x: 0, y: -s / 2 },
+    ];
+
+    // Calculate the intersection point
+    const res = intersect.polygon(bounds, points, point);
+
+    return { x: res.x - 0.5, y: res.y - 0.5 }; // Adjusted result
+  };
 
   node.intersect = function (point) {
-    log.debug(
-      'APA12 Intersect called SPLIT\npoint:',
-      point,
-      '\nnode:\n',
-      node,
-      '\nres:',
-      intersect.polygon(node, points, point)
-    );
-    return intersect.polygon(node, points, point);
+    // @ts-ignore TODO fix this (KNSV)
+    return this.calcIntersect(node as Bounds, point);
   };
 
   return shapeSvg;
diff --git a/packages/mermaid/src/rendering-util/types.ts b/packages/mermaid/src/rendering-util/types.ts
index 86cfd50b3..8dc3ddf20 100644
--- a/packages/mermaid/src/rendering-util/types.ts
+++ b/packages/mermaid/src/rendering-util/types.ts
@@ -2,6 +2,7 @@ export type MarkdownWordType = 'normal' | 'strong' | 'em';
 import type { MermaidConfig } from '../config.type.js';
 import type { ClusterShapeID } from './rendering-elements/clusters.js';
 import type { ShapeID } from './rendering-elements/shapes.js';
+import type { Bounds, Point } from '../types.js';
 export interface MarkdownWord {
   content: string;
   type: MarkdownWordType;
@@ -43,6 +44,7 @@ interface BaseNode {
   height?: number;
   // Specific properties for State Diagram nodes TODO remove and use generic properties
   intersect?: (point: any) => any;
+  calcIntersect?: (bounds: Bounds, point: Point) => any;
 
   // Non-generic properties
   rx?: number; // Used for rounded corners in Rect, Ellipse, etc.Maybe it to specialized RectNode, EllipseNode, etc.
diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts
index 5587ca3f4..7e219d7fe 100644
--- a/packages/mermaid/src/types.ts
+++ b/packages/mermaid/src/types.ts
@@ -18,6 +18,12 @@ export interface Point {
   x: number;
   y: number;
 }
+export interface Bounds {
+  x: number;
+  y: number;
+  width: number;
+  height: number;
+}
 
 export interface TextDimensionConfig {
   fontSize?: number;
diff --git a/packages/mermaid/src/utils/lineWithOffset.ts b/packages/mermaid/src/utils/lineWithOffset.ts
index 800a5ffaf..057944325 100644
--- a/packages/mermaid/src/utils/lineWithOffset.ts
+++ b/packages/mermaid/src/utils/lineWithOffset.ts
@@ -3,7 +3,7 @@ import type { EdgeData, Point } from '../types.js';
 // We need to draw the lines a bit shorter to avoid drawing
 // under any transparent markers.
 // The offsets are calculated from the markers' dimensions.
-const markerOffsets = {
+export const markerOffsets = {
   aggregation: 18,
   extension: 18,
   composition: 18,
@@ -104,7 +104,6 @@ export const getLineFunctionsWithOffset = (
         adjustment *= DIRECTION === 'right' ? -1 : 1;
         offset += adjustment;
       }
-
       return pointTransformer(d).x + offset;
     },
     y: function (

From 1e3ea133234590e2cb00f96a59db7a30820673e0 Mon Sep 17 00:00:00 2001
From: Knut Sveidqvist 
Date: Fri, 29 Nov 2024 09:07:47 +0100
Subject: [PATCH 004/314] Fix for when last point is on the intersection

---
 packages/mermaid-layout-elk/src/render.ts     | 125 ++++--------------
 .../rendering-elements/shapes/question.ts     |  20 ++-
 2 files changed, 44 insertions(+), 101 deletions(-)

diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts
index 2e666ab8d..8f49a2476 100644
--- a/packages/mermaid-layout-elk/src/render.ts
+++ b/packages/mermaid-layout-elk/src/render.ts
@@ -4,7 +4,8 @@ import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from '
 import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
 
 type Node = LayoutData['nodes'][number];
-
+// Used to calculate distances in order to avoid floating number rounding issues when comparing floating numbers
+const epsilon = 0.0001;
 interface LabelData {
   width: number;
   height: number;
@@ -17,7 +18,16 @@ interface NodeWithVertex extends Omit {
   labelData?: LabelData;
   domId?: Node['domId'] | SVGGroup | d3.Selection;
 }
-
+interface Point {
+  x: number;
+  y: number;
+}
+function distance(p1?: Point, p2?: Point): number {
+  if (!p1 || !p2) {
+    return 0;
+  }
+  return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
+}
 export const render = async (
   data4Layout: LayoutData,
   svg: SVG,
@@ -460,80 +470,6 @@ export const render = async (
     }
   }
 
-  const outsideNode = (
-    node: { x: any; y: any; width: number; height: number },
-    point: { x: number; y: number }
-  ) => {
-    const x = node.x;
-    const y = node.y;
-    const dx = Math.abs(point.x - x);
-    const dy = Math.abs(point.y - y);
-    const w = node.width / 2;
-    const h = node.height / 2;
-    if (dx >= w || dy >= h) {
-      return true;
-    }
-    return false;
-  };
-  /**
-   * This function will page a path and node where the last point(s) in the path is inside the node
-   * and return an update path ending by the border of the node.
-   */
-  const cutPathAtIntersect = (
-    _points: any[],
-    bounds: { x: any; y: any; width: any; height: any; padding: any },
-    calcIntersect: any
-  ) => {
-    log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds);
-    const points: any[] = [];
-    let lastPointOutside = _points[0];
-    let isInside = false;
-    _points.forEach((point: any) => {
-      // check if point is inside the boundary rect
-      if (!outsideNode(bounds, point) && !isInside) {
-        // First point inside the rect found
-        // Calc the intersection coord between the point anf the last point outside the rect
-        const inter = calcIntersect({ ...bounds, ...point }, lastPointOutside);
-        // console.log(
-        //   'APA30 inside',
-        //   '\nbounds',
-        //   { ...bounds, ...point },
-        //   `\npoint`,
-        //   point,
-        //   '\no outside point',
-        //   lastPointOutside,
-        //   '\npoints',
-        //   ...points,
-        //   '\nIntersection',
-        //   inter
-        // );
-
-        // Check case where the intersection is the same as the last point
-        let pointPresent = false;
-        points.forEach((p) => {
-          pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y);
-        });
-        // if (!pointPresent) {
-        if (!points.some((e) => e.x === inter.x && e.y === inter.y)) {
-          points.push(inter);
-        } else {
-          log.debug('abc88 no intersect', inter, points);
-        }
-        // points.push(inter);
-        isInside = true;
-      } else {
-        // Outside
-        log.debug('abc88 outside', point, lastPointOutside, points);
-        lastPointOutside = point;
-        // points.push(point);
-        if (!isInside) {
-          points.push(point);
-        }
-      }
-    });
-    return points;
-  };
-
   // @ts-ignore - ELK is not typed
   const elk = new ELK();
   const element = svg.select('g');
@@ -747,40 +683,34 @@ export const render = async (
         }
 
         if (startNode.calcIntersect) {
-          edge.points.unshift({
-            x: startNode.offset.posX + startNode.width / 2,
-            y: startNode.offset.posY + startNode.height / 2,
-            width: startNode.width,
-            height: startNode.height,
-          });
-          edge.points = cutPathAtIntersect(
-            edge.points.reverse(),
+          const intersection = startNode.calcIntersect(
             {
               x: startNode.offset.posX + startNode.width / 2,
               y: startNode.offset.posY + startNode.height / 2,
-              width: sw,
+              width: startNode.width,
               height: startNode.height,
-              padding: startNode.padding,
             },
-            startNode.calcIntersect
-          ).reverse();
+            edge.points[0]
+          );
+
+          if (distance(intersection, edge.points[0]) > epsilon) {
+            edge.points.unshift(intersection);
+          }
         }
         if (endNode.calcIntersect) {
-          edge.points.push({
-            x: endNode.offset.posX + endNode.width / 2,
-            y: endNode.offset.posY + endNode.height / 2,
-          });
-          edge.points = cutPathAtIntersect(
-            edge.points,
+          const intersection = endNode.calcIntersect(
             {
               x: endNode.offset.posX + endNode.width / 2,
               y: endNode.offset.posY + endNode.height / 2,
-              width: ew,
+              width: endNode.width,
               height: endNode.height,
-              padding: endNode.padding,
             },
-            endNode.calcIntersect
+            edge.points[edge.points.length - 1]
           );
+
+          if (distance(intersection, edge.points[edge.points.length - 1]) > epsilon) {
+            edge.points.push(intersection);
+          }
         }
 
         const paths = insertEdge(
@@ -792,7 +722,6 @@ export const render = async (
           endNode,
           data4Layout.diagramId
         );
-        log.info('APA12 edge points after insert', JSON.stringify(edge.points));
 
         edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2;
         edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2;
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts
index 48511a866..07180b090 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts
+++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts
@@ -60,9 +60,23 @@ export async function question(parent: D3Selection
 
   updateNodeBounds(node, polygon);
   node.calcIntersect = function (bounds: Bounds, point: Point) {
-    const w = bbox.width + node.padding;
-    const h = bbox.height + node.padding;
-    const s = w + h;
+    const s = bounds.width;
+
+    // console.log(
+    //   'APA10\nbounds width:',
+    //   bounds.width,
+    //   '\nbounds height:',
+    //   bounds.height,
+    //   'point:',
+    //   point.x,
+    //   point.y,
+    //   '\nw:',
+    //   w,
+    //   '\nh',
+    //   h,
+    //   '\ns',
+    //   s
+    // );
 
     // Define polygon points
     const points = [

From 3dd6107e7640394b90df16e49779dc93c77644d1 Mon Sep 17 00:00:00 2001
From: Anthony Juckel 
Date: Mon, 3 Mar 2025 14:58:17 -0600
Subject: [PATCH 005/314] fix: Allow equals sign in sequenceDiagram labels

---
 .../sequence/parser/sequenceDiagram.jison     |  4 ++--
 .../diagrams/sequence/sequenceDiagram.spec.js | 20 +++++++++++++++++++
 2 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
index 11b39d232..784aff82f 100644
--- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
+++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
@@ -33,7 +33,7 @@
 "actor"                                                   		{ this.begin('ID'); return 'participant_actor'; }
 "create"                                                        return 'create';
 "destroy"                                                       { this.begin('ID'); return 'destroy'; }
-[^\<->\->:\n,;]+?([\-]*[^\<->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$)     { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
+[^<\->\->:\n,;]+?([\-]*[^<\->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$)     { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
 "as"                                                     { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
 (?:)                                                     { this.popState(); this.popState(); return 'NEWLINE'; }
 "loop"                                                          { this.begin('LINE'); return 'loop'; }
@@ -73,7 +73,7 @@ accDescr\s*"{"\s*                                { this.begin("acc_descr_multili
 "off"															return 'off';
 ","                                                             return ',';
 ";"                                                             return 'NEWLINE';
-[^\+\<->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+\<->\->:\n,;]+)*             { yytext = yytext.trim(); return 'ACTOR'; }
+[^+<\->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+<\->\->:\n,;]+)*             { yytext = yytext.trim(); return 'ACTOR'; }
 "->>"                                                           return 'SOLID_ARROW';
 "<<->>"                                                           return 'BIDIRECTIONAL_SOLID_ARROW';
 "-->>"                                                          return 'DOTTED_ARROW';
diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js
index 1fb35bce6..1610bd72a 100644
--- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js
+++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js
@@ -350,6 +350,26 @@ Bob-->Alice-in-Wonderland:I am good thanks!`);
     expect(messages[1].from).toBe('Bob');
   });
 
+  it('should handle equals in participant names', async () => {
+    const diagram = await Diagram.fromText(`
+sequenceDiagram
+participant Alice=Wonderland
+participant Bob
+Alice=Wonderland->Bob:Hello Bob, how are - you?
+Bob-->Alice=Wonderland:I am good thanks!`);
+
+    const actors = diagram.db.getActors();
+    expect([...actors.keys()]).toEqual(['Alice=Wonderland', 'Bob']);
+    expect(actors.get('Alice=Wonderland').description).toBe('Alice=Wonderland');
+    expect(actors.get('Bob').description).toBe('Bob');
+
+    const messages = diagram.db.getMessages();
+
+    expect(messages.length).toBe(2);
+    expect(messages[0].from).toBe('Alice=Wonderland');
+    expect(messages[1].from).toBe('Bob');
+  });
+
   it('should alias participants', async () => {
     const diagram = await Diagram.fromText(`
 sequenceDiagram

From 6cc192680a2531cab28f87a8061a53b786e010f3 Mon Sep 17 00:00:00 2001
From: Anthony Juckel 
Date: Mon, 3 Mar 2025 15:09:17 -0600
Subject: [PATCH 006/314] Added changeset

---
 .changeset/red-zebras-happen.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/red-zebras-happen.md

diff --git a/.changeset/red-zebras-happen.md b/.changeset/red-zebras-happen.md
new file mode 100644
index 000000000..cadf3d0fb
--- /dev/null
+++ b/.changeset/red-zebras-happen.md
@@ -0,0 +1,5 @@
+---
+'mermaid': patch
+---
+
+fix: Allow equals sign in sequenceDiagram labels

From f2eef3759919d4f500b4165724b7c2e698ed3662 Mon Sep 17 00:00:00 2001
From: Knut Sveidqvist 
Date: Fri, 6 Jun 2025 11:20:31 +0200
Subject: [PATCH 007/314] Updated renderinig flow for mindmaps

---
 .gitignore                                    |   1 +
 cypress/platform/knsv2.html                   |  67 +++-
 packages/mermaid-layout-elk/src/render.ts     |  57 +++-
 packages/mermaid/src/config.type.ts           |   4 +
 .../mindmap/mindmapDb.getData.test.ts         | 293 ++++++++++++++++++
 .../mermaid/src/diagrams/mindmap/mindmapDb.ts | 174 +++++++++++
 .../src/diagrams/mindmap/mindmapRenderer.ts   | 226 ++++----------
 .../mermaid/src/diagrams/mindmap/styles.ts    |   3 +
 .../mermaid/src/diagrams/mindmap/test.mmd     |  22 ++
 .../mermaid/src/rendering-util/createGraph.ts | 151 +++++++++
 .../cose-bilkent/cytoscape-setup.test.ts      | 281 +++++++++++++++++
 .../cose-bilkent/cytoscape-setup.ts           | 207 +++++++++++++
 .../layout-algorithms/cose-bilkent/index.ts   |  86 +++++
 .../cose-bilkent/layout.test.ts               | 250 +++++++++++++++
 .../layout-algorithms/cose-bilkent/layout.ts  |  79 +++++
 .../layout-algorithms/cose-bilkent/types.ts   |  43 +++
 packages/mermaid/src/rendering-util/render.ts |   4 +
 .../mermaid/src/schemas/config.schema.yaml    |   5 +
 18 files changed, 1781 insertions(+), 172 deletions(-)
 create mode 100644 packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts
 create mode 100644 packages/mermaid/src/diagrams/mindmap/test.mmd
 create mode 100644 packages/mermaid/src/rendering-util/createGraph.ts
 create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.test.ts
 create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.ts
 create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts
 create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.test.ts
 create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.ts
 create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/types.ts

diff --git a/.gitignore b/.gitignore
index 7448f2a81..7eb55d5cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ node_modules/
 coverage/
 .idea/
 .pnpm-store/
+.instructions/
 
 dist
 v8-compile-cache-0
diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html
index 934d6f44c..8f5aa428a 100644
--- a/cypress/platform/knsv2.html
+++ b/cypress/platform/knsv2.html
@@ -105,20 +105,77 @@
   
 
   
-    
-      flowchart LR
-        AB["apa@apa@"] --> B(("`apa@apa`"))
+    
+      ---
+      config:
+        layout: dagre
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+          Pen and paper
+          Mermaid
+
+
     
+      ---
+      config:
+        layout: dagre
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Europe
+          Asia
+            East
+            West
+        Background
+          Rich
+          Poor
+
+
+
+
+
+    
+
+      ---
+      config:
+        layout: elk
+      ---
+      flowchart LR
+      root{mindmap} --- Origins --- Europe
+      root --- Origins --- Asia
+      root --- Background --- Rich
+      root --- Background --- Poor
+
+
+
+
+    
+
       flowchart
         D(("for D"))
     
-
+    
       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
diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts
index 4d124c04f..3e0194019 100644
--- a/packages/mermaid-layout-elk/src/render.ts
+++ b/packages/mermaid-layout-elk/src/render.ts
@@ -51,14 +51,28 @@ export const render = async (
 
     // Add the element to the DOM
     if (!node.isGroup) {
+      // Create a clean node object for ELK with only the properties it expects
       const child: NodeWithVertex = {
-        ...node,
+        id: node.id,
+        width: node.width,
+        height: node.height,
+        // Store the original node data for later use
+        label: node.label,
+        isGroup: node.isGroup,
+        shape: node.shape,
+        padding: node.padding,
+        cssClasses: node.cssClasses,
+        cssStyles: node.cssStyles,
+        look: node.look,
+        // Include parentId for subgraph processing
+        parentId: node.parentId,
       };
       graph.children.push(child);
       nodeDb[node.id] = child;
 
       const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
       const boundingBox = childNodeEl.node()!.getBBox();
+      // Store the domId separately for rendering, not in the ELK graph
       child.domId = childNodeEl;
       child.width = boundingBox.width;
       child.height = boundingBox.height;
@@ -866,11 +880,16 @@ export const render = async (
       delete node.height;
     }
   });
-  elkGraph.edges.forEach((edge: any) => {
+  log.debug('APA01 processing edges, count:', elkGraph.edges.length);
+  elkGraph.edges.forEach((edge: any, index: number) => {
+    log.debug('APA01 processing edge', index, ':', edge);
     const source = edge.sources[0];
     const target = edge.targets[0];
+    log.debug('APA01 source:', source, 'target:', target);
+    log.debug('APA01 nodeDb[source]:', nodeDb[source]);
+    log.debug('APA01 nodeDb[target]:', nodeDb[target]);
 
-    if (nodeDb[source].parentId !== nodeDb[target].parentId) {
+    if (nodeDb[source] && nodeDb[target] && nodeDb[source].parentId !== nodeDb[target].parentId) {
       const ancestorId = findCommonAncestor(source, target, parentLookupDb);
       // an edge that breaks a subgraph has been identified, set configuration accordingly
       setIncludeChildrenPolicy(source, ancestorId);
@@ -878,7 +897,37 @@ export const render = async (
     }
   });
 
-  const g = await elk.layout(elkGraph);
+  log.debug('APA01 before');
+  log.debug('APA01 elkGraph structure:', JSON.stringify(elkGraph, null, 2));
+  log.debug('APA01 elkGraph.children length:', elkGraph.children?.length);
+  log.debug('APA01 elkGraph.edges length:', elkGraph.edges?.length);
+
+  // Validate that all edge references exist as nodes
+  elkGraph.edges?.forEach((edge: any, index: number) => {
+    log.debug(`APA01 validating edge ${index}:`, edge);
+    if (edge.sources) {
+      edge.sources.forEach((sourceId: any) => {
+        const sourceExists = elkGraph.children?.some((child: any) => child.id === sourceId);
+        log.debug(`APA01 source ${sourceId} exists:`, sourceExists);
+      });
+    }
+    if (edge.targets) {
+      edge.targets.forEach((targetId: any) => {
+        const targetExists = elkGraph.children?.some((child: any) => child.id === targetId);
+        log.debug(`APA01 target ${targetId} exists:`, targetExists);
+      });
+    }
+  });
+
+  let g;
+  try {
+    g = await elk.layout(elkGraph);
+    log.debug('APA01 after - success');
+    log.debug('APA01 layout result:', JSON.stringify(g, null, 2));
+  } catch (error) {
+    log.error('APA01 ELK layout error:', error);
+    throw error;
+  }
 
   // debugger;
   await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts
index 8cd451c16..355e021d5 100644
--- a/packages/mermaid/src/config.type.ts
+++ b/packages/mermaid/src/config.type.ts
@@ -1065,6 +1065,10 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
 export interface MindmapDiagramConfig extends BaseDiagramConfig {
   padding?: number;
   maxNodeWidth?: number;
+  /**
+   * Layout algorithm to use for positioning mindmap nodes
+   */
+  layoutAlgorithm?: string;
 }
 /**
  * The object containing configurations specific for kanban diagrams
diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts b/packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts
new file mode 100644
index 000000000..c1842ff35
--- /dev/null
+++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts
@@ -0,0 +1,293 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import db from './mindmapDb.js';
+import type { MindmapLayoutNode, MindmapLayoutEdge } from './mindmapDb.js';
+
+// Mock the getConfig function
+vi.mock('../../diagram-api/diagramAPI.js', () => ({
+  getConfig: vi.fn(() => ({
+    mindmap: {
+      layoutAlgorithm: 'cose-bilkent',
+      padding: 10,
+      maxNodeWidth: 200,
+      useMaxWidth: true,
+    },
+  })),
+}));
+
+describe('MindmapDb getData function', () => {
+  beforeEach(() => {
+    // Clear the database before each test
+    db.clear();
+  });
+
+  describe('getData', () => {
+    it('should return empty data when no mindmap is set', () => {
+      const result = db.getData();
+
+      expect(result.nodes).toEqual([]);
+      expect(result.edges).toEqual([]);
+      expect(result.config).toBeDefined();
+      expect(result.rootNode).toBeUndefined();
+    });
+
+    it('should return structured data for simple mindmap', () => {
+      // Create a simple mindmap structure
+      db.addNode(0, 'root', 'Root Node', 0);
+      db.addNode(1, 'child1', 'Child 1', 0);
+      db.addNode(1, 'child2', 'Child 2', 0);
+
+      const result = db.getData();
+
+      expect(result.nodes).toHaveLength(3);
+      expect(result.edges).toHaveLength(2);
+      expect(result.config).toBeDefined();
+      expect(result.rootNode).toBeDefined();
+
+      // Check root node
+      const rootNode = result.nodes.find((n: any) => n.id === '0') as MindmapLayoutNode;
+      expect(rootNode).toBeDefined();
+      expect(rootNode?.label).toBe('Root Node');
+      expect(rootNode?.level).toBe(0);
+
+      // Check child nodes
+      const child1 = result.nodes.find((n: any) => n.id === '1') as MindmapLayoutNode;
+      expect(child1).toBeDefined();
+      expect(child1?.label).toBe('Child 1');
+      expect(child1?.level).toBe(1);
+
+      // Check edges
+      expect(result.edges).toContainEqual(
+        expect.objectContaining({
+          start: '0',
+          end: '1',
+          depth: 0,
+        })
+      );
+    });
+
+    it('should return structured data for hierarchical mindmap', () => {
+      // Create a hierarchical mindmap structure
+      db.addNode(0, 'root', 'Root Node', 0);
+      db.addNode(1, 'child1', 'Child 1', 0);
+      db.addNode(2, 'grandchild1', 'Grandchild 1', 0);
+      db.addNode(2, 'grandchild2', 'Grandchild 2', 0);
+      db.addNode(1, 'child2', 'Child 2', 0);
+
+      const result = db.getData();
+
+      expect(result.nodes).toHaveLength(5);
+      expect(result.edges).toHaveLength(4);
+
+      // Check that all levels are represented
+      const levels = result.nodes.map((n) => (n as MindmapLayoutNode).level);
+      expect(levels).toContain(0); // root
+      expect(levels).toContain(1); // children
+      expect(levels).toContain(2); // grandchildren
+
+      // Check edge relationships
+      const edgeRelations = result.edges.map(
+        (e) => `${(e as MindmapLayoutEdge).start}->${(e as MindmapLayoutEdge).end}`
+      );
+      expect(edgeRelations).toContain('0->1'); // root to child1
+      expect(edgeRelations).toContain('1->2'); // child1 to grandchild1
+      expect(edgeRelations).toContain('1->3'); // child1 to grandchild2
+      expect(edgeRelations).toContain('0->4'); // root to child2
+    });
+
+    it('should preserve node properties in processed data', () => {
+      // Add a node with specific properties
+      db.addNode(0, 'root', 'Root Node', 2); // type 2 = rectangle
+
+      // Set additional properties
+      const mindmap = db.getMindmap();
+      if (mindmap) {
+        mindmap.width = 150;
+        mindmap.height = 75;
+        mindmap.padding = 15;
+        mindmap.section = 1;
+        mindmap.class = 'custom-class';
+        mindmap.icon = 'star';
+      }
+
+      const result = db.getData();
+
+      expect(result.nodes).toHaveLength(1);
+      const node = result.nodes[0] as MindmapLayoutNode;
+
+      expect(node.type).toBe(2);
+      expect(node.width).toBe(150);
+      expect(node.height).toBe(75);
+      expect(node.padding).toBe(15);
+      expect(node.section).toBeUndefined(); // Root node has undefined section
+      expect(node.cssClasses).toBe('mindmap-node section-root section--1 custom-class');
+      expect(node.icon).toBe('star');
+    });
+
+    it('should generate unique edge IDs', () => {
+      db.addNode(0, 'root', 'Root Node', 0);
+      db.addNode(1, 'child1', 'Child 1', 0);
+      db.addNode(1, 'child2', 'Child 2', 0);
+      db.addNode(1, 'child3', 'Child 3', 0);
+
+      const result = db.getData();
+
+      const edgeIds = result.edges.map((e: any) => e.id);
+      const uniqueIds = new Set(edgeIds);
+
+      expect(edgeIds).toHaveLength(3);
+      expect(uniqueIds.size).toBe(3); // All IDs should be unique
+    });
+
+    it('should handle nodes with missing optional properties', () => {
+      db.addNode(0, 'root', 'Root Node', 0);
+
+      const result = db.getData();
+      const node = result.nodes[0] as MindmapLayoutNode;
+
+      // Should handle undefined/missing properties gracefully
+      expect(node.section).toBeUndefined(); // Root node has undefined section
+      expect(node.cssClasses).toBe('mindmap-node section-root section--1'); // Root node gets special classes
+      expect(node.icon).toBeUndefined();
+      expect(node.x).toBeUndefined();
+      expect(node.y).toBeUndefined();
+    });
+
+    it('should assign correct section classes based on sibling position', () => {
+      // Create the example mindmap structure:
+      // A
+      //   a0
+      //     aa0
+      //   a1
+      //     aaa
+      //   a2
+      db.addNode(0, 'A', 'A', 0); // Root
+      db.addNode(1, 'a0', 'a0', 0); // First child of root
+      db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
+      db.addNode(1, 'a1', 'a1', 0); // Second child of root
+      db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
+      db.addNode(1, 'a2', 'a2', 0); // Third child of root
+
+      const result = db.getData();
+
+      // Find nodes by their labels
+      const nodeA = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
+      const nodeA0 = result.nodes.find((n) => n.label === 'a0') as MindmapLayoutNode;
+      const nodeAa0 = result.nodes.find((n) => n.label === 'aa0') as MindmapLayoutNode;
+      const nodeA1 = result.nodes.find((n) => n.label === 'a1') as MindmapLayoutNode;
+      const nodeAaa = result.nodes.find((n) => n.label === 'aaa') as MindmapLayoutNode;
+      const nodeA2 = result.nodes.find((n) => n.label === 'a2') as MindmapLayoutNode;
+
+      // Check section assignments
+      expect(nodeA.section).toBeUndefined(); // Root has undefined section
+      expect(nodeA0.section).toBe(0); // First child of root
+      expect(nodeAa0.section).toBe(0); // Inherits from parent a0
+      expect(nodeA1.section).toBe(1); // Second child of root
+      expect(nodeAaa.section).toBe(1); // Inherits from parent a1
+      expect(nodeA2.section).toBe(2); // Third child of root
+
+      // Check CSS classes
+      expect(nodeA.cssClasses).toBe('mindmap-node section-root section--1');
+      expect(nodeA0.cssClasses).toBe('mindmap-node section-0');
+      expect(nodeAa0.cssClasses).toBe('mindmap-node section-0');
+      expect(nodeA1.cssClasses).toBe('mindmap-node section-1');
+      expect(nodeAaa.cssClasses).toBe('mindmap-node section-1');
+      expect(nodeA2.cssClasses).toBe('mindmap-node section-2');
+    });
+
+    it('should preserve custom classes while adding section classes', () => {
+      db.addNode(0, 'root', 'Root Node', 0);
+      db.addNode(1, 'child', 'Child Node', 0);
+
+      // Add custom classes to nodes
+      const mindmap = db.getMindmap();
+      if (mindmap) {
+        mindmap.class = 'custom-root-class';
+        if (mindmap.children?.[0]) {
+          mindmap.children[0].class = 'custom-child-class';
+        }
+      }
+
+      const result = db.getData();
+      const rootNode = result.nodes.find((n) => n.label === 'Root Node') as MindmapLayoutNode;
+      const childNode = result.nodes.find((n) => n.label === 'Child Node') as MindmapLayoutNode;
+
+      // Should include both section classes and custom classes
+      expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1 custom-root-class');
+      expect(childNode.cssClasses).toBe('mindmap-node section-0 custom-child-class');
+    });
+
+    it('should not create any fake root nodes', () => {
+      // Create a simple mindmap
+      db.addNode(0, 'A', 'A', 0);
+      db.addNode(1, 'a0', 'a0', 0);
+      db.addNode(1, 'a1', 'a1', 0);
+
+      const result = db.getData();
+
+      // Check that we only have the expected nodes
+      expect(result.nodes).toHaveLength(3);
+      expect(result.nodes.map((n) => n.label)).toEqual(['A', 'a0', 'a1']);
+
+      // Check that there's no node with label "mindmap" or any other fake root
+      const mindmapNode = result.nodes.find((n) => n.label === 'mindmap');
+      expect(mindmapNode).toBeUndefined();
+
+      // Verify the root node has the correct classes
+      const rootNode = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
+      expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1');
+      expect(rootNode.level).toBe(0);
+    });
+
+    it('should assign correct section classes to edges', () => {
+      // Create the example mindmap structure:
+      // A
+      //   a0
+      //     aa0
+      //   a1
+      //     aaa
+      //   a2
+      db.addNode(0, 'A', 'A', 0); // Root
+      db.addNode(1, 'a0', 'a0', 0); // First child of root
+      db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
+      db.addNode(1, 'a1', 'a1', 0); // Second child of root
+      db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
+      db.addNode(1, 'a2', 'a2', 0); // Third child of root
+
+      const result = db.getData();
+
+      // Should have 5 edges: A->a0, a0->aa0, A->a1, a1->aaa, A->a2
+      expect(result.edges).toHaveLength(5);
+
+      // Find edges by their start and end nodes
+      const edgeA_a0 = result.edges.find(
+        (e) => e.start === '0' && e.end === '1'
+      ) as MindmapLayoutEdge;
+      const edgeA0_aa0 = result.edges.find(
+        (e) => e.start === '1' && e.end === '2'
+      ) as MindmapLayoutEdge;
+      const edgeA_a1 = result.edges.find(
+        (e) => e.start === '0' && e.end === '3'
+      ) as MindmapLayoutEdge;
+      const edgeA1_aaa = result.edges.find(
+        (e) => e.start === '3' && e.end === '4'
+      ) as MindmapLayoutEdge;
+      const edgeA_a2 = result.edges.find(
+        (e) => e.start === '0' && e.end === '5'
+      ) as MindmapLayoutEdge;
+
+      // Check edge classes
+      expect(edgeA_a0.classes).toBe('edge section-edge-0 edge-depth-1'); // A->a0: section-0, depth-1
+      expect(edgeA0_aa0.classes).toBe('edge section-edge-0 edge-depth-2'); // a0->aa0: section-0, depth-2
+      expect(edgeA_a1.classes).toBe('edge section-edge-1 edge-depth-1'); // A->a1: section-1, depth-1
+      expect(edgeA1_aaa.classes).toBe('edge section-edge-1 edge-depth-2'); // a1->aaa: section-1, depth-2
+      expect(edgeA_a2.classes).toBe('edge section-edge-2 edge-depth-1'); // A->a2: section-2, depth-1
+
+      // Check section assignments match the child nodes
+      expect(edgeA_a0.section).toBe(0);
+      expect(edgeA0_aa0.section).toBe(0);
+      expect(edgeA_a1.section).toBe(1);
+      expect(edgeA1_aaa.section).toBe(1);
+      expect(edgeA_a2.section).toBe(2);
+    });
+  });
+});
diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
index e7041e9d6..f401881d4 100644
--- a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
+++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
@@ -4,6 +4,21 @@ import { sanitizeText } from '../../diagrams/common/common.js';
 import { log } from '../../logger.js';
 import type { MindmapNode } from './mindmapTypes.js';
 import defaultConfig from '../../defaultConfig.js';
+import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
+
+// Extend Node type for mindmap-specific properties
+export type MindmapLayoutNode = Node & {
+  level: number;
+  nodeId: string;
+  type: number;
+  section?: number;
+};
+
+// Extend Edge type for mindmap-specific properties
+export type MindmapLayoutEdge = Edge & {
+  depth: number;
+  section?: number;
+};
 
 let nodes: MindmapNode[] = [];
 let cnt = 0;
@@ -139,6 +154,164 @@ const type2Str = (type: number) => {
   }
 };
 
+/**
+ * Assign section numbers to nodes based on their position relative to root
+ * @param node - The mindmap node to process
+ * @param sectionNumber - The section number to assign (undefined for root)
+ */
+const assignSections = (node: MindmapNode, sectionNumber?: number): void => {
+  // Assign section number to the current node
+  node.section = sectionNumber;
+
+  // For root node's children, assign section numbers based on their index
+  // For other nodes, inherit parent's section number
+  if (node.children) {
+    node.children.forEach((child, index) => {
+      const childSectionNumber = node.level === 0 ? index : sectionNumber;
+      assignSections(child, childSectionNumber);
+    });
+  }
+};
+
+/**
+ * Convert mindmap tree structure to flat array of nodes
+ * @param node - The mindmap node to process
+ * @param processedNodes - Array to collect processed nodes
+ */
+const flattenNodes = (node: MindmapNode, processedNodes: MindmapLayoutNode[]): void => {
+  // Build CSS classes for the node
+  let cssClasses = 'mindmap-node';
+
+  // Add section-specific classes
+  if (node.level === 0) {
+    // Root node gets special classes
+    cssClasses += ' section-root section--1';
+  } else if (node.section !== undefined) {
+    // Child nodes get section class based on their section number
+    cssClasses += ` section-${node.section}`;
+  }
+
+  // Add any custom classes from the node
+  if (node.class) {
+    cssClasses += ` ${node.class}`;
+  }
+
+  const processedNode: MindmapLayoutNode = {
+    id: 'node_' + node.id.toString(),
+    domId: 'node_' + node.id.toString(),
+    label: node.descr,
+    isGroup: false,
+    shape: 'rect', // Default shape, can be customized based on node.type
+    width: node.width,
+    height: node.height ?? 0,
+    padding: node.padding,
+    cssClasses: cssClasses,
+    cssStyles: [],
+    look: 'default',
+    icon: node.icon,
+    x: node.x,
+    y: node.y,
+    // Mindmap-specific properties
+    level: node.level,
+    nodeId: node.nodeId,
+    type: node.type,
+    section: node.section,
+  };
+
+  processedNodes.push(processedNode);
+
+  // Recursively process children
+  if (node.children) {
+    node.children.forEach((child) => flattenNodes(child, processedNodes));
+  }
+};
+
+/**
+ * Generate edges from parent-child relationships in mindmap tree
+ * @param node - The mindmap node to process
+ * @param edges - Array to collect edges
+ */
+const generateEdges = (node: MindmapNode, edges: MindmapLayoutEdge[]): void => {
+  if (node.children) {
+    node.children.forEach((child) => {
+      // Build CSS classes for the edge
+      let edgeClasses = 'edge';
+
+      // Add section-specific classes based on the child's section
+      if (child.section !== undefined) {
+        edgeClasses += ` section-edge-${child.section}`;
+      }
+
+      // Add depth class based on the parent's level + 1 (depth of the edge)
+      const edgeDepth = node.level + 1;
+      edgeClasses += ` edge-depth-${edgeDepth}`;
+
+      const edge: MindmapLayoutEdge = {
+        id: `edge_${node.id}_${child.id}`,
+        start: 'node_' + node.id.toString(),
+        end: 'node_' + child.id.toString(),
+        type: 'normal',
+        curve: 'basis',
+        thickness: 'normal',
+        look: 'default',
+        classes: edgeClasses,
+        // Store mindmap-specific data
+        depth: node.level,
+        section: child.section,
+      };
+
+      edges.push(edge);
+
+      // Recursively process child edges
+      generateEdges(child, edges);
+    });
+  }
+};
+
+/**
+ * Get structured data for layout algorithms
+ * Following the pattern established by ER diagrams
+ * @returns Structured data containing nodes, edges, and config
+ */
+const getData = (): LayoutData => {
+  const mindmapRoot = getMindmap();
+  const config = getConfig();
+
+  if (!mindmapRoot) {
+    return {
+      nodes: [],
+      edges: [],
+      config,
+    };
+  }
+  log.debug('getData: mindmapRoot', mindmapRoot, config);
+
+  // Assign section numbers to all nodes based on their position relative to root
+  assignSections(mindmapRoot);
+
+  // Convert tree structure to flat arrays
+  const processedNodes: MindmapLayoutNode[] = [];
+  const processedEdges: MindmapLayoutEdge[] = [];
+
+  flattenNodes(mindmapRoot, processedNodes);
+  generateEdges(mindmapRoot, processedEdges);
+
+  log.debug(`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`);
+
+  return {
+    nodes: processedNodes,
+    edges: processedEdges,
+    config,
+    // Store the root node for mindmap-specific layout algorithms
+    rootNode: mindmapRoot,
+    // Properties required by dagre layout algorithm
+    markers: [], // Mindmaps don't use markers
+    direction: 'TB', // Top-to-bottom direction for mindmaps
+    nodeSpacing: 50, // Default spacing between nodes
+    rankSpacing: 50, // Default spacing between ranks
+  };
+};
+
 // Expose logger to grammar
 const getLogger = () => log;
 const getElementById = (id: number) => elements[id];
@@ -154,6 +327,7 @@ const db = {
   type2Str,
   getLogger,
   getElementById,
+  getData,
 } as const;
 
 export default db;
diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts b/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts
index 708b3cc28..d273fa49e 100644
--- a/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts
+++ b/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts
@@ -1,200 +1,100 @@
-import cytoscape from 'cytoscape';
-// @ts-expect-error No types available
-import coseBilkent from 'cytoscape-cose-bilkent';
-import { select } from 'd3';
-import type { MermaidConfig } from '../../config.type.js';
 import { getConfig } from '../../diagram-api/diagramAPI.js';
 import type { DrawDefinition } from '../../diagram-api/types.js';
 import { log } from '../../logger.js';
-import type { D3Element } from '../../types.js';
-import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
-import { setupGraphViewbox } from '../../setupGraphViewbox.js';
-import type { FilledMindMapNode, MindmapDB, MindmapNode } from './mindmapTypes.js';
-import { drawNode, positionNode } from './svgDraw.js';
+import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
+import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
+import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
+import type { LayoutData } from '../../rendering-util/types.js';
+import type { FilledMindMapNode, MindmapDB } from './mindmapTypes.js';
+import { drawNode } from './svgDraw.js';
 import defaultConfig from '../../defaultConfig.js';
 
-// Inject the layout algorithm into cytoscape
-cytoscape.use(coseBilkent);
-
-async function drawNodes(
+async function _drawNodes(
   db: MindmapDB,
-  svg: D3Element,
+  svg: any,
   mindmap: FilledMindMapNode,
   section: number,
-  conf: MermaidConfig
+  conf: any
 ) {
   await drawNode(db, svg, mindmap, section, conf);
   if (mindmap.children) {
     await Promise.all(
       mindmap.children.map((child, index) =>
-        drawNodes(db, svg, child, section < 0 ? index : section, conf)
+        _drawNodes(db, svg, child, section < 0 ? index : section, conf)
       )
     );
   }
 }
 
-declare module 'cytoscape' {
-  interface EdgeSingular {
-    _private: {
-      bodyBounds: unknown;
-      rscratch: {
-        startX: number;
-        startY: number;
-        midX: number;
-        midY: number;
-        endX: number;
-        endY: number;
-      };
-    };
-  }
-}
-
-function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
-  cy.edges().map((edge, id) => {
-    const data = edge.data();
-    if (edge[0]._private.bodyBounds) {
-      const bounds = edge[0]._private.rscratch;
-      log.trace('Edge: ', id, data);
-      edgesEl
-        .insert('path')
-        .attr(
-          'd',
-          `M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
-        )
-        .attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
+/**
+ * Update the layout data with actual node dimensions after drawing
+ */
+function _updateNodeDimensions(data4Layout: LayoutData, mindmapRoot: FilledMindMapNode) {
+  const updateNode = (node: FilledMindMapNode) => {
+    // Find the corresponding node in the layout data
+    const layoutNode = data4Layout.nodes.find((n) => n.id === node.id.toString());
+    if (layoutNode) {
+      // Update with the actual dimensions calculated by drawNode
+      layoutNode.width = node.width;
+      layoutNode.height = node.height;
+      log.debug('Updated node dimensions:', node.id, 'width:', node.width, 'height:', node.height);
     }
-  });
-}
 
-function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
-  cy.add({
-    group: 'nodes',
-    data: {
-      id: mindmap.id.toString(),
-      labelText: mindmap.descr,
-      height: mindmap.height,
-      width: mindmap.width,
-      level: level,
-      nodeId: mindmap.id,
-      padding: mindmap.padding,
-      type: mindmap.type,
-    },
-    position: {
-      x: mindmap.x!,
-      y: mindmap.y!,
-    },
-  });
-  if (mindmap.children) {
-    mindmap.children.forEach((child) => {
-      addNodes(child, cy, conf, level + 1);
-      cy.add({
-        group: 'edges',
-        data: {
-          id: `${mindmap.id}_${child.id}`,
-          source: mindmap.id,
-          target: child.id,
-          depth: level,
-          section: child.section,
-        },
-      });
-    });
-  }
-}
+    // Recursively update children
+    if (node.children) {
+      node.children.forEach(updateNode);
+    }
+  };
 
-function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise {
-  return new Promise((resolve) => {
-    // Add temporary render element
-    const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
-    const cy = cytoscape({
-      container: document.getElementById('cy'), // container to render in
-      style: [
-        {
-          selector: 'edge',
-          style: {
-            'curve-style': 'bezier',
-          },
-        },
-      ],
-    });
-    // Remove element after layout
-    renderEl.remove();
-    addNodes(node, cy, conf, 0);
-
-    // Make cytoscape care about the dimensions of the nodes
-    cy.nodes().forEach(function (n) {
-      n.layoutDimensions = () => {
-        const data = n.data();
-        return { w: data.width, h: data.height };
-      };
-    });
-
-    cy.layout({
-      name: 'cose-bilkent',
-      // @ts-ignore Types for cose-bilkent are not correct?
-      quality: 'proof',
-      styleEnabled: false,
-      animate: false,
-    }).run();
-    cy.ready((e) => {
-      log.info('Ready', e);
-      resolve(cy);
-    });
-  });
-}
-
-function positionNodes(db: MindmapDB, cy: cytoscape.Core) {
-  cy.nodes().map((node, id) => {
-    const data = node.data();
-    data.x = node.position().x;
-    data.y = node.position().y;
-    positionNode(db, data);
-    const el = db.getElementById(data.nodeId);
-    log.info('id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
-    el.attr(
-      'transform',
-      `translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
-    );
-    el.attr('attr', `apa-${id})`);
-  });
+  updateNode(mindmapRoot);
 }
 
 export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
   log.debug('Rendering mindmap diagram\n' + text);
+  const { securityLevel, mindmap: conf, layout } = getConfig();
 
+  // Draw the nodes first to get their dimensions, then update the layout data
   const db = diagObj.db as MindmapDB;
+
+  // The getData method provided in all supported diagrams is used to extract the data from the parsed structure
+  // into the Layout data format
+  const data4Layout = db.getData();
+
+  // Create the root SVG - the element is the div containing the SVG element
+  const svg = getDiagramElement(id, securityLevel);
+
+  data4Layout.type = diagObj.type;
+  data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout, {
+    fallback: 'cose-bilkent',
+  });
+  // For mindmap diagrams, prioritize mindmap-specific layout algorithm configuration
+  const preferredLayout = conf?.layoutAlgorithm ?? layout ?? 'cose-bilkent';
+  log.debug('Mindmap renderer - preferredLayout:', preferredLayout);
+  log.debug('Mindmap renderer - conf?.layoutAlgorithm:', conf?.layoutAlgorithm);
+  log.debug('Mindmap renderer - layout:', layout);
+
+  log.debug('Mindmap renderer - selected layoutAlgorithm:', data4Layout.layoutAlgorithm);
+  log.debug('APA01 Mindmap renderer - data4Layout.rootNode exists:', !!data4Layout.rootNode);
+
+  data4Layout.diagramId = id;
+
+  // Ensure required properties are set for compatibility with different layout algorithms
+  data4Layout.markers = ['point'];
+  data4Layout.direction = 'TB';
+
   const mm = db.getMindmap();
   if (!mm) {
     return;
   }
 
-  const conf = getConfig();
-  conf.htmlLabels = false;
-
-  const svg = selectSvgElement(id);
-
-  // Draw the graph and start with drawing the nodes without proper position
-  // this gives us the size of the nodes and we can set the positions later
-
-  const edgesElem = svg.append('g');
-  edgesElem.attr('class', 'mindmap-edges');
-  const nodesElem = svg.append('g');
-  nodesElem.attr('class', 'mindmap-nodes');
-  await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
-
-  // Next step is to layout the mindmap, giving each node a position
-
-  const cy = await layoutMindmap(mm, conf);
-
-  // After this we can draw, first the edges and the then nodes with the correct position
-  drawEdges(edgesElem, cy);
-  positionNodes(db, cy);
-
+  // Use the unified rendering system
+  await render(data4Layout, svg);
   // Setup the view box and size of the svg element
-  setupGraphViewbox(
-    undefined,
+  setupViewPortForSVG(
     svg,
-    conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
-    conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
+    conf?.padding ?? defaultConfig.mindmap.padding,
+    'mindmapDiagram',
+    conf?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
   );
 };
 
diff --git a/packages/mermaid/src/diagrams/mindmap/styles.ts b/packages/mermaid/src/diagrams/mindmap/styles.ts
index fffa6e4d9..8731a4ffc 100644
--- a/packages/mermaid/src/diagrams/mindmap/styles.ts
+++ b/packages/mermaid/src/diagrams/mindmap/styles.ts
@@ -64,6 +64,9 @@ const getStyles: DiagramStylesProvider = (options) =>
   .section-root text {
     fill: ${options.gitBranchLabel0};
   }
+  .section-root span {
+    color: ${options.gitBranchLabel0};
+  }
   .icon-container {
     height:100%;
     display: flex;
diff --git a/packages/mermaid/src/diagrams/mindmap/test.mmd b/packages/mermaid/src/diagrams/mindmap/test.mmd
new file mode 100644
index 000000000..f9acaa4e7
--- /dev/null
+++ b/packages/mermaid/src/diagrams/mindmap/test.mmd
@@ -0,0 +1,22 @@
+---
+config:
+  theme: redux-color
+---
+mindmap
+  root((mindmap))
+    Origins
+      Long history
+      ::icon(fa fa-book)
+      Popularization
+        British popular psychology author Tony Buzan
+    Research
+      On effectiveness
and features + On Automatic creation + Ammmmmmmmmmmmmmmmmmmmmmmm + Uses + Creative techniques + Strategic planning + Argument mapping + Tools + Pen and paper + Mermaid diff --git a/packages/mermaid/src/rendering-util/createGraph.ts b/packages/mermaid/src/rendering-util/createGraph.ts new file mode 100644 index 000000000..02ac6053b --- /dev/null +++ b/packages/mermaid/src/rendering-util/createGraph.ts @@ -0,0 +1,151 @@ +import { insertNode } from './rendering-elements/nodes.js'; +import type { LayoutData } from './types.ts'; +import type { Selection } from 'd3'; +import { getConfig } from '../diagram-api/diagramAPI.js'; +import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; + +// Update type: +type D3Selection = Selection< + T, + unknown, + Element | null, + unknown +>; + +/** + * Creates a graph by merging the graph construction and DOM element insertion. + * + * This function creates the graph, inserts the SVG groups (clusters, edgePaths, edgeLabels, nodes) + * into the provided element, and uses `insertNode` to add nodes to the diagram. Node dimensions + * are computed using each node's bounding box. + * + * @param element - The D3 selection in which the SVG groups are inserted. + * @param data4Layout - The layout data containing nodes and edges. + * @returns A promise resolving to an object containing the Graphology graph and the inserted groups. + */ +export async function createGraphWithElements( + element: D3Selection, + data4Layout: LayoutData +): Promise<{ + graph: graphlib.Graph; + groups: { + clusters: D3Selection; + edgePaths: D3Selection; + edgeLabels: D3Selection; + nodes: D3Selection; + rootGroups: D3Selection; + }; + nodeElements: Map>; +}> { + // Create a directed, multi graph. + const graph = new graphlib.Graph({ + multigraph: true, + compound: true, + }); + const edgesToProcess = [...data4Layout.edges]; + const config = getConfig(); + // Create groups for clusters, edge paths, edge labels, and nodes. + const clusters = element.insert('g').attr('class', 'clusters'); + const edgePaths = element.insert('g').attr('class', 'edges edgePath'); + const edgeLabels = element.insert('g').attr('class', 'edgeLabels'); + const nodesGroup = element.insert('g').attr('class', 'nodes'); + const rootGroups = element.insert('g').attr('class', 'root'); + + const nodeElements = new Map>(); + + // Insert nodes into the DOM and add them to the graph. + await Promise.all( + data4Layout.nodes.map(async (node) => { + if (node.isGroup) { + graph.setNode(node.id, { ...node }); + } else { + const childNodeEl = await insertNode(nodesGroup, node, { config, dir: node.dir }); + const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 }; + nodeElements.set(node.id, childNodeEl as D3Selection); + node.width = boundingBox.width; + node.height = boundingBox.height; + graph.setNode(node.id, { ...node }); + if (node.parentId) { + // Optionally store the parent relationship (Graphology doesn't have a native parent-child concept) + // e.g., you could update the node attributes or handle it as needed. + } + } + }) + ); + // Add edges to the graph. + for (const edge of edgesToProcess) { + if (edge.label && edge.label?.length > 0) { + // Create a label node for the edge + const labelNodeId = `edge-label-${edge.start}-${edge.end}-${edge.id}`; + const labelNode = { + id: labelNodeId, + label: edge.label, + edgeStart: edge.start, + edgeEnd: edge.end, + shape: 'labelRect', + width: 0, // Will be updated after insertion + height: 0, // Will be updated after insertion + isEdgeLabel: true, + isDummy: true, + parentId: edge.parentId, + ...(edge.dir ? { dir: edge.dir } : {}), + }; + + // Insert the label node into the DOM + const labelNodeEl = await insertNode(nodesGroup, labelNode, { config, dir: edge.dir }); + const boundingBox = labelNodeEl.node()?.getBBox() ?? { width: 0, height: 0 }; + + // Update node dimensions + labelNode.width = boundingBox.width; + labelNode.height = boundingBox.height; + + // Add to graph and tracking maps + graph.setNode(labelNodeId, { ...labelNode }); + nodeElements.set(labelNodeId, labelNodeEl as D3Selection); + data4Layout.nodes.push(labelNode); + + // Create two edges to replace the original one + const edgeToLabel = { + ...edge, + id: `${edge.id}-to-label`, + end: labelNodeId, + label: undefined, + isLabelEdge: true, + arrowTypeEnd: 'none', + arrowTypeStart: 'none', + }; + const edgeFromLabel = { + ...edge, + id: `${edge.id}-from-label`, + start: labelNodeId, + end: edge.end, + label: undefined, + isLabelEdge: true, + arrowTypeStart: 'none', + arrowTypeEnd: 'arrow_point', + }; + graph.setEdge(edgeToLabel.id, edgeToLabel.start, edgeToLabel.end, { ...edgeToLabel }); + graph.setEdge(edgeFromLabel.id, edgeFromLabel.start, edgeFromLabel.end, { ...edgeFromLabel }); + data4Layout.edges.push(edgeToLabel, edgeFromLabel); + const edgeIdToRemove = edge.id; + data4Layout.edges = data4Layout.edges.filter((edge) => edge.id !== edgeIdToRemove); + const indexInOriginal = data4Layout.edges.findIndex((e) => e.id === edge.id); + if (indexInOriginal !== -1) { + data4Layout.edges.splice(indexInOriginal, 1); + } + } else { + // Regular edge without label + graph.setEdge(edge.id, edge.start, edge.end, { ...edge }); + const edgeExists = data4Layout.edges.some((existingEdge) => existingEdge.id === edge.id); + if (!edgeExists) { + data4Layout.edges.push(edge); + } + } + } + + return { + graph, + groups: { clusters, edgePaths, edgeLabels, nodes: nodesGroup, rootGroups }, + nodeElements, + }; +} diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.test.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.test.ts new file mode 100644 index 000000000..2592b57db --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.test.ts @@ -0,0 +1,281 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + addNodes, + addEdges, + extractPositionedNodes, + extractPositionedEdges, +} from './cytoscape-setup.js'; +import type { Node, Edge } from '../../types.js'; + +// Mock cytoscape +const mockCy = { + add: vi.fn(), + nodes: vi.fn(), + edges: vi.fn(), +}; + +vi.mock('cytoscape', () => { + const mockCytoscape = vi.fn(() => mockCy) as any; + mockCytoscape.use = vi.fn(); + return { + default: mockCytoscape, + }; +}); + +vi.mock('cytoscape-cose-bilkent', () => ({ + default: vi.fn(), +})); + +vi.mock('d3', () => ({ + select: vi.fn(() => ({ + append: vi.fn(() => ({ + attr: vi.fn(() => ({ + attr: vi.fn(() => ({ + remove: vi.fn(), + })), + })), + })), + })), +})); + +describe('Cytoscape Setup', () => { + let mockNodes: Node[]; + let mockEdges: Edge[]; + + beforeEach(() => { + vi.clearAllMocks(); + + mockNodes = [ + { + id: '1', + label: 'Root', + isGroup: false, + shape: 'rect', + width: 100, + height: 50, + padding: 10, + x: 100, + y: 100, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + { + id: '2', + label: 'Child 1', + isGroup: false, + shape: 'rect', + width: 80, + height: 40, + padding: 10, + x: 150, + y: 150, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + ]; + + mockEdges = [ + { + id: '1_2', + start: '1', + end: '2', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + ]; + }); + + describe('addNodes', () => { + it('should add nodes to cytoscape', () => { + addNodes([mockNodes[0]], mockCy as unknown as any); + + expect(mockCy.add).toHaveBeenCalledWith({ + group: 'nodes', + data: { + id: '1', + labelText: 'Root', + height: 50, + width: 100, + padding: 10, + isGroup: false, + shape: 'rect', + cssClasses: '', + cssStyles: [], + look: 'default', + }, + position: { + x: 100, + y: 100, + }, + }); + }); + + it('should add multiple nodes to cytoscape', () => { + addNodes(mockNodes, mockCy as unknown as any); + + expect(mockCy.add).toHaveBeenCalledTimes(2); + + expect(mockCy.add).toHaveBeenCalledWith({ + group: 'nodes', + data: { + id: '1', + labelText: 'Root', + height: 50, + width: 100, + padding: 10, + isGroup: false, + shape: 'rect', + cssClasses: '', + cssStyles: [], + look: 'default', + }, + position: { + x: 100, + y: 100, + }, + }); + + expect(mockCy.add).toHaveBeenCalledWith({ + group: 'nodes', + data: { + id: '2', + labelText: 'Child 1', + height: 40, + width: 80, + padding: 10, + isGroup: false, + shape: 'rect', + cssClasses: '', + cssStyles: [], + look: 'default', + }, + position: { + x: 150, + y: 150, + }, + }); + }); + }); + + describe('addEdges', () => { + it('should add edges to cytoscape', () => { + addEdges(mockEdges, mockCy as unknown as any); + + expect(mockCy.add).toHaveBeenCalledWith({ + group: 'edges', + data: { + id: '1_2', + source: '1', + target: '2', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + }); + }); + }); + + describe('extractPositionedNodes', () => { + it('should extract positioned nodes from cytoscape', () => { + const mockCytoscapeNodes = [ + { + data: () => ({ + id: '1', + labelText: 'Root', + width: 100, + height: 50, + padding: 10, + isGroup: false, + shape: 'rect', + }), + position: () => ({ x: 100, y: 100 }), + }, + { + data: () => ({ + id: '2', + labelText: 'Child 1', + width: 80, + height: 40, + padding: 10, + isGroup: false, + shape: 'rect', + }), + position: () => ({ x: 150, y: 150 }), + }, + ]; + + mockCy.nodes.mockReturnValue({ + map: (fn: unknown) => mockCytoscapeNodes.map(fn as any), + }); + + const result = extractPositionedNodes(mockCy as unknown as any); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + id: '1', + x: 100, + y: 100, + labelText: 'Root', + width: 100, + height: 50, + padding: 10, + isGroup: false, + shape: 'rect', + }); + }); + }); + + describe('extractPositionedEdges', () => { + it('should extract positioned edges from cytoscape', () => { + const mockCytoscapeEdges = [ + { + data: () => ({ + id: '1_2', + source: '1', + target: '2', + type: 'edge', + }), + _private: { + rscratch: { + startX: 100, + startY: 100, + midX: 125, + midY: 125, + endX: 150, + endY: 150, + }, + }, + }, + ]; + + mockCy.edges.mockReturnValue({ + map: (fn: unknown) => mockCytoscapeEdges.map(fn as any), + }); + + const result = extractPositionedEdges(mockCy as unknown as any); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + id: '1_2', + source: '1', + target: '2', + type: 'edge', + startX: 100, + startY: 100, + midX: 125, + midY: 125, + endX: 150, + endY: 150, + }); + }); + }); +}); diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.ts new file mode 100644 index 000000000..8fb9b2599 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.ts @@ -0,0 +1,207 @@ +import cytoscape from 'cytoscape'; +import coseBilkent from 'cytoscape-cose-bilkent'; +import { select } from 'd3'; +import { log } from '../../../logger.js'; +import type { LayoutData, Node, Edge } from '../../types.js'; +import type { CytoscapeLayoutConfig, PositionedNode, PositionedEdge } from './types.js'; + +// Inject the layout algorithm into cytoscape +cytoscape.use(coseBilkent); + +/** + * Declare module augmentation for cytoscape edge types + */ +declare module 'cytoscape' { + interface EdgeSingular { + _private: { + bodyBounds: unknown; + rscratch: { + startX: number; + startY: number; + midX: number; + midY: number; + endX: number; + endY: number; + }; + }; + } +} + +/** + * Add nodes to cytoscape instance from provided node array + * This function processes only the nodes provided in the data structure + * @param nodes - Array of nodes to add + * @param cy - The cytoscape instance + */ +export function addNodes(nodes: Node[], cy: cytoscape.Core): void { + nodes.forEach((node) => { + const nodeData: Record = { + id: node.id, + labelText: node.label, + height: node.height, + width: node.width, + padding: node.padding ?? 0, + }; + + // Add any additional properties from the node + Object.keys(node).forEach((key) => { + if (!['id', 'label', 'height', 'width', 'padding', 'x', 'y'].includes(key)) { + nodeData[key] = (node as unknown as Record)[key]; + } + }); + + cy.add({ + group: 'nodes', + data: nodeData, + position: { + x: node.x ?? 0, + y: node.y ?? 0, + }, + }); + }); +} + +/** + * Add edges to cytoscape instance from provided edge array + * This function processes only the edges provided in the data structure + * @param edges - Array of edges to add + * @param cy - The cytoscape instance + */ +export function addEdges(edges: Edge[], cy: cytoscape.Core): void { + edges.forEach((edge) => { + const edgeData: Record = { + id: edge.id, + source: edge.start, + target: edge.end, + }; + + // Add any additional properties from the edge + Object.keys(edge).forEach((key) => { + if (!['id', 'start', 'end'].includes(key)) { + edgeData[key] = (edge as unknown as Record)[key]; + } + }); + + cy.add({ + group: 'edges', + data: edgeData, + }); + }); +} + +/** + * Create and configure cytoscape instance + * @param data - Layout data containing nodes and edges + * @returns Promise resolving to configured cytoscape instance + */ +export function createCytoscapeInstance(data: LayoutData): Promise { + return new Promise((resolve) => { + // Add temporary render element + const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none'); + + const cy = cytoscape({ + container: document.getElementById('cy'), // container to render in + style: [ + { + selector: 'edge', + style: { + 'curve-style': 'bezier', + }, + }, + ], + }); + + // Remove element after layout + renderEl.remove(); + + // Add all nodes and edges to cytoscape using the generic functions + addNodes(data.nodes, cy); + addEdges(data.edges, cy); + + // Make cytoscape care about the dimensions of the nodes + cy.nodes().forEach(function (n) { + n.layoutDimensions = () => { + const nodeData = n.data(); + return { w: nodeData.width, h: nodeData.height }; + }; + }); + + // Configure and run the cose-bilkent layout + const layoutConfig: CytoscapeLayoutConfig = { + name: 'cose-bilkent', + // @ts-ignore Types for cose-bilkent are not correct? + quality: 'proof', + styleEnabled: false, + animate: false, + }; + + cy.layout(layoutConfig).run(); + + cy.ready((e) => { + log.info('Cytoscape ready', e); + resolve(cy); + }); + }); +} + +/** + * Extract positioned nodes from cytoscape instance + * @param cy - The cytoscape instance after layout + * @returns Array of positioned nodes + */ +export function extractPositionedNodes(cy: cytoscape.Core): PositionedNode[] { + return cy.nodes().map((node) => { + const data = node.data(); + const position = node.position(); + + // Create a positioned node with all original data plus position + const positionedNode: PositionedNode = { + id: data.id, + x: position.x, + y: position.y, + }; + + // Add all other properties from the original data + Object.keys(data).forEach((key) => { + if (key !== 'id') { + positionedNode[key] = data[key]; + } + }); + + return positionedNode; + }); +} + +/** + * Extract positioned edges from cytoscape instance + * @param cy - The cytoscape instance after layout + * @returns Array of positioned edges + */ +export function extractPositionedEdges(cy: cytoscape.Core): PositionedEdge[] { + return cy.edges().map((edge) => { + const data = edge.data(); + const rscratch = edge._private.rscratch; + + // Create a positioned edge with all original data plus position + const positionedEdge: PositionedEdge = { + id: data.id, + source: data.source, + target: data.target, + startX: rscratch.startX, + startY: rscratch.startY, + midX: rscratch.midX, + midY: rscratch.midY, + endX: rscratch.endX, + endY: rscratch.endY, + }; + + // Add all other properties from the original data + Object.keys(data).forEach((key) => { + if (!['id', 'source', 'target'].includes(key)) { + positionedEdge[key] = data[key]; + } + }); + + return positionedEdge; + }); +} diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts new file mode 100644 index 000000000..78a82f02b --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts @@ -0,0 +1,86 @@ +import type { SVG } from '../../../diagram-api/types.js'; +import type { InternalHelpers } from '../../../internals.js'; +import { log } from '../../../logger.js'; +import type { LayoutData } from '../../types.js'; +import type { RenderOptions } from '../../render.js'; +import { executeCoseBilkentLayout } from './layout.js'; + +/** + * Cose-Bilkent Layout Algorithm for Generic Diagrams + * + * This module provides a layout algorithm implementation using Cytoscape + * with the cose-bilkent algorithm for positioning nodes and edges. + * + * The algorithm follows the unified rendering pattern and can be used + * by any diagram type that provides compatible LayoutData. + */ + +/** + * Render function for the cose-bilkent layout algorithm + * + * This function follows the unified rendering pattern used by all layout algorithms. + * It takes LayoutData, positions the nodes using Cytoscape with cose-bilkent, + * and renders the positioned elements to the SVG. + * + * @param layoutData - Layout data containing nodes, edges, and configuration + * @param svg - SVG element to render to + * @param helpers - Internal helper functions for rendering + * @param options - Rendering options + */ +export const render = async ( + layoutData: LayoutData, + _svg: SVG, + _helpers: InternalHelpers, + _options?: RenderOptions +): Promise => { + log.debug('Cose-bilkent layout algorithm starting'); + log.debug('LayoutData keys:', Object.keys(layoutData)); + + try { + // Validate input data + if (!layoutData.nodes || !Array.isArray(layoutData.nodes)) { + throw new Error('No nodes found in layout data'); + } + + if (!layoutData.edges || !Array.isArray(layoutData.edges)) { + throw new Error('No edges found in layout data'); + } + + log.debug(`Processing ${layoutData.nodes.length} nodes and ${layoutData.edges.length} edges`); + + // Execute the layout algorithm directly with the provided data + const result = await executeCoseBilkentLayout(layoutData, layoutData.config); + + // Update the original layout data with the positioned nodes and edges + layoutData.nodes.forEach((node) => { + const positionedNode = result.nodes.find((n) => n.id === node.id); + if (positionedNode) { + node.x = positionedNode.x; + node.y = positionedNode.y; + log.debug('Updated node position:', node.id, 'at', positionedNode.x, positionedNode.y); + } + }); + + layoutData.edges.forEach((edge) => { + const positionedEdge = result.edges.find((e) => e.id === edge.id); + if (positionedEdge) { + // Update edge with positioning information if needed + const edgeWithPosition = edge as unknown as Record; + edgeWithPosition.startX = positionedEdge.startX; + edgeWithPosition.startY = positionedEdge.startY; + edgeWithPosition.midX = positionedEdge.midX; + edgeWithPosition.midY = positionedEdge.midY; + edgeWithPosition.endX = positionedEdge.endX; + edgeWithPosition.endY = positionedEdge.endY; + } + }); + + log.debug('Cose-bilkent layout algorithm completed successfully'); + log.debug(`Positioned ${result.nodes.length} nodes and ${result.edges.length} edges`); + } catch (error) { + log.error('Cose-bilkent layout algorithm failed:', error); + throw new Error( + `Layout algorithm failed: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } +}; diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.test.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.test.ts new file mode 100644 index 000000000..1f8416f35 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.test.ts @@ -0,0 +1,250 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Mock cytoscape and cytoscape-cose-bilkent before importing the modules +vi.mock('cytoscape', () => { + const mockCy = { + add: vi.fn(), + nodes: vi.fn(() => ({ + forEach: vi.fn(), + map: vi.fn((fn) => [ + fn({ + data: () => ({ + id: '1', + nodeId: '1', + labelText: 'Root', + level: 0, + type: 0, + width: 100, + height: 50, + padding: 10, + }), + position: () => ({ x: 100, y: 100 }), + }), + ]), + })), + edges: vi.fn(() => ({ + map: vi.fn((fn) => [ + fn({ + data: () => ({ + id: '1_2', + source: '1', + target: '2', + depth: 0, + }), + _private: { + rscratch: { + startX: 100, + startY: 100, + midX: 150, + midY: 150, + endX: 200, + endY: 200, + }, + }, + }), + ]), + })), + layout: vi.fn(() => ({ + run: vi.fn(), + })), + ready: vi.fn((callback) => callback({})), + }; + + const mockCytoscape = vi.fn(() => mockCy); + mockCytoscape.use = vi.fn(); + + return { + default: mockCytoscape, + }; +}); + +vi.mock('cytoscape-cose-bilkent', () => ({ + default: vi.fn(), +})); + +vi.mock('d3', () => ({ + select: vi.fn(() => ({ + append: vi.fn(() => ({ + attr: vi.fn(() => ({ + attr: vi.fn(() => ({ + remove: vi.fn(), + })), + })), + })), + })), +})); + +// Import modules after mocks +import { layout, validateLayoutData } from './index.js'; +import type { MindmapLayoutData, LayoutResult } from './types.js'; +import type { MindmapNode } from '../../../diagrams/mindmap/mindmapTypes.js'; +import type { MermaidConfig } from '../../../config.type.js'; + +describe('Cose-Bilkent Layout Algorithm', () => { + let mockConfig: MermaidConfig; + let mockRootNode: MindmapNode; + let mockLayoutData: MindmapLayoutData; + + beforeEach(() => { + mockConfig = { + mindmap: { + layoutAlgorithm: 'cose-bilkent', + padding: 10, + maxNodeWidth: 200, + useMaxWidth: true, + }, + } as MermaidConfig; + + mockRootNode = { + id: 1, + nodeId: '1', + level: 0, + descr: 'Root', + type: 0, + width: 100, + height: 50, + padding: 10, + x: 0, + y: 0, + children: [ + { + id: 2, + nodeId: '2', + level: 1, + descr: 'Child 1', + type: 0, + width: 80, + height: 40, + padding: 10, + x: 0, + y: 0, + }, + ], + } as MindmapNode; + + mockLayoutData = { + nodes: [ + { + id: '1', + nodeId: '1', + level: 0, + descr: 'Root', + type: 0, + width: 100, + height: 50, + padding: 10, + }, + { + id: '2', + nodeId: '2', + level: 1, + descr: 'Child 1', + type: 0, + width: 80, + height: 40, + padding: 10, + }, + ], + edges: [ + { + id: '1_2', + source: '1', + target: '2', + depth: 0, + }, + ], + config: mockConfig, + rootNode: mockRootNode, + }; + }); + + describe('validateLayoutData', () => { + it('should validate correct layout data', () => { + expect(() => validateLayoutData(mockLayoutData)).not.toThrow(); + }); + + it('should throw error for missing data', () => { + expect(() => validateLayoutData(null as any)).toThrow('Layout data is required'); + }); + + it('should throw error for missing root node', () => { + const invalidData = { ...mockLayoutData, rootNode: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Root node is required'); + }); + + it('should throw error for missing config', () => { + const invalidData = { ...mockLayoutData, config: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required'); + }); + + it('should throw error for invalid nodes array', () => { + const invalidData = { ...mockLayoutData, nodes: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required'); + }); + + it('should throw error for invalid edges array', () => { + const invalidData = { ...mockLayoutData, edges: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required'); + }); + }); + + describe('layout function', () => { + it('should execute layout algorithm successfully', async () => { + const result: LayoutResult = await layout(mockLayoutData, mockConfig); + + expect(result).toBeDefined(); + expect(result.nodes).toBeDefined(); + expect(result.edges).toBeDefined(); + expect(Array.isArray(result.nodes)).toBe(true); + expect(Array.isArray(result.edges)).toBe(true); + }); + + it('should return positioned nodes with coordinates', async () => { + const result: LayoutResult = await layout(mockLayoutData, mockConfig); + + expect(result.nodes.length).toBeGreaterThan(0); + result.nodes.forEach((node) => { + expect(node.x).toBeDefined(); + expect(node.y).toBeDefined(); + expect(typeof node.x).toBe('number'); + expect(typeof node.y).toBe('number'); + }); + }); + + it('should return positioned edges with coordinates', async () => { + const result: LayoutResult = await layout(mockLayoutData, mockConfig); + + expect(result.edges.length).toBeGreaterThan(0); + result.edges.forEach((edge) => { + expect(edge.startX).toBeDefined(); + expect(edge.startY).toBeDefined(); + expect(edge.midX).toBeDefined(); + expect(edge.midY).toBeDefined(); + expect(edge.endX).toBeDefined(); + expect(edge.endY).toBeDefined(); + }); + }); + + it('should handle empty mindmap data gracefully', async () => { + const emptyData: MindmapLayoutData = { + nodes: [], + edges: [], + config: mockConfig, + rootNode: mockRootNode, + }; + + const result: LayoutResult = await layout(emptyData, mockConfig); + expect(result).toBeDefined(); + expect(result.nodes).toBeDefined(); + expect(result.edges).toBeDefined(); + expect(Array.isArray(result.nodes)).toBe(true); + expect(Array.isArray(result.edges)).toBe(true); + }); + + it('should throw error for invalid data', async () => { + const invalidData = { ...mockLayoutData, rootNode: null as any }; + + await expect(layout(invalidData, mockConfig)).rejects.toThrow(); + }); + }); +}); diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.ts new file mode 100644 index 000000000..ba4ec0e12 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.ts @@ -0,0 +1,79 @@ +import type { MermaidConfig } from '../../../config.type.js'; +import { log } from '../../../logger.js'; +import type { LayoutData } from '../../types.js'; +import type { LayoutResult } from './types.js'; +import { + createCytoscapeInstance, + extractPositionedNodes, + extractPositionedEdges, +} from './cytoscape-setup.js'; + +/** + * Execute the cose-bilkent layout algorithm on generic layout data + * + * This function takes layout data and uses Cytoscape with the cose-bilkent + * algorithm to calculate optimal node positions and edge paths. + * + * @param data - The layout data containing nodes, edges, and configuration + * @param config - Mermaid configuration object + * @returns Promise resolving to layout result with positioned nodes and edges + */ +export async function executeCoseBilkentLayout( + data: LayoutData, + _config: MermaidConfig +): Promise { + log.debug('Starting cose-bilkent layout algorithm'); + + try { + // Validate input data + if (!data.nodes || !Array.isArray(data.nodes)) { + throw new Error('No nodes found in layout data'); + } + + if (!data.edges || !Array.isArray(data.edges)) { + throw new Error('No edges found in layout data'); + } + + // Create and configure cytoscape instance + const cy = await createCytoscapeInstance(data); + + // Extract positioned nodes and edges after layout + const positionedNodes = extractPositionedNodes(cy); + const positionedEdges = extractPositionedEdges(cy); + + log.debug(`Layout completed: ${positionedNodes.length} nodes, ${positionedEdges.length} edges`); + + return { + nodes: positionedNodes, + edges: positionedEdges, + }; + } catch (error) { + log.error('Error in cose-bilkent layout algorithm:', error); + throw error; + } +} + +/** + * Validate layout data structure + * @param data - The data to validate + * @returns True if data is valid, throws error otherwise + */ +export function validateLayoutData(data: LayoutData): boolean { + if (!data) { + throw new Error('Layout data is required'); + } + + if (!data.config) { + throw new Error('Configuration is required in layout data'); + } + + if (!Array.isArray(data.nodes)) { + throw new Error('Nodes array is required in layout data'); + } + + if (!Array.isArray(data.edges)) { + throw new Error('Edges array is required in layout data'); + } + + return true; +} diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/types.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/types.ts new file mode 100644 index 000000000..fade24682 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/types.ts @@ -0,0 +1,43 @@ +/** + * Positioned node after layout calculation + */ +export interface PositionedNode { + id: string; + x: number; + y: number; + [key: string]: unknown; // Allow additional properties +} + +/** + * Positioned edge after layout calculation + */ +export interface PositionedEdge { + id: string; + source: string; + target: string; + startX: number; + startY: number; + midX: number; + midY: number; + endX: number; + endY: number; + [key: string]: unknown; // Allow additional properties +} + +/** + * Result of layout algorithm execution + */ +export interface LayoutResult { + nodes: PositionedNode[]; + edges: PositionedEdge[]; +} + +/** + * Cytoscape layout configuration + */ +export interface CytoscapeLayoutConfig { + name: 'cose-bilkent'; + quality: 'proof'; + styleEnabled: boolean; + animate: boolean; +} diff --git a/packages/mermaid/src/rendering-util/render.ts b/packages/mermaid/src/rendering-util/render.ts index b975e7bf9..22a7b96b0 100644 --- a/packages/mermaid/src/rendering-util/render.ts +++ b/packages/mermaid/src/rendering-util/render.ts @@ -39,6 +39,10 @@ const registerDefaultLayoutLoaders = () => { name: 'dagre', loader: async () => await import('./layout-algorithms/dagre/index.js'), }, + { + name: 'cose-bilkent', + loader: async () => await import('./layout-algorithms/cose-bilkent/index.js'), + }, ]); }; diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 6dd21e884..756a55e5d 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -962,6 +962,7 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) - useMaxWidth - padding - maxNodeWidth + - layoutAlgorithm properties: padding: type: number @@ -969,6 +970,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) maxNodeWidth: type: number default: 200 + layoutAlgorithm: + description: Layout algorithm to use for positioning mindmap nodes + type: string + default: 'cose-bilkent' KanbanDiagramConfig: title: Kanban Diagram Config From 34027bc58946e5e4110bbf5d31fee27a4fea2461 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Fri, 6 Jun 2025 20:03:34 +0200 Subject: [PATCH 008/314] Repackaging cose-bilkins --- cypress/platform/knsv2.html | 8 +- .../mermaid/src/diagrams/mindmap/mindmapDb.ts | 40 +++- .../layout-algorithms/cose-bilkent/index.ts | 67 +------ .../layout-algorithms/cose-bilkent/render.ts | 183 ++++++++++++++++++ test-unified-rendering.html | 40 ++++ 5 files changed, 269 insertions(+), 69 deletions(-) create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts create mode 100644 test-unified-rendering.html diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 8f5aa428a..347b50418 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -133,7 +133,7 @@
       ---
       config:
-        layout: dagre
+        layout: cose-bilkent
       ---
       mindmap
       root((mindmap))
@@ -154,13 +154,13 @@
     
       ---
       config:
-        layout: elk
+        layout: cose-bilkent
       ---
       flowchart LR
       root{mindmap} --- Origins --- Europe
-      root --- Origins --- Asia
+      Origins --> Asia
       root --- Background --- Rich
-      root --- Background --- Poor
+      Background --- Poor
 
 
 
diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
index f401881d4..75fbfd27b 100644
--- a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
+++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
@@ -196,12 +196,34 @@ const flattenNodes = (node: MindmapNode, processedNodes: MindmapLayoutNode[]): v
     cssClasses += ` ${node.class}`;
   }
 
+  // Map mindmap node type to valid shape name
+  const getShapeFromType = (type: number) => {
+    switch (type) {
+      case nodeType.CIRCLE:
+        return 'circle';
+      case nodeType.RECT:
+        return 'rect';
+      case nodeType.ROUNDED_RECT:
+        return 'rounded';
+      case nodeType.CLOUD:
+        return 'rounded'; // Map cloud to rounded for now
+      case nodeType.BANG:
+        return 'circle'; // Map bang to circle for now
+      case nodeType.HEXAGON:
+        return 'hexagon';
+      case nodeType.DEFAULT:
+      case nodeType.NO_BORDER:
+      default:
+        return 'rect';
+    }
+  };
+
   const processedNode: MindmapLayoutNode = {
     id: 'node_' + node.id.toString(),
     domId: 'node_' + node.id.toString(),
     label: node.descr,
     isGroup: false,
-    shape: 'rect', // Default shape, can be customized based on node.type
+    shape: getShapeFromType(node.type),
     width: node.width,
     height: node.height ?? 0,
     padding: node.padding,
@@ -298,6 +320,17 @@ const getData = (): LayoutData => {
 
   log.debug(`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`);
 
+  // Create shapes map for ELK compatibility
+  const shapes = new Map();
+  processedNodes.forEach((node) => {
+    shapes.set(node.id, {
+      shape: node.shape,
+      width: node.width,
+      height: node.height,
+      padding: node.padding,
+    });
+  });
+
   return {
     nodes: processedNodes,
     edges: processedEdges,
@@ -309,6 +342,11 @@ const getData = (): LayoutData => {
     direction: 'TB', // Top-to-bottom direction for mindmaps
     nodeSpacing: 50, // Default spacing between nodes
     rankSpacing: 50, // Default spacing between ranks
+    // Add shapes for ELK compatibility
+    shapes: Object.fromEntries(shapes),
+    // Additional properties that layout algorithms might expect
+    type: 'mindmap',
+    diagramId: 'mindmap-' + Date.now(),
   };
 };
 
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts
index 78a82f02b..9e12d38a7 100644
--- a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts
@@ -1,9 +1,4 @@
-import type { SVG } from '../../../diagram-api/types.js';
-import type { InternalHelpers } from '../../../internals.js';
-import { log } from '../../../logger.js';
-import type { LayoutData } from '../../types.js';
-import type { RenderOptions } from '../../render.js';
-import { executeCoseBilkentLayout } from './layout.js';
+import { render as renderWithCoseBilkent } from './render.js';
 
 /**
  * Cose-Bilkent Layout Algorithm for Generic Diagrams
@@ -19,7 +14,7 @@ import { executeCoseBilkentLayout } from './layout.js';
  * Render function for the cose-bilkent layout algorithm
  *
  * This function follows the unified rendering pattern used by all layout algorithms.
- * It takes LayoutData, positions the nodes using Cytoscape with cose-bilkent,
+ * It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm,
  * and renders the positioned elements to the SVG.
  *
  * @param layoutData - Layout data containing nodes, edges, and configuration
@@ -27,60 +22,4 @@ import { executeCoseBilkentLayout } from './layout.js';
  * @param helpers - Internal helper functions for rendering
  * @param options - Rendering options
  */
-export const render = async (
-  layoutData: LayoutData,
-  _svg: SVG,
-  _helpers: InternalHelpers,
-  _options?: RenderOptions
-): Promise => {
-  log.debug('Cose-bilkent layout algorithm starting');
-  log.debug('LayoutData keys:', Object.keys(layoutData));
-
-  try {
-    // Validate input data
-    if (!layoutData.nodes || !Array.isArray(layoutData.nodes)) {
-      throw new Error('No nodes found in layout data');
-    }
-
-    if (!layoutData.edges || !Array.isArray(layoutData.edges)) {
-      throw new Error('No edges found in layout data');
-    }
-
-    log.debug(`Processing ${layoutData.nodes.length} nodes and ${layoutData.edges.length} edges`);
-
-    // Execute the layout algorithm directly with the provided data
-    const result = await executeCoseBilkentLayout(layoutData, layoutData.config);
-
-    // Update the original layout data with the positioned nodes and edges
-    layoutData.nodes.forEach((node) => {
-      const positionedNode = result.nodes.find((n) => n.id === node.id);
-      if (positionedNode) {
-        node.x = positionedNode.x;
-        node.y = positionedNode.y;
-        log.debug('Updated node position:', node.id, 'at', positionedNode.x, positionedNode.y);
-      }
-    });
-
-    layoutData.edges.forEach((edge) => {
-      const positionedEdge = result.edges.find((e) => e.id === edge.id);
-      if (positionedEdge) {
-        // Update edge with positioning information if needed
-        const edgeWithPosition = edge as unknown as Record;
-        edgeWithPosition.startX = positionedEdge.startX;
-        edgeWithPosition.startY = positionedEdge.startY;
-        edgeWithPosition.midX = positionedEdge.midX;
-        edgeWithPosition.midY = positionedEdge.midY;
-        edgeWithPosition.endX = positionedEdge.endX;
-        edgeWithPosition.endY = positionedEdge.endY;
-      }
-    });
-
-    log.debug('Cose-bilkent layout algorithm completed successfully');
-    log.debug(`Positioned ${result.nodes.length} nodes and ${result.edges.length} edges`);
-  } catch (error) {
-    log.error('Cose-bilkent layout algorithm failed:', error);
-    throw new Error(
-      `Layout algorithm failed: ${error instanceof Error ? error.message : 'Unknown error'}`
-    );
-  }
-};
+export const render = renderWithCoseBilkent;
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts
new file mode 100644
index 000000000..1616fb781
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts
@@ -0,0 +1,183 @@
+import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
+import { executeCoseBilkentLayout } from './layout.js';
+
+type Node = LayoutData['nodes'][number];
+
+interface NodeWithPosition extends Node {
+  x?: number;
+  y?: number;
+  domId?: SVGGroup;
+}
+
+/**
+ * Render function for cose-bilkent layout algorithm
+ *
+ * This follows the same pattern as ELK and dagre renderers:
+ * 1. Insert nodes into DOM to get their actual dimensions
+ * 2. Run the layout algorithm to calculate positions
+ * 3. Position the nodes and edges based on layout results
+ */
+export const render = async (
+  data4Layout: LayoutData,
+  svg: SVG,
+  {
+    insertCluster,
+    insertEdge,
+    insertEdgeLabel,
+    insertMarkers,
+    insertNode,
+    log,
+    positionEdgeLabel,
+  }: InternalHelpers,
+  { algorithm }: RenderOptions
+) => {
+  const nodeDb: Record = {};
+  const clusterDb: Record = {};
+
+  // Insert markers for edges
+  const element = svg.select('g');
+  insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
+
+  // Create container groups
+  const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
+  const edgePaths = element.insert('g').attr('class', 'edgePaths');
+  const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
+  const nodes = element.insert('g').attr('class', 'nodes');
+
+  // Step 1: Insert nodes into DOM to get their actual dimensions
+  log.debug('Inserting nodes into DOM for dimension calculation');
+
+  await Promise.all(
+    data4Layout.nodes.map(async (node) => {
+      if (node.isGroup) {
+        // Handle subgraphs/clusters
+        const clusterNode: NodeWithPosition = { ...node };
+        clusterDb[node.id] = clusterNode;
+        nodeDb[node.id] = clusterNode;
+
+        // Insert cluster to get dimensions
+        await insertCluster(subGraphsEl, node);
+      } else {
+        // Handle regular nodes
+        const nodeWithPosition: NodeWithPosition = { ...node };
+        nodeDb[node.id] = nodeWithPosition;
+
+        // Insert node to get actual dimensions
+        const nodeEl = await insertNode(nodes, node, {
+          config: data4Layout.config,
+          dir: data4Layout.direction || 'TB',
+        });
+
+        // Get the actual bounding box after insertion
+        const boundingBox = nodeEl.node()!.getBBox();
+        nodeWithPosition.width = boundingBox.width;
+        nodeWithPosition.height = boundingBox.height;
+        nodeWithPosition.domId = nodeEl;
+
+        log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
+      }
+    })
+  );
+
+  // Step 2: Run the cose-bilkent layout algorithm
+  log.debug('Running cose-bilkent layout algorithm');
+
+  // Update the layout data with actual dimensions
+  const updatedLayoutData = {
+    ...data4Layout,
+    nodes: data4Layout.nodes.map((node) => {
+      const nodeWithDimensions = nodeDb[node.id];
+      return {
+        ...node,
+        width: nodeWithDimensions.width,
+        height: nodeWithDimensions.height,
+      };
+    }),
+  };
+
+  const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config);
+
+  // Step 3: Position the nodes based on layout results
+  log.debug('Positioning nodes based on layout results');
+
+  layoutResult.nodes.forEach((positionedNode) => {
+    const node = nodeDb[positionedNode.id];
+    if (node && node.domId) {
+      // Position the node at the calculated coordinates
+      // The positionedNode.x/y represents the center of the node, so use directly
+      node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
+
+      // Store the final position
+      node.x = positionedNode.x;
+      node.y = positionedNode.y;
+
+      log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`);
+    }
+  });
+
+  // Step 4: Insert and position edges
+  log.debug('Inserting and positioning edges');
+
+  await Promise.all(
+    data4Layout.edges.map(async (edge) => {
+      // Insert edge label first
+      const edgeLabel = await insertEdgeLabel(edgeLabels, edge);
+
+      // Get start and end nodes
+      const startNode = nodeDb[edge.start];
+      const endNode = nodeDb[edge.end];
+
+      if (startNode && endNode) {
+        // Find the positioned edge data
+        const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
+
+        if (positionedEdge) {
+          // Create edge path with positioned coordinates
+          const edgeWithPath = {
+            ...edge,
+            points: [
+              { x: positionedEdge.startX, y: positionedEdge.startY },
+              { x: positionedEdge.endX, y: positionedEdge.endY },
+            ],
+          };
+
+          // Insert the edge path
+          const paths = insertEdge(
+            edgePaths,
+            edgeWithPath,
+            clusterDb,
+            data4Layout.type,
+            startNode,
+            endNode,
+            data4Layout.diagramId
+          );
+
+          // Position the edge label
+          positionEdgeLabel(edgeWithPath, paths);
+        } else {
+          // Fallback: create a simple straight line between nodes
+          const edgeWithPath = {
+            ...edge,
+            points: [
+              { x: startNode.x || 0, y: startNode.y || 0 },
+              { x: endNode.x || 0, y: endNode.y || 0 },
+            ],
+          };
+
+          const paths = insertEdge(
+            edgePaths,
+            edgeWithPath,
+            clusterDb,
+            data4Layout.type,
+            startNode,
+            endNode,
+            data4Layout.diagramId
+          );
+          positionEdgeLabel(edgeWithPath, paths);
+        }
+      }
+    })
+  );
+
+  log.debug('Cose-bilkent rendering completed');
+};
diff --git a/test-unified-rendering.html b/test-unified-rendering.html
new file mode 100644
index 000000000..4fdb0efe3
--- /dev/null
+++ b/test-unified-rendering.html
@@ -0,0 +1,40 @@
+
+
+  
+    Test Unified Rendering - Mindmap with Different Layout Algorithms
+    
+  
+  
+    

Test Unified Rendering System

+ +

Mindmap with Cose-Bilkent (Default)

+
+ mindmap root((mindmap)) Origins Long history ::icon(fa fa-book) Popularisation British popular + psychology author Tony Buzan Research On effectiveness
and features On Automatic creation + Uses Creative techniques Strategic planning Argument mapping Tools Pen and paper Mermaid +
+ +

Mindmap with Dagre Layout

+
+ --- config: layout: dagre --- mindmap root((mindmap)) Origins Long history Popularisation + British popular psychology author Tony Buzan Research On effectiveness
and features On + Automatic creation Uses Creative techniques Strategic planning Tools Pen and paper Mermaid +
+ +

ER Diagram with Cose-Bilkent Layout

+
+ --- config: layout: cose-bilkent --- erDiagram CUSTOMER ||--o{ ORDER : places ORDER ||--|{ + LINE-ITEM : contains CUSTOMER }|..|{ DELIVERY-ADDRESS : uses +
+ + + + From fbae6114065ea4132bb47cf63be22f98906626f7 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Fri, 6 Jun 2025 20:10:19 +0200 Subject: [PATCH 009/314] Refresh --- packages/mermaid-layout-elk/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mermaid-layout-elk/README.md b/packages/mermaid-layout-elk/README.md index c32803292..5120fe85c 100644 --- a/packages/mermaid-layout-elk/README.md +++ b/packages/mermaid-layout-elk/README.md @@ -2,7 +2,7 @@ This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine. -> [!NOTE] +> [!NOTE] > The ELK Layout engine will not be available in all providers that support mermaid by default. > The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine. @@ -69,4 +69,4 @@ mermaid.registerLayoutLoaders(elkLayouts); - `elk.mrtree`: Multi-root tree layout - `elk.sporeOverlap`: Spore overlap layout - + From eadb343292494afb59a768f218b113b45b703113 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 11 Jun 2025 16:55:58 +0200 Subject: [PATCH 010/314] Tidy-tree POC --- cypress/platform/knsv2.html | 131 ++++++-- packages/mermaid/package.json | 1 + .../tidy-tree/cytoscape-setup.test.ts | 281 ++++++++++++++++ .../tidy-tree/cytoscape-setup.ts | 207 ++++++++++++ .../layout-algorithms/tidy-tree/index.ts | 25 ++ .../tidy-tree/layout.test.ts | 250 ++++++++++++++ .../layout-algorithms/tidy-tree/layout.ts | 79 +++++ .../layout-algorithms/tidy-tree/render.ts | 183 +++++++++++ .../layout-algorithms/tidy-tree/types.ts | 43 +++ packages/mermaid/src/rendering-util/render.ts | 4 + pnpm-lock.yaml | 309 ++++++++++++------ 11 files changed, 1387 insertions(+), 126 deletions(-) create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.test.ts create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.ts create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/index.ts create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.test.ts create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.ts create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/render.ts create mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/types.ts diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 697be86b4..431e139a5 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -105,6 +105,29 @@ +
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+        Tools
+          Pen and paper
+          Mermaid
+    
+
+      ---
+      config:
+        layout: elk
+      ---
+      flowchart TB
+          A --> n0["1"]
+          A --> n1["2"]
+          A --> n2["3"]
+          A --> n3["4"]
+    
       ---
       config:
@@ -127,10 +150,8 @@
         Tools
           Pen and paper
           Mermaid
-
-
     
-
+    
       ---
       config:
         layout: cose-bilkent
@@ -138,23 +159,67 @@
       mindmap
       root((mindmap))
         Origins
-          Europe
-          Asia
-            East
-            West
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+          Pen and paper
+          Mermaid
+    
+
+      ---
+      config:
+        layout: elk
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+          Pen and paper
+          Mermaid
+    
+
+      ---
+      config:
+        layout: cose-bilkent
+      ---
+      flowchart LR
+      root{mindmap} --- Origins --- Europe
+      Origins --> Asia
+      root --- Background --- Rich
+      Background --- Poor
+      subgraph apa
         Background
-          Rich
-          Poor
-
+        Poor
+      end
 
 
 
 
     
-
+    
       ---
       config:
-        layout: cose-bilkent
+        layout: elk
       ---
       flowchart LR
       root{mindmap} --- Origins --- Europe
@@ -269,7 +334,7 @@ config:
       end
       end
     
-
+    
 ---
 config:
   layout: elk
@@ -282,7 +347,7 @@ config:
       D-->I
       D-->I
     
-
+    
 ---
 config:
   layout: elk
@@ -321,7 +386,7 @@ flowchart LR
     n8@{ shape: rect}
 
     
-
+    
 ---
 config:
   layout: elk
@@ -337,7 +402,7 @@ flowchart LR
 
 
     
-
+    
 ---
 config:
   layout: elk
@@ -346,7 +411,7 @@ flowchart LR
     A{A} --> B & C
 
-
+    
 ---
 config:
   layout: elk
@@ -376,7 +441,7 @@ flowchart LR
 
 
     
-
+    
 ---
 config:
   kanban:
@@ -395,81 +460,81 @@ kanban
     task3[💻 Develop login feature]@{ ticket: 103 }
 
     
-
+    
 flowchart LR
 nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
 
     
-
+    
 flowchart LR
 nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
 style A fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
 nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
 A:::AClass
 classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
   nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
 
     
-
+    
 flowchart LR
 nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
 
     
-
+    
 flowchart LR
 nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
 style A fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
 nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
 A:::AClass
 classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
   nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
 
     
-
+    
 flowchart LR
 nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
 
     
-
+    
 flowchart LR
 nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
 style A fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
 nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
 A:::AClass
 classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
   nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
   A:::AClass
   classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 flowchart LR
   nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
   style A fill:#f9f,stroke:#333,stroke-width:4px
     
-
+    
 kanban
   id2[In progress]
     docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
     
-
+    
 ---
 config:
   kanban:
diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json
index 7f8230229..157f00669 100644
--- a/packages/mermaid/package.json
+++ b/packages/mermaid/package.json
@@ -83,6 +83,7 @@
     "khroma": "^2.1.0",
     "lodash-es": "^4.17.21",
     "marked": "^15.0.7",
+    "non-layered-tidy-tree-layout": "^2.0.2",
     "roughjs": "^4.6.6",
     "stylis": "^4.3.6",
     "ts-dedent": "^2.2.0",
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.test.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.test.ts
new file mode 100644
index 000000000..2592b57db
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.test.ts
@@ -0,0 +1,281 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import {
+  addNodes,
+  addEdges,
+  extractPositionedNodes,
+  extractPositionedEdges,
+} from './cytoscape-setup.js';
+import type { Node, Edge } from '../../types.js';
+
+// Mock cytoscape
+const mockCy = {
+  add: vi.fn(),
+  nodes: vi.fn(),
+  edges: vi.fn(),
+};
+
+vi.mock('cytoscape', () => {
+  const mockCytoscape = vi.fn(() => mockCy) as any;
+  mockCytoscape.use = vi.fn();
+  return {
+    default: mockCytoscape,
+  };
+});
+
+vi.mock('cytoscape-cose-bilkent', () => ({
+  default: vi.fn(),
+}));
+
+vi.mock('d3', () => ({
+  select: vi.fn(() => ({
+    append: vi.fn(() => ({
+      attr: vi.fn(() => ({
+        attr: vi.fn(() => ({
+          remove: vi.fn(),
+        })),
+      })),
+    })),
+  })),
+}));
+
+describe('Cytoscape Setup', () => {
+  let mockNodes: Node[];
+  let mockEdges: Edge[];
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+
+    mockNodes = [
+      {
+        id: '1',
+        label: 'Root',
+        isGroup: false,
+        shape: 'rect',
+        width: 100,
+        height: 50,
+        padding: 10,
+        x: 100,
+        y: 100,
+        cssClasses: '',
+        cssStyles: [],
+        look: 'default',
+      },
+      {
+        id: '2',
+        label: 'Child 1',
+        isGroup: false,
+        shape: 'rect',
+        width: 80,
+        height: 40,
+        padding: 10,
+        x: 150,
+        y: 150,
+        cssClasses: '',
+        cssStyles: [],
+        look: 'default',
+      },
+    ];
+
+    mockEdges = [
+      {
+        id: '1_2',
+        start: '1',
+        end: '2',
+        type: 'edge',
+        classes: '',
+        style: [],
+        animate: false,
+        arrowTypeEnd: 'arrow_point',
+        arrowTypeStart: 'none',
+      },
+    ];
+  });
+
+  describe('addNodes', () => {
+    it('should add nodes to cytoscape', () => {
+      addNodes([mockNodes[0]], mockCy as unknown as any);
+
+      expect(mockCy.add).toHaveBeenCalledWith({
+        group: 'nodes',
+        data: {
+          id: '1',
+          labelText: 'Root',
+          height: 50,
+          width: 100,
+          padding: 10,
+          isGroup: false,
+          shape: 'rect',
+          cssClasses: '',
+          cssStyles: [],
+          look: 'default',
+        },
+        position: {
+          x: 100,
+          y: 100,
+        },
+      });
+    });
+
+    it('should add multiple nodes to cytoscape', () => {
+      addNodes(mockNodes, mockCy as unknown as any);
+
+      expect(mockCy.add).toHaveBeenCalledTimes(2);
+
+      expect(mockCy.add).toHaveBeenCalledWith({
+        group: 'nodes',
+        data: {
+          id: '1',
+          labelText: 'Root',
+          height: 50,
+          width: 100,
+          padding: 10,
+          isGroup: false,
+          shape: 'rect',
+          cssClasses: '',
+          cssStyles: [],
+          look: 'default',
+        },
+        position: {
+          x: 100,
+          y: 100,
+        },
+      });
+
+      expect(mockCy.add).toHaveBeenCalledWith({
+        group: 'nodes',
+        data: {
+          id: '2',
+          labelText: 'Child 1',
+          height: 40,
+          width: 80,
+          padding: 10,
+          isGroup: false,
+          shape: 'rect',
+          cssClasses: '',
+          cssStyles: [],
+          look: 'default',
+        },
+        position: {
+          x: 150,
+          y: 150,
+        },
+      });
+    });
+  });
+
+  describe('addEdges', () => {
+    it('should add edges to cytoscape', () => {
+      addEdges(mockEdges, mockCy as unknown as any);
+
+      expect(mockCy.add).toHaveBeenCalledWith({
+        group: 'edges',
+        data: {
+          id: '1_2',
+          source: '1',
+          target: '2',
+          type: 'edge',
+          classes: '',
+          style: [],
+          animate: false,
+          arrowTypeEnd: 'arrow_point',
+          arrowTypeStart: 'none',
+        },
+      });
+    });
+  });
+
+  describe('extractPositionedNodes', () => {
+    it('should extract positioned nodes from cytoscape', () => {
+      const mockCytoscapeNodes = [
+        {
+          data: () => ({
+            id: '1',
+            labelText: 'Root',
+            width: 100,
+            height: 50,
+            padding: 10,
+            isGroup: false,
+            shape: 'rect',
+          }),
+          position: () => ({ x: 100, y: 100 }),
+        },
+        {
+          data: () => ({
+            id: '2',
+            labelText: 'Child 1',
+            width: 80,
+            height: 40,
+            padding: 10,
+            isGroup: false,
+            shape: 'rect',
+          }),
+          position: () => ({ x: 150, y: 150 }),
+        },
+      ];
+
+      mockCy.nodes.mockReturnValue({
+        map: (fn: unknown) => mockCytoscapeNodes.map(fn as any),
+      });
+
+      const result = extractPositionedNodes(mockCy as unknown as any);
+
+      expect(result).toHaveLength(2);
+      expect(result[0]).toEqual({
+        id: '1',
+        x: 100,
+        y: 100,
+        labelText: 'Root',
+        width: 100,
+        height: 50,
+        padding: 10,
+        isGroup: false,
+        shape: 'rect',
+      });
+    });
+  });
+
+  describe('extractPositionedEdges', () => {
+    it('should extract positioned edges from cytoscape', () => {
+      const mockCytoscapeEdges = [
+        {
+          data: () => ({
+            id: '1_2',
+            source: '1',
+            target: '2',
+            type: 'edge',
+          }),
+          _private: {
+            rscratch: {
+              startX: 100,
+              startY: 100,
+              midX: 125,
+              midY: 125,
+              endX: 150,
+              endY: 150,
+            },
+          },
+        },
+      ];
+
+      mockCy.edges.mockReturnValue({
+        map: (fn: unknown) => mockCytoscapeEdges.map(fn as any),
+      });
+
+      const result = extractPositionedEdges(mockCy as unknown as any);
+
+      expect(result).toHaveLength(1);
+      expect(result[0]).toEqual({
+        id: '1_2',
+        source: '1',
+        target: '2',
+        type: 'edge',
+        startX: 100,
+        startY: 100,
+        midX: 125,
+        midY: 125,
+        endX: 150,
+        endY: 150,
+      });
+    });
+  });
+});
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.ts
new file mode 100644
index 000000000..8fb9b2599
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.ts
@@ -0,0 +1,207 @@
+import cytoscape from 'cytoscape';
+import coseBilkent from 'cytoscape-cose-bilkent';
+import { select } from 'd3';
+import { log } from '../../../logger.js';
+import type { LayoutData, Node, Edge } from '../../types.js';
+import type { CytoscapeLayoutConfig, PositionedNode, PositionedEdge } from './types.js';
+
+// Inject the layout algorithm into cytoscape
+cytoscape.use(coseBilkent);
+
+/**
+ * Declare module augmentation for cytoscape edge types
+ */
+declare module 'cytoscape' {
+  interface EdgeSingular {
+    _private: {
+      bodyBounds: unknown;
+      rscratch: {
+        startX: number;
+        startY: number;
+        midX: number;
+        midY: number;
+        endX: number;
+        endY: number;
+      };
+    };
+  }
+}
+
+/**
+ * Add nodes to cytoscape instance from provided node array
+ * This function processes only the nodes provided in the data structure
+ * @param nodes - Array of nodes to add
+ * @param cy - The cytoscape instance
+ */
+export function addNodes(nodes: Node[], cy: cytoscape.Core): void {
+  nodes.forEach((node) => {
+    const nodeData: Record = {
+      id: node.id,
+      labelText: node.label,
+      height: node.height,
+      width: node.width,
+      padding: node.padding ?? 0,
+    };
+
+    // Add any additional properties from the node
+    Object.keys(node).forEach((key) => {
+      if (!['id', 'label', 'height', 'width', 'padding', 'x', 'y'].includes(key)) {
+        nodeData[key] = (node as unknown as Record)[key];
+      }
+    });
+
+    cy.add({
+      group: 'nodes',
+      data: nodeData,
+      position: {
+        x: node.x ?? 0,
+        y: node.y ?? 0,
+      },
+    });
+  });
+}
+
+/**
+ * Add edges to cytoscape instance from provided edge array
+ * This function processes only the edges provided in the data structure
+ * @param edges - Array of edges to add
+ * @param cy - The cytoscape instance
+ */
+export function addEdges(edges: Edge[], cy: cytoscape.Core): void {
+  edges.forEach((edge) => {
+    const edgeData: Record = {
+      id: edge.id,
+      source: edge.start,
+      target: edge.end,
+    };
+
+    // Add any additional properties from the edge
+    Object.keys(edge).forEach((key) => {
+      if (!['id', 'start', 'end'].includes(key)) {
+        edgeData[key] = (edge as unknown as Record)[key];
+      }
+    });
+
+    cy.add({
+      group: 'edges',
+      data: edgeData,
+    });
+  });
+}
+
+/**
+ * Create and configure cytoscape instance
+ * @param data - Layout data containing nodes and edges
+ * @returns Promise resolving to configured cytoscape instance
+ */
+export function createCytoscapeInstance(data: LayoutData): Promise {
+  return new Promise((resolve) => {
+    // Add temporary render element
+    const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
+
+    const cy = cytoscape({
+      container: document.getElementById('cy'), // container to render in
+      style: [
+        {
+          selector: 'edge',
+          style: {
+            'curve-style': 'bezier',
+          },
+        },
+      ],
+    });
+
+    // Remove element after layout
+    renderEl.remove();
+
+    // Add all nodes and edges to cytoscape using the generic functions
+    addNodes(data.nodes, cy);
+    addEdges(data.edges, cy);
+
+    // Make cytoscape care about the dimensions of the nodes
+    cy.nodes().forEach(function (n) {
+      n.layoutDimensions = () => {
+        const nodeData = n.data();
+        return { w: nodeData.width, h: nodeData.height };
+      };
+    });
+
+    // Configure and run the cose-bilkent layout
+    const layoutConfig: CytoscapeLayoutConfig = {
+      name: 'cose-bilkent',
+      // @ts-ignore Types for cose-bilkent are not correct?
+      quality: 'proof',
+      styleEnabled: false,
+      animate: false,
+    };
+
+    cy.layout(layoutConfig).run();
+
+    cy.ready((e) => {
+      log.info('Cytoscape ready', e);
+      resolve(cy);
+    });
+  });
+}
+
+/**
+ * Extract positioned nodes from cytoscape instance
+ * @param cy - The cytoscape instance after layout
+ * @returns Array of positioned nodes
+ */
+export function extractPositionedNodes(cy: cytoscape.Core): PositionedNode[] {
+  return cy.nodes().map((node) => {
+    const data = node.data();
+    const position = node.position();
+
+    // Create a positioned node with all original data plus position
+    const positionedNode: PositionedNode = {
+      id: data.id,
+      x: position.x,
+      y: position.y,
+    };
+
+    // Add all other properties from the original data
+    Object.keys(data).forEach((key) => {
+      if (key !== 'id') {
+        positionedNode[key] = data[key];
+      }
+    });
+
+    return positionedNode;
+  });
+}
+
+/**
+ * Extract positioned edges from cytoscape instance
+ * @param cy - The cytoscape instance after layout
+ * @returns Array of positioned edges
+ */
+export function extractPositionedEdges(cy: cytoscape.Core): PositionedEdge[] {
+  return cy.edges().map((edge) => {
+    const data = edge.data();
+    const rscratch = edge._private.rscratch;
+
+    // Create a positioned edge with all original data plus position
+    const positionedEdge: PositionedEdge = {
+      id: data.id,
+      source: data.source,
+      target: data.target,
+      startX: rscratch.startX,
+      startY: rscratch.startY,
+      midX: rscratch.midX,
+      midY: rscratch.midY,
+      endX: rscratch.endX,
+      endY: rscratch.endY,
+    };
+
+    // Add all other properties from the original data
+    Object.keys(data).forEach((key) => {
+      if (!['id', 'source', 'target'].includes(key)) {
+        positionedEdge[key] = data[key];
+      }
+    });
+
+    return positionedEdge;
+  });
+}
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/index.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/index.ts
new file mode 100644
index 000000000..9e12d38a7
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/index.ts
@@ -0,0 +1,25 @@
+import { render as renderWithCoseBilkent } from './render.js';
+
+/**
+ * Cose-Bilkent Layout Algorithm for Generic Diagrams
+ *
+ * This module provides a layout algorithm implementation using Cytoscape
+ * with the cose-bilkent algorithm for positioning nodes and edges.
+ *
+ * The algorithm follows the unified rendering pattern and can be used
+ * by any diagram type that provides compatible LayoutData.
+ */
+
+/**
+ * Render function for the cose-bilkent layout algorithm
+ *
+ * This function follows the unified rendering pattern used by all layout algorithms.
+ * It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm,
+ * and renders the positioned elements to the SVG.
+ *
+ * @param layoutData - Layout data containing nodes, edges, and configuration
+ * @param svg - SVG element to render to
+ * @param helpers - Internal helper functions for rendering
+ * @param options - Rendering options
+ */
+export const render = renderWithCoseBilkent;
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.test.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.test.ts
new file mode 100644
index 000000000..1f8416f35
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.test.ts
@@ -0,0 +1,250 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+// Mock cytoscape and cytoscape-cose-bilkent before importing the modules
+vi.mock('cytoscape', () => {
+  const mockCy = {
+    add: vi.fn(),
+    nodes: vi.fn(() => ({
+      forEach: vi.fn(),
+      map: vi.fn((fn) => [
+        fn({
+          data: () => ({
+            id: '1',
+            nodeId: '1',
+            labelText: 'Root',
+            level: 0,
+            type: 0,
+            width: 100,
+            height: 50,
+            padding: 10,
+          }),
+          position: () => ({ x: 100, y: 100 }),
+        }),
+      ]),
+    })),
+    edges: vi.fn(() => ({
+      map: vi.fn((fn) => [
+        fn({
+          data: () => ({
+            id: '1_2',
+            source: '1',
+            target: '2',
+            depth: 0,
+          }),
+          _private: {
+            rscratch: {
+              startX: 100,
+              startY: 100,
+              midX: 150,
+              midY: 150,
+              endX: 200,
+              endY: 200,
+            },
+          },
+        }),
+      ]),
+    })),
+    layout: vi.fn(() => ({
+      run: vi.fn(),
+    })),
+    ready: vi.fn((callback) => callback({})),
+  };
+
+  const mockCytoscape = vi.fn(() => mockCy);
+  mockCytoscape.use = vi.fn();
+
+  return {
+    default: mockCytoscape,
+  };
+});
+
+vi.mock('cytoscape-cose-bilkent', () => ({
+  default: vi.fn(),
+}));
+
+vi.mock('d3', () => ({
+  select: vi.fn(() => ({
+    append: vi.fn(() => ({
+      attr: vi.fn(() => ({
+        attr: vi.fn(() => ({
+          remove: vi.fn(),
+        })),
+      })),
+    })),
+  })),
+}));
+
+// Import modules after mocks
+import { layout, validateLayoutData } from './index.js';
+import type { MindmapLayoutData, LayoutResult } from './types.js';
+import type { MindmapNode } from '../../../diagrams/mindmap/mindmapTypes.js';
+import type { MermaidConfig } from '../../../config.type.js';
+
+describe('Cose-Bilkent Layout Algorithm', () => {
+  let mockConfig: MermaidConfig;
+  let mockRootNode: MindmapNode;
+  let mockLayoutData: MindmapLayoutData;
+
+  beforeEach(() => {
+    mockConfig = {
+      mindmap: {
+        layoutAlgorithm: 'cose-bilkent',
+        padding: 10,
+        maxNodeWidth: 200,
+        useMaxWidth: true,
+      },
+    } as MermaidConfig;
+
+    mockRootNode = {
+      id: 1,
+      nodeId: '1',
+      level: 0,
+      descr: 'Root',
+      type: 0,
+      width: 100,
+      height: 50,
+      padding: 10,
+      x: 0,
+      y: 0,
+      children: [
+        {
+          id: 2,
+          nodeId: '2',
+          level: 1,
+          descr: 'Child 1',
+          type: 0,
+          width: 80,
+          height: 40,
+          padding: 10,
+          x: 0,
+          y: 0,
+        },
+      ],
+    } as MindmapNode;
+
+    mockLayoutData = {
+      nodes: [
+        {
+          id: '1',
+          nodeId: '1',
+          level: 0,
+          descr: 'Root',
+          type: 0,
+          width: 100,
+          height: 50,
+          padding: 10,
+        },
+        {
+          id: '2',
+          nodeId: '2',
+          level: 1,
+          descr: 'Child 1',
+          type: 0,
+          width: 80,
+          height: 40,
+          padding: 10,
+        },
+      ],
+      edges: [
+        {
+          id: '1_2',
+          source: '1',
+          target: '2',
+          depth: 0,
+        },
+      ],
+      config: mockConfig,
+      rootNode: mockRootNode,
+    };
+  });
+
+  describe('validateLayoutData', () => {
+    it('should validate correct layout data', () => {
+      expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
+    });
+
+    it('should throw error for missing data', () => {
+      expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
+    });
+
+    it('should throw error for missing root node', () => {
+      const invalidData = { ...mockLayoutData, rootNode: null as any };
+      expect(() => validateLayoutData(invalidData)).toThrow('Root node is required');
+    });
+
+    it('should throw error for missing config', () => {
+      const invalidData = { ...mockLayoutData, config: null as any };
+      expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
+    });
+
+    it('should throw error for invalid nodes array', () => {
+      const invalidData = { ...mockLayoutData, nodes: null as any };
+      expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required');
+    });
+
+    it('should throw error for invalid edges array', () => {
+      const invalidData = { ...mockLayoutData, edges: null as any };
+      expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
+    });
+  });
+
+  describe('layout function', () => {
+    it('should execute layout algorithm successfully', async () => {
+      const result: LayoutResult = await layout(mockLayoutData, mockConfig);
+
+      expect(result).toBeDefined();
+      expect(result.nodes).toBeDefined();
+      expect(result.edges).toBeDefined();
+      expect(Array.isArray(result.nodes)).toBe(true);
+      expect(Array.isArray(result.edges)).toBe(true);
+    });
+
+    it('should return positioned nodes with coordinates', async () => {
+      const result: LayoutResult = await layout(mockLayoutData, mockConfig);
+
+      expect(result.nodes.length).toBeGreaterThan(0);
+      result.nodes.forEach((node) => {
+        expect(node.x).toBeDefined();
+        expect(node.y).toBeDefined();
+        expect(typeof node.x).toBe('number');
+        expect(typeof node.y).toBe('number');
+      });
+    });
+
+    it('should return positioned edges with coordinates', async () => {
+      const result: LayoutResult = await layout(mockLayoutData, mockConfig);
+
+      expect(result.edges.length).toBeGreaterThan(0);
+      result.edges.forEach((edge) => {
+        expect(edge.startX).toBeDefined();
+        expect(edge.startY).toBeDefined();
+        expect(edge.midX).toBeDefined();
+        expect(edge.midY).toBeDefined();
+        expect(edge.endX).toBeDefined();
+        expect(edge.endY).toBeDefined();
+      });
+    });
+
+    it('should handle empty mindmap data gracefully', async () => {
+      const emptyData: MindmapLayoutData = {
+        nodes: [],
+        edges: [],
+        config: mockConfig,
+        rootNode: mockRootNode,
+      };
+
+      const result: LayoutResult = await layout(emptyData, mockConfig);
+      expect(result).toBeDefined();
+      expect(result.nodes).toBeDefined();
+      expect(result.edges).toBeDefined();
+      expect(Array.isArray(result.nodes)).toBe(true);
+      expect(Array.isArray(result.edges)).toBe(true);
+    });
+
+    it('should throw error for invalid data', async () => {
+      const invalidData = { ...mockLayoutData, rootNode: null as any };
+
+      await expect(layout(invalidData, mockConfig)).rejects.toThrow();
+    });
+  });
+});
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.ts
new file mode 100644
index 000000000..ba4ec0e12
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.ts
@@ -0,0 +1,79 @@
+import type { MermaidConfig } from '../../../config.type.js';
+import { log } from '../../../logger.js';
+import type { LayoutData } from '../../types.js';
+import type { LayoutResult } from './types.js';
+import {
+  createCytoscapeInstance,
+  extractPositionedNodes,
+  extractPositionedEdges,
+} from './cytoscape-setup.js';
+
+/**
+ * Execute the cose-bilkent layout algorithm on generic layout data
+ *
+ * This function takes layout data and uses Cytoscape with the cose-bilkent
+ * algorithm to calculate optimal node positions and edge paths.
+ *
+ * @param data - The layout data containing nodes, edges, and configuration
+ * @param config - Mermaid configuration object
+ * @returns Promise resolving to layout result with positioned nodes and edges
+ */
+export async function executeCoseBilkentLayout(
+  data: LayoutData,
+  _config: MermaidConfig
+): Promise {
+  log.debug('Starting cose-bilkent layout algorithm');
+
+  try {
+    // Validate input data
+    if (!data.nodes || !Array.isArray(data.nodes)) {
+      throw new Error('No nodes found in layout data');
+    }
+
+    if (!data.edges || !Array.isArray(data.edges)) {
+      throw new Error('No edges found in layout data');
+    }
+
+    // Create and configure cytoscape instance
+    const cy = await createCytoscapeInstance(data);
+
+    // Extract positioned nodes and edges after layout
+    const positionedNodes = extractPositionedNodes(cy);
+    const positionedEdges = extractPositionedEdges(cy);
+
+    log.debug(`Layout completed: ${positionedNodes.length} nodes, ${positionedEdges.length} edges`);
+
+    return {
+      nodes: positionedNodes,
+      edges: positionedEdges,
+    };
+  } catch (error) {
+    log.error('Error in cose-bilkent layout algorithm:', error);
+    throw error;
+  }
+}
+
+/**
+ * Validate layout data structure
+ * @param data - The data to validate
+ * @returns True if data is valid, throws error otherwise
+ */
+export function validateLayoutData(data: LayoutData): boolean {
+  if (!data) {
+    throw new Error('Layout data is required');
+  }
+
+  if (!data.config) {
+    throw new Error('Configuration is required in layout data');
+  }
+
+  if (!Array.isArray(data.nodes)) {
+    throw new Error('Nodes array is required in layout data');
+  }
+
+  if (!Array.isArray(data.edges)) {
+    throw new Error('Edges array is required in layout data');
+  }
+
+  return true;
+}
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/render.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/render.ts
new file mode 100644
index 000000000..1616fb781
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/render.ts
@@ -0,0 +1,183 @@
+import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
+import { executeCoseBilkentLayout } from './layout.js';
+
+type Node = LayoutData['nodes'][number];
+
+interface NodeWithPosition extends Node {
+  x?: number;
+  y?: number;
+  domId?: SVGGroup;
+}
+
+/**
+ * Render function for cose-bilkent layout algorithm
+ *
+ * This follows the same pattern as ELK and dagre renderers:
+ * 1. Insert nodes into DOM to get their actual dimensions
+ * 2. Run the layout algorithm to calculate positions
+ * 3. Position the nodes and edges based on layout results
+ */
+export const render = async (
+  data4Layout: LayoutData,
+  svg: SVG,
+  {
+    insertCluster,
+    insertEdge,
+    insertEdgeLabel,
+    insertMarkers,
+    insertNode,
+    log,
+    positionEdgeLabel,
+  }: InternalHelpers,
+  { algorithm }: RenderOptions
+) => {
+  const nodeDb: Record = {};
+  const clusterDb: Record = {};
+
+  // Insert markers for edges
+  const element = svg.select('g');
+  insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
+
+  // Create container groups
+  const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
+  const edgePaths = element.insert('g').attr('class', 'edgePaths');
+  const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
+  const nodes = element.insert('g').attr('class', 'nodes');
+
+  // Step 1: Insert nodes into DOM to get their actual dimensions
+  log.debug('Inserting nodes into DOM for dimension calculation');
+
+  await Promise.all(
+    data4Layout.nodes.map(async (node) => {
+      if (node.isGroup) {
+        // Handle subgraphs/clusters
+        const clusterNode: NodeWithPosition = { ...node };
+        clusterDb[node.id] = clusterNode;
+        nodeDb[node.id] = clusterNode;
+
+        // Insert cluster to get dimensions
+        await insertCluster(subGraphsEl, node);
+      } else {
+        // Handle regular nodes
+        const nodeWithPosition: NodeWithPosition = { ...node };
+        nodeDb[node.id] = nodeWithPosition;
+
+        // Insert node to get actual dimensions
+        const nodeEl = await insertNode(nodes, node, {
+          config: data4Layout.config,
+          dir: data4Layout.direction || 'TB',
+        });
+
+        // Get the actual bounding box after insertion
+        const boundingBox = nodeEl.node()!.getBBox();
+        nodeWithPosition.width = boundingBox.width;
+        nodeWithPosition.height = boundingBox.height;
+        nodeWithPosition.domId = nodeEl;
+
+        log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
+      }
+    })
+  );
+
+  // Step 2: Run the cose-bilkent layout algorithm
+  log.debug('Running cose-bilkent layout algorithm');
+
+  // Update the layout data with actual dimensions
+  const updatedLayoutData = {
+    ...data4Layout,
+    nodes: data4Layout.nodes.map((node) => {
+      const nodeWithDimensions = nodeDb[node.id];
+      return {
+        ...node,
+        width: nodeWithDimensions.width,
+        height: nodeWithDimensions.height,
+      };
+    }),
+  };
+
+  const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config);
+
+  // Step 3: Position the nodes based on layout results
+  log.debug('Positioning nodes based on layout results');
+
+  layoutResult.nodes.forEach((positionedNode) => {
+    const node = nodeDb[positionedNode.id];
+    if (node && node.domId) {
+      // Position the node at the calculated coordinates
+      // The positionedNode.x/y represents the center of the node, so use directly
+      node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
+
+      // Store the final position
+      node.x = positionedNode.x;
+      node.y = positionedNode.y;
+
+      log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`);
+    }
+  });
+
+  // Step 4: Insert and position edges
+  log.debug('Inserting and positioning edges');
+
+  await Promise.all(
+    data4Layout.edges.map(async (edge) => {
+      // Insert edge label first
+      const edgeLabel = await insertEdgeLabel(edgeLabels, edge);
+
+      // Get start and end nodes
+      const startNode = nodeDb[edge.start];
+      const endNode = nodeDb[edge.end];
+
+      if (startNode && endNode) {
+        // Find the positioned edge data
+        const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
+
+        if (positionedEdge) {
+          // Create edge path with positioned coordinates
+          const edgeWithPath = {
+            ...edge,
+            points: [
+              { x: positionedEdge.startX, y: positionedEdge.startY },
+              { x: positionedEdge.endX, y: positionedEdge.endY },
+            ],
+          };
+
+          // Insert the edge path
+          const paths = insertEdge(
+            edgePaths,
+            edgeWithPath,
+            clusterDb,
+            data4Layout.type,
+            startNode,
+            endNode,
+            data4Layout.diagramId
+          );
+
+          // Position the edge label
+          positionEdgeLabel(edgeWithPath, paths);
+        } else {
+          // Fallback: create a simple straight line between nodes
+          const edgeWithPath = {
+            ...edge,
+            points: [
+              { x: startNode.x || 0, y: startNode.y || 0 },
+              { x: endNode.x || 0, y: endNode.y || 0 },
+            ],
+          };
+
+          const paths = insertEdge(
+            edgePaths,
+            edgeWithPath,
+            clusterDb,
+            data4Layout.type,
+            startNode,
+            endNode,
+            data4Layout.diagramId
+          );
+          positionEdgeLabel(edgeWithPath, paths);
+        }
+      }
+    })
+  );
+
+  log.debug('Cose-bilkent rendering completed');
+};
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/types.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/types.ts
new file mode 100644
index 000000000..fade24682
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/types.ts
@@ -0,0 +1,43 @@
+/**
+ * Positioned node after layout calculation
+ */
+export interface PositionedNode {
+  id: string;
+  x: number;
+  y: number;
+  [key: string]: unknown; // Allow additional properties
+}
+
+/**
+ * Positioned edge after layout calculation
+ */
+export interface PositionedEdge {
+  id: string;
+  source: string;
+  target: string;
+  startX: number;
+  startY: number;
+  midX: number;
+  midY: number;
+  endX: number;
+  endY: number;
+  [key: string]: unknown; // Allow additional properties
+}
+
+/**
+ * Result of layout algorithm execution
+ */
+export interface LayoutResult {
+  nodes: PositionedNode[];
+  edges: PositionedEdge[];
+}
+
+/**
+ * Cytoscape layout configuration
+ */
+export interface CytoscapeLayoutConfig {
+  name: 'cose-bilkent';
+  quality: 'proof';
+  styleEnabled: boolean;
+  animate: boolean;
+}
diff --git a/packages/mermaid/src/rendering-util/render.ts b/packages/mermaid/src/rendering-util/render.ts
index 22a7b96b0..7d4ee84d5 100644
--- a/packages/mermaid/src/rendering-util/render.ts
+++ b/packages/mermaid/src/rendering-util/render.ts
@@ -43,6 +43,10 @@ const registerDefaultLayoutLoaders = () => {
       name: 'cose-bilkent',
       loader: async () => await import('./layout-algorithms/cose-bilkent/index.js'),
     },
+    {
+      name: 'tidy-tree',
+      loader: async () => await import('./layout-algorithms/tidy-tree/index.js'),
+    },
   ]);
 };
 
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 40039466f..3c13b7f92 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -265,6 +265,9 @@ importers:
       marked:
         specifier: ^15.0.7
         version: 15.0.7
+      non-layered-tidy-tree-layout:
+        specifier: ^2.0.2
+        version: 2.0.2
       roughjs:
         specifier: ^4.6.6
         version: 4.6.6(patch_hash=3543d47108cb41b68ec6a671c0e1f9d0cfe2ce524fea5b0992511ae84c3c6b64)
@@ -508,6 +511,67 @@ importers:
         specifier: ^7.3.0
         version: 7.3.0
 
+  packages/mermaid/src/vitepress:
+    dependencies:
+      '@mdi/font':
+        specifier: ^7.4.47
+        version: 7.4.47
+      '@vueuse/core':
+        specifier: ^12.7.0
+        version: 12.7.0(typescript@5.7.3)
+      font-awesome:
+        specifier: ^4.7.0
+        version: 4.7.0
+      jiti:
+        specifier: ^2.4.2
+        version: 2.4.2
+      mermaid:
+        specifier: workspace:^
+        version: link:../..
+      vue:
+        specifier: ^3.4.38
+        version: 3.5.13(typescript@5.7.3)
+    devDependencies:
+      '@iconify-json/carbon':
+        specifier: ^1.1.37
+        version: 1.2.1
+      '@unocss/reset':
+        specifier: ^66.0.0
+        version: 66.0.0
+      '@vite-pwa/vitepress':
+        specifier: ^0.5.3
+        version: 0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))
+      '@vitejs/plugin-vue':
+        specifier: ^5.0.5
+        version: 5.2.1(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
+      fast-glob:
+        specifier: ^3.3.3
+        version: 3.3.3
+      https-localhost:
+        specifier: ^4.7.1
+        version: 4.7.1
+      pathe:
+        specifier: ^2.0.3
+        version: 2.0.3
+      unocss:
+        specifier: ^66.0.0
+        version: 66.0.0(postcss@8.5.3)(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
+      unplugin-vue-components:
+        specifier: ^28.4.0
+        version: 28.4.0(@babel/parser@7.27.2)(vue@3.5.13(typescript@5.7.3))
+      vite:
+        specifier: ^6.1.1
+        version: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
+      vite-plugin-pwa:
+        specifier: ^0.21.1
+        version: 0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
+      vitepress:
+        specifier: 1.6.3
+        version: 1.6.3(@algolia/client-search@5.20.3)(@types/node@22.13.5)(axios@1.8.4)(postcss@8.5.3)(search-insights@2.17.2)(terser@5.39.0)(typescript@5.7.3)
+      workbox-window:
+        specifier: ^7.3.0
+        version: 7.3.0
+
   packages/parser:
     dependencies:
       langium:
@@ -912,10 +976,6 @@ packages:
     resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-string-parser@7.25.9':
-    resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/helper-string-parser@7.27.1':
     resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
     engines: {node: '>=6.9.0'}
@@ -3627,6 +3687,15 @@ packages:
     peerDependencies:
       vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
 
+  '@vite-pwa/vitepress@0.5.4':
+    resolution: {integrity: sha512-g57qwG983WTyQNLnOcDVPQEIeN+QDgK/HdqghmygiUFp3a/MzVvmLXC/EVnPAXxWa8W2g9pZ9lE3EiDGs2HjsA==}
+    peerDependencies:
+      '@vite-pwa/assets-generator': ^0.2.6
+      vite-plugin-pwa: '>=0.21.2 <1'
+    peerDependenciesMeta:
+      '@vite-pwa/assets-generator':
+        optional: true
+
   '@vite-pwa/vitepress@1.0.0':
     resolution: {integrity: sha512-i5RFah4urA6tZycYlGyBslVx8cVzbZBcARJLDg5rWMfAkRmyLtpRU6usGfVOwyN9kjJ2Bkm+gBHXF1hhr7HptQ==}
     peerDependencies:
@@ -4372,10 +4441,6 @@ packages:
     resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
     engines: {node: '>= 0.4'}
 
-  call-bound@1.0.3:
-    resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==}
-    engines: {node: '>= 0.4'}
-
   call-bound@1.0.4:
     resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
     engines: {node: '>= 0.4'}
@@ -6082,10 +6147,6 @@ packages:
     resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
     engines: {node: '>=18'}
 
-  get-intrinsic@1.2.7:
-    resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==}
-    engines: {node: '>= 0.4'}
-
   get-intrinsic@1.3.0:
     resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
     engines: {node: '>= 0.4'}
@@ -7517,10 +7578,6 @@ packages:
     resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
     engines: {node: '>= 0.6'}
 
-  mime-db@1.53.0:
-    resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==}
-    engines: {node: '>= 0.6'}
-
   mime-db@1.54.0:
     resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
     engines: {node: '>= 0.6'}
@@ -7714,6 +7771,9 @@ packages:
     resolution: {integrity: sha512-fiVbT7BqxiQqjlR9U3FDGOSERFCKoXVCdxV2FwZuNN7/cmJ42iQx35nUFOAFDcyvemu9Adp+IlsCGlKQYLmBKw==}
     deprecated: Package no longer supported. Contact support@npmjs.com for more info.
 
+  non-layered-tidy-tree-layout@2.0.2:
+    resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
+
   normalize-path@3.0.0:
     resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
     engines: {node: '>=0.10.0'}
@@ -9594,6 +9654,18 @@ packages:
     peerDependencies:
       vite: '>=4 <=6'
 
+  vite-plugin-pwa@0.21.2:
+    resolution: {integrity: sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg==}
+    engines: {node: '>=16.0.0'}
+    peerDependencies:
+      '@vite-pwa/assets-generator': ^0.2.6
+      vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+      workbox-build: ^7.3.0
+      workbox-window: ^7.3.0
+    peerDependenciesMeta:
+      '@vite-pwa/assets-generator':
+        optional: true
+
   vite-plugin-pwa@1.0.0:
     resolution: {integrity: sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==}
     engines: {node: '>=16.0.0'}
@@ -10648,7 +10720,7 @@ snapshots:
 
   '@babel/code-frame@7.26.2':
     dependencies:
-      '@babel/helper-validator-identifier': 7.25.9
+      '@babel/helper-validator-identifier': 7.27.1
       js-tokens: 4.0.0
       picocolors: 1.1.1
 
@@ -10670,10 +10742,10 @@ snapshots:
       '@babel/helper-compilation-targets': 7.26.5
       '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.9)
       '@babel/helpers': 7.26.9
-      '@babel/parser': 7.26.9
+      '@babel/parser': 7.27.2
       '@babel/template': 7.26.9
       '@babel/traverse': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
       convert-source-map: 2.0.0
       debug: 4.4.0(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
@@ -10704,8 +10776,8 @@ snapshots:
 
   '@babel/generator@7.26.9':
     dependencies:
-      '@babel/parser': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/parser': 7.27.2
+      '@babel/types': 7.27.1
       '@jridgewell/gen-mapping': 0.3.8
       '@jridgewell/trace-mapping': 0.3.25
       jsesc: 3.1.0
@@ -10810,7 +10882,7 @@ snapshots:
   '@babel/helper-module-imports@7.25.9':
     dependencies:
       '@babel/traverse': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
     transitivePeerDependencies:
       - supports-color
 
@@ -10825,7 +10897,7 @@ snapshots:
     dependencies:
       '@babel/core': 7.26.9
       '@babel/helper-module-imports': 7.25.9
-      '@babel/helper-validator-identifier': 7.25.9
+      '@babel/helper-validator-identifier': 7.27.1
       '@babel/traverse': 7.26.9
     transitivePeerDependencies:
       - supports-color
@@ -10901,8 +10973,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/helper-string-parser@7.25.9': {}
-
   '@babel/helper-string-parser@7.27.1': {}
 
   '@babel/helper-validator-identifier@7.25.9': {}
@@ -10924,7 +10994,7 @@ snapshots:
   '@babel/helpers@7.26.9':
     dependencies:
       '@babel/template': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
 
   '@babel/helpers@7.27.1':
     dependencies:
@@ -10933,7 +11003,7 @@ snapshots:
 
   '@babel/parser@7.26.9':
     dependencies:
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
 
   '@babel/parser@7.27.2':
     dependencies:
@@ -11945,8 +12015,8 @@ snapshots:
   '@babel/template@7.26.9':
     dependencies:
       '@babel/code-frame': 7.26.2
-      '@babel/parser': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/parser': 7.27.2
+      '@babel/types': 7.27.1
 
   '@babel/template@7.27.2':
     dependencies:
@@ -11958,9 +12028,9 @@ snapshots:
     dependencies:
       '@babel/code-frame': 7.26.2
       '@babel/generator': 7.26.9
-      '@babel/parser': 7.26.9
+      '@babel/parser': 7.27.2
       '@babel/template': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
       debug: 4.4.0(supports-color@8.1.1)
       globals: 11.12.0
     transitivePeerDependencies:
@@ -11980,8 +12050,8 @@ snapshots:
 
   '@babel/types@7.26.9':
     dependencies:
-      '@babel/helper-string-parser': 7.25.9
-      '@babel/helper-validator-identifier': 7.25.9
+      '@babel/helper-string-parser': 7.27.1
+      '@babel/helper-validator-identifier': 7.27.1
 
   '@babel/types@7.27.1':
     dependencies:
@@ -13634,24 +13704,24 @@ snapshots:
 
   '@types/babel__core@7.20.5':
     dependencies:
-      '@babel/parser': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/parser': 7.27.2
+      '@babel/types': 7.27.1
       '@types/babel__generator': 7.6.8
       '@types/babel__template': 7.4.4
       '@types/babel__traverse': 7.20.6
 
   '@types/babel__generator@7.6.8':
     dependencies:
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
 
   '@types/babel__template@7.4.4':
     dependencies:
-      '@babel/parser': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/parser': 7.27.2
+      '@babel/types': 7.27.1
 
   '@types/babel__traverse@7.20.6':
     dependencies:
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
 
   '@types/body-parser@1.19.5':
     dependencies:
@@ -14191,6 +14261,16 @@ snapshots:
     transitivePeerDependencies:
       - vue
 
+  '@unocss/astro@66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))':
+    dependencies:
+      '@unocss/core': 66.0.0
+      '@unocss/reset': 66.0.0
+      '@unocss/vite': 66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
+    optionalDependencies:
+      vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
+    transitivePeerDependencies:
+      - vue
+
   '@unocss/cli@66.0.0':
     dependencies:
       '@ampproject/remapping': 2.3.0
@@ -14326,6 +14406,24 @@ snapshots:
     transitivePeerDependencies:
       - vue
 
+  '@unocss/vite@66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))':
+    dependencies:
+      '@ampproject/remapping': 2.3.0
+      '@unocss/config': 66.0.0
+      '@unocss/core': 66.0.0
+      '@unocss/inspector': 66.0.0(vue@3.5.13(typescript@5.7.3))
+      chokidar: 3.6.0
+      magic-string: 0.30.17
+      tinyglobby: 0.2.12
+      unplugin-utils: 0.2.4
+      vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
+    transitivePeerDependencies:
+      - vue
+
+  '@vite-pwa/vitepress@0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
+    dependencies:
+      vite-plugin-pwa: 0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
+
   '@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
     dependencies:
       vite-plugin-pwa: 1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
@@ -14340,6 +14438,11 @@ snapshots:
       vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
       vue: 3.5.13(typescript@5.7.3)
 
+  '@vitejs/plugin-vue@5.2.1(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))':
+    dependencies:
+      vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
+      vue: 3.5.13(typescript@5.7.3)
+
   '@vitest/coverage-v8@3.0.6(vitest@3.0.6)':
     dependencies:
       '@ampproject/remapping': 2.3.0
@@ -14418,7 +14521,7 @@ snapshots:
 
   '@vue/compiler-core@3.5.13':
     dependencies:
-      '@babel/parser': 7.26.9
+      '@babel/parser': 7.27.2
       '@vue/shared': 3.5.13
       entities: 4.5.0
       estree-walker: 2.0.2
@@ -14431,7 +14534,7 @@ snapshots:
 
   '@vue/compiler-sfc@3.5.13':
     dependencies:
-      '@babel/parser': 7.26.9
+      '@babel/parser': 7.27.2
       '@vue/compiler-core': 3.5.13
       '@vue/compiler-dom': 3.5.13
       '@vue/compiler-ssr': 3.5.13
@@ -14884,7 +14987,7 @@ snapshots:
 
   array-buffer-byte-length@1.0.2:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       is-array-buffer: 3.0.5
 
   array-flatten@1.1.1: {}
@@ -14989,7 +15092,7 @@ snapshots:
   babel-plugin-jest-hoist@29.6.3:
     dependencies:
       '@babel/template': 7.26.9
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
       '@types/babel__core': 7.20.5
       '@types/babel__traverse': 7.20.6
 
@@ -15233,14 +15336,9 @@ snapshots:
     dependencies:
       call-bind-apply-helpers: 1.0.2
       es-define-property: 1.0.1
-      get-intrinsic: 1.2.7
+      get-intrinsic: 1.3.0
       set-function-length: 1.2.2
 
-  call-bound@1.0.3:
-    dependencies:
-      call-bind-apply-helpers: 1.0.2
-      get-intrinsic: 1.2.7
-
   call-bound@1.0.4:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -15508,7 +15606,7 @@ snapshots:
 
   compressible@2.0.18:
     dependencies:
-      mime-db: 1.53.0
+      mime-db: 1.54.0
 
   compression@1.7.4:
     dependencies:
@@ -16175,7 +16273,7 @@ snapshots:
       array-buffer-byte-length: 1.0.2
       call-bind: 1.0.8
       es-get-iterator: 1.1.3
-      get-intrinsic: 1.2.7
+      get-intrinsic: 1.3.0
       is-arguments: 1.1.1
       is-array-buffer: 3.0.5
       is-date-object: 1.1.0
@@ -16498,7 +16596,7 @@ snapshots:
   es-get-iterator@1.1.3:
     dependencies:
       call-bind: 1.0.8
-      get-intrinsic: 1.2.7
+      get-intrinsic: 1.3.0
       has-symbols: 1.1.0
       is-arguments: 1.1.1
       is-map: 2.0.3
@@ -16518,7 +16616,7 @@ snapshots:
   es-set-tostringtag@2.1.0:
     dependencies:
       es-errors: 1.3.0
-      get-intrinsic: 1.2.7
+      get-intrinsic: 1.3.0
       has-tostringtag: 1.0.2
       hasown: 2.0.2
 
@@ -17214,7 +17312,7 @@ snapshots:
 
   find-test-names@1.29.5(@babel/core@7.26.9):
     dependencies:
-      '@babel/parser': 7.26.9
+      '@babel/parser': 7.27.2
       '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.26.9)
       acorn-walk: 8.3.4
       debug: 4.4.0(supports-color@8.1.1)
@@ -17376,19 +17474,6 @@ snapshots:
 
   get-east-asian-width@1.3.0: {}
 
-  get-intrinsic@1.2.7:
-    dependencies:
-      call-bind-apply-helpers: 1.0.2
-      es-define-property: 1.0.1
-      es-errors: 1.3.0
-      es-object-atoms: 1.1.1
-      function-bind: 1.1.2
-      get-proto: 1.0.1
-      gopd: 1.2.0
-      has-symbols: 1.1.0
-      hasown: 2.0.2
-      math-intrinsics: 1.1.0
-
   get-intrinsic@1.3.0:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -17842,8 +17927,8 @@ snapshots:
   is-array-buffer@3.0.5:
     dependencies:
       call-bind: 1.0.8
-      call-bound: 1.0.3
-      get-intrinsic: 1.2.7
+      call-bound: 1.0.4
+      get-intrinsic: 1.3.0
 
   is-arrayish@0.2.1: {}
 
@@ -17867,7 +17952,7 @@ snapshots:
 
   is-boolean-object@1.2.2:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       has-tostringtag: 1.0.2
 
   is-builtin-module@5.0.0:
@@ -17888,7 +17973,7 @@ snapshots:
 
   is-date-object@1.1.0:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       has-tostringtag: 1.0.2
 
   is-decimal@1.0.4: {}
@@ -17937,7 +18022,7 @@ snapshots:
 
   is-number-object@1.1.1:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       has-tostringtag: 1.0.2
 
   is-number@7.0.0: {}
@@ -17960,7 +18045,7 @@ snapshots:
 
   is-regex@1.2.1:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       gopd: 1.2.0
       has-tostringtag: 1.0.2
       hasown: 2.0.2
@@ -17971,7 +18056,7 @@ snapshots:
 
   is-shared-array-buffer@1.0.4:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
 
   is-stream@1.1.0: {}
 
@@ -17981,7 +18066,7 @@ snapshots:
 
   is-string@1.1.1:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       has-tostringtag: 1.0.2
 
   is-subdir@1.2.0:
@@ -17990,7 +18075,7 @@ snapshots:
 
   is-symbol@1.1.1:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       has-symbols: 1.1.0
       safe-regex-test: 1.1.0
 
@@ -18015,7 +18100,7 @@ snapshots:
   is-weakset@2.0.3:
     dependencies:
       call-bind: 1.0.8
-      get-intrinsic: 1.2.7
+      get-intrinsic: 1.3.0
 
   is-what@4.1.16: {}
 
@@ -18053,7 +18138,7 @@ snapshots:
   istanbul-lib-instrument@5.2.1:
     dependencies:
       '@babel/core': 7.26.9
-      '@babel/parser': 7.26.9
+      '@babel/parser': 7.27.2
       '@istanbuljs/schema': 0.1.3
       istanbul-lib-coverage: 3.2.2
       semver: 6.3.1
@@ -18063,7 +18148,7 @@ snapshots:
   istanbul-lib-instrument@6.0.3:
     dependencies:
       '@babel/core': 7.26.9
-      '@babel/parser': 7.26.9
+      '@babel/parser': 7.27.2
       '@istanbuljs/schema': 0.1.3
       istanbul-lib-coverage: 3.2.2
       semver: 7.7.1
@@ -18382,7 +18467,7 @@ snapshots:
       '@babel/generator': 7.26.9
       '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.26.9)
       '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.26.9)
-      '@babel/types': 7.26.9
+      '@babel/types': 7.27.1
       '@jest/expect-utils': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
@@ -19246,8 +19331,6 @@ snapshots:
 
   mime-db@1.52.0: {}
 
-  mime-db@1.53.0: {}
-
   mime-db@1.54.0: {}
 
   mime-types@2.1.35:
@@ -19392,13 +19475,15 @@ snapshots:
 
   node-source-walk@7.0.0:
     dependencies:
-      '@babel/parser': 7.26.9
+      '@babel/parser': 7.27.2
 
   nomnom@1.5.2:
     dependencies:
       colors: 0.5.1
       underscore: 1.1.7
 
+  non-layered-tidy-tree-layout@2.0.2: {}
+
   normalize-path@3.0.0: {}
 
   normalize-url@6.1.0: {}
@@ -19497,7 +19582,7 @@ snapshots:
   object.assign@4.1.7:
     dependencies:
       call-bind: 1.0.8
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       define-properties: 1.2.1
       es-object-atoms: 1.1.1
       has-symbols: 1.1.0
@@ -20388,7 +20473,7 @@ snapshots:
 
   safe-regex-test@1.1.0:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       es-errors: 1.3.0
       is-regex: 1.2.1
 
@@ -20524,7 +20609,7 @@ snapshots:
       define-data-property: 1.1.4
       es-errors: 1.3.0
       function-bind: 1.1.2
-      get-intrinsic: 1.2.7
+      get-intrinsic: 1.3.0
       gopd: 1.2.0
       has-property-descriptors: 1.0.2
 
@@ -20613,16 +20698,16 @@ snapshots:
 
   side-channel-map@1.0.1:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       es-errors: 1.3.0
-      get-intrinsic: 1.2.7
+      get-intrinsic: 1.3.0
       object-inspect: 1.13.4
 
   side-channel-weakmap@1.0.2:
     dependencies:
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       es-errors: 1.3.0
-      get-intrinsic: 1.2.7
+      get-intrinsic: 1.3.0
       object-inspect: 1.13.4
       side-channel-map: 1.0.1
 
@@ -21085,7 +21170,7 @@ snapshots:
   terser@5.34.1:
     dependencies:
       '@jridgewell/source-map': 0.3.6
-      acorn: 8.14.0
+      acorn: 8.14.1
       commander: 2.20.3
       source-map-support: 0.5.21
 
@@ -21455,6 +21540,33 @@ snapshots:
       - supports-color
       - vue
 
+  unocss@66.0.0(postcss@8.5.3)(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3)):
+    dependencies:
+      '@unocss/astro': 66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
+      '@unocss/cli': 66.0.0
+      '@unocss/core': 66.0.0
+      '@unocss/postcss': 66.0.0(postcss@8.5.3)
+      '@unocss/preset-attributify': 66.0.0
+      '@unocss/preset-icons': 66.0.0
+      '@unocss/preset-mini': 66.0.0
+      '@unocss/preset-tagify': 66.0.0
+      '@unocss/preset-typography': 66.0.0
+      '@unocss/preset-uno': 66.0.0
+      '@unocss/preset-web-fonts': 66.0.0
+      '@unocss/preset-wind': 66.0.0
+      '@unocss/preset-wind3': 66.0.0
+      '@unocss/transformer-attributify-jsx': 66.0.0
+      '@unocss/transformer-compile-class': 66.0.0
+      '@unocss/transformer-directives': 66.0.0
+      '@unocss/transformer-variant-group': 66.0.0
+      '@unocss/vite': 66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
+    optionalDependencies:
+      vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
+    transitivePeerDependencies:
+      - postcss
+      - supports-color
+      - vue
+
   unpipe@1.0.0: {}
 
   unplugin-utils@0.2.4:
@@ -21480,7 +21592,7 @@ snapshots:
 
   unplugin@2.2.0:
     dependencies:
-      acorn: 8.14.0
+      acorn: 8.14.1
       webpack-virtual-modules: 0.6.2
 
   untildify@4.0.0: {}
@@ -21570,6 +21682,17 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  vite-plugin-pwa@0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0):
+    dependencies:
+      debug: 4.4.0(supports-color@8.1.1)
+      pretty-bytes: 6.1.1
+      tinyglobby: 0.2.12
+      vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
+      workbox-build: 7.1.1(@types/babel__core@7.20.5)
+      workbox-window: 7.3.0
+    transitivePeerDependencies:
+      - supports-color
+
   vite-plugin-pwa@1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0):
     dependencies:
       debug: 4.4.0(supports-color@8.1.1)
@@ -22035,7 +22158,7 @@ snapshots:
     dependencies:
       available-typed-arrays: 1.0.7
       call-bind: 1.0.8
-      call-bound: 1.0.3
+      call-bound: 1.0.4
       for-each: 0.3.5
       gopd: 1.2.0
       has-tostringtag: 1.0.2

From d2463f41b52feaaa287471f43952696ffb496e61 Mon Sep 17 00:00:00 2001
From: Knut Sveidqvist 
Date: Wed, 11 Jun 2025 18:04:21 +0200
Subject: [PATCH 011/314] Tidy-tree WIP

---
 cypress/platform/knsv2.html                   |   1 +
 instructions.md                               | 131 +++++++
 .../tidy-tree/cytoscape-setup.test.ts         | 281 --------------
 .../tidy-tree/cytoscape-setup.ts              | 207 ----------
 .../layout-algorithms/tidy-tree/index.ts      |  38 +-
 .../tidy-tree/layout.test.ts                  | 362 +++++++++++-------
 .../layout-algorithms/tidy-tree/layout.ts     | 352 +++++++++++++++--
 .../layout-algorithms/tidy-tree/render.ts     |  72 ++--
 .../layout-algorithms/tidy-tree/types.ts      |  25 +-
 9 files changed, 769 insertions(+), 700 deletions(-)
 create mode 100644 instructions.md
 delete mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.test.ts
 delete mode 100644 packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/cytoscape-setup.ts

diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html
index 431e139a5..f51da5039 100644
--- a/cypress/platform/knsv2.html
+++ b/cypress/platform/knsv2.html
@@ -116,6 +116,7 @@
         Tools
           Pen and paper
           Mermaid
+        Third
     
       ---
diff --git a/instructions.md b/instructions.md
new file mode 100644
index 000000000..d30b7ea8d
--- /dev/null
+++ b/instructions.md
@@ -0,0 +1,131 @@
+Please help me to implement the tidy-tree algorithm.
+* I have added a placeholder with the correct signature in `packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/`.
+
+Replace the cytoscape layout with one using tidy-tree.
+
+This is the API for tidy-tree:
+```
+# non-layered-tidy-tree-layout
+
+Draw non-layered tidy trees in linear time.
+
+> This a JavaScript port from the project [cwi-swat/non-layered-tidy-trees](https://github.com/cwi-swat/non-layered-tidy-trees), which is written in Java. The algorithm used in that project is from the paper by _A.J. van der Ploeg_, [Drawing Non-layered Tidy Trees in Linear Time](http://oai.cwi.nl/oai/asset/21856/21856B.pdf). There is another JavaScript port from that project [d3-flextree](https://github.com/Klortho/d3-flextree), which depends on _d3-hierarchy_. This project is dependency free.
+
+## Getting started
+
+### Installation
+
+```
+npm install non-layered-tidy-tree-layout
+```
+
+Or
+
+```
+yarn add non-layered-tidy-tree-layout
+```
+
+There's also a built verison: `dist/non-layered-tidy-tree-layout.js` for use with browser `
+  
+
diff --git a/docs/syntax/pie.md b/docs/syntax/pie.md
index b8f452b66..87fb3fc58 100644
--- a/docs/syntax/pie.md
+++ b/docs/syntax/pie.md
@@ -37,6 +37,11 @@ Drawing a pie chart is really simple in mermaid.
   - Followed by `:` colon as separator
   - Followed by `positive numeric value` (supported up to two decimal places)
 
+**Note:**
+
+> Pie chart values must be **positive numbers greater than zero**.\
+> **Zero and negative values are not allowed** and will result in an error.
+
 \[pie] \[showData] (OPTIONAL)
 \[title] \[titlevalue] (OPTIONAL)
 "\[datakey1]" : \[dataValue1]
diff --git a/packages/mermaid/src/diagrams/pie/pie.spec.ts b/packages/mermaid/src/diagrams/pie/pie.spec.ts
index f00906cc5..2b50f5604 100644
--- a/packages/mermaid/src/diagrams/pie/pie.spec.ts
+++ b/packages/mermaid/src/diagrams/pie/pie.spec.ts
@@ -139,6 +139,24 @@ describe('pie', () => {
       }).rejects.toThrowError();
     });
 
+    it('should handle simple pie with zero slice value', async () => {
+      await expect(async () => {
+        await parser.parse(`pie
+        "ash" : 0
+        "bat" : 40.12
+        `);
+      }).rejects.toThrowError();
+    });
+
+    it('should handle simple pie with negative slice value', async () => {
+      await expect(async () => {
+        await parser.parse(`pie
+        "ash" : -60
+        "bat" : 40.12
+        `);
+      }).rejects.toThrowError();
+    });
+
     it('should handle unsafe properties', async () => {
       await expect(
         parser.parse(`pie title Unsafe props test
diff --git a/packages/mermaid/src/diagrams/pie/pieDb.ts b/packages/mermaid/src/diagrams/pie/pieDb.ts
index 64831495c..87735b84e 100644
--- a/packages/mermaid/src/diagrams/pie/pieDb.ts
+++ b/packages/mermaid/src/diagrams/pie/pieDb.ts
@@ -34,6 +34,17 @@ const clear = (): void => {
 };
 
 const addSection = ({ label, value }: D3Section): void => {
+  if (value <= 0) {
+    const error: any = new Error(
+      `Section "${label}" has invalid value: ${value}. Zero and negative values are not allowed in pie charts. All slice values must be > 0`
+    );
+    error.hash = {
+      text: `pie "${label}": ${value}`,
+      token: `${value}`,
+      expected: ['a positive number (> 0)'],
+    };
+    throw error;
+  }
   if (!sections.has(label)) {
     sections.set(label, value);
     log.debug(`added new section: ${label}, with value: ${value}`);
diff --git a/packages/mermaid/src/docs/syntax/pie.md b/packages/mermaid/src/docs/syntax/pie.md
index 2e7a1799a..4ad0dc4e0 100644
--- a/packages/mermaid/src/docs/syntax/pie.md
+++ b/packages/mermaid/src/docs/syntax/pie.md
@@ -24,6 +24,11 @@ Drawing a pie chart is really simple in mermaid.
   - Followed by `:` colon as separator
   - Followed by `positive numeric value` (supported up to two decimal places)
 
+**Note:**
+
+> Pie chart values must be **positive numbers greater than zero**.  
+> **Zero and negative values are not allowed** and will result in an error.
+
 [pie] [showData] (OPTIONAL)
 [title] [titlevalue] (OPTIONAL)
 "[datakey1]" : [dataValue1]
diff --git a/packages/parser/src/language/common/common.langium b/packages/parser/src/language/common/common.langium
index b74ffc34d..e6d3d488f 100644
--- a/packages/parser/src/language/common/common.langium
+++ b/packages/parser/src/language/common/common.langium
@@ -17,8 +17,8 @@ terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}
 terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/;
 terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/;
 
-terminal FLOAT returns number: /[0-9]+\.[0-9]+(?!\.)/;
-terminal INT returns number: /0|[1-9][0-9]*(?!\.)/;
+terminal FLOAT returns number: /-?[0-9]+\.[0-9]+(?!\.)/;
+terminal INT returns number: /-?(0|[1-9][0-9]*)(?!\.)/;
 terminal NUMBER returns number: FLOAT | INT;
 
 terminal STRING returns string: /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/;

From 9da6fb39ae278401771943ac85d6d1b875f78cf1 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Thu, 12 Jun 2025 19:18:36 +0530
Subject: [PATCH 016/314] added changeset

---
 .changeset/seven-papayas-film.md | 9 +++++++++
 1 file changed, 9 insertions(+)
 create mode 100644 .changeset/seven-papayas-film.md

diff --git a/.changeset/seven-papayas-film.md b/.changeset/seven-papayas-film.md
new file mode 100644
index 000000000..109743117
--- /dev/null
+++ b/.changeset/seven-papayas-film.md
@@ -0,0 +1,9 @@
+---
+'mermaid': patch
+---
+
+Add validation for negative values in pie charts:
+
+Prevents crashes during parsing by validating values post-parsing.
+
+Provides clearer, user-friendly error messages for invalid negative inputs.

From ea60525988918dcfb110fbbf0291132af399943e Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Fri, 13 Jun 2025 19:33:43 +0530
Subject: [PATCH 017/314] remove beta suffix from xyChart

---
 cypress/integration/rendering/xyChart.spec.js |  77 ++++++------
 demos/xychart.html                            |  22 ++--
 docs/syntax/xyChart.md                        |  16 +--
 .../src/diagrams/xychart/parser/xychart.jison |   1 +
 .../xychart/parser/xychart.jison.spec.ts      | 111 +++++++++---------
 .../src/diagrams/xychart/xychartDetector.ts   |   2 +-
 packages/mermaid/src/docs/syntax/xyChart.md   |  12 +-
 packages/mermaid/src/mermaidAPI.spec.ts       |   1 +
 8 files changed, 124 insertions(+), 118 deletions(-)

diff --git a/cypress/integration/rendering/xyChart.spec.js b/cypress/integration/rendering/xyChart.spec.js
index a582355e8..d54086fa7 100644
--- a/cypress/integration/rendering/xyChart.spec.js
+++ b/cypress/integration/rendering/xyChart.spec.js
@@ -1,7 +1,7 @@
 import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
 
 describe('XY Chart', () => {
-  it('should render the simplest possible chart', () => {
+  it('should render the simplest possible xy-beta chart', () => {
     imgSnapshotTest(
       `
       xychart-beta
@@ -10,10 +10,19 @@ describe('XY Chart', () => {
       {}
     );
   });
+  it('should render the simplest possible xy chart', () => {
+    imgSnapshotTest(
+      `
+      xychart
+        line [10, 30, 20]
+      `,
+      {}
+    );
+  });
   it('Should render a complete chart', () => {
     imgSnapshotTest(
       `
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
@@ -26,7 +35,7 @@ describe('XY Chart', () => {
   it('Should render a chart without title', () => {
     imgSnapshotTest(
       `
-      xychart-beta
+      xychart
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
         bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
@@ -38,7 +47,7 @@ describe('XY Chart', () => {
   it('y-axis title not required', () => {
     imgSnapshotTest(
       `
-      xychart-beta
+      xychart
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis 4000 --> 11000
         bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
@@ -50,7 +59,7 @@ describe('XY Chart', () => {
   it('Should render a chart without y-axis with different range', () => {
     imgSnapshotTest(
       `
-      xychart-beta
+      xychart
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         bar [5000, 6000, 7500, 8200, 9500, 10500, 14000, 3200, 9200, 9900, 3400, 6000]
         line [2000, 7000, 6500, 9200, 9500, 7500, 11000, 10200, 3200, 8500, 7000, 8800]
@@ -61,7 +70,7 @@ describe('XY Chart', () => {
   it('x axis title not required', () => {
     imgSnapshotTest(
       `
-      xychart-beta
+      xychart
         x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         bar [5000, 6000, 7500, 8200, 9500, 10500, 14000, 3200, 9200, 9900, 3400, 6000]
         line [2000, 7000, 6500, 9200, 9500, 7500, 11000, 10200, 3200, 8500, 7000, 8800]
@@ -72,7 +81,7 @@ describe('XY Chart', () => {
   it('Multiple plots can be rendered', () => {
     imgSnapshotTest(
       `
-      xychart-beta
+      xychart
         line [23, 46, 77, 34]
         line [45, 32, 33, 12]
         bar [87, 54, 99, 85]
@@ -86,7 +95,7 @@ describe('XY Chart', () => {
   it('Decimals and negative numbers are supported', () => {
     imgSnapshotTest(
       `
-      xychart-beta
+      xychart
         y-axis -2.4 --> 3.5
         line [+1.3, .6, 2.4, -.34]
       `,
@@ -104,7 +113,7 @@ describe('XY Chart', () => {
           height: 20
           plotReservedSpacePercent: 100
       ---
-      xychart-beta
+      xychart
         line [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
       `,
       {}
@@ -130,7 +139,7 @@ describe('XY Chart', () => {
             showTick: false
             showAxisLine: false
       ---
-      xychart-beta
+      xychart
         bar [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
       `,
       {}
@@ -140,7 +149,7 @@ describe('XY Chart', () => {
     imgSnapshotTest(
       `
       %%{init: {"xyChart": {"width": 1000, "height": 600, "titlePadding": 5, "titleFontSize": 10, "xAxis": {"labelFontSize": "20", "labelPadding": 10, "titleFontSize": 30, "titlePadding": 20, "tickLength": 10, "tickWidth": 5},  "yAxis": {"labelFontSize": "20", "labelPadding": 10, "titleFontSize": 30, "titlePadding": 20, "tickLength": 10, "tickWidth": 5}, "plotBorderWidth": 5, "chartOrientation": "horizontal", "plotReservedSpacePercent": 60  }}}%%
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
@@ -181,7 +190,7 @@ describe('XY Chart', () => {
           plotReservedSpacePercent: 60
           showDataLabel: true
       ---
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
@@ -202,7 +211,7 @@ describe('XY Chart', () => {
           yAxis:
             showTitle: false
       ---
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
@@ -223,7 +232,7 @@ describe('XY Chart', () => {
           yAxis:
             showLabel: false
       ---
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
@@ -244,7 +253,7 @@ describe('XY Chart', () => {
           yAxis:
             showTick: false
       ---
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
@@ -265,7 +274,7 @@ describe('XY Chart', () => {
           yAxis:
             showAxisLine: false
       ---
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
@@ -294,7 +303,7 @@ describe('XY Chart', () => {
             xAxisLineColor: "#87ceeb"
             plotColorPalette: "#008000, #faba63"
       ---
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
         y-axis "Revenue (in $)" 4000 --> 11000
@@ -307,7 +316,7 @@ describe('XY Chart', () => {
   it('should use the correct distances between data points', () => {
     imgSnapshotTest(
       `
-      xychart-beta
+      xychart
         x-axis 0 --> 2
         line [0, 1, 0, 1]
         bar [1, 0, 1, 0]
@@ -325,7 +334,7 @@ describe('XY Chart', () => {
       xyChart:
         showDataLabel: true
     ---
-    xychart-beta
+    xychart
       title "Sales Revenue"
       x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
       y-axis "Revenue (in $)" 4000 --> 11000
@@ -344,7 +353,7 @@ describe('XY Chart', () => {
         showDataLabel: true
         chartOrientation: horizontal
     ---
-    xychart-beta
+    xychart
       title "Sales Revenue"
       x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
       y-axis "Revenue (in $)" 4000 --> 11000
@@ -357,7 +366,7 @@ describe('XY Chart', () => {
   it('should render vertical bar chart without labels by default', () => {
     imgSnapshotTest(
       `
-    xychart-beta
+    xychart
       title "Sales Revenue"
       x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
       y-axis "Revenue (in $)" 4000 --> 11000
@@ -375,7 +384,7 @@ describe('XY Chart', () => {
       xyChart:
         chartOrientation: horizontal
     ---
-    xychart-beta
+    xychart
       title "Sales Revenue"
       x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
       y-axis "Revenue (in $)" 4000 --> 11000
@@ -393,7 +402,7 @@ describe('XY Chart', () => {
       xyChart:
         showDataLabel: true
     ---
-      xychart-beta
+      xychart
         title "Multiple Bar Plots"
         x-axis Categories [A, B, C]
         y-axis "Values" 0 --> 100
@@ -412,7 +421,7 @@ describe('XY Chart', () => {
         showDataLabel: true
         chartOrientation: horizontal
     ---
-      xychart-beta
+      xychart
         title "Multiple Bar Plots"
         x-axis Categories [A, B, C]
         y-axis "Values" 0 --> 100
@@ -430,7 +439,7 @@ describe('XY Chart', () => {
       xyChart:
         showDataLabel: true
     ---
-      xychart-beta
+      xychart
         title "Single Bar Chart"
         x-axis Categories [A]
         y-axis "Value" 0 --> 100
@@ -449,7 +458,7 @@ describe('XY Chart', () => {
         showDataLabel: true
         chartOrientation: horizontal
     ---
-      xychart-beta
+      xychart
         title "Single Bar Chart"
         x-axis Categories [A]
         y-axis "Value" 0 --> 100
@@ -467,7 +476,7 @@ describe('XY Chart', () => {
       xyChart:
         showDataLabel: true
     ---
-      xychart-beta
+      xychart
         title "Decimal and Negative Values"
         x-axis Categories [A, B, C]
         y-axis -10 --> 10
@@ -486,7 +495,7 @@ describe('XY Chart', () => {
         showDataLabel: true
         chartOrientation: horizontal
     ---
-      xychart-beta
+      xychart
         title "Decimal and Negative Values"
         x-axis Categories [A, B, C]
         y-axis -10 --> 10
@@ -504,7 +513,7 @@ describe('XY Chart', () => {
       xyChart:
         showDataLabel: true
     ---
-    xychart-beta
+    xychart
             title "Sales Revenue"
             x-axis Months [jan,b,c]
             y-axis "Revenue (in $)" 4000 --> 12000
@@ -561,7 +570,7 @@ describe('XY Chart', () => {
         showDataLabel: true
         chartOrientation: horizontal
     ---
-    xychart-beta
+    xychart
             title "Sales Revenue"
             x-axis Months [jan,b,c]
             y-axis "Revenue (in $)" 4000 --> 12000
@@ -615,7 +624,7 @@ describe('XY Chart', () => {
         xyChart:
           showDataLabel: true
       ---
-      xychart-beta
+      xychart
         title "Sales Revenue"
         x-axis Months [jan,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s]
         y-axis "Revenue (in $)" 4000 --> 12000
@@ -672,7 +681,7 @@ describe('XY Chart', () => {
         showDataLabel: true
         chartOrientation: horizontal
     ---
-    xychart-beta
+    xychart
       title "Sales Revenue"
       x-axis Months [jan,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s]
       y-axis "Revenue (in $)" 4000 --> 12000
@@ -726,7 +735,7 @@ describe('XY Chart', () => {
       xyChart:
         showDataLabel: true
     ---
-    xychart-beta
+    xychart
             title "Sales Revenue"
             x-axis Months [jan]
             y-axis "Revenue (in $)" 3000 --> 12000
@@ -783,7 +792,7 @@ describe('XY Chart', () => {
         showDataLabel: true
         chartOrientation: horizontal
     ---
-    xychart-beta
+    xychart
             title "Sales Revenue"
             x-axis Months [jan]
             y-axis "Revenue (in $)" 3000 --> 12000
diff --git a/demos/xychart.html b/demos/xychart.html
index 25f8ec8ca..5706b15ea 100644
--- a/demos/xychart.html
+++ b/demos/xychart.html
@@ -16,7 +16,7 @@
   
     

XY Charts demos

-    xychart-beta
+    xychart
     title "Sales Revenue (in $)"
     x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
     y-axis "Revenue (in $)" 4000 --> 11000
@@ -26,7 +26,7 @@
     

XY Charts horizontal

-    xychart-beta horizontal
+    xychart horizontal
     title "Basic xychart"
     x-axis "this is x axis" [category1, "category 2", category3, category4]
     y-axis yaxisText 10 --> 150
@@ -36,7 +36,7 @@
     

XY Charts only lines and bar

-    xychart-beta
+    xychart
     line [23, 46, 77, 34]
     line [45, 32, 33, 12]
     line [87, 54, 99, 85]
@@ -48,13 +48,13 @@
     

XY Charts with +ve and -ve numbers

-    xychart-beta
+    xychart
     line [+1.3, .6, 2.4, -.34]
     

XY Charts Bar with multiple category

-    xychart-beta
+    xychart
     title "Basic xychart with many categories"
     x-axis "this is x axis" [category1, "category 2", category3, category4, category5, category6, category7]
     y-axis yaxisText 10 --> 150
@@ -63,7 +63,7 @@
 
     

XY Charts line with multiple category

-    xychart-beta
+    xychart
     title "Line chart with many category"
     x-axis "this is x axis" [category1, "category 2", category3, category4, category5, category6, category7]
     y-axis yaxisText 10 --> 150
@@ -72,7 +72,7 @@
 
     

XY Charts category with large text

-    xychart-beta
+    xychart
     title "Basic xychart with many categories with category overlap"
     x-axis "this is x axis" [category1, "Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.", category3, category4, category5, category6, category7]
     y-axis yaxisText 10 --> 150
@@ -89,7 +89,7 @@ config:
     height: 20
     plotReservedSpacePercent: 100
 ---
-    xychart-beta
+    xychart
       line [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
     
@@ -103,7 +103,7 @@ config: height: 20 plotReservedSpacePercent: 100 --- - xychart-beta + xychart bar [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
@@ -136,7 +136,7 @@ config: chartOrientation: horizontal plotReservedSpacePercent: 60 --- - xychart-beta + xychart title "Sales Revenue" x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Revenue (in $)" 4000 --> 11000 @@ -162,7 +162,7 @@ config: xAxisLineColor: "#87ceeb" plotColorPalette: "#008000, #faba63" --- - xychart-beta + xychart title "Sales Revenue" x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Revenue (in $)" 4000 --> 11000 diff --git a/docs/syntax/xyChart.md b/docs/syntax/xyChart.md index dd64f742d..dec16a518 100644 --- a/docs/syntax/xyChart.md +++ b/docs/syntax/xyChart.md @@ -13,7 +13,7 @@ ## Example ```mermaid-example -xychart-beta +xychart title "Sales Revenue" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Revenue (in $)" 4000 --> 11000 @@ -22,7 +22,7 @@ xychart-beta ``` ```mermaid -xychart-beta +xychart title "Sales Revenue" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Revenue (in $)" 4000 --> 11000 @@ -40,7 +40,7 @@ xychart-beta The chart can be drawn horizontal or vertical, default value is vertical. ``` -xychart-beta horizontal +xychart horizontal ... ``` @@ -51,7 +51,7 @@ The title is a short description of the chart and it will always render on top o #### Example ``` -xychart-beta +xychart title "This is a simple example" ... ``` @@ -98,10 +98,10 @@ A bar chart offers the capability to graphically depict bars. #### Simplest example -The only two things required are the chart name (`xychart-beta`) and one data set. So you will be able to draw a chart with a simple config like +The only two things required are the chart name (`xychart`) and one data set. So you will be able to draw a chart with a simple config like ``` -xychart-beta +xychart line [+1.3, .6, 2.4, -.34] ``` @@ -176,7 +176,7 @@ config: xyChart: titleColor: "#ff0000" --- -xychart-beta +xychart title "Sales Revenue" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Revenue (in $)" 4000 --> 11000 @@ -195,7 +195,7 @@ config: xyChart: titleColor: "#ff0000" --- -xychart-beta +xychart title "Sales Revenue" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Revenue (in $)" 4000 --> 11000 diff --git a/packages/mermaid/src/diagrams/xychart/parser/xychart.jison b/packages/mermaid/src/diagrams/xychart/parser/xychart.jison index 987132d17..8a47b1aac 100644 --- a/packages/mermaid/src/diagrams/xychart/parser/xychart.jison +++ b/packages/mermaid/src/diagrams/xychart/parser/xychart.jison @@ -29,6 +29,7 @@ "{" { this.popState(); } [^\}]* { return "acc_descr_multiline_value"; } +"xychart" {return 'XYCHART';} "xychart-beta" {return 'XYCHART';} (?:"vertical"|"horizontal") {return 'CHART_ORIENTATION'} diff --git a/packages/mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts b/packages/mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts index d7de15f66..ee7c94581 100644 --- a/packages/mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts +++ b/packages/mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts @@ -33,44 +33,44 @@ describe('Testing xychart jison file', () => { clearMocks(); }); - it('should throw error if xychart-beta text is not there', () => { - const str = 'xychart-beta-1'; + it('should throw error if xychart text is not there', () => { + const str = 'xychart-1'; expect(parserFnConstructor(str)).toThrow(); }); it('should not throw error if only xychart is there', () => { - const str = 'xychart-beta'; + const str = 'xychart'; expect(parserFnConstructor(str)).not.toThrow(); }); it('parse title of the chart within "', () => { - const str = 'xychart-beta \n title "This is a title"'; + const str = 'xychart \n title "This is a title"'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setDiagramTitle).toHaveBeenCalledWith('This is a title'); }); it('parse title of the chart without "', () => { - const str = 'xychart-beta \n title oneLinertitle'; + const str = 'xychart \n title oneLinertitle'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setDiagramTitle).toHaveBeenCalledWith('oneLinertitle'); }); it('parse chart orientation', () => { - const str = 'xychart-beta vertical'; + const str = 'xychart vertical'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setOrientation).toHaveBeenCalledWith('vertical'); }); it('parse chart orientation with spaces', () => { - let str = 'xychart-beta horizontal '; + let str = 'xychart horizontal '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setOrientation).toHaveBeenCalledWith('horizontal'); - str = 'xychart-beta abc'; + str = 'xychart abc'; expect(parserFnConstructor(str)).toThrow(); }); it('parse x-axis', () => { - const str = 'xychart-beta \nx-axis xAxisName\n'; + const str = 'xychart \nx-axis xAxisName\n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', @@ -79,7 +79,7 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis with axis name without "', () => { - const str = 'xychart-beta \nx-axis xAxisName \n'; + const str = 'xychart \nx-axis xAxisName \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', @@ -88,7 +88,7 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis with axis name with "', () => { - const str = 'xychart-beta \n x-axis "xAxisName has space"\n'; + const str = 'xychart \n x-axis "xAxisName has space"\n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName has space', @@ -97,7 +97,7 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis with axis name with " with spaces', () => { - const str = 'xychart-beta \n x-axis " xAxisName has space " \n'; + const str = 'xychart \n x-axis " xAxisName has space " \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: ' xAxisName has space ', @@ -106,7 +106,7 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis with axis name and range data', () => { - const str = 'xychart-beta \nx-axis xAxisName 45.5 --> 33 \n'; + const str = 'xychart \nx-axis xAxisName 45.5 --> 33 \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', @@ -115,11 +115,11 @@ describe('Testing xychart jison file', () => { expect(mockDB.setXAxisRangeData).toHaveBeenCalledWith(45.5, 33); }); it('parse x-axis throw error for invalid range data', () => { - const str = 'xychart-beta \nx-axis xAxisName aaa --> 33 \n'; + const str = 'xychart \nx-axis xAxisName aaa --> 33 \n'; expect(parserFnConstructor(str)).toThrow(); }); it('parse x-axis with axis name and range data with only decimal part', () => { - const str = 'xychart-beta \nx-axis xAxisName 45.5 --> .34 \n'; + const str = 'xychart \nx-axis xAxisName 45.5 --> .34 \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', @@ -129,7 +129,7 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis without axis name and range data', () => { - const str = 'xychart-beta \nx-axis 45.5 --> 1.34 \n'; + const str = 'xychart \nx-axis 45.5 --> 1.34 \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: '', @@ -139,7 +139,7 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis with axis name and category data', () => { - const str = 'xychart-beta \nx-axis xAxisName [ "cat1" , cat2a ] \n '; + const str = 'xychart \nx-axis xAxisName [ "cat1" , cat2a ] \n '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', @@ -155,7 +155,7 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis without axis name and category data', () => { - const str = 'xychart-beta \nx-axis [ "cat1" , cat2a ] \n '; + const str = 'xychart \nx-axis [ "cat1" , cat2a ] \n '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: '', @@ -171,14 +171,14 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis throw error if unbalanced bracket', () => { - let str = 'xychart-beta \nx-axis xAxisName [ "cat1" [ cat2a ] \n '; + let str = 'xychart \nx-axis xAxisName [ "cat1" [ cat2a ] \n '; expect(parserFnConstructor(str)).toThrow(); - str = 'xychart-beta \nx-axis xAxisName [ "cat1" , cat2a ] ] \n '; + str = 'xychart \nx-axis xAxisName [ "cat1" , cat2a ] ] \n '; expect(parserFnConstructor(str)).toThrow(); }); it('parse x-axis complete variant 1', () => { - const str = `xychart-beta \n x-axis "this is x axis" [category1, "category 2", category3]\n`; + const str = `xychart \n x-axis "this is x axis" [category1, "category 2", category3]\n`; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'this is x axis', type: 'text' }); expect(mockDB.setXAxisBand).toHaveBeenCalledWith([ @@ -189,8 +189,7 @@ describe('Testing xychart jison file', () => { }); it('parse x-axis complete variant 2', () => { - const str = - 'xychart-beta \nx-axis xAxisName [ "cat1 with space" , cat2 , cat3] \n '; + const str = 'xychart \nx-axis xAxisName [ "cat1 with space" , cat2 , cat3] \n '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisBand).toHaveBeenCalledWith([ @@ -202,7 +201,7 @@ describe('Testing xychart jison file', () => { it('parse x-axis complete variant 3', () => { const str = - 'xychart-beta \nx-axis xAxisName [ "cat1 with space" , cat2 asdf , cat3] \n '; + 'xychart \nx-axis xAxisName [ "cat1 with space" , cat2 asdf , cat3] \n '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setXAxisBand).toHaveBeenCalledWith([ @@ -213,17 +212,17 @@ describe('Testing xychart jison file', () => { }); it('parse y-axis with axis name', () => { - const str = 'xychart-beta \ny-axis yAxisName\n'; + const str = 'xychart \ny-axis yAxisName\n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); }); it('parse y-axis with axis name with spaces', () => { - const str = 'xychart-beta \ny-axis yAxisName \n'; + const str = 'xychart \ny-axis yAxisName \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); }); it('parse y-axis with axis name with "', () => { - const str = 'xychart-beta \n y-axis "yAxisName has space"\n'; + const str = 'xychart \n y-axis "yAxisName has space"\n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName has space', @@ -231,7 +230,7 @@ describe('Testing xychart jison file', () => { }); }); it('parse y-axis with axis name with " and spaces', () => { - const str = 'xychart-beta \n y-axis " yAxisName has space " \n'; + const str = 'xychart \n y-axis " yAxisName has space " \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: ' yAxisName has space ', @@ -239,39 +238,39 @@ describe('Testing xychart jison file', () => { }); }); it('parse y-axis with axis name with range data', () => { - const str = 'xychart-beta \ny-axis yAxisName 45.5 --> 33 \n'; + const str = 'xychart \ny-axis yAxisName 45.5 --> 33 \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 33); }); it('parse y-axis without axis name with range data', () => { - const str = 'xychart-beta \ny-axis 45.5 --> 33 \n'; + const str = 'xychart \ny-axis 45.5 --> 33 \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: '', type: 'text' }); expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 33); }); it('parse y-axis with axis name with range data with only decimal part', () => { - const str = 'xychart-beta \ny-axis yAxisName 45.5 --> .33 \n'; + const str = 'xychart \ny-axis yAxisName 45.5 --> .33 \n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 0.33); }); it('parse y-axis throw error for invalid number in range data', () => { - const str = 'xychart-beta \ny-axis yAxisName 45.5 --> abc \n'; + const str = 'xychart \ny-axis yAxisName 45.5 --> abc \n'; expect(parserFnConstructor(str)).toThrow(); }); it('parse y-axis throws error if range data is passed', () => { - const str = 'xychart-beta \ny-axis yAxisName [ 45.3, 33 ] \n'; + const str = 'xychart \ny-axis yAxisName [ 45.3, 33 ] \n'; expect(parserFnConstructor(str)).toThrow(); }); it('parse both axis at once', () => { - const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n'; + const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); }); it('parse line Data', () => { - const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line lineTitle [23, 45, 56.6]'; + const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line lineTitle [23, 45, 56.6]'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setLineData).toHaveBeenCalledWith( { text: 'lineTitle', type: 'text' }, @@ -282,7 +281,7 @@ describe('Testing xychart jison file', () => { }); it('parse line Data with spaces and +,- symbols', () => { const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 , 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 , 56.6 ] '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); @@ -292,8 +291,7 @@ describe('Testing xychart jison file', () => { ); }); it('parse line Data without title', () => { - const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line [ +23 , -45 , 56.6 , .33] '; + const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line [ +23 , -45 , 56.6 , .33] '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); @@ -304,34 +302,32 @@ describe('Testing xychart jison file', () => { }); it('parse line Data throws error unbalanced brackets', () => { let str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 [ -45 , 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 [ -45 , 56.6 ] '; expect(parserFnConstructor(str)).toThrow(); str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 ] 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 ] 56.6 ] '; expect(parserFnConstructor(str)).toThrow(); }); it('parse line Data throws error if data is not provided', () => { - const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" '; + const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" '; expect(parserFnConstructor(str)).toThrow(); }); it('parse line Data throws error if data is empty', () => { - const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ ] '; + const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ ] '; expect(parserFnConstructor(str)).toThrow(); }); it('parse line Data throws error if , is not in proper', () => { const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , , -45 , 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , , -45 , 56.6 ] '; expect(parserFnConstructor(str)).toThrow(); }); it('parse line Data throws error if not number', () => { const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -4aa5 , 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -4aa5 , 56.6 ] '; expect(parserFnConstructor(str)).toThrow(); }); it('parse bar Data', () => { - const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle [23, 45, 56.6, .22]'; + const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle [23, 45, 56.6, .22]'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); @@ -342,7 +338,7 @@ describe('Testing xychart jison file', () => { }); it('parse bar Data spaces and +,- symbol', () => { const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 , 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 , 56.6 ] '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); @@ -352,8 +348,7 @@ describe('Testing xychart jison file', () => { ); }); it('parse bar Data without plot title', () => { - const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar [ +23 , -45 , 56.6 ] '; + const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar [ +23 , -45 , 56.6 ] '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); @@ -361,34 +356,34 @@ describe('Testing xychart jison file', () => { }); it('parse bar should throw for unbalanced brackets', () => { let str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 [ -45 , 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 [ -45 , 56.6 ] '; expect(parserFnConstructor(str)).toThrow(); str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 ] 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 ] 56.6 ] '; expect(parserFnConstructor(str)).toThrow(); }); it('parse bar should throw error if data is not provided', () => { - const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" '; + const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" '; expect(parserFnConstructor(str)).toThrow(); }); it('parse bar should throw error if data is empty', () => { const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ ] '; expect(parserFnConstructor(str)).toThrow(); }); it('parse bar should throw error if comma is not proper', () => { const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , , -45 , 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , , -45 , 56.6 ] '; expect(parserFnConstructor(str)).toThrow(); }); it('parse bar should throw error if number is not passed', () => { const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -4aa5 , 56.6 ] '; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -4aa5 , 56.6 ] '; expect(parserFnConstructor(str)).toThrow(); }); it('parse multiple bar and line variant 1', () => { const str = - 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle1 [23, 45, 56.6] \n line lineTitle1 [11, 45.5, 67, 23] \n bar barTitle2 [13, 42, 56.89] \n line lineTitle2 [45, 99, 012]'; + 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle1 [23, 45, 56.6] \n line lineTitle1 [11, 45.5, 67, 23] \n bar barTitle2 [13, 42, 56.89] \n line lineTitle2 [45, 99, 012]'; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' }); expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' }); @@ -411,7 +406,7 @@ describe('Testing xychart jison file', () => { }); it('parse multiple bar and line variant 2', () => { const str = ` - xychart-beta horizontal + xychart horizontal title Basic xychart x-axis "this is x axis" [category1, "category 2", category3] y-axis yaxisText 10 --> 150 diff --git a/packages/mermaid/src/diagrams/xychart/xychartDetector.ts b/packages/mermaid/src/diagrams/xychart/xychartDetector.ts index 08be05b01..3a848cd7f 100644 --- a/packages/mermaid/src/diagrams/xychart/xychartDetector.ts +++ b/packages/mermaid/src/diagrams/xychart/xychartDetector.ts @@ -7,7 +7,7 @@ import type { const id = 'xychart'; const detector: DiagramDetector = (txt) => { - return /^\s*xychart-beta/.test(txt); + return /^\s*xychart(-beta)?/.test(txt); }; const loader: DiagramLoader = async () => { diff --git a/packages/mermaid/src/docs/syntax/xyChart.md b/packages/mermaid/src/docs/syntax/xyChart.md index 7de3d4144..4154fb2f0 100644 --- a/packages/mermaid/src/docs/syntax/xyChart.md +++ b/packages/mermaid/src/docs/syntax/xyChart.md @@ -7,7 +7,7 @@ ## Example ```mermaid-example -xychart-beta +xychart title "Sales Revenue" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Revenue (in $)" 4000 --> 11000 @@ -26,7 +26,7 @@ All text values that contain only one word can be written without `"`. If a text The chart can be drawn horizontal or vertical, default value is vertical. ``` -xychart-beta horizontal +xychart horizontal ... ``` @@ -37,7 +37,7 @@ The title is a short description of the chart and it will always render on top o #### Example ``` -xychart-beta +xychart title "This is a simple example" ... ``` @@ -86,10 +86,10 @@ A bar chart offers the capability to graphically depict bars. #### Simplest example -The only two things required are the chart name (`xychart-beta`) and one data set. So you will be able to draw a chart with a simple config like +The only two things required are the chart name (`xychart`) and one data set. So you will be able to draw a chart with a simple config like ``` -xychart-beta +xychart line [+1.3, .6, 2.4, -.34] ``` @@ -164,7 +164,7 @@ config: xyChart: titleColor: "#ff0000" --- -xychart-beta +xychart title "Sales Revenue" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Revenue (in $)" 4000 --> 11000 diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 3e28dbfd4..91022516a 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -796,6 +796,7 @@ graph TD;A--x|text including URL space|B;`) { textDiagramType: 'pie', expectedType: 'pie' }, { textDiagramType: 'packet-beta', expectedType: 'packet' }, { textDiagramType: 'xychart-beta', expectedType: 'xychart' }, + { textDiagramType: 'xychart', expectedType: 'xychart' }, { textDiagramType: 'requirementDiagram', expectedType: 'requirement' }, { textDiagramType: 'sequenceDiagram', expectedType: 'sequence' }, { textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' }, From 6979aa1013142fded5ad6d93126802906ef67c91 Mon Sep 17 00:00:00 2001 From: darshanr0107 Date: Mon, 16 Jun 2025 14:53:09 +0530 Subject: [PATCH 018/314] Fix failing test cases and update error messages for negative and zero inputs --- packages/mermaid/src/diagrams/pie/pieDb.ts | 10 ++-------- packages/parser/src/language/common/common.langium | 4 ++-- packages/parser/src/language/pie/pie.langium | 6 +++++- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/mermaid/src/diagrams/pie/pieDb.ts b/packages/mermaid/src/diagrams/pie/pieDb.ts index 87735b84e..ade44c331 100644 --- a/packages/mermaid/src/diagrams/pie/pieDb.ts +++ b/packages/mermaid/src/diagrams/pie/pieDb.ts @@ -35,15 +35,9 @@ const clear = (): void => { const addSection = ({ label, value }: D3Section): void => { if (value <= 0) { - const error: any = new Error( - `Section "${label}" has invalid value: ${value}. Zero and negative values are not allowed in pie charts. All slice values must be > 0` + throw new Error( + `"${label}" has invalid value: ${value}. Zero and negative values are not allowed in pie charts. All slice values must be > 0.` ); - error.hash = { - text: `pie "${label}": ${value}`, - token: `${value}`, - expected: ['a positive number (> 0)'], - }; - throw error; } if (!sections.has(label)) { sections.set(label, value); diff --git a/packages/parser/src/language/common/common.langium b/packages/parser/src/language/common/common.langium index e6d3d488f..b74ffc34d 100644 --- a/packages/parser/src/language/common/common.langium +++ b/packages/parser/src/language/common/common.langium @@ -17,8 +17,8 @@ terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^} terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/; terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/; -terminal FLOAT returns number: /-?[0-9]+\.[0-9]+(?!\.)/; -terminal INT returns number: /-?(0|[1-9][0-9]*)(?!\.)/; +terminal FLOAT returns number: /[0-9]+\.[0-9]+(?!\.)/; +terminal INT returns number: /0|[1-9][0-9]*(?!\.)/; terminal NUMBER returns number: FLOAT | INT; terminal STRING returns string: /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/; diff --git a/packages/parser/src/language/pie/pie.langium b/packages/parser/src/language/pie/pie.langium index a80caa81f..f6802d718 100644 --- a/packages/parser/src/language/pie/pie.langium +++ b/packages/parser/src/language/pie/pie.langium @@ -12,5 +12,9 @@ entry Pie: ; PieSection: - label=STRING ":" value=NUMBER EOL + label=STRING ":" value=NUMBER_PIE EOL ; + +terminal FLOAT_PIE returns number: /-?[0-9]+\.[0-9]+(?!\.)/; +terminal INT_PIE returns number: /-?(0|[1-9][0-9]*)(?!\.)/; +terminal NUMBER_PIE returns number: FLOAT_PIE | INT_PIE; \ No newline at end of file From cfbce5463844f0c537ba9e74ad5b964943e75b7e Mon Sep 17 00:00:00 2001 From: darshanr0107 Date: Mon, 16 Jun 2025 19:11:29 +0530 Subject: [PATCH 019/314] prevent zero% slices from rendering in pie chart --- .../mermaid/src/diagrams/pie/pieRenderer.ts | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.ts b/packages/mermaid/src/diagrams/pie/pieRenderer.ts index a0cdce3df..5b87613ff 100644 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.ts +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.ts @@ -10,20 +10,14 @@ import { cleanAndMerge, parseFontSize } from '../../utils.js'; import type { D3Section, PieDB, Sections } from './pieTypes.js'; const createPieArcs = (sections: Sections): d3.PieArcDatum[] => { - // Compute the position of each group on the pie: + const sum = [...sections.values()].reduce((acc, val) => acc + val, 0); + const pieData: D3Section[] = [...sections.entries()] - .map((element: [string, number]): D3Section => { - return { - label: element[0], - value: element[1], - }; - }) - .sort((a: D3Section, b: D3Section): number => { - return b.value - a.value; - }); - const pie: d3.Pie = d3pie().value( - (d3Section: D3Section): number => d3Section.value - ); + .map(([label, value]) => ({ label, value })) + .filter((d) => (d.value / sum) * 100 >= 1) // Remove values < 1% + .sort((a, b) => b.value - a.value); + + const pie: d3.Pie = d3pie().value((d) => d.value); return pie(pieData); }; @@ -89,13 +83,21 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { themeVariables.pie11, themeVariables.pie12, ]; + let sum = 0; + sections.forEach((section) => { + sum += section; + }); + + // Filter out arcs that would render as 0% + const filteredArcs = arcs.filter((datum) => ((datum.data.value / sum) * 100).toFixed(0) !== '0'); + // Set the color scale const color: d3.ScaleOrdinal = scaleOrdinal(myGeneratedColors); // Build the pie chart: each part of the pie is a path that we build using the arc function. group .selectAll('mySlices') - .data(arcs) + .data(filteredArcs) .enter() .append('path') .attr('d', arcGenerator) @@ -104,15 +106,11 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { }) .attr('class', 'pieCircle'); - let sum = 0; - sections.forEach((section) => { - sum += section; - }); // Now add the percentage. // Use the centroid method to get the best coordinates. group .selectAll('mySlices') - .data(arcs) + .data(filteredArcs) .enter() .append('text') .text((datum: d3.PieArcDatum): string => { @@ -133,15 +131,20 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { .attr('class', 'pieTitleText'); // Add the legends/annotations for each section + const allSectionData: D3Section[] = [...sections.entries()].map(([label, value]) => ({ + label, + value, + })); + const legend = group .selectAll('.legend') - .data(color.domain()) + .data(allSectionData) .enter() .append('g') .attr('class', 'legend') .attr('transform', (_datum, index: number): string => { const height = LEGEND_RECT_SIZE + LEGEND_SPACING; - const offset = (height * color.domain().length) / 2; + const offset = (height * allSectionData.length) / 2; const horizontal = 12 * LEGEND_RECT_SIZE; const vertical = index * height - offset; return 'translate(' + horizontal + ',' + vertical + ')'; @@ -151,20 +154,18 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { .append('rect') .attr('width', LEGEND_RECT_SIZE) .attr('height', LEGEND_RECT_SIZE) - .style('fill', color) - .style('stroke', color); + .style('fill', (d) => color(d.label)) + .style('stroke', (d) => color(d.label)); legend - .data(arcs) .append('text') .attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING) .attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING) - .text((datum: d3.PieArcDatum): string => { - const { label, value } = datum.data; + .text((d) => { if (db.getShowData()) { - return `${label} [${value}]`; + return `${d.label} [${d.value}]`; } - return label; + return d.label; }); const longestTextWidth = Math.max( From 72c0d9df26687a4deca0999ae69e8b06d361c227 Mon Sep 17 00:00:00 2001 From: darshanr0107 Date: Mon, 16 Jun 2025 19:38:58 +0530 Subject: [PATCH 020/314] added test case for preventing zero% values from rendering --- cypress/integration/rendering/pie.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cypress/integration/rendering/pie.spec.ts b/cypress/integration/rendering/pie.spec.ts index 171a83057..8f6ef7de3 100644 --- a/cypress/integration/rendering/pie.spec.ts +++ b/cypress/integration/rendering/pie.spec.ts @@ -82,4 +82,13 @@ describe('pie chart', () => { ` ); }); + it('should render pie slices only for non-zero values but shows all legends', () => { + imgSnapshotTest( + ` pie title Pets adopted by volunteers + "Dogs" : 386 + "Cats" : 85 + "Rats" : 1 + ` + ); + }); }); From 9dc987b28b34d81580be59b7499e7eab6f3e636c Mon Sep 17 00:00:00 2001 From: darshanr0107 Date: Tue, 17 Jun 2025 12:12:40 +0530 Subject: [PATCH 021/314] remove beta support for sankey diagram --- cypress/integration/rendering/sankey.spec.ts | 4 ++-- demos/sankey.html | 16 ++++++++------ docs/syntax/sankey.md | 22 +++++++++---------- .../src/diagrams/sankey/parser/sankey.jison | 1 + .../src/diagrams/sankey/parser/sankey.spec.ts | 21 ++++++++++++++++-- .../src/diagrams/sankey/sankeyDetector.ts | 2 +- packages/mermaid/src/docs/syntax/sankey.md | 12 +++++----- 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/cypress/integration/rendering/sankey.spec.ts b/cypress/integration/rendering/sankey.spec.ts index ad0d75f18..b9940d998 100644 --- a/cypress/integration/rendering/sankey.spec.ts +++ b/cypress/integration/rendering/sankey.spec.ts @@ -15,7 +15,7 @@ describe('Sankey Diagram', () => { describe('when given a linkColor', function () { this.beforeAll(() => { cy.wrap( - `sankey-beta + `sankey a,b,10 ` ).as('graph'); @@ -62,7 +62,7 @@ describe('Sankey Diagram', () => { this.beforeAll(() => { cy.wrap( ` - sankey-beta + sankey a,b,8 b,c,8 diff --git a/demos/sankey.html b/demos/sankey.html index 2439cb589..11bb541c2 100644 --- a/demos/sankey.html +++ b/demos/sankey.html @@ -20,12 +20,14 @@ width: 800 nodeAlignment: left --- - sankey-beta - Revenue,Expenses,10 - Revenue,Profit,10 - Expenses,Manufacturing,5 - Expenses,Tax,3 - Expenses,Research,2 + sankey + a,b,8 + b,c,8 + c,d,8 + d,e,8 + + x,c,4 + c,y,4

Energy flow

@@ -40,7 +42,7 @@ linkColor: gradient nodeAlignment: justify --- - sankey-beta + sankey Agricultural 'waste',Bio-conversion,124.729 Bio-conversion,Liquid,0.597 diff --git a/docs/syntax/sankey.md b/docs/syntax/sankey.md index ccabc11c9..e981358b5 100644 --- a/docs/syntax/sankey.md +++ b/docs/syntax/sankey.md @@ -23,7 +23,7 @@ config: sankey: showValues: false --- -sankey-beta +sankey Agricultural 'waste',Bio-conversion,124.729 Bio-conversion,Liquid,0.597 @@ -101,7 +101,7 @@ config: sankey: showValues: false --- -sankey-beta +sankey Agricultural 'waste',Bio-conversion,124.729 Bio-conversion,Liquid,0.597 @@ -175,7 +175,7 @@ Wind,Electricity grid,289.366 ## Syntax -The idea behind syntax is that a user types `sankey-beta` keyword first, then pastes raw CSV below and get the result. +The idea behind syntax is that a user types `sankey` keyword first, then pastes raw CSV below and get the result. It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180.txt) with subtle **differences**: @@ -187,7 +187,7 @@ It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180. It is implied that 3 columns inside CSV should represent `source`, `target` and `value` accordingly: ```mermaid-example -sankey-beta +sankey %% source,target,value Electricity grid,Over generation / exports,104.453 @@ -196,7 +196,7 @@ Electricity grid,H2 conversion,27.14 ``` ```mermaid -sankey-beta +sankey %% source,target,value Electricity grid,Over generation / exports,104.453 @@ -209,7 +209,7 @@ Electricity grid,H2 conversion,27.14 CSV does not support empty lines without comma delimiters by default. But you can add them if needed: ```mermaid-example -sankey-beta +sankey Bio-conversion,Losses,26.862 @@ -219,7 +219,7 @@ Bio-conversion,Gas,81.144 ``` ```mermaid -sankey-beta +sankey Bio-conversion,Losses,26.862 @@ -233,14 +233,14 @@ Bio-conversion,Gas,81.144 If you need to have a comma, wrap it in double quotes: ```mermaid-example -sankey-beta +sankey Pumped heat,"Heating and cooling, homes",193.026 Pumped heat,"Heating and cooling, commercial",70.672 ``` ```mermaid -sankey-beta +sankey Pumped heat,"Heating and cooling, homes",193.026 Pumped heat,"Heating and cooling, commercial",70.672 @@ -251,14 +251,14 @@ Pumped heat,"Heating and cooling, commercial",70.672 If you need to have double quote, put a pair of them inside quoted string: ```mermaid-example -sankey-beta +sankey Pumped heat,"Heating and cooling, ""homes""",193.026 Pumped heat,"Heating and cooling, ""commercial""",70.672 ``` ```mermaid -sankey-beta +sankey Pumped heat,"Heating and cooling, ""homes""",193.026 Pumped heat,"Heating and cooling, ""commercial""",70.672 diff --git a/packages/mermaid/src/diagrams/sankey/parser/sankey.jison b/packages/mermaid/src/diagrams/sankey/parser/sankey.jison index 9d66b69a4..d531c438a 100644 --- a/packages/mermaid/src/diagrams/sankey/parser/sankey.jison +++ b/packages/mermaid/src/diagrams/sankey/parser/sankey.jison @@ -27,6 +27,7 @@ TEXTDATA [\u0020-\u0021\u0023-\u002B\u002D-\u007E] %% "sankey-beta" { this.pushState('csv'); return 'SANKEY'; } +"sankey" { this.pushState('csv'); return 'SANKEY'; } <> { return 'EOF' } // match end of file ({CRLF}|{LF}) { return 'NEWLINE' } {COMMA} { return 'COMMA' } diff --git a/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts b/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts index 007cda6f9..10fc86963 100644 --- a/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts +++ b/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts @@ -13,7 +13,7 @@ describe('Sankey diagram', function () { sankey.parser.yy.clear(); }); - it('parses csv', () => { + it('parses csv with sankey-beta syntax', () => { const csv = path.resolve(__dirname, './energy.csv'); const data = fs.readFileSync(csv, 'utf8'); const graphDefinition = prepareTextForParsing(cleanupComments('sankey-beta\n\n ' + data)); @@ -21,7 +21,15 @@ describe('Sankey diagram', function () { sankey.parser.parse(graphDefinition); }); - it('allows __proto__ as id', function () { + it('parses csv with sankey syntax', () => { + const csv = path.resolve(__dirname, './energy.csv'); + const data = fs.readFileSync(csv, 'utf8'); + const graphDefinition = prepareTextForParsing(cleanupComments('sankey\n\n ' + data)); + + sankey.parser.parse(graphDefinition); + }); + + it('allows __proto__ as id with sankey-beta syntax', function () { sankey.parser.parse( prepareTextForParsing(`sankey-beta __proto__,A,0.597 @@ -29,5 +37,14 @@ describe('Sankey diagram', function () { `) ); }); + + it('allows __proto__ as id with sankey syntax', function () { + sankey.parser.parse( + prepareTextForParsing(`sankey + __proto__,A,0.597 + A,__proto__,0.403 + `) + ); + }); }); }); diff --git a/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts b/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts index 73c4d1428..589bc9ade 100644 --- a/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts +++ b/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts @@ -3,7 +3,7 @@ import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-a const id = 'sankey'; const detector: DiagramDetector = (txt) => { - return /^\s*sankey-beta/.test(txt); + return /^\s*sankey(-beta)?/.test(txt); }; const loader = async () => { diff --git a/packages/mermaid/src/docs/syntax/sankey.md b/packages/mermaid/src/docs/syntax/sankey.md index 6b7c359e3..bf04d5553 100644 --- a/packages/mermaid/src/docs/syntax/sankey.md +++ b/packages/mermaid/src/docs/syntax/sankey.md @@ -18,7 +18,7 @@ config: sankey: showValues: false --- -sankey-beta +sankey Agricultural 'waste',Bio-conversion,124.729 Bio-conversion,Liquid,0.597 @@ -92,7 +92,7 @@ Wind,Electricity grid,289.366 ## Syntax -The idea behind syntax is that a user types `sankey-beta` keyword first, then pastes raw CSV below and get the result. +The idea behind syntax is that a user types `sankey` keyword first, then pastes raw CSV below and get the result. It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180.txt) with subtle **differences**: @@ -104,7 +104,7 @@ It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180. It is implied that 3 columns inside CSV should represent `source`, `target` and `value` accordingly: ```mermaid-example -sankey-beta +sankey %% source,target,value Electricity grid,Over generation / exports,104.453 @@ -117,7 +117,7 @@ Electricity grid,H2 conversion,27.14 CSV does not support empty lines without comma delimiters by default. But you can add them if needed: ```mermaid-example -sankey-beta +sankey Bio-conversion,Losses,26.862 @@ -131,7 +131,7 @@ Bio-conversion,Gas,81.144 If you need to have a comma, wrap it in double quotes: ```mermaid-example -sankey-beta +sankey Pumped heat,"Heating and cooling, homes",193.026 Pumped heat,"Heating and cooling, commercial",70.672 @@ -142,7 +142,7 @@ Pumped heat,"Heating and cooling, commercial",70.672 If you need to have double quote, put a pair of them inside quoted string: ```mermaid-example -sankey-beta +sankey Pumped heat,"Heating and cooling, ""homes""",193.026 Pumped heat,"Heating and cooling, ""commercial""",70.672 From a3d164fde8673054529b9c333b0c2429f5f413b5 Mon Sep 17 00:00:00 2001 From: darshanr0107 Date: Tue, 17 Jun 2025 12:43:26 +0530 Subject: [PATCH 022/314] remove beta suffix for block diagram --- cypress/integration/rendering/block.spec.js | 56 ++++---- demos/block.html | 22 ++-- docs/syntax/block.md | 122 +++++++++--------- .../src/diagrams/block/blockDetector.ts | 2 +- .../src/diagrams/block/parser/block.jison | 10 +- .../src/diagrams/block/parser/block.spec.ts | 46 +++---- packages/mermaid/src/docs/syntax/block.md | 62 ++++----- 7 files changed, 160 insertions(+), 160 deletions(-) diff --git a/cypress/integration/rendering/block.spec.js b/cypress/integration/rendering/block.spec.js index 589a30fde..12843f2a2 100644 --- a/cypress/integration/rendering/block.spec.js +++ b/cypress/integration/rendering/block.spec.js @@ -16,7 +16,7 @@ describe('Block diagram', () => { it('BL2: should handle columns statement in sub-blocks', () => { imgSnapshotTest( - `block-beta + `block id1["Hello"] block columns 3 @@ -32,7 +32,7 @@ describe('Block diagram', () => { it('BL3: should align block widths and handle columns statement in sub-blocks', () => { imgSnapshotTest( - `block-beta + `block block columns 1 id1 @@ -48,7 +48,7 @@ describe('Block diagram', () => { it('BL4: should align block widths and handle columns statements in deeper sub-blocks then 1 level', () => { imgSnapshotTest( - `block-beta + `block columns 1 block columns 1 @@ -68,7 +68,7 @@ describe('Block diagram', () => { it('BL5: should align block widths and handle columns statements in deeper sub-blocks then 1 level (alt)', () => { imgSnapshotTest( - `block-beta + `block columns 1 block id1 @@ -87,7 +87,7 @@ describe('Block diagram', () => { it('BL6: should handle block arrows and spece statements', () => { imgSnapshotTest( - `block-beta + `block columns 3 space:3 ida idb idc @@ -106,7 +106,7 @@ describe('Block diagram', () => { it('BL7: should handle different types of edges', () => { imgSnapshotTest( - `block-beta + `block columns 3 A space:5 A --o B @@ -119,7 +119,7 @@ describe('Block diagram', () => { it('BL8: should handle sub-blocks without columns statements', () => { imgSnapshotTest( - `block-beta + `block columns 2 C A B block @@ -133,7 +133,7 @@ describe('Block diagram', () => { it('BL9: should handle edges from blocks in sub blocks to other blocks', () => { imgSnapshotTest( - `block-beta + `block columns 3 B space block @@ -147,7 +147,7 @@ describe('Block diagram', () => { it('BL10: should handle edges from composite blocks', () => { imgSnapshotTest( - `block-beta + `block columns 3 B space block BL @@ -161,7 +161,7 @@ describe('Block diagram', () => { it('BL11: should handle edges to composite blocks', () => { imgSnapshotTest( - `block-beta + `block columns 3 B space block BL @@ -175,7 +175,7 @@ describe('Block diagram', () => { it('BL12: edges should handle labels', () => { imgSnapshotTest( - `block-beta + `block A space A -- "apa" --> E @@ -186,7 +186,7 @@ describe('Block diagram', () => { it('BL13: should handle block arrows in different directions', () => { imgSnapshotTest( - `block-beta + `block columns 3 space blockArrowId1<["down"]>(down) space blockArrowId2<["right"]>(right) blockArrowId3<["Sync"]>(x, y) blockArrowId4<["left"]>(left) @@ -199,7 +199,7 @@ describe('Block diagram', () => { it('BL14: should style statements and class statements', () => { imgSnapshotTest( - `block-beta + `block A B classDef blue fill:#66f,stroke:#333,stroke-width:2px; @@ -212,7 +212,7 @@ describe('Block diagram', () => { it('BL15: width alignment - D and E should share available space', () => { imgSnapshotTest( - `block-beta + `block block D E @@ -225,7 +225,7 @@ describe('Block diagram', () => { it('BL16: width alignment - C should be as wide as the composite block', () => { imgSnapshotTest( - `block-beta + `block block A("This is the text") B @@ -238,7 +238,7 @@ describe('Block diagram', () => { it('BL17: width alignment - blocks should be equal in width', () => { imgSnapshotTest( - `block-beta + `block A("This is the text") B C @@ -249,7 +249,7 @@ describe('Block diagram', () => { it('BL18: block types 1 - square, rounded and circle', () => { imgSnapshotTest( - `block-beta + `block A["square"] B("rounded") C(("circle")) @@ -260,7 +260,7 @@ describe('Block diagram', () => { it('BL19: block types 2 - odd, diamond and hexagon', () => { imgSnapshotTest( - `block-beta + `block A>"rect_left_inv_arrow"] B{"diamond"} C{{"hexagon"}} @@ -271,7 +271,7 @@ describe('Block diagram', () => { it('BL20: block types 3 - stadium', () => { imgSnapshotTest( - `block-beta + `block A(["stadium"]) `, {} @@ -280,7 +280,7 @@ describe('Block diagram', () => { it('BL21: block types 4 - lean right, lean left, trapezoid and inv trapezoid', () => { imgSnapshotTest( - `block-beta + `block A[/"lean right"/] B[\"lean left"\] C[/"trapezoid"\] @@ -292,7 +292,7 @@ describe('Block diagram', () => { it('BL22: block types 1 - square, rounded and circle', () => { imgSnapshotTest( - `block-beta + `block A["square"] B("rounded") C(("circle")) @@ -303,7 +303,7 @@ describe('Block diagram', () => { it('BL23: sizing - it should be possible to make a block wider', () => { imgSnapshotTest( - `block-beta + `block A("rounded"):2 B:2 C @@ -314,7 +314,7 @@ describe('Block diagram', () => { it('BL24: sizing - it should be possible to make a composite block wider', () => { imgSnapshotTest( - `block-beta + `block block:2 A end @@ -326,7 +326,7 @@ describe('Block diagram', () => { it('BL25: block in the middle with space on each side', () => { imgSnapshotTest( - `block-beta + `block columns 3 space middle["In the middle"] @@ -337,7 +337,7 @@ describe('Block diagram', () => { }); it('BL26: space and an edge', () => { imgSnapshotTest( - `block-beta + `block columns 5 A space B A --x B @@ -347,7 +347,7 @@ describe('Block diagram', () => { }); it('BL27: block sizes for regular blocks', () => { imgSnapshotTest( - `block-beta + `block columns 3 a["A wide one"] b:2 c:2 d `, @@ -356,7 +356,7 @@ describe('Block diagram', () => { }); it('BL28: composite block with a set width - f should use the available space', () => { imgSnapshotTest( - `block-beta + `block columns 3 a:3 block:e:3 @@ -370,7 +370,7 @@ describe('Block diagram', () => { it('BL29: composite block with a set width - f and g should split the available space', () => { imgSnapshotTest( - `block-beta + `block columns 3 a:3 block:e:3 diff --git a/demos/block.html b/demos/block.html index f0957b670..5296126e0 100644 --- a/demos/block.html +++ b/demos/block.html @@ -10,7 +10,7 @@

Block diagram demos

-block-beta
+block
 columns 1
   db(("DB"))
   blockArrowId6<["   "]>(down)
@@ -26,7 +26,7 @@ columns 1
   style B fill:#f9F,stroke:#333,stroke-width:4px
     
-block-beta
+block
     A1["square"]
     B1("rounded")
     C1(("circle"))
@@ -36,7 +36,7 @@ block-beta
     
-block-beta
+block
     A1(["stadium"])
     A2[["subroutine"]]
     B1[("cylinder")]
@@ -48,7 +48,7 @@ block-beta
     
-block-beta
+block
   block:e:4
     columns 2
       f
@@ -57,7 +57,7 @@ block-beta
 
     
-block-beta
+block
   block:e:4
     columns 2
       f
@@ -67,7 +67,7 @@ block-beta
 
     
-block-beta
+block
   columns 3
   a:3
   block:e:3
@@ -80,7 +80,7 @@ block-beta
 
     
-block-beta
+block
   columns 4
   a b c d
   block:e:4
@@ -97,19 +97,19 @@ flowchart LR
   X-- "a label" -->z
     
-block-beta
+block
 columns 5
    A space B
    A --x B
     
-block-beta
+block
 columns 3
   a["A wide one"] b:2 c:2 d
     
-block-beta
+block
 columns 3
   a b c
   e:3
@@ -117,7 +117,7 @@ columns 3
     
-block-beta
+block
 
   A1:3
   A2:1
diff --git a/docs/syntax/block.md b/docs/syntax/block.md
index d711a2c92..5ca0db719 100644
--- a/docs/syntax/block.md
+++ b/docs/syntax/block.md
@@ -9,7 +9,7 @@
 ## Introduction to Block Diagrams
 
 ```mermaid-example
-block-beta
+block
 columns 1
   db(("DB"))
   blockArrowId6<["   "]>(down)
@@ -26,7 +26,7 @@ columns 1
 ```
 
 ```mermaid
-block-beta
+block
 columns 1
   db(("DB"))
   blockArrowId6<["   "]>(down)
@@ -80,12 +80,12 @@ At its core, a block diagram consists of blocks representing different entities
 To create a simple block diagram with three blocks labeled 'a', 'b', and 'c', the syntax is as follows:
 
 ```mermaid-example
-block-beta
+block
   a b c
 ```
 
 ```mermaid
-block-beta
+block
   a b c
 ```
 
@@ -101,13 +101,13 @@ While simple block diagrams are linear and straightforward, more complex systems
 In scenarios where you need to distribute blocks across multiple columns, you can specify the number of columns and arrange the blocks accordingly. Here's how to create a block diagram with three columns and four blocks, where the fourth block appears in a second row:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   a b c d
 ```
 
 ```mermaid
-block-beta
+block
   columns 3
   a b c d
 ```
@@ -130,13 +130,13 @@ In more complex diagrams, you may need blocks that span multiple columns to emph
 To create a block diagram where one block spans across two columns, you can specify the desired width for each block:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   a["A label"] b:2 c:2 d
 ```
 
 ```mermaid
-block-beta
+block
   columns 3
   a["A label"] b:2 c:2 d
 ```
@@ -153,7 +153,7 @@ Composite blocks, or blocks within blocks, are an advanced feature in Mermaid's
 Creating a composite block involves defining a parent block and then nesting other blocks within it. Here's how to define a composite block with nested elements:
 
 ```mermaid-example
-block-beta
+block
     block
       D
     end
@@ -161,7 +161,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
     block
       D
     end
@@ -180,7 +180,7 @@ Mermaid also allows for dynamic adjustment of column widths based on the content
 In diagrams with varying block sizes, Mermaid automatically adjusts the column widths to fit the largest block in each column. Here's an example:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   a:3
   block:group1:2
@@ -195,7 +195,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
   columns 3
   a:3
   block:group1:2
@@ -215,7 +215,7 @@ This example demonstrates how Mermaid dynamically adjusts the width of the colum
 In scenarios where you need to stack blocks horizontally, you can use column width to accomplish the task. Blocks can be arranged vertically by putting them in a single column. Here is how you can create a block diagram in which 4 blocks are stacked on top of each other:
 
 ```mermaid-example
-block-beta
+block
   block
     columns 1
     a["A label"] b c d
@@ -223,7 +223,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
   block
     columns 1
     a["A label"] b c d
@@ -247,12 +247,12 @@ Mermaid supports a range of block shapes to suit different diagramming needs, fr
 To create a block with round edges, which can be used to represent a softer or more flexible component:
 
 ```mermaid-example
-block-beta
+block
     id1("This is the text in the box")
 ```
 
 ```mermaid
-block-beta
+block
     id1("This is the text in the box")
 ```
 
@@ -261,12 +261,12 @@ block-beta
 A stadium-shaped block, resembling an elongated circle, can be used for components that are process-oriented:
 
 ```mermaid-example
-block-beta
+block
     id1(["This is the text in the box"])
 ```
 
 ```mermaid
-block-beta
+block
     id1(["This is the text in the box"])
 ```
 
@@ -275,12 +275,12 @@ block-beta
 For representing subroutines or contained processes, a block with double vertical lines is useful:
 
 ```mermaid-example
-block-beta
+block
     id1[["This is the text in the box"]]
 ```
 
 ```mermaid
-block-beta
+block
     id1[["This is the text in the box"]]
 ```
 
@@ -289,12 +289,12 @@ block-beta
 The cylindrical shape is ideal for representing databases or storage components:
 
 ```mermaid-example
-block-beta
+block
     id1[("Database")]
 ```
 
 ```mermaid
-block-beta
+block
     id1[("Database")]
 ```
 
@@ -303,12 +303,12 @@ block-beta
 A circle can be used for centralized or pivotal components:
 
 ```mermaid-example
-block-beta
+block
     id1(("This is the text in the circle"))
 ```
 
 ```mermaid
-block-beta
+block
     id1(("This is the text in the circle"))
 ```
 
@@ -319,36 +319,36 @@ For decision points, use a rhombus, and for unique or specialized processes, asy
 **Asymmetric**
 
 ```mermaid-example
-block-beta
+block
   id1>"This is the text in the box"]
 ```
 
 ```mermaid
-block-beta
+block
   id1>"This is the text in the box"]
 ```
 
 **Rhombus**
 
 ```mermaid-example
-block-beta
+block
     id1{"This is the text in the box"}
 ```
 
 ```mermaid
-block-beta
+block
     id1{"This is the text in the box"}
 ```
 
 **Hexagon**
 
 ```mermaid-example
-block-beta
+block
     id1{{"This is the text in the box"}}
 ```
 
 ```mermaid
-block-beta
+block
     id1{{"This is the text in the box"}}
 ```
 
@@ -357,7 +357,7 @@ block-beta
 Parallelogram and trapezoid shapes are perfect for inputs/outputs and transitional processes:
 
 ```mermaid-example
-block-beta
+block
   id1[/"This is the text in the box"/]
   id2[\"This is the text in the box"\]
   A[/"Christmas"\]
@@ -365,7 +365,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
   id1[/"This is the text in the box"/]
   id2[\"This is the text in the box"\]
   A[/"Christmas"\]
@@ -377,12 +377,12 @@ block-beta
 For highlighting critical or high-priority components, a double circle can be effective:
 
 ```mermaid-example
-block-beta
+block
     id1((("This is the text in the circle")))
 ```
 
 ```mermaid
-block-beta
+block
     id1((("This is the text in the circle")))
 ```
 
@@ -395,7 +395,7 @@ Mermaid also offers unique shapes like block arrows and space blocks for directi
 Block arrows can visually indicate direction or flow within a process:
 
 ```mermaid-example
-block-beta
+block
   blockArrowId<["Label"]>(right)
   blockArrowId2<["Label"]>(left)
   blockArrowId3<["Label"]>(up)
@@ -406,7 +406,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
   blockArrowId<["Label"]>(right)
   blockArrowId2<["Label"]>(left)
   blockArrowId3<["Label"]>(up)
@@ -421,14 +421,14 @@ block-beta
 Space blocks can be used to create intentional empty spaces in the diagram, which is useful for layout and readability:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   a space b
   c   d   e
 ```
 
 ```mermaid
-block-beta
+block
   columns 3
   a space b
   c   d   e
@@ -437,12 +437,12 @@ block-beta
 or
 
 ```mermaid-example
-block-beta
+block
   ida space:3 idb idc
 ```
 
 ```mermaid
-block-beta
+block
   ida space:3 idb idc
 ```
 
@@ -467,13 +467,13 @@ The most fundamental aspect of connecting blocks is the use of arrows or links.
 A simple link with an arrow can be created to show direction or flow from one block to another:
 
 ```mermaid-example
-block-beta
+block
   A space B
   A-->B
 ```
 
 ```mermaid
-block-beta
+block
   A space B
   A-->B
 ```
@@ -490,13 +490,13 @@ Example - Text with Links
 To add text to a link, the syntax includes the text within the link definition:
 
 ```mermaid-example
-block-beta
+block
   A space:2 B
   A-- "X" -->B
 ```
 
 ```mermaid
-block-beta
+block
   A space:2 B
   A-- "X" -->B
 ```
@@ -506,7 +506,7 @@ This example show how to add descriptive text to the links, enhancing the inform
 Example - Edges and Styles:
 
 ```mermaid-example
-block-beta
+block
 columns 1
   db(("DB"))
   blockArrowId6<["   "]>(down)
@@ -523,7 +523,7 @@ columns 1
 ```
 
 ```mermaid
-block-beta
+block
 columns 1
   db(("DB"))
   blockArrowId6<["   "]>(down)
@@ -552,7 +552,7 @@ Mermaid enables detailed styling of individual blocks, allowing you to apply var
 To apply custom styles to a block, you can use the `style` keyword followed by the block identifier and the desired CSS properties:
 
 ```mermaid-example
-block-beta
+block
   id1 space id2
   id1("Start")-->id2("Stop")
   style id1 fill:#636,stroke:#333,stroke-width:4px
@@ -560,7 +560,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
   id1 space id2
   id1("Start")-->id2("Stop")
   style id1 fill:#636,stroke:#333,stroke-width:4px
@@ -574,7 +574,7 @@ Mermaid enables applying styling to classes, which could make styling easier if
 #### Example - Styling a Single Class
 
 ```mermaid-example
-block-beta
+block
   A space B
   A-->B
   classDef blue fill:#6e6ce6,stroke:#333,stroke-width:4px;
@@ -583,7 +583,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
   A space B
   A-->B
   classDef blue fill:#6e6ce6,stroke:#333,stroke-width:4px;
@@ -608,7 +608,7 @@ Combining the elements of structure, linking, and styling, we can create compreh
 Illustrating a simple software system architecture with interconnected components:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   Frontend blockArrowId6<[" "]>(right) Backend
   space:2 down<[" "]>(down)
@@ -621,7 +621,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
   columns 3
   Frontend blockArrowId6<[" "]>(right) Backend
   space:2 down<[" "]>(down)
@@ -640,7 +640,7 @@ This example shows a basic architecture with a frontend, backend, and database.
 Representing a business process flow with decision points and multiple stages:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   Start(("Start")) space:2
   down<[" "]>(down) space:2
@@ -653,7 +653,7 @@ block-beta
 ```
 
 ```mermaid
-block-beta
+block
   columns 3
   Start(("Start")) space:2
   down<[" "]>(down) space:2
@@ -682,7 +682,7 @@ Understanding and avoiding common syntax errors is key to a smooth experience wi
 A common mistake is incorrect linking syntax, which can lead to unexpected results or broken diagrams:
 
 ```
-block-beta
+block
   A - B
 ```
 
@@ -690,13 +690,13 @@ block-beta
 Ensure that links between blocks are correctly specified with arrows (--> or ---) to define the direction and type of connection. Also remember that one of the fundamentals for block diagram is to give the author full control of where the boxes are positioned so in the example you need to add a space between the boxes:
 
 ```mermaid-example
-block-beta
+block
   A space B
   A --> B
 ```
 
 ```mermaid
-block-beta
+block
   A space B
   A --> B
 ```
@@ -706,13 +706,13 @@ block-beta
 Applying styles in the wrong context or with incorrect syntax can lead to blocks not being styled as intended:
 
 ```mermaid-example
-  block-beta
+  block
     A
     style A fill#969;
 ```
 
 ```mermaid
-  block-beta
+  block
     A
     style A fill#969;
 ```
@@ -721,14 +721,14 @@ Applying styles in the wrong context or with incorrect syntax can lead to blocks
 Correct the syntax by ensuring proper separation of style properties with commas and using the correct CSS property format:
 
 ```mermaid-example
-block-beta
+block
   A
   style A fill:#969,stroke:#333;
 
 ```
 
 ```mermaid
-block-beta
+block
   A
   style A fill:#969,stroke:#333;
 
diff --git a/packages/mermaid/src/diagrams/block/blockDetector.ts b/packages/mermaid/src/diagrams/block/blockDetector.ts
index c4da643f0..6122221e8 100644
--- a/packages/mermaid/src/diagrams/block/blockDetector.ts
+++ b/packages/mermaid/src/diagrams/block/blockDetector.ts
@@ -3,7 +3,7 @@ import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-a
 const id = 'block';
 
 const detector: DiagramDetector = (txt) => {
-  return /^\s*block-beta/.test(txt);
+  return /^\s*block(-beta)?/.test(txt);
 };
 
 const loader = async () => {
diff --git a/packages/mermaid/src/diagrams/block/parser/block.jison b/packages/mermaid/src/diagrams/block/parser/block.jison
index 88bdf729e..3aa2759a6 100644
--- a/packages/mermaid/src/diagrams/block/parser/block.jison
+++ b/packages/mermaid/src/diagrams/block/parser/block.jison
@@ -36,10 +36,10 @@ CRLF \u000D\u000A
 
 %%
 
-"block-beta"                                             { return 'BLOCK_DIAGRAM_KEY'; }
-"block"\s+            { yy.getLogger().debug('Found space-block'); return 'block';}
-"block"\n+            { yy.getLogger().debug('Found nl-block'); return 'block';}
-"block:"            { yy.getLogger().debug('Found space-block'); return 'id-block';}
+"block-beta"              { yy.getLogger().debug('Found block-beta'); return 'BLOCK_DIAGRAM_KEY'; }
+"block:"                  { yy.getLogger().debug('Found id-block'); return 'id-block'; }
+"block"                   { yy.getLogger().debug('Found block'); return 'BLOCK_DIAGRAM_KEY'; }
+
 // \s*\%\%.*                                                       { yy.getLogger().debug('Found comment',yytext); }
 [\s]+                                                           { yy.getLogger().debug('.', yytext); /* skip all whitespace */  }
 [\n]+ {yy.getLogger().debug('_', yytext);                 /* skip all whitespace */   }
@@ -240,7 +240,7 @@ columnsStatement
 
 blockStatement
   : id-block nodeStatement document end { yy.getLogger().debug('Rule: id-block statement : ', $2, $3); const id2 = yy.generateId(); $$ = { ...$2, type:'composite', children: $3 }; }
-  | block document end { yy.getLogger().debug('Rule: blockStatement : ', $1, $2, $3); const id = yy.generateId(); $$ = { id, type:'composite', label:'', children: $2 }; }
+  | BLOCK_DIAGRAM_KEY document end { yy.getLogger().debug('Rule: blockStatement : ', $1, $2, $3); const id = yy.generateId(); $$ = { id, type:'composite', label:'', children: $2 }; }
   ;
 
 node
diff --git a/packages/mermaid/src/diagrams/block/parser/block.spec.ts b/packages/mermaid/src/diagrams/block/parser/block.spec.ts
index 4bf3290d8..09f3eb02a 100644
--- a/packages/mermaid/src/diagrams/block/parser/block.spec.ts
+++ b/packages/mermaid/src/diagrams/block/parser/block.spec.ts
@@ -22,7 +22,7 @@ describe('Block diagram', function () {
       expect(blocks[0].label).toBe('id');
     });
     it('a node with a square shape and a label', () => {
-      const str = `block-beta
+      const str = `block
           id["A label"]
           `;
 
@@ -34,7 +34,7 @@ describe('Block diagram', function () {
       expect(blocks[0].type).toBe('square');
     });
     it('a diagram with multiple nodes', () => {
-      const str = `block-beta
+      const str = `block
           id1
           id2
       `;
@@ -50,7 +50,7 @@ describe('Block diagram', function () {
       expect(blocks[1].type).toBe('na');
     });
     it('a diagram with multiple nodes', () => {
-      const str = `block-beta
+      const str = `block
           id1
           id2
           id3
@@ -71,7 +71,7 @@ describe('Block diagram', function () {
     });
 
     it('a node with a square shape and a label', () => {
-      const str = `block-beta
+      const str = `block
           id["A label"]
           id2`;
 
@@ -86,7 +86,7 @@ describe('Block diagram', function () {
       expect(blocks[1].type).toBe('na');
     });
     it('a diagram with multiple nodes with edges abc123', () => {
-      const str = `block-beta
+      const str = `block
           id1["first"]  -->   id2["second"]
       `;
 
@@ -100,7 +100,7 @@ describe('Block diagram', function () {
       expect(edges[0].arrowTypeEnd).toBe('arrow_point');
     });
     it('a diagram with multiple nodes with edges abc123', () => {
-      const str = `block-beta
+      const str = `block
           id1["first"]  -- "a label" -->   id2["second"]
       `;
 
@@ -115,7 +115,7 @@ describe('Block diagram', function () {
       expect(edges[0].label).toBe('a label');
     });
     it('a diagram with column statements', () => {
-      const str = `block-beta
+      const str = `block
           columns 2
           block1["Block 1"]
       `;
@@ -126,7 +126,7 @@ describe('Block diagram', function () {
       expect(blocks.length).toBe(1);
     });
     it('a diagram without column statements', () => {
-      const str = `block-beta
+      const str = `block
           block1["Block 1"]
       `;
 
@@ -136,7 +136,7 @@ describe('Block diagram', function () {
       expect(blocks.length).toBe(1);
     });
     it('a diagram with auto column statements', () => {
-      const str = `block-beta
+      const str = `block
           columns auto
           block1["Block 1"]
       `;
@@ -148,7 +148,7 @@ describe('Block diagram', function () {
     });
 
     it('blocks next to each other', () => {
-      const str = `block-beta
+      const str = `block
           columns 2
           block1["Block 1"]
           block2["Block 2"]
@@ -162,7 +162,7 @@ describe('Block diagram', function () {
     });
 
     it('blocks on top of each other', () => {
-      const str = `block-beta
+      const str = `block
           columns 1
           block1["Block 1"]
           block2["Block 2"]
@@ -176,7 +176,7 @@ describe('Block diagram', function () {
     });
 
     it('compound blocks 2', () => {
-      const str = `block-beta
+      const str = `block
           block
             aBlock["ABlock"]
             bBlock["BBlock"]
@@ -204,7 +204,7 @@ describe('Block diagram', function () {
       expect(bBlock.type).toBe('square');
     });
     it('compound blocks of compound blocks', () => {
-      const str = `block-beta
+      const str = `block
           block
             aBlock["ABlock"]
             block
@@ -239,7 +239,7 @@ describe('Block diagram', function () {
       expect(bBlock.type).toBe('square');
     });
     it('compound blocks with title', () => {
-      const str = `block-beta
+      const str = `block
           block:compoundBlock["Compound block"]
             columns 1
             block2["Block 2"]
@@ -264,7 +264,7 @@ describe('Block diagram', function () {
       expect(block2.type).toBe('square');
     });
     it('blocks mixed with compound blocks', () => {
-      const str = `block-beta
+      const str = `block
           columns 1
           block1["Block 1"]
 
@@ -291,7 +291,7 @@ describe('Block diagram', function () {
     });
 
     it('Arrow blocks', () => {
-      const str = `block-beta
+      const str = `block
         columns 3
         block1["Block 1"]
         blockArrow<["   "]>(right)
@@ -315,7 +315,7 @@ describe('Block diagram', function () {
       expect(blockArrow.directions).toContain('right');
     });
     it('Arrow blocks with multiple points', () => {
-      const str = `block-beta
+      const str = `block
         columns 1
         A
         blockArrow<["   "]>(up, down)
@@ -338,7 +338,7 @@ describe('Block diagram', function () {
       expect(blockArrow.directions).not.toContain('right');
     });
     it('blocks with different widths', () => {
-      const str = `block-beta
+      const str = `block
         columns 3
         one["One Slot"]
         two["Two slots"]:2
@@ -353,7 +353,7 @@ describe('Block diagram', function () {
       expect(two.widthInColumns).toBe(2);
     });
     it('empty blocks', () => {
-      const str = `block-beta
+      const str = `block
         columns 3
         space
         middle["In the middle"]
@@ -372,7 +372,7 @@ describe('Block diagram', function () {
       expect(middle.label).toBe('In the middle');
     });
     it('classDef statements applied to a block', () => {
-      const str = `block-beta
+      const str = `block
         classDef black color:#ffffff, fill:#000000;
 
         mc["Memcache"]
@@ -390,7 +390,7 @@ describe('Block diagram', function () {
       expect(black.styles[0]).toEqual('color:#ffffff');
     });
     it('style statements applied to a block', () => {
-      const str = `block-beta
+      const str = `block
 columns 1
     B["A wide one in the middle"]
   style B fill:#f9F,stroke:#333,stroke-width:4px
@@ -406,9 +406,9 @@ columns 1
 
   describe('prototype properties', function () {
     function validateProperty(prop: string) {
-      expect(() => block.parse(`block-beta\n${prop}`)).not.toThrow();
+      expect(() => block.parse(`block\n${prop}`)).not.toThrow();
       expect(() =>
-        block.parse(`block-beta\nA; classDef ${prop} color:#ffffff,fill:#000000; class A ${prop}`)
+        block.parse(`block\nA; classDef ${prop} color:#ffffff,fill:#000000; class A ${prop}`)
       ).not.toThrow();
     }
 
diff --git a/packages/mermaid/src/docs/syntax/block.md b/packages/mermaid/src/docs/syntax/block.md
index ac5b53d5e..985ce1d8c 100644
--- a/packages/mermaid/src/docs/syntax/block.md
+++ b/packages/mermaid/src/docs/syntax/block.md
@@ -8,7 +8,7 @@ outline: 'deep' # shows all h3 headings in outline in Vitepress
 ## Introduction to Block Diagrams
 
 ```mermaid-example
-block-beta
+block
 columns 1
   db(("DB"))
   blockArrowId6<["   "]>(down)
@@ -62,7 +62,7 @@ At its core, a block diagram consists of blocks representing different entities
 To create a simple block diagram with three blocks labeled 'a', 'b', and 'c', the syntax is as follows:
 
 ```mermaid-example
-block-beta
+block
   a b c
 ```
 
@@ -78,7 +78,7 @@ While simple block diagrams are linear and straightforward, more complex systems
 In scenarios where you need to distribute blocks across multiple columns, you can specify the number of columns and arrange the blocks accordingly. Here's how to create a block diagram with three columns and four blocks, where the fourth block appears in a second row:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   a b c d
 ```
@@ -101,7 +101,7 @@ In more complex diagrams, you may need blocks that span multiple columns to emph
 To create a block diagram where one block spans across two columns, you can specify the desired width for each block:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   a["A label"] b:2 c:2 d
 ```
@@ -118,7 +118,7 @@ Composite blocks, or blocks within blocks, are an advanced feature in Mermaid's
 Creating a composite block involves defining a parent block and then nesting other blocks within it. Here's how to define a composite block with nested elements:
 
 ```mermaid-example
-block-beta
+block
     block
       D
     end
@@ -137,7 +137,7 @@ Mermaid also allows for dynamic adjustment of column widths based on the content
 In diagrams with varying block sizes, Mermaid automatically adjusts the column widths to fit the largest block in each column. Here's an example:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   a:3
   block:group1:2
@@ -157,7 +157,7 @@ This example demonstrates how Mermaid dynamically adjusts the width of the colum
 In scenarios where you need to stack blocks horizontally, you can use column width to accomplish the task. Blocks can be arranged vertically by putting them in a single column. Here is how you can create a block diagram in which 4 blocks are stacked on top of each other:
 
 ```mermaid-example
-block-beta
+block
   block
     columns 1
     a["A label"] b c d
@@ -181,7 +181,7 @@ Mermaid supports a range of block shapes to suit different diagramming needs, fr
 To create a block with round edges, which can be used to represent a softer or more flexible component:
 
 ```mermaid-example
-block-beta
+block
     id1("This is the text in the box")
 ```
 
@@ -190,7 +190,7 @@ block-beta
 A stadium-shaped block, resembling an elongated circle, can be used for components that are process-oriented:
 
 ```mermaid-example
-block-beta
+block
     id1(["This is the text in the box"])
 ```
 
@@ -199,7 +199,7 @@ block-beta
 For representing subroutines or contained processes, a block with double vertical lines is useful:
 
 ```mermaid-example
-block-beta
+block
     id1[["This is the text in the box"]]
 ```
 
@@ -208,7 +208,7 @@ block-beta
 The cylindrical shape is ideal for representing databases or storage components:
 
 ```mermaid-example
-block-beta
+block
     id1[("Database")]
 ```
 
@@ -217,7 +217,7 @@ block-beta
 A circle can be used for centralized or pivotal components:
 
 ```mermaid-example
-block-beta
+block
     id1(("This is the text in the circle"))
 ```
 
@@ -228,21 +228,21 @@ For decision points, use a rhombus, and for unique or specialized processes, asy
 **Asymmetric**
 
 ```mermaid-example
-block-beta
+block
   id1>"This is the text in the box"]
 ```
 
 **Rhombus**
 
 ```mermaid-example
-block-beta
+block
     id1{"This is the text in the box"}
 ```
 
 **Hexagon**
 
 ```mermaid-example
-block-beta
+block
     id1{{"This is the text in the box"}}
 ```
 
@@ -251,7 +251,7 @@ block-beta
 Parallelogram and trapezoid shapes are perfect for inputs/outputs and transitional processes:
 
 ```mermaid-example
-block-beta
+block
   id1[/"This is the text in the box"/]
   id2[\"This is the text in the box"\]
   A[/"Christmas"\]
@@ -263,7 +263,7 @@ block-beta
 For highlighting critical or high-priority components, a double circle can be effective:
 
 ```mermaid-example
-block-beta
+block
     id1((("This is the text in the circle")))
 ```
 
@@ -276,7 +276,7 @@ Mermaid also offers unique shapes like block arrows and space blocks for directi
 Block arrows can visually indicate direction or flow within a process:
 
 ```mermaid-example
-block-beta
+block
   blockArrowId<["Label"]>(right)
   blockArrowId2<["Label"]>(left)
   blockArrowId3<["Label"]>(up)
@@ -291,7 +291,7 @@ block-beta
 Space blocks can be used to create intentional empty spaces in the diagram, which is useful for layout and readability:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   a space b
   c   d   e
@@ -300,7 +300,7 @@ block-beta
 or
 
 ```mermaid-example
-block-beta
+block
   ida space:3 idb idc
 ```
 
@@ -325,7 +325,7 @@ The most fundamental aspect of connecting blocks is the use of arrows or links.
 A simple link with an arrow can be created to show direction or flow from one block to another:
 
 ```mermaid-example
-block-beta
+block
   A space B
   A-->B
 ```
@@ -342,7 +342,7 @@ Example - Text with Links
 To add text to a link, the syntax includes the text within the link definition:
 
 ```mermaid-example
-block-beta
+block
   A space:2 B
   A-- "X" -->B
 ```
@@ -352,7 +352,7 @@ This example show how to add descriptive text to the links, enhancing the inform
 Example - Edges and Styles:
 
 ```mermaid-example
-block-beta
+block
 columns 1
   db(("DB"))
   blockArrowId6<["   "]>(down)
@@ -381,7 +381,7 @@ Mermaid enables detailed styling of individual blocks, allowing you to apply var
 To apply custom styles to a block, you can use the `style` keyword followed by the block identifier and the desired CSS properties:
 
 ```mermaid-example
-block-beta
+block
   id1 space id2
   id1("Start")-->id2("Stop")
   style id1 fill:#636,stroke:#333,stroke-width:4px
@@ -395,7 +395,7 @@ Mermaid enables applying styling to classes, which could make styling easier if
 #### Example - Styling a Single Class
 
 ```mermaid-example
-block-beta
+block
   A space B
   A-->B
   classDef blue fill:#6e6ce6,stroke:#333,stroke-width:4px;
@@ -420,7 +420,7 @@ Combining the elements of structure, linking, and styling, we can create compreh
 Illustrating a simple software system architecture with interconnected components:
 
 ```mermaid
-block-beta
+block
   columns 3
   Frontend blockArrowId6<[" "]>(right) Backend
   space:2 down<[" "]>(down)
@@ -439,7 +439,7 @@ This example shows a basic architecture with a frontend, backend, and database.
 Representing a business process flow with decision points and multiple stages:
 
 ```mermaid-example
-block-beta
+block
   columns 3
   Start(("Start")) space:2
   down<[" "]>(down) space:2
@@ -468,7 +468,7 @@ Understanding and avoiding common syntax errors is key to a smooth experience wi
 A common mistake is incorrect linking syntax, which can lead to unexpected results or broken diagrams:
 
 ```
-block-beta
+block
   A - B
 ```
 
@@ -476,7 +476,7 @@ block-beta
 Ensure that links between blocks are correctly specified with arrows (--> or ---) to define the direction and type of connection. Also remember that one of the fundamentals for block diagram is to give the author full control of where the boxes are positioned so in the example you need to add a space between the boxes:
 
 ```mermaid-example
-block-beta
+block
   A space B
   A --> B
 ```
@@ -486,7 +486,7 @@ block-beta
 Applying styles in the wrong context or with incorrect syntax can lead to blocks not being styled as intended:
 
 ```mermaid-example
-  block-beta
+  block
     A
     style A fill#969;
 ```
@@ -495,7 +495,7 @@ Applying styles in the wrong context or with incorrect syntax can lead to blocks
 Correct the syntax by ensuring proper separation of style properties with commas and using the correct CSS property format:
 
 ```mermaid-example
-block-beta
+block
   A
   style A fill:#969,stroke:#333;
 

From 98442294edf7bdf0ed5f38c63a0b7c5f1bf5e7e8 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Tue, 17 Jun 2025 13:44:12 +0530
Subject: [PATCH 023/314] resolved PR comments

---
 packages/mermaid/src/diagrams/pie/pie.spec.ts | 28 ++++++++++++-------
 1 file changed, 18 insertions(+), 10 deletions(-)

diff --git a/packages/mermaid/src/diagrams/pie/pie.spec.ts b/packages/mermaid/src/diagrams/pie/pie.spec.ts
index 2b50f5604..7b898ca8f 100644
--- a/packages/mermaid/src/diagrams/pie/pie.spec.ts
+++ b/packages/mermaid/src/diagrams/pie/pie.spec.ts
@@ -141,20 +141,28 @@ describe('pie', () => {
 
     it('should handle simple pie with zero slice value', async () => {
       await expect(async () => {
-        await parser.parse(`pie
-        "ash" : 0
-        "bat" : 40.12
-        `);
-      }).rejects.toThrowError();
+        await parser.parse(`pie title Default text position: Animal adoption
+        accTitle: simple pie char demo
+        accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
+         "dogs" : 0
+        "rats" : 40.12
+    `);
+      }).rejects.toThrowError(
+        '"dogs" has invalid value: 0. Zero and negative values are not allowed in pie charts. All slice values must be > 0'
+      );
     });
 
     it('should handle simple pie with negative slice value', async () => {
       await expect(async () => {
-        await parser.parse(`pie
-        "ash" : -60
-        "bat" : 40.12
-        `);
-      }).rejects.toThrowError();
+        await parser.parse(`pie title Default text position: Animal adoption
+        accTitle: simple pie char demo
+        accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
+         "dogs" : -60.67
+        "rats" : 40.12
+    `);
+      }).rejects.toThrowError(
+        '"dogs" has invalid value: -60.67. Zero and negative values are not allowed in pie charts. All slice values must be > 0'
+      );
     });
 
     it('should handle unsafe properties', async () => {

From 8224a81ab6a9484053465de985eb377358988af5 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Tue, 17 Jun 2025 13:52:04 +0530
Subject: [PATCH 024/314] fix xychart test cases

---
 packages/mermaid/src/diagrams/xychart/parser/xychart.jison      | 2 +-
 .../mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/mermaid/src/diagrams/xychart/parser/xychart.jison b/packages/mermaid/src/diagrams/xychart/parser/xychart.jison
index 8a47b1aac..33575b946 100644
--- a/packages/mermaid/src/diagrams/xychart/parser/xychart.jison
+++ b/packages/mermaid/src/diagrams/xychart/parser/xychart.jison
@@ -29,8 +29,8 @@
 "{"                 { this.popState(); }
 [^\}]*               { return "acc_descr_multiline_value"; }
 
-"xychart"                                 {return 'XYCHART';}
 "xychart-beta"                            {return 'XYCHART';}
+"xychart"                                 {return 'XYCHART';}
 (?:"vertical"|"horizontal")               {return 'CHART_ORIENTATION'}
 
 "x-axis"                                  { this.pushState("axis_data"); return "X_AXIS"; }
diff --git a/packages/mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts b/packages/mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts
index ee7c94581..409972828 100644
--- a/packages/mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts
+++ b/packages/mermaid/src/diagrams/xychart/parser/xychart.jison.spec.ts
@@ -407,7 +407,7 @@ describe('Testing xychart jison file', () => {
   it('parse multiple bar and line variant 2', () => {
     const str = `
     xychart horizontal
-    title Basic xychart
+    title "Basic xychart"
     x-axis "this is x axis" [category1, "category 2", category3]
     y-axis yaxisText 10 --> 150
  bar barTitle1 [23, 45, 56.6]

From 2c0931da46794b49d2523211e25f782900c34e94 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Tue, 17 Jun 2025 15:53:25 +0530
Subject: [PATCH 025/314] added changeset

---
 .changeset/chatty-lemons-stick.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/chatty-lemons-stick.md

diff --git a/.changeset/chatty-lemons-stick.md b/.changeset/chatty-lemons-stick.md
new file mode 100644
index 000000000..d0a8a4d58
--- /dev/null
+++ b/.changeset/chatty-lemons-stick.md
@@ -0,0 +1,5 @@
+---
+'mermaid': patch
+---
+
+Removed the "Beta" suffix from the XYChart, Block , Sankey diagrams to reflect their stable status

From 3920ad442dbc79bb1d42e34081ee90965bd3c8f5 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Mon, 23 Jun 2025 15:08:11 +0530
Subject: [PATCH 026/314] resolved PR comments

---
 docs/syntax/pie.md                            |  2 +-
 packages/mermaid/src/diagrams/pie/pie.spec.ts | 14 +++++++-------
 packages/mermaid/src/diagrams/pie/pieDb.ts    |  4 ++--
 packages/mermaid/src/docs/syntax/pie.md       |  2 +-
 4 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/docs/syntax/pie.md b/docs/syntax/pie.md
index 87fb3fc58..6a2eefb27 100644
--- a/docs/syntax/pie.md
+++ b/docs/syntax/pie.md
@@ -40,7 +40,7 @@ Drawing a pie chart is really simple in mermaid.
 **Note:**
 
 > Pie chart values must be **positive numbers greater than zero**.\
-> **Zero and negative values are not allowed** and will result in an error.
+> **Negative values are not allowed** and will result in an error.
 
 \[pie] \[showData] (OPTIONAL)
 \[title] \[titlevalue] (OPTIONAL)
diff --git a/packages/mermaid/src/diagrams/pie/pie.spec.ts b/packages/mermaid/src/diagrams/pie/pie.spec.ts
index 7b898ca8f..d5d39692f 100644
--- a/packages/mermaid/src/diagrams/pie/pie.spec.ts
+++ b/packages/mermaid/src/diagrams/pie/pie.spec.ts
@@ -140,16 +140,16 @@ describe('pie', () => {
     });
 
     it('should handle simple pie with zero slice value', async () => {
-      await expect(async () => {
-        await parser.parse(`pie title Default text position: Animal adoption
+      await parser.parse(`pie title Default text position: Animal adoption
         accTitle: simple pie char demo
         accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
          "dogs" : 0
         "rats" : 40.12
-    `);
-      }).rejects.toThrowError(
-        '"dogs" has invalid value: 0. Zero and negative values are not allowed in pie charts. All slice values must be > 0'
-      );
+      `);
+
+      const sections = db.getSections();
+      expect(sections.get('dogs')).toBe(0);
+      expect(sections.get('rats')).toBe(40.12);
     });
 
     it('should handle simple pie with negative slice value', async () => {
@@ -161,7 +161,7 @@ describe('pie', () => {
         "rats" : 40.12
     `);
       }).rejects.toThrowError(
-        '"dogs" has invalid value: -60.67. Zero and negative values are not allowed in pie charts. All slice values must be > 0'
+        '"dogs" has invalid value: -60.67.Negative values are not allowed in pie charts. All slice values must be > 0'
       );
     });
 
diff --git a/packages/mermaid/src/diagrams/pie/pieDb.ts b/packages/mermaid/src/diagrams/pie/pieDb.ts
index ade44c331..d5c66ff78 100644
--- a/packages/mermaid/src/diagrams/pie/pieDb.ts
+++ b/packages/mermaid/src/diagrams/pie/pieDb.ts
@@ -34,9 +34,9 @@ const clear = (): void => {
 };
 
 const addSection = ({ label, value }: D3Section): void => {
-  if (value <= 0) {
+  if (value < 0) {
     throw new Error(
-      `"${label}" has invalid value: ${value}. Zero and negative values are not allowed in pie charts. All slice values must be > 0.`
+      `"${label}" has invalid value: ${value}.Negative values are not allowed in pie charts. All slice values must be > 0.`
     );
   }
   if (!sections.has(label)) {
diff --git a/packages/mermaid/src/docs/syntax/pie.md b/packages/mermaid/src/docs/syntax/pie.md
index 4ad0dc4e0..416119b5b 100644
--- a/packages/mermaid/src/docs/syntax/pie.md
+++ b/packages/mermaid/src/docs/syntax/pie.md
@@ -27,7 +27,7 @@ Drawing a pie chart is really simple in mermaid.
 **Note:**
 
 > Pie chart values must be **positive numbers greater than zero**.  
-> **Zero and negative values are not allowed** and will result in an error.
+> **Negative values are not allowed** and will result in an error.
 
 [pie] [showData] (OPTIONAL)
 [title] [titlevalue] (OPTIONAL)

From 24257de8a631cd61604eb08015e32cf2fab4acc7 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Tue, 24 Jun 2025 14:50:20 +0530
Subject: [PATCH 027/314] fix state diagram edge label position

---
 .../rendering-util/rendering-elements/edges.js  |  4 ++++
 packages/mermaid/src/utils.ts                   | 17 +++++++++++++++++
 2 files changed, 21 insertions(+)

diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
index a97668d5f..154040d87 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js
+++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
@@ -638,6 +638,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
 
   addEdgeMarkers(svgPath, edge, url, id, diagramType, strokeColor);
 
+  if (!utils.isPointInDAttr(points, svgPath.attr('d'))) {
+    pointsHasChanged = true;
+  }
+
   let paths = {};
   if (pointsHasChanged) {
     paths.updatedPath = points;
diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts
index 6ed935cf6..61157f827 100644
--- a/packages/mermaid/src/utils.ts
+++ b/packages/mermaid/src/utils.ts
@@ -884,6 +884,7 @@ export default {
   runFunc,
   entityDecode,
   insertTitle,
+  isPointInDAttr,
   parseFontSize,
   InitIDGenerator,
 };
@@ -960,3 +961,19 @@ export function handleUndefinedAttr(
 ) {
   return attrValue ?? null;
 }
+
+export function isPointInDAttr(points: Point[], dAttr: string) {
+  if (!points || points.length < 2 || !dAttr) {
+    return false;
+  }
+
+  const point = points[1];
+  const roundedX = Math.round(point.x);
+  const roundedY = Math.round(point.y);
+
+  const sanitizedD = dAttr.replace(/(\d+\.\d+)/g, (match) =>
+    Math.round(parseFloat(match)).toString()
+  );
+
+  return sanitizedD.includes(roundedX.toString()) || sanitizedD.includes(roundedY.toString());
+}

From 33e08daf175125295a06b1b80279437004a4e865 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Tue, 24 Jun 2025 15:10:36 +0530
Subject: [PATCH 028/314] added changeset

---
 .changeset/cold-sites-accept.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/cold-sites-accept.md

diff --git a/.changeset/cold-sites-accept.md b/.changeset/cold-sites-accept.md
new file mode 100644
index 000000000..4166d40d4
--- /dev/null
+++ b/.changeset/cold-sites-accept.md
@@ -0,0 +1,5 @@
+---
+'mermaid': patch
+---
+
+edge label in the state diagram was not positioned correctly relative to the edge

From 579c22cf5d94dc038206ad95d812f0f36c4b28b8 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Tue, 24 Jun 2025 17:08:42 +0530
Subject: [PATCH 029/314] refactor code

---
 .../rendering-util/rendering-elements/edges.js |  5 +++--
 packages/mermaid/src/utils.ts                  | 18 +++++++++++-------
 2 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
index 154040d87..db48e313c 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js
+++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js
@@ -637,8 +637,9 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
   log.info('arrowTypeEnd', edge.arrowTypeEnd);
 
   addEdgeMarkers(svgPath, edge, url, id, diagramType, strokeColor);
-
-  if (!utils.isPointInDAttr(points, svgPath.attr('d'))) {
+  const midIndex = Math.floor(points.length / 2);
+  const point = points[midIndex];
+  if (!utils.isLabelCoordinateInPath(point, svgPath.attr('d'))) {
     pointsHasChanged = true;
   }
 
diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts
index 61157f827..0f1bfbecf 100644
--- a/packages/mermaid/src/utils.ts
+++ b/packages/mermaid/src/utils.ts
@@ -884,7 +884,7 @@ export default {
   runFunc,
   entityDecode,
   insertTitle,
-  isPointInDAttr,
+  isLabelCoordinateInPath,
   parseFontSize,
   InitIDGenerator,
 };
@@ -962,12 +962,16 @@ export function handleUndefinedAttr(
   return attrValue ?? null;
 }
 
-export function isPointInDAttr(points: Point[], dAttr: string) {
-  if (!points || points.length < 2 || !dAttr) {
-    return false;
-  }
-
-  const point = points[1];
+/**
+ * Checks if the  x or y coordinate of the edge label
+ * appears in the given SVG path data string.
+ *
+ * @param point  - The Point object with x and y properties to check.
+ * @param dAttr  - SVG path data string (the 'd' attribute of an SVG path element).
+ * @returns      - True if the rounded x or y coordinate of the edge label is found
+ *                 in the sanitized path data string; otherwise, false.
+ */
+export function isLabelCoordinateInPath(point: Point, dAttr: string) {
   const roundedX = Math.round(point.x);
   const roundedY = Math.round(point.y);
 

From 6621f6ddb2eabff58c10d82a54bc42d7660f1cca Mon Sep 17 00:00:00 2001
From: Khizar Hasan <83395218+zarhasan@users.noreply.github.com>
Date: Wed, 25 Jun 2025 16:25:47 +0530
Subject: [PATCH 030/314] Update integrations-community.md

Added 'WP Documentation' in integrations.
---
 packages/mermaid/src/docs/ecosystem/integrations-community.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/mermaid/src/docs/ecosystem/integrations-community.md b/packages/mermaid/src/docs/ecosystem/integrations-community.md
index f28570196..4eacc294e 100644
--- a/packages/mermaid/src/docs/ecosystem/integrations-community.md
+++ b/packages/mermaid/src/docs/ecosystem/integrations-community.md
@@ -98,6 +98,7 @@ Blogging frameworks and platforms
   - [Mermaid](https://nextra.site/docs/guide/mermaid)
 - [WordPress](https://wordpress.org)
   - [MerPRess](https://wordpress.org/plugins/merpress/)
+  - [WP Documentation](https://wordpress.org/themes/wp-documentation/)
 
 ### CMS/ECM
 

From e70be4f1553bb61d1ecc183aca9186a5b8fd7fef Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Wed, 25 Jun 2025 11:00:55 +0000
Subject: [PATCH 031/314] [autofix.ci] apply automated fixes

---
 docs/ecosystem/integrations-community.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docs/ecosystem/integrations-community.md b/docs/ecosystem/integrations-community.md
index 601c4699d..a0b4794d3 100644
--- a/docs/ecosystem/integrations-community.md
+++ b/docs/ecosystem/integrations-community.md
@@ -103,6 +103,7 @@ Blogging frameworks and platforms
   - [Mermaid](https://nextra.site/docs/guide/mermaid)
 - [WordPress](https://wordpress.org)
   - [MerPRess](https://wordpress.org/plugins/merpress/)
+  - [WP Documentation](https://wordpress.org/themes/wp-documentation/)
 
 ### CMS/ECM
 

From cc476d59d1440d919f5a32ab4a393fd93138a926 Mon Sep 17 00:00:00 2001
From: omkarht 
Date: Fri, 27 Jun 2025 13:42:19 +0530
Subject: [PATCH 032/314] 6637-Add new participant types to Sequence Diagrams

---
 .../sequence/parser/sequenceDiagram.jison     |  25 +
 .../src/diagrams/sequence/sequenceDb.ts       |  11 +
 .../src/diagrams/sequence/sequenceRenderer.ts |  12 +-
 .../mermaid/src/diagrams/sequence/styles.js   |   6 +
 .../mermaid/src/diagrams/sequence/svgDraw.js  | 615 ++++++++++++++++++
 5 files changed, 666 insertions(+), 3 deletions(-)

diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
index d2e81df5f..4b5a82851 100644
--- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
+++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
@@ -31,6 +31,12 @@
 "box"															{ this.begin('LINE'); return 'box'; }
 "participant"                                                   { this.begin('ID'); return 'participant'; }
 "actor"                                                   		{ this.begin('ID'); return 'participant_actor'; }
+"boundary"                                                      { this.begin('ID'); return 'participant_boundary'; }
+"control"                                                       { this.begin('ID'); return 'participant_control'; }
+"entity"                                                        { this.begin('ID'); return 'participant_entity'; }
+"database"                                                      { this.begin('ID'); return 'participant_database'; }
+"collections"                                                   { this.begin('ID'); return 'participant_collections'; }
+"queue"                                                         { this.begin('ID'); return 'participant_queue'; }
 "create"                                                        return 'create';
 "destroy"                                                       { this.begin('ID'); return 'destroy'; }
 [^\<->\->:\n,;]+?([\-]*[^\<->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$)     { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
@@ -231,6 +237,25 @@ participant_statement
 	| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
 	| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
 	| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
+
+	| 'participant_boundary' actor 'AS' restOfLine 'NEWLINE' {$2.draw='boundary'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
+	| 'participant_boundary' actor 'NEWLINE' {$2.draw='boundary'; $2.type='addParticipant'; $$=$2;}
+
+	| 'participant_control' actor 'AS' restOfLine 'NEWLINE' {$2.draw='control'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
+	| 'participant_control' actor 'NEWLINE' {$2.draw='control'; $2.type='addParticipant'; $$=$2;}
+
+	| 'participant_entity' actor 'AS' restOfLine 'NEWLINE' {$2.draw='entity'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
+	| 'participant_entity' actor 'NEWLINE' {$2.draw='entity'; $2.type='addParticipant'; $$=$2;}
+
+	| 'participant_database' actor 'AS' restOfLine 'NEWLINE' {$2.draw='database'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
+	| 'participant_database' actor 'NEWLINE' {$2.draw='database'; $2.type='addParticipant'; $$=$2;}
+
+	| 'participant_collections' actor 'AS' restOfLine 'NEWLINE' {$2.draw='collections'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
+	| 'participant_collections' actor 'NEWLINE' {$2.draw='collections'; $2.type='addParticipant'; $$=$2;}
+
+	| 'participant_queue' actor 'AS' restOfLine 'NEWLINE' {$2.draw='queue'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
+	| 'participant_queue' actor 'NEWLINE' {$2.draw='queue'; $2.type='addParticipant'; $$=$2;}
+
 	;
 
 note_statement
diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts
index c6b44dac0..d8421b84d 100644
--- a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts
+++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts
@@ -75,6 +75,17 @@ const PLACEMENT = {
   OVER: 2,
 } as const;
 
+export const PARTICIPANT_TYPE = {
+  ACTOR: 'actor',
+  BOUNDARY: 'boundary',
+  COLLECTIONS: 'collections',
+  CONTROL: 'control',
+  DATABASE: 'database',
+  ENTITY: 'entity',
+  PARTICIPANT: 'participant',
+  QUEUE: 'queue',
+} as const;
+
 export class SequenceDB implements DiagramDB {
   private readonly state = new ImperativeState(() => ({
     prevActor: undefined,
diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
index cfba92b79..8b4842fc1 100644
--- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
+++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
@@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth.js';
 import utils from '../../utils.js';
 import { configureSvgSize } from '../../setupGraphViewbox.js';
 import type { Diagram } from '../../Diagram.js';
+import { PARTICIPANT_TYPE } from './sequenceDb.js';
 
 let conf = {};
 
@@ -724,11 +725,14 @@ function adjustCreatedDestroyedData(
       msgModel.startx = msgModel.startx - adjustment;
     }
   }
+  const actorArray = [PARTICIPANT_TYPE.ACTOR, PARTICIPANT_TYPE.CONTROL, PARTICIPANT_TYPE.ENTITY];
 
   // if it is a create message
   if (createdActors.get(msg.to) == index) {
     const actor = actors.get(msg.to);
-    const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
+    const adjustment = actorArray.includes(actor.type)
+      ? ACTOR_TYPE_WIDTH / 2 + 3
+      : actor.width / 2 + 3;
     receiverAdjustment(actor, adjustment);
     actor.starty = lineStartY - actor.height / 2;
     bounds.bumpVerticalPos(actor.height / 2);
@@ -737,7 +741,7 @@ function adjustCreatedDestroyedData(
   else if (destroyedActors.get(msg.from) == index) {
     const actor = actors.get(msg.from);
     if (conf.mirrorActors) {
-      const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
+      const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
       senderAdjustment(actor, adjustment);
     }
     actor.stopy = lineStartY - actor.height / 2;
@@ -747,7 +751,9 @@ function adjustCreatedDestroyedData(
   else if (destroyedActors.get(msg.to) == index) {
     const actor = actors.get(msg.to);
     if (conf.mirrorActors) {
-      const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
+      const adjustment = actorArray.includes(actor.type)
+        ? ACTOR_TYPE_WIDTH / 2 + 3
+        : actor.width / 2 + 3;
       receiverAdjustment(actor, adjustment);
     }
     actor.stopy = lineStartY - actor.height / 2;
diff --git a/packages/mermaid/src/diagrams/sequence/styles.js b/packages/mermaid/src/diagrams/sequence/styles.js
index 5c36b4ed1..3cee9d3dc 100644
--- a/packages/mermaid/src/diagrams/sequence/styles.js
+++ b/packages/mermaid/src/diagrams/sequence/styles.js
@@ -12,6 +12,11 @@ const getStyles = (options) =>
   .actor-line {
     stroke: ${options.actorLineColor};
   }
+  
+  .innerArc {
+    stroke-width: 1.5;
+    stroke-dasharray: none;
+  }
 
   .messageLine0 {
     stroke-width: 1.5;
@@ -115,6 +120,7 @@ const getStyles = (options) =>
     fill: ${options.actorBkg};
     stroke-width: 2px;
   }
+
 `;
 
 export default getStyles;
diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js
index c681c9491..3af3d3cf0 100644
--- a/packages/mermaid/src/diagrams/sequence/svgDraw.js
+++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js
@@ -3,6 +3,8 @@ import * as svgDrawCommon from '../common/svgDrawCommon.js';
 import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
 import { sanitizeUrl } from '@braintree/sanitize-url';
 import * as configApi from '../../config.js';
+import { createInnerCylinderPathD } from '../../rendering-util/rendering-elements/shapes/tiltedCylinder.js';
+import { createCylinderPathD } from '../../rendering-util/rendering-elements/shapes/cylinder.js';
 
 export const ACTOR_TYPE_WIDTH = 18 * 2;
 const TOP_ACTOR_CLASS = 'actor-top';
@@ -411,6 +413,607 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
   return height;
 };
 
+/**
+ * Draws an actor in the diagram with the attached line
+ *
+ * @param {any} elem - The diagram we'll draw to.
+ * @param {any} actor - The actor to draw.
+ * @param {any} conf - DrawText implementation discriminator object
+ * @param {boolean} isFooter - If the actor is the footer one
+ */
+const drawActorTypeCollections = function (elem, actor, conf, isFooter) {
+  const actorY = isFooter ? actor.stopy : actor.starty;
+  const center = actor.x + actor.width / 2;
+  const centerY = actorY + actor.height;
+
+  const boxplusLineGroup = elem.append('g').lower();
+  var g = boxplusLineGroup;
+
+  if (!isFooter) {
+    actorCnt++;
+    if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
+      g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
+    }
+    g.append('line')
+      .attr('id', 'actor' + actorCnt)
+      .attr('x1', center)
+      .attr('y1', centerY)
+      .attr('x2', center)
+      .attr('y2', 2000)
+      .attr('class', 'actor-line 200')
+      .attr('stroke-width', '0.5px')
+      .attr('stroke', '#999')
+      .attr('name', actor.name);
+
+    g = boxplusLineGroup.append('g');
+    actor.actorCnt = actorCnt;
+
+    if (actor.links != null) {
+      g.attr('id', 'root-' + actorCnt);
+    }
+  }
+
+  const rect = svgDrawCommon.getNoteRect();
+  var cssclass = 'actor';
+  if (actor.properties?.class) {
+    cssclass = actor.properties.class;
+  } else {
+    rect.fill = '#eaeaea';
+  }
+  if (isFooter) {
+    cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
+  } else {
+    cssclass += ` ${TOP_ACTOR_CLASS}`;
+  }
+  rect.x = actor.x;
+  rect.y = actorY;
+  rect.width = actor.width;
+  rect.height = actor.height;
+  rect.class = cssclass;
+  // rect.rx = 3;
+  // rect.ry = 3;
+  rect.name = actor.name;
+
+  // 🔹 DRAW STACKED RECTANGLES
+  const offset = 6;
+  const shadowRect = {
+    ...rect,
+    x: rect.x + offset,
+    y: rect.y + (isFooter ? +offset : -offset),
+    class: 'actor',
+  };
+  drawRect(g, shadowRect);
+  const rectElem = drawRect(g, rect); // draw main rectangle on top
+  actor.rectData = rect;
+
+  if (actor.properties?.icon) {
+    const iconSrc = actor.properties.icon.trim();
+    if (iconSrc.charAt(0) === '@') {
+      svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
+    } else {
+      svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
+    }
+  }
+
+  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+    actor.description,
+    g,
+    rect.x,
+    rect.y,
+    rect.width,
+    rect.height,
+    { class: `actor ${ACTOR_BOX_CLASS}` },
+    conf
+  );
+
+  let height = actor.height;
+  if (rectElem.node) {
+    const bounds = rectElem.node().getBBox();
+    actor.height = bounds.height;
+    height = bounds.height;
+  }
+
+  return height;
+};
+
+const drawActorTypeQueue = function (elem, actor, conf, isFooter) {
+  const actorY = isFooter ? actor.stopy : actor.starty;
+  const center = actor.x + actor.width / 2;
+  const centerY = actorY + actor.height;
+
+  const boxplusLineGroup = elem.append('g').lower();
+  let g = boxplusLineGroup;
+
+  if (!isFooter) {
+    actorCnt++;
+    if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
+      g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
+    }
+    g.append('line')
+      .attr('id', 'actor' + actorCnt)
+      .attr('x1', center)
+      .attr('y1', centerY)
+      .attr('x2', center)
+      .attr('y2', 2000)
+      .attr('class', 'actor-line 200')
+      .attr('stroke-width', '0.5px')
+      .attr('stroke', '#999')
+      .attr('name', actor.name);
+
+    g = boxplusLineGroup.append('g');
+    actor.actorCnt = actorCnt;
+
+    if (actor.links != null) {
+      g.attr('id', 'root-' + actorCnt);
+    }
+  }
+
+  const rect = svgDrawCommon.getNoteRect();
+  let cssclass = 'actor';
+  if (actor.properties?.class) {
+    cssclass = actor.properties.class;
+  } else {
+    rect.fill = '#eaeaea';
+  }
+
+  if (isFooter) {
+    cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
+  } else {
+    cssclass += ` ${TOP_ACTOR_CLASS}`;
+  }
+
+  rect.x = actor.x;
+  rect.y = actorY;
+  rect.width = actor.width;
+  rect.height = actor.height;
+  rect.class = cssclass;
+  rect.name = actor.name;
+
+  // Cylinder dimensions
+  const ry = rect.height / 2;
+  const rx = ry / (2.5 + rect.height / 50);
+
+  // Cylinder base group
+  const cylinderGroup = g.append('g');
+  const cylinderArc = g.append('g');
+
+  // Main cylinder body
+  cylinderGroup
+    .append('path')
+    .attr(
+      'd',
+      `
+    M ${rect.x},${rect.y + ry}
+    a ${rx},${ry} 0 0 0 0,${rect.height}
+    h ${rect.width - 2 * rx}
+    a ${rx},${ry} 0 0 0 0,-${rect.height}
+    Z
+  `
+    )
+    .attr('class', cssclass);
+  cylinderArc
+    .append('path')
+    .attr('d', createInnerCylinderPathD(rect.x - rx, rect.y + ry, rect.width, rect.height, rx, ry))
+    .attr('stroke', '#666')
+    .attr('stroke-width', '1px')
+    .attr('class', cssclass);
+
+  if (!isFooter) {
+    cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2) - rect.y})`);
+    cylinderArc.attr('transform', `translate(${rect.width / 2}, ${rect.height / 2})`);
+  } else {
+    cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`);
+    cylinderArc.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`);
+  }
+
+  actor.rectData = rect;
+
+  if (actor.properties?.icon) {
+    const iconSrc = actor.properties.icon.trim();
+    const iconX = rect.x + rect.width - 20;
+    const iconY = rect.y + 10;
+    if (iconSrc.charAt(0) === '@') {
+      svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1));
+    } else {
+      svgDrawCommon.drawImage(g, iconX, iconY, iconSrc);
+    }
+  }
+
+  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+    actor.description,
+    g,
+    rect.x,
+    rect.y,
+    rect.width,
+    rect.height,
+    { class: `actor ${ACTOR_BOX_CLASS}` },
+    conf
+  );
+
+  let height = actor.height;
+  const lastPath = cylinderGroup.select('path:last-child');
+  if (lastPath.node()) {
+    const bounds = lastPath.node().getBBox();
+    actor.height = bounds.height;
+    height = bounds.height;
+  }
+
+  return height;
+};
+
+const drawActorTypeControl = function (elem, actor, conf, isFooter) {
+  const actorY = isFooter ? actor.stopy : actor.starty;
+  const center = actor.x + actor.width / 2;
+  const centerY = actorY + 75;
+
+  const line = elem.append('g').lower();
+
+  if (!isFooter) {
+    actorCnt++;
+    line
+      .append('line')
+      .attr('id', 'actor' + actorCnt)
+      .attr('x1', center)
+      .attr('y1', centerY)
+      .attr('x2', center)
+      .attr('y2', 2000)
+      .attr('class', 'actor-line 200')
+      .attr('stroke-width', '0.5px')
+      .attr('stroke', '#999')
+      .attr('name', actor.name);
+
+    actor.actorCnt = actorCnt;
+  }
+  const actElem = elem.append('g');
+  let cssClass = ACTOR_MAN_FIGURE_CLASS;
+  if (isFooter) {
+    cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
+  } else {
+    cssClass += ` ${TOP_ACTOR_CLASS}`;
+  }
+  actElem.attr('class', cssClass);
+  actElem.attr('name', actor.name);
+
+  const rect = svgDrawCommon.getNoteRect();
+  rect.x = actor.x;
+  rect.y = actorY;
+  rect.fill = '#eaeaea';
+  rect.width = actor.width;
+  rect.height = actor.height;
+  rect.class = 'actor';
+
+  const cx = actor.x + actor.width / 2;
+  const cy = actorY + 30;
+  const r = 18;
+
+  actElem
+    .append('defs')
+    .append('marker')
+    .attr('id', 'filled-head-control')
+    .attr('refX', 15.5)
+    .attr('refY', 7)
+    .attr('markerWidth', 20)
+    .attr('markerHeight', 28)
+    .attr('orient', '180')
+    .append('path')
+    .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
+
+  // Draw the base circle
+  actElem
+    .append('circle')
+    .attr('cx', cx)
+    .attr('cy', cy)
+    .attr('r', r)
+    .attr('fill', '#eaeaf7')
+    .attr('stroke', '#666')
+    .attr('stroke-width', 1.2);
+
+  // Draw looping arrow as arc path
+  actElem
+    .append('path')
+    .attr('d', `M ${cx},${cy - r}`)
+    .attr('stroke-width', 1.5)
+    .attr('marker-end', 'url(#filled-head-control)');
+
+  const bounds = actElem.node().getBBox();
+  actor.height = bounds.height + conf.boxTextMargin;
+
+  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+    actor.description,
+    actElem,
+    rect.x,
+    rect.y + (!isFooter ? 30 : 40),
+    rect.width,
+    rect.height,
+    { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
+    conf
+  );
+
+  return actor.height;
+};
+
+const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
+  const actorY = isFooter ? actor.stopy : actor.starty;
+  const center = actor.x + actor.width / 2;
+  const centerY = actorY + 75;
+
+  const line = elem.append('g').lower();
+
+  const actElem = elem.append('g');
+  let cssClass = ACTOR_MAN_FIGURE_CLASS;
+  if (isFooter) {
+    cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
+  } else {
+    cssClass += ` ${TOP_ACTOR_CLASS}`;
+  }
+  actElem.attr('class', cssClass);
+  actElem.attr('name', actor.name);
+
+  const rect = svgDrawCommon.getNoteRect();
+  rect.x = actor.x;
+  rect.y = actorY;
+  rect.fill = '#eaeaea';
+  rect.width = actor.width;
+  rect.height = actor.height;
+  rect.class = 'actor';
+
+  const cx = actor.x + actor.width / 2;
+  const cy = actorY + 25;
+  const r = 18;
+
+  actElem
+    .append('circle')
+    .attr('cx', cx)
+    .attr('cy', cy)
+    .attr('r', r)
+    .attr('width', actor.width)
+    .attr('height', actor.height);
+
+  actElem
+    .append('line')
+    .attr('x1', cx - r)
+    .attr('x2', cx + r)
+    .attr('y1', cy + r)
+    .attr('y2', cy + r)
+    .attr('stroke', '#333')
+    .attr('stroke-width', 2);
+
+  const boundBox = actElem.node().getBBox();
+  actor.height = boundBox.height + conf.boxTextMargin;
+
+  if (!isFooter) {
+    actorCnt++;
+    line
+      .append('line')
+      .attr('id', 'actor' + actorCnt)
+      .attr('x1', center)
+      .attr('y1', centerY)
+      .attr('x2', center)
+      .attr('y2', 2000)
+      .attr('class', 'actor-line 200')
+      .attr('stroke-width', '0.5px')
+      .attr('stroke', '#999')
+      .attr('name', actor.name);
+
+    actor.actorCnt = actorCnt;
+  }
+
+  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+    actor.description,
+    actElem,
+    rect.x,
+    rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY) / 2 + r + 2),
+    rect.width,
+    rect.height,
+    { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
+    conf
+  );
+
+  if (!isFooter) {
+    actElem.attr('transform', `translate(${0}, ${r / 2})`);
+  } else {
+    actElem.attr('transform', `translate(${0}, ${r / 2})`);
+  }
+
+  return actor.height;
+};
+
+const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
+  const actorY = isFooter ? actor.stopy : actor.starty;
+  const center = actor.x + actor.width / 2;
+  const centerY = actorY + actor.height;
+
+  const boxplusLineGroup = elem.append('g').lower();
+  let g = boxplusLineGroup;
+
+  if (!isFooter) {
+    actorCnt++;
+    if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
+      g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
+    }
+    g.append('line')
+      .attr('id', 'actor' + actorCnt)
+      .attr('x1', center)
+      .attr('y1', centerY)
+      .attr('x2', center)
+      .attr('y2', 2000)
+      .attr('class', 'actor-line 200')
+      .attr('stroke-width', '0.5px')
+      .attr('stroke', '#999')
+      .attr('name', actor.name);
+
+    g = boxplusLineGroup.append('g');
+    actor.actorCnt = actorCnt;
+
+    if (actor.links != null) {
+      g.attr('id', 'root-' + actorCnt);
+    }
+  }
+
+  const rect = svgDrawCommon.getNoteRect();
+
+  let cssclass = 'actor';
+  if (actor.properties?.class) {
+    cssclass = actor.properties.class;
+  } else {
+    rect.fill = '#eaeaea';
+  }
+
+  if (isFooter) {
+    cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
+  } else {
+    cssclass += ` ${TOP_ACTOR_CLASS}`;
+  }
+
+  rect.x = actor.x;
+  rect.y = actorY;
+  rect.width = actor.width;
+  rect.height = actor.height;
+  rect.class = cssclass;
+  rect.name = actor.name;
+
+  // Cylinder dimensions
+  rect.x = actor.x;
+  rect.y = actorY;
+  const w = rect.width;
+  const h = rect.height;
+  const rx = w / 2;
+  const ry = rx / (2.5 + w / 50);
+
+  // Cylinder base group
+  const cylinderGroup = g.append('g');
+
+  const pathData = createCylinderPathD(
+    rect.x,
+    rect.y,
+    w / 2,
+    isFooter ? h - ry : h / 2,
+    rx / 4,
+    ry / 4
+  );
+  cylinderGroup.append('path').attr('d', pathData).attr('class', cssclass);
+
+  if (!isFooter) {
+    cylinderGroup.attr('transform', `translate(${w / 4}, ${-centerY / 2 + 2.75 * ry})`);
+  } else {
+    cylinderGroup.attr('transform', `translate(${w / 4}, ${ry / 2})`);
+  }
+
+  actor.rectData = rect;
+
+  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+    actor.description,
+    g,
+    rect.x,
+    rect.y + (!isFooter ? ry * 1.75 : h / 2 + ry),
+    rect.width,
+    rect.height,
+    { class: `actor ${ACTOR_BOX_CLASS}` },
+    conf
+  );
+
+  let height = actor.height;
+  const lastPath = cylinderGroup.select('path:last-child');
+  if (lastPath.node()) {
+    const bounds = lastPath.node().getBBox();
+    actor.height = bounds.height;
+    height = bounds.height;
+  }
+
+  return height;
+};
+
+const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
+  const actorY = isFooter ? actor.stopy : actor.starty;
+  const center = actor.x + actor.width / 2;
+  const centerY = actorY + 80;
+  const radius = 30;
+  const line = elem.append('g').lower();
+
+  if (!isFooter) {
+    actorCnt++;
+    line
+      .append('line')
+      .attr('id', 'actor' + actorCnt)
+      .attr('x1', center)
+      .attr('y1', centerY)
+      .attr('x2', center)
+      .attr('y2', 2000)
+      .attr('class', 'actor-line 200')
+      .attr('stroke-width', '0.5px')
+      .attr('stroke', '#999')
+      .attr('name', actor.name);
+
+    actor.actorCnt = actorCnt;
+  }
+  const actElem = elem.append('g');
+  let cssClass = ACTOR_MAN_FIGURE_CLASS;
+  if (isFooter) {
+    cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
+  } else {
+    cssClass += ` ${TOP_ACTOR_CLASS}`;
+  }
+  actElem.attr('class', cssClass);
+  actElem.attr('name', actor.name);
+
+  const rect = svgDrawCommon.getNoteRect();
+  rect.x = actor.x;
+  rect.y = actorY;
+  rect.fill = '#eaeaea';
+  rect.width = actor.width;
+  rect.height = actor.height;
+  rect.class = 'actor';
+  rect.rx = 3;
+  rect.ry = 3;
+
+  actElem
+    .append('line')
+    .attr('id', 'actor-man-torso' + actorCnt)
+    .attr('x1', actor.x + actor.width / 2 - radius * 2.5)
+    .attr('y1', actorY + 10)
+    .attr('x2', actor.x + actor.width / 2 - 15)
+    .attr('y2', actorY + 10);
+
+  actElem
+    .append('line')
+    .attr('id', 'actor-man-arms' + actorCnt)
+    .attr('x1', actor.x + actor.width / 2 - radius * 2.5)
+    .attr('y1', actorY + 0) // starting Y
+    .attr('x2', actor.x + actor.width / 2 - radius * 2.5)
+    .attr('y2', actorY + 20); // ending Y (26px long, adjust as needed)
+
+  actElem
+    .append('circle')
+    .attr('cx', actor.x + actor.width / 2)
+    .attr('cy', actorY + 10)
+    .attr('r', radius)
+    .attr('width', actor.width);
+
+  const bounds = actElem.node().getBBox();
+  actor.height = bounds.height + conf.boxTextMargin;
+
+  _drawTextCandidateFunc(conf, hasKatex(actor.description))(
+    actor.description,
+    actElem,
+    rect.x,
+    rect.y + (!isFooter ? radius / 2 : radius / 2),
+    rect.width,
+    rect.height,
+    { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
+    conf
+  );
+
+  if (!isFooter) {
+    actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
+  } else {
+    actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
+  }
+
+  // actElem.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`);
+
+  return actor.height;
+};
+
 const drawActorTypeActor = function (elem, actor, conf, isFooter) {
   const actorY = isFooter ? actor.stopy : actor.starty;
   const center = actor.x + actor.width / 2;
@@ -512,6 +1115,18 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
       return await drawActorTypeActor(elem, actor, conf, isFooter);
     case 'participant':
       return await drawActorTypeParticipant(elem, actor, conf, isFooter);
+    case 'boundary':
+      return await drawActorTypeBoundary(elem, actor, conf, isFooter);
+    case 'control':
+      return await drawActorTypeControl(elem, actor, conf, isFooter);
+    case 'entity':
+      return await drawActorTypeEntity(elem, actor, conf, isFooter);
+    case 'database':
+      return await drawActorTypeDatabase(elem, actor, conf, isFooter);
+    case 'collections':
+      return await drawActorTypeCollections(elem, actor, conf, isFooter);
+    case 'queue':
+      return await drawActorTypeQueue(elem, actor, conf, isFooter);
   }
 };
 

From bfa0eefa32a46628d97a4f5bc3f86e9d07764a53 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Fri, 27 Jun 2025 15:01:45 +0530
Subject: [PATCH 033/314] fix gantt chart date format issue

---
 cypress/integration/rendering/gantt.spec.js       | 12 ++++++++++++
 .../mermaid/src/diagrams/gantt/ganttRenderer.js   | 15 +++++++++++++--
 2 files changed, 25 insertions(+), 2 deletions(-)

diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js
index 2cc67918c..fed4151ec 100644
--- a/cypress/integration/rendering/gantt.spec.js
+++ b/cypress/integration/rendering/gantt.spec.js
@@ -565,6 +565,18 @@ describe('Gantt diagram', () => {
     );
   });
 
+  it('should render only the day when using dateFormat D', () => {
+    imgSnapshotTest(
+      `
+    gantt
+      title Test
+      dateFormat D
+      A :a, 1, 1d
+    `,
+      {}
+    );
+  });
+
   // TODO: fix it
   //
   // This test is skipped deliberately
diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js
index f5c8c2e38..66ada61c8 100644
--- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js
+++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js
@@ -615,9 +615,20 @@ export const draw = function (text, id, version, diagObj) {
    * @param h
    */
   function makeGrid(theSidePad, theTopPad, w, h) {
+    const dateFormat = diagObj.db.getDateFormat();
+    const userAxisFormat = diagObj.db.getAxisFormat();
+    let axisFormat;
+    if (userAxisFormat) {
+      axisFormat = userAxisFormat;
+    } else if (dateFormat === 'D') {
+      axisFormat = '%d';
+    } else {
+      axisFormat = conf.axisFormat ?? '%Y-%m-%d';
+    }
+
     let bottomXAxis = axisBottom(timeScale)
       .tickSize(-h + theTopPad + conf.gridLineStartPadding)
-      .tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d'));
+      .tickFormat(timeFormat(axisFormat));
 
     const reTickInterval = /^([1-9]\d*)(millisecond|second|minute|hour|day|week|month)$/;
     const resultTickInterval = reTickInterval.exec(
@@ -669,7 +680,7 @@ export const draw = function (text, id, version, diagObj) {
     if (diagObj.db.topAxisEnabled() || conf.topAxis) {
       let topXAxis = axisTop(timeScale)
         .tickSize(-h + theTopPad + conf.gridLineStartPadding)
-        .tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d'));
+        .tickFormat(timeFormat(axisFormat));
 
       if (resultTickInterval !== null) {
         const every = resultTickInterval[1];

From 814b68b4a94813f7c6b3d7fb4559532a7bab2652 Mon Sep 17 00:00:00 2001
From: darshanr0107 
Date: Fri, 27 Jun 2025 15:16:20 +0530
Subject: [PATCH 034/314] added changeset

---
 .changeset/empty-clouds-cry.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/empty-clouds-cry.md

diff --git a/.changeset/empty-clouds-cry.md b/.changeset/empty-clouds-cry.md
new file mode 100644
index 000000000..87ba93c6d
--- /dev/null
+++ b/.changeset/empty-clouds-cry.md
@@ -0,0 +1,5 @@
+---
+'mermaid': major
+---
+
+fix: apply correct dateFormat in Gantt chart to show only day when specified

From 8693be56ee78382b491387f77c6a2bed9599fcdd Mon Sep 17 00:00:00 2001
From: rajat-ht 
Date: Fri, 27 Jun 2025 17:26:54 +0530
Subject: [PATCH 035/314] feat: Add start pro trial button.

---
 .../.vitepress/components/EditorSelectionModal.vue    | 11 +++++++++++
 packages/mermaid/src/docs/.vitepress/config.ts        |  6 ++----
 packages/mermaid/src/docs/.vitepress/theme/index.ts   |  2 +-
 3 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue
index e41a7096d..52b635392 100644
--- a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue
+++ b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue
@@ -11,6 +11,7 @@ interface EditorColumn {
   description: string;
   redirectUrl: string;
   highlighted?: boolean;
+  proTrialUrl?: string;
   features: Feature[];
 }
 
@@ -32,6 +33,8 @@ const editorColumns: EditorColumn[] = [
     redirectUrl:
       'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=mermaid_chart&redirect=%2Fapp%2Fdiagrams%2Fnew',
     highlighted: true,
+    proTrialUrl:
+      'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=mermaid_chart_trial&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout',
     features: [
       { iconUrl: '/icons/folder.svg', featureName: 'Storage' },
       { iconUrl: '/icons/terminal.svg', featureName: 'Code editor' },
@@ -121,6 +124,14 @@ onUnmounted(() => {
         >
           Start free
         
+        
+          Start Pro trial
+        
         
  • h(TopBar), 'doc-before': () => h(TopBar), 'layout-bottom': () => h(Tooltip), - 'home-hero-after': () => h(EditorSelectionModal), + 'layout-top': () => h(EditorSelectionModal), }); }, enhanceApp({ app, router }: EnhanceAppContext) { From 76d073b0272d5b3694cb754860717b874195ac49 Mon Sep 17 00:00:00 2001 From: omkarht Date: Fri, 27 Jun 2025 17:56:03 +0530 Subject: [PATCH 036/314] chore: adjusted database and queue shape --- .../src/diagrams/sequence/sequenceRenderer.ts | 7 +- .../mermaid/src/diagrams/sequence/svgDraw.js | 65 +++++++++---------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 8b4842fc1..e44431928 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -725,7 +725,12 @@ function adjustCreatedDestroyedData( msgModel.startx = msgModel.startx - adjustment; } } - const actorArray = [PARTICIPANT_TYPE.ACTOR, PARTICIPANT_TYPE.CONTROL, PARTICIPANT_TYPE.ENTITY]; + const actorArray = [ + PARTICIPANT_TYPE.ACTOR, + PARTICIPANT_TYPE.CONTROL, + PARTICIPANT_TYPE.ENTITY, + PARTICIPANT_TYPE.DATABASE, + ]; // if it is a create message if (createdActors.get(msg.to) == index) { diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index 3af3d3cf0..5304f57f5 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -3,8 +3,6 @@ import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js'; import { sanitizeUrl } from '@braintree/sanitize-url'; import * as configApi from '../../config.js'; -import { createInnerCylinderPathD } from '../../rendering-util/rendering-elements/shapes/tiltedCylinder.js'; -import { createCylinderPathD } from '../../rendering-util/rendering-elements/shapes/cylinder.js'; export const ACTOR_TYPE_WIDTH = 18 * 2; const TOP_ACTOR_CLASS = 'actor-top'; @@ -582,8 +580,7 @@ const drawActorTypeQueue = function (elem, actor, conf, isFooter) { .append('path') .attr( 'd', - ` - M ${rect.x},${rect.y + ry} + `M ${rect.x},${rect.y + ry} a ${rx},${ry} 0 0 0 0,${rect.height} h ${rect.width - 2 * rx} a ${rx},${ry} 0 0 0 0,-${rect.height} @@ -593,18 +590,17 @@ const drawActorTypeQueue = function (elem, actor, conf, isFooter) { .attr('class', cssclass); cylinderArc .append('path') - .attr('d', createInnerCylinderPathD(rect.x - rx, rect.y + ry, rect.width, rect.height, rx, ry)) + .attr( + 'd', + `M ${rect.x},${rect.y + ry} + a ${rx},${ry} 0 0 0 0,${rect.height}` + ) .attr('stroke', '#666') .attr('stroke-width', '1px') .attr('class', cssclass); - if (!isFooter) { - cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2) - rect.y})`); - cylinderArc.attr('transform', `translate(${rect.width / 2}, ${rect.height / 2})`); - } else { - cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`); - cylinderArc.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`); - } + cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`); + cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`); actor.rectData = rect; @@ -821,7 +817,7 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) { const drawActorTypeDatabase = function (elem, actor, conf, isFooter) { const actorY = isFooter ? actor.stopy : actor.starty; const center = actor.x + actor.width / 2; - const centerY = actorY + actor.height; + const centerY = actorY + actor.height + 2 * conf.boxTextMargin; const boxplusLineGroup = elem.append('g').lower(); let g = boxplusLineGroup; @@ -875,52 +871,55 @@ const drawActorTypeDatabase = function (elem, actor, conf, isFooter) { // Cylinder dimensions rect.x = actor.x; rect.y = actorY; - const w = rect.width; - const h = rect.height; + const w = rect.width / 4; + const h = rect.height * 0.8; const rx = w / 2; const ry = rx / (2.5 + w / 50); // Cylinder base group const cylinderGroup = g.append('g'); - const pathData = createCylinderPathD( - rect.x, - rect.y, - w / 2, - isFooter ? h - ry : h / 2, - rx / 4, - ry / 4 - ); - cylinderGroup.append('path').attr('d', pathData).attr('class', cssclass); + const d = ` + M ${rect.x},${rect.y + ry} + a ${rx},${ry} 0 0 0 ${w},0 + a ${rx},${ry} 0 0 0 -${w},0 + l 0,${h - 2 * ry} + a ${rx},${ry} 0 0 0 ${w},0 + l 0,-${h - 2 * ry} +`; + // Draw the main cylinder body + cylinderGroup + .append('path') + .attr('d', d) + .attr('fill', '#eaeaea') + .attr('stroke', '#000') + .attr('stroke-width', 1) + .attr('class', cssclass); if (!isFooter) { - cylinderGroup.attr('transform', `translate(${w / 4}, ${-centerY / 2 + 2.75 * ry})`); + cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`); } else { - cylinderGroup.attr('transform', `translate(${w / 4}, ${ry / 2})`); + cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`); } - actor.rectData = rect; - _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, g, rect.x, - rect.y + (!isFooter ? ry * 1.75 : h / 2 + ry), + rect.y + (!isFooter ? rect.height / 2 : h / 2 + ry), rect.width, rect.height, { class: `actor ${ACTOR_BOX_CLASS}` }, conf ); - let height = actor.height; const lastPath = cylinderGroup.select('path:last-child'); if (lastPath.node()) { const bounds = lastPath.node().getBBox(); - actor.height = bounds.height; - height = bounds.height; + actor.height = bounds.height + 2 * conf.boxTextMargin; } - return height; + return actor.height; }; const drawActorTypeBoundary = function (elem, actor, conf, isFooter) { From 939da082b2f5d09fd9b8997114aafa53f597a3a7 Mon Sep 17 00:00:00 2001 From: qraqras Date: Sat, 28 Jun 2025 08:18:19 +0000 Subject: [PATCH 037/314] Fix 6633 --- packages/mermaid/src/diagrams/block/layout.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/block/layout.ts b/packages/mermaid/src/diagrams/block/layout.ts index a00e935ac..9daf0b4c1 100644 --- a/packages/mermaid/src/diagrams/block/layout.ts +++ b/packages/mermaid/src/diagrams/block/layout.ts @@ -270,7 +270,11 @@ function layoutBlocks(block: Block, db: BlockDB) { if (child.children) { layoutBlocks(child, db); } - columnPos += child?.widthInColumns ?? 1; + let columnsFilled = child?.widthInColumns ?? 1; + if (columns > 0) { + columnsFilled = Math.min(columnsFilled, columns - (columnPos % columns)); + } + columnPos += columnsFilled; log.debug('abc88 columnsPos', child, columnPos); } } From 4012cbf013a979bead300d29675b4ebfae07bdfd Mon Sep 17 00:00:00 2001 From: darshanr0107 Date: Mon, 30 Jun 2025 18:32:31 +0530 Subject: [PATCH 038/314] Update changeset Co-authored-by: Sidharth Vinod --- .changeset/cold-sites-accept.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cold-sites-accept.md b/.changeset/cold-sites-accept.md index 4166d40d4..cba7ae414 100644 --- a/.changeset/cold-sites-accept.md +++ b/.changeset/cold-sites-accept.md @@ -2,4 +2,4 @@ 'mermaid': patch --- -edge label in the state diagram was not positioned correctly relative to the edge +fix: Position the edge label in state diagram correctly relative to the edge From 4254bdd4733a3b528c002f29fb583949c3f7be22 Mon Sep 17 00:00:00 2001 From: darshanr0107 Date: Mon, 30 Jun 2025 20:15:32 +0530 Subject: [PATCH 039/314] added test cases for edge label positions --- .../rendering/stateDiagram-v2.spec.js | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/cypress/integration/rendering/stateDiagram-v2.spec.js b/cypress/integration/rendering/stateDiagram-v2.spec.js index 83190dbc7..e5a0cedd0 100644 --- a/cypress/integration/rendering/stateDiagram-v2.spec.js +++ b/cypress/integration/rendering/stateDiagram-v2.spec.js @@ -602,6 +602,231 @@ State1 --> [*] -- 55 } +`, + {} + ); + }); + it('should render edge labels correctly', () => { + imgSnapshotTest( + `--- +title: On The Way To Something Something DarkSide +config: + look: default + theme: default +--- + +stateDiagram-v2 + + state State1_____________ + { + c0 + } + + state State2_____________ + { + c1 + } + + state State3_____________ + { + c7 + } + + state State4_____________ + { + c2 + } + + state State5_____________ + { + c3 + } + + state State6_____________ + { + c4 + } + + state State7_____________ + { + c5 + } + + state State8_____________ + { + c6 + } + + +[*] --> State1_____________ +State1_____________ --> State2_____________ : Transition1_____ +State2_____________ --> State4_____________ : Transition2_____ +State2_____________ --> State3_____________ : Transition3_____ +State3_____________ --> State2_____________ +State4_____________ --> State2_____________ : Transition5_____ +State4_____________ --> State5_____________ : Transition6_____ +State5_____________ --> State6_____________ : Transition7_____ +State6_____________ --> State4_____________ : Transition8_____ +State2_____________ --> State7_____________ : Transition4_____ +State4_____________ --> State7_____________ : Transition4_____ +State5_____________ --> State7_____________ : Transition4_____ +State6_____________ --> State7_____________ : Transition4_____ +State7_____________ --> State1_____________ : Transition9_____ +State5_____________ --> State8_____________ : Transition10____ +State8_____________ --> State5_____________ : Transition11____ +`, + {} + ); + }); + it('should render edge labels correctly with multiple transitions', () => { + imgSnapshotTest( + `--- +title: Multiple Transitions +config: + look: default + theme: default +--- + +stateDiagram-v2 + + state State1_____________ + { + c0 + } + + state State2_____________ + { + c1 + } + + state State3_____________ + { + c7 + } + + state State4_____________ + { + c2 + } + + state State5_____________ + { + c3 + } + + state State6_____________ + { + c4 + } + + state State7_____________ + { + c5 + } + + state State8_____________ + { + c6 + } + + state State9_____________ + { + c9 + } + +[*] --> State1_____________ +State1_____________ --> State2_____________ : Transition1_____ +State2_____________ --> State4_____________ : Transition2_____ +State2_____________ --> State3_____________ : Transition3_____ +State3_____________ --> State2_____________ +State4_____________ --> State2_____________ : Transition5_____ +State4_____________ --> State5_____________ : Transition6_____ +State5_____________ --> State6_____________ : Transition7_____ +State6_____________ --> State4_____________ : Transition8_____ +State2_____________ --> State7_____________ : Transition4_____ +State4_____________ --> State7_____________ : Transition4_____ +State5_____________ --> State7_____________ : Transition4_____ +State6_____________ --> State7_____________ : Transition4_____ +State7_____________ --> State1_____________ : Transition9_____ +State5_____________ --> State8_____________ : Transition10____ +State8_____________ --> State5_____________ : Transition11____ +State9_____________ --> State8_____________ : Transition12____ +`, + {} + ); + }); + + it('should render edge labels correctly with multiple states', () => { + imgSnapshotTest( + `--- +title: Multiple States +config: + look: default + theme: default +--- + +stateDiagram-v2 + + state State1_____________ + { + c0 + } + + state State2_____________ + { + c1 + } + + state State3_____________ + { + c7 + } + + state State4_____________ + { + c2 + } + + state State5_____________ + { + c3 + } + + state State6_____________ + { + c4 + } + + state State7_____________ + { + c5 + } + + state State8_____________ + { + c6 + } + + state State9_____________ + { + c9 + } + + state State10_____________ + { + c10 + } + +[*] --> State1_____________ +State1_____________ --> State2_____________ : Transition1_____ +State2_____________ --> State3_____________ : Transition2_____ +State3_____________ --> State4_____________ : Transition3_____ +State4_____________ --> State5_____________ : Transition4_____ +State5_____________ --> State6_____________ : Transition5_____ +State6_____________ --> State7_____________ : Transition6_____ +State7_____________ --> State8_____________ : Transition7_____ +State8_____________ --> State9_____________ : Transition8_____ +State9_____________ --> State10_____________ : Transition9_____ `, {} ); From b61bec8fafee523cc545493023151fcd0c13a45d Mon Sep 17 00:00:00 2001 From: omkarht Date: Tue, 1 Jul 2025 12:58:04 +0530 Subject: [PATCH 040/314] added test cases for new participant types of sequence diagram --- .../rendering/sequencediagram-v2.spec.js | 644 ++++++++++++++++++ 1 file changed, 644 insertions(+) create mode 100644 cypress/integration/rendering/sequencediagram-v2.spec.js diff --git a/cypress/integration/rendering/sequencediagram-v2.spec.js b/cypress/integration/rendering/sequencediagram-v2.spec.js new file mode 100644 index 000000000..4b7fe88d5 --- /dev/null +++ b/cypress/integration/rendering/sequencediagram-v2.spec.js @@ -0,0 +1,644 @@ +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; + +const looks = ['classic']; +const participantTypes = [ + 'participant', + 'actor', + 'boundary', + 'control', + 'entity', + 'database', + 'collections', + 'queue', +]; + +const interactionTypes = [ + '->>', // Solid arrow with arrowhead + '-->>', // Dotted arrow with arrowhead + '->', // Solid arrow without arrowhead + '-->', // Dotted arrow without arrowhead + '-x', // Solid arrow with cross + '--x', // Dotted arrow with cross + '->>+', // Solid arrow with arrowhead (activate) + '-->>+', // Dotted arrow with arrowhead (activate) +]; + +const notePositions = ['left of', 'right of', 'over']; + +looks.forEach((look) => { + describe(`Sequence Diagram Tests - ${look} look`, () => { + it('should render all participant types', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.forEach((type, index) => { + diagramCode += ` ${type} ${type}${index} as ${type} ${index}\n`; + }); + // Add some basic interactions + for (let i = 0; i < participantTypes.length - 1; i++) { + diagramCode += ` ${participantTypes[i]}${i} ->> ${participantTypes[i + 1]}${i + 1}: Message ${i}\n`; + } + imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } }); + }); + + it('should render all interaction types', () => { + let diagramCode = `sequenceDiagram\n`; + // Create two participants + // Add all interaction types + diagramCode += ` participant A\n`; + diagramCode += ` participant B\n`; + interactionTypes.forEach((interaction, index) => { + diagramCode += ` A ${interaction} B: ${interaction} message ${index}\n`; + }); + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render participant creation and destruction', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.forEach((type, index) => { + diagramCode += ` ${type} A\n`; + diagramCode += ` ${type} B\n`; + diagramCode += ` create ${type} ${type}${index}\n`; + diagramCode += ` A ->> ${type}${index}: Hello ${type}\n`; + if (index % 2 === 0) { + diagramCode += ` destroy ${type}${index}\n`; + } + }); + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render notes in all positions', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += ` participant A\n`; + diagramCode += ` participant B\n`; + notePositions.forEach((position, index) => { + diagramCode += ` Note ${position} A: Note ${position} ${index}\n`; + }); + diagramCode += ` A ->> B: Message with notes\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render parallel interactions', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 4).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` par Parallel actions\n`; + for (let i = 0; i < participantTypes.length - 1; i += 2) { + diagramCode += ` ${participantTypes[i]}${i} ->> ${participantTypes[i + 1]}${i + 1}: Message ${i}\n`; + if (i < participantTypes.length - 2) { + diagramCode += ` and\n`; + } + } + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render alternative flows', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += ` participant A\n`; + diagramCode += ` participant B\n`; + diagramCode += ` alt Successful case\n`; + diagramCode += ` A ->> B: Request\n`; + diagramCode += ` B -->> A: Success\n`; + diagramCode += ` else Failure case\n`; + diagramCode += ` A ->> B: Request\n`; + diagramCode += ` B --x A: Failure\n`; + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render loops', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 3).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` loop For each participant\n`; + for (let i = 0; i < 3; i++) { + diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[1]}1: Message ${i}\n`; + } + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render boxes around groups', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += ` box Group 1\n`; + participantTypes.slice(0, 3).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` end\n`; + diagramCode += ` box rgb(200,220,255) Group 2\n`; + participantTypes.slice(3, 6).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` end\n`; + // Add some interactions + diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[3]}0: Cross-group message\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render with different font settings', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 3).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[1]}1: Regular message\n`; + diagramCode += ` Note right of ${participantTypes[1]}1: Regular note\n`; + imgSnapshotTest(diagramCode, { + look, + sequence: { + actorFontFamily: 'courier', + actorFontSize: 14, + messageFontFamily: 'Arial', + messageFontSize: 12, + noteFontFamily: 'times', + noteFontSize: 16, + noteAlign: 'left', + }, + }); + }); + }); +}); + +// Additional tests for specific combinations +describe('Sequence Diagram Special Cases', () => { + it('should render complex sequence with all features', () => { + const diagramCode = ` + sequenceDiagram + box rgb(200,220,255) Authentication + actor User + boundary LoginUI + control AuthService + database UserDB + end + + box rgb(200,255,220) Order Processing + entity Order + queue OrderQueue + collections AuditLogs + end + + User ->> LoginUI: Enter credentials + LoginUI ->> AuthService: Validate + AuthService ->> UserDB: Query user + UserDB -->> AuthService: User data + alt Valid credentials + AuthService -->> LoginUI: Success + LoginUI -->> User: Welcome + + par Place order + User ->> Order: New order + Order ->> OrderQueue: Process + and + Order ->> AuditLogs: Record + end + + loop Until confirmed + OrderQueue ->> Order: Update status + Order -->> User: Notification + end + else Invalid credentials + AuthService --x LoginUI: Failure + LoginUI --x User: Retry + end + `; + imgSnapshotTest(diagramCode, {}); + }); + + it('should render with wrapped messages and notes', () => { + const diagramCode = ` + sequenceDiagram + participant A + participant B + + A ->> B: This is a very long message that should wrap properly in the diagram rendering + Note over A,B: This is a very long note that should also wrap properly when rendered in the diagram + + par Wrapped parallel + A ->> B: Parallel message 1
    with explicit line break + and + B ->> A: Parallel message 2
    with explicit line break + end + + loop Wrapped loop + Note right of B: This is a long note
    in a loop + A ->> B: Message in loop + end + `; + imgSnapshotTest(diagramCode, { sequence: { wrap: true } }); + }); + describe('Sequence Diagram Rendering with Different Participant Types', () => { + it('should render a sequence diagram with various participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor User + participant AuthService as Authentication Service + boundary UI + control OrderController + entity Product + database MongoDB + collections Products + queue OrderQueue + User ->> UI: Login request + UI ->> AuthService: Validate credentials + AuthService -->> UI: Authentication token + UI ->> OrderController: Place order + OrderController ->> Product: Check availability + Product -->> OrderController: Available + OrderController ->> MongoDB: Save order + MongoDB -->> OrderController: Order saved + OrderController ->> OrderQueue: Process payment + OrderQueue -->> User: Order confirmation + ` + ); + }); + + it('should render participant creation and destruction with different types', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Customer + participant Frontend + boundary PaymentGateway + Customer ->> Frontend: Place order + Frontend ->> OrderProcessor: Process order + create database OrderDB + OrderProcessor ->> OrderDB: Save order + ` + ); + }); + + it('should handle complex interactions between different participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + box rgba(200,220,255,0.5) System Components + actor User + boundary WebUI + control API + entity BusinessLogic + database MainDB + end + box rgba(200,255,220,0.5) External Services + queue MessageQueue + database AuditLogs + end + + User ->> WebUI: Submit request + WebUI ->> API: Process request + API ->> BusinessLogic: Execute business rules + BusinessLogic ->> MainDB: Query data + MainDB -->> BusinessLogic: Return results + BusinessLogic ->> MessageQueue: Publish event + MessageQueue -->> AuditLogs: Store audit trail + AuditLogs -->> API: Audit complete + API -->> WebUI: Return response + WebUI -->> User: Show results + `, + { sequence: { useMaxWidth: false } } + ); + }); + + it('should render parallel processes with different participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Customer + participant Frontend + boundary PaymentService + control InventoryManager + entity Order + database OrdersDB + queue NotificationQueue + + Customer ->> Frontend: Place order + Frontend ->> Order: Create order + par Parallel Processing + Order ->> PaymentService: Process payment + and + Order ->> InventoryManager: Reserve items + end + PaymentService -->> Order: Payment confirmed + InventoryManager -->> Order: Items reserved + Order ->> OrdersDB: Save finalized order + OrdersDB -->> Order: Order saved + Order ->> NotificationQueue: Send confirmation + NotificationQueue -->> Customer: Order confirmation + ` + ); + }); + + it('should render different participant types with notes and loops', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Admin + participant Dashboard + boundary AuthService + control UserManager + entity UserProfile + database UserDB + database Logs + + Admin ->> Dashboard: Open user management + loop Authentication check + Dashboard ->> AuthService: Verify admin rights + AuthService ->> Dashboard: Access granted + end + Dashboard ->> UserManager: List users + UserManager ->> UserDB: Query users + UserDB ->> UserManager: Return user data + Note right of UserDB: Encrypted data
    requires decryption + UserManager ->> UserProfile: Format profiles + UserProfile ->> UserManager: Formatted data + UserManager ->> Dashboard: Display users + Dashboard ->> Logs: Record access + Logs ->> Admin: Audit trail + ` + ); + }); + + it('should render different participant types with alternative flows', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Client + participant MobileApp + boundary CloudService + control DataProcessor + entity Transaction + database TransactionsDB + queue EventBus + + Client ->> MobileApp: Initiate transaction + MobileApp ->> CloudService: Authenticate + alt Authentication successful + CloudService -->> MobileApp: Auth token + MobileApp ->> DataProcessor: Process data + DataProcessor ->> Transaction: Create transaction + Transaction ->> TransactionsDB: Save record + TransactionsDB -->> Transaction: Confirmation + Transaction ->> EventBus: Publish event + EventBus -->> Client: Notification + else Authentication failed + CloudService -->> MobileApp: Error + MobileApp -->> Client: Show error + end + ` + ); + }); + + it('should render different participant types with wrapping text', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor LongNameUser as User With A Very
    Long Name + participant FE as Frontend Service
    With Long Name + boundary B as Boundary With
    Multiline Name + control C as Control With
    Multiline Name + entity E as Entity With
    Multiline Name + database DB as Database With
    Multiline Name + collections COL as Collections With
    Multiline Name + queue Q as Queue With
    Multiline Name + + LongNameUser ->> FE: This is a very long message that should wrap properly in the diagram + FE ->> B: Another long message
    with explicit
    line breaks + B -->> FE: Response message that is also quite long and needs to wrap + FE ->> C: Process data + C ->> E: Validate + E -->> C: Validation result + C ->> DB: Save + DB -->> C: Save result + C ->> COL: Log + COL -->> Q: Forward + Q -->> LongNameUser: Final response with confirmation of all actions taken + `, + { sequence: { wrap: true } } + ); + }); + describe('Sequence Diagram - New Participant Types with Long Notes and Messages', () => { + it('should render long notes left of boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + boundary Alice + actor Bob + Alice->>Bob: Hola + Note left of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes left of control', () => { + imgSnapshotTest( + ` + sequenceDiagram + control Alice + actor Bob + Alice->>Bob: Hola + Note left of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long notes right of entity', () => { + imgSnapshotTest( + ` + sequenceDiagram + entity Alice + actor Bob + Alice->>Bob: Hola + Note right of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes right of database', () => { + imgSnapshotTest( + ` + sequenceDiagram + database Alice + actor Bob + Alice->>Bob: Hola + Note right of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long notes over collections', () => { + imgSnapshotTest( + ` + sequenceDiagram + collections Alice + actor Bob + Alice->>Bob: Hola + Note over Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes over queue', () => { + imgSnapshotTest( + ` + sequenceDiagram + queue Alice + actor Bob + Alice->>Bob: Hola + Note over Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render notes over actor and boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Alice + boundary Charlie + note over Alice: Some note + note over Charlie: Other note + `, + {} + ); + }); + + it('should render long messages from database to collections', () => { + imgSnapshotTest( + ` + sequenceDiagram + database Alice + collections Bob + Alice->>Bob: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long messages from control to entity', () => { + imgSnapshotTest( + ` + sequenceDiagram + control Alice + entity Bob + Alice->>Bob:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long messages from queue to boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + queue Alice + boundary Bob + Alice->>Bob: I'm short + Bob->>Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + `, + {} + ); + }); + + it('should render wrapped long messages from actor to database', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Alice + database Bob + Alice->>Bob: I'm short + Bob->>Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + `, + {} + ); + }); + }); + }); + + describe('svg size', () => { + it('should render a sequence diagram when useMaxWidth is true (default)', () => { + renderGraph( + ` + sequenceDiagram + actor Alice + boundary Bob + control John as John
    Second Line + Alice ->> Bob: Hello Bob, how are you? + Bob-->>John: How about you John? + Bob--x Alice: I am good thanks! + Bob-x John: I am good thanks! + Note right of John: Bob thinks a long
    long time, so long
    that the text does
    not fit on a row. + Bob-->Alice: Checking with John... + alt either this + Alice->>John: Yes + else or this + Alice->>John: No + else or this will happen + Alice->John: Maybe + end + par this happens in parallel + Alice -->> Bob: Parallel message 1 + and + Alice -->> John: Parallel message 2 + end + `, + { sequence: { useMaxWidth: true } } + ); + cy.get('svg').should((svg) => { + expect(svg).to.have.attr('width', '100%'); + const style = svg.attr('style'); + expect(style).to.match(/^max-width: [\d.]+px;$/); + const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); + expect(maxWidthValue).to.be.within(820 * 0.95, 820 * 1.05); + }); + }); + + it('should render a sequence diagram when useMaxWidth is false', () => { + renderGraph( + ` + sequenceDiagram + actor Alice + boundary Bob + control John as John
    Second Line + Alice ->> Bob: Hello Bob, how are you? + Bob-->>John: How about you John? + Bob--x Alice: I am good thanks! + Bob-x John: I am good thanks! + Note right of John: Bob thinks a long
    long time, so long
    that the text does
    not fit on a row. + Bob-->Alice: Checking with John... + alt either this + Alice->>John: Yes + else or this + Alice->>John: No + else or this will happen + Alice->John: Maybe + end + par this happens in parallel + Alice -->> Bob: Parallel message 1 + and + Alice -->> John: Parallel message 2 + end + `, + { sequence: { useMaxWidth: false } } + ); + cy.get('svg').should((svg) => { + const width = parseFloat(svg.attr('width')); + expect(width).to.be.within(820 * 0.95, 820 * 1.05); + expect(svg).to.not.have.attr('style'); + }); + }); + }); +}); From a4a27611dd94ecbb6ebb85ee49fd12c83984596e Mon Sep 17 00:00:00 2001 From: omkarht Date: Tue, 1 Jul 2025 18:11:41 +0530 Subject: [PATCH 041/314] fix: minor refinement --- .../src/diagrams/sequence/sequenceRenderer.ts | 12 ++++++++---- packages/mermaid/src/diagrams/sequence/svgDraw.js | 12 ++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index e44431928..d390ebada 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -1076,10 +1076,11 @@ export const draw = async function (_text: string, id: string, _version: string, for (const box of bounds.models.boxes) { box.height = bounds.getVerticalPos() - box.y; bounds.insert(box.x, box.y, box.x + box.width, box.height); - box.startx = box.x; - box.starty = box.y; - box.stopx = box.startx + box.width; - box.stopy = box.starty + box.height; + const boxPadding = conf.boxMargin * 2; + box.startx = box.x - boxPadding; + box.starty = box.y - boxPadding * 0.25; + box.stopx = box.startx + box.width + 2 * boxPadding; + box.stopy = box.starty + box.height + boxPadding * 0.75; box.stroke = 'rgb(0,0,0, 0.5)'; svgDraw.drawBox(diagram, box, conf); } @@ -1344,6 +1345,9 @@ async function calculateActorMargins( return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0)); }, 0); + const standardBoxPadding = conf.boxMargin * 8; + totalWidth += standardBoxPadding; + totalWidth -= 2 * conf.boxTextMargin; if (box.wrap) { box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont); diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index 5304f57f5..6b8fa0208 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -712,13 +712,13 @@ const drawActorTypeControl = function (elem, actor, conf, isFooter) { .attr('marker-end', 'url(#filled-head-control)'); const bounds = actElem.node().getBBox(); - actor.height = bounds.height + conf.boxTextMargin; + actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0); _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, actElem, rect.x, - rect.y + (!isFooter ? 30 : 40), + rect.y + 30, rect.width, rect.height, { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` }, @@ -774,8 +774,8 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) { .attr('stroke', '#333') .attr('stroke-width', 2); - const boundBox = actElem.node().getBBox(); - actor.height = boundBox.height + conf.boxTextMargin; + const bounds = actElem.node().getBBox(); + actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0); if (!isFooter) { actorCnt++; @@ -916,7 +916,7 @@ const drawActorTypeDatabase = function (elem, actor, conf, isFooter) { const lastPath = cylinderGroup.select('path:last-child'); if (lastPath.node()) { const bounds = lastPath.node().getBBox(); - actor.height = bounds.height + 2 * conf.boxTextMargin; + actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0); } return actor.height; @@ -989,7 +989,7 @@ const drawActorTypeBoundary = function (elem, actor, conf, isFooter) { .attr('width', actor.width); const bounds = actElem.node().getBBox(); - actor.height = bounds.height + conf.boxTextMargin; + actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0); _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, From 012530e98e9b8b80962ab270b6bb3b6d9f6ada05 Mon Sep 17 00:00:00 2001 From: omkarht Date: Tue, 1 Jul 2025 19:02:57 +0530 Subject: [PATCH 042/314] added changeset --- .changeset/hungry-baths-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hungry-baths-glow.md diff --git a/.changeset/hungry-baths-glow.md b/.changeset/hungry-baths-glow.md new file mode 100644 index 000000000..b3084bcab --- /dev/null +++ b/.changeset/hungry-baths-glow.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`. From 4a86b68e01d1ba4acaa9c755beed6f356f076c2e Mon Sep 17 00:00:00 2001 From: rajat-ht Date: Wed, 2 Jul 2025 18:51:06 +0530 Subject: [PATCH 043/314] chore: update UI as per suggestion --- .../components/EditorSelectionModal.vue | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue index 52b635392..c78b10de1 100644 --- a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue +++ b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue @@ -10,6 +10,7 @@ interface EditorColumn { title: string; description: string; redirectUrl: string; + buttonText: string; highlighted?: boolean; proTrialUrl?: string; features: Feature[]; @@ -21,6 +22,7 @@ const editorColumns: EditorColumn[] = [ description: 'Basic features, no login', redirectUrl: 'https://www.mermaidchart.com/play?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=playground', + buttonText: 'Start free', features: [ { iconUrl: '/icons/public.svg', featureName: 'Diagram stored in URL' }, { iconUrl: '/icons/terminal.svg', featureName: 'Code editor' }, @@ -28,10 +30,11 @@ const editorColumns: EditorColumn[] = [ ], }, { - title: 'Free', - description: 'Advanced features, free account', + title: 'Free or Pro', + description: 'Advanced features, Free or Pro account', redirectUrl: 'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=mermaid_chart&redirect=%2Fapp%2Fdiagrams%2Fnew', + buttonText: 'Start Free', highlighted: true, proTrialUrl: 'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=mermaid_chart_trial&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout', @@ -53,6 +56,7 @@ const editorColumns: EditorColumn[] = [ title: 'Open Source', description: 'Code only, no login', redirectUrl: 'https://mermaid.live/edit', + buttonText: 'Start free', features: [ { iconUrl: '/icons/public.svg', featureName: 'Diagram stored in URL' }, { iconUrl: '/icons/terminal.svg', featureName: 'Code editor' }, @@ -87,15 +91,17 @@ onUnmounted(() => {