zombie-mermaid 4.2.2 → 4.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/package.json +9 -7
  2. package/src/__tests__/architecture-integration.test.ts +0 -217
  3. package/src/__tests__/ascii-ansi.test.ts +0 -539
  4. package/src/__tests__/ascii-draw-bundles.test.ts +0 -849
  5. package/src/__tests__/ascii-edge-bundling-routing.test.ts +0 -214
  6. package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +0 -216
  7. package/src/__tests__/ascii-grid-deep-chain.test.ts +0 -64
  8. package/src/__tests__/ascii-hyperlinks.test.ts +0 -364
  9. package/src/__tests__/ascii-nested-subgraph-sibling-order-444.test.ts +0 -108
  10. package/src/__tests__/ascii-nonmember-beside-frame-1283.test.ts +0 -113
  11. package/src/__tests__/ascii-package-boundary.test.ts +0 -291
  12. package/src/__tests__/ascii-parallel-edges-329.test.ts +0 -315
  13. package/src/__tests__/ascii-parallel-lane-border-1230.test.ts +0 -71
  14. package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +0 -282
  15. package/src/__tests__/ascii-stacked-frame-geometry-1285.test.ts +0 -107
  16. package/src/__tests__/ascii-subgraph-bbox.test.ts +0 -139
  17. package/src/__tests__/ascii-subgraph-later-edgeless-301.test.ts +0 -277
  18. package/src/__tests__/browser.test.ts +0 -34
  19. package/src/__tests__/c4-integration.test.ts +0 -161
  20. package/src/__tests__/c4-orientation.test.ts +0 -162
  21. package/src/__tests__/c4-upstream-integration.test.ts +0 -211
  22. package/src/__tests__/class-click-interactions.test.ts +0 -232
  23. package/src/__tests__/class-integration.test.ts +0 -154
  24. package/src/__tests__/class-notes.test.ts +0 -317
  25. package/src/__tests__/class-parser-error-messages.test.ts +0 -162
  26. package/src/__tests__/class-parser.test.ts +0 -347
  27. package/src/__tests__/class-styling.test.ts +0 -485
  28. package/src/__tests__/cli-diagrams.test.ts +0 -106
  29. package/src/__tests__/cli-e2e.test.ts +0 -275
  30. package/src/__tests__/cli-entrypoint.test.ts +0 -146
  31. package/src/__tests__/cli-mcp-dispatch.test.ts +0 -68
  32. package/src/__tests__/cli-mcp.test.ts +0 -44
  33. package/src/__tests__/cli-png.test.ts +0 -35
  34. package/src/__tests__/cli-render.test.ts +0 -988
  35. package/src/__tests__/cli-test-helpers.ts +0 -54
  36. package/src/__tests__/cli-web-dispatch.test.ts +0 -62
  37. package/src/__tests__/cli-web.test.ts +0 -203
  38. package/src/__tests__/cli.test.ts +0 -1027
  39. package/src/__tests__/compound-flat-layout.test.ts +0 -901
  40. package/src/__tests__/direction-override.test.ts +0 -397
  41. package/src/__tests__/edge-approach-direction.test.ts +0 -295
  42. package/src/__tests__/edge-bundling-node-clearance.test.ts +0 -179
  43. package/src/__tests__/elk-layout-cache.test.ts +0 -223
  44. package/src/__tests__/er-click-unsupported.test.ts +0 -69
  45. package/src/__tests__/er-integration.test.ts +0 -531
  46. package/src/__tests__/er-parser-error-messages.test.ts +0 -82
  47. package/src/__tests__/er-parser.test.ts +0 -294
  48. package/src/__tests__/expanded-shapes.test.ts +0 -229
  49. package/src/__tests__/flowchart-back-edge-prereversal-1239.test.ts +0 -212
  50. package/src/__tests__/flowchart-syntax-gaps.test.ts +0 -371
  51. package/src/__tests__/guides-examples.test.ts +0 -181
  52. package/src/__tests__/integration.test.ts +0 -729
  53. package/src/__tests__/interactivity-config.test.ts +0 -723
  54. package/src/__tests__/interactivity-option.test.ts +0 -173
  55. package/src/__tests__/layout-disconnected.test.ts +0 -384
  56. package/src/__tests__/layout-engine.test.ts +0 -91
  57. package/src/__tests__/linkstyle.test.ts +0 -133
  58. package/src/__tests__/mcp-render-ascii-tool.test.ts +0 -95
  59. package/src/__tests__/mcp-render-svg-theme-guard.test.ts +0 -36
  60. package/src/__tests__/mcp-render-svg-tool.test.ts +0 -118
  61. package/src/__tests__/mermaid-entity-codes.test.ts +0 -114
  62. package/src/__tests__/monospace-metrics.test.ts +0 -74
  63. package/src/__tests__/multiline-labels.test.ts +0 -924
  64. package/src/__tests__/nested-subgraph-direction-crossing.test.ts +0 -322
  65. package/src/__tests__/nested-subgraph-edge-container-offset.test.ts +0 -151
  66. package/src/__tests__/package-info.test.ts +0 -51
  67. package/src/__tests__/parser.test.ts +0 -1544
  68. package/src/__tests__/render-options-overrides.test.ts +0 -257
  69. package/src/__tests__/render-options-per-type.test.ts +0 -178
  70. package/src/__tests__/renderer.test.ts +0 -743
  71. package/src/__tests__/resolve-colors.test.ts +0 -448
  72. package/src/__tests__/self-loop-edges.test.ts +0 -112
  73. package/src/__tests__/sequence-activation-check.test.ts +0 -219
  74. package/src/__tests__/sequence-integration.test.ts +0 -312
  75. package/src/__tests__/sequence-layout.test.ts +0 -1103
  76. package/src/__tests__/sequence-parser-syntax-errors.test.ts +0 -179
  77. package/src/__tests__/sequence-parser.test.ts +0 -821
  78. package/src/__tests__/start-marker-only-links-1386.test.ts +0 -104
  79. package/src/__tests__/state-composite-order-1287.test.ts +0 -132
  80. package/src/__tests__/state-flow-order-1240.test.ts +0 -113
  81. package/src/__tests__/state-styling.test.ts +0 -150
  82. package/src/__tests__/statements.test.ts +0 -264
  83. package/src/__tests__/styles.test.ts +0 -220
  84. package/src/__tests__/subgraph-title-edge-gap-1239.test.ts +0 -238
  85. package/src/__tests__/svg-accessible-name-conformance.test.ts +0 -276
  86. package/src/__tests__/svg-accessible-name.test.ts +0 -421
  87. package/src/__tests__/svg-csp-style-options.test.ts +0 -427
  88. package/src/__tests__/workspace-package-boundaries.test.ts +0 -336
  89. package/src/__tests__/xychart-integration.test.ts +0 -137
  90. package/src/__tests__/xychart-layout.test.ts +0 -205
  91. package/src/__tests__/xychart-parser-categories.test.ts +0 -78
  92. package/src/__tests__/xychart-parser-errors.test.ts +0 -95
@@ -1,539 +0,0 @@
1
- /**
2
- * Tests for the ANSI/HTML color utilities in src/ascii/ansi.ts.
3
- */
4
- import { describe, it, expect, afterEach, vi } from 'vitest'
5
- import {
6
- DEFAULT_ASCII_THEME,
7
- diagramColorsToAsciiTheme,
8
- detectColorMode,
9
- getAnsiColor,
10
- getAnsiReset,
11
- colorizeChar,
12
- colorizeLine,
13
- colorizeText,
14
- } from '../../packages/ascii-renderer/src/ansi.ts'
15
- import type {
16
- CharRole,
17
- AsciiTheme,
18
- ColorMode,
19
- } from '../../packages/ascii-renderer/src/types.ts'
20
- import type { DiagramColors } from '@zombie-mermaid/core'
21
-
22
- const RESET = '\x1b[0m'
23
-
24
- const theme: AsciiTheme = {
25
- fg: '#27272a',
26
- border: '#a1a1aa',
27
- line: '#71717a',
28
- arrow: '#52525b',
29
- }
30
-
31
- describe('diagramColorsToAsciiTheme', () => {
32
- it('derives line/border/arrow via color-mix when no enrichment colors are set', () => {
33
- const colors: DiagramColors = { bg: '#ffffff', fg: '#000000' }
34
- const result = diagramColorsToAsciiTheme(colors)
35
- expect(result).toEqual({
36
- fg: '#000000',
37
- // mixColors(fg=#000000, bg=#ffffff, 50%) -> 0*.5 + 255*.5 = 128 = 0x80
38
- border: '#cccccc',
39
- line: '#808080',
40
- // mixColors(fg=#000000, bg=#ffffff, 85%) -> 0*.85 + 255*.15 = 38.25 -> 38 = 0x26
41
- arrow: '#262626',
42
- accent: undefined,
43
- bg: '#ffffff',
44
- corner: '#808080',
45
- junction: '#cccccc',
46
- })
47
- })
48
-
49
- it('uses explicit enrichment colors (line/border/accent) directly, bypassing color-mix', () => {
50
- const colors: DiagramColors = {
51
- bg: '#ffffff',
52
- fg: '#000000',
53
- line: '#111111',
54
- border: '#222222',
55
- accent: '#333333',
56
- }
57
- const result = diagramColorsToAsciiTheme(colors)
58
- expect(result).toEqual({
59
- fg: '#000000',
60
- border: '#222222',
61
- line: '#111111',
62
- arrow: '#333333',
63
- accent: '#333333',
64
- bg: '#ffffff',
65
- corner: '#111111',
66
- junction: '#222222',
67
- })
68
- })
69
- })
70
-
71
- describe('DEFAULT_ASCII_THEME', () => {
72
- it('matches the documented zinc palette', () => {
73
- expect(DEFAULT_ASCII_THEME).toEqual({
74
- fg: '#27272a',
75
- border: '#a1a1aa',
76
- line: '#71717a',
77
- arrow: '#52525b',
78
- corner: '#71717a',
79
- junction: '#a1a1aa',
80
- })
81
- })
82
- })
83
-
84
- describe('detectColorMode', () => {
85
- const originalIsTTY = process.stdout.isTTY
86
-
87
- afterEach(() => {
88
- Object.defineProperty(process.stdout, 'isTTY', {
89
- value: originalIsTTY,
90
- configurable: true,
91
- })
92
- vi.unstubAllEnvs()
93
- })
94
-
95
- function setTTY(value: boolean | undefined) {
96
- Object.defineProperty(process.stdout, 'isTTY', {
97
- value,
98
- configurable: true,
99
- })
100
- }
101
-
102
- it('returns none when stdout is not a TTY (piped/redirected)', () => {
103
- setTTY(false)
104
- expect(detectColorMode()).toBe('none')
105
- })
106
-
107
- it('returns truecolor when COLORTERM=truecolor', () => {
108
- setTTY(true)
109
- vi.stubEnv('COLORTERM', 'truecolor')
110
- vi.stubEnv('TERM', 'xterm')
111
- expect(detectColorMode()).toBe('truecolor')
112
- })
113
-
114
- it('returns truecolor for COLORTERM=24bit, case-insensitively', () => {
115
- setTTY(true)
116
- vi.stubEnv('COLORTERM', '24BIT')
117
- vi.stubEnv('TERM', 'xterm')
118
- expect(detectColorMode()).toBe('truecolor')
119
- })
120
-
121
- it('returns ansi256 when TERM contains "256color"', () => {
122
- setTTY(true)
123
- vi.stubEnv('COLORTERM', '')
124
- vi.stubEnv('TERM', 'xterm-256color')
125
- expect(detectColorMode()).toBe('ansi256')
126
- })
127
-
128
- it('returns ansi256 when TERM contains "256" without a "color" suffix', () => {
129
- setTTY(true)
130
- vi.stubEnv('COLORTERM', '')
131
- vi.stubEnv('TERM', 'screen-256')
132
- expect(detectColorMode()).toBe('ansi256')
133
- })
134
-
135
- it('returns ansi16 for a basic, non-dumb TERM', () => {
136
- setTTY(true)
137
- vi.stubEnv('COLORTERM', '')
138
- vi.stubEnv('TERM', 'xterm')
139
- expect(detectColorMode()).toBe('ansi16')
140
- })
141
-
142
- it('returns none when TERM=dumb', () => {
143
- setTTY(true)
144
- vi.stubEnv('COLORTERM', '')
145
- vi.stubEnv('TERM', 'dumb')
146
- expect(detectColorMode()).toBe('none')
147
- })
148
-
149
- it('returns none when TERM is unset', () => {
150
- setTTY(true)
151
- vi.stubEnv('COLORTERM', '')
152
- vi.stubEnv('TERM', '')
153
- expect(detectColorMode()).toBe('none')
154
- })
155
-
156
- it('returns none when COLORTERM and TERM are entirely absent from env (not just empty)', () => {
157
- setTTY(true)
158
- const hadColorTerm = 'COLORTERM' in process.env
159
- const hadTerm = 'TERM' in process.env
160
- const prevColorTerm = process.env.COLORTERM
161
- const prevTerm = process.env.TERM
162
- delete process.env.COLORTERM
163
- delete process.env.TERM
164
- try {
165
- expect(detectColorMode()).toBe('none')
166
- } finally {
167
- if (hadColorTerm) process.env.COLORTERM = prevColorTerm
168
- if (hadTerm) process.env.TERM = prevTerm
169
- }
170
- })
171
-
172
- it('returns html when no process object exists but document is defined', () => {
173
- // Vitest's own RPC layer relies on the real `process` object (via
174
- // process.nextTick) outside this synchronous callback, so it must be
175
- // restored before this test body returns rather than in afterEach.
176
- const realProcess = globalThis.process
177
- // @ts-expect-error - simulating a non-Node (browser) environment
178
- globalThis.process = undefined
179
- // @ts-expect-error - `document` is not declared in this Node test env
180
- globalThis.document = {}
181
- try {
182
- expect(detectColorMode()).toBe('html')
183
- } finally {
184
- globalThis.process = realProcess
185
- // @ts-expect-error - clean up the simulated global
186
- delete globalThis.document
187
- }
188
- })
189
-
190
- it('returns none when neither process nor document exist', () => {
191
- const realProcess = globalThis.process
192
- // @ts-expect-error - simulating an environment with neither process nor document
193
- globalThis.process = undefined
194
- try {
195
- expect(detectColorMode()).toBe('none')
196
- } finally {
197
- globalThis.process = realProcess
198
- }
199
- })
200
- })
201
-
202
- describe('getAnsiColor', () => {
203
- it('returns an empty string for mode "none"', () => {
204
- expect(getAnsiColor('text', theme, 'none')).toBe('')
205
- })
206
-
207
- it('generates a 24-bit truecolor escape sequence', () => {
208
- expect(getAnsiColor('text', theme, 'truecolor')).toBe('\x1b[38;2;39;39;42m')
209
- })
210
-
211
- it('resolves the "arrow" role to theme.arrow', () => {
212
- expect(getAnsiColor('arrow', theme, 'truecolor')).toBe(
213
- getAnsiColor('text', { ...theme, fg: theme.arrow }, 'truecolor'),
214
- )
215
- })
216
-
217
- it('supports 3-digit shorthand hex colors', () => {
218
- expect(getAnsiColor('text', { ...theme, fg: '#fff' }, 'truecolor')).toBe(
219
- '\x1b[38;2;255;255;255m',
220
- )
221
- })
222
-
223
- it('generates a 256-color escape sequence using the 6x6x6 color cube', () => {
224
- // #ff0000 is far from grayscale, so it uses the cube: 16 + 36*5 + 6*0 + 0 = 196
225
- expect(getAnsiColor('text', { ...theme, fg: '#ff0000' }, 'ansi256')).toBe(
226
- '\x1b[38;5;196m',
227
- )
228
- })
229
-
230
- it('generates a 256-color escape sequence for a channel in the mid cube range [48,115)', () => {
231
- // #500000: r=80 falls in the [48,115) bucket -> index 1; g=b=0 -> index 0
232
- // index = 16 + 36*1 + 6*0 + 0 = 52
233
- expect(getAnsiColor('text', { ...theme, fg: '#500000' }, 'ansi256')).toBe(
234
- '\x1b[38;5;52m',
235
- )
236
- })
237
-
238
- it('generates a 256-color grayscale escape sequence when r/g/b are close', () => {
239
- // #808080 is grayscale: round((128/255)*23) = 12 -> 232 + 12 = 244
240
- expect(getAnsiColor('text', { ...theme, fg: '#808080' }, 'ansi256')).toBe(
241
- '\x1b[38;5;244m',
242
- )
243
- })
244
-
245
- it('clamps the 256-color grayscale ramp at white and black', () => {
246
- expect(getAnsiColor('text', { ...theme, fg: '#ffffff' }, 'ansi256')).toBe(
247
- '\x1b[38;5;255m',
248
- )
249
- expect(getAnsiColor('text', { ...theme, fg: '#000000' }, 'ansi256')).toBe(
250
- '\x1b[38;5;232m',
251
- )
252
- })
253
-
254
- it('maps saturated colors to the matching 16-color ANSI code', () => {
255
- expect(getAnsiColor('text', { ...theme, fg: '#ff0000' }, 'ansi16')).toBe(
256
- '\x1b[91m', // red, bright (luma < 100)
257
- )
258
- expect(getAnsiColor('text', { ...theme, fg: '#00ff00' }, 'ansi16')).toBe(
259
- '\x1b[32m', // green, not bright (luma > 100)
260
- )
261
- expect(getAnsiColor('text', { ...theme, fg: '#ffff00' }, 'ansi16')).toBe(
262
- '\x1b[33m', // yellow
263
- )
264
- expect(getAnsiColor('text', { ...theme, fg: '#0000ff' }, 'ansi16')).toBe(
265
- '\x1b[94m', // blue, bright
266
- )
267
- expect(getAnsiColor('text', { ...theme, fg: '#ff00ff' }, 'ansi16')).toBe(
268
- '\x1b[35m', // magenta
269
- )
270
- expect(getAnsiColor('text', { ...theme, fg: '#00ffff' }, 'ansi16')).toBe(
271
- '\x1b[36m', // cyan
272
- )
273
- })
274
-
275
- it('maps white to bright white and black to bright black in 16-color mode', () => {
276
- expect(getAnsiColor('text', { ...theme, fg: '#ffffff' }, 'ansi16')).toBe(
277
- '\x1b[37m',
278
- )
279
- expect(getAnsiColor('text', { ...theme, fg: '#000000' }, 'ansi16')).toBe(
280
- '\x1b[90m',
281
- )
282
- })
283
-
284
- it('defaults an ambiguous gray to white in 16-color mode', () => {
285
- expect(getAnsiColor('text', { ...theme, fg: '#808080' }, 'ansi16')).toBe(
286
- '\x1b[37m',
287
- )
288
- })
289
-
290
- it('returns an empty string for an unhandled mode (html)', () => {
291
- expect(getAnsiColor('text', theme, 'html')).toBe('')
292
- })
293
-
294
- it('accepts a bare hex digit string with no leading # (backward compatibility)', () => {
295
- expect(getAnsiColor('text', { ...theme, fg: 'ff0000' }, 'truecolor')).toBe(
296
- '\x1b[38;2;255;0;0m',
297
- )
298
- })
299
-
300
- it('falls back to black for a color that is neither a valid hex nor #-prefixed', () => {
301
- // parseHex accepts a bare hex digit string for backward compatibility
302
- // (prepending '#' itself), but something that isn't valid hex either
303
- // way must fall back to black rather than emit a NaN escape sequence.
304
- expect(getAnsiColor('text', { ...theme, fg: 'zzzzzz' }, 'truecolor')).toBe(
305
- '\x1b[38;2;0;0;0m',
306
- )
307
- })
308
-
309
- it('falls back to line/border for corner/junction roles when unset', () => {
310
- const noCornerJunction: AsciiTheme = {
311
- fg: '#27272a',
312
- border: '#a1a1aa',
313
- line: '#71717a',
314
- arrow: '#52525b',
315
- }
316
- expect(getAnsiColor('corner', noCornerJunction, 'truecolor')).toBe(
317
- getAnsiColor('line', noCornerJunction, 'truecolor'),
318
- )
319
- expect(getAnsiColor('junction', noCornerJunction, 'truecolor')).toBe(
320
- getAnsiColor('border', noCornerJunction, 'truecolor'),
321
- )
322
- })
323
-
324
- it('uses explicit corner/junction colors when set', () => {
325
- const withCornerJunction: AsciiTheme = {
326
- ...theme,
327
- corner: '#111111',
328
- junction: '#222222',
329
- }
330
- expect(getAnsiColor('corner', withCornerJunction, 'truecolor')).toBe(
331
- '\x1b[38;2;17;17;17m',
332
- )
333
- expect(getAnsiColor('junction', withCornerJunction, 'truecolor')).toBe(
334
- '\x1b[38;2;34;34;34m',
335
- )
336
- })
337
-
338
- it('falls back to fg for an unrecognized role', () => {
339
- // Deliberately out-of-domain: CharRole is a closed union, so this cast
340
- // is the only way to exercise the runtime fallback for a bad value.
341
- expect(getAnsiColor('bogus' as CharRole, theme, 'truecolor')).toBe(
342
- getAnsiColor('text', theme, 'truecolor'),
343
- )
344
- })
345
- })
346
-
347
- describe('getAnsiReset', () => {
348
- it('returns an empty string for mode "none"', () => {
349
- expect(getAnsiReset('none')).toBe('')
350
- })
351
-
352
- it('returns the ANSI reset sequence for color modes', () => {
353
- expect(getAnsiReset('truecolor')).toBe(RESET)
354
- expect(getAnsiReset('ansi16')).toBe(RESET)
355
- })
356
- })
357
-
358
- describe('colorizeChar', () => {
359
- it('returns the character unchanged for mode "none"', () => {
360
- expect(colorizeChar('A', 'text', theme, 'none')).toBe('A')
361
- })
362
-
363
- it('returns the character unchanged when role is null', () => {
364
- expect(colorizeChar('A', null, theme, 'truecolor')).toBe('A')
365
- })
366
-
367
- it('returns whitespace unchanged even with a valid role and mode', () => {
368
- expect(colorizeChar(' ', 'text', theme, 'truecolor')).toBe(' ')
369
- })
370
-
371
- it('wraps a character with its role color and a reset', () => {
372
- expect(colorizeChar('A', 'text', theme, 'truecolor')).toBe(
373
- `\x1b[38;2;39;39;42mA${RESET}`,
374
- )
375
- })
376
- })
377
-
378
- describe('colorizeLine', () => {
379
- it('joins characters unchanged for mode "none"', () => {
380
- expect(
381
- colorizeLine(['A', 'B', 'C'], ['text', 'text', 'border'], theme, 'none'),
382
- ).toBe('ABC')
383
- })
384
-
385
- it('returns an empty string for an empty line', () => {
386
- expect(colorizeLine([], [], theme, 'truecolor')).toBe('')
387
- })
388
-
389
- it('groups consecutive same-role characters into a single escape sequence', () => {
390
- const result = colorizeLine(
391
- ['A', 'B', ' ', 'C'],
392
- ['text', 'text', null, 'border'],
393
- theme,
394
- 'truecolor',
395
- )
396
- expect(result).toBe(
397
- `\x1b[38;2;39;39;42mAB${RESET} \x1b[38;2;161;161;170mC${RESET}`,
398
- )
399
- })
400
-
401
- it('starts a new group when the role changes without whitespace between', () => {
402
- const result = colorizeLine(
403
- ['A', 'B'],
404
- ['text', 'border'],
405
- theme,
406
- 'truecolor',
407
- )
408
- expect(result).toBe(
409
- `\x1b[38;2;39;39;42mA${RESET}\x1b[38;2;161;161;170mB${RESET}`,
410
- )
411
- })
412
-
413
- it('emits an unroled (null) run as raw text, mid-line', () => {
414
- const result = colorizeLine(['X', 'Y'], [null, 'text'], theme, 'truecolor')
415
- expect(result).toBe(`X\x1b[38;2;39;39;42mY${RESET}`)
416
- })
417
-
418
- it('emits a trailing unroled (null) run as raw text', () => {
419
- const result = colorizeLine(['A', 'B'], ['text', null], theme, 'truecolor')
420
- expect(result).toBe(`\x1b[38;2;39;39;42mA${RESET}B`)
421
- })
422
-
423
- it('emits an unroled (null) run as raw text when flushed by whitespace', () => {
424
- const result = colorizeLine(
425
- ['X', ' ', 'Y'],
426
- [null, null, 'text'],
427
- theme,
428
- 'truecolor',
429
- )
430
- expect(result).toBe(`X \x1b[38;2;39;39;42mY${RESET}`)
431
- })
432
-
433
- it('handles leading/consecutive whitespace with nothing buffered yet', () => {
434
- const result = colorizeLine(
435
- [' ', ' ', 'A'],
436
- [null, null, 'text'],
437
- theme,
438
- 'truecolor',
439
- )
440
- expect(result).toBe(` \x1b[38;2;39;39;42mA${RESET}`)
441
- })
442
-
443
- it('delegates to HTML rendering for mode "html"', () => {
444
- const result = colorizeLine(['<', '>'], [null, null], theme, 'html')
445
- expect(result).toBe('&lt;&gt;')
446
- })
447
- })
448
-
449
- describe('colorizeLine — HTML mode', () => {
450
- it('wraps a colored run in a <span> with an inline color style', () => {
451
- const result = colorizeLine(['A'], ['border'], theme, 'html')
452
- expect(result).toBe(`<span style="color:${theme.border}">A</span>`)
453
- })
454
-
455
- it('escapes HTML-significant characters in both colored and uncolored runs', () => {
456
- const colored = colorizeLine(['&'], ['text'], theme, 'html')
457
- expect(colored).toBe(`<span style="color:${theme.fg}">&amp;</span>`)
458
-
459
- const uncolored = colorizeLine(
460
- ['<', '&', '>'],
461
- [null, null, null],
462
- theme,
463
- 'html',
464
- )
465
- expect(uncolored).toBe('&lt;&amp;&gt;')
466
- })
467
-
468
- it('emits whitespace bare, breaking runs without wrapping the space itself', () => {
469
- const result = colorizeLine(
470
- ['a', ' ', 'b'],
471
- ['text', 'text', 'text'],
472
- theme,
473
- 'html',
474
- )
475
- expect(result).toBe(
476
- `<span style="color:${theme.fg}">a</span> <span style="color:${theme.fg}">b</span>`,
477
- )
478
- })
479
-
480
- it('backs a run of full blocks with its own color so abutting glyphs cannot seam', () => {
481
- const result = colorizeLine(
482
- ['█', '█', '█'],
483
- ['border', 'border', 'border'],
484
- theme,
485
- 'html',
486
- )
487
- expect(result).toBe(
488
- `<span style="color:${theme.border};background:${theme.border}">███</span>`,
489
- )
490
- })
491
-
492
- it('does not back a run that mixes blocks with other characters', () => {
493
- const result = colorizeLine(['█', 'a'], ['border', 'border'], theme, 'html')
494
- expect(result).toBe(`<span style="color:${theme.border}">█a</span>`)
495
- })
496
-
497
- it('does not back half blocks, which must not fill the whole cell', () => {
498
- const result = colorizeLine(['▄'], ['border'], theme, 'html')
499
- expect(result).toBe(`<span style="color:${theme.border}">▄</span>`)
500
- })
501
- })
502
-
503
- describe('colorizeText', () => {
504
- it('returns text unchanged for mode "none"', () => {
505
- expect(colorizeText('hello', '#ff0000', 'none')).toBe('hello')
506
- })
507
-
508
- it('returns an empty string unchanged regardless of mode', () => {
509
- expect(colorizeText('', '#ff0000', 'truecolor')).toBe('')
510
- })
511
-
512
- it('wraps text in an HTML span for mode "html"', () => {
513
- expect(colorizeText('hi', '#ff0000', 'html')).toBe(
514
- '<span style="color:#ff0000">hi</span>',
515
- )
516
- })
517
-
518
- it('wraps text with a truecolor escape sequence and reset', () => {
519
- expect(colorizeText('hi', '#27272a', 'truecolor')).toBe(
520
- `\x1b[38;2;39;39;42mhi${RESET}`,
521
- )
522
- })
523
-
524
- it('wraps text with a 256-color escape sequence and reset', () => {
525
- expect(colorizeText('hi', '#ff0000', 'ansi256')).toBe(
526
- `\x1b[38;5;196mhi${RESET}`,
527
- )
528
- })
529
-
530
- it('wraps text with a 16-color escape sequence and reset', () => {
531
- expect(colorizeText('hi', '#ff0000', 'ansi16')).toBe(`\x1b[91mhi${RESET}`)
532
- })
533
-
534
- it('returns text unchanged for an unhandled mode', () => {
535
- // Deliberately out-of-domain: ColorMode is a closed union, so this cast
536
- // is the only way to exercise the runtime fallback for a bad value.
537
- expect(colorizeText('hi', '#ff0000', 'bogus' as ColorMode)).toBe('hi')
538
- })
539
- })