zombie-mermaid 2.2.2 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/ascii.cjs +8 -8
- package/dist/ascii.cjs.map +1 -1
- package/dist/ascii.js +1137 -1070
- package/dist/ascii.js.map +1 -1
- package/dist/cli.js +3380 -3295
- package/dist/index.cjs +59 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1780 -1712
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs +246 -52
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +246 -52
- package/dist/mcp.js.map +1 -1
- package/package.json +25 -21
- package/packages/ascii-renderer/src/canvas.ts +7 -4
- package/packages/ascii-renderer/src/class-diagram.ts +192 -12
- package/packages/ascii-renderer/src/draw-arrows.ts +79 -35
- package/packages/ascii-renderer/src/sequence.ts +42 -8
- package/packages/ascii-renderer/src/shapes/hexagon.ts +7 -3
- package/packages/ascii-renderer/src/shapes/special.ts +12 -4
- package/packages/core/src/generated/mono-font-subset.ts +32 -0
- package/packages/core/src/text-metrics.ts +22 -0
- package/packages/core/src/theme.ts +29 -13
- package/packages/mermaid-parser/src/xychart/parser.ts +44 -1
- package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
- package/src/__tests__/ascii-class-box-occupancy.test.ts +82 -43
- package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +8 -8
- package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
- package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
- package/src/__tests__/ascii-draw-arrows-coverage.test.ts +56 -0
- package/src/__tests__/ascii-edge-ending-glyphs.test.ts +3 -3
- package/src/__tests__/ascii-sequence-form-invariants.test.ts +51 -0
- package/src/__tests__/text-metrics.test.ts +13 -0
- package/src/__tests__/theme.test.ts +30 -0
- package/src/__tests__/workspace-package-boundaries.test.ts +2 -2
- package/src/__tests__/xychart-parser-categories.test.ts +78 -0
- package/src/cli/html-viewer.ts +5 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Regenerate with `pnpm run build:mono-font`
|
|
5
|
+
* (scripts/build-mono-font-subset.ts), which subsets
|
|
6
|
+
* third_party/fonts/jetbrains-mono-nl/JetBrainsMonoNL-Regular.ttf down to
|
|
7
|
+
* Basic Latin + Latin-1 Supplement — the self-hosted, embeddable
|
|
8
|
+
* `@font-face` packages/core/src/theme.ts's `buildStyleBlock()` inlines
|
|
9
|
+
* into an SVG's own `<style>` for its `.mono` rule (class-diagram method
|
|
10
|
+
* signatures, ER-diagram attribute types), replacing a Google Fonts CDN
|
|
11
|
+
* `@import` this library's SVG output previously depended on by default
|
|
12
|
+
* (#1061). See that script's header comment for why this subset is
|
|
13
|
+
* deliberately narrower than the site's own (demo/components/generated/
|
|
14
|
+
* mono-font-subset.ts).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The self-hosted family name every consumer's font stack should list
|
|
18
|
+
* first — matches this subset's own `name` table entry. */
|
|
19
|
+
export const MONO_FONT_FAMILY = 'JetBrains Mono NL'
|
|
20
|
+
|
|
21
|
+
/** The `@font-face` rule embedding the subset as a base64 `woff2` data
|
|
22
|
+
* URI — no separate network request, no page-depth-relative path to get
|
|
23
|
+
* wrong (see the script header comment for why data-URI over a static
|
|
24
|
+
* asset file). */
|
|
25
|
+
export const MONO_FONT_FACE_CSS = `@font-face {
|
|
26
|
+
font-family: 'JetBrains Mono NL';
|
|
27
|
+
font-style: normal;
|
|
28
|
+
font-weight: 400;
|
|
29
|
+
font-display: swap;
|
|
30
|
+
src: url(data:font/woff2;base64,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) format('woff2');
|
|
31
|
+
unicode-range: U+0020-007E, U+00A0-00FF;
|
|
32
|
+
}`
|
|
@@ -273,6 +273,27 @@ function isGeometricShapesTextDefault(code: number): boolean {
|
|
|
273
273
|
return code >= 0x25a0 && code <= 0x25ff
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Arrows-block glyphs that carry Extended_Pictographic (emoji-capable) but
|
|
278
|
+
* no default Emoji_Presentation, and render as a single narrow column in
|
|
279
|
+
* every real terminal — the same misclassification the Geometric Shapes
|
|
280
|
+
* block above hits. Covers:
|
|
281
|
+
*
|
|
282
|
+
* - ↖ U+2196, ↗ U+2197, ↘ U+2198, ↙ U+2199 — drawn by the ASCII renderer
|
|
283
|
+
* itself for diagonal edge-routing arrowheads
|
|
284
|
+
* (packages/ascii-renderer/src/draw-arrows.ts's `drawArrowHead`) in place
|
|
285
|
+
* of the filled triangles (◢◣◤◥) JetBrains Mono NL has no glyph for at
|
|
286
|
+
* all — see issue #1062.
|
|
287
|
+
* - ↔ U+2194, ↕ U+2195, ↩ U+21A9, ↪ U+21AA — not drawn by the renderer
|
|
288
|
+
* itself, but reachable if a diagram author types one into node/edge/note
|
|
289
|
+
* label text, which passes through to the ASCII grid verbatim.
|
|
290
|
+
*/
|
|
291
|
+
function isArrowsBlockTextDefault(code: number): boolean {
|
|
292
|
+
return (
|
|
293
|
+
(code >= 0x2194 && code <= 0x2199) || code === 0x21a9 || code === 0x21aa
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
276
297
|
/**
|
|
277
298
|
* Check if a character is an emoji (fullwidth)
|
|
278
299
|
*/
|
|
@@ -280,6 +301,7 @@ function isEmoji(char: string): boolean {
|
|
|
280
301
|
if (EMOJI_PRESENTATION_REGEX.test(char)) return true
|
|
281
302
|
const code = char.codePointAt(0)
|
|
282
303
|
if (code !== undefined && isGeometricShapesTextDefault(code)) return false
|
|
304
|
+
if (code !== undefined && isArrowsBlockTextDefault(code)) return false
|
|
283
305
|
return EXTENDED_PICTOGRAPHIC_REGEX.test(char)
|
|
284
306
|
}
|
|
285
307
|
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
import { escapeXml, escapeAttr } from './multiline-utils.ts'
|
|
15
15
|
import { parseHexColor } from './color-utils.ts'
|
|
16
|
+
import {
|
|
17
|
+
MONO_FONT_FAMILY as SVG_MONO_FONT_FAMILY,
|
|
18
|
+
MONO_FONT_FACE_CSS as SVG_MONO_FONT_FACE_CSS,
|
|
19
|
+
} from './generated/mono-font-subset.ts'
|
|
16
20
|
|
|
17
21
|
// ============================================================================
|
|
18
22
|
// Types
|
|
@@ -412,6 +416,21 @@ export function styleOpenTag(nonce?: string): string {
|
|
|
412
416
|
*
|
|
413
417
|
* Any other value is treated as a literal font name: sanitized and quoted
|
|
414
418
|
* exactly as before.
|
|
419
|
+
*
|
|
420
|
+
* `hasMonoFont`'s `.mono` rule (class-diagram method signatures, ER-diagram
|
|
421
|
+
* attribute types) never reaches out to Google Fonts at all — it used to
|
|
422
|
+
* `@import` 'JetBrains Mono' from Google's CDN, a third-party network
|
|
423
|
+
* dependency a published library's default SVG output had no business
|
|
424
|
+
* making (#1061, filed as a follow-up from #1059's ASCII-side self-hosting
|
|
425
|
+
* fix). It now embeds {@link SVG_MONO_FONT_FACE_CSS} — a small, subsetted
|
|
426
|
+
* (Basic Latin + Latin-1 Supplement), self-hosted `@font-face` as a base64
|
|
427
|
+
* `woff2` data URI, generated by scripts/build-mono-font-subset.ts into
|
|
428
|
+
* ./generated/mono-font-subset.ts — directly in the SVG's own `<style>`
|
|
429
|
+
* block. Unlike `font` above, this isn't caller-configurable: it's the same
|
|
430
|
+
* embed for every consumer, on by default, with no network request either
|
|
431
|
+
* way — a strict improvement over the dead-or-alive CDN fetch it replaces,
|
|
432
|
+
* and the only way a *standalone* SVG (no surrounding host page providing
|
|
433
|
+
* its own copy of the font) renders `.mono` text correctly out of the box.
|
|
415
434
|
*/
|
|
416
435
|
export function buildStyleBlock(
|
|
417
436
|
font: string,
|
|
@@ -426,18 +445,11 @@ export function buildStyleBlock(
|
|
|
426
445
|
const safeFont = isVarReference ? font.trim() : font.replace(/[<>{};'"]/g, '')
|
|
427
446
|
const skipImport = isVarReference || isFontStackOrGenericFamily(font)
|
|
428
447
|
|
|
429
|
-
const fontImports =
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
]),
|
|
435
|
-
...(hasMonoFont
|
|
436
|
-
? [
|
|
437
|
-
`@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');`,
|
|
438
|
-
]
|
|
439
|
-
: []),
|
|
440
|
-
]
|
|
448
|
+
const fontImports = skipImport
|
|
449
|
+
? []
|
|
450
|
+
: [
|
|
451
|
+
`@import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(safeFont)}:wght@400;500;600;700&display=swap');`,
|
|
452
|
+
]
|
|
441
453
|
|
|
442
454
|
// Derived CSS variables: use override if set, else mix from bg+fg.
|
|
443
455
|
// The --_ prefix signals "private/derived" — not meant for external override.
|
|
@@ -459,10 +471,14 @@ export function buildStyleBlock(
|
|
|
459
471
|
return [
|
|
460
472
|
styleOpenTag(nonce),
|
|
461
473
|
...(fontImports.length > 0 ? [` ${fontImports.join('\n ')}`] : []),
|
|
474
|
+
// Self-hosted, embedded @font-face for `.mono` text — see the doc
|
|
475
|
+
// comment above for why this never goes through the CDN-import/skip
|
|
476
|
+
// logic `font` above does.
|
|
477
|
+
...(hasMonoFont ? [` ${SVG_MONO_FONT_FACE_CSS}`] : []),
|
|
462
478
|
` text { font-family: ${isVarReference ? safeFont : `'${safeFont}'`}, system-ui, sans-serif; }`,
|
|
463
479
|
...(hasMonoFont
|
|
464
480
|
? [
|
|
465
|
-
` .mono { font-family: '
|
|
481
|
+
` .mono { font-family: '${SVG_MONO_FONT_FAMILY}', 'SF Mono', 'Fira Code', ui-monospace, monospace; }`,
|
|
466
482
|
]
|
|
467
483
|
: []),
|
|
468
484
|
` svg {${derivedVars}`,
|
|
@@ -48,7 +48,9 @@ export function parseXYChart(lines: Statement[]): XYChart {
|
|
|
48
48
|
const xCatMatch = line.match(/^x-axis\s+(?:"([^"]*)"\s*)?\[([^\]]+)\]/)
|
|
49
49
|
if (xCatMatch) {
|
|
50
50
|
if (xCatMatch[1]) xAxis.title = xCatMatch[1]
|
|
51
|
-
xAxis.categories = xCatMatch[2]
|
|
51
|
+
xAxis.categories = splitCategoryList(xCatMatch[2]!).map((s) =>
|
|
52
|
+
unquote(s.trim()),
|
|
53
|
+
)
|
|
52
54
|
continue
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -148,6 +150,47 @@ export function parseXYChart(lines: Statement[]): XYChart {
|
|
|
148
150
|
return { title, horizontal, xAxis, yAxis, series }
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Split a categorical x-axis bracket's comma-separated contents into raw
|
|
155
|
+
* item strings, treating a comma *inside* a double-quoted item as literal
|
|
156
|
+
* text rather than a delimiter. A naive `.split(',')` would break
|
|
157
|
+
* `["Jan, Feb", "Mar"]` into three pieces instead of two — quoting a value
|
|
158
|
+
* that itself contains a comma is exactly the case quoting exists for. Each
|
|
159
|
+
* returned raw item is still trimmed and unquoted by the caller.
|
|
160
|
+
*/
|
|
161
|
+
function splitCategoryList(str: string): string[] {
|
|
162
|
+
const items: string[] = []
|
|
163
|
+
let current = ''
|
|
164
|
+
let inQuotes = false
|
|
165
|
+
for (const char of str) {
|
|
166
|
+
if (char === '"') {
|
|
167
|
+
inQuotes = !inQuotes
|
|
168
|
+
current += char
|
|
169
|
+
} else if (char === ',' && !inQuotes) {
|
|
170
|
+
items.push(current)
|
|
171
|
+
current = ''
|
|
172
|
+
} else {
|
|
173
|
+
current += char
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
items.push(current)
|
|
177
|
+
return items
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Strip one matching pair of leading/trailing double quotes from a category
|
|
182
|
+
* item, if present — mirroring how an axis *title* capture group already
|
|
183
|
+
* unquotes via its regex. `x-axis [A, B, C]` items are only split and
|
|
184
|
+
* trimmed, so a quoted item like `"CLI output / logs"` previously kept its
|
|
185
|
+
* literal quote characters in the rendered label. See issue #1087.
|
|
186
|
+
*/
|
|
187
|
+
function unquote(value: string): string {
|
|
188
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
189
|
+
return value.slice(1, -1)
|
|
190
|
+
}
|
|
191
|
+
return value
|
|
192
|
+
}
|
|
193
|
+
|
|
151
194
|
/**
|
|
152
195
|
* Parse a `bar`/`line` series' comma-separated bracket contents into
|
|
153
196
|
* numbers, throwing on any element that isn't a valid number instead of
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for #1083: the last edge in a mixed-style fan-out got a
|
|
3
|
+
* bogus diagonal arrowhead (↘) instead of a straight one (▼), even though
|
|
4
|
+
* its line was perfectly vertical with zero horizontal drift.
|
|
5
|
+
*
|
|
6
|
+
* Root cause: that edge's route fell through to determinePath's Case-4
|
|
7
|
+
* direct fallback (see #418), producing a single non-axis-aligned grid
|
|
8
|
+
* segment. draw-lines.ts's drawLine draws that as an L (horizontal run,
|
|
9
|
+
* then vertical run) folded into one `linesDrawn` entry. drawArrowHead used
|
|
10
|
+
* to derive direction from that entry's *first* and *last* point, which
|
|
11
|
+
* spans both legs of the L and reads as diagonal — even when the actual
|
|
12
|
+
* final approach into the arrowhead is a plain vertical step. Fixed by
|
|
13
|
+
* deriving direction from the last two points (the final step) instead.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from 'vitest'
|
|
16
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
17
|
+
|
|
18
|
+
const ARROWHEAD_CHARS = '▼▲◄►↖↗↘↙'
|
|
19
|
+
|
|
20
|
+
function arrowCharsInLastRowBeforeTargets(source: string): string[] {
|
|
21
|
+
const lines = renderMermaidASCII(source, { colorMode: 'none' }).split('\n')
|
|
22
|
+
const arrowRow = lines.find((l) =>
|
|
23
|
+
[...l].some((ch) => ARROWHEAD_CHARS.includes(ch)),
|
|
24
|
+
)!
|
|
25
|
+
return [...arrowRow].filter((ch) => ARROWHEAD_CHARS.includes(ch))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('arrowhead direction on a Case-4 direct-fallback (diagonal) path', () => {
|
|
29
|
+
it('renders every arrowhead as ▼ when thick is the last mixed-style edge', () => {
|
|
30
|
+
const source = `graph TD
|
|
31
|
+
A[Source] -->|solid| B[Target 1]
|
|
32
|
+
A -.->|dotted| C[Target 2]
|
|
33
|
+
A ==>|thick| D[Target 3]`
|
|
34
|
+
|
|
35
|
+
const arrows = arrowCharsInLastRowBeforeTargets(source)
|
|
36
|
+
expect(arrows).toEqual(['▼', '▼', '▼'])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders every arrowhead as ▼ regardless of which style is last', () => {
|
|
40
|
+
// Same three styles, reordered so `solid` (not `thick`) is the one that
|
|
41
|
+
// falls through to the Case-4 fallback — the bug followed *position*,
|
|
42
|
+
// not the specific style.
|
|
43
|
+
const source = `graph TD
|
|
44
|
+
A[Source] ==>|thick| B[Target 1]
|
|
45
|
+
A -.->|dotted| C[Target 2]
|
|
46
|
+
A -->|solid| D[Target 3]`
|
|
47
|
+
|
|
48
|
+
const arrows = arrowCharsInLastRowBeforeTargets(source)
|
|
49
|
+
expect(arrows).toEqual(['▼', '▼', '▼'])
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -137,70 +137,109 @@ ${manyAttrs('b', 6)}
|
|
|
137
137
|
// pair via the level-BFS (Root has no parent, Kid is its level-1 child),
|
|
138
138
|
// placed on a different row from the Alpha/Mid/Charlie cycle.
|
|
139
139
|
//
|
|
140
|
-
// Two
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// `findBoxRect`). If the different-row filter worked, both
|
|
140
|
+
// Two variants of the same Root/Kid pair are compared: a narrow `Kid`
|
|
141
|
+
// never reaches the cycle's column span (baseline); a `Kid` wide enough
|
|
142
|
+
// to need real space, with `Root` declared right after `Alpha`, overlaps
|
|
143
|
+
// Alpha's (and Mid's) column range instead (confirmed below via
|
|
144
|
+
// `findBoxRect`). If the different-row filter worked, both variants
|
|
145
145
|
// render the cycle identically — Root/Kid's actual row is irrelevant to
|
|
146
|
-
// it either way. A test that only used the non-overlapping
|
|
146
|
+
// it either way. A test that only used the non-overlapping variant would
|
|
147
147
|
// pass even with the filter deleted (verified: an earlier version of this
|
|
148
148
|
// test did exactly that, and CodeRabbit correctly flagged it as not
|
|
149
|
-
// actually exercising the filter — see the PR discussion).
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
149
|
+
// actually exercising the filter — see the PR discussion).
|
|
150
|
+
//
|
|
151
|
+
// Declaration order alone no longer controls Kid's column (issue #971:
|
|
152
|
+
// `Kid`, as the sole occupant of its level with exactly one qualifying
|
|
153
|
+
// parent, now aligns under Root's own — already-placed — column instead
|
|
154
|
+
// of always landing at the leftmost slot). This test used to force the
|
|
155
|
+
// overlapping/non-overlapping split by moving `Root`/`Kid`'s declaration
|
|
156
|
+
// point around the cycle alone; that no longer produces reliably
|
|
157
|
+
// different geometry, since Kid's position now tracks wherever Root
|
|
158
|
+
// ends up rather than its own declaration slot. Widening `Kid` (so its
|
|
159
|
+
// box needs more room than the gap next to Root provides) is what
|
|
160
|
+
// reliably produces a real overlap now, confirmed empirically against
|
|
161
|
+
// the current renderer rather than assumed.
|
|
162
|
+
//
|
|
163
|
+
// Sabotage-verified: deleting the filter leaves both variants' `+ bN:
|
|
164
|
+
// String` lines intact but makes the overlapping variant one row taller
|
|
165
|
+
// than the baseline (19 vs. 18), which the equality assertion below
|
|
153
166
|
// catches.
|
|
154
167
|
it('3-node cycle, unrelated Root->Kid pair on a different level: obstruction search ignores it regardless of whether it overlaps the detour span', () => {
|
|
155
|
-
|
|
156
|
-
|
|
168
|
+
// Baseline: Root/Kid declared after the whole cycle, Kid narrow enough
|
|
169
|
+
// to never need more room than the gap next to Root provides.
|
|
170
|
+
const nonOverlapping = renderMermaidASCII(
|
|
171
|
+
`classDiagram
|
|
172
|
+
class Alpha
|
|
173
|
+
class Mid {
|
|
174
|
+
${manyAttrs('b', 6)}
|
|
175
|
+
}
|
|
176
|
+
class Charlie
|
|
177
|
+
class Root
|
|
157
178
|
class Kid
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
Alpha --> Mid
|
|
180
|
+
Mid --> Charlie
|
|
181
|
+
Charlie --> Alpha
|
|
182
|
+
Root --> Kid`,
|
|
183
|
+
{ useAscii: true },
|
|
184
|
+
)
|
|
185
|
+
// Overlapping: Root declared immediately after Alpha, Kid wide enough
|
|
186
|
+
// that centering it under Root's (narrow) column reaches into Alpha's.
|
|
187
|
+
const overlapping = renderMermaidASCII(
|
|
188
|
+
`classDiagram
|
|
189
|
+
class Alpha
|
|
190
|
+
class Root
|
|
191
|
+
class VeryLongKidName
|
|
192
|
+
class Mid {
|
|
162
193
|
${manyAttrs('b', 6)}
|
|
163
194
|
}
|
|
164
195
|
class Charlie
|
|
165
196
|
Alpha --> Mid
|
|
166
197
|
Mid --> Charlie
|
|
167
198
|
Charlie --> Alpha
|
|
168
|
-
Root -->
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const nonOverlapping = renderMermaidASCII(withRootKid(false), {
|
|
172
|
-
useAscii: true,
|
|
173
|
-
})
|
|
174
|
-
const overlapping = renderMermaidASCII(withRootKid(true), {
|
|
175
|
-
useAscii: true,
|
|
176
|
-
})
|
|
199
|
+
Root --> VeryLongKidName`,
|
|
200
|
+
{ useAscii: true },
|
|
201
|
+
)
|
|
177
202
|
|
|
178
203
|
// Confirms the geometry this test relies on: in the "overlapping"
|
|
179
|
-
//
|
|
204
|
+
// variant, Kid really does sit on a different row from the cycle, and
|
|
180
205
|
// really does overlap Alpha's own column range (i.e. the obstruction
|
|
181
206
|
// search has something to incorrectly notice if the different-row
|
|
182
|
-
// filter is missing); in the
|
|
207
|
+
// filter is missing); in the baseline, it doesn't.
|
|
183
208
|
const overlappingAlphaRect = findBoxRect(overlapping, 'Alpha')
|
|
184
|
-
const overlappingKidRect = findBoxRect(overlapping, '
|
|
209
|
+
const overlappingKidRect = findBoxRect(overlapping, 'VeryLongKidName')
|
|
185
210
|
expect(overlappingKidRect.y0).toBeGreaterThan(overlappingAlphaRect.y1)
|
|
186
|
-
expect(overlappingKidRect.x0).
|
|
211
|
+
expect(overlappingKidRect.x0).toBeLessThanOrEqual(overlappingAlphaRect.x1)
|
|
187
212
|
const nonOverlappingAlphaRect = findBoxRect(nonOverlapping, 'Alpha')
|
|
188
213
|
const nonOverlappingKidRect = findBoxRect(nonOverlapping, 'Kid')
|
|
189
|
-
expect(
|
|
190
|
-
nonOverlappingAlphaRect.x0
|
|
191
|
-
|
|
214
|
+
expect(
|
|
215
|
+
nonOverlappingKidRect.x1 < nonOverlappingAlphaRect.x0 ||
|
|
216
|
+
nonOverlappingKidRect.x0 > nonOverlappingAlphaRect.x1,
|
|
217
|
+
).toBe(true)
|
|
192
218
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
expectAllPresentOnce(nonOverlapping, [
|
|
220
|
+
'+ b0: String',
|
|
221
|
+
'+ b1: String',
|
|
222
|
+
'+ b2: String',
|
|
223
|
+
'+ b3: String',
|
|
224
|
+
'+ b4: String',
|
|
225
|
+
'+ b5: String',
|
|
226
|
+
])
|
|
227
|
+
expectNoBoxOverlap(nonOverlapping, [
|
|
228
|
+
'Alpha',
|
|
229
|
+
'Mid',
|
|
230
|
+
'Charlie',
|
|
231
|
+
'Root',
|
|
232
|
+
'Kid',
|
|
233
|
+
])
|
|
234
|
+
expectAllPresentOnce(overlapping, [
|
|
235
|
+
'+ b0: String',
|
|
236
|
+
'+ b1: String',
|
|
237
|
+
'+ b2: String',
|
|
238
|
+
'+ b3: String',
|
|
239
|
+
'+ b4: String',
|
|
240
|
+
'+ b5: String',
|
|
241
|
+
])
|
|
242
|
+
expectNoBoxOverlap(overlapping, ['Alpha', 'Mid', 'Charlie', 'Root'])
|
|
204
243
|
|
|
205
244
|
// The real check: Kid overlapping the detour span must not change the
|
|
206
245
|
// cycle's own rendered height compared to Kid sitting clear of it.
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest'
|
|
28
28
|
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
29
|
+
import { findBoxRect } from './helpers/ascii-form.ts'
|
|
29
30
|
|
|
30
31
|
const MVC_SOURCE = `classDiagram
|
|
31
32
|
class Model {
|
|
@@ -85,17 +86,16 @@ describe('ASCII class diagram — detour-aware relationship label placement (iss
|
|
|
85
86
|
const ascii = renderMermaidASCII(MVC_SOURCE, { colorMode: 'none' })
|
|
86
87
|
const lines = ascii.split('\n')
|
|
87
88
|
|
|
88
|
-
// Model's
|
|
89
|
-
//
|
|
89
|
+
// Model's box right edge (column-based, not a literal-column-0
|
|
90
|
+
// assumption — issue #971 can legitimately shift Model's own column
|
|
91
|
+
// when it has a real single qualifying parent to align under, same as
|
|
92
|
+
// it does here against Controller). `refreshes` — the label for the
|
|
90
93
|
// relationship that detours around Model to reach View — must land
|
|
91
|
-
// strictly to the right of that
|
|
94
|
+
// strictly to the right of that edge on every row it occupies, never
|
|
92
95
|
// overlapping Model's own footprint the way the straight-line midpoint
|
|
93
96
|
// used to (issue #487).
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
expect(modelTopRow).toBeGreaterThanOrEqual(0)
|
|
98
|
-
const modelRightBorder = lines[modelTopRow]!.indexOf('┐')
|
|
97
|
+
const modelRect = findBoxRect(ascii, 'Model')
|
|
98
|
+
const modelRightBorder = modelRect.x1
|
|
99
99
|
expect(modelRightBorder).toBeGreaterThan(0)
|
|
100
100
|
|
|
101
101
|
const refreshesRow = lines.findIndex((l) => l.includes('refreshes'))
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for issue #971 — "ASCII class-diagram layout: single-
|
|
3
|
+
* parent x-alignment," the first implementation stage of #964's fix
|
|
4
|
+
* (docs/decisions/ascii-class-diagram-x-coordinate-assignment-970.md).
|
|
5
|
+
*
|
|
6
|
+
* Before this fix, `class-diagram.ts` packed each level's classes strictly
|
|
7
|
+
* left-to-right in declaration order, with no reference to any parent's
|
|
8
|
+
* column — a level with exactly one occupant always landed at column 0,
|
|
9
|
+
* even when its real parent sat far to the right. This fix aligns a
|
|
10
|
+
* "singleton block" (no sibling at its level shares its exact parent-id
|
|
11
|
+
* set) with exactly one qualifying (strictly-shallower) parent under that
|
|
12
|
+
* parent's own box center instead. Every other shape (no parents, an
|
|
13
|
+
* all-same-level/rootless-cycle parent set, or more than one class sharing
|
|
14
|
+
* a parent set) is out of scope here — it keeps its pre-fix left-to-right
|
|
15
|
+
* position, tracked separately by issue #972.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest'
|
|
19
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
20
|
+
import { findBoxRect } from './helpers/ascii-form.ts'
|
|
21
|
+
|
|
22
|
+
describe('ASCII class diagram — single-parent x-alignment (issue #971)', () => {
|
|
23
|
+
it("#964's exact repro: a lone child aligns under its real parent's center, not the leftmost column", () => {
|
|
24
|
+
const src = `classDiagram
|
|
25
|
+
class Left
|
|
26
|
+
class TallMid {
|
|
27
|
+
+String b0
|
|
28
|
+
+String b1
|
|
29
|
+
+String b2
|
|
30
|
+
+String b3
|
|
31
|
+
+String b4
|
|
32
|
+
+String b5
|
|
33
|
+
}
|
|
34
|
+
class RightSrc
|
|
35
|
+
class Kid
|
|
36
|
+
RightSrc --> Kid`
|
|
37
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
38
|
+
|
|
39
|
+
const rightSrc = findBoxRect(ascii, 'RightSrc')
|
|
40
|
+
const kid = findBoxRect(ascii, 'Kid')
|
|
41
|
+
const rightSrcCenter =
|
|
42
|
+
rightSrc.x0 + Math.floor((rightSrc.x1 - rightSrc.x0) / 2)
|
|
43
|
+
const kidCenter = kid.x0 + Math.floor((kid.x1 - kid.x0) / 2)
|
|
44
|
+
|
|
45
|
+
// Kid's box center lands on (or within 1 cell of, for odd-width
|
|
46
|
+
// rounding) RightSrc's box center -- not at column 0, where every
|
|
47
|
+
// sole-occupant level used to land regardless of its real parent.
|
|
48
|
+
expect(Math.abs(kidCenter - rightSrcCenter)).toBeLessThanOrEqual(1)
|
|
49
|
+
expect(kid.x0).toBeGreaterThan(0)
|
|
50
|
+
|
|
51
|
+
// Left is the leftmost root and must stay exactly where declaration
|
|
52
|
+
// order has always put it -- this fix only ever moves a class that has
|
|
53
|
+
// a real single parent to align to.
|
|
54
|
+
const left = findBoxRect(ascii, 'Left')
|
|
55
|
+
expect(left.x0).toBe(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('a root class with no parent at all is unaffected', () => {
|
|
59
|
+
const src = `classDiagram
|
|
60
|
+
class Lonely`
|
|
61
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
62
|
+
const lonely = findBoxRect(ascii, 'Lonely')
|
|
63
|
+
expect(lonely.x0).toBe(0)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('a class whose only parent edges are same-level (a rootless relationship cycle) is unaffected', () => {
|
|
67
|
+
// A -> B -> C -> A has no external root: the level-BFS puts every
|
|
68
|
+
// member at level 0 together, but the `parents` map still records
|
|
69
|
+
// each one's in-cycle predecessor. That predecessor is same-level, not
|
|
70
|
+
// strictly shallower, so it doesn't qualify -- every member keeps its
|
|
71
|
+
// plain declaration-order position, same as before this fix.
|
|
72
|
+
const src = `classDiagram
|
|
73
|
+
class A
|
|
74
|
+
class B
|
|
75
|
+
class C
|
|
76
|
+
A --> B
|
|
77
|
+
B --> C
|
|
78
|
+
C --> A`
|
|
79
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
80
|
+
const a = findBoxRect(ascii, 'A')
|
|
81
|
+
const b = findBoxRect(ascii, 'B')
|
|
82
|
+
const c = findBoxRect(ascii, 'C')
|
|
83
|
+
expect(a.x0).toBe(0)
|
|
84
|
+
expect(a.x0).toBeLessThan(b.x0)
|
|
85
|
+
expect(b.x0).toBeLessThan(c.x0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Two children sharing the exact same single parent are a *multi-member*
|
|
89
|
+
// block -- explicitly out of #971's scope (see #972) -- so this sample
|
|
90
|
+
// must render byte-for-byte the way it always has. Chosen because
|
|
91
|
+
// docs/decisions/ascii-class-diagram-x-coordinate-assignment-970.md
|
|
92
|
+
// names this exact samples-data.ts sample ("Class: Inheritance (<|--)")
|
|
93
|
+
// as one it verified stays unchanged.
|
|
94
|
+
it('an existing catalog sample with a multi-child parent (Class: Inheritance) keeps its exact current positions', () => {
|
|
95
|
+
const src = `classDiagram
|
|
96
|
+
class Animal {
|
|
97
|
+
+String name
|
|
98
|
+
+eat() void
|
|
99
|
+
}
|
|
100
|
+
class Dog {
|
|
101
|
+
+String breed
|
|
102
|
+
+bark() void
|
|
103
|
+
}
|
|
104
|
+
class Cat {
|
|
105
|
+
+bool isIndoor
|
|
106
|
+
+meow() void
|
|
107
|
+
}
|
|
108
|
+
Animal <|-- Dog
|
|
109
|
+
Animal <|-- Cat`
|
|
110
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
111
|
+
const animal = findBoxRect(ascii, 'Animal')
|
|
112
|
+
const dog = findBoxRect(ascii, 'Dog')
|
|
113
|
+
const cat = findBoxRect(ascii, 'Cat')
|
|
114
|
+
expect(animal).toEqual({ x0: 0, x1: 17, y0: 0, y1: 6 })
|
|
115
|
+
expect(dog).toEqual({ x0: 0, x1: 18, y0: 10, y1: 16 })
|
|
116
|
+
expect(cat).toEqual({ x0: 23, x1: 42, y0: 10, y1: 16 })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('two independent single-parent children at the same level both align, and compaction keeps them from overlapping', () => {
|
|
120
|
+
// ParentA and ParentB are both roots; KidA is ParentA's only child,
|
|
121
|
+
// KidB is ParentB's only child. Each is its own singleton block with
|
|
122
|
+
// exactly one qualifying parent, so both get a real desired center --
|
|
123
|
+
// this exercises the compaction pass itself (#971 must implement it
|
|
124
|
+
// for every block shape, not just multi-parent ones): two different
|
|
125
|
+
// singleton blocks can still want colliding desired positions.
|
|
126
|
+
const src = `classDiagram
|
|
127
|
+
class ParentA
|
|
128
|
+
class ParentB
|
|
129
|
+
class KidA
|
|
130
|
+
class KidB
|
|
131
|
+
ParentA --> KidA
|
|
132
|
+
ParentB --> KidB`
|
|
133
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
134
|
+
const kidA = findBoxRect(ascii, 'KidA')
|
|
135
|
+
const kidB = findBoxRect(ascii, 'KidB')
|
|
136
|
+
// Declaration order is preserved -- KidA never lands right of KidB.
|
|
137
|
+
expect(kidA.x1).toBeLessThan(kidB.x0)
|
|
138
|
+
})
|
|
139
|
+
})
|