@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 +19 -30
- package/dist/index.js +346 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/integration.test.ts +3 -3
- package/src/__tests__/renderer.test.ts +5 -5
- package/src/ascii/draw.ts +110 -12
- package/src/layout-engine.ts +396 -0
- package/src/renderer.ts +57 -5
- package/src/theme.ts +3 -3
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
|
-
](https://www.npmjs.com/package/beautiful-mermaid)
|
|
9
|
+
[](https://www.npmjs.com/package/@ktrysmt/beautiful-mermaid)
|
|
12
10
|
[](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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
11
|
-
textSec:
|
|
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:
|
|
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
|
|
2931
|
-
const
|
|
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,
|
|
3027
|
+
function drawBoxStart(graph, path, firstLine, sourceNode) {
|
|
2994
3028
|
const canvas = copyCanvas(graph.canvas);
|
|
2995
3029
|
if (graph.config.useAscii) return canvas;
|
|
2996
|
-
if (
|
|
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
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
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 `<
|
|
7698
|
+
return `<path ${dataAttrs.join(" ")} d="${pointsToRoundedPath(edge.points)}" fill="none" stroke="${strokeColor}" stroke-width="${strokeWidth}"${dashArray}${markers} />`;
|
|
7402
7699
|
}
|
|
7403
|
-
function
|
|
7404
|
-
|
|
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);
|