@mdxui/terminal 2.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/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI to CSS Mapping Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: Tests for converting ANSI escape codes to CSS styles.
|
|
5
|
+
* All tests should FAIL initially because ansiToCSS/parseAnsiToSpans doesn't exist yet.
|
|
6
|
+
*
|
|
7
|
+
* This enables web rendering of ANSI terminal output in browser terminals.
|
|
8
|
+
* The converter parses ANSI escape sequences and produces:
|
|
9
|
+
* - CSS color values (names, hex, rgb())
|
|
10
|
+
* - CSS text decoration (bold, italic, underline)
|
|
11
|
+
* - Structured spans with inline styles
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from 'vitest'
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types for ANSI to CSS Converter (contract definition)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* CSS style properties for a styled text span
|
|
23
|
+
*/
|
|
24
|
+
interface CSSStyleProperties {
|
|
25
|
+
color?: string
|
|
26
|
+
backgroundColor?: string
|
|
27
|
+
fontWeight?: 'bold' | 'normal'
|
|
28
|
+
fontStyle?: 'italic' | 'normal'
|
|
29
|
+
textDecoration?: 'underline' | 'line-through' | 'none'
|
|
30
|
+
opacity?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A span of text with associated CSS styles
|
|
35
|
+
*/
|
|
36
|
+
interface StyledSpan {
|
|
37
|
+
text: string
|
|
38
|
+
style: CSSStyleProperties
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Result of parsing ANSI string to CSS
|
|
43
|
+
*/
|
|
44
|
+
interface ANSIToCSSResult {
|
|
45
|
+
spans: StyledSpan[]
|
|
46
|
+
plainText: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Test Suite: ANSI 16 Colors to CSS
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
describe('ansiToCSS', () => {
|
|
54
|
+
describe('ANSI 16 foreground colors to CSS color names', () => {
|
|
55
|
+
it('converts ANSI black (30) to CSS color black', async () => {
|
|
56
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
57
|
+
|
|
58
|
+
const ansiString = '\x1b[30mBlack text\x1b[0m'
|
|
59
|
+
const result = ansiToCSS(ansiString)
|
|
60
|
+
|
|
61
|
+
expect(result.spans[0].style.color).toBe('black')
|
|
62
|
+
expect(result.spans[0].text).toBe('Black text')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('converts ANSI red (31) to CSS color red', async () => {
|
|
66
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
67
|
+
|
|
68
|
+
const ansiString = '\x1b[31mRed text\x1b[0m'
|
|
69
|
+
const result = ansiToCSS(ansiString)
|
|
70
|
+
|
|
71
|
+
expect(result.spans[0].style.color).toBe('red')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('converts ANSI green (32) to CSS color green', async () => {
|
|
75
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
76
|
+
|
|
77
|
+
const ansiString = '\x1b[32mGreen text\x1b[0m'
|
|
78
|
+
const result = ansiToCSS(ansiString)
|
|
79
|
+
|
|
80
|
+
expect(result.spans[0].style.color).toBe('green')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('converts ANSI yellow (33) to CSS color yellow', async () => {
|
|
84
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
85
|
+
|
|
86
|
+
const ansiString = '\x1b[33mYellow text\x1b[0m'
|
|
87
|
+
const result = ansiToCSS(ansiString)
|
|
88
|
+
|
|
89
|
+
expect(result.spans[0].style.color).toBe('yellow')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('converts ANSI blue (34) to CSS color blue', async () => {
|
|
93
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
94
|
+
|
|
95
|
+
const ansiString = '\x1b[34mBlue text\x1b[0m'
|
|
96
|
+
const result = ansiToCSS(ansiString)
|
|
97
|
+
|
|
98
|
+
expect(result.spans[0].style.color).toBe('blue')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('converts ANSI magenta (35) to CSS color magenta', async () => {
|
|
102
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
103
|
+
|
|
104
|
+
const ansiString = '\x1b[35mMagenta text\x1b[0m'
|
|
105
|
+
const result = ansiToCSS(ansiString)
|
|
106
|
+
|
|
107
|
+
expect(result.spans[0].style.color).toBe('magenta')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('converts ANSI cyan (36) to CSS color cyan', async () => {
|
|
111
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
112
|
+
|
|
113
|
+
const ansiString = '\x1b[36mCyan text\x1b[0m'
|
|
114
|
+
const result = ansiToCSS(ansiString)
|
|
115
|
+
|
|
116
|
+
expect(result.spans[0].style.color).toBe('cyan')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('converts ANSI white (37) to CSS color white', async () => {
|
|
120
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
121
|
+
|
|
122
|
+
const ansiString = '\x1b[37mWhite text\x1b[0m'
|
|
123
|
+
const result = ansiToCSS(ansiString)
|
|
124
|
+
|
|
125
|
+
expect(result.spans[0].style.color).toBe('white')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Bright/High-Intensity Foreground Colors (90-97)
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
describe('ANSI bright foreground colors to CSS', () => {
|
|
134
|
+
it('converts ANSI bright black/gray (90) to CSS color', async () => {
|
|
135
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
136
|
+
|
|
137
|
+
const ansiString = '\x1b[90mGray text\x1b[0m'
|
|
138
|
+
const result = ansiToCSS(ansiString)
|
|
139
|
+
|
|
140
|
+
// Bright black is typically gray - could be 'gray' or '#808080'
|
|
141
|
+
expect(result.spans[0].style.color).toBe('gray')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('converts ANSI bright red (91) to CSS color', async () => {
|
|
145
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
146
|
+
|
|
147
|
+
const ansiString = '\x1b[91mBright red\x1b[0m'
|
|
148
|
+
const result = ansiToCSS(ansiString)
|
|
149
|
+
|
|
150
|
+
// Bright colors use hex values for precision
|
|
151
|
+
expect(result.spans[0].style.color).toBe('#ff5555')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('converts ANSI bright green (92) to CSS color', async () => {
|
|
155
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
156
|
+
|
|
157
|
+
const ansiString = '\x1b[92mBright green\x1b[0m'
|
|
158
|
+
const result = ansiToCSS(ansiString)
|
|
159
|
+
|
|
160
|
+
expect(result.spans[0].style.color).toBe('#55ff55')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('converts ANSI bright yellow (93) to CSS color', async () => {
|
|
164
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
165
|
+
|
|
166
|
+
const ansiString = '\x1b[93mBright yellow\x1b[0m'
|
|
167
|
+
const result = ansiToCSS(ansiString)
|
|
168
|
+
|
|
169
|
+
expect(result.spans[0].style.color).toBe('#ffff55')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('converts ANSI bright blue (94) to CSS color', async () => {
|
|
173
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
174
|
+
|
|
175
|
+
const ansiString = '\x1b[94mBright blue\x1b[0m'
|
|
176
|
+
const result = ansiToCSS(ansiString)
|
|
177
|
+
|
|
178
|
+
expect(result.spans[0].style.color).toBe('#5555ff')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('converts ANSI bright magenta (95) to CSS color', async () => {
|
|
182
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
183
|
+
|
|
184
|
+
const ansiString = '\x1b[95mBright magenta\x1b[0m'
|
|
185
|
+
const result = ansiToCSS(ansiString)
|
|
186
|
+
|
|
187
|
+
expect(result.spans[0].style.color).toBe('#ff55ff')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('converts ANSI bright cyan (96) to CSS color', async () => {
|
|
191
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
192
|
+
|
|
193
|
+
const ansiString = '\x1b[96mBright cyan\x1b[0m'
|
|
194
|
+
const result = ansiToCSS(ansiString)
|
|
195
|
+
|
|
196
|
+
expect(result.spans[0].style.color).toBe('#55ffff')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('converts ANSI bright white (97) to CSS color', async () => {
|
|
200
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
201
|
+
|
|
202
|
+
const ansiString = '\x1b[97mBright white\x1b[0m'
|
|
203
|
+
const result = ansiToCSS(ansiString)
|
|
204
|
+
|
|
205
|
+
expect(result.spans[0].style.color).toBe('#ffffff')
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Background Colors (40-47)
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
describe('ANSI 16 background colors to CSS', () => {
|
|
214
|
+
it('converts ANSI black background (40) to CSS', async () => {
|
|
215
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
216
|
+
|
|
217
|
+
const ansiString = '\x1b[40mBlack bg\x1b[0m'
|
|
218
|
+
const result = ansiToCSS(ansiString)
|
|
219
|
+
|
|
220
|
+
expect(result.spans[0].style.backgroundColor).toBe('black')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('converts ANSI red background (41) to CSS', async () => {
|
|
224
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
225
|
+
|
|
226
|
+
const ansiString = '\x1b[41mRed bg\x1b[0m'
|
|
227
|
+
const result = ansiToCSS(ansiString)
|
|
228
|
+
|
|
229
|
+
expect(result.spans[0].style.backgroundColor).toBe('red')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('converts ANSI green background (42) to CSS', async () => {
|
|
233
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
234
|
+
|
|
235
|
+
const ansiString = '\x1b[42mGreen bg\x1b[0m'
|
|
236
|
+
const result = ansiToCSS(ansiString)
|
|
237
|
+
|
|
238
|
+
expect(result.spans[0].style.backgroundColor).toBe('green')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('converts ANSI yellow background (43) to CSS', async () => {
|
|
242
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
243
|
+
|
|
244
|
+
const ansiString = '\x1b[43mYellow bg\x1b[0m'
|
|
245
|
+
const result = ansiToCSS(ansiString)
|
|
246
|
+
|
|
247
|
+
expect(result.spans[0].style.backgroundColor).toBe('yellow')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('converts ANSI blue background (44) to CSS', async () => {
|
|
251
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
252
|
+
|
|
253
|
+
const ansiString = '\x1b[44mBlue bg\x1b[0m'
|
|
254
|
+
const result = ansiToCSS(ansiString)
|
|
255
|
+
|
|
256
|
+
expect(result.spans[0].style.backgroundColor).toBe('blue')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('converts ANSI magenta background (45) to CSS', async () => {
|
|
260
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
261
|
+
|
|
262
|
+
const ansiString = '\x1b[45mMagenta bg\x1b[0m'
|
|
263
|
+
const result = ansiToCSS(ansiString)
|
|
264
|
+
|
|
265
|
+
expect(result.spans[0].style.backgroundColor).toBe('magenta')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('converts ANSI cyan background (46) to CSS', async () => {
|
|
269
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
270
|
+
|
|
271
|
+
const ansiString = '\x1b[46mCyan bg\x1b[0m'
|
|
272
|
+
const result = ansiToCSS(ansiString)
|
|
273
|
+
|
|
274
|
+
expect(result.spans[0].style.backgroundColor).toBe('cyan')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('converts ANSI white background (47) to CSS', async () => {
|
|
278
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
279
|
+
|
|
280
|
+
const ansiString = '\x1b[47mWhite bg\x1b[0m'
|
|
281
|
+
const result = ansiToCSS(ansiString)
|
|
282
|
+
|
|
283
|
+
expect(result.spans[0].style.backgroundColor).toBe('white')
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// ANSI 256 Colors to CSS Hex Values
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
describe('ANSI 256 colors to CSS hex values', () => {
|
|
292
|
+
it('converts ANSI 256 foreground color 196 (red) to hex', async () => {
|
|
293
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
294
|
+
|
|
295
|
+
// \x1b[38;5;196m = foreground 256-color mode, color 196
|
|
296
|
+
const ansiString = '\x1b[38;5;196mRed 196\x1b[0m'
|
|
297
|
+
const result = ansiToCSS(ansiString)
|
|
298
|
+
|
|
299
|
+
// Color 196 in 256 palette is #ff0000 (bright red)
|
|
300
|
+
expect(result.spans[0].style.color).toBe('#ff0000')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('converts ANSI 256 foreground color 46 (green) to hex', async () => {
|
|
304
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
305
|
+
|
|
306
|
+
const ansiString = '\x1b[38;5;46mGreen 46\x1b[0m'
|
|
307
|
+
const result = ansiToCSS(ansiString)
|
|
308
|
+
|
|
309
|
+
// Color 46 in 256 palette is #00ff00
|
|
310
|
+
expect(result.spans[0].style.color).toBe('#00ff00')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('converts ANSI 256 foreground color 21 (blue) to hex', async () => {
|
|
314
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
315
|
+
|
|
316
|
+
const ansiString = '\x1b[38;5;21mBlue 21\x1b[0m'
|
|
317
|
+
const result = ansiToCSS(ansiString)
|
|
318
|
+
|
|
319
|
+
// Color 21 in 256 palette is #0000ff
|
|
320
|
+
expect(result.spans[0].style.color).toBe('#0000ff')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('converts ANSI 256 background color 196 (red) to hex', async () => {
|
|
324
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
325
|
+
|
|
326
|
+
// \x1b[48;5;196m = background 256-color mode, color 196
|
|
327
|
+
const ansiString = '\x1b[48;5;196mRed bg 196\x1b[0m'
|
|
328
|
+
const result = ansiToCSS(ansiString)
|
|
329
|
+
|
|
330
|
+
expect(result.spans[0].style.backgroundColor).toBe('#ff0000')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('converts ANSI 256 grayscale color 240 to hex', async () => {
|
|
334
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
335
|
+
|
|
336
|
+
const ansiString = '\x1b[38;5;240mGray 240\x1b[0m'
|
|
337
|
+
const result = ansiToCSS(ansiString)
|
|
338
|
+
|
|
339
|
+
// Color 240 is in grayscale ramp (232-255), approximately #585858
|
|
340
|
+
expect(result.spans[0].style.color).toBe('#585858')
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('converts ANSI 256 grayscale color 232 (darkest) to hex', async () => {
|
|
344
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
345
|
+
|
|
346
|
+
const ansiString = '\x1b[38;5;232mGray 232\x1b[0m'
|
|
347
|
+
const result = ansiToCSS(ansiString)
|
|
348
|
+
|
|
349
|
+
// Color 232 is the darkest in grayscale ramp, #080808
|
|
350
|
+
expect(result.spans[0].style.color).toBe('#080808')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('converts ANSI 256 grayscale color 255 (lightest) to hex', async () => {
|
|
354
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
355
|
+
|
|
356
|
+
const ansiString = '\x1b[38;5;255mGray 255\x1b[0m'
|
|
357
|
+
const result = ansiToCSS(ansiString)
|
|
358
|
+
|
|
359
|
+
// Color 255 is the lightest in grayscale ramp, #eeeeee
|
|
360
|
+
expect(result.spans[0].style.color).toBe('#eeeeee')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('converts ANSI 256 color cube value 75 to hex', async () => {
|
|
364
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
365
|
+
|
|
366
|
+
const ansiString = '\x1b[38;5;75mCube 75\x1b[0m'
|
|
367
|
+
const result = ansiToCSS(ansiString)
|
|
368
|
+
|
|
369
|
+
// Color 75 in 6x6x6 cube: r=1, g=2, b=3 -> #5fafff (light blue)
|
|
370
|
+
expect(result.spans[0].style.color).toBe('#5fafff')
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('converts ANSI 256 standard colors 0-15 to CSS', async () => {
|
|
374
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
375
|
+
|
|
376
|
+
// Color 0 = black
|
|
377
|
+
const result0 = ansiToCSS('\x1b[38;5;0mBlack\x1b[0m')
|
|
378
|
+
expect(result0.spans[0].style.color).toBe('#000000')
|
|
379
|
+
|
|
380
|
+
// Color 15 = bright white
|
|
381
|
+
const result15 = ansiToCSS('\x1b[38;5;15mBright White\x1b[0m')
|
|
382
|
+
expect(result15.spans[0].style.color).toBe('#ffffff')
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// ============================================================================
|
|
387
|
+
// ANSI True Color (24-bit RGB) to CSS rgb()
|
|
388
|
+
// ============================================================================
|
|
389
|
+
|
|
390
|
+
describe('ANSI true color to CSS rgb()', () => {
|
|
391
|
+
it('converts ANSI true color foreground to CSS rgb()', async () => {
|
|
392
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
393
|
+
|
|
394
|
+
// \x1b[38;2;59;130;246m = foreground true color, RGB(59, 130, 246)
|
|
395
|
+
const ansiString = '\x1b[38;2;59;130;246mBlue 500\x1b[0m'
|
|
396
|
+
const result = ansiToCSS(ansiString)
|
|
397
|
+
|
|
398
|
+
expect(result.spans[0].style.color).toBe('rgb(59, 130, 246)')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('converts ANSI true color background to CSS rgb()', async () => {
|
|
402
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
403
|
+
|
|
404
|
+
// \x1b[48;2;239;68;68m = background true color, RGB(239, 68, 68)
|
|
405
|
+
const ansiString = '\x1b[48;2;239;68;68mRed 500 bg\x1b[0m'
|
|
406
|
+
const result = ansiToCSS(ansiString)
|
|
407
|
+
|
|
408
|
+
expect(result.spans[0].style.backgroundColor).toBe('rgb(239, 68, 68)')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('converts ANSI true color with full RGB range', async () => {
|
|
412
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
413
|
+
|
|
414
|
+
// Pure white
|
|
415
|
+
const white = ansiToCSS('\x1b[38;2;255;255;255mWhite\x1b[0m')
|
|
416
|
+
expect(white.spans[0].style.color).toBe('rgb(255, 255, 255)')
|
|
417
|
+
|
|
418
|
+
// Pure black
|
|
419
|
+
const black = ansiToCSS('\x1b[38;2;0;0;0mBlack\x1b[0m')
|
|
420
|
+
expect(black.spans[0].style.color).toBe('rgb(0, 0, 0)')
|
|
421
|
+
|
|
422
|
+
// Arbitrary color
|
|
423
|
+
const custom = ansiToCSS('\x1b[38;2;128;64;192mCustom\x1b[0m')
|
|
424
|
+
expect(custom.spans[0].style.color).toBe('rgb(128, 64, 192)')
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('handles both foreground and background true colors', async () => {
|
|
428
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
429
|
+
|
|
430
|
+
const ansiString = '\x1b[38;2;255;255;255m\x1b[48;2;0;0;128mWhite on Navy\x1b[0m'
|
|
431
|
+
const result = ansiToCSS(ansiString)
|
|
432
|
+
|
|
433
|
+
expect(result.spans[0].style.color).toBe('rgb(255, 255, 255)')
|
|
434
|
+
expect(result.spans[0].style.backgroundColor).toBe('rgb(0, 0, 128)')
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// Text Styling to CSS Properties
|
|
440
|
+
// ============================================================================
|
|
441
|
+
|
|
442
|
+
describe('ANSI text styles to CSS properties', () => {
|
|
443
|
+
it('converts ANSI bold (1) to CSS fontWeight bold', async () => {
|
|
444
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
445
|
+
|
|
446
|
+
const ansiString = '\x1b[1mBold text\x1b[0m'
|
|
447
|
+
const result = ansiToCSS(ansiString)
|
|
448
|
+
|
|
449
|
+
expect(result.spans[0].style.fontWeight).toBe('bold')
|
|
450
|
+
expect(result.spans[0].text).toBe('Bold text')
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('converts ANSI dim (2) to CSS opacity', async () => {
|
|
454
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
455
|
+
|
|
456
|
+
const ansiString = '\x1b[2mDim text\x1b[0m'
|
|
457
|
+
const result = ansiToCSS(ansiString)
|
|
458
|
+
|
|
459
|
+
expect(result.spans[0].style.opacity).toBe(0.5)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('converts ANSI italic (3) to CSS fontStyle italic', async () => {
|
|
463
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
464
|
+
|
|
465
|
+
const ansiString = '\x1b[3mItalic text\x1b[0m'
|
|
466
|
+
const result = ansiToCSS(ansiString)
|
|
467
|
+
|
|
468
|
+
expect(result.spans[0].style.fontStyle).toBe('italic')
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('converts ANSI underline (4) to CSS textDecoration underline', async () => {
|
|
472
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
473
|
+
|
|
474
|
+
const ansiString = '\x1b[4mUnderlined text\x1b[0m'
|
|
475
|
+
const result = ansiToCSS(ansiString)
|
|
476
|
+
|
|
477
|
+
expect(result.spans[0].style.textDecoration).toBe('underline')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('converts ANSI strikethrough (9) to CSS textDecoration line-through', async () => {
|
|
481
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
482
|
+
|
|
483
|
+
const ansiString = '\x1b[9mStrikethrough text\x1b[0m'
|
|
484
|
+
const result = ansiToCSS(ansiString)
|
|
485
|
+
|
|
486
|
+
expect(result.spans[0].style.textDecoration).toBe('line-through')
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('converts ANSI inverse (7) to swapped fg/bg colors', async () => {
|
|
490
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
491
|
+
|
|
492
|
+
// Red text with inverse should become red background with default foreground
|
|
493
|
+
const ansiString = '\x1b[31m\x1b[7mInverse red\x1b[0m'
|
|
494
|
+
const result = ansiToCSS(ansiString)
|
|
495
|
+
|
|
496
|
+
// After inverse, red moves to background
|
|
497
|
+
expect(result.spans[0].style.backgroundColor).toBe('red')
|
|
498
|
+
// Foreground should be default or explicitly set
|
|
499
|
+
expect(result.spans[0].style.color).toBeDefined()
|
|
500
|
+
})
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
// ============================================================================
|
|
504
|
+
// Combination Styles
|
|
505
|
+
// ============================================================================
|
|
506
|
+
|
|
507
|
+
describe('combination styles', () => {
|
|
508
|
+
it('combines bold and color', async () => {
|
|
509
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
510
|
+
|
|
511
|
+
const ansiString = '\x1b[1m\x1b[31mBold Red\x1b[0m'
|
|
512
|
+
const result = ansiToCSS(ansiString)
|
|
513
|
+
|
|
514
|
+
expect(result.spans[0].style.fontWeight).toBe('bold')
|
|
515
|
+
expect(result.spans[0].style.color).toBe('red')
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it('combines italic and underline', async () => {
|
|
519
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
520
|
+
|
|
521
|
+
const ansiString = '\x1b[3m\x1b[4mItalic Underline\x1b[0m'
|
|
522
|
+
const result = ansiToCSS(ansiString)
|
|
523
|
+
|
|
524
|
+
expect(result.spans[0].style.fontStyle).toBe('italic')
|
|
525
|
+
expect(result.spans[0].style.textDecoration).toBe('underline')
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it('combines foreground and background colors', async () => {
|
|
529
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
530
|
+
|
|
531
|
+
const ansiString = '\x1b[37m\x1b[44mWhite on Blue\x1b[0m'
|
|
532
|
+
const result = ansiToCSS(ansiString)
|
|
533
|
+
|
|
534
|
+
expect(result.spans[0].style.color).toBe('white')
|
|
535
|
+
expect(result.spans[0].style.backgroundColor).toBe('blue')
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('combines all style properties', async () => {
|
|
539
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
540
|
+
|
|
541
|
+
const ansiString = '\x1b[1m\x1b[3m\x1b[4m\x1b[36m\x1b[40mFull Style\x1b[0m'
|
|
542
|
+
const result = ansiToCSS(ansiString)
|
|
543
|
+
|
|
544
|
+
expect(result.spans[0].style.fontWeight).toBe('bold')
|
|
545
|
+
expect(result.spans[0].style.fontStyle).toBe('italic')
|
|
546
|
+
expect(result.spans[0].style.textDecoration).toBe('underline')
|
|
547
|
+
expect(result.spans[0].style.color).toBe('cyan')
|
|
548
|
+
expect(result.spans[0].style.backgroundColor).toBe('black')
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
it('handles SGR sequences with multiple parameters', async () => {
|
|
552
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
553
|
+
|
|
554
|
+
// \x1b[1;31;44m = bold;red;blue-bg in single sequence
|
|
555
|
+
const ansiString = '\x1b[1;31;44mCombined SGR\x1b[0m'
|
|
556
|
+
const result = ansiToCSS(ansiString)
|
|
557
|
+
|
|
558
|
+
expect(result.spans[0].style.fontWeight).toBe('bold')
|
|
559
|
+
expect(result.spans[0].style.color).toBe('red')
|
|
560
|
+
expect(result.spans[0].style.backgroundColor).toBe('blue')
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
// ============================================================================
|
|
565
|
+
// Reset Handling
|
|
566
|
+
// ============================================================================
|
|
567
|
+
|
|
568
|
+
describe('reset handling', () => {
|
|
569
|
+
it('resets all styles with code 0', async () => {
|
|
570
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
571
|
+
|
|
572
|
+
const ansiString = '\x1b[1m\x1b[31mStyled\x1b[0mPlain'
|
|
573
|
+
const result = ansiToCSS(ansiString)
|
|
574
|
+
|
|
575
|
+
expect(result.spans).toHaveLength(2)
|
|
576
|
+
expect(result.spans[0].style.fontWeight).toBe('bold')
|
|
577
|
+
expect(result.spans[0].style.color).toBe('red')
|
|
578
|
+
expect(result.spans[1].style.fontWeight).toBeUndefined()
|
|
579
|
+
expect(result.spans[1].style.color).toBeUndefined()
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('resets bold with code 22', async () => {
|
|
583
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
584
|
+
|
|
585
|
+
const ansiString = '\x1b[1mBold\x1b[22mNormal'
|
|
586
|
+
const result = ansiToCSS(ansiString)
|
|
587
|
+
|
|
588
|
+
expect(result.spans[0].style.fontWeight).toBe('bold')
|
|
589
|
+
expect(result.spans[1].style.fontWeight).toBe('normal')
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('resets italic with code 23', async () => {
|
|
593
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
594
|
+
|
|
595
|
+
const ansiString = '\x1b[3mItalic\x1b[23mNormal'
|
|
596
|
+
const result = ansiToCSS(ansiString)
|
|
597
|
+
|
|
598
|
+
expect(result.spans[0].style.fontStyle).toBe('italic')
|
|
599
|
+
expect(result.spans[1].style.fontStyle).toBe('normal')
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('resets underline with code 24', async () => {
|
|
603
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
604
|
+
|
|
605
|
+
const ansiString = '\x1b[4mUnderline\x1b[24mNormal'
|
|
606
|
+
const result = ansiToCSS(ansiString)
|
|
607
|
+
|
|
608
|
+
expect(result.spans[0].style.textDecoration).toBe('underline')
|
|
609
|
+
expect(result.spans[1].style.textDecoration).toBe('none')
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it('resets foreground color with code 39', async () => {
|
|
613
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
614
|
+
|
|
615
|
+
const ansiString = '\x1b[31mRed\x1b[39mDefault'
|
|
616
|
+
const result = ansiToCSS(ansiString)
|
|
617
|
+
|
|
618
|
+
expect(result.spans[0].style.color).toBe('red')
|
|
619
|
+
expect(result.spans[1].style.color).toBeUndefined()
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('resets background color with code 49', async () => {
|
|
623
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
624
|
+
|
|
625
|
+
const ansiString = '\x1b[41mRed BG\x1b[49mDefault'
|
|
626
|
+
const result = ansiToCSS(ansiString)
|
|
627
|
+
|
|
628
|
+
expect(result.spans[0].style.backgroundColor).toBe('red')
|
|
629
|
+
expect(result.spans[1].style.backgroundColor).toBeUndefined()
|
|
630
|
+
})
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
// ============================================================================
|
|
634
|
+
// Plain Text Extraction
|
|
635
|
+
// ============================================================================
|
|
636
|
+
|
|
637
|
+
describe('plain text extraction', () => {
|
|
638
|
+
it('extracts plain text from styled string', async () => {
|
|
639
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
640
|
+
|
|
641
|
+
const ansiString = '\x1b[1m\x1b[31mHello\x1b[0m \x1b[32mWorld\x1b[0m'
|
|
642
|
+
const result = ansiToCSS(ansiString)
|
|
643
|
+
|
|
644
|
+
expect(result.plainText).toBe('Hello World')
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
it('handles plain text without ANSI codes', async () => {
|
|
648
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
649
|
+
|
|
650
|
+
const ansiString = 'Just plain text'
|
|
651
|
+
const result = ansiToCSS(ansiString)
|
|
652
|
+
|
|
653
|
+
expect(result.plainText).toBe('Just plain text')
|
|
654
|
+
expect(result.spans).toHaveLength(1)
|
|
655
|
+
expect(result.spans[0].text).toBe('Just plain text')
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
it('handles empty string', async () => {
|
|
659
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
660
|
+
|
|
661
|
+
const result = ansiToCSS('')
|
|
662
|
+
|
|
663
|
+
expect(result.plainText).toBe('')
|
|
664
|
+
expect(result.spans).toHaveLength(0)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it('handles string with only ANSI codes (no text)', async () => {
|
|
668
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
669
|
+
|
|
670
|
+
const ansiString = '\x1b[31m\x1b[0m'
|
|
671
|
+
const result = ansiToCSS(ansiString)
|
|
672
|
+
|
|
673
|
+
expect(result.plainText).toBe('')
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
// ============================================================================
|
|
678
|
+
// Multiple Spans
|
|
679
|
+
// ============================================================================
|
|
680
|
+
|
|
681
|
+
describe('multiple styled spans', () => {
|
|
682
|
+
it('creates separate spans for different styles', async () => {
|
|
683
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
684
|
+
|
|
685
|
+
const ansiString = '\x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m \x1b[34mBlue\x1b[0m'
|
|
686
|
+
const result = ansiToCSS(ansiString)
|
|
687
|
+
|
|
688
|
+
expect(result.spans.length).toBeGreaterThanOrEqual(3)
|
|
689
|
+
|
|
690
|
+
const redSpan = result.spans.find(s => s.text.includes('Red'))
|
|
691
|
+
const greenSpan = result.spans.find(s => s.text.includes('Green'))
|
|
692
|
+
const blueSpan = result.spans.find(s => s.text.includes('Blue'))
|
|
693
|
+
|
|
694
|
+
expect(redSpan?.style.color).toBe('red')
|
|
695
|
+
expect(greenSpan?.style.color).toBe('green')
|
|
696
|
+
expect(blueSpan?.style.color).toBe('blue')
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('handles style changes mid-text', async () => {
|
|
700
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
701
|
+
|
|
702
|
+
const ansiString = 'Normal \x1b[1mBold\x1b[0m Normal'
|
|
703
|
+
const result = ansiToCSS(ansiString)
|
|
704
|
+
|
|
705
|
+
expect(result.spans).toHaveLength(3)
|
|
706
|
+
expect(result.spans[0].text).toBe('Normal ')
|
|
707
|
+
expect(result.spans[0].style.fontWeight).toBeUndefined()
|
|
708
|
+
expect(result.spans[1].text).toBe('Bold')
|
|
709
|
+
expect(result.spans[1].style.fontWeight).toBe('bold')
|
|
710
|
+
expect(result.spans[2].text).toBe(' Normal')
|
|
711
|
+
expect(result.spans[2].style.fontWeight).toBeUndefined()
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('handles nested style changes', async () => {
|
|
715
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
716
|
+
|
|
717
|
+
// Bold, then add red, then remove bold but keep red
|
|
718
|
+
const ansiString = '\x1b[1mBold \x1b[31mBold+Red\x1b[22mJust Red\x1b[0m'
|
|
719
|
+
const result = ansiToCSS(ansiString)
|
|
720
|
+
|
|
721
|
+
const boldSpan = result.spans.find(s => s.text === 'Bold ')
|
|
722
|
+
const boldRedSpan = result.spans.find(s => s.text === 'Bold+Red')
|
|
723
|
+
const justRedSpan = result.spans.find(s => s.text === 'Just Red')
|
|
724
|
+
|
|
725
|
+
expect(boldSpan?.style.fontWeight).toBe('bold')
|
|
726
|
+
expect(boldSpan?.style.color).toBeUndefined()
|
|
727
|
+
|
|
728
|
+
expect(boldRedSpan?.style.fontWeight).toBe('bold')
|
|
729
|
+
expect(boldRedSpan?.style.color).toBe('red')
|
|
730
|
+
|
|
731
|
+
expect(justRedSpan?.style.fontWeight).toBe('normal')
|
|
732
|
+
expect(justRedSpan?.style.color).toBe('red')
|
|
733
|
+
})
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
// ============================================================================
|
|
737
|
+
// Edge Cases
|
|
738
|
+
// ============================================================================
|
|
739
|
+
|
|
740
|
+
describe('edge cases', () => {
|
|
741
|
+
it('handles incomplete escape sequences gracefully', async () => {
|
|
742
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
743
|
+
|
|
744
|
+
// Incomplete sequence (missing m terminator)
|
|
745
|
+
const ansiString = '\x1b[31Incomplete'
|
|
746
|
+
|
|
747
|
+
// Should not throw, handle gracefully
|
|
748
|
+
expect(() => ansiToCSS(ansiString)).not.toThrow()
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
it('handles unknown SGR codes gracefully', async () => {
|
|
752
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
753
|
+
|
|
754
|
+
// Code 999 is not a valid SGR code
|
|
755
|
+
const ansiString = '\x1b[999mUnknown\x1b[0m'
|
|
756
|
+
|
|
757
|
+
expect(() => ansiToCSS(ansiString)).not.toThrow()
|
|
758
|
+
const result = ansiToCSS(ansiString)
|
|
759
|
+
expect(result.plainText).toBe('Unknown')
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('handles cursor movement codes (ignores them)', async () => {
|
|
763
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
764
|
+
|
|
765
|
+
// Cursor up (\x1b[A), down (\x1b[B) - not SGR codes
|
|
766
|
+
const ansiString = '\x1b[AUp\x1b[BDown'
|
|
767
|
+
const result = ansiToCSS(ansiString)
|
|
768
|
+
|
|
769
|
+
// Should strip non-SGR sequences and return text
|
|
770
|
+
expect(result.plainText).toContain('Up')
|
|
771
|
+
expect(result.plainText).toContain('Down')
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
it('handles hyperlink sequences (OSC 8)', async () => {
|
|
775
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
776
|
+
|
|
777
|
+
// OSC 8 hyperlink: \x1b]8;;URL\x1b\\text\x1b]8;;\x1b\\
|
|
778
|
+
const ansiString = '\x1b]8;;https://example.com\x1b\\Link Text\x1b]8;;\x1b\\'
|
|
779
|
+
const result = ansiToCSS(ansiString)
|
|
780
|
+
|
|
781
|
+
// Should extract text, may or may not preserve link info
|
|
782
|
+
expect(result.plainText).toContain('Link Text')
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
it('handles newlines within styled text', async () => {
|
|
786
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
787
|
+
|
|
788
|
+
const ansiString = '\x1b[31mLine 1\nLine 2\x1b[0m'
|
|
789
|
+
const result = ansiToCSS(ansiString)
|
|
790
|
+
|
|
791
|
+
expect(result.plainText).toContain('Line 1')
|
|
792
|
+
expect(result.plainText).toContain('Line 2')
|
|
793
|
+
// Style should apply to both lines
|
|
794
|
+
expect(result.spans[0].style.color).toBe('red')
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it('handles Unicode characters', async () => {
|
|
798
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
799
|
+
|
|
800
|
+
const ansiString = '\x1b[32m\u2713 Success\x1b[0m'
|
|
801
|
+
const result = ansiToCSS(ansiString)
|
|
802
|
+
|
|
803
|
+
expect(result.plainText).toContain('\u2713')
|
|
804
|
+
expect(result.spans[0].style.color).toBe('green')
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
it('handles emoji in styled text', async () => {
|
|
808
|
+
const { ansiToCSS } = await import('../../renderers/ansi-css')
|
|
809
|
+
|
|
810
|
+
const ansiString = '\x1b[33mWarning! \x1b[0m'
|
|
811
|
+
const result = ansiToCSS(ansiString)
|
|
812
|
+
|
|
813
|
+
expect(result.plainText).toBe('Warning! ')
|
|
814
|
+
expect(result.spans[0].style.color).toBe('yellow')
|
|
815
|
+
})
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
// ============================================================================
|
|
819
|
+
// CSS String Generation
|
|
820
|
+
// ============================================================================
|
|
821
|
+
|
|
822
|
+
describe('CSS string generation', () => {
|
|
823
|
+
it('generates valid CSS style string from span', async () => {
|
|
824
|
+
const { ansiToCSS, spanToInlineStyle } = await import('../../renderers/ansi-css')
|
|
825
|
+
|
|
826
|
+
const ansiString = '\x1b[1m\x1b[31mBold Red\x1b[0m'
|
|
827
|
+
const result = ansiToCSS(ansiString)
|
|
828
|
+
|
|
829
|
+
const cssString = spanToInlineStyle(result.spans[0])
|
|
830
|
+
|
|
831
|
+
expect(cssString).toContain('font-weight: bold')
|
|
832
|
+
expect(cssString).toContain('color: red')
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
it('generates empty string for unstyled span', async () => {
|
|
836
|
+
const { ansiToCSS, spanToInlineStyle } = await import('../../renderers/ansi-css')
|
|
837
|
+
|
|
838
|
+
const result = ansiToCSS('Plain text')
|
|
839
|
+
const cssString = spanToInlineStyle(result.spans[0])
|
|
840
|
+
|
|
841
|
+
expect(cssString).toBe('')
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
it('generates correct CSS for 256-color', async () => {
|
|
845
|
+
const { ansiToCSS, spanToInlineStyle } = await import('../../renderers/ansi-css')
|
|
846
|
+
|
|
847
|
+
const result = ansiToCSS('\x1b[38;5;196mRed 196\x1b[0m')
|
|
848
|
+
const cssString = spanToInlineStyle(result.spans[0])
|
|
849
|
+
|
|
850
|
+
expect(cssString).toContain('color: #ff0000')
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it('generates correct CSS for true color', async () => {
|
|
854
|
+
const { ansiToCSS, spanToInlineStyle } = await import('../../renderers/ansi-css')
|
|
855
|
+
|
|
856
|
+
const result = ansiToCSS('\x1b[38;2;59;130;246mBlue\x1b[0m')
|
|
857
|
+
const cssString = spanToInlineStyle(result.spans[0])
|
|
858
|
+
|
|
859
|
+
expect(cssString).toContain('color: rgb(59, 130, 246)')
|
|
860
|
+
})
|
|
861
|
+
})
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
// ============================================================================
|
|
865
|
+
// HTML Generation Tests
|
|
866
|
+
// ============================================================================
|
|
867
|
+
|
|
868
|
+
describe('ansiToHTML', () => {
|
|
869
|
+
it('converts ANSI string to HTML with span elements', async () => {
|
|
870
|
+
const { ansiToHTML } = await import('../../renderers/ansi-css')
|
|
871
|
+
|
|
872
|
+
const ansiString = '\x1b[31mRed\x1b[0m Normal \x1b[32mGreen\x1b[0m'
|
|
873
|
+
const html = ansiToHTML(ansiString)
|
|
874
|
+
|
|
875
|
+
expect(html).toContain('<span')
|
|
876
|
+
expect(html).toContain('color: red')
|
|
877
|
+
expect(html).toContain('Red')
|
|
878
|
+
expect(html).toContain('color: green')
|
|
879
|
+
expect(html).toContain('Green')
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
it('escapes HTML entities in text content', async () => {
|
|
883
|
+
const { ansiToHTML } = await import('../../renderers/ansi-css')
|
|
884
|
+
|
|
885
|
+
const ansiString = '\x1b[31m<script>alert("xss")</script>\x1b[0m'
|
|
886
|
+
const html = ansiToHTML(ansiString)
|
|
887
|
+
|
|
888
|
+
expect(html).not.toContain('<script>')
|
|
889
|
+
expect(html).toContain('<script>')
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
it('preserves newlines as <br> elements', async () => {
|
|
893
|
+
const { ansiToHTML } = await import('../../renderers/ansi-css')
|
|
894
|
+
|
|
895
|
+
const ansiString = 'Line 1\nLine 2\nLine 3'
|
|
896
|
+
const html = ansiToHTML(ansiString)
|
|
897
|
+
|
|
898
|
+
expect(html).toContain('<br>')
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
it('handles empty string', async () => {
|
|
902
|
+
const { ansiToHTML } = await import('../../renderers/ansi-css')
|
|
903
|
+
|
|
904
|
+
const html = ansiToHTML('')
|
|
905
|
+
|
|
906
|
+
expect(html).toBe('')
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
it('handles plain text without ANSI codes', async () => {
|
|
910
|
+
const { ansiToHTML } = await import('../../renderers/ansi-css')
|
|
911
|
+
|
|
912
|
+
const html = ansiToHTML('Plain text')
|
|
913
|
+
|
|
914
|
+
expect(html).toBe('Plain text')
|
|
915
|
+
})
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
// ============================================================================
|
|
919
|
+
// React Component Generation Tests (optional, for React integration)
|
|
920
|
+
// ============================================================================
|
|
921
|
+
|
|
922
|
+
describe('parseAnsiToSpans (for React)', () => {
|
|
923
|
+
it('returns array of span objects ready for React rendering', async () => {
|
|
924
|
+
const { parseAnsiToSpans } = await import('../../renderers/ansi-css')
|
|
925
|
+
|
|
926
|
+
const ansiString = '\x1b[1m\x1b[36mCyan Bold\x1b[0m'
|
|
927
|
+
const spans = parseAnsiToSpans(ansiString)
|
|
928
|
+
|
|
929
|
+
expect(Array.isArray(spans)).toBe(true)
|
|
930
|
+
expect(spans[0]).toHaveProperty('text')
|
|
931
|
+
expect(spans[0]).toHaveProperty('style')
|
|
932
|
+
expect(spans[0].style).toHaveProperty('fontWeight', 'bold')
|
|
933
|
+
expect(spans[0].style).toHaveProperty('color', 'cyan')
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
it('returns style object compatible with React CSSProperties', async () => {
|
|
937
|
+
const { parseAnsiToSpans } = await import('../../renderers/ansi-css')
|
|
938
|
+
|
|
939
|
+
const spans = parseAnsiToSpans('\x1b[38;2;100;150;200mTrue Color\x1b[0m')
|
|
940
|
+
|
|
941
|
+
// Style should use camelCase for React compatibility
|
|
942
|
+
const style = spans[0].style
|
|
943
|
+
expect(style.color).toBe('rgb(100, 150, 200)')
|
|
944
|
+
// Should not have hyphenated properties
|
|
945
|
+
expect(style).not.toHaveProperty('font-weight')
|
|
946
|
+
expect(style).not.toHaveProperty('background-color')
|
|
947
|
+
})
|
|
948
|
+
})
|