@ktrysmt/beautiful-mermaid 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,39 +1,28 @@
1
1
  <div align="center">
2
2
 
3
- # beautiful-mermaid
3
+ # @ktrysmt/beautiful-mermaid
4
4
 
5
5
  **Render Mermaid diagrams as beautiful SVGs or ASCII art**
6
6
 
7
7
  Ultra-fast, fully themeable, zero DOM dependencies. Built for the AI era.
8
8
 
9
- ![beautiful-mermaid sequence diagram example](hero.png)
10
-
11
- [![npm version](https://img.shields.io/npm/v/beautiful-mermaid.svg)](https://www.npmjs.com/package/beautiful-mermaid)
9
+ [![npm version](https://img.shields.io/npm/v/@ktrysmt/beautiful-mermaid.svg)](https://www.npmjs.com/package/@ktrysmt/beautiful-mermaid)
12
10
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
13
11
 
14
- [**Live Demo & Samples**](https://agents.craft.do/mermaid)
15
-
16
- **[→ Use it live in Craft Agents](https://agents.craft.do)**
17
-
18
12
  </div>
19
13
 
20
14
  ---
21
15
 
22
- ## Why We Built This
23
-
24
- Diagrams are essential for AI-assisted programming. When you're working with an AI coding assistant, being able to visualize data flows, state machines, and system architecture—directly in your terminal or chat interface—makes complex concepts instantly graspable.
25
-
26
- [Mermaid](https://mermaid.js.org/) is the de facto standard for text-based diagrams. It's brilliant. But the default renderer has problems:
27
-
28
- - **Aesthetics** — Might be personal preference, but wished they looked more professional
29
- - **Complex theming** — Customizing colors requires wrestling with CSS classes
30
- - **No terminal output** — Can't render to ASCII for CLI tools
31
- - **Heavy dependencies** — Pulls in a lot of code for simple diagrams
16
+ ## About This Fork
32
17
 
33
- We built `beautiful-mermaid` at [Craft](https://craft.do) to power diagrams in [Craft Agents](https://agents.craft.do). It's fast, beautiful, and works everywhere—from rich UIs to plain terminals.
18
+ This is a fork of [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid), originally built by the team at [Craft](https://craft.do). This fork is independently maintained by [@ktrysmt](https://github.com/ktrysmt) with additional bug fixes and improvements including:
34
19
 
20
+ - CJK/Unicode label support for subgraphs
21
+ - Subgraph layout fixes (vertical stacking, containment, label width)
22
+ - Edge routing improvements
23
+ - Scoped npm package (`@ktrysmt/beautiful-mermaid`)
35
24
 
36
- The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff. We ported it from Go to TypeScript and extended it. Thank you Alexander for the excellent foundation! (And inspiration that this was possible.)
25
+ The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff, ported from Go to TypeScript. Thank you Alexander for the excellent foundation!
37
26
 
38
27
  ## Features
39
28
 
@@ -50,11 +39,11 @@ The ASCII rendering engine is based on [mermaid-ascii](https://github.com/Alexan
50
39
  ## Installation
51
40
 
52
41
  ```bash
53
- npm install beautiful-mermaid
42
+ npm install @ktrysmt/beautiful-mermaid
54
43
  # or
55
- bun add beautiful-mermaid
44
+ bun add @ktrysmt/beautiful-mermaid
56
45
  # or
57
- pnpm add beautiful-mermaid
46
+ pnpm add @ktrysmt/beautiful-mermaid
58
47
  ```
59
48
 
60
49
  ## Quick Start
@@ -62,7 +51,7 @@ pnpm add beautiful-mermaid
62
51
  ### SVG Output
63
52
 
64
53
  ```typescript
65
- import { renderMermaidSVG } from 'beautiful-mermaid'
54
+ import { renderMermaidSVG } from '@ktrysmt/beautiful-mermaid'
66
55
 
67
56
  const svg = renderMermaidSVG(`
68
57
  graph TD
@@ -79,7 +68,7 @@ Need async? Use `renderMermaidSVGAsync()` — same output, returns a `Promise<st
79
68
  ### ASCII Output
80
69
 
81
70
  ```typescript
82
- import { renderMermaidASCII } from 'beautiful-mermaid'
71
+ import { renderMermaidASCII } from '@ktrysmt/beautiful-mermaid'
83
72
 
84
73
  const ascii = renderMermaidASCII(`graph LR; A --> B --> C`)
85
74
  ```
@@ -99,7 +88,7 @@ const ascii = renderMermaidASCII(`graph LR; A --> B --> C`)
99
88
  Because rendering is synchronous, you can use `useMemo()` for zero-flash diagram rendering:
100
89
 
101
90
  ```tsx
102
- import { renderMermaidSVG } from 'beautiful-mermaid'
91
+ import { renderMermaidSVG } from '@ktrysmt/beautiful-mermaid'
103
92
 
104
93
  function MermaidDiagram({ code }: { code: string }) {
105
94
  const { svg, error } = React.useMemo(() => {
@@ -224,7 +213,7 @@ const svg = renderMermaidSVG(diagram, {
224
213
  | `one-dark` | Dark | `#282c34` | `#c678dd` |
225
214
 
226
215
  ```typescript
227
- import { renderMermaidSVG, THEMES } from 'beautiful-mermaid'
216
+ import { renderMermaidSVG, THEMES } from '@ktrysmt/beautiful-mermaid'
228
217
 
229
218
  const svg = renderMermaidSVG(diagram, THEMES['tokyo-night'])
230
219
  ```
@@ -259,7 +248,7 @@ Use **any VS Code theme** directly via Shiki integration. This gives you access
259
248
 
260
249
  ```typescript
261
250
  import { getSingletonHighlighter } from 'shiki'
262
- import { renderMermaidSVG, fromShikiTheme } from 'beautiful-mermaid'
251
+ import { renderMermaidSVG, fromShikiTheme } from '@ktrysmt/beautiful-mermaid'
263
252
 
264
253
  // Load any theme from Shiki's registry
265
254
  const highlighter = await getSingletonHighlighter({
@@ -437,7 +426,7 @@ The chart renderer follows a clean, minimal design philosophy inspired by Apple
437
426
  For terminal environments, CLI tools, or anywhere you need plain text, render to ASCII or Unicode box-drawing characters:
438
427
 
439
428
  ```typescript
440
- import { renderMermaidASCII } from 'beautiful-mermaid'
429
+ import { renderMermaidASCII } from '@ktrysmt/beautiful-mermaid'
441
430
 
442
431
  // Unicode mode (default) — prettier box drawing
443
432
  const unicode = renderMermaidASCII(`graph LR; A --> B`)
@@ -580,6 +569,6 @@ MIT — see [LICENSE](LICENSE) for details.
580
569
 
581
570
  <div align="center">
582
571
 
583
- Built with care by the team at [Craft](https://craft.do)
572
+ Originally built by the team at [Craft](https://craft.do). Fork maintained by [@ktrysmt](https://github.com/ktrysmt).
584
573
 
585
574
  </div>
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ var MIX = {
7
7
  /** Primary text: near-full fg */
8
8
  text: 100,
9
9
  // just use --fg directly
10
- /** Secondary text (group headers): fg mixed at 60% */
11
- textSec: 60,
10
+ /** Secondary text (group headers, edge labels): fg mixed at 75% */
11
+ textSec: 75,
12
12
  /** Muted text (edge labels, notes): fg mixed at 40% */
13
13
  textMuted: 40,
14
14
  /** Faint text (de-emphasized): fg mixed at 25% */
@@ -153,7 +153,7 @@ function buildStyleBlock(font, hasMonoFont) {
153
153
  const derivedVars = `
154
154
  /* Derived from --bg and --fg (overridable via --line, --accent, etc.) */
155
155
  --_text: var(--fg);
156
- --_text-sec: var(--muted, color-mix(in srgb, var(--fg) ${MIX.textSec}%, var(--bg)));
156
+ --_text-sec: color-mix(in srgb, var(--fg) ${MIX.textSec}%, var(--bg));
157
157
  --_text-muted: var(--muted, color-mix(in srgb, var(--fg) ${MIX.textMuted}%, var(--bg)));
158
158
  --_text-faint: color-mix(in srgb, var(--fg) ${MIX.textFaint}%, var(--bg));
159
159
  --_line: var(--line, color-mix(in srgb, var(--fg) ${MIX.line}%, var(--bg)));
@@ -2927,8 +2927,9 @@ function drawArrow(graph, edge) {
2927
2927
  return [empty, empty, empty, empty, empty, empty];
2928
2928
  }
2929
2929
  const labelCanvas = drawArrowLabel(graph, edge);
2930
- const [pathCanvas, linesDrawn, lineDirs] = drawPath(graph, edge.path, edge.style);
2931
- const boxStartCanvas = drawBoxStart(graph, edge.path, linesDrawn[0], edge.from.shape);
2930
+ const endCoordOverride = computeEndCoordOverride(graph, edge);
2931
+ const [pathCanvas, linesDrawn, lineDirs] = drawPath(graph, edge.path, edge.style, void 0, endCoordOverride);
2932
+ const boxStartCanvas = drawBoxStart(graph, edge.path, linesDrawn[0], edge.from);
2932
2933
  let arrowHeadEndCanvas;
2933
2934
  if (edge.hasArrowEnd) {
2934
2935
  arrowHeadEndCanvas = drawArrowHead(
@@ -2957,6 +2958,38 @@ function drawArrow(graph, edge) {
2957
2958
  const cornersCanvas = drawCorners(graph, edge.path);
2958
2959
  return [pathCanvas, boxStartCanvas, arrowHeadEndCanvas, arrowHeadStartCanvas, cornersCanvas, labelCanvas];
2959
2960
  }
2961
+ function computeEndCoordOverride(graph, edge) {
2962
+ if (edge.path.length < 2) return void 0;
2963
+ const lastPathCoord = edge.path[edge.path.length - 1];
2964
+ const prevPathCoord = edge.path[edge.path.length - 2];
2965
+ const dir = determineDirection(prevPathCoord, lastPathCoord);
2966
+ const defaultDC = gridToDrawingCoord(graph, lastPathCoord);
2967
+ const targetDC = edge.to.drawingCoord;
2968
+ const targetGC = edge.to.gridCoord;
2969
+ if (!targetDC || !targetGC) return void 0;
2970
+ let boxW = 0;
2971
+ for (let i = 0; i < 2; i++) boxW += graph.columnWidth.get(targetGC.x + i) ?? 0;
2972
+ let boxH = 0;
2973
+ for (let i = 0; i < 2; i++) boxH += graph.rowHeight.get(targetGC.y + i) ?? 0;
2974
+ if (dirEquals(dir, Left)) {
2975
+ const borderX = targetDC.x + boxW;
2976
+ if (borderX === defaultDC.x) return void 0;
2977
+ return { x: borderX, y: defaultDC.y };
2978
+ } else if (dirEquals(dir, Right)) {
2979
+ const borderX = targetDC.x;
2980
+ if (borderX === defaultDC.x) return void 0;
2981
+ return { x: borderX, y: defaultDC.y };
2982
+ } else if (dirEquals(dir, Up)) {
2983
+ const borderY = targetDC.y + boxH;
2984
+ if (borderY === defaultDC.y) return void 0;
2985
+ return { x: defaultDC.x, y: borderY };
2986
+ } else if (dirEquals(dir, Down)) {
2987
+ const borderY = targetDC.y;
2988
+ if (borderY === defaultDC.y) return void 0;
2989
+ return { x: defaultDC.x, y: borderY };
2990
+ }
2991
+ return void 0;
2992
+ }
2960
2993
  function reverseDirection(dir) {
2961
2994
  if (dirEquals(dir, Up)) return Down;
2962
2995
  if (dirEquals(dir, Down)) return Up;
@@ -2968,15 +3001,16 @@ function reverseDirection(dir) {
2968
3001
  if (dirEquals(dir, LowerRight)) return UpperLeft;
2969
3002
  return Middle;
2970
3003
  }
2971
- function drawPath(graph, path, style = "solid") {
3004
+ function drawPath(graph, path, style = "solid", startCoordOverride, endCoordOverride) {
2972
3005
  const canvas = copyCanvas(graph.canvas);
2973
3006
  let previousCoord = path[0];
2974
3007
  const linesDrawn = [];
2975
3008
  const lineDirs = [];
3009
+ const lastIdx = path.length - 1;
2976
3010
  for (let i = 1; i < path.length; i++) {
2977
3011
  const nextCoord = path[i];
2978
- const prevDC = gridToDrawingCoord(graph, previousCoord);
2979
- const nextDC = gridToDrawingCoord(graph, nextCoord);
3012
+ const prevDC = i === 1 && startCoordOverride ? startCoordOverride : gridToDrawingCoord(graph, previousCoord);
3013
+ const nextDC = i === lastIdx && endCoordOverride ? endCoordOverride : gridToDrawingCoord(graph, nextCoord);
2980
3014
  if (drawingCoordEquals(prevDC, nextDC)) {
2981
3015
  previousCoord = nextCoord;
2982
3016
  continue;
@@ -2990,18 +3024,45 @@ function drawPath(graph, path, style = "solid") {
2990
3024
  }
2991
3025
  return [canvas, linesDrawn, lineDirs];
2992
3026
  }
2993
- function drawBoxStart(graph, path, firstLine, sourceShape) {
3027
+ function drawBoxStart(graph, path, firstLine, sourceNode) {
2994
3028
  const canvas = copyCanvas(graph.canvas);
2995
3029
  if (graph.config.useAscii) return canvas;
2996
- if (sourceShape === "state-start" || sourceShape === "state-end") {
3030
+ if (sourceNode.shape === "state-start" || sourceNode.shape === "state-end") {
2997
3031
  return canvas;
2998
3032
  }
2999
3033
  const from = firstLine[0];
3000
3034
  const dir = determineDirection(path[0], path[1]);
3001
- if (dirEquals(dir, Up)) canvas[from.x][from.y + 1] = "\u2534";
3002
- else if (dirEquals(dir, Down)) canvas[from.x][from.y - 1] = "\u252C";
3003
- else if (dirEquals(dir, Left)) canvas[from.x + 1][from.y] = "\u2524";
3004
- else if (dirEquals(dir, Right)) canvas[from.x - 1][from.y] = "\u251C";
3035
+ const dc = sourceNode.drawingCoord;
3036
+ const gc = sourceNode.gridCoord;
3037
+ if (dirEquals(dir, Right)) {
3038
+ let boxW = 0;
3039
+ for (let i = 0; i < 2; i++) boxW += graph.columnWidth.get(gc.x + i) ?? 0;
3040
+ const borderX = dc.x + boxW;
3041
+ canvas[borderX][from.y] = "\u251C";
3042
+ for (let x = borderX + 1; x < from.x; x++) {
3043
+ canvas[x][from.y] = "\u2500";
3044
+ }
3045
+ } else if (dirEquals(dir, Left)) {
3046
+ const borderX = dc.x;
3047
+ canvas[borderX][from.y] = "\u2524";
3048
+ for (let x = from.x + 1; x < borderX; x++) {
3049
+ canvas[x][from.y] = "\u2500";
3050
+ }
3051
+ } else if (dirEquals(dir, Down)) {
3052
+ let boxH = 0;
3053
+ for (let i = 0; i < 2; i++) boxH += graph.rowHeight.get(gc.y + i) ?? 0;
3054
+ const borderY = dc.y + boxH;
3055
+ canvas[from.x][borderY] = "\u252C";
3056
+ for (let y = borderY + 1; y < from.y; y++) {
3057
+ canvas[from.x][y] = "\u2502";
3058
+ }
3059
+ } else if (dirEquals(dir, Up)) {
3060
+ const borderY = dc.y;
3061
+ canvas[from.x][borderY] = "\u2534";
3062
+ for (let y = from.y + 1; y < borderY; y++) {
3063
+ canvas[from.x][y] = "\u2502";
3064
+ }
3065
+ }
3005
3066
  return canvas;
3006
3067
  }
3007
3068
  function drawArrowHead(graph, lastLine, fallbackDir) {
@@ -6533,6 +6594,7 @@ function mermaidToElk(graph, opts) {
6533
6594
  "elk.layered.highDegreeNodes.threshold": "8",
6534
6595
  "elk.layered.compaction.postCompaction.strategy": "LEFT_RIGHT_CONSTRAINT_LOCKING",
6535
6596
  "elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
6597
+ "elk.layered.cycleBreaking.strategy": "DEPTH_FIRST",
6536
6598
  "elk.layered.wrapping.strategy": "OFF",
6537
6599
  // Use SEPARATE when subgraphs have direction overrides (enables proper direction handling)
6538
6600
  // Use INCLUDE_CHILDREN otherwise (simpler cross-hierarchy edge routing)
@@ -6637,6 +6699,7 @@ function subgraphToElk(sg, graph, opts, edgesBySubgraph, subgraphPorts) {
6637
6699
  "elk.layered.spacing.edgeEdgeBetweenLayers": "12",
6638
6700
  "elk.layered.spacing.edgeNodeBetweenLayers": "12",
6639
6701
  "elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED",
6702
+ "elk.layered.cycleBreaking.strategy": "DEPTH_FIRST",
6640
6703
  "elk.layered.spacing.nodeNodeBetweenLayers": String(opts.layerSpacing),
6641
6704
  "elk.spacing.nodeNode": String(opts.nodeSpacing)
6642
6705
  };
@@ -6741,6 +6804,7 @@ function elkToPositioned(elkResult, graph, mergeEdges = false) {
6741
6804
  collectAllSubgraphIds(sg, subgraphIds);
6742
6805
  }
6743
6806
  extractNodesAndGroups(elkResult, graph, subgraphIds, nodes, groups, 0, 0);
6807
+ expandGroupsForLabels(groups, nodes);
6744
6808
  const allBounds = flattenGroupBounds(groups);
6745
6809
  const margins = allBounds.length > 0 ? {
6746
6810
  leftX: Math.min(...allBounds.map((b) => b.x)) - 20,
@@ -6751,6 +6815,7 @@ function elkToPositioned(elkResult, graph, mergeEdges = false) {
6751
6815
  if (mergeEdges) {
6752
6816
  bundleEdgePaths(edges, nodes, groups, graph.direction);
6753
6817
  }
6818
+ rerouteBackEdges(edges, nodes, groups, graph.direction);
6754
6819
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
6755
6820
  for (const edge of edges) {
6756
6821
  const sourceNode = nodeMap.get(edge.source);
@@ -6784,6 +6849,34 @@ function elkToPositioned(elkResult, graph, mergeEdges = false) {
6784
6849
  groups
6785
6850
  };
6786
6851
  }
6852
+ function expandGroupsForLabels(groups, nodes) {
6853
+ for (const group of groups) {
6854
+ if (group.children.length > 0) {
6855
+ expandGroupsForLabels(group.children, nodes);
6856
+ }
6857
+ if (!group.label) continue;
6858
+ const metrics = measureMultilineText(group.label, FONT_SIZES.groupHeader, FONT_WEIGHTS.groupHeader);
6859
+ const needed = metrics.width + 12 + 16;
6860
+ if (needed <= group.width) continue;
6861
+ const extra = needed - group.width;
6862
+ const shift = extra / 2;
6863
+ for (const node of nodes) {
6864
+ if (node.x >= group.x && node.x + node.width <= group.x + group.width + extra && node.y >= group.y && node.y + node.height <= group.y + group.height) {
6865
+ node.x += shift;
6866
+ }
6867
+ }
6868
+ for (const child of group.children) {
6869
+ shiftGroupX(child, shift);
6870
+ }
6871
+ group.width = needed;
6872
+ }
6873
+ }
6874
+ function shiftGroupX(group, dx) {
6875
+ group.x += dx;
6876
+ for (const child of group.children) {
6877
+ shiftGroupX(child, dx);
6878
+ }
6879
+ }
6787
6880
  function extractNodesAndGroups(elkNode, graph, subgraphIds, nodes, groups, offsetX, offsetY) {
6788
6881
  if (!elkNode.children) return;
6789
6882
  for (const child of elkNode.children) {
@@ -6936,6 +7029,99 @@ function orthogonalizeEdgePoints(points, margins, edgeIndex = 0) {
6936
7029
  }
6937
7030
  return result;
6938
7031
  }
7032
+ function rerouteBackEdges(edges, nodes, groups, direction) {
7033
+ const isVertical = direction === "TD" || direction === "TB" || direction === "BT";
7034
+ if (!isVertical) return;
7035
+ const isDownward = direction === "TD" || direction === "TB";
7036
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
7037
+ const backEdgeIndices = [];
7038
+ for (let i = 0; i < edges.length; i++) {
7039
+ const edge = edges[i];
7040
+ const src = nodeMap.get(edge.source);
7041
+ const tgt = nodeMap.get(edge.target);
7042
+ if (!src || !tgt || edge.points.length < 2) continue;
7043
+ const isBackEdge = isDownward ? src.y > tgt.y : src.y + src.height < tgt.y + tgt.height;
7044
+ if (isBackEdge) backEdgeIndices.push(i);
7045
+ }
7046
+ if (backEdgeIndices.length === 0) return;
7047
+ const nodeExitCount = /* @__PURE__ */ new Map();
7048
+ const nodeEnterCount = /* @__PURE__ */ new Map();
7049
+ for (const idx of backEdgeIndices) {
7050
+ const edge = edges[idx];
7051
+ nodeExitCount.set(edge.source, (nodeExitCount.get(edge.source) ?? 0) + 1);
7052
+ nodeEnterCount.set(edge.target, (nodeEnterCount.get(edge.target) ?? 0) + 1);
7053
+ }
7054
+ const nodeTotalPorts = /* @__PURE__ */ new Map();
7055
+ const allNodeIds = /* @__PURE__ */ new Set([...nodeExitCount.keys(), ...nodeEnterCount.keys()]);
7056
+ for (const id of allNodeIds) {
7057
+ nodeTotalPorts.set(id, (nodeExitCount.get(id) ?? 0) + (nodeEnterCount.get(id) ?? 0));
7058
+ }
7059
+ const nodeNextExitSlot = /* @__PURE__ */ new Map();
7060
+ const nodeNextEnterSlot = /* @__PURE__ */ new Map();
7061
+ const allBounds = flattenGroupBounds(groups);
7062
+ for (const n of nodes) {
7063
+ allBounds.push({ x: n.x, y: n.y, right: n.x + n.width, bottom: n.y + n.height });
7064
+ }
7065
+ const diagramRight = Math.max(...allBounds.map((b) => b.right));
7066
+ const EDGE_SPACING = 12;
7067
+ const PORT_MARGIN = 6;
7068
+ function portY(node, slotIndex, totalSlots) {
7069
+ if (totalSlots === 1) return node.y + node.height / 2;
7070
+ const usable = node.height - PORT_MARGIN * 2;
7071
+ return node.y + PORT_MARGIN + usable * (slotIndex + 0.5) / totalSlots;
7072
+ }
7073
+ for (let bi = 0; bi < backEdgeIndices.length; bi++) {
7074
+ const edge = edges[backEdgeIndices[bi]];
7075
+ const src = nodeMap.get(edge.source);
7076
+ const tgt = nodeMap.get(edge.target);
7077
+ const srcExitIdx = nodeNextExitSlot.get(edge.source) ?? 0;
7078
+ nodeNextExitSlot.set(edge.source, srcExitIdx + 1);
7079
+ const srcExitY = portY(src, srcExitIdx, nodeTotalPorts.get(edge.source));
7080
+ const tgtEnterIdx = nodeNextEnterSlot.get(edge.target) ?? 0;
7081
+ nodeNextEnterSlot.set(edge.target, tgtEnterIdx + 1);
7082
+ const tgtExitOffset = nodeExitCount.get(edge.target) ?? 0;
7083
+ const tgtEntryY = portY(tgt, tgtExitOffset + tgtEnterIdx, nodeTotalPorts.get(edge.target));
7084
+ const srcRight = src.x + src.width;
7085
+ const tgtRight = tgt.x + tgt.width;
7086
+ const marginX = diagramRight + 20 + bi * EDGE_SPACING;
7087
+ edge.points = [
7088
+ { x: srcRight, y: srcExitY },
7089
+ { x: marginX, y: srcExitY },
7090
+ { x: marginX, y: tgtEntryY },
7091
+ { x: tgtRight, y: tgtEntryY }
7092
+ ];
7093
+ }
7094
+ const LABEL_PADDING = 8;
7095
+ const placedLabels = [];
7096
+ for (const idx of backEdgeIndices) {
7097
+ const edge = edges[idx];
7098
+ if (!edge.label) continue;
7099
+ const metrics = measureMultilineText(edge.label, FONT_SIZES.edgeLabel, FONT_WEIGHTS.edgeLabel);
7100
+ const halfW = (metrics.width + LABEL_PADDING * 2) / 2;
7101
+ const halfH = (metrics.height + LABEL_PADDING * 2) / 2;
7102
+ const marginX = edge.points[1].x;
7103
+ const topY = Math.min(edge.points[1].y, edge.points[2].y);
7104
+ const botY = Math.max(edge.points[1].y, edge.points[2].y);
7105
+ let labelX = marginX;
7106
+ let labelY = (topY + botY) / 2;
7107
+ for (let attempt = 0; attempt < placedLabels.length + 1; attempt++) {
7108
+ let overlap = false;
7109
+ for (const prev of placedLabels) {
7110
+ const overlapX = Math.abs(labelX - prev.x) < halfW + prev.hw;
7111
+ const overlapY = Math.abs(labelY - prev.y) < halfH + prev.hh;
7112
+ if (overlapX && overlapY) {
7113
+ labelY = prev.y + prev.hh + halfH + 2;
7114
+ overlap = true;
7115
+ break;
7116
+ }
7117
+ }
7118
+ if (!overlap) break;
7119
+ }
7120
+ labelY = Math.max(topY + halfH, Math.min(botY - halfH, labelY));
7121
+ edge.labelPosition = { x: labelX, y: labelY };
7122
+ placedLabels.push({ x: labelX, y: labelY, hw: halfW, hh: halfH });
7123
+ }
7124
+ }
6939
7125
  function collectEdgeSegments(elkNode, segments, offsetX, offsetY) {
6940
7126
  if (elkNode.edges) {
6941
7127
  for (const elkEdge of elkNode.edges) {
@@ -7276,9 +7462,121 @@ function bundleEdgePaths(edges, nodes, groups, direction) {
7276
7462
  }
7277
7463
  }
7278
7464
  }
7465
+ function buildFlatElkGraph(compoundElk) {
7466
+ const flatNodes = [];
7467
+ const flatEdges = [];
7468
+ function collectNodes(node) {
7469
+ if (!node.children) return;
7470
+ for (const child of node.children) {
7471
+ if (child.children) {
7472
+ if (child.children.length > 0) collectNodes(child);
7473
+ } else {
7474
+ flatNodes.push({ id: child.id, width: child.width, height: child.height, labels: child.labels });
7475
+ }
7476
+ }
7477
+ }
7478
+ collectNodes(compoundElk);
7479
+ const flatNodeIds = new Set(flatNodes.map((n) => n.id));
7480
+ function collectEdges(node) {
7481
+ if (node.edges) {
7482
+ for (const e of node.edges) {
7483
+ if (e.id.endsWith("_internal")) continue;
7484
+ const src = e.sources[0];
7485
+ const tgt = e.targets[0];
7486
+ if (src && tgt && flatNodeIds.has(src) && flatNodeIds.has(tgt)) {
7487
+ flatEdges.push({ id: e.id, sources: [...e.sources], targets: [...e.targets] });
7488
+ }
7489
+ }
7490
+ }
7491
+ if (node.children) {
7492
+ for (const child of node.children) collectEdges(child);
7493
+ }
7494
+ }
7495
+ collectEdges(compoundElk);
7496
+ return {
7497
+ id: "flat_root",
7498
+ layoutOptions: {
7499
+ "elk.algorithm": "layered",
7500
+ "elk.direction": compoundElk.layoutOptions?.["elk.direction"] ?? "DOWN",
7501
+ "elk.spacing.nodeNode": compoundElk.layoutOptions?.["elk.spacing.nodeNode"] ?? "28",
7502
+ "elk.layered.spacing.nodeNodeBetweenLayers": compoundElk.layoutOptions?.["elk.layered.spacing.nodeNodeBetweenLayers"] ?? "48",
7503
+ "elk.spacing.edgeEdge": "12",
7504
+ "elk.edgeRouting": "ORTHOGONAL",
7505
+ "elk.layered.cycleBreaking.strategy": "DEPTH_FIRST",
7506
+ "elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
7507
+ "elk.padding": compoundElk.layoutOptions?.["elk.padding"] ?? "[top=40,left=40,bottom=40,right=40]"
7508
+ },
7509
+ children: flatNodes,
7510
+ edges: flatEdges
7511
+ };
7512
+ }
7513
+ function extractFlatLayers(flatResult, layerSpacing, direction) {
7514
+ const isHorizontal = direction === "LR" || direction === "RL";
7515
+ const positions = /* @__PURE__ */ new Map();
7516
+ for (const child of flatResult.children ?? []) {
7517
+ positions.set(child.id, isHorizontal ? child.x ?? 0 : child.y ?? 0);
7518
+ }
7519
+ const sorted = [...positions.entries()].sort((a, b) => a[1] - b[1]);
7520
+ if (sorted.length === 0) return /* @__PURE__ */ new Map();
7521
+ const nodeLayer = /* @__PURE__ */ new Map();
7522
+ const threshold = layerSpacing * 0.6;
7523
+ let layerIdx = 0;
7524
+ let prevPos = sorted[0][1];
7525
+ nodeLayer.set(sorted[0][0], 0);
7526
+ for (let i = 1; i < sorted.length; i++) {
7527
+ if (sorted[i][1] - prevPos > threshold) layerIdx++;
7528
+ nodeLayer.set(sorted[i][0], layerIdx);
7529
+ prevPos = sorted[i][1];
7530
+ }
7531
+ return nodeLayer;
7532
+ }
7533
+ function addLayerConstraintEdges(elkGraph, nodeLayer, subgraphs) {
7534
+ const sgNodeMap = /* @__PURE__ */ new Map();
7535
+ function findSgNodes(node) {
7536
+ if (!node.children) return;
7537
+ for (const child of node.children) {
7538
+ if (child.children && child.children.length > 0) {
7539
+ sgNodeMap.set(child.id, child);
7540
+ findSgNodes(child);
7541
+ }
7542
+ }
7543
+ }
7544
+ findSgNodes(elkGraph);
7545
+ for (const sg of subgraphs) {
7546
+ const elkSg = sgNodeMap.get(sg.id);
7547
+ if (!elkSg || !elkSg.edges) continue;
7548
+ const members = sg.nodeIds.filter((id) => nodeLayer.has(id)).sort((a, b) => nodeLayer.get(a) - nodeLayer.get(b));
7549
+ if (members.length < 2) continue;
7550
+ const firstLayer = nodeLayer.get(members[0]);
7551
+ const lastLayer = nodeLayer.get(members[members.length - 1]);
7552
+ if (firstLayer === lastLayer) continue;
7553
+ for (let i = 0; i < members.length - 1; i++) {
7554
+ const srcLayer = nodeLayer.get(members[i]);
7555
+ const tgtLayer = nodeLayer.get(members[i + 1]);
7556
+ if (srcLayer < tgtLayer) {
7557
+ elkSg.edges.push({
7558
+ id: `lc_${sg.id}_${i}`,
7559
+ sources: [members[i]],
7560
+ targets: [members[i + 1]]
7561
+ });
7562
+ }
7563
+ }
7564
+ if (sg.children.length > 0) {
7565
+ addLayerConstraintEdges(elkGraph, nodeLayer, sg.children);
7566
+ }
7567
+ }
7568
+ }
7279
7569
  function layoutGraphSync(graph, options = {}) {
7280
7570
  const opts = { ...DEFAULTS2, ...options };
7281
7571
  const elkGraph = mermaidToElk(graph, opts);
7572
+ if (graph.subgraphs.length > 0) {
7573
+ const flatElk = buildFlatElkGraph(elkGraph);
7574
+ if ((flatElk.children?.length ?? 0) >= 2 && (flatElk.edges?.length ?? 0) > 0) {
7575
+ const flatResult = elkLayoutSync(flatElk);
7576
+ const nodeLayer = extractFlatLayers(flatResult, opts.layerSpacing, graph.direction);
7577
+ addLayerConstraintEdges(elkGraph, nodeLayer, graph.subgraphs);
7578
+ }
7579
+ }
7282
7580
  const result = elkLayoutSync(elkGraph);
7283
7581
  return elkToPositioned(result, graph, DEFAULTS2.mergeEdges);
7284
7582
  }
@@ -7378,7 +7676,6 @@ function renderGroup(group, font) {
7378
7676
  }
7379
7677
  function renderEdge(edge) {
7380
7678
  if (edge.points.length < 2) return "";
7381
- const pathData = pointsToPolylinePath(edge.points);
7382
7679
  const dashArray = edge.style === "dotted" ? ' stroke-dasharray="4 4"' : "";
7383
7680
  const baseStrokeWidth = edge.style === "thick" ? STROKE_WIDTHS.connector * 2 : STROKE_WIDTHS.connector;
7384
7681
  const strokeColor = escapeAttr(edge.inlineStyle?.stroke ?? "var(--_line)");
@@ -7398,10 +7695,41 @@ function renderEdge(edge) {
7398
7695
  if (edge.label) {
7399
7696
  dataAttrs.push(`data-label="${escapeAttr(edge.label)}"`);
7400
7697
  }
7401
- return `<polyline ${dataAttrs.join(" ")} points="${pathData}" fill="none" stroke="${strokeColor}" stroke-width="${strokeWidth}"${dashArray}${markers} />`;
7698
+ return `<path ${dataAttrs.join(" ")} d="${pointsToRoundedPath(edge.points)}" fill="none" stroke="${strokeColor}" stroke-width="${strokeWidth}"${dashArray}${markers} />`;
7402
7699
  }
7403
- function pointsToPolylinePath(points) {
7404
- return points.map((p) => `${p.x},${p.y}`).join(" ");
7700
+ function pointsToRoundedPath(points, radius = 6) {
7701
+ if (points.length < 2) return "";
7702
+ if (points.length === 2 || radius <= 0) {
7703
+ return `M${points[0].x},${points[0].y}` + points.slice(1).map((p) => `L${p.x},${p.y}`).join("");
7704
+ }
7705
+ const parts = [`M${points[0].x},${points[0].y}`];
7706
+ for (let i = 1; i < points.length - 1; i++) {
7707
+ const prev = points[i - 1];
7708
+ const curr = points[i];
7709
+ const next = points[i + 1];
7710
+ const lenIn = Math.abs(curr.x - prev.x) + Math.abs(curr.y - prev.y);
7711
+ const lenOut = Math.abs(next.x - curr.x) + Math.abs(next.y - curr.y);
7712
+ const r2 = Math.min(radius, lenIn / 2, lenOut / 2);
7713
+ if (r2 < 1) {
7714
+ parts.push(`L${curr.x},${curr.y}`);
7715
+ continue;
7716
+ }
7717
+ const dx1 = curr.x - prev.x;
7718
+ const dy1 = curr.y - prev.y;
7719
+ const d1 = Math.sqrt(dx1 * dx1 + dy1 * dy1) || 1;
7720
+ const startX = curr.x - dx1 / d1 * r2;
7721
+ const startY = curr.y - dy1 / d1 * r2;
7722
+ const dx2 = next.x - curr.x;
7723
+ const dy2 = next.y - curr.y;
7724
+ const d2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
7725
+ const endX = curr.x + dx2 / d2 * r2;
7726
+ const endY = curr.y + dy2 / d2 * r2;
7727
+ parts.push(`L${startX},${startY}`);
7728
+ parts.push(`Q${curr.x},${curr.y} ${endX},${endY}`);
7729
+ }
7730
+ const last = points[points.length - 1];
7731
+ parts.push(`L${last.x},${last.y}`);
7732
+ return parts.join("");
7405
7733
  }
7406
7734
  function renderEdgeLabel(edge, font) {
7407
7735
  const mid = edge.labelPosition ?? edgeMidpoint(edge.points);