@zombie-mermaid/ascii-renderer 2.2.6 → 3.0.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-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.0.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/core": "
|
|
39
|
-
"@zombie-mermaid/mermaid-parser": "
|
|
38
|
+
"@zombie-mermaid/core": "3.0.0",
|
|
39
|
+
"@zombie-mermaid/mermaid-parser": "3.0.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,171 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII sequence diagrams — activation bars on lifelines (issue #1133)
|
|
3
|
+
//
|
|
4
|
+
// Mermaid overlays a highlighted rectangle on a lifeline while a participant
|
|
5
|
+
// is actively processing a call, driven by standalone `activate`/`deactivate`
|
|
6
|
+
// statements or the `+`/`-` arrow shorthand (`A->>+B` / `A-->>-B`). The
|
|
7
|
+
// parser already models both forms (`SequenceDiagram.activations` and
|
|
8
|
+
// `Message.activate`/`Message.deactivate` — see mermaid-parser's
|
|
9
|
+
// sequence/types.ts) and the SVG renderer already draws the bars
|
|
10
|
+
// (svg-renderer's sequence/layout.ts), but the ASCII renderer drew every
|
|
11
|
+
// lifeline as a uniform `│` regardless of activation state.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
16
|
+
|
|
17
|
+
describe('ASCII sequence diagrams – activation bars (issue #1133)', () => {
|
|
18
|
+
it('draws a double-line glyph on the recipient lifeline for +/- shorthand activation', () => {
|
|
19
|
+
const result = renderMermaidASCII(
|
|
20
|
+
`
|
|
21
|
+
sequenceDiagram
|
|
22
|
+
participant Client
|
|
23
|
+
participant Server
|
|
24
|
+
Client->>+Server: request
|
|
25
|
+
Server-->>-Client: response`,
|
|
26
|
+
{ useAscii: false },
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
expect(result).toBe(
|
|
30
|
+
[
|
|
31
|
+
'┌────────┐ ┌────────┐ ',
|
|
32
|
+
'│ Client │ │ Server │ ',
|
|
33
|
+
'└────┬───┘ └────┬───┘ ',
|
|
34
|
+
' │ │ ',
|
|
35
|
+
' │ request │ ',
|
|
36
|
+
' │─────────────▶ ',
|
|
37
|
+
' │ ║ ',
|
|
38
|
+
' │ response ║ ',
|
|
39
|
+
' ◀╌╌╌╌╌╌╌╌╌╌╌╌╌║ ',
|
|
40
|
+
' │ │ ',
|
|
41
|
+
'┌────┴───┐ ┌────┴───┐ ',
|
|
42
|
+
'│ Client │ │ Server │ ',
|
|
43
|
+
'└────────┘ └────────┘ ',
|
|
44
|
+
].join('\n'),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('uses the ASCII-fallback glyph (‖) in useAscii mode', () => {
|
|
49
|
+
const result = renderMermaidASCII(
|
|
50
|
+
`
|
|
51
|
+
sequenceDiagram
|
|
52
|
+
participant Client
|
|
53
|
+
participant Server
|
|
54
|
+
Client->>+Server: request
|
|
55
|
+
Server-->>-Client: response`,
|
|
56
|
+
{ useAscii: true },
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const lines = result.split('\n')
|
|
60
|
+
expect(lines.some((l) => l.includes('‖'))).toBe(true)
|
|
61
|
+
expect(result).not.toContain('║')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('draws the same bar for standalone activate/deactivate statements', () => {
|
|
65
|
+
const result = renderMermaidASCII(
|
|
66
|
+
`
|
|
67
|
+
sequenceDiagram
|
|
68
|
+
participant A
|
|
69
|
+
participant B
|
|
70
|
+
A->>B: hello
|
|
71
|
+
activate B
|
|
72
|
+
B->>A: world
|
|
73
|
+
deactivate B`,
|
|
74
|
+
{ useAscii: false },
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
expect(result).toBe(
|
|
78
|
+
[
|
|
79
|
+
' ┌───┐ ┌───┐ ',
|
|
80
|
+
' │ A │ │ B │ ',
|
|
81
|
+
' └─┬─┘ └─┬─┘ ',
|
|
82
|
+
' │ │ ',
|
|
83
|
+
' │ hello │ ',
|
|
84
|
+
' │──────────▶ ',
|
|
85
|
+
' │ ║ ',
|
|
86
|
+
' │ world ║ ',
|
|
87
|
+
' ◀──────────║ ',
|
|
88
|
+
' │ │ ',
|
|
89
|
+
' ┌─┴─┐ ┌─┴─┐ ',
|
|
90
|
+
' │ A │ │ B │ ',
|
|
91
|
+
' └───┘ └───┘ ',
|
|
92
|
+
].join('\n'),
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('keeps a nested self-activation active across the outer activation without a distinct nesting glyph', () => {
|
|
97
|
+
// B activates itself while already active (A->>+B, then B->>+B on the
|
|
98
|
+
// same actor): the lifeline stays marked active continuously from the
|
|
99
|
+
// outer activate through the outer deactivate, including the nested
|
|
100
|
+
// span — this renderer intentionally doesn't offset nested activations
|
|
101
|
+
// sideways the way the SVG renderer does (no spare column per lifeline
|
|
102
|
+
// without reflowing spacing), so nesting depth isn't visually distinct.
|
|
103
|
+
const result = renderMermaidASCII(
|
|
104
|
+
`
|
|
105
|
+
sequenceDiagram
|
|
106
|
+
participant A
|
|
107
|
+
participant B
|
|
108
|
+
A->>+B: outer
|
|
109
|
+
B->>+B: inner
|
|
110
|
+
B-->>-B: inner done
|
|
111
|
+
B-->>-A: outer done`,
|
|
112
|
+
{ useAscii: false },
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
expect(result).toBe(
|
|
116
|
+
[
|
|
117
|
+
' ┌───┐ ┌───┐ ',
|
|
118
|
+
' │ A │ │ B │ ',
|
|
119
|
+
' └─┬─┘ └─┬─┘ ',
|
|
120
|
+
' │ │ ',
|
|
121
|
+
' │ outer │ ',
|
|
122
|
+
' │───────────────▶ ',
|
|
123
|
+
' │ ║ ',
|
|
124
|
+
' │ ├───┐ ',
|
|
125
|
+
' │ ║ │ inner ',
|
|
126
|
+
' │ ◀───┘ ',
|
|
127
|
+
' │ ║ ',
|
|
128
|
+
' │ ├╌╌╌┐ ',
|
|
129
|
+
' │ ║ │ inner done ',
|
|
130
|
+
' │ ◀╌╌╌┘ ',
|
|
131
|
+
' │ ║ ',
|
|
132
|
+
' │ outer done ║ ',
|
|
133
|
+
' ◀╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌║ ',
|
|
134
|
+
' │ │ ',
|
|
135
|
+
' ┌─┴─┐ ┌─┴─┐ ',
|
|
136
|
+
' │ A │ │ B │ ',
|
|
137
|
+
' └───┘ └───┘ ',
|
|
138
|
+
].join('\n'),
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('keeps an unbalanced activate (no matching deactivate) active through to the lifeline end', () => {
|
|
143
|
+
const result = renderMermaidASCII(
|
|
144
|
+
`
|
|
145
|
+
sequenceDiagram
|
|
146
|
+
participant A
|
|
147
|
+
participant B
|
|
148
|
+
A->>+B: call
|
|
149
|
+
B->>A: reply`,
|
|
150
|
+
{ useAscii: false },
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
expect(result).toBe(
|
|
154
|
+
[
|
|
155
|
+
' ┌───┐ ┌───┐ ',
|
|
156
|
+
' │ A │ │ B │ ',
|
|
157
|
+
' └─┬─┘ └─┬─┘ ',
|
|
158
|
+
' │ │ ',
|
|
159
|
+
' │ call │ ',
|
|
160
|
+
' │──────────▶ ',
|
|
161
|
+
' │ ║ ',
|
|
162
|
+
' │ reply ║ ',
|
|
163
|
+
' ◀──────────║ ',
|
|
164
|
+
' │ ║ ',
|
|
165
|
+
' ┌─┴─┐ ┌─┴─┐ ',
|
|
166
|
+
' │ A │ │ B │ ',
|
|
167
|
+
' └───┘ └───┘ ',
|
|
168
|
+
].join('\n'),
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { diamondRenderer } from '../shapes/
|
|
2
|
+
import { diamondRenderer } from '../shapes/index.ts'
|
|
3
3
|
import { canvasToString } from '../canvas.ts'
|
|
4
4
|
import type { ShapeRenderOptions } from '../shapes/types.ts'
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { roundedRenderer } from '../shapes/
|
|
2
|
+
import { roundedRenderer } from '../shapes/index.ts'
|
|
3
3
|
import { canvasToString } from '../canvas.ts'
|
|
4
4
|
import type { ShapeRenderOptions } from '../shapes/types.ts'
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { hexagonRenderer } from '../shapes/
|
|
2
|
+
import { hexagonRenderer } from '../shapes/index.ts'
|
|
3
3
|
import { canvasToString } from '../canvas.ts'
|
|
4
4
|
import type { ShapeRenderOptions } from '../shapes/types.ts'
|
|
5
5
|
|
|
@@ -150,6 +150,41 @@ describe('cylinder shape [(text)]', () => {
|
|
|
150
150
|
expect(out).toMatch(/'-+'/)
|
|
151
151
|
})
|
|
152
152
|
|
|
153
|
+
it('draws a rim line distinguishing it from a plain rounded node (issue #1119)', () => {
|
|
154
|
+
// The actual flowchart drawing path (drawNode -> drawBoxWithGridDimensions
|
|
155
|
+
// in draw-boxes.ts) draws every node shape as a plain bordered rectangle
|
|
156
|
+
// with shape-specific corner glyphs — it never calls cylinderRenderer
|
|
157
|
+
// (exercised only in isolation, above), and cylinder's corners are
|
|
158
|
+
// identical to rounded's own (see corners.ts's SHAPE_CORNERS), so a
|
|
159
|
+
// cylinder rendered in a real flowchart used to be indistinguishable
|
|
160
|
+
// from a rounded node — a weekly form-judge audit flagged this exact
|
|
161
|
+
// regression (#1119). drawBoxWithGridDimensions now draws a rim line
|
|
162
|
+
// just inside the top and bottom border for 'cylinder' specifically,
|
|
163
|
+
// using the extra height getShapeDimensions already reserves for it.
|
|
164
|
+
const src = `flowchart TD
|
|
165
|
+
A(Rounded)
|
|
166
|
+
B[(Database)]`
|
|
167
|
+
const out = renderMermaidASCII(src, { useAscii: false })
|
|
168
|
+
const lines = out.split('\n')
|
|
169
|
+
const topBorderLineIdx = lines.findIndex((l) => l.includes('╭'))
|
|
170
|
+
const topBorderLine = lines[topBorderLineIdx]!
|
|
171
|
+
const roundedX = topBorderLine.indexOf('╭')
|
|
172
|
+
const roundedEndX = topBorderLine.indexOf('╮', roundedX)
|
|
173
|
+
const databaseX = topBorderLine.indexOf('╭', roundedX + 1)
|
|
174
|
+
const databaseEndX = topBorderLine.indexOf('╮', databaseX)
|
|
175
|
+
expect(roundedX).toBeGreaterThanOrEqual(0)
|
|
176
|
+
expect(databaseX).toBeGreaterThan(roundedEndX)
|
|
177
|
+
|
|
178
|
+
// Row just inside the top border: Rounded's own interior span is blank
|
|
179
|
+
// (no rim); Database's is a solid rule between its vertical borders.
|
|
180
|
+
const rimRow = lines[topBorderLineIdx + 1]!
|
|
181
|
+
const roundedRimSpan = rimRow.slice(roundedX + 1, roundedEndX)
|
|
182
|
+
expect(roundedRimSpan.trim()).toBe('')
|
|
183
|
+
const databaseRimSpan = rimRow.slice(databaseX + 1, databaseEndX)
|
|
184
|
+
expect(databaseRimSpan.length).toBeGreaterThan(0)
|
|
185
|
+
expect(databaseRimSpan).toMatch(/^─+$/)
|
|
186
|
+
})
|
|
187
|
+
|
|
153
188
|
it('computes extra height for the curved top/bottom rows', () => {
|
|
154
189
|
const dims = cylinderRenderer.getDimensions('DB', unicodeOpts)
|
|
155
190
|
expect(dims.gridRows[0]).toBe(2)
|
|
@@ -8,10 +8,8 @@ import {
|
|
|
8
8
|
DIAGONAL_CHARS,
|
|
9
9
|
hasDiagonalLines,
|
|
10
10
|
findDiagonalLines,
|
|
11
|
-
assertNoDiagonals,
|
|
12
11
|
findOrphanedJunctions,
|
|
13
|
-
|
|
14
|
-
} from '../validate.ts'
|
|
12
|
+
} from './validate.ts'
|
|
15
13
|
|
|
16
14
|
describe('DIAGONAL_CHARS', () => {
|
|
17
15
|
it('exposes ascii, unicode, and combined character sets', () => {
|
|
@@ -90,35 +88,6 @@ describe('findDiagonalLines', () => {
|
|
|
90
88
|
})
|
|
91
89
|
})
|
|
92
90
|
|
|
93
|
-
describe('assertNoDiagonals', () => {
|
|
94
|
-
it('does not throw when there are no diagonals', () => {
|
|
95
|
-
expect(() => assertNoDiagonals('┌───┐\n│ A │\n└───┘')).not.toThrow()
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('throws with position details when diagonals are found', () => {
|
|
99
|
-
expect(() => assertNoDiagonals('a / b')).toThrow(
|
|
100
|
-
/Diagonal lines detected\. .*Found 1 diagonal character\(s\):\n\s*Line 1, Col 3: '\/'/s,
|
|
101
|
-
)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('includes the context string in the error message when provided', () => {
|
|
105
|
-
expect(() => assertNoDiagonals('a / b', 'my-diagram')).toThrow(
|
|
106
|
-
/Diagonal lines detected in "my-diagram"\./,
|
|
107
|
-
)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('omits the context clause when no context is provided', () => {
|
|
111
|
-
try {
|
|
112
|
-
assertNoDiagonals('a / b')
|
|
113
|
-
throw new Error('expected assertNoDiagonals to throw')
|
|
114
|
-
} catch (err) {
|
|
115
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
116
|
-
expect(message).toContain('Diagonal lines detected. ')
|
|
117
|
-
expect(message).not.toContain(' in "')
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
91
|
describe('findOrphanedJunctions', () => {
|
|
123
92
|
it('returns an empty array when there are no junction characters', () => {
|
|
124
93
|
expect(findOrphanedJunctions('┌───┐\n│ A │\n└───┘')).toEqual([])
|
|
@@ -208,13 +177,3 @@ describe('findOrphanedJunctions', () => {
|
|
|
208
177
|
])
|
|
209
178
|
})
|
|
210
179
|
})
|
|
211
|
-
|
|
212
|
-
describe('hasOrphanedJunctions', () => {
|
|
213
|
-
it('returns false when there are no orphaned junctions', () => {
|
|
214
|
-
expect(hasOrphanedJunctions('┌───┐\n│ A │\n└───┘')).toBe(false)
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('returns true when at least one orphaned junction is found', () => {
|
|
218
|
-
expect(hasOrphanedJunctions(' ┬ ')).toBe(true)
|
|
219
|
-
})
|
|
220
|
-
})
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { describe, it, expect } from 'vitest'
|
|
11
11
|
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
12
|
-
import { hasDiagonalLines, DIAGONAL_CHARS } from '
|
|
12
|
+
import { hasDiagonalLines, DIAGONAL_CHARS } from './validate.ts'
|
|
13
13
|
import { readdirSync, readFileSync } from 'node:fs'
|
|
14
14
|
import { join, dirname } from 'node:path'
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|