@zombie-mermaid/ascii-renderer 2.2.6 → 3.1.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/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1245 -1203
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +1 -1
- package/src/__tests__/ascii-er-attribute-column-order-1134.test.ts +69 -0
- package/src/__tests__/ascii-er-corner-glyphs.test.ts +27 -4
- package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +13 -3
- package/src/__tests__/ascii-er-relationship-overwrite.test.ts +107 -0
- package/src/__tests__/ascii-er-stray-connectors.test.ts +15 -9
- package/src/__tests__/ascii-fan-in-then-fan-out-upstream-63.test.ts +78 -0
- package/src/__tests__/ascii-fan-out-upstream-113.test.ts +101 -0
- package/src/__tests__/ascii-sequence-activation.test.ts +171 -0
- package/src/__tests__/ascii-shape-diamond.test.ts +1 -1
- package/src/__tests__/ascii-shape-rounded.test.ts +1 -1
- package/src/__tests__/ascii-shapes-circle.test.ts +1 -1
- package/src/__tests__/ascii-shapes-hexagon.test.ts +1 -1
- package/src/__tests__/ascii-shapes-special.test.ts +35 -0
- package/src/__tests__/ascii-validate.test.ts +1 -42
- package/src/__tests__/ascii.test.ts +1 -1
- package/src/__tests__/testdata/ascii/er_attributes.txt +9 -9
- package/src/__tests__/testdata/ascii/er_identifying.txt +8 -8
- package/src/__tests__/testdata/unicode/er_attributes.txt +9 -9
- package/src/__tests__/testdata/unicode/er_identifying.txt +8 -8
- package/src/{validate.ts → __tests__/validate.ts} +3 -40
- package/src/draw-boxes.ts +14 -0
- package/src/er-diagram.ts +134 -31
- package/src/sequence.ts +87 -1
- package/src/shapes/index.ts +47 -5
- package/src/shapes/circle.ts +0 -31
- package/src/shapes/diamond.ts +0 -31
- package/src/shapes/hexagon.ts +0 -35
- package/src/shapes/rounded.ts +0 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zombie-mermaid/ascii-renderer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "ASCII and Unicode box-drawing renderer for zombie-mermaid diagrams.",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"LICENSE"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@zombie-mermaid/
|
|
39
|
-
"@zombie-mermaid/
|
|
38
|
+
"@zombie-mermaid/mermaid-parser": "3.1.0",
|
|
39
|
+
"@zombie-mermaid/core": "3.1.0"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { describe, it, expect } from 'vitest'
|
|
16
16
|
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
17
|
-
import { findOrphanedJunctions } from '
|
|
17
|
+
import { findOrphanedJunctions } from './validate.ts'
|
|
18
18
|
import { readdirSync, readFileSync } from 'node:fs'
|
|
19
19
|
import { join, dirname } from 'node:path'
|
|
20
20
|
import { fileURLToPath } from 'node:url'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII ER diagram: attribute columns render as type/name/keys (issue #1134)
|
|
3
|
+
//
|
|
4
|
+
// formatAttribute (src/ascii/er-diagram.ts) used to build "PK type name" —
|
|
5
|
+
// keys, then type, then name — but real mermaid's SVG lays attribute
|
|
6
|
+
// columns out left-to-right as type/name/keys (confirmed against the SVG's
|
|
7
|
+
// attribute-type/attribute-name/attribute-keys x-coordinates in the
|
|
8
|
+
// form-judge report on issue #1119). This file pins the corrected order.
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
13
|
+
|
|
14
|
+
describe('ASCII ER attribute columns render as type/name/keys (issue #1134)', () => {
|
|
15
|
+
it('renders a single-key attribute as "type name KEY", not "KEY type name"', () => {
|
|
16
|
+
const ascii = renderMermaidASCII(
|
|
17
|
+
`erDiagram
|
|
18
|
+
ORDER {
|
|
19
|
+
int id PK
|
|
20
|
+
}`,
|
|
21
|
+
{ colorMode: 'none' },
|
|
22
|
+
)
|
|
23
|
+
expect(ascii).toContain('int id PK')
|
|
24
|
+
expect(ascii).not.toContain('PK int id')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders a multi-key attribute\'s keys joined and trailing, e.g. "type name PK,FK"', () => {
|
|
28
|
+
const ascii = renderMermaidASCII(
|
|
29
|
+
`erDiagram
|
|
30
|
+
LINE_ITEM {
|
|
31
|
+
int product_id PK FK
|
|
32
|
+
}`,
|
|
33
|
+
{ colorMode: 'none' },
|
|
34
|
+
)
|
|
35
|
+
expect(ascii).toContain('int product_id PK,FK')
|
|
36
|
+
expect(ascii).not.toContain('PK,FK int product_id')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders a keyless attribute with no reserved key-column padding', () => {
|
|
40
|
+
const ascii = renderMermaidASCII(
|
|
41
|
+
`erDiagram
|
|
42
|
+
CUSTOMER {
|
|
43
|
+
string name
|
|
44
|
+
}`,
|
|
45
|
+
{ colorMode: 'none' },
|
|
46
|
+
)
|
|
47
|
+
// The pre-#1134 format always reserved a fixed-width 3-space key
|
|
48
|
+
// prefix (' ') even when an attribute had no key at all — a keyless
|
|
49
|
+
// attribute now renders with nothing but its own type/name text, so
|
|
50
|
+
// the entity's box no longer wastes 3 columns it never needed.
|
|
51
|
+
expect(ascii).toContain('string name')
|
|
52
|
+
expect(ascii).not.toContain(' string name')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('keeps a mix of keyed and keyless attributes readable in the same entity', () => {
|
|
56
|
+
const ascii = renderMermaidASCII(
|
|
57
|
+
`erDiagram
|
|
58
|
+
CUSTOMER {
|
|
59
|
+
string name PK
|
|
60
|
+
string email UK
|
|
61
|
+
int age
|
|
62
|
+
}`,
|
|
63
|
+
{ colorMode: 'none' },
|
|
64
|
+
)
|
|
65
|
+
expect(ascii).toContain('string name PK')
|
|
66
|
+
expect(ascii).toContain('string email UK')
|
|
67
|
+
expect(ascii).toContain('int age')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -57,17 +57,29 @@ describe('ASCII ER relationship routing draws a corner glyph at each turn (issue
|
|
|
57
57
|
// the detour line, rather than inset into the detour's own row the
|
|
58
58
|
// way the old inset positioning did. Checked separately below instead
|
|
59
59
|
// of assuming they share a row.
|
|
60
|
-
|
|
60
|
+
//
|
|
61
|
+
// The detour line used to have "tracks" (MIDDLE_ENTITY--FIFTH_ENTITY's
|
|
62
|
+
// own label, a *different*, vertical relationship) embedded directly in
|
|
63
|
+
// its dashes, with no separator — the same label-lands-on-a-foreign-line
|
|
64
|
+
// defect issue #1119 fixes. "tracks" now relocates to its own row next
|
|
65
|
+
// to its own marker instead.
|
|
66
|
+
expect(ascii).toContain('└─────────────────│─────────────────┘')
|
|
61
67
|
const lines = ascii.split('\n')
|
|
62
68
|
const detourLineIndex = lines.findIndex((l) =>
|
|
63
|
-
l.includes('
|
|
69
|
+
l.includes('└─────────────────│─────────────────┘'),
|
|
64
70
|
)
|
|
65
71
|
expect(detourLineIndex).toBeGreaterThan(0)
|
|
66
72
|
// MIDDLE_ENTITY's "tracks" marker ('┼', flush against its own border)
|
|
67
73
|
// sits on the row immediately above the detour line.
|
|
68
74
|
expect(lines[detourLineIndex - 1]).toMatch(/┼/)
|
|
75
|
+
// "relates_to" (this detour's own label) sits on the row right below it.
|
|
76
|
+
expect(lines[detourLineIndex + 1]).toContain('relates_to')
|
|
69
77
|
expect(ascii).toContain('relates_to')
|
|
70
78
|
expect(ascii).toContain('tracks')
|
|
79
|
+
// "tracks" renders next to its own bottom marker ('○╟'), not spliced
|
|
80
|
+
// onto the unrelated detour line above.
|
|
81
|
+
const tracksLine = lines.find((l) => l.includes('tracks'))
|
|
82
|
+
expect(tracksLine).toContain('○╟')
|
|
71
83
|
expect(ascii).not.toMatch(/[a-z]+[─│┊╌][a-z]+/)
|
|
72
84
|
})
|
|
73
85
|
|
|
@@ -155,8 +167,19 @@ describe('ASCII ER relationship routing draws a corner glyph at each turn (issue
|
|
|
155
167
|
// from a single shared-center bypass to two side-by-side ones; the
|
|
156
168
|
// corner glyphs at the bypass's own turns are still there, just at the
|
|
157
169
|
// updated column.
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
//
|
|
171
|
+
// These two exact substrings shifted again with issue #1134's ER
|
|
172
|
+
// attribute-column reorder (type/name/keys instead of keys/type/name):
|
|
173
|
+
// A/D/G's boxes carry no keyed attributes here, so the old format's
|
|
174
|
+
// fixed-width key prefix (' ' when absent) padded every attribute
|
|
175
|
+
// line by 3 unused columns; the reordered format only emits a key
|
|
176
|
+
// suffix when a key actually exists, so these now-narrower boxes shift
|
|
177
|
+
// the bypass's free-column search enough to change both jog rows'
|
|
178
|
+
// exact shape — each jog's two corners now land on the very same row
|
|
179
|
+
// (self-contained '┌'...'┘' runs) rather than sharing a row with a
|
|
180
|
+
// marker glyph the way the pre-#1134 geometry did.
|
|
181
|
+
expect(ascii).toContain('┌─────────────────────────┘')
|
|
182
|
+
expect(ascii).toContain('┌────────────────────┘')
|
|
160
183
|
expect(ascii).toContain('ag')
|
|
161
184
|
})
|
|
162
185
|
|
|
@@ -451,8 +451,18 @@ describe('ASCII ER relationship routing — multi-row obstruction (CodeRabbit fo
|
|
|
451
451
|
(l, i) => l.includes('┌') && lines[i + 1]?.includes('│ D'),
|
|
452
452
|
)
|
|
453
453
|
expect(dTopIdx, 'expected to find D’s top-border row').toBeGreaterThan(-1)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
454
|
+
// Issue #1134 reordered ER attribute columns to type/name/keys, dropping
|
|
455
|
+
// the old format's fixed-width key prefix (' ' when an attribute had
|
|
456
|
+
// no key) that every one of A/D/G's keyless attribute lines used to pay
|
|
457
|
+
// for unconditionally. That narrows all three boxes here enough that the
|
|
458
|
+
// "ag" bypass no longer needs to jog laterally past D on D's own
|
|
459
|
+
// top-border row — it now clears D vertically, with its downward-turn
|
|
460
|
+
// marker landing in the gap row directly above D's box instead. That gap
|
|
461
|
+
// row is still unambiguously outside D's box (which starts at dTopIdx),
|
|
462
|
+
// so a connector/marker character there is the same proof as before:
|
|
463
|
+
// the line was actually routed around D, not silently guarded away
|
|
464
|
+
// inside it (setCGuarded would leave no trace at all in that case).
|
|
465
|
+
const gapLine = lines[dTopIdx - 1]!
|
|
466
|
+
expect(gapLine).toMatch(/[│╢╟○┌└]/)
|
|
457
467
|
})
|
|
458
468
|
})
|
|
@@ -322,4 +322,111 @@ describe('ASCII ER relationship draws do not overwrite existing text (issue #392
|
|
|
322
322
|
expect(row![idx - 1]).toBe(' ')
|
|
323
323
|
expect(row![idx + 'authors'.length]).toBe(' ')
|
|
324
324
|
})
|
|
325
|
+
|
|
326
|
+
it('renders the "includes" line solid at its crossing with "ships-via", not dashed (issue #1145)', () => {
|
|
327
|
+
// Real catalog sample ("ER: Mixed Identifying & Non-Identifying").
|
|
328
|
+
// PRODUCT--LINE_ITEM ("includes", solid — identifying) and
|
|
329
|
+
// ORDER--SHIPMENT ("ships-via", dashed — non-identifying, detoured
|
|
330
|
+
// beneath LINE_ITEM which sits between ORDER and SHIPMENT in the same
|
|
331
|
+
// row) route through the same cell: ships-via's horizontal detour fill
|
|
332
|
+
// crosses directly through includes' vertical stem just below
|
|
333
|
+
// LINE_ITEM. setCGuarded now refuses to let a dashed write land on an
|
|
334
|
+
// already-drawn solid glyph there, so "includes" renders solid at the
|
|
335
|
+
// crossing rather than picking up ships-via's dash.
|
|
336
|
+
const ascii = renderMermaidASCII(
|
|
337
|
+
`erDiagram
|
|
338
|
+
ORDER ||--|{ LINE_ITEM : contains
|
|
339
|
+
ORDER ||..o{ SHIPMENT : ships-via
|
|
340
|
+
PRODUCT ||--o{ LINE_ITEM : includes
|
|
341
|
+
PRODUCT ||..o{ REVIEW : receives`,
|
|
342
|
+
{ colorMode: 'none' },
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
const lines = ascii.split('\n')
|
|
346
|
+
// ships-via's detour fill sits one row below LINE_ITEM's own
|
|
347
|
+
// zero-many marker ('╢○', for "includes") — anchoring on that marker
|
|
348
|
+
// (unambiguous: it's only ever drawn once, at LINE_ITEM's own bottom
|
|
349
|
+
// edge) rather than searching for a '└...┘' corner pair directly,
|
|
350
|
+
// since every entity box's own border also has solid corners and a
|
|
351
|
+
// naive substring search can match those instead (they're a different
|
|
352
|
+
// row, but a row can contain more than one corner pair).
|
|
353
|
+
const markerRowIdx = lines.findIndex((l) => l.includes('╢○'))
|
|
354
|
+
expect(markerRowIdx).toBeGreaterThanOrEqual(0)
|
|
355
|
+
const detourRow = lines[markerRowIdx + 1]!
|
|
356
|
+
const detourStart = detourRow.indexOf('└╌')
|
|
357
|
+
const detourEnd = detourRow.indexOf('╌┘') + 1
|
|
358
|
+
expect(detourStart).toBeGreaterThanOrEqual(0)
|
|
359
|
+
|
|
360
|
+
// includes' own vertical stem (solid '│') crosses this same row exactly
|
|
361
|
+
// once, between the detour's two corners — that crossing cell must
|
|
362
|
+
// read as solid, not dashed.
|
|
363
|
+
const crossingIdx = detourRow.indexOf('│', detourStart)
|
|
364
|
+
expect(crossingIdx).toBeGreaterThan(detourStart)
|
|
365
|
+
expect(crossingIdx).toBeLessThan(detourEnd)
|
|
366
|
+
expect(detourRow[crossingIdx]).toBe('│')
|
|
367
|
+
|
|
368
|
+
// And the fill on either side of that crossing is still ships-via's own
|
|
369
|
+
// dashed glyph — the fix only protects the one style-conflicting cell,
|
|
370
|
+
// it doesn't let the solid write bleed into its neighbors.
|
|
371
|
+
expect(detourRow[crossingIdx - 1]).toBe('╌')
|
|
372
|
+
expect(detourRow[crossingIdx + 1]).toBe('╌')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('never lets a later-drawn dashed relationship line overwrite an earlier solid one at their crossing (issue #1145)', () => {
|
|
376
|
+
// Same geometry as the sample above (same entities, same connections,
|
|
377
|
+
// same declaration/draw order — contains, ships-via, includes,
|
|
378
|
+
// receives), but with which relationship is solid vs. dashed flipped:
|
|
379
|
+
// "ships-via" is now identifying (solid, "--") and "includes" is now
|
|
380
|
+
// non-identifying (dashed, ".."). `rel.identifying` only selects which
|
|
381
|
+
// glyph set a relationship draws with — it doesn't affect any routing
|
|
382
|
+
// decision (obstruction detection, jog-row search, detour placement) —
|
|
383
|
+
// so this renders with the exact same cell layout as the original
|
|
384
|
+
// sample, just with the two relationships' styles swapped. That makes
|
|
385
|
+
// it a clean, order-preserving way to flip *which* style the
|
|
386
|
+
// later-drawn relationship carries, without perturbing the routing
|
|
387
|
+
// itself the way reordering the relationships outright would.
|
|
388
|
+
//
|
|
389
|
+
// ships-via (now solid) is still drawn before includes (now dashed) —
|
|
390
|
+
// same relationship order as above — so before the fix, includes'
|
|
391
|
+
// later dashed write unconditionally overwrote ships-via's
|
|
392
|
+
// already-drawn solid detour fill at their crossing cell, opening a
|
|
393
|
+
// single-character dashed gap in an otherwise solid line: exactly the
|
|
394
|
+
// "solid stem ends up showing a dashed glyph, purely from draw order"
|
|
395
|
+
// defect the issue describes. The fix refuses that overwrite, so the
|
|
396
|
+
// detour fill must now be solid all the way across, with no dashed
|
|
397
|
+
// character breaking it.
|
|
398
|
+
const ascii = renderMermaidASCII(
|
|
399
|
+
`erDiagram
|
|
400
|
+
ORDER ||--|{ LINE_ITEM : contains
|
|
401
|
+
ORDER ||--o{ SHIPMENT : ships-via
|
|
402
|
+
PRODUCT ||..o{ LINE_ITEM : includes
|
|
403
|
+
PRODUCT ||..o{ REVIEW : receives`,
|
|
404
|
+
{ colorMode: 'none' },
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
const lines = ascii.split('\n')
|
|
408
|
+
// Same anchor as the test above: ships-via's detour fill sits one row
|
|
409
|
+
// below LINE_ITEM's zero-many marker. Anchoring there (rather than
|
|
410
|
+
// searching for a '└...┘' pair directly) avoids a false match against
|
|
411
|
+
// an entity box's own solid border elsewhere in the render — with the
|
|
412
|
+
// fill itself now solid too (styles swapped for this test), a naive
|
|
413
|
+
// substring search matches ORDER's own box border instead of the
|
|
414
|
+
// detour row.
|
|
415
|
+
const markerRowIdx = lines.findIndex((l) => l.includes('╢○'))
|
|
416
|
+
expect(markerRowIdx).toBeGreaterThanOrEqual(0)
|
|
417
|
+
const detourRow = lines[markerRowIdx + 1]!
|
|
418
|
+
const detourStart = detourRow.indexOf('└─') // index of the '└' corner
|
|
419
|
+
const detourEnd = detourRow.indexOf('─┘') + 1 // index of the '┘' corner
|
|
420
|
+
expect(detourStart).toBeGreaterThanOrEqual(0)
|
|
421
|
+
// Everything strictly between the two corners.
|
|
422
|
+
const fill = detourRow.slice(detourStart + 1, detourEnd)
|
|
423
|
+
|
|
424
|
+
// No dashed glyph ('┊', includes' own now-dashed vertical stem's
|
|
425
|
+
// style) may appear anywhere inside the solid detour fill.
|
|
426
|
+
expect(fill).not.toContain('┊')
|
|
427
|
+
// And the fill is unbroken solid line across its whole span (not, say,
|
|
428
|
+
// silently dropped to a blank gap instead of dashed — the crossing
|
|
429
|
+
// cell must still carry a line glyph, just the solid one).
|
|
430
|
+
expect(fill).toBe('─'.repeat(fill.length))
|
|
431
|
+
})
|
|
325
432
|
})
|
|
@@ -200,10 +200,15 @@ describe('ASCII ER — relationships routed without colliding (issue #351)', ()
|
|
|
200
200
|
// occupancy before choosing a jog row. Found by a differential search
|
|
201
201
|
// over random small diagrams comparing the old (narrow) and fixed
|
|
202
202
|
// (label-span-widened) occupancy ranges; this is the minimal case that
|
|
203
|
-
// reproduces it deterministically — DDDD--CCC's jog
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
203
|
+
// reproduces it deterministically — DDDD--CCC's jog used to land on the
|
|
204
|
+
// exact row BB--A's own label ("l1") already used, splicing the two
|
|
205
|
+
// together into one unbroken run ("l1longerlabelhere2") with no
|
|
206
|
+
// separator at all.
|
|
207
|
+
//
|
|
208
|
+
// Since #1119's fix (a label search also avoids a *different*
|
|
209
|
+
// relationship's own line, not just its own), DDDD--CCC's label
|
|
210
|
+
// relocates to its own row instead of sharing BB--A's — so the two
|
|
211
|
+
// no longer read as a single spliced word.
|
|
207
212
|
const ascii = renderMermaidASCII(
|
|
208
213
|
`erDiagram
|
|
209
214
|
DDDD ||--o{ A : x0
|
|
@@ -211,14 +216,15 @@ describe('ASCII ER — relationships routed without colliding (issue #351)', ()
|
|
|
211
216
|
DDDD ||--o{ CCC : longerlabelhere2`,
|
|
212
217
|
{ colorMode: 'none' },
|
|
213
218
|
)
|
|
219
|
+
expect(ascii).toContain('longerlabelhere2')
|
|
220
|
+
expect(ascii).toContain('l1')
|
|
221
|
+
expect(ascii).not.toContain('l1longerlabelhere2')
|
|
214
222
|
const lines = ascii.split('\n')
|
|
215
223
|
const labelLine = lines.find((l) => l.includes('longerlabelhere2'))
|
|
216
224
|
expect(labelLine).toBeDefined()
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
// vertical stem sharing this row.
|
|
221
|
-
expect(labelLine!.slice(Math.max(0, idx - 3), idx)).not.toContain('│')
|
|
225
|
+
// The label itself must render as one intact, uninterrupted word — not
|
|
226
|
+
// split by a stray line character routed through its middle.
|
|
227
|
+
expect(labelLine).not.toMatch(/longer[─│┊╌]/)
|
|
222
228
|
})
|
|
223
229
|
})
|
|
224
230
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for upstream lukilabs/beautiful-mermaid#63: an LR fan-in
|
|
3
|
+
* followed by a fan-out (`A --> C`, `B --> C`, `C --> D`, `C --> E`).
|
|
4
|
+
*
|
|
5
|
+
* Upstream's bug: C->E was routed along B->C's corridor, so B's row rendered a
|
|
6
|
+
* misleading `├───┴──►E`, as if B pointed at E. The fixed shape has C->E on its
|
|
7
|
+
* own trunk (`┬` off C's right side, then `└────┐` down to E) while B->C ends
|
|
8
|
+
* at `┘` under C.
|
|
9
|
+
*
|
|
10
|
+
* This fork fixes it through findUnrelatedOverlap / createEdgeCellOwners
|
|
11
|
+
* (edge-cell-styles.ts) via rerouteAroundStyleConflicts (grid.ts), not by
|
|
12
|
+
* porting upstream's routing change; these tests pin the rendered outcome, not
|
|
13
|
+
* the mechanism.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from 'vitest'
|
|
16
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
17
|
+
|
|
18
|
+
const lines = (s: string): string[] => s.split('\n').map((l) => l.trimEnd())
|
|
19
|
+
|
|
20
|
+
const SOURCE = `graph LR
|
|
21
|
+
A --> C
|
|
22
|
+
B --> C
|
|
23
|
+
C --> D
|
|
24
|
+
C --> E`
|
|
25
|
+
|
|
26
|
+
describe('upstream #63: LR fan-in then fan-out keeps C->E off B->C corridor', () => {
|
|
27
|
+
it('renders C->E on its own trunk (unicode)', () => {
|
|
28
|
+
expect(lines(renderMermaidASCII(SOURCE, { colorMode: 'none' }))).toEqual([
|
|
29
|
+
'┌───┐ ┌───┐ ┌───┐',
|
|
30
|
+
'│ │ │ │ │ │',
|
|
31
|
+
'│ A ├────►│ C ├──┬─►│ D │',
|
|
32
|
+
'│ │ │ │ │ │ │',
|
|
33
|
+
'└───┘ └───┘ │ └───┘',
|
|
34
|
+
' ▲ │',
|
|
35
|
+
' │ │',
|
|
36
|
+
' │ └────┐',
|
|
37
|
+
' │ │',
|
|
38
|
+
' │ ▼',
|
|
39
|
+
'┌───┐ │ ┌───┐',
|
|
40
|
+
'│ │ │ │ │',
|
|
41
|
+
'│ B ├───────┘ │ E │',
|
|
42
|
+
'│ │ │ │',
|
|
43
|
+
'└───┘ └───┘',
|
|
44
|
+
])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('B row ends at the corner under C and never reaches E (unicode)', () => {
|
|
48
|
+
const out = lines(renderMermaidASCII(SOURCE, { colorMode: 'none' }))
|
|
49
|
+
const bRow = out.find((l) => l.includes('│ B ├'))!
|
|
50
|
+
// B's edge terminates at exactly one `┘`; nothing horizontal after it.
|
|
51
|
+
expect(bRow.match(/┘/g)).toHaveLength(1)
|
|
52
|
+
expect(bRow.slice(bRow.indexOf('┘') + 1)).not.toMatch(/[─├┴┬►]/)
|
|
53
|
+
// The misleading tee joining B's corridor to E must not appear anywhere.
|
|
54
|
+
expect(out.join('\n')).not.toContain('┴')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders the same shape with the ASCII charset', () => {
|
|
58
|
+
expect(
|
|
59
|
+
lines(renderMermaidASCII(SOURCE, { colorMode: 'none', useAscii: true })),
|
|
60
|
+
).toEqual([
|
|
61
|
+
'+---+ +---+ +---+',
|
|
62
|
+
'| | | | | |',
|
|
63
|
+
'| A +---->| C +--+->| D |',
|
|
64
|
+
'| | | | | | |',
|
|
65
|
+
'+---+ +---+ | +---+',
|
|
66
|
+
' ^ |',
|
|
67
|
+
' | |',
|
|
68
|
+
' | +----+',
|
|
69
|
+
' | |',
|
|
70
|
+
' | v',
|
|
71
|
+
'+---+ | +---+',
|
|
72
|
+
'| | | | |',
|
|
73
|
+
'| B +-------+ | E |',
|
|
74
|
+
'| | | |',
|
|
75
|
+
'+---+ +---+',
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for upstream lukilabs/beautiful-mermaid#113 (fixing
|
|
3
|
+
* upstream issues #111 and #112): ASCII fan-out routing.
|
|
4
|
+
*
|
|
5
|
+
* - #111: sibling edges from one source (TB, labelled) must share a single
|
|
6
|
+
* horizontal trunk leaving the source's right border, each label sitting on
|
|
7
|
+
* its own vertical branch, instead of later edges detouring to a lower row.
|
|
8
|
+
* - #112: in an LR fan-out, a sibling edge's label widens the source's border
|
|
9
|
+
* column; the first edge's box-start connector must stay flush on the
|
|
10
|
+
* source box's real border rather than drifting right with the grid cell.
|
|
11
|
+
*
|
|
12
|
+
* This fork solves these through edge-bundling / draw-bundles / draw-arrows
|
|
13
|
+
* rather than porting upstream's pathfinder changes; these tests pin the
|
|
14
|
+
* rendered outcome, not the mechanism.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from 'vitest'
|
|
17
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
18
|
+
|
|
19
|
+
const lines = (s: string): string[] => s.split('\n').map((l) => l.trimEnd())
|
|
20
|
+
|
|
21
|
+
const TD_FANOUT = `flowchart TB
|
|
22
|
+
Src["Source"]
|
|
23
|
+
Left["Left Target"]
|
|
24
|
+
Center["Center Target"]
|
|
25
|
+
Right["Right Target"]
|
|
26
|
+
Src -->|left*| Left
|
|
27
|
+
Src -->|center*| Center
|
|
28
|
+
Src -->|right*| Right`
|
|
29
|
+
|
|
30
|
+
const LR_FANOUT = `flowchart LR
|
|
31
|
+
Src["Source"]
|
|
32
|
+
Top["Top Target"]
|
|
33
|
+
Mid["Middle Target"]
|
|
34
|
+
Bot["Bottom Target"]
|
|
35
|
+
Src -->|top*| Top
|
|
36
|
+
Src -->|mid*| Mid
|
|
37
|
+
Src -->|bot*| Bot`
|
|
38
|
+
|
|
39
|
+
describe('upstream #111: TB fan-out siblings share one trunk', () => {
|
|
40
|
+
it('renders the shared trunk with labels on their own vertical branches (unicode)', () => {
|
|
41
|
+
const out = renderMermaidASCII(TD_FANOUT, { colorMode: 'none' })
|
|
42
|
+
expect(lines(out)).toEqual([
|
|
43
|
+
'┌─────────────┐',
|
|
44
|
+
'│ │',
|
|
45
|
+
'│ Source ├─────────────┬─────────────────────┐',
|
|
46
|
+
'│ │ │ │',
|
|
47
|
+
'└──────┬──────┘ center* right*',
|
|
48
|
+
' │ │ │',
|
|
49
|
+
' left* │ │',
|
|
50
|
+
' │ │ │',
|
|
51
|
+
' │ │ │',
|
|
52
|
+
' ▼ ▼ ▼',
|
|
53
|
+
'┌─────────────┐ ┌───────────────┐ ┌──────────────┐',
|
|
54
|
+
'│ │ │ │ │ │',
|
|
55
|
+
'│ Left Target │ │ Center Target │ │ Right Target │',
|
|
56
|
+
'│ │ │ │ │ │',
|
|
57
|
+
'└─────────────┘ └───────────────┘ └──────────────┘',
|
|
58
|
+
])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('has exactly one horizontal trunk row off the source border (ASCII)', () => {
|
|
62
|
+
const out = lines(
|
|
63
|
+
renderMermaidASCII(TD_FANOUT, { colorMode: 'none', useAscii: true }),
|
|
64
|
+
)
|
|
65
|
+
// Box border rows start with '+'; only the source-border row carries a long horizontal run; the buggy
|
|
66
|
+
// upstream output put a second run (the "right*" detour) on a lower row.
|
|
67
|
+
const rowsWithRun = out.filter((l) => !l.startsWith('+') && /-{5,}/.test(l))
|
|
68
|
+
expect(rowsWithRun).toHaveLength(1)
|
|
69
|
+
expect(rowsWithRun[0]).toMatch(/^\|\s+Source\s+\+-+\+-+\+$/)
|
|
70
|
+
// No label is stranded on a horizontal segment.
|
|
71
|
+
expect(out.join('\n')).not.toMatch(/[-─]\/?(center|right)\*[-─]/)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('upstream #112: LR fan-out box-start connector stays on the border', () => {
|
|
76
|
+
it('places ├ directly under the source box right border column (unicode)', () => {
|
|
77
|
+
const out = lines(renderMermaidASCII(LR_FANOUT, { colorMode: 'none' }))
|
|
78
|
+
const sourceRow = out.find((l) => l.includes('Source'))!
|
|
79
|
+
const borderCol = sourceRow.indexOf('├')
|
|
80
|
+
expect(borderCol).toBe(out[0]!.indexOf('┐'))
|
|
81
|
+
// Every row of the source box (top border down to bottom border) agrees
|
|
82
|
+
// on the border column.
|
|
83
|
+
expect(out[0]![borderCol]).toBe('┐')
|
|
84
|
+
expect(out[4]![borderCol]).toBe('┘')
|
|
85
|
+
// The connector is the border glyph itself, not preceded by a gap.
|
|
86
|
+
expect(sourceRow[borderCol - 1]).toBe(' ')
|
|
87
|
+
expect(sourceRow.slice(0, borderCol)).toBe('│ Source ')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('renders the full fan-out with ├ flush on the border (unicode)', () => {
|
|
91
|
+
const out = renderMermaidASCII(LR_FANOUT, { colorMode: 'none' })
|
|
92
|
+
expect(lines(out).slice(0, 6)).toEqual([
|
|
93
|
+
'┌────────┐ ┌───────────────┐',
|
|
94
|
+
'│ │ │ │',
|
|
95
|
+
'│ Source ├top*─►│ Top Target │',
|
|
96
|
+
'│ │ │ │',
|
|
97
|
+
'└────┬───┘ └───────────────┘',
|
|
98
|
+
' │',
|
|
99
|
+
])
|
|
100
|
+
})
|
|
101
|
+
})
|