@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,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI to CSS Converter
|
|
3
|
+
*
|
|
4
|
+
* Converts ANSI escape codes to CSS styles for web rendering.
|
|
5
|
+
* This enables browser-based terminal emulators to display styled output.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - 16 basic colors (30-37, 40-47)
|
|
9
|
+
* - Bright colors (90-97, 100-107)
|
|
10
|
+
* - 256-color mode (38;5;n and 48;5;n)
|
|
11
|
+
* - True color / 24-bit RGB (38;2;r;g;b and 48;2;r;g;b)
|
|
12
|
+
* - Text styles (bold, dim, italic, underline, strikethrough, inverse)
|
|
13
|
+
* - Reset codes (0, 22, 23, 24, 39, 49)
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* CSS style properties for a styled text span
|
|
24
|
+
*/
|
|
25
|
+
export interface CSSStyleProperties {
|
|
26
|
+
color?: string
|
|
27
|
+
backgroundColor?: string
|
|
28
|
+
fontWeight?: 'bold' | 'normal'
|
|
29
|
+
fontStyle?: 'italic' | 'normal'
|
|
30
|
+
textDecoration?: 'underline' | 'line-through' | 'none'
|
|
31
|
+
opacity?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A span of text with associated CSS styles
|
|
36
|
+
*/
|
|
37
|
+
export interface StyledSpan {
|
|
38
|
+
text: string
|
|
39
|
+
style: CSSStyleProperties
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Result of parsing ANSI string to CSS
|
|
44
|
+
*/
|
|
45
|
+
export interface ANSIToCSSResult {
|
|
46
|
+
spans: StyledSpan[]
|
|
47
|
+
plainText: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Internal state for tracking current styles during parsing
|
|
52
|
+
*/
|
|
53
|
+
interface StyleState {
|
|
54
|
+
color?: string
|
|
55
|
+
backgroundColor?: string
|
|
56
|
+
bold?: boolean
|
|
57
|
+
dim?: boolean
|
|
58
|
+
italic?: boolean
|
|
59
|
+
underline?: boolean
|
|
60
|
+
strikethrough?: boolean
|
|
61
|
+
inverse?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Color Maps
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/** Standard 16 foreground color names (codes 30-37) */
|
|
69
|
+
const FG_16_COLORS: Record<number, string> = {
|
|
70
|
+
30: 'black',
|
|
71
|
+
31: 'red',
|
|
72
|
+
32: 'green',
|
|
73
|
+
33: 'yellow',
|
|
74
|
+
34: 'blue',
|
|
75
|
+
35: 'magenta',
|
|
76
|
+
36: 'cyan',
|
|
77
|
+
37: 'white',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Bright foreground color hex values (codes 90-97) */
|
|
81
|
+
const FG_BRIGHT_COLORS: Record<number, string> = {
|
|
82
|
+
90: 'gray',
|
|
83
|
+
91: '#ff5555',
|
|
84
|
+
92: '#55ff55',
|
|
85
|
+
93: '#ffff55',
|
|
86
|
+
94: '#5555ff',
|
|
87
|
+
95: '#ff55ff',
|
|
88
|
+
96: '#55ffff',
|
|
89
|
+
97: '#ffffff',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Standard 16 background color names (codes 40-47) */
|
|
93
|
+
const BG_16_COLORS: Record<number, string> = {
|
|
94
|
+
40: 'black',
|
|
95
|
+
41: 'red',
|
|
96
|
+
42: 'green',
|
|
97
|
+
43: 'yellow',
|
|
98
|
+
44: 'blue',
|
|
99
|
+
45: 'magenta',
|
|
100
|
+
46: 'cyan',
|
|
101
|
+
47: 'white',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Standard 16 colors for 256-mode codes 0-15 */
|
|
105
|
+
const STANDARD_16_HEX: string[] = [
|
|
106
|
+
'#000000', // 0 - black
|
|
107
|
+
'#800000', // 1 - maroon
|
|
108
|
+
'#008000', // 2 - green
|
|
109
|
+
'#808000', // 3 - olive
|
|
110
|
+
'#000080', // 4 - navy
|
|
111
|
+
'#800080', // 5 - purple
|
|
112
|
+
'#008080', // 6 - teal
|
|
113
|
+
'#c0c0c0', // 7 - silver
|
|
114
|
+
'#808080', // 8 - gray
|
|
115
|
+
'#ff0000', // 9 - red
|
|
116
|
+
'#00ff00', // 10 - lime
|
|
117
|
+
'#ffff00', // 11 - yellow
|
|
118
|
+
'#0000ff', // 12 - blue
|
|
119
|
+
'#ff00ff', // 13 - fuchsia
|
|
120
|
+
'#00ffff', // 14 - aqua
|
|
121
|
+
'#ffffff', // 15 - white
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Color Conversion Utilities
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Convert 256-color code to hex value.
|
|
130
|
+
*
|
|
131
|
+
* The 256-color palette is organized as:
|
|
132
|
+
* - 0-15: Standard 16 colors
|
|
133
|
+
* - 16-231: 6x6x6 color cube (216 colors)
|
|
134
|
+
* - 232-255: Grayscale ramp (24 shades)
|
|
135
|
+
*/
|
|
136
|
+
function ansi256ToHex(code: number): string {
|
|
137
|
+
// Standard 16 colors (0-15)
|
|
138
|
+
if (code < 16) {
|
|
139
|
+
return STANDARD_16_HEX[code]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Grayscale ramp (232-255)
|
|
143
|
+
if (code >= 232) {
|
|
144
|
+
const gray = (code - 232) * 10 + 8
|
|
145
|
+
const hex = gray.toString(16).padStart(2, '0')
|
|
146
|
+
return `#${hex}${hex}${hex}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 6x6x6 color cube (16-231)
|
|
150
|
+
const cubeIndex = code - 16
|
|
151
|
+
const r = Math.floor(cubeIndex / 36)
|
|
152
|
+
const g = Math.floor((cubeIndex % 36) / 6)
|
|
153
|
+
const b = cubeIndex % 6
|
|
154
|
+
|
|
155
|
+
// Each color channel maps: 0=0, 1=95, 2=135, 3=175, 4=215, 5=255
|
|
156
|
+
const toValue = (n: number): number => (n === 0 ? 0 : 55 + n * 40)
|
|
157
|
+
|
|
158
|
+
const rVal = toValue(r).toString(16).padStart(2, '0')
|
|
159
|
+
const gVal = toValue(g).toString(16).padStart(2, '0')
|
|
160
|
+
const bVal = toValue(b).toString(16).padStart(2, '0')
|
|
161
|
+
|
|
162
|
+
return `#${rVal}${gVal}${bVal}`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// SGR Code Processing
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Process a single SGR (Select Graphic Rendition) parameter
|
|
171
|
+
*/
|
|
172
|
+
function processSGRCode(
|
|
173
|
+
code: number,
|
|
174
|
+
params: number[],
|
|
175
|
+
index: number,
|
|
176
|
+
state: StyleState
|
|
177
|
+
): number {
|
|
178
|
+
// Reset all styles
|
|
179
|
+
if (code === 0) {
|
|
180
|
+
state.color = undefined
|
|
181
|
+
state.backgroundColor = undefined
|
|
182
|
+
state.bold = undefined
|
|
183
|
+
state.dim = undefined
|
|
184
|
+
state.italic = undefined
|
|
185
|
+
state.underline = undefined
|
|
186
|
+
state.strikethrough = undefined
|
|
187
|
+
state.inverse = undefined
|
|
188
|
+
return index
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Text styles
|
|
192
|
+
if (code === 1) {
|
|
193
|
+
state.bold = true
|
|
194
|
+
return index
|
|
195
|
+
}
|
|
196
|
+
if (code === 2) {
|
|
197
|
+
state.dim = true
|
|
198
|
+
return index
|
|
199
|
+
}
|
|
200
|
+
if (code === 3) {
|
|
201
|
+
state.italic = true
|
|
202
|
+
return index
|
|
203
|
+
}
|
|
204
|
+
if (code === 4) {
|
|
205
|
+
state.underline = true
|
|
206
|
+
return index
|
|
207
|
+
}
|
|
208
|
+
if (code === 7) {
|
|
209
|
+
state.inverse = true
|
|
210
|
+
return index
|
|
211
|
+
}
|
|
212
|
+
if (code === 9) {
|
|
213
|
+
state.strikethrough = true
|
|
214
|
+
return index
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Reset specific styles
|
|
218
|
+
if (code === 22) {
|
|
219
|
+
state.bold = false
|
|
220
|
+
state.dim = false
|
|
221
|
+
return index
|
|
222
|
+
}
|
|
223
|
+
if (code === 23) {
|
|
224
|
+
state.italic = false
|
|
225
|
+
return index
|
|
226
|
+
}
|
|
227
|
+
if (code === 24) {
|
|
228
|
+
state.underline = false
|
|
229
|
+
return index
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Standard 16 foreground colors (30-37)
|
|
233
|
+
if (code >= 30 && code <= 37) {
|
|
234
|
+
state.color = FG_16_COLORS[code]
|
|
235
|
+
return index
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Reset foreground color
|
|
239
|
+
if (code === 39) {
|
|
240
|
+
state.color = undefined
|
|
241
|
+
return index
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Standard 16 background colors (40-47)
|
|
245
|
+
if (code >= 40 && code <= 47) {
|
|
246
|
+
state.backgroundColor = BG_16_COLORS[code]
|
|
247
|
+
return index
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Reset background color
|
|
251
|
+
if (code === 49) {
|
|
252
|
+
state.backgroundColor = undefined
|
|
253
|
+
return index
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Bright foreground colors (90-97)
|
|
257
|
+
if (code >= 90 && code <= 97) {
|
|
258
|
+
state.color = FG_BRIGHT_COLORS[code]
|
|
259
|
+
return index
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Extended colors (38 = foreground, 48 = background)
|
|
263
|
+
if (code === 38 || code === 48) {
|
|
264
|
+
const isForeground = code === 38
|
|
265
|
+
|
|
266
|
+
// Check for 256-color mode (38;5;n or 48;5;n)
|
|
267
|
+
if (params[index + 1] === 5 && params[index + 2] !== undefined) {
|
|
268
|
+
const colorCode = params[index + 2]
|
|
269
|
+
const hexColor = ansi256ToHex(colorCode)
|
|
270
|
+
if (isForeground) {
|
|
271
|
+
state.color = hexColor
|
|
272
|
+
} else {
|
|
273
|
+
state.backgroundColor = hexColor
|
|
274
|
+
}
|
|
275
|
+
return index + 2
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check for true color mode (38;2;r;g;b or 48;2;r;g;b)
|
|
279
|
+
if (
|
|
280
|
+
params[index + 1] === 2 &&
|
|
281
|
+
params[index + 2] !== undefined &&
|
|
282
|
+
params[index + 3] !== undefined &&
|
|
283
|
+
params[index + 4] !== undefined
|
|
284
|
+
) {
|
|
285
|
+
const r = params[index + 2]
|
|
286
|
+
const g = params[index + 3]
|
|
287
|
+
const b = params[index + 4]
|
|
288
|
+
const rgbColor = `rgb(${r}, ${g}, ${b})`
|
|
289
|
+
if (isForeground) {
|
|
290
|
+
state.color = rgbColor
|
|
291
|
+
} else {
|
|
292
|
+
state.backgroundColor = rgbColor
|
|
293
|
+
}
|
|
294
|
+
return index + 4
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Unknown code - ignore
|
|
299
|
+
return index
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// Style State to CSS Properties
|
|
304
|
+
// ============================================================================
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Convert internal style state to CSS properties object
|
|
308
|
+
*/
|
|
309
|
+
function stateToCSS(state: StyleState): CSSStyleProperties {
|
|
310
|
+
const css: CSSStyleProperties = {}
|
|
311
|
+
|
|
312
|
+
let fg = state.color
|
|
313
|
+
let bg = state.backgroundColor
|
|
314
|
+
|
|
315
|
+
// Handle inverse: swap foreground and background
|
|
316
|
+
if (state.inverse) {
|
|
317
|
+
const tempFg = fg
|
|
318
|
+
fg = bg || 'inherit' // If no bg, use default
|
|
319
|
+
bg = tempFg
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (fg) {
|
|
323
|
+
css.color = fg
|
|
324
|
+
}
|
|
325
|
+
if (bg) {
|
|
326
|
+
css.backgroundColor = bg
|
|
327
|
+
}
|
|
328
|
+
if (state.bold) {
|
|
329
|
+
css.fontWeight = 'bold'
|
|
330
|
+
} else if (state.bold === false) {
|
|
331
|
+
css.fontWeight = 'normal'
|
|
332
|
+
}
|
|
333
|
+
if (state.dim) {
|
|
334
|
+
css.opacity = 0.5
|
|
335
|
+
}
|
|
336
|
+
if (state.italic) {
|
|
337
|
+
css.fontStyle = 'italic'
|
|
338
|
+
} else if (state.italic === false) {
|
|
339
|
+
css.fontStyle = 'normal'
|
|
340
|
+
}
|
|
341
|
+
if (state.underline) {
|
|
342
|
+
css.textDecoration = 'underline'
|
|
343
|
+
} else if (state.underline === false) {
|
|
344
|
+
css.textDecoration = 'none'
|
|
345
|
+
}
|
|
346
|
+
if (state.strikethrough) {
|
|
347
|
+
css.textDecoration = 'line-through'
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return css
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check if style state has any active styles
|
|
355
|
+
*/
|
|
356
|
+
function hasActiveStyles(state: StyleState): boolean {
|
|
357
|
+
return !!(
|
|
358
|
+
state.color ||
|
|
359
|
+
state.backgroundColor ||
|
|
360
|
+
state.bold ||
|
|
361
|
+
state.dim ||
|
|
362
|
+
state.italic ||
|
|
363
|
+
state.underline ||
|
|
364
|
+
state.strikethrough ||
|
|
365
|
+
state.inverse
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Clone a style state
|
|
371
|
+
*/
|
|
372
|
+
function cloneState(state: StyleState): StyleState {
|
|
373
|
+
return { ...state }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ============================================================================
|
|
377
|
+
// ANSI Parsing
|
|
378
|
+
// ============================================================================
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Regex to match ANSI escape sequences:
|
|
382
|
+
* - CSI sequences: ESC [ ... m (SGR codes)
|
|
383
|
+
* - OSC sequences: ESC ] ... (like hyperlinks)
|
|
384
|
+
* - Other escape sequences we want to strip
|
|
385
|
+
*/
|
|
386
|
+
const ANSI_REGEX = /\x1b(?:\[([0-9;]*)([A-Za-z])|\].*?(?:\x1b\\|\x07)|\[[^m]*)/g
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Parse ANSI string and extract styled spans
|
|
390
|
+
*/
|
|
391
|
+
function parseAnsi(ansiString: string): StyledSpan[] {
|
|
392
|
+
if (!ansiString) {
|
|
393
|
+
return []
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const spans: StyledSpan[] = []
|
|
397
|
+
const state: StyleState = {}
|
|
398
|
+
|
|
399
|
+
let lastIndex = 0
|
|
400
|
+
let match: RegExpExecArray | null
|
|
401
|
+
|
|
402
|
+
// Reset regex state
|
|
403
|
+
ANSI_REGEX.lastIndex = 0
|
|
404
|
+
|
|
405
|
+
while ((match = ANSI_REGEX.exec(ansiString)) !== null) {
|
|
406
|
+
// Extract text before this escape sequence
|
|
407
|
+
const textBefore = ansiString.slice(lastIndex, match.index)
|
|
408
|
+
if (textBefore) {
|
|
409
|
+
spans.push({
|
|
410
|
+
text: textBefore,
|
|
411
|
+
style: stateToCSS(cloneState(state)),
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Process SGR sequence (ends with 'm')
|
|
416
|
+
const params = match[1]
|
|
417
|
+
const terminator = match[2]
|
|
418
|
+
|
|
419
|
+
if (terminator === 'm' && params !== undefined) {
|
|
420
|
+
// Parse the parameters
|
|
421
|
+
const codes = params
|
|
422
|
+
.split(';')
|
|
423
|
+
.filter((p) => p !== '')
|
|
424
|
+
.map((p) => parseInt(p, 10))
|
|
425
|
+
|
|
426
|
+
// Handle empty params (just \x1b[m means reset)
|
|
427
|
+
if (codes.length === 0) {
|
|
428
|
+
codes.push(0)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Process each code
|
|
432
|
+
let i = 0
|
|
433
|
+
while (i < codes.length) {
|
|
434
|
+
i = processSGRCode(codes[i], codes, i, state) + 1
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
lastIndex = match.index + match[0].length
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Handle remaining text after last escape sequence
|
|
442
|
+
const remainingText = ansiString.slice(lastIndex)
|
|
443
|
+
if (remainingText) {
|
|
444
|
+
spans.push({
|
|
445
|
+
text: remainingText,
|
|
446
|
+
style: stateToCSS(cloneState(state)),
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return spans
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================================================================
|
|
454
|
+
// Public API
|
|
455
|
+
// ============================================================================
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Converts ANSI-escaped string to CSS-styled spans.
|
|
459
|
+
*
|
|
460
|
+
* @param ansiString - String containing ANSI escape codes
|
|
461
|
+
* @returns Object with spans array and plain text
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```ts
|
|
465
|
+
* const result = ansiToCSS('\x1b[31mRed text\x1b[0m')
|
|
466
|
+
* // result.spans[0].style.color === 'red'
|
|
467
|
+
* // result.plainText === 'Red text'
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
export function ansiToCSS(ansiString: string): ANSIToCSSResult {
|
|
471
|
+
const spans = parseAnsi(ansiString)
|
|
472
|
+
const plainText = spans.map((s) => s.text).join('')
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
spans,
|
|
476
|
+
plainText,
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Converts a styled span to an inline CSS style string.
|
|
482
|
+
*
|
|
483
|
+
* @param span - A styled span object
|
|
484
|
+
* @returns CSS inline style string (e.g., "color: red; font-weight: bold")
|
|
485
|
+
*/
|
|
486
|
+
export function spanToInlineStyle(span: StyledSpan): string {
|
|
487
|
+
const parts: string[] = []
|
|
488
|
+
|
|
489
|
+
if (span.style.color) {
|
|
490
|
+
parts.push(`color: ${span.style.color}`)
|
|
491
|
+
}
|
|
492
|
+
if (span.style.backgroundColor) {
|
|
493
|
+
parts.push(`background-color: ${span.style.backgroundColor}`)
|
|
494
|
+
}
|
|
495
|
+
if (span.style.fontWeight) {
|
|
496
|
+
parts.push(`font-weight: ${span.style.fontWeight}`)
|
|
497
|
+
}
|
|
498
|
+
if (span.style.fontStyle) {
|
|
499
|
+
parts.push(`font-style: ${span.style.fontStyle}`)
|
|
500
|
+
}
|
|
501
|
+
if (span.style.textDecoration) {
|
|
502
|
+
parts.push(`text-decoration: ${span.style.textDecoration}`)
|
|
503
|
+
}
|
|
504
|
+
if (span.style.opacity !== undefined) {
|
|
505
|
+
parts.push(`opacity: ${span.style.opacity}`)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return parts.join('; ')
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Escape HTML special characters to prevent XSS
|
|
513
|
+
*/
|
|
514
|
+
function escapeHTML(text: string): string {
|
|
515
|
+
return text
|
|
516
|
+
.replace(/&/g, '&')
|
|
517
|
+
.replace(/</g, '<')
|
|
518
|
+
.replace(/>/g, '>')
|
|
519
|
+
.replace(/"/g, '"')
|
|
520
|
+
.replace(/'/g, ''')
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Converts ANSI-escaped string to HTML with styled span elements.
|
|
525
|
+
*
|
|
526
|
+
* @param ansiString - String containing ANSI escape codes
|
|
527
|
+
* @returns HTML string with styled spans
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```ts
|
|
531
|
+
* const html = ansiToHTML('\x1b[31mRed text\x1b[0m')
|
|
532
|
+
* // html === '<span style="color: red">Red text</span>'
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
export function ansiToHTML(ansiString: string): string {
|
|
536
|
+
if (!ansiString) {
|
|
537
|
+
return ''
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const { spans } = ansiToCSS(ansiString)
|
|
541
|
+
|
|
542
|
+
if (spans.length === 0) {
|
|
543
|
+
return ''
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// If single unstyled span, return plain text
|
|
547
|
+
if (spans.length === 1 && Object.keys(spans[0].style).length === 0) {
|
|
548
|
+
const text = escapeHTML(spans[0].text)
|
|
549
|
+
return text.replace(/\n/g, '<br>')
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const htmlParts: string[] = []
|
|
553
|
+
|
|
554
|
+
for (const span of spans) {
|
|
555
|
+
const escapedText = escapeHTML(span.text).replace(/\n/g, '<br>')
|
|
556
|
+
const styleString = spanToInlineStyle(span)
|
|
557
|
+
|
|
558
|
+
if (styleString) {
|
|
559
|
+
htmlParts.push(`<span style="${styleString}">${escapedText}</span>`)
|
|
560
|
+
} else {
|
|
561
|
+
htmlParts.push(escapedText)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return htmlParts.join('')
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Parses ANSI string to array of styled spans for React rendering.
|
|
570
|
+
*
|
|
571
|
+
* @param ansiString - String containing ANSI escape codes
|
|
572
|
+
* @returns Array of styled spans with React-compatible style objects
|
|
573
|
+
*/
|
|
574
|
+
export function parseAnsiToSpans(ansiString: string): StyledSpan[] {
|
|
575
|
+
return parseAnsi(ansiString)
|
|
576
|
+
}
|