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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for issue #972 — "ASCII class-diagram layout:
|
|
3
|
+
* multi-parent / overlap resolution," the second implementation stage of
|
|
4
|
+
* #964's fix (docs/decisions/ascii-class-diagram-x-coordinate-assignment-970.md),
|
|
5
|
+
* building on #971's single-parent alignment + universal compaction pass.
|
|
6
|
+
*
|
|
7
|
+
* #971 only computed a real desired position for a *singleton* block (no
|
|
8
|
+
* sibling at its level shares its exact parent-id set) with exactly one
|
|
9
|
+
* qualifying (strictly-shallower) parent. This fix adds the remaining
|
|
10
|
+
* block shapes #970's decision named: a block of more than one class
|
|
11
|
+
* sharing one qualifying-parent-set spreads evenly around that shared
|
|
12
|
+
* center; a block with more than one qualifying parent centers on the
|
|
13
|
+
* mean of their centers. Both reuse #971's exact compaction pass
|
|
14
|
+
* unchanged, which already handles "an aligned position collides with a
|
|
15
|
+
* sibling" — #972 does not add new nudging logic, only new
|
|
16
|
+
* desired-position computations for `if (qualifying.length === 0)` this
|
|
17
|
+
* pass now escapes into.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest'
|
|
21
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
22
|
+
import { findBoxRect, rectsOverlap } from './helpers/ascii-form.ts'
|
|
23
|
+
|
|
24
|
+
function boxCenter(rect: { x0: number; x1: number }): number {
|
|
25
|
+
return rect.x0 + Math.floor((rect.x1 - rect.x0 + 1) / 2)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('ASCII class diagram — multi-parent / overlap resolution (issue #972)', () => {
|
|
29
|
+
it('two children of the same parent spread evenly around its center, not packed from column 0', () => {
|
|
30
|
+
// WidePadding gives Animal real room on both sides -- proves genuine
|
|
31
|
+
// symmetric spreading, not just an edge-of-canvas clamp coincidence.
|
|
32
|
+
const src = `classDiagram
|
|
33
|
+
class WidePadding {
|
|
34
|
+
+String p0
|
|
35
|
+
+String p1
|
|
36
|
+
+String p2
|
|
37
|
+
}
|
|
38
|
+
class Animal
|
|
39
|
+
class Dog
|
|
40
|
+
class Cat
|
|
41
|
+
Animal <|-- Dog
|
|
42
|
+
Animal <|-- Cat`
|
|
43
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
44
|
+
const animal = findBoxRect(ascii, 'Animal')
|
|
45
|
+
const dog = findBoxRect(ascii, 'Dog')
|
|
46
|
+
const cat = findBoxRect(ascii, 'Cat')
|
|
47
|
+
|
|
48
|
+
// Declaration order preserved: Dog left of Cat, neither overlapping.
|
|
49
|
+
expect(dog.x1).toBeLessThan(cat.x0)
|
|
50
|
+
// The pair's combined footprint centers on Animal's own center.
|
|
51
|
+
const blockCenter = dog.x0 + Math.floor((cat.x1 - dog.x0 + 1) / 2)
|
|
52
|
+
expect(Math.abs(blockCenter - boxCenter(animal))).toBeLessThanOrEqual(1)
|
|
53
|
+
// Neither child sits at column 0 (the pre-#972 leftover artifact).
|
|
54
|
+
expect(dog.x0).toBeGreaterThan(0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('a child with two parents at different levels centers on the mean of their centers', () => {
|
|
58
|
+
// Controller (level 0) -> Model (level 1) -> View (level 2); View also
|
|
59
|
+
// has a direct edge from Controller. View's qualifying parents are
|
|
60
|
+
// both Controller and Model (both strictly shallower than View), so
|
|
61
|
+
// its desired center is their mean -- not just Controller's alone.
|
|
62
|
+
const src = `classDiagram
|
|
63
|
+
class Controller {
|
|
64
|
+
+handleInput() void
|
|
65
|
+
}
|
|
66
|
+
class Model {
|
|
67
|
+
+getData() void
|
|
68
|
+
}
|
|
69
|
+
class View {
|
|
70
|
+
+render() void
|
|
71
|
+
}
|
|
72
|
+
Controller --> Model
|
|
73
|
+
Controller --> View
|
|
74
|
+
Model ..> View`
|
|
75
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
76
|
+
const controller = findBoxRect(ascii, 'Controller')
|
|
77
|
+
const model = findBoxRect(ascii, 'Model')
|
|
78
|
+
const view = findBoxRect(ascii, 'View')
|
|
79
|
+
|
|
80
|
+
const expectedCenter = Math.round(
|
|
81
|
+
(boxCenter(controller) + boxCenter(model)) / 2,
|
|
82
|
+
)
|
|
83
|
+
expect(Math.abs(boxCenter(view) - expectedCenter)).toBeLessThanOrEqual(1)
|
|
84
|
+
// Not simply parked back at column 0 the way an unresolved block used to be.
|
|
85
|
+
expect(view.x0).toBeGreaterThan(0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('a multi-child block whose desired position collides with an earlier sibling is pushed right without overlapping it', () => {
|
|
89
|
+
// Two parents both near the canvas's left edge: EarlyParent's own
|
|
90
|
+
// child (EarlySibling) is placed first; WidePair's two children want
|
|
91
|
+
// to center on WidePair, which sits close enough that their desired
|
|
92
|
+
// block would otherwise land on top of EarlySibling. The universal
|
|
93
|
+
// compaction pass (#971, reused unchanged by #972) must push the
|
|
94
|
+
// whole block right of EarlySibling instead of overlapping it.
|
|
95
|
+
const src = `classDiagram
|
|
96
|
+
class EarlyParent
|
|
97
|
+
class WidePair
|
|
98
|
+
class EarlySibling
|
|
99
|
+
class ChildA
|
|
100
|
+
class ChildB
|
|
101
|
+
EarlyParent --> EarlySibling
|
|
102
|
+
WidePair --> ChildA
|
|
103
|
+
WidePair --> ChildB`
|
|
104
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
105
|
+
const names = [
|
|
106
|
+
'EarlyParent',
|
|
107
|
+
'WidePair',
|
|
108
|
+
'EarlySibling',
|
|
109
|
+
'ChildA',
|
|
110
|
+
'ChildB',
|
|
111
|
+
]
|
|
112
|
+
const rects = names.map((n) => ({ n, r: findBoxRect(ascii, n) }))
|
|
113
|
+
for (let i = 0; i < rects.length; i++) {
|
|
114
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
115
|
+
expect(
|
|
116
|
+
rectsOverlap(rects[i]!.r, rects[j]!.r),
|
|
117
|
+
`${rects[i]!.n} and ${rects[j]!.n} boxes overlap`,
|
|
118
|
+
).toBe(false)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Declaration order preserved even after compaction pushes the block right.
|
|
122
|
+
const earlySibling = findBoxRect(ascii, 'EarlySibling')
|
|
123
|
+
const childA = findBoxRect(ascii, 'ChildA')
|
|
124
|
+
expect(earlySibling.x1).toBeLessThan(childA.x0)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('a block with no qualifying parents (a rootless cycle with two same-level "parent" edges) keeps its plain baseline position', () => {
|
|
128
|
+
// A and B mutually reference each other with no external root: both
|
|
129
|
+
// land at level 0, and neither's same-level "parent" edge qualifies --
|
|
130
|
+
// #972 must not spread or converge this shape, only #971's untouched
|
|
131
|
+
// no-resolvable-parents fallback applies.
|
|
132
|
+
const src = `classDiagram
|
|
133
|
+
class A
|
|
134
|
+
class B
|
|
135
|
+
A --> B
|
|
136
|
+
B --> A`
|
|
137
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
138
|
+
const a = findBoxRect(ascii, 'A')
|
|
139
|
+
const b = findBoxRect(ascii, 'B')
|
|
140
|
+
expect(a.x0).toBe(0)
|
|
141
|
+
expect(a.x1).toBeLessThan(b.x0)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Regression for a CodeRabbit finding on #972's own PR (thread
|
|
145
|
+
// PRRT_kwDOT5ndxc6hpu-C): two classes with no resolvable parents but the
|
|
146
|
+
// exact same (non-qualifying) parent-id set used to be merged into one
|
|
147
|
+
// joint placement unit the moment the *first* of them was reached in
|
|
148
|
+
// declaration order -- silently pulling an unrelated class declared
|
|
149
|
+
// *between* them ahead of its own position, since it hadn't been placed
|
|
150
|
+
// yet when the merged pair's combined placement consumed the columns it
|
|
151
|
+
// would have occupied.
|
|
152
|
+
it('an unrelated class declared between two same-parent-set siblings with no qualifying parent keeps its own declaration-order position', () => {
|
|
153
|
+
// Q and R share the exact same parent-id set ({P}), but P is at the
|
|
154
|
+
// same level as Q/R (a 3-way mutual-reference cycle), so neither
|
|
155
|
+
// qualifies -- both are "no resolvable parents" blocks on their own.
|
|
156
|
+
// Interloper is a plain, unrelated root declared between Q and R.
|
|
157
|
+
const src = `classDiagram
|
|
158
|
+
class P
|
|
159
|
+
class Q
|
|
160
|
+
class Interloper
|
|
161
|
+
class R
|
|
162
|
+
P --> Q
|
|
163
|
+
P --> R
|
|
164
|
+
Q --> P
|
|
165
|
+
R --> P`
|
|
166
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
167
|
+
const q = findBoxRect(ascii, 'Q')
|
|
168
|
+
const interloper = findBoxRect(ascii, 'Interloper')
|
|
169
|
+
const r = findBoxRect(ascii, 'R')
|
|
170
|
+
// Declared order Q, Interloper, R must survive as rendered left-to-right order.
|
|
171
|
+
expect(q.x1).toBeLessThan(interloper.x0)
|
|
172
|
+
expect(interloper.x1).toBeLessThan(r.x0)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Regression for a CodeRabbit finding on #972's own PR (thread
|
|
176
|
+
// PRRT_kwDOT5ndxc6hpu-M): a multi-member block's desired left edge used
|
|
177
|
+
// to be derived from centering the *padded slot* span (every member's
|
|
178
|
+
// own reach padding, including the outermost edges with no neighbor to
|
|
179
|
+
// separate from) on the shared parent center, rather than the *visible
|
|
180
|
+
// box* span -- so a block whose first member has real reach padding
|
|
181
|
+
// (e.g. its own outgoing relationship carries a long label) and whose
|
|
182
|
+
// last member has none rendered visibly off-center from the parent it
|
|
183
|
+
// was supposed to align under.
|
|
184
|
+
it('a multi-child block with asymmetric reach padding on one child still visually centers on the shared parent', () => {
|
|
185
|
+
const src = `classDiagram
|
|
186
|
+
class Padding {
|
|
187
|
+
+String p0
|
|
188
|
+
+String p1
|
|
189
|
+
+String p2
|
|
190
|
+
+String p3
|
|
191
|
+
}
|
|
192
|
+
class Animal
|
|
193
|
+
class Dog
|
|
194
|
+
class Cat
|
|
195
|
+
class SomewhereElse
|
|
196
|
+
Animal <|-- Dog
|
|
197
|
+
Animal <|-- Cat
|
|
198
|
+
Dog --> SomewhereElse : a moderately long label`
|
|
199
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
200
|
+
const animal = findBoxRect(ascii, 'Animal')
|
|
201
|
+
const dog = findBoxRect(ascii, 'Dog')
|
|
202
|
+
const cat = findBoxRect(ascii, 'Cat')
|
|
203
|
+
const visibleBlockCenter = dog.x0 + Math.floor((cat.x1 - dog.x0 + 1) / 2)
|
|
204
|
+
expect(
|
|
205
|
+
Math.abs(visibleBlockCenter - boxCenter(animal)),
|
|
206
|
+
).toBeLessThanOrEqual(1)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, it, expect } from 'vitest'
|
|
7
7
|
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
8
|
+
import {
|
|
9
|
+
unicodeArrowChar,
|
|
10
|
+
asciiArrowChar,
|
|
11
|
+
} from '../../packages/ascii-renderer/src/draw-arrows.ts'
|
|
12
|
+
import {
|
|
13
|
+
Up,
|
|
14
|
+
Down,
|
|
15
|
+
Left,
|
|
16
|
+
Right,
|
|
17
|
+
UpperRight,
|
|
18
|
+
UpperLeft,
|
|
19
|
+
LowerRight,
|
|
20
|
+
LowerLeft,
|
|
21
|
+
Middle,
|
|
22
|
+
} from '../../packages/ascii-renderer/src/types.ts'
|
|
8
23
|
|
|
9
24
|
describe('ASCII arrow drawing: no-arrow edges', () => {
|
|
10
25
|
it('renders a solid no-arrow edge without an arrowhead in unicode mode', () => {
|
|
@@ -157,6 +172,47 @@ describe('ASCII arrow drawing: bidirectional edges', () => {
|
|
|
157
172
|
})
|
|
158
173
|
})
|
|
159
174
|
|
|
175
|
+
describe('unicodeArrowChar / asciiArrowChar: direction-to-glyph mapping', () => {
|
|
176
|
+
// Direct unit coverage for every Direction, including LowerLeft — the
|
|
177
|
+
// ELK/pathfinder layout this module draws from only produces a diagonal
|
|
178
|
+
// direction via determinePath's rare Case-4 fallback, and every
|
|
179
|
+
// hand-written or gallery diagram that triggers it happens to need the
|
|
180
|
+
// target down-and-*right* of its source, never down-and-left. See
|
|
181
|
+
// issue #1062's drawArrowHead refactor.
|
|
182
|
+
it('maps all four orthogonal directions to filled triangles (unicode)', () => {
|
|
183
|
+
expect(unicodeArrowChar(Up)).toBe('▲')
|
|
184
|
+
expect(unicodeArrowChar(Down)).toBe('▼')
|
|
185
|
+
expect(unicodeArrowChar(Left)).toBe('◄')
|
|
186
|
+
expect(unicodeArrowChar(Right)).toBe('►')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('maps all four diagonal directions to Arrows-block glyphs (unicode)', () => {
|
|
190
|
+
expect(unicodeArrowChar(UpperRight)).toBe('↗')
|
|
191
|
+
expect(unicodeArrowChar(UpperLeft)).toBe('↖')
|
|
192
|
+
expect(unicodeArrowChar(LowerRight)).toBe('↘')
|
|
193
|
+
expect(unicodeArrowChar(LowerLeft)).toBe('↙')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('returns undefined for Middle (no arrowhead glyph)', () => {
|
|
197
|
+
expect(unicodeArrowChar(Middle)).toBeUndefined()
|
|
198
|
+
expect(asciiArrowChar(Middle)).toBeUndefined()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('maps all four orthogonal directions to ASCII carets (ascii mode)', () => {
|
|
202
|
+
expect(asciiArrowChar(Up)).toBe('^')
|
|
203
|
+
expect(asciiArrowChar(Down)).toBe('v')
|
|
204
|
+
expect(asciiArrowChar(Left)).toBe('<')
|
|
205
|
+
expect(asciiArrowChar(Right)).toBe('>')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('has no diagonal glyphs in ASCII mode', () => {
|
|
209
|
+
expect(asciiArrowChar(UpperRight)).toBeUndefined()
|
|
210
|
+
expect(asciiArrowChar(UpperLeft)).toBeUndefined()
|
|
211
|
+
expect(asciiArrowChar(LowerRight)).toBeUndefined()
|
|
212
|
+
expect(asciiArrowChar(LowerLeft)).toBeUndefined()
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
160
216
|
describe('ASCII arrow drawing: upward edge labels', () => {
|
|
161
217
|
it('offsets the label on an edge that routes upward', () => {
|
|
162
218
|
const result = renderMermaidASCII(`
|
|
@@ -19,7 +19,7 @@ describe('ASCII flowchart edges: --o circle-end / --x cross-end glyphs', () => {
|
|
|
19
19
|
A --o B
|
|
20
20
|
`)
|
|
21
21
|
expect(result).toContain('○')
|
|
22
|
-
expect(result).not.toMatch(/[
|
|
22
|
+
expect(result).not.toMatch(/[►◄▲▼◢◣◤◥↖↗↘↙]/)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
it('draws an x glyph for --x in unicode mode', () => {
|
|
@@ -28,7 +28,7 @@ describe('ASCII flowchart edges: --o circle-end / --x cross-end glyphs', () => {
|
|
|
28
28
|
A --x B
|
|
29
29
|
`)
|
|
30
30
|
expect(result).toContain('✕')
|
|
31
|
-
expect(result).not.toMatch(/[
|
|
31
|
+
expect(result).not.toMatch(/[►◄▲▼◢◣◤◥↖↗↘↙]/)
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
it('draws a circle glyph for --o in ascii mode', () => {
|
|
@@ -138,7 +138,7 @@ describe('ASCII flowchart edges: --o circle-end / --x cross-end glyphs', () => {
|
|
|
138
138
|
F --o C
|
|
139
139
|
`)
|
|
140
140
|
expect(result).toContain('○')
|
|
141
|
-
expect(result).not.toMatch(/[
|
|
141
|
+
expect(result).not.toMatch(/[►◄▲▼◢◣◤◥↖↗↘↙]/)
|
|
142
142
|
})
|
|
143
143
|
|
|
144
144
|
// A mixed fan-in (one edge --o, another plain -->) has no single correct
|
|
@@ -280,6 +280,57 @@ describe('ASCII sequence — notes do not collide with an unrelated lifeline', (
|
|
|
280
280
|
expect(carolCol < noteRect.x0 || carolCol > noteRect.x1).toBe(true)
|
|
281
281
|
})
|
|
282
282
|
|
|
283
|
+
// Fixed by issue #992: a single-actor `Note over` centers on that actor's
|
|
284
|
+
// own lifeline (`nx = llX[aIdx] - Math.floor(nWidth / 2)`), but neither
|
|
285
|
+
// half of that width was reserved during layout — so a note wider than
|
|
286
|
+
// the actor's own column spilled into whichever neighbor was closer,
|
|
287
|
+
// exactly the shape cases A-C above already fixed for `left`/`right`
|
|
288
|
+
// notes. This is the exact repro from the issue (two actors, a note over
|
|
289
|
+
// the first one). A *multi*-actor `Note over A,B` is unaffected —
|
|
290
|
+
// spanning both actors is its intended shape (see the "note over two
|
|
291
|
+
// actors" case below), not this bug.
|
|
292
|
+
it("single-actor 'over' note on the first actor does not overlap the next actor's lifeline (issue #992 repro)", () => {
|
|
293
|
+
const src = `sequenceDiagram
|
|
294
|
+
participant A as Alice
|
|
295
|
+
participant B as Bob
|
|
296
|
+
Note over A: ${LONG_LABEL}
|
|
297
|
+
A->>B: hi`
|
|
298
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
299
|
+
const noteRect = findBoxRect(ascii, LONG_LABEL)
|
|
300
|
+
const bobCol = lifelineColumn(ascii, 'Bob')
|
|
301
|
+
expect(bobCol < noteRect.x0 || bobCol > noteRect.x1).toBe(true)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// Same fix, but a middle actor: both halves of the note (left half toward
|
|
305
|
+
// Alice, right half toward Carol) need to be reserved, not just the
|
|
306
|
+
// leftmost-actor case above.
|
|
307
|
+
it("single-actor 'over' note on a middle actor does not overlap either neighbor's lifeline", () => {
|
|
308
|
+
const src = `sequenceDiagram
|
|
309
|
+
participant A as Alice
|
|
310
|
+
participant B as Bob
|
|
311
|
+
participant C as Carol
|
|
312
|
+
A->>B: hi
|
|
313
|
+
Note over B: ${LONG_LABEL}`
|
|
314
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
315
|
+
const noteRect = findBoxRect(ascii, LONG_LABEL)
|
|
316
|
+
const aliceCol = lifelineColumn(ascii, 'Alice')
|
|
317
|
+
const carolCol = lifelineColumn(ascii, 'Carol')
|
|
318
|
+
expect(aliceCol < noteRect.x0 || aliceCol > noteRect.x1).toBe(true)
|
|
319
|
+
expect(carolCol < noteRect.x0 || carolCol > noteRect.x1).toBe(true)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it("short single-actor 'over' note does not overlap the next actor", () => {
|
|
323
|
+
const src = `sequenceDiagram
|
|
324
|
+
participant A as Alice
|
|
325
|
+
participant B as Bob
|
|
326
|
+
Note over A: ok
|
|
327
|
+
A->>B: hi`
|
|
328
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
329
|
+
const noteRect = findBoxRect(ascii, 'ok')
|
|
330
|
+
const bobCol = lifelineColumn(ascii, 'Bob')
|
|
331
|
+
expect(bobCol < noteRect.x0 || bobCol > noteRect.x1).toBe(true)
|
|
332
|
+
})
|
|
333
|
+
|
|
283
334
|
// Regression locks: short notes, which don't hit any of the three bugs
|
|
284
335
|
// above, must keep rendering without collision.
|
|
285
336
|
it('short note left of a middle actor does not overlap its left neighbor', () => {
|
|
@@ -469,5 +469,18 @@ describe('isWideChar', () => {
|
|
|
469
469
|
expect(isWideChar('◥')).toBe(false)
|
|
470
470
|
expect(isWideChar('○')).toBe(false)
|
|
471
471
|
expect(isWideChar('✕')).toBe(false)
|
|
472
|
+
// Diagonal edge-routing arrowheads (draw-arrows.ts's drawArrowHead) as
|
|
473
|
+
// of issue #1062 — JetBrains Mono NL has no glyph for the ◢◣◤◥ set
|
|
474
|
+
// above, so these Arrows-block glyphs are drawn instead.
|
|
475
|
+
expect(isWideChar('↖')).toBe(false)
|
|
476
|
+
expect(isWideChar('↗')).toBe(false)
|
|
477
|
+
expect(isWideChar('↘')).toBe(false)
|
|
478
|
+
expect(isWideChar('↙')).toBe(false)
|
|
479
|
+
// Same Arrows-block misclassification, not drawn by the renderer itself
|
|
480
|
+
// but reachable if typed into node/edge/note label text.
|
|
481
|
+
expect(isWideChar('↔')).toBe(false)
|
|
482
|
+
expect(isWideChar('↕')).toBe(false)
|
|
483
|
+
expect(isWideChar('↩')).toBe(false)
|
|
484
|
+
expect(isWideChar('↪')).toBe(false)
|
|
472
485
|
})
|
|
473
486
|
})
|
|
@@ -130,3 +130,33 @@ describe('buildStyleBlock – font handling', () => {
|
|
|
130
130
|
)
|
|
131
131
|
})
|
|
132
132
|
})
|
|
133
|
+
|
|
134
|
+
describe('buildStyleBlock – mono font (#1061)', () => {
|
|
135
|
+
it('never reaches out to Google Fonts for the .mono rule', () => {
|
|
136
|
+
const block = buildStyleBlock('Inter', true)
|
|
137
|
+
// The main `font` still gets its own Google Fonts @import (unrelated to
|
|
138
|
+
// this regression test), but nothing in the block should ever request
|
|
139
|
+
// JetBrains Mono from a third-party CDN.
|
|
140
|
+
expect(block).not.toContain('fonts.googleapis.com/css2?family=JetBrains')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('embeds a self-hosted @font-face as a base64 woff2 data URI for the mono font', () => {
|
|
144
|
+
const block = buildStyleBlock('Inter', true)
|
|
145
|
+
expect(block).toContain('@font-face')
|
|
146
|
+
expect(block).toContain('data:font/woff2;base64,')
|
|
147
|
+
expect(block).toContain("font-family: 'JetBrains Mono NL';")
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it("leads the .mono rule's font stack with the self-hosted family name", () => {
|
|
151
|
+
const block = buildStyleBlock('Inter', true)
|
|
152
|
+
expect(block).toContain(
|
|
153
|
+
" .mono { font-family: 'JetBrains Mono NL', 'SF Mono', 'Fira Code', ui-monospace, monospace; }",
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('emits neither the @font-face embed nor the .mono rule when hasMonoFont is false', () => {
|
|
158
|
+
const block = buildStyleBlock('Inter', false)
|
|
159
|
+
expect(block).not.toContain('@font-face')
|
|
160
|
+
expect(block).not.toContain('.mono')
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -226,7 +226,7 @@ describe('workspace package manifests', () => {
|
|
|
226
226
|
// no longer `"private": true` — and its own independent build
|
|
227
227
|
// (`packages/<name>/vite.config.ts`, via `vite.config.package.ts`).
|
|
228
228
|
//
|
|
229
|
-
// That is preparation only. The umbrella (`vite.config.lib.ts`) still
|
|
229
|
+
// That is preparation only. The umbrella (`config/vite.config.lib.ts`) still
|
|
230
230
|
// bundles all five packages' source directly into its own `dist/` (they
|
|
231
231
|
// are absent from `isExternal` there, same as before #769) — none of
|
|
232
232
|
// them is a real external `dependencies` entry of `zombie-mermaid`, none
|
|
@@ -234,7 +234,7 @@ describe('workspace package manifests', () => {
|
|
|
234
234
|
// none has ever actually been published to npm. Externalizing them for
|
|
235
235
|
// real is a separate, future PR gated on completing npm's one-time
|
|
236
236
|
// trusted-publishing setup for each new package name — see
|
|
237
|
-
// RELEASING.md's "Future: multi-package publish" section.
|
|
237
|
+
// docs/RELEASING.md's "Future: multi-package publish" section.
|
|
238
238
|
it.each(['core', 'mermaid-parser', 'svg-renderer', 'ascii-renderer', 'mcp'])(
|
|
239
239
|
'%s is publish-ready in shape under the @zombie-mermaid/ scope (not yet externalized or published)',
|
|
240
240
|
(pkg) => {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category-label unquoting tests for the xychart-beta parser (issue #1087).
|
|
3
|
+
*
|
|
4
|
+
* `x-axis [A, "B", C]` categorical items were only `.split(',').map(trim)`,
|
|
5
|
+
* so a quoted item like `"CLI output / logs"` kept its literal quote
|
|
6
|
+
* characters in the rendered label instead of being unquoted the way an
|
|
7
|
+
* axis *title* already is. These tests assert each category item is
|
|
8
|
+
* unquoted the same way, and that a comma embedded inside a quoted item
|
|
9
|
+
* (e.g. `"Jan, Feb"`) is treated as literal text rather than a delimiter.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { parseXYChart as parseXYChartStatements } from '@zombie-mermaid/mermaid-parser'
|
|
13
|
+
import { splitStatements } from '@zombie-mermaid/core'
|
|
14
|
+
|
|
15
|
+
/** Helper to parse — preprocesses text the same way index.ts does */
|
|
16
|
+
function parseXYChart(sourceLines: string[]) {
|
|
17
|
+
return parseXYChartStatements(splitStatements(sourceLines.join('\n')))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('parseXYChart – categorical x-axis quoting', () => {
|
|
21
|
+
it('strips quotes from quoted category items', () => {
|
|
22
|
+
const chart = parseXYChart([
|
|
23
|
+
'xychart-beta',
|
|
24
|
+
'x-axis ["Total used", "CLI output / logs"]',
|
|
25
|
+
'bar [86, 18]',
|
|
26
|
+
])
|
|
27
|
+
expect(chart.xAxis.categories).toEqual(['Total used', 'CLI output / logs'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('handles a mix of quoted and unquoted category items', () => {
|
|
31
|
+
const chart = parseXYChart([
|
|
32
|
+
'xychart-beta',
|
|
33
|
+
'x-axis [Jan, "Feb 2024", Mar]',
|
|
34
|
+
'bar [1, 2, 3]',
|
|
35
|
+
])
|
|
36
|
+
expect(chart.xAxis.categories).toEqual(['Jan', 'Feb 2024', 'Mar'])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('still unquotes categories when an axis title is also present', () => {
|
|
40
|
+
const chart = parseXYChart([
|
|
41
|
+
'xychart-beta',
|
|
42
|
+
'x-axis "Month" ["Jan", "Feb"]',
|
|
43
|
+
'bar [1, 2]',
|
|
44
|
+
])
|
|
45
|
+
expect(chart.xAxis.title).toBe('Month')
|
|
46
|
+
expect(chart.xAxis.categories).toEqual(['Jan', 'Feb'])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('leaves unquoted category items unchanged', () => {
|
|
50
|
+
const chart = parseXYChart([
|
|
51
|
+
'xychart-beta',
|
|
52
|
+
'x-axis [Jan, Feb, Mar]',
|
|
53
|
+
'bar [1, 2, 3]',
|
|
54
|
+
])
|
|
55
|
+
expect(chart.xAxis.categories).toEqual(['Jan', 'Feb', 'Mar'])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('treats a comma inside a quoted item as literal text, not a delimiter', () => {
|
|
59
|
+
const chart = parseXYChart([
|
|
60
|
+
'xychart-beta',
|
|
61
|
+
'x-axis ["Jan, Feb", "Mar"]',
|
|
62
|
+
'bar [1, 2]',
|
|
63
|
+
])
|
|
64
|
+
// A naive split(',') would break this into three pieces ('"Jan',
|
|
65
|
+
// ' Feb"', ' "Mar"') instead of two -- quoting a value that itself
|
|
66
|
+
// contains a comma is exactly the case quoting exists for.
|
|
67
|
+
expect(chart.xAxis.categories).toEqual(['Jan, Feb', 'Mar'])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('handles multiple comma-containing quoted items in one list', () => {
|
|
71
|
+
const chart = parseXYChart([
|
|
72
|
+
'xychart-beta',
|
|
73
|
+
'x-axis ["A, B", "C, D, E", F]',
|
|
74
|
+
'bar [1, 2, 3]',
|
|
75
|
+
])
|
|
76
|
+
expect(chart.xAxis.categories).toEqual(['A, B', 'C, D, E', 'F'])
|
|
77
|
+
})
|
|
78
|
+
})
|
package/src/cli/html-viewer.ts
CHANGED
|
@@ -128,6 +128,11 @@ const PAGE_CSS = `
|
|
|
128
128
|
@media print {
|
|
129
129
|
#toolbar { display: none; }
|
|
130
130
|
#stage { position: static; overflow: visible; }
|
|
131
|
+
/* Genuinely needs !important (zombie-mermaid#1080 audit): the client
|
|
132
|
+
* script's apply() sets #diagram's transform inline on every pan/zoom,
|
|
133
|
+
* so whatever pan/zoom state the viewer was left in is a live inline
|
|
134
|
+
* style this print rule must beat -- there's no static default to
|
|
135
|
+
* hoist it to instead. */
|
|
131
136
|
#diagram { position: static; transform: none !important; }
|
|
132
137
|
}
|
|
133
138
|
`.trim()
|