zombie-mermaid 4.2.2 → 4.2.3

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.
Files changed (92) hide show
  1. package/package.json +9 -7
  2. package/src/__tests__/architecture-integration.test.ts +0 -217
  3. package/src/__tests__/ascii-ansi.test.ts +0 -539
  4. package/src/__tests__/ascii-draw-bundles.test.ts +0 -849
  5. package/src/__tests__/ascii-edge-bundling-routing.test.ts +0 -214
  6. package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +0 -216
  7. package/src/__tests__/ascii-grid-deep-chain.test.ts +0 -64
  8. package/src/__tests__/ascii-hyperlinks.test.ts +0 -364
  9. package/src/__tests__/ascii-nested-subgraph-sibling-order-444.test.ts +0 -108
  10. package/src/__tests__/ascii-nonmember-beside-frame-1283.test.ts +0 -113
  11. package/src/__tests__/ascii-package-boundary.test.ts +0 -291
  12. package/src/__tests__/ascii-parallel-edges-329.test.ts +0 -315
  13. package/src/__tests__/ascii-parallel-lane-border-1230.test.ts +0 -71
  14. package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +0 -282
  15. package/src/__tests__/ascii-stacked-frame-geometry-1285.test.ts +0 -107
  16. package/src/__tests__/ascii-subgraph-bbox.test.ts +0 -139
  17. package/src/__tests__/ascii-subgraph-later-edgeless-301.test.ts +0 -277
  18. package/src/__tests__/browser.test.ts +0 -34
  19. package/src/__tests__/c4-integration.test.ts +0 -161
  20. package/src/__tests__/c4-orientation.test.ts +0 -162
  21. package/src/__tests__/c4-upstream-integration.test.ts +0 -211
  22. package/src/__tests__/class-click-interactions.test.ts +0 -232
  23. package/src/__tests__/class-integration.test.ts +0 -154
  24. package/src/__tests__/class-notes.test.ts +0 -317
  25. package/src/__tests__/class-parser-error-messages.test.ts +0 -162
  26. package/src/__tests__/class-parser.test.ts +0 -347
  27. package/src/__tests__/class-styling.test.ts +0 -485
  28. package/src/__tests__/cli-diagrams.test.ts +0 -106
  29. package/src/__tests__/cli-e2e.test.ts +0 -275
  30. package/src/__tests__/cli-entrypoint.test.ts +0 -146
  31. package/src/__tests__/cli-mcp-dispatch.test.ts +0 -68
  32. package/src/__tests__/cli-mcp.test.ts +0 -44
  33. package/src/__tests__/cli-png.test.ts +0 -35
  34. package/src/__tests__/cli-render.test.ts +0 -988
  35. package/src/__tests__/cli-test-helpers.ts +0 -54
  36. package/src/__tests__/cli-web-dispatch.test.ts +0 -62
  37. package/src/__tests__/cli-web.test.ts +0 -203
  38. package/src/__tests__/cli.test.ts +0 -1027
  39. package/src/__tests__/compound-flat-layout.test.ts +0 -901
  40. package/src/__tests__/direction-override.test.ts +0 -397
  41. package/src/__tests__/edge-approach-direction.test.ts +0 -295
  42. package/src/__tests__/edge-bundling-node-clearance.test.ts +0 -179
  43. package/src/__tests__/elk-layout-cache.test.ts +0 -223
  44. package/src/__tests__/er-click-unsupported.test.ts +0 -69
  45. package/src/__tests__/er-integration.test.ts +0 -531
  46. package/src/__tests__/er-parser-error-messages.test.ts +0 -82
  47. package/src/__tests__/er-parser.test.ts +0 -294
  48. package/src/__tests__/expanded-shapes.test.ts +0 -229
  49. package/src/__tests__/flowchart-back-edge-prereversal-1239.test.ts +0 -212
  50. package/src/__tests__/flowchart-syntax-gaps.test.ts +0 -371
  51. package/src/__tests__/guides-examples.test.ts +0 -181
  52. package/src/__tests__/integration.test.ts +0 -729
  53. package/src/__tests__/interactivity-config.test.ts +0 -723
  54. package/src/__tests__/interactivity-option.test.ts +0 -173
  55. package/src/__tests__/layout-disconnected.test.ts +0 -384
  56. package/src/__tests__/layout-engine.test.ts +0 -91
  57. package/src/__tests__/linkstyle.test.ts +0 -133
  58. package/src/__tests__/mcp-render-ascii-tool.test.ts +0 -95
  59. package/src/__tests__/mcp-render-svg-theme-guard.test.ts +0 -36
  60. package/src/__tests__/mcp-render-svg-tool.test.ts +0 -118
  61. package/src/__tests__/mermaid-entity-codes.test.ts +0 -114
  62. package/src/__tests__/monospace-metrics.test.ts +0 -74
  63. package/src/__tests__/multiline-labels.test.ts +0 -924
  64. package/src/__tests__/nested-subgraph-direction-crossing.test.ts +0 -322
  65. package/src/__tests__/nested-subgraph-edge-container-offset.test.ts +0 -151
  66. package/src/__tests__/package-info.test.ts +0 -51
  67. package/src/__tests__/parser.test.ts +0 -1544
  68. package/src/__tests__/render-options-overrides.test.ts +0 -257
  69. package/src/__tests__/render-options-per-type.test.ts +0 -178
  70. package/src/__tests__/renderer.test.ts +0 -743
  71. package/src/__tests__/resolve-colors.test.ts +0 -448
  72. package/src/__tests__/self-loop-edges.test.ts +0 -112
  73. package/src/__tests__/sequence-activation-check.test.ts +0 -219
  74. package/src/__tests__/sequence-integration.test.ts +0 -312
  75. package/src/__tests__/sequence-layout.test.ts +0 -1103
  76. package/src/__tests__/sequence-parser-syntax-errors.test.ts +0 -179
  77. package/src/__tests__/sequence-parser.test.ts +0 -821
  78. package/src/__tests__/start-marker-only-links-1386.test.ts +0 -104
  79. package/src/__tests__/state-composite-order-1287.test.ts +0 -132
  80. package/src/__tests__/state-flow-order-1240.test.ts +0 -113
  81. package/src/__tests__/state-styling.test.ts +0 -150
  82. package/src/__tests__/statements.test.ts +0 -264
  83. package/src/__tests__/styles.test.ts +0 -220
  84. package/src/__tests__/subgraph-title-edge-gap-1239.test.ts +0 -238
  85. package/src/__tests__/svg-accessible-name-conformance.test.ts +0 -276
  86. package/src/__tests__/svg-accessible-name.test.ts +0 -421
  87. package/src/__tests__/svg-csp-style-options.test.ts +0 -427
  88. package/src/__tests__/workspace-package-boundaries.test.ts +0 -336
  89. package/src/__tests__/xychart-integration.test.ts +0 -137
  90. package/src/__tests__/xychart-layout.test.ts +0 -205
  91. package/src/__tests__/xychart-parser-categories.test.ts +0 -78
  92. package/src/__tests__/xychart-parser-errors.test.ts +0 -95
@@ -1,282 +0,0 @@
1
- /**
2
- * Regression tests for #629: the reverse-direction half of #329.
3
- *
4
- * #329 fixed two edges running the *same* way between one pair of nodes
5
- * (`A -->|One| B` twice) by giving every edge past the first its own offset
6
- * lane. That fix keyed its grouping on the **ordered** pair
7
- * `(source, target)`, so `Browser --> Server` and `Server --> Browser`
8
- * landed in two different groups and neither was ever tagged as a sibling
9
- * of the other. Both then routed through the same horizontal channel and
10
- * reproduced #329's original defect byte for byte:
11
- *
12
- * ┌─────────┐ ┌────────┐
13
- * │ Browser ├rrequest─►┤ Server │ <- "request"/"response" composited
14
- * └─────────┘ └────────┘ and one arrowhead dropped
15
- *
16
- * The fix (edge-routing.ts): `parallelGroupKey` orders the two node names
17
- * so a reciprocal pair shares one group — but only when the two nodes are
18
- * laid out side by side on the same grid row (`sharesHorizontalChannel`),
19
- * which is the layout where they actually collide. A *vertically* stacked
20
- * reciprocal pair is deliberately left in separate groups: draw-arrows.ts's
21
- * `drawTextOnLine` already pulls each of those labels into its own half of
22
- * the shared vertical segment (#530), and that mechanism only works on a
23
- * vertical segment, so routing one of the two through a lane would take it
24
- * out of play. `determinePath` additionally swaps in the *alternative*
25
- * start/end direction pair for a lane edge whose preferred pair is
26
- * degenerate (same face on both ends, e.g. Down/Down for a backwards LR
27
- * edge), a shape `buildParallelLanePath`'s offset math cannot express.
28
- */
29
- import { describe, it, expect } from 'vitest'
30
- import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
31
- import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
32
- import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
33
- import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
34
- import { pathCells } from '../../packages/ascii-renderer/src/grid-occupancy.ts'
35
- import type {
36
- AsciiConfig,
37
- AsciiEdge,
38
- AsciiGraph,
39
- } from '../../packages/ascii-renderer/src/types.ts'
40
-
41
- const RIGHT_ARROW = '►'
42
- const LEFT_ARROW = '◄'
43
- const UP_ARROW = '▲'
44
- const DOWN_ARROW = '▼'
45
-
46
- function render(source: string): string {
47
- return renderMermaidASCII(source, { colorMode: 'none' })
48
- }
49
-
50
- function buildGraph(source: string, graphDirection: 'LR' | 'TD'): AsciiGraph {
51
- const parsed = parseMermaid(source)
52
- const config: AsciiConfig = {
53
- useAscii: false,
54
- paddingX: 5,
55
- paddingY: 5,
56
- boxBorderPadding: 1,
57
- graphDirection,
58
- }
59
- const graph = convertToAsciiGraph(parsed, config)
60
- createMapping(graph)
61
- return graph
62
- }
63
-
64
- function edgeBetween(
65
- graph: AsciiGraph,
66
- fromName: string,
67
- toName: string,
68
- ): AsciiEdge {
69
- const edge = graph.edges.find(
70
- (e) => e.from.name === fromName && e.to.name === toName,
71
- )
72
- if (!edge) throw new Error(`no ${fromName} --> ${toName} edge in graph`)
73
- return edge
74
- }
75
-
76
- function cellKey(c: { x: number; y: number }): string {
77
- return `${c.x},${c.y}`
78
- }
79
-
80
- /** Total arrowheads drawn, in any of the four glyph directions. */
81
- function countArrowheads(out: string): number {
82
- return [...out].filter((ch) =>
83
- [RIGHT_ARROW, LEFT_ARROW, UP_ARROW, DOWN_ARROW].includes(ch),
84
- ).length
85
- }
86
-
87
- const BOTH_LABELLED = `graph LR
88
- Browser -- request --> Server
89
- Server -- response --> Browser`
90
-
91
- describe('opposite-direction edges between one node pair (#629)', () => {
92
- it('the exact issue repro: both labels render intact and uncorrupted', () => {
93
- const out = render(BOTH_LABELLED)
94
-
95
- expect(out).toContain('request')
96
- expect(out).toContain('response')
97
- // The reported corruption: "response"'s leading "r" landing on the cell
98
- // before "request"'s own, so neither word is recoverable.
99
- expect(out).not.toContain('rrequest')
100
- })
101
-
102
- it('the exact issue repro: both arrowheads survive', () => {
103
- const out = render(BOTH_LABELLED)
104
-
105
- // Two declared edges, so two arrowheads — the issue's defect #2 was
106
- // that only one was drawn, making the pair read as one-way.
107
- expect(countArrowheads(out)).toBe(2)
108
- })
109
-
110
- it('the exact issue repro: the two edges occupy different rows', () => {
111
- const rows = render(BOTH_LABELLED).split('\n')
112
- const requestRow = rows.findIndex((r) => r.includes('request'))
113
- const responseRow = rows.findIndex((r) => r.includes('response'))
114
-
115
- expect(requestRow).toBeGreaterThan(-1)
116
- expect(responseRow).toBeGreaterThan(-1)
117
- // The issue's defect #3: one shared row, so nothing in the output
118
- // indicated the source had declared two edges.
119
- expect(requestRow).not.toBe(responseRow)
120
- })
121
-
122
- it('only the forward edge labelled: label intact, edges kept apart', () => {
123
- const source = `graph LR
124
- Browser -- request --> Server
125
- Server --> Browser`
126
- const out = render(source)
127
-
128
- expect(out).toContain('request')
129
- expect(countArrowheads(out)).toBe(2)
130
- // Before the fix these two rendered as a single `◄request►` line, which
131
- // reads as one bidirectional edge labelled "request" rather than a
132
- // labelled edge plus an unlabelled one — so assert they were actually
133
- // separated, not just that two arrowheads survived.
134
- const graph = buildGraph(source, 'LR')
135
- expect(edgeBetween(graph, 'Browser', 'Server').parallelLane?.index).toBe(0)
136
- expect(edgeBetween(graph, 'Server', 'Browser').parallelLane?.index).toBe(1)
137
- })
138
-
139
- it('only the reverse edge labelled: label intact, both arrowheads drawn', () => {
140
- const out = render(`graph LR
141
- Browser --> Server
142
- Server -- response --> Browser`)
143
-
144
- expect(out).toContain('response')
145
- // This orientation dropped the reverse edge's own `◄` before the fix.
146
- expect(countArrowheads(out)).toBe(2)
147
- })
148
-
149
- it('a chain of two opposed pairs keeps all four labels', () => {
150
- const out = render(`graph LR
151
- A -- a1 --> B
152
- B -- b1 --> A
153
- B -- b2 --> C
154
- C -- c1 --> B`)
155
-
156
- // b1 and c1 vanished entirely before the fix — each was drawn onto the
157
- // cells its forward partner had already claimed.
158
- for (const label of ['a1', 'b1', 'b2', 'c1']) {
159
- expect(out).toContain(label)
160
- }
161
- expect(countArrowheads(out)).toBe(4)
162
- })
163
-
164
- it('routes the reciprocal pair through distinct, non-identical paths', () => {
165
- const graph = buildGraph(BOTH_LABELLED, 'LR')
166
- const forward = edgeBetween(graph, 'Browser', 'Server')
167
- const reverse = edgeBetween(graph, 'Server', 'Browser')
168
-
169
- const forwardCells = new Set(forward.path.flatMap((c) => cellKey(c)))
170
- expect(reverse.path.map(cellKey).some((k) => !forwardCells.has(k))).toBe(
171
- true,
172
- )
173
- })
174
-
175
- it('gives the reciprocal pair non-overlapping label lines', () => {
176
- const graph = buildGraph(BOTH_LABELLED, 'LR')
177
- const forward = edgeBetween(graph, 'Browser', 'Server')
178
- const reverse = edgeBetween(graph, 'Server', 'Browser')
179
-
180
- const forwardCells = new Set(pathCells(forward.labelLine).map(cellKey))
181
- const overlap = pathCells(reverse.labelLine)
182
- .map(cellKey)
183
- .filter((k) => forwardCells.has(k))
184
- expect(overlap).toEqual([])
185
- })
186
-
187
- it('keeps the reverse edge’s lane between its own two nodes', () => {
188
- const graph = buildGraph(BOTH_LABELLED, 'LR')
189
- const reverse = edgeBetween(graph, 'Server', 'Browser')
190
- const from = reverse.from.gridCoord!
191
- const to = reverse.to.gridCoord!
192
- // A node's reserved block spans 3 grid columns from its own x.
193
- const minX = Math.min(from.x, to.x)
194
- const maxX = Math.max(from.x, to.x) + 2
195
-
196
- // determineStartAndEndDir hands a backwards LR edge a degenerate
197
- // Down/Down pair — leave and re-enter on the same (bottom) face —
198
- // which buildParallelLanePath's offset math reads as a *vertical*
199
- // departure and so offsets by column. That pushes the lane, and its
200
- // label with it, out past the right-hand node instead of down into the
201
- // channel below the pair, and drags the horizontal jogs across both
202
- // nodes' own bottom border rows on the way. determinePath swaps in the
203
- // alternative straight-through pair to avoid exactly this.
204
- for (const point of reverse.labelLine) {
205
- expect(point.x).toBeGreaterThanOrEqual(minX)
206
- expect(point.x).toBeLessThanOrEqual(maxX)
207
- }
208
- })
209
-
210
- it('tags exactly one of the two as a lane edge', () => {
211
- const graph = buildGraph(BOTH_LABELLED, 'LR')
212
- const forward = edgeBetween(graph, 'Browser', 'Server')
213
- const reverse = edgeBetween(graph, 'Server', 'Browser')
214
-
215
- // Same group, so index 0 keeps the ordinary center path and index 1
216
- // gets the offset lane.
217
- expect(forward.parallelLane?.index).toBe(0)
218
- expect(reverse.parallelLane?.index).toBe(1)
219
- expect(reverse.parallelLane?.total).toBe(2)
220
- })
221
-
222
- it('never folds a lane-assigned edge into a fan-in/fan-out bundle', () => {
223
- // Longest-path layering ranks a reciprocal pair on different levels (as
224
- // dagre does), so B and C can no longer sit side by side with a lane
225
- // group between them. The invariant still matters where a pair and a
226
- // fan-in bundle (B, C --> D) share the same edges' neighbourhood: no
227
- // edge of the pair may end up inside a bundle, and none may carry a lane
228
- // into one.
229
- const graph = buildGraph(
230
- `graph TD
231
- A --> B
232
- A --> C
233
- B --> D
234
- C --> D
235
- B -- x --> C
236
- C -- y --> B`,
237
- 'TD',
238
- )
239
-
240
- const bundled = (graph.bundles ?? []).flatMap((b) => b.edges)
241
- expect(bundled.length).toBeGreaterThan(0)
242
- expect(bundled.filter((e) => e.parallelLane)).toEqual([])
243
- const pair = new Set([
244
- edgeBetween(graph, 'B', 'C'),
245
- edgeBetween(graph, 'C', 'B'),
246
- ])
247
- expect(bundled.filter((e) => pair.has(e))).toEqual([])
248
- })
249
-
250
- it('leaves a vertically stacked reciprocal pair on the #530 mechanism', () => {
251
- const graph = buildGraph(
252
- `graph TD
253
- A -- down --> B
254
- B -- up --> A`,
255
- 'TD',
256
- )
257
-
258
- // Stacked in one column, not side by side: drawTextOnLine already
259
- // separates these two labels, so no lane is assigned (a lane here would
260
- // route one of them out of the shared vertical segment and disable it).
261
- expect(edgeBetween(graph, 'A', 'B').parallelLane).toBeUndefined()
262
- expect(edgeBetween(graph, 'B', 'A').parallelLane).toBeUndefined()
263
-
264
- const out = render(`graph TD
265
- A -- down --> B
266
- B -- up --> A`)
267
- expect(out).toContain('down')
268
- expect(out).toContain('up')
269
- expect(out).toContain(UP_ARROW)
270
- expect(out).toContain(DOWN_ARROW)
271
- })
272
-
273
- it('leaves a single `<-->` bidirectional edge merged onto one line', () => {
274
- const out = render(`graph LR
275
- A <--> B`)
276
-
277
- // One declared edge, drawn as one line with a head at each end — this
278
- // is a different code path from the per-edge lane assignment and must
279
- // stay untouched.
280
- expect(out).toContain(`${LEFT_ARROW}────${RIGHT_ARROW}`)
281
- })
282
- })
@@ -1,107 +0,0 @@
1
- /**
2
- * Regression tests for #1285 — uneven subgraph frame geometry in a stacked
3
- * layout. Two of its three complaints are fixed:
4
- *
5
- * - a back edge running beside a frame keeps a clear column from the wall
6
- * (it used to sit directly against it: `││`, then cross it with `┼┘`);
7
- * - a title widened for an entering stroke keeps a clear column before the
8
- * right wall (`Layer Three │`, not `Layer Three│`).
9
- *
10
- * The third (frames not left-aligned) is deliberate: real mermaid centres
11
- * frames, so it is left alone.
12
- */
13
- import { describe, it, expect } from 'vitest'
14
- import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
15
-
16
- const SOURCE = `graph TD
17
- subgraph L1[Layer One]
18
- A
19
- end
20
- subgraph L2[Layer Two]
21
- C
22
- end
23
- subgraph L3[Layer Three]
24
- E
25
- end
26
- A --> C
27
- C --> E
28
- E --> A`
29
-
30
- describe('ASCII: stacked frames keep clear columns beside walls and titles (#1285)', () => {
31
- const lines = renderMermaidASCII(SOURCE).split('\n')
32
-
33
- it('never draws the back edge directly against a frame wall', () => {
34
- expect(lines.some((l) => l.includes('││'))).toBe(false)
35
- })
36
-
37
- it('keeps a clear column when the edge runs beside a frame it passes', () => {
38
- const out = renderMermaidASCII(`graph TD
39
- subgraph S1[One]
40
- a
41
- end
42
- subgraph S2[Two]
43
- b
44
- end
45
- subgraph S3[Three]
46
- c
47
- end
48
- a --> b
49
- b --> c
50
- a --> x
51
- a --> c`)
52
- const trimmed = out
53
- .split('\n')
54
- .map((l) => l.trimEnd())
55
- .join('\n')
56
- expect(trimmed).toBe(
57
- [
58
- '┌───────┐',
59
- '│ One │',
60
- '│ │',
61
- '│ │',
62
- '│ ┌───┐ │',
63
- '│ │ │ │',
64
- '│ │ a ├─┼───────────┐',
65
- '│ │ │ │ │',
66
- '│ └─┬─┘ │ │',
67
- '│ │ │ │',
68
- '└───┼───┘ │',
69
- ' │ │',
70
- ' │ │',
71
- ' ├───────┐ │',
72
- '┌───┼─────┐ │ │',
73
- '│ │ Two │ │ │',
74
- '│ │ │ │ │',
75
- '│ ▼ │ │ ▼',
76
- '│ ┌───┐ │ │ ┌───┐',
77
- '│ │ │ │ │ │ │',
78
- '│ │ b │ │ │ │ x │',
79
- '│ │ │ │ │ │ │',
80
- '│ └─┬─┘ │ │ └───┘',
81
- '│ │ │ │',
82
- '└───┼─────┘ │',
83
- ' │ │',
84
- ' │ │',
85
- ' ├───────┘',
86
- '┌───┼───────┐',
87
- '│ │ Three │',
88
- '│ │ │',
89
- '│ ▼ │',
90
- '│ ┌───┐ │',
91
- '│ │ │ │',
92
- '│ │ c │ │',
93
- '│ │ │ │',
94
- '│ └───┘ │',
95
- '│ │',
96
- '└───────────┘',
97
- ].join('\n'),
98
- )
99
- })
100
-
101
- it('keeps a clear column between a widened title and the right wall', () => {
102
- for (const title of ['Layer Two', 'Layer Three']) {
103
- const line = lines.find((l) => l.includes(title))!
104
- expect(line).toMatch(new RegExp(`${title} +│`))
105
- }
106
- })
107
- })
@@ -1,139 +0,0 @@
1
- /**
2
- * Regression tests for #90 — "ASCII: edge-less node inside a subgraph merges
3
- * that subgraph's frame with a neighboring one."
4
- *
5
- * Root cause: root-node placement in `createMapping` (src/ascii/grid.ts) was
6
- * entirely subgraph-agnostic. An edge-less node (treated as an initial "root"
7
- * since it has no incoming edges) could land in the same row/column band as
8
- * an unrelated sibling subgraph's real root, purely because both were
9
- * "roots" and roots were laid out along one shared level regardless of which
10
- * subgraph they belonged to. That made one subgraph's bounding box (computed
11
- * as min/max over its members' actual positions) balloon out to enclose a
12
- * completely unrelated sibling subgraph, corrupting both frames' borders and
13
- * titles when drawn.
14
- *
15
- * The fix defers placement of root nodes that belong to a subgraph with
16
- * *other* members that are unreachable from that root, anchoring them next
17
- * to their already-placed subgraph siblings instead of the shared root
18
- * level. See `src/ascii/grid.ts`'s `createMapping` (deferredRoots handling).
19
- */
20
- import { describe, it, expect } from 'vitest'
21
- import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
22
- import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
23
- import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
24
- import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
25
- import type {
26
- AsciiConfig,
27
- AsciiGraph,
28
- AsciiSubgraph,
29
- } from '../../packages/ascii-renderer/src/types.ts'
30
-
31
- function layoutFlowchart(
32
- source: string,
33
- direction: 'TD' | 'LR' = 'TD',
34
- ): AsciiGraph {
35
- const parsed = parseMermaid(source)
36
- const config: AsciiConfig = {
37
- useAscii: false,
38
- paddingX: 5,
39
- paddingY: 5,
40
- boxBorderPadding: 1,
41
- graphDirection: direction,
42
- }
43
- const graph = convertToAsciiGraph(parsed, config)
44
- createMapping(graph)
45
- return graph
46
- }
47
-
48
- function boxesOverlap(a: AsciiSubgraph, b: AsciiSubgraph): boolean {
49
- return !(
50
- a.maxX <= b.minX ||
51
- b.maxX <= a.minX ||
52
- a.maxY <= b.minY ||
53
- b.maxY <= a.minY
54
- )
55
- }
56
-
57
- function findSubgraph(graph: AsciiGraph, name: string): AsciiSubgraph {
58
- const sg = graph.subgraphs.find((s) => s.name === name)
59
- if (!sg) throw new Error(`subgraph "${name}" not found`)
60
- return sg
61
- }
62
-
63
- describe('ASCII subgraph bounding boxes stay disjoint (#90)', () => {
64
- const issueRepro = `flowchart TB
65
- subgraph a["Frontend tier"]
66
- a1["load balancer"]
67
- end
68
- subgraph b["Application tier"]
69
- b1["worker pool"]
70
- b2["api server"]
71
- end
72
- a1 --> b1`
73
-
74
- it('keeps an edge-less subgraph member from merging with a sibling subgraph (TD, exact issue repro)', () => {
75
- const graph = layoutFlowchart(issueRepro, 'TD')
76
- expect(graph.subgraphs.length).toBe(2)
77
-
78
- const frontend = findSubgraph(graph, 'Frontend tier')
79
- const application = findSubgraph(graph, 'Application tier')
80
- expect(boxesOverlap(frontend, application)).toBe(false)
81
-
82
- const output = renderMermaidASCII(issueRepro)
83
- expect(output).toContain('Frontend tier')
84
- expect(output).toContain('Application tier')
85
- // Guard against the exact corrupted-title symptom from the issue, where
86
- // the two titles interleaved character-by-character into
87
- // "Frontendptieration tier".
88
- expect(output).not.toContain('Frontendptieration')
89
- })
90
-
91
- it('keeps an edge-less subgraph member from merging with a sibling subgraph (LR)', () => {
92
- const lrSource = issueRepro.replace('flowchart TB', 'flowchart LR')
93
- const graph = layoutFlowchart(lrSource, 'LR')
94
-
95
- const frontend = findSubgraph(graph, 'Frontend tier')
96
- const application = findSubgraph(graph, 'Application tier')
97
- expect(boxesOverlap(frontend, application)).toBe(false)
98
-
99
- const output = renderMermaidASCII(lrSource)
100
- expect(output).toContain('Frontend tier')
101
- expect(output).toContain('Application tier')
102
- })
103
-
104
- it('keeps multiple subgraphs — each with its own root plus an edge-less sibling — mutually disjoint', () => {
105
- const source = `flowchart TD
106
- subgraph a["Alpha"]
107
- a1["root"]
108
- end
109
- subgraph b["Beta"]
110
- b1["worker"]
111
- b2["idle"]
112
- end
113
- subgraph c["Gamma"]
114
- c1["worker"]
115
- c2["idle"]
116
- end
117
- a1 --> b1
118
- b1 --> c1`
119
-
120
- const graph = layoutFlowchart(source, 'TD')
121
- expect(graph.subgraphs.length).toBe(3)
122
-
123
- for (let i = 0; i < graph.subgraphs.length; i++) {
124
- for (let j = i + 1; j < graph.subgraphs.length; j++) {
125
- const sgI = graph.subgraphs[i]!
126
- const sgJ = graph.subgraphs[j]!
127
- expect(
128
- boxesOverlap(sgI, sgJ),
129
- `subgraph "${sgI.name}" overlaps "${sgJ.name}"`,
130
- ).toBe(false)
131
- }
132
- }
133
-
134
- const output = renderMermaidASCII(source)
135
- expect(output).toContain('Alpha')
136
- expect(output).toContain('Beta')
137
- expect(output).toContain('Gamma')
138
- })
139
- })