@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.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. 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, '&amp;')
517
+ .replace(/</g, '&lt;')
518
+ .replace(/>/g, '&gt;')
519
+ .replace(/"/g, '&quot;')
520
+ .replace(/'/g, '&#39;')
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
+ }