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,277 +0,0 @@
1
- /**
2
- * Regression tests for #301 — "ASCII backend: a subgraph that carries no
3
- * edge is absorbed into an earlier subgraph's frame and its title is
4
- * deleted."
5
- *
6
- * This is the mirror image of #90/#143 (see ascii-subgraph-bbox.test.ts):
7
- * that fix deferred an edge-less *root* node's placement so it wouldn't be
8
- * laid out at the shared root level alongside an unrelated sibling
9
- * subgraph's root. But the deferred node still found its free slot via a
10
- * subgraph-agnostic blind slide (`reserveSpotInGrid`'s built-in collision
11
- * handling) — sliding one grid step at a time along the shared axis with no
12
- * notion of subgraph ownership. When an *earlier* subgraph has such a
13
- * deferred node AND a *later*, entirely edge-less sibling subgraph already
14
- * claimed the very next slot, the blind slide walked straight through that
15
- * later subgraph's root to find free space beyond it — landing the deferred
16
- * node on the far side. That sandwiched the later subgraph's node between
17
- * the earlier subgraph's own members, so the earlier subgraph's bounding box
18
- * (min/max over its members' positions) ballooned out to enclose the later
19
- * one, corrupting both frames and dropping the later subgraph's title
20
- * entirely.
21
- *
22
- * The fix (`src/ascii/grid.ts`): attach each subgraph's deferred nodes to
23
- * whichever of its own roots gets placed first, immediately, before control
24
- * returns to the root-placement loop — so an unrelated subgraph's root can
25
- * never claim the slot a deferred sibling needs (`deferredByTopSg` /
26
- * `placeDeferredSiblingsNextToRoot`). A `findSubgraphAdjacentSlot` fallback
27
- * additionally keeps the later pass (for deferred nodes whose anchor is
28
- * itself a non-root) from enclosing a foreign subgraph's node when sliding.
29
- */
30
- import { describe, it, expect } from 'vitest'
31
- import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
32
- import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
33
- import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
34
- import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
35
- import type {
36
- AsciiConfig,
37
- AsciiGraph,
38
- AsciiSubgraph,
39
- } from '../../packages/ascii-renderer/src/types.ts'
40
-
41
- function layoutFlowchart(
42
- source: string,
43
- direction: 'TD' | 'LR' = 'TD',
44
- ): AsciiGraph {
45
- const parsed = parseMermaid(source)
46
- const config: AsciiConfig = {
47
- useAscii: false,
48
- paddingX: 5,
49
- paddingY: 5,
50
- boxBorderPadding: 1,
51
- graphDirection: direction,
52
- }
53
- const graph = convertToAsciiGraph(parsed, config)
54
- createMapping(graph)
55
- return graph
56
- }
57
-
58
- function boxesOverlap(a: AsciiSubgraph, b: AsciiSubgraph): boolean {
59
- return !(
60
- a.maxX <= b.minX ||
61
- b.maxX <= a.minX ||
62
- a.maxY <= b.minY ||
63
- b.maxY <= a.minY
64
- )
65
- }
66
-
67
- function findSubgraph(graph: AsciiGraph, name: string): AsciiSubgraph {
68
- const sg = graph.subgraphs.find((s) => s.name === name)
69
- if (!sg) throw new Error(`subgraph "${name}" not found`)
70
- return sg
71
- }
72
-
73
- /** Whether a node's drawn box sits fully inside a subgraph's bounding box. */
74
- function nodeInsideBox(
75
- graph: AsciiGraph,
76
- nodeName: string,
77
- sg: AsciiSubgraph,
78
- ): boolean {
79
- const node = graph.nodes.find((n) => n.name === nodeName)
80
- if (!node?.drawingCoord || !node.drawing) {
81
- throw new Error(`node "${nodeName}" wasn't drawn`)
82
- }
83
- const minX = node.drawingCoord.x
84
- const minY = node.drawingCoord.y
85
- const maxX = minX + node.drawing.length - 1
86
- const maxY = minY + node.drawing[0]!.length - 1
87
- return (
88
- minX >= sg.minX && maxX <= sg.maxX && minY >= sg.minY && maxY <= sg.maxY
89
- )
90
- }
91
-
92
- describe('ASCII: a later edge-less subgraph is not absorbed into an earlier one (#301)', () => {
93
- const issueRepro = `flowchart TB
94
- subgraph a["Group A"]
95
- a1["alpha"]
96
- a2["bravo"]
97
- a3["charlie"]
98
- end
99
- subgraph b["Group B"]
100
- b1["delta"]
101
- end
102
- a1 --> a2`
103
-
104
- it('keeps two subgraphs disjoint: earlier one has an edge-less member, later one carries no edge at all (exact issue repro)', () => {
105
- const graph = layoutFlowchart(issueRepro, 'TD')
106
- expect(graph.subgraphs.length).toBe(2)
107
-
108
- const groupA = findSubgraph(graph, 'Group A')
109
- const groupB = findSubgraph(graph, 'Group B')
110
- expect(boxesOverlap(groupA, groupB)).toBe(false)
111
-
112
- // delta belongs to Group B and must be drawn inside Group B's frame,
113
- // not dragged into Group A's.
114
- expect(nodeInsideBox(graph, 'b1', groupB)).toBe(true)
115
- expect(nodeInsideBox(graph, 'b1', groupA)).toBe(false)
116
-
117
- const output = renderMermaidASCII(issueRepro)
118
- expect(output).toContain('Group A')
119
- expect(output).toContain('Group B')
120
- })
121
-
122
- it('keeps the same two subgraphs disjoint in LR', () => {
123
- const lrSource = issueRepro.replace('flowchart TB', 'flowchart LR')
124
- const graph = layoutFlowchart(lrSource, 'LR')
125
-
126
- const groupA = findSubgraph(graph, 'Group A')
127
- const groupB = findSubgraph(graph, 'Group B')
128
- expect(boxesOverlap(groupA, groupB)).toBe(false)
129
- expect(nodeInsideBox(graph, 'b1', groupB)).toBe(true)
130
-
131
- const output = renderMermaidASCII(lrSource)
132
- expect(output).toContain('Group A')
133
- expect(output).toContain('Group B')
134
- })
135
-
136
- it('does not collapse a third, equally edge-less subgraph into the same frame (N-subgraph case)', () => {
137
- const source = `flowchart TB
138
- subgraph a["Group A"]
139
- a1["alpha"]
140
- a2["bravo"]
141
- a3["charlie"]
142
- end
143
- subgraph b["Group B"]
144
- b1["delta"]
145
- end
146
- subgraph c["Group C"]
147
- c1["echo"]
148
- end
149
- a1 --> a2`
150
-
151
- const graph = layoutFlowchart(source, 'TD')
152
- expect(graph.subgraphs.length).toBe(3)
153
-
154
- const groupA = findSubgraph(graph, 'Group A')
155
- const groupB = findSubgraph(graph, 'Group B')
156
- const groupC = findSubgraph(graph, 'Group C')
157
-
158
- for (const [nameI, sgI] of [
159
- ['Group A', groupA],
160
- ['Group B', groupB],
161
- ['Group C', groupC],
162
- ] as const) {
163
- for (const [nameJ, sgJ] of [
164
- ['Group A', groupA],
165
- ['Group B', groupB],
166
- ['Group C', groupC],
167
- ] as const) {
168
- if (nameI >= nameJ) continue
169
- expect(
170
- boxesOverlap(sgI, sgJ),
171
- `subgraph "${nameI}" overlaps "${nameJ}"`,
172
- ).toBe(false)
173
- }
174
- }
175
-
176
- expect(nodeInsideBox(graph, 'b1', groupB)).toBe(true)
177
- expect(nodeInsideBox(graph, 'c1', groupC)).toBe(true)
178
- expect(nodeInsideBox(graph, 'b1', groupA)).toBe(false)
179
- expect(nodeInsideBox(graph, 'c1', groupA)).toBe(false)
180
-
181
- const output = renderMermaidASCII(source)
182
- expect(output).toContain('Group A')
183
- expect(output).toContain('Group B')
184
- expect(output).toContain('Group C')
185
- })
186
-
187
- it('slides a second edge-less sibling past the first instead of colliding with it', () => {
188
- // a3 and a4 are both edge-less and both unreachable from a1, so both
189
- // get deferred and both attach to a1. The second one (a4) must slide
190
- // past the first (a3) — exercising findSubgraphAdjacentSlot's "occupant
191
- // belongs to the same subgraph, keep sliding" branch — rather than
192
- // colliding with it.
193
- const source = `flowchart TB
194
- subgraph a["Group A"]
195
- a1["alpha"]
196
- a2["bravo"]
197
- a3["charlie"]
198
- a4["echo"]
199
- end
200
- a1 --> a2`
201
-
202
- const graph = layoutFlowchart(source, 'TD')
203
- const groupA = findSubgraph(graph, 'Group A')
204
- expect(groupA.nodes.length).toBe(4)
205
-
206
- const a3 = graph.nodes.find((n) => n.name === 'a3')!
207
- const a4 = graph.nodes.find((n) => n.name === 'a4')!
208
- expect(a3.gridCoord).not.toBeNull()
209
- expect(a4.gridCoord).not.toBeNull()
210
- // Distinct slots — a4 didn't overwrite a3's spot.
211
- expect(a4.gridCoord).not.toEqual(a3.gridCoord)
212
-
213
- const output = renderMermaidASCII(source)
214
- expect(output).toContain('Group A')
215
- expect(output).toContain('charlie')
216
- expect(output).toContain('echo')
217
- })
218
-
219
- it('falls back gracefully (no crash, titles intact) when a non-root anchor is foreign-blocked on both sides', () => {
220
- // b2 is edge-less and unreachable from b1, so it's deferred and anchors
221
- // to b1 — but b1 is itself a *non-root* (a child of a1 via an edge), so
222
- // this only resolves in the later fallback pass, after an unrelated
223
- // external node's own child (e2) has already claimed the slot
224
- // immediately next to b1, and the opposite slot is off-grid (negative).
225
- // findSubgraphAdjacentSlot correctly refuses to slide through e2 in
226
- // either direction (returning null), so this exercises its
227
- // "foreign-blocked" path on both sides — but with no free adjacent slot
228
- // available, the caller falls back to the pre-existing (#143-era) blind
229
- // slide, which can still enclose e2. That fallback is a known, narrow
230
- // limitation (see the "Defensive fallback" comment in grid.ts) distinct
231
- // from the bug #301 fixes: it only arises when the deferred node's
232
- // anchor is itself a non-root, not for the reported root-anchored case.
233
- // This test guards against a crash or dropped title, not full
234
- // disjointness.
235
- const source = `flowchart TB
236
- subgraph a["Group A"]
237
- a1["alpha"]
238
- end
239
- subgraph b["Group B"]
240
- b1["bravo"]
241
- b2["charlie"]
242
- end
243
- e1["delta"]
244
- a1 --> b1
245
- e1 --> e2["echo"]`
246
-
247
- const graph = layoutFlowchart(source, 'TD')
248
- expect(graph.nodes.find((n) => n.name === 'b2')?.gridCoord).not.toBeNull()
249
-
250
- const output = renderMermaidASCII(source)
251
- expect(output).toContain('Group A')
252
- expect(output).toContain('Group B')
253
- expect(output).toContain('charlie')
254
- })
255
-
256
- it('OK case: continues to work when every node in the earlier subgraph is on an edge', () => {
257
- const source = `flowchart TB
258
- subgraph a["Group A"]
259
- a1["alpha"]
260
- a2["bravo"]
261
- end
262
- subgraph b["Group B"]
263
- b1["delta"]
264
- end
265
- a1 --> a2`
266
-
267
- const graph = layoutFlowchart(source, 'TD')
268
- const groupA = findSubgraph(graph, 'Group A')
269
- const groupB = findSubgraph(graph, 'Group B')
270
- expect(boxesOverlap(groupA, groupB)).toBe(false)
271
- expect(nodeInsideBox(graph, 'b1', groupB)).toBe(true)
272
-
273
- const output = renderMermaidASCII(source)
274
- expect(output).toContain('Group A')
275
- expect(output).toContain('Group B')
276
- })
277
- })
@@ -1,34 +0,0 @@
1
- /**
2
- * Tests for the browser entry point.
3
- *
4
- * `src/browser.ts` runs in a plain `<script>` context where `window` is
5
- * ambient, but our test environment is Node, so we stub a fake `window`
6
- * before importing the module and reset the module registry so the
7
- * top-level assignment actually re-runs.
8
- */
9
- import { describe, it, expect, afterEach, vi } from 'vitest'
10
- import type { MermaidBrowserGlobal } from '../browser.ts'
11
-
12
- describe('browser entry point', () => {
13
- afterEach(() => {
14
- vi.unstubAllGlobals()
15
- vi.resetModules()
16
- })
17
-
18
- it('attaches the mermaid API surface to window.__mermaid', async () => {
19
- const fakeWindow: { __mermaid?: MermaidBrowserGlobal } = {}
20
- vi.stubGlobal('window', fakeWindow)
21
-
22
- await import('../browser.ts')
23
-
24
- const api = fakeWindow.__mermaid
25
- expect(api).toBeDefined()
26
- if (!api) throw new Error('window.__mermaid was not set')
27
- expect(api.renderMermaidSVGAsync).toBeTypeOf('function')
28
- expect(api.renderMermaidASCII).toBeTypeOf('function')
29
- expect(api.diagramColorsToAsciiTheme).toBeTypeOf('function')
30
- expect(api.getSeriesColor).toBeTypeOf('function')
31
- expect(api.THEMES).toBeTypeOf('object')
32
- expect(typeof api.CHART_ACCENT_FALLBACK).not.toBe('undefined')
33
- })
34
- })
@@ -1,161 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { renderMermaidSVG, renderMermaidASCII } from '../index.ts'
3
-
4
- // End-to-end coverage for C4 diagrams through both front doors (the
5
- // dedicated C4 renderers, not the flowchart pipeline). The fixtures ported
6
- // from lukilabs/beautiful-mermaid#71 live in c4-upstream-integration.test.ts.
7
-
8
- const CONTEXT = `C4Context
9
- title System Context for Internet Banking
10
- Person(customer, "Personal Banking Customer", "A customer of the bank")
11
- Enterprise_Boundary(b0, "Bank") {
12
- System(banking, "Internet Banking System", "Lets customers view accounts")
13
- SystemDb(ledger, "Ledger", "Stores transactions")
14
- }
15
- System_Ext(mail, "E-mail System", "Sends e-mails")
16
- Rel(customer, banking, "Uses", "HTTPS")
17
- Rel(banking, ledger, "Reads from and writes to")
18
- Rel_Back(customer, mail, "Sends e-mails to")
19
- Rel(banking, mail, "Sends e-mail using")`
20
-
21
- const CONTAINER = `C4Container
22
- Person(user, "User")
23
- System_Boundary(sys, "Shop") {
24
- Container(web, "Web App", "React", "Storefront")
25
- Container(api, "API", "Node.js", "Business logic")
26
- ContainerDb(db, "Database", "PostgreSQL", "Orders")
27
- ContainerQueue(q, "Events", "Kafka")
28
- }
29
- System_Ext(pay, "Payments")
30
- Rel(user, web, "Browses")
31
- Rel(web, api, "Calls", "JSON/HTTPS")
32
- Rel(api, db, "Reads/writes", "SQL")
33
- BiRel(api, q, "Publishes", "AMQP")
34
- Rel(api, pay, "Charges cards")`
35
-
36
- const COMPONENT = `C4Component
37
- Container_Boundary(api, "API") {
38
- Component(ctl, "Controller", "Express", "Routes")
39
- Component(svc, "Service", "TypeScript", "Rules")
40
- }
41
- ContainerDb(db, "Database", "Postgres")
42
- Rel(ctl, svc, "Uses")
43
- Rel(svc, db, "Reads")`
44
-
45
- describe('C4 SVG rendering', () => {
46
- for (const [name, src] of [
47
- ['context', CONTEXT],
48
- ['container', CONTAINER],
49
- ['component', COMPONENT],
50
- ] as const) {
51
- it(`renders a ${name} diagram`, () => {
52
- const svg = renderMermaidSVG(src)
53
- expect(svg.startsWith('<svg')).toBe(true)
54
- expect(svg).toContain('</svg>')
55
- })
56
- }
57
-
58
- it('draws element names, type lines and boundary labels', () => {
59
- const svg = renderMermaidSVG(CONTAINER)
60
- for (const text of [
61
- 'Web App',
62
- '[Container: React]',
63
- '[Container: PostgreSQL]',
64
- '[Software System]',
65
- 'Shop',
66
- 'Calls',
67
- '[JSON/HTTPS]',
68
- ]) {
69
- expect(svg).toContain(text)
70
- }
71
- })
72
-
73
- it('applies the C4 palette to people and external systems', () => {
74
- const svg = renderMermaidSVG(CONTEXT)
75
- expect(svg).toContain('#08427b') // person
76
- expect(svg).toContain('#1168bd') // internal system
77
- expect(svg).toContain('#999999') // external system
78
- })
79
-
80
- it('draws a cylinder for Db variants', () => {
81
- const db = renderMermaidSVG(
82
- 'C4Context\nSystemDb(d, "DB")\nSystem(s, "S")\nRel(s, d, "x")',
83
- )
84
- const plain = renderMermaidSVG(
85
- 'C4Context\nSystem(d, "DB")\nSystem(s, "S")\nRel(s, d, "x")',
86
- )
87
- // Only the cylinder has elliptical caps.
88
- expect(db).toContain('<ellipse')
89
- expect(plain).not.toContain('<ellipse')
90
- })
91
-
92
- it('ignores the direction option in SVG, as Mermaid has none for C4', () => {
93
- // Shapes are placed in rows in declaration order, whatever the direction.
94
- const strip = (svg: string) => svg.replace(/zm-title-\d+/g, 'zm-title')
95
- const tb = strip(renderMermaidSVG(COMPONENT))
96
- for (const direction of ['BT', 'LR', 'RL'] as const) {
97
- expect(strip(renderMermaidSVG(COMPONENT, { direction }))).toBe(tb)
98
- }
99
- })
100
-
101
- it('surfaces parse errors with a line number', () => {
102
- expect(() =>
103
- renderMermaidSVG('C4Context\nSystem(a, "A")\nSystem(a, "B")'),
104
- ).toThrow(/line 3: duplicate alias/)
105
- })
106
- })
107
-
108
- describe('C4 ASCII rendering', () => {
109
- for (const [name, src] of [
110
- ['context', CONTEXT],
111
- ['container', CONTAINER],
112
- ['component', COMPONENT],
113
- ] as const) {
114
- it(`renders a ${name} diagram`, () => {
115
- const out = renderMermaidASCII(src)
116
- expect(out.length).toBeGreaterThan(0)
117
- expect(out).toMatch(/[┌+]/)
118
- })
119
- }
120
-
121
- it('shows element names, type lines and boundary titles', () => {
122
- const out = renderMermaidASCII(CONTAINER)
123
- for (const text of ['Web App', '[Container: React]', 'Shop', 'Calls']) {
124
- expect(out).toContain(text)
125
- }
126
- })
127
-
128
- it('is deterministic', () => {
129
- expect(renderMermaidASCII(COMPONENT)).toBe(renderMermaidASCII(COMPONENT))
130
- })
131
-
132
- it('surfaces parse errors with a line number', () => {
133
- expect(() => renderMermaidASCII('C4Context\nBogus(a, "A")')).toThrow(
134
- /line 2: unknown C4 macro "Bogus"/,
135
- )
136
- })
137
- })
138
-
139
- describe('Rel_Back arrow direction', () => {
140
- const src = `C4Context
141
- System(a, "A")
142
- System(b, "B")
143
- Rel_Back(a, b, "Returns")`
144
-
145
- it('puts the SVG arrowhead at the declared "from" end', () => {
146
- const svg = renderMermaidSVG(src)
147
- const d =
148
- /<path class="c4-relationship"[^>]* d="M ([\d.-]+),([\d.-]+) /.exec(svg)!
149
- const [sx, sy] = [Number(d[1]), Number(d[2])]
150
- const tip = /<polygon class="c4-arrowhead" points="([\d.]+),([\d.]+) /.exec(
151
- svg,
152
- )!
153
- expect(Math.hypot(Number(tip[1]) - sx, Number(tip[2]) - sy)).toBeLessThan(1)
154
- })
155
-
156
- it('draws exactly one ASCII arrowhead, pointing up at "from"', () => {
157
- const out = renderMermaidASCII(src)
158
- expect(out).toContain('▲')
159
- expect(out).not.toContain('▼')
160
- })
161
- })
@@ -1,162 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { renderMermaidSVG, renderMermaidASCII } from '../index.ts'
3
-
4
- // `RenderOptions.direction` through the C4 renderers.
5
- //
6
- // SVG follows Mermaid's C4 renderer, which has no direction: shapes are placed
7
- // in rows in declaration order, so the option changes nothing. What must hold
8
- // in every direction is that each arrowhead sits on the right end of its
9
- // relationship (`Rel` at `to`, `Rel_Back` at `from`, `BiRel` at both).
10
- //
11
- // ASCII keeps its own layout: top to bottom, `BT` flips it, and `LR`/`RL`
12
- // render like `TB`.
13
-
14
- const DIRECTIONS = ['TB', 'BT', 'LR', 'RL'] as const
15
- type Dir = (typeof DIRECTIONS)[number]
16
-
17
- const SOURCE = `C4Context
18
- Person(a, "A")
19
- Enterprise_Boundary(b0, "Bank") {
20
- System(b, "B")
21
- SystemDb(c, "C")
22
- }
23
- System_Ext(d, "D")
24
- Rel(a, b, "Uses", "HTTPS")
25
- Rel(b, c, "Reads from and writes to")
26
- Rel_Back(a, d, "Sends to")
27
- BiRel(b, d, "Syncs")`
28
-
29
- const svgOf = (dir: Dir): string => renderMermaidSVG(SOURCE, { direction: dir })
30
-
31
- interface Line {
32
- from: string
33
- to: string
34
- start: [number, number]
35
- end: [number, number]
36
- }
37
-
38
- /** Each relationship's end points, read from its path (`M sx,sy L|Q ... ex,ey`). */
39
- function lines(svg: string): Line[] {
40
- return [
41
- ...svg.matchAll(
42
- /<path class="c4-relationship" data-from="([^"]+)" data-to="([^"]+)"[^>]* d="([^"]+)"/g,
43
- ),
44
- ].map((m) => {
45
- const nums = m[3]!.match(/-?[\d.]+/g)!.map(Number)
46
- return {
47
- from: m[1]!,
48
- to: m[2]!,
49
- start: [nums[0]!, nums[1]!],
50
- end: [nums[nums.length - 2]!, nums[nums.length - 1]!],
51
- }
52
- })
53
- }
54
-
55
- /** Arrowhead tips: the first vertex of each arrowhead polygon. */
56
- function tips(svg: string): [number, number][] {
57
- return [
58
- ...svg.matchAll(
59
- /<polygon class="c4-arrowhead" points="(-?[\d.]+),(-?[\d.]+) /g,
60
- ),
61
- ].map((m) => [Number(m[1]), Number(m[2])])
62
- }
63
-
64
- const near = (p: [number, number], q: [number, number]): boolean =>
65
- Math.abs(p[0] - q[0]) < 1 && Math.abs(p[1] - q[1]) < 1
66
-
67
- describe.each(DIRECTIONS)('C4 SVG, direction %s', (dir) => {
68
- const svg = svgOf(dir)
69
- const line = (from: string, to: string): Line => {
70
- const l = lines(svg).find((x) => x.from === from && x.to === to)
71
- if (!l) throw new Error(`no relationship ${from} -> ${to}`)
72
- return l
73
- }
74
- const headsAt = (p: [number, number]): number =>
75
- tips(svg).filter((t) => near(t, p)).length
76
-
77
- it('puts the arrowhead at "to" for Rel', () => {
78
- const l = line('a', 'b')
79
- expect(headsAt(l.end)).toBe(1)
80
- expect(headsAt(l.start)).toBe(0)
81
- })
82
-
83
- it('puts the arrowhead at "from" for Rel_Back', () => {
84
- const l = line('a', 'd')
85
- expect(headsAt(l.start)).toBe(1)
86
- expect(headsAt(l.end)).toBe(0)
87
- })
88
-
89
- it('puts an arrowhead at both ends for BiRel', () => {
90
- const l = line('b', 'd')
91
- expect(headsAt(l.start)).toBe(1)
92
- expect(headsAt(l.end)).toBe(1)
93
- })
94
-
95
- it('draws one arrowhead per Rel and two for BiRel', () => {
96
- expect(tips(svg)).toHaveLength(1 + 1 + 1 + 2)
97
- })
98
-
99
- it('is the same drawing in every direction', () => {
100
- const strip = (s: string) => s.replace(/zm-title-\d+/g, 'zm-title')
101
- expect(strip(svg)).toBe(strip(svgOf('TB')))
102
- })
103
- })
104
-
105
- describe('C4 ASCII orientations', () => {
106
- const ascii = (dir: Dir): string =>
107
- renderMermaidASCII(SOURCE, { direction: dir })
108
-
109
- it('draws arrowheads on the right ends in TB', () => {
110
- const out = ascii('TB')
111
- // Rel, Rel, and one end each of Rel_Back and BiRel point down; the
112
- // Rel_Back head and the other BiRel head point up.
113
- expect([...out].filter((c) => c === '▼')).toHaveLength(3)
114
- expect([...out].filter((c) => c === '▲')).toHaveLength(2)
115
- })
116
-
117
- it('reverses the rows and the arrowheads in BT', () => {
118
- const tb = ascii('TB')
119
- const bt = ascii('BT')
120
- expect([...bt].filter((c) => c === '▲')).toHaveLength(
121
- [...tb].filter((c) => c === '▼').length,
122
- )
123
- expect([...bt].filter((c) => c === '▼')).toHaveLength(
124
- [...tb].filter((c) => c === '▲').length,
125
- )
126
- expect(bt.indexOf('[Person]')).toBeGreaterThan(
127
- bt.indexOf('[Software System]'),
128
- )
129
- })
130
-
131
- it('flips the contents of a boundary too in BT', () => {
132
- const rows = ascii('BT').split('\n')
133
- const at = (re: RegExp) => rows.findIndex((l) => re.test(l))
134
- // B -> C flows upward, so C sits above B inside the frame.
135
- expect(at(/│ +C +│/)).toBeLessThan(at(/│ +B +│/))
136
- })
137
-
138
- it.each(['TB', 'BT'] as const)(
139
- 'keeps every relationship label in %s, wrapped if it must be',
140
- (dir) => {
141
- const out = ascii(dir)
142
- for (const word of [
143
- 'Uses',
144
- '[HTTPS]',
145
- 'Reads',
146
- 'from',
147
- 'and',
148
- 'writes',
149
- 'to',
150
- 'Sends',
151
- 'Syncs',
152
- ]) {
153
- expect(out, word).toContain(word)
154
- }
155
- },
156
- )
157
-
158
- it('is top-to-bottom only: LR and RL render exactly like TB', () => {
159
- expect(ascii('LR')).toBe(ascii('TB'))
160
- expect(ascii('RL')).toBe(ascii('TB'))
161
- })
162
- })