@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.10

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 (212) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +70 -23
  3. package/dist/Text-DV9CuzAT.d.ts +452 -0
  4. package/dist/ansi.d.ts +217 -0
  5. package/dist/ansi.js +87 -0
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BAC9-TZX.js +592 -0
  11. package/dist/color-policy-CCxuHIdD.d.ts +22 -0
  12. package/dist/color-profile-CyeHnG1T.d.ts +97 -0
  13. package/dist/color-profile-DHhQHY55.js +36 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-B3dL4Q11.js +374 -0
  18. package/dist/detect-Db6GbKOm.d.ts +195 -0
  19. package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-D48vQhhe.d.ts +21 -0
  24. package/dist/index-DDVME65c.d.ts +919 -0
  25. package/dist/index.d.ts +1310 -0
  26. package/dist/index.js +3209 -0
  27. package/dist/osc-BFKKSqpg.js +71 -0
  28. package/dist/osc-Cn0fw77g.d.ts +23 -0
  29. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  30. package/dist/query-BNc2B8GD.d.ts +152 -0
  31. package/dist/router.d.ts +392 -0
  32. package/dist/router.js +709 -0
  33. package/dist/sample-Cqw1bjUL.js +445 -0
  34. package/dist/screen-CgC2WlVM.d.ts +49 -0
  35. package/dist/screen-CiPytswf.js +342 -0
  36. package/dist/screen.d.ts +5 -0
  37. package/dist/screen.js +5 -0
  38. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  39. package/dist/serialize-BTkAZgw1.js +79 -0
  40. package/dist/session-DDQ5V300.js +723 -0
  41. package/dist/sgr-BhwaWAJB.js +246 -0
  42. package/dist/store-C1P5fOUi.d.ts +72 -0
  43. package/dist/string-width-CijQwpIk.js +69 -0
  44. package/dist/strip-BvU4toXG.js +6 -0
  45. package/dist/terminal.d.ts +121 -0
  46. package/dist/terminal.js +2 -0
  47. package/dist/tokenize-AjqbvtiT.js +1242 -0
  48. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  49. package/dist/truncate-D31fhU6i.js +562 -0
  50. package/dist/use-focus-CcCIdCWA.js +1337 -0
  51. package/dist/yoga-5jKhYCJC.js +3465 -0
  52. package/dist/yoga.d.ts +2 -0
  53. package/dist/yoga.js +2 -0
  54. package/package.json +70 -24
  55. package/src/ansi/chalk.ts +138 -0
  56. package/src/ansi/cursor.ts +46 -0
  57. package/src/ansi/east-asian-width.ts +259 -0
  58. package/src/ansi/escapes.ts +120 -0
  59. package/src/ansi/graphemes.ts +8 -0
  60. package/src/ansi/hyperlink.ts +44 -0
  61. package/src/ansi/index.ts +27 -0
  62. package/src/ansi/osc.ts +77 -0
  63. package/src/ansi/sgr.ts +234 -0
  64. package/src/ansi/slice.ts +43 -0
  65. package/src/ansi/string-width.ts +121 -0
  66. package/src/ansi/strip.ts +13 -0
  67. package/src/ansi/tokenize.ts +380 -0
  68. package/src/ansi/truncate.ts +196 -0
  69. package/src/ansi/wrap.ts +765 -0
  70. package/src/ansi-tokenizer.ts +510 -0
  71. package/src/capabilities/color-policy.ts +34 -0
  72. package/src/capabilities/detect.ts +608 -0
  73. package/src/capabilities/index.ts +37 -0
  74. package/src/capabilities/query.ts +679 -0
  75. package/src/capabilities/store.ts +394 -0
  76. package/src/code-excerpt.ts +39 -0
  77. package/src/color/index.ts +3 -0
  78. package/src/color/paint.ts +169 -0
  79. package/src/color/palette.ts +48 -0
  80. package/src/color/sample.ts +323 -0
  81. package/src/color.ts +1 -0
  82. package/src/components/AccessibilityContext.ts +5 -0
  83. package/src/components/AnimationContext.ts +24 -0
  84. package/src/components/AnsiText.tsx +42 -0
  85. package/src/components/App.tsx +878 -0
  86. package/src/components/AppContext.ts +111 -0
  87. package/src/components/BackgroundContext.ts +7 -0
  88. package/src/components/Box.tsx +100 -0
  89. package/src/components/CursorContext.ts +19 -0
  90. package/src/components/ErrorBoundary.tsx +39 -0
  91. package/src/components/ErrorOverview.tsx +135 -0
  92. package/src/components/FocusContext.ts +30 -0
  93. package/src/components/Hyperlink.tsx +56 -0
  94. package/src/components/Newline.tsx +16 -0
  95. package/src/components/Spacer.tsx +11 -0
  96. package/src/components/Static.tsx +60 -0
  97. package/src/components/StderrContext.ts +24 -0
  98. package/src/components/StdinContext.ts +48 -0
  99. package/src/components/StdoutContext.ts +26 -0
  100. package/src/components/TerminalOscContext.ts +25 -0
  101. package/src/components/Text.tsx +122 -0
  102. package/src/components/Transform.tsx +38 -0
  103. package/src/components/VirtualList.tsx +128 -0
  104. package/src/cursor-position.ts +103 -0
  105. package/src/devtools.ts +103 -0
  106. package/src/dom.ts +301 -0
  107. package/src/env.ts +12 -0
  108. package/src/get-max-width.ts +11 -0
  109. package/src/global.d.ts +38 -0
  110. package/src/glyphs.ts +99 -0
  111. package/src/hooks/use-animation.ts +142 -0
  112. package/src/hooks/use-app.ts +8 -0
  113. package/src/hooks/use-box-metrics.ts +134 -0
  114. package/src/hooks/use-capabilities.ts +73 -0
  115. package/src/hooks/use-cursor.ts +33 -0
  116. package/src/hooks/use-focus-manager.ts +62 -0
  117. package/src/hooks/use-focus.ts +82 -0
  118. package/src/hooks/use-input.ts +267 -0
  119. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  120. package/src/hooks/use-paste.ts +78 -0
  121. package/src/hooks/use-stderr.ts +8 -0
  122. package/src/hooks/use-stdin.ts +10 -0
  123. package/src/hooks/use-stdout.ts +8 -0
  124. package/src/hooks/use-terminal-osc.ts +59 -0
  125. package/src/hooks/use-virtual-scroll.ts +84 -0
  126. package/src/hooks/use-window-size.ts +37 -0
  127. package/src/index.ts +96 -0
  128. package/src/ink.tsx +1452 -0
  129. package/src/input-parser.ts +303 -0
  130. package/src/instances.ts +9 -0
  131. package/src/kitty-keyboard.ts +185 -0
  132. package/src/measure-element.ts +62 -0
  133. package/src/measure-text.ts +31 -0
  134. package/src/paint-tree.ts +220 -0
  135. package/src/parse-keypress.ts +515 -0
  136. package/src/parse-stack-line.ts +138 -0
  137. package/src/patch-console.ts +106 -0
  138. package/src/quick-lru.ts +85 -0
  139. package/src/reconciler.ts +425 -0
  140. package/src/render-background.ts +59 -0
  141. package/src/render-border.ts +167 -0
  142. package/src/render-frame.ts +83 -0
  143. package/src/render-to-string.ts +146 -0
  144. package/src/render.ts +284 -0
  145. package/src/router/components.tsx +343 -0
  146. package/src/router/context.ts +41 -0
  147. package/src/router/history.ts +194 -0
  148. package/src/router/hooks.tsx +391 -0
  149. package/src/router/index.ts +34 -0
  150. package/src/router/matcher.ts +571 -0
  151. package/src/sanitize-ansi.ts +33 -0
  152. package/src/screen/ansi.ts +184 -0
  153. package/src/screen/canvas.ts +160 -0
  154. package/src/screen/cell.ts +138 -0
  155. package/src/screen/color-profile.ts +47 -0
  156. package/src/screen/geometry.ts +9 -0
  157. package/src/screen/index.ts +6 -0
  158. package/src/screen/screen.ts +305 -0
  159. package/src/screen/serialize.ts +129 -0
  160. package/src/screen.ts +1 -0
  161. package/src/semantic-text-style.ts +118 -0
  162. package/src/signal-exit.ts +106 -0
  163. package/src/squash-text-nodes.ts +37 -0
  164. package/src/stream.ts +28 -0
  165. package/src/structured-text.ts +325 -0
  166. package/src/styles.ts +753 -0
  167. package/src/terminal/index.ts +2 -0
  168. package/src/terminal/inline-presenter.ts +120 -0
  169. package/src/terminal/input.ts +92 -0
  170. package/src/terminal/render-scheduler.ts +37 -0
  171. package/src/terminal/screen-presenter.ts +242 -0
  172. package/src/terminal/session.ts +408 -0
  173. package/src/terminal-size.ts +58 -0
  174. package/src/terminal.ts +1 -0
  175. package/src/testing/browser.ts +588 -0
  176. package/src/testing/emulators.ts +205 -0
  177. package/src/testing/explorer-app/index.html +12 -0
  178. package/src/testing/explorer-app/main.ts +381 -0
  179. package/src/testing/explorer-app/style.css +194 -0
  180. package/src/testing/explorer-app/tsconfig.json +15 -0
  181. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  182. package/src/testing/index.ts +26 -0
  183. package/src/testing/keys.ts +56 -0
  184. package/src/testing/live.ts +85 -0
  185. package/src/testing/matchers.ts +70 -0
  186. package/src/testing/public.ts +94 -0
  187. package/src/testing/terminal.ts +361 -0
  188. package/src/testing/vitest.ts +157 -0
  189. package/src/throttle.ts +73 -0
  190. package/src/transform-adapter.ts +14 -0
  191. package/src/types.ts +10 -0
  192. package/src/virtual-scroll.ts +133 -0
  193. package/src/wrap-text.ts +54 -0
  194. package/src/yoga/config.ts +57 -0
  195. package/src/yoga/core/absoluteLayout.ts +626 -0
  196. package/src/yoga/core/baseline.ts +66 -0
  197. package/src/yoga/core/cache.ts +136 -0
  198. package/src/yoga/core/calculateLayout.ts +2926 -0
  199. package/src/yoga/core/config.ts +104 -0
  200. package/src/yoga/core/flexLine.ts +177 -0
  201. package/src/yoga/core/helpers.ts +293 -0
  202. package/src/yoga/core/layoutResults.ts +167 -0
  203. package/src/yoga/core/node.ts +611 -0
  204. package/src/yoga/core/numeric.ts +44 -0
  205. package/src/yoga/core/pixelGrid.ts +151 -0
  206. package/src/yoga/core/style.ts +887 -0
  207. package/src/yoga/core/types.ts +224 -0
  208. package/src/yoga/generated/YGEnums.ts +263 -0
  209. package/src/yoga/index.ts +19 -0
  210. package/src/yoga/node.ts +1140 -0
  211. package/dist/index.d.mts +0 -2379
  212. package/dist/index.mjs +0 -10072
@@ -0,0 +1,380 @@
1
+ import { tokenizeAnsi } from "#/ansi-tokenizer.ts";
2
+ import { isFullwidthGrapheme } from "#/ansi/east-asian-width.ts";
3
+ // ANSI-aware style tokenizer: splits a string into grapheme clusters and
4
+ // ANSI codes, tracks active styles per character, and re-emits minimal
5
+ // escape sequences. This is the style model Ink's renderer is built on.
6
+ // Style semantics ported from `@alcalzone/ansi-tokenize` (MIT, AlCalzone);
7
+ // escape recognition delegates to the shared ECMA-48 grammar in
8
+ // `src/ansi-tokenizer.ts` (also behind sanitize-ansi), and grapheme widths
9
+ // come from `east-asian-width.ts` — one grammar, one width table.
10
+ import { BEL, C1_ST, ESC } from "#/ansi/escapes.ts";
11
+ import { graphemes } from "#/ansi/graphemes.ts";
12
+ import { codes as sgrCodes, foreground, background, styles } from "#/ansi/sgr.ts";
13
+
14
+ // Single-character introducer suffixes (`ESC [` = CSI, `ESC ]` = OSC).
15
+ const BACKSLASH = "\\";
16
+ const CSI = "[";
17
+ const OSC = "]";
18
+
19
+ // OSC 8 hyperlink constants
20
+ const linkCodePrefix = `${ESC}${OSC}8;`;
21
+ const linkCodeSuffix = BEL;
22
+ const linkEndCode = `${ESC}${OSC}8;;${BEL}`;
23
+ const linkEndCodeST = `${ESC}${OSC}8;;${ESC}${BACKSLASH}`;
24
+ const linkEndCodeC1ST = `${ESC}${OSC}8;;${C1_ST}`;
25
+
26
+ /**
27
+ An ANSI code paired with the code that ends its effect.
28
+ */
29
+ export type AnsiCode = {
30
+ readonly code: string;
31
+ readonly endCode: string;
32
+ };
33
+
34
+ export type AnsiToken = AnsiCode & {
35
+ readonly type: "ansi";
36
+ };
37
+
38
+ export type ControlToken = {
39
+ readonly type: "control";
40
+ readonly code: string;
41
+ };
42
+
43
+ export type CharToken = {
44
+ readonly type: "char";
45
+ readonly value: string;
46
+ readonly fullWidth: boolean;
47
+ };
48
+
49
+ export type Token = AnsiToken | ControlToken | CharToken;
50
+
51
+ export type StyledChar = CharToken & {
52
+ readonly styles: AnsiCode[];
53
+ };
54
+
55
+ export const endCodesSet = new Set<string>();
56
+ const endCodesMap = new Map<string, string>();
57
+
58
+ for (const [start, end] of sgrCodes) {
59
+ endCodesSet.add(foreground.ansi(end));
60
+ endCodesMap.set(foreground.ansi(start), foreground.ansi(end));
61
+ }
62
+
63
+ export function getLinkStartCode(url: string, params?: Record<string, string>): string {
64
+ const paramsString = params
65
+ ? Object.entries(params)
66
+ .map(([key, value]) => `${key}=${value}`)
67
+ .join(":")
68
+ : "";
69
+ return `${linkCodePrefix}${paramsString};${url}${linkCodeSuffix}`;
70
+ }
71
+
72
+ export function getEndCode(code: string): string {
73
+ if (endCodesSet.has(code)) return code;
74
+ if (endCodesMap.has(code)) return endCodesMap.get(code)!;
75
+
76
+ // We have a few special cases to handle here:
77
+ // Links:
78
+ if (code.startsWith(linkCodePrefix)) {
79
+ if (code.endsWith(`${ESC}${BACKSLASH}`)) return linkEndCodeST;
80
+ if (code.endsWith(C1_ST)) return linkEndCodeC1ST;
81
+ return linkEndCode; // BEL (\u0007)
82
+ }
83
+
84
+ code = code.slice(2);
85
+
86
+ // 8-bit/24-bit colors:
87
+ if (code.startsWith("38")) {
88
+ return foreground.close;
89
+ }
90
+
91
+ if (code.startsWith("48")) {
92
+ return background.close;
93
+ }
94
+
95
+ // Extended underline color (T.416) closes with 59m; not in the SGR map.
96
+ if (code.startsWith("58")) {
97
+ return `${ESC}[59m`;
98
+ }
99
+
100
+ // Otherwise find the reset code in the SGR code map
101
+ const endCode = sgrCodes.get(Number.parseInt(code, 10));
102
+ if (endCode !== undefined) {
103
+ return foreground.ansi(endCode);
104
+ }
105
+
106
+ return styles.reset.open;
107
+ }
108
+
109
+ export function ansiCodesToString(codes: readonly AnsiCode[]): string {
110
+ // Deduplicate ANSI code strings before joining
111
+ const deduplicated = new Set(codes.map((code) => code.code));
112
+ return [...deduplicated].join("");
113
+ }
114
+
115
+ /**
116
+ Check if a code is an intensity code (bold or dim) — these share the end code
117
+ `22m` but can coexist.
118
+ */
119
+ export function isIntensityCode(code: AnsiCode): boolean {
120
+ return code.code === styles.bold.open || code.code === styles.dim.open;
121
+ }
122
+
123
+ /**
124
+ Splits compound SGR sequences like `\u001B[1;3;31m` into individual components.
125
+ */
126
+ function splitCompoundSgrSequences(code: string): string[] {
127
+ if (!code.includes(";")) {
128
+ // Not a compound code
129
+ return [code];
130
+ }
131
+
132
+ const codeParts = code
133
+ // Strip off the escape sequences \u001B[ and m
134
+ .slice(2, -1)
135
+ .split(";");
136
+
137
+ const result: string[] = [];
138
+ for (let index = 0; index < codeParts.length; index++) {
139
+ const rawCode = codeParts[index]!;
140
+ // Keep 8-bit and 24-bit color codes (containing multiple ";") together
141
+ if (rawCode === "38" || rawCode === "48" || rawCode === "58") {
142
+ if (index + 2 < codeParts.length && codeParts[index + 1] === "5") {
143
+ // 8-bit color, followed by another number
144
+ result.push(codeParts.slice(index, index + 3).join(";"));
145
+ index += 2;
146
+ continue;
147
+ } else if (index + 4 < codeParts.length && codeParts[index + 1] === "2") {
148
+ // 24-bit color, followed by three numbers
149
+ result.push(codeParts.slice(index, index + 5).join(";"));
150
+ index += 4;
151
+ continue;
152
+ }
153
+ }
154
+
155
+ // Not a (valid) 8/24-bit color code, push as is
156
+ result.push(rawCode);
157
+ }
158
+
159
+ return result.map((part) => `${ESC}[${part}m`);
160
+ }
161
+
162
+ // SGR parameters: digits separated by `;` or `:` (ITU T.416 colon form).
163
+ const SGR_PARAMETERS_RE = /^[\d;:]*$/;
164
+
165
+ const c1LinkCodePrefix = "\u009D8;";
166
+
167
+ export function tokenize(string: string, endChar = Number.POSITIVE_INFINITY): Token[] {
168
+ const result: Token[] = [];
169
+ let visible = 0;
170
+
171
+ outer: for (const ansiToken of tokenizeAnsi(string)) {
172
+ if (ansiToken.type === "text") {
173
+ for (const segment of graphemes(ansiToken.value)) {
174
+ const fullWidth = isFullwidthGrapheme(segment, segment.codePointAt(0)!);
175
+ result.push({
176
+ type: "char",
177
+ value: segment,
178
+ fullWidth,
179
+ });
180
+
181
+ visible += fullWidth ? 2 : 1;
182
+ if (visible >= endChar) {
183
+ break outer;
184
+ }
185
+ }
186
+
187
+ continue;
188
+ }
189
+
190
+ if (ansiToken.type === "csi") {
191
+ // SGR sequences carry style state; C1 introducers are normalized to the
192
+ // 7-bit form so downstream comparisons see a single representation.
193
+ if (
194
+ ansiToken.finalCharacter === "m" &&
195
+ ansiToken.intermediateString === "" &&
196
+ SGR_PARAMETERS_RE.test(ansiToken.parameterString)
197
+ ) {
198
+ // Split compound codes into individual tokens
199
+ const code = `${ESC}${CSI}${ansiToken.parameterString}m`;
200
+ for (const individualCode of splitCompoundSgrSequences(code)) {
201
+ result.push({
202
+ type: "ansi",
203
+ code: individualCode,
204
+ endCode: getEndCode(individualCode),
205
+ });
206
+ }
207
+ } else {
208
+ result.push({
209
+ type: "control",
210
+ code: ansiToken.value,
211
+ });
212
+ }
213
+
214
+ continue;
215
+ }
216
+
217
+ if (ansiToken.type === "osc") {
218
+ // OSC 8 hyperlinks are paired codes with an endCode; C1 introducers are
219
+ // normalized to the 7-bit form. Other OSC sequences (window title,
220
+ // etc.) are self-contained control codes with no endCode.
221
+ const { value } = ansiToken;
222
+ const prefix = value.startsWith(linkCodePrefix)
223
+ ? linkCodePrefix
224
+ : value.startsWith(c1LinkCodePrefix)
225
+ ? c1LinkCodePrefix
226
+ : undefined;
227
+
228
+ if (prefix !== undefined && value.includes(";", prefix.length)) {
229
+ const code =
230
+ prefix === linkCodePrefix ? value : `${linkCodePrefix}${value.slice(prefix.length)}`;
231
+ result.push({
232
+ type: "ansi",
233
+ code,
234
+ endCode: getEndCode(code),
235
+ });
236
+ } else {
237
+ result.push({
238
+ type: "control",
239
+ code: value,
240
+ });
241
+ }
242
+
243
+ continue;
244
+ }
245
+
246
+ // Everything else the grammar recognizes (ESC sequences, DCS/PM/APC/SOS
247
+ // control strings, stray ST or C1 bytes, malformed tails) is a zero-width
248
+ // control unit, preserved verbatim.
249
+ result.push({
250
+ type: "control",
251
+ code: ansiToken.value,
252
+ });
253
+ }
254
+
255
+ return result;
256
+ }
257
+
258
+ /**
259
+ Reduces the given array of ANSI codes to the minimum necessary to render with
260
+ the same style.
261
+ */
262
+ export function reduceAnsiCodes(codes: readonly AnsiCode[]): AnsiCode[] {
263
+ return reduceAnsiCodesIncremental([], codes);
264
+ }
265
+
266
+ /**
267
+ Like {@link reduceAnsiCodes}, but assumes that `codes` is already reduced.
268
+ Further reductions are only done for the items in `newCodes`.
269
+ */
270
+ export function reduceAnsiCodesIncremental(
271
+ codes: readonly AnsiCode[],
272
+ newCodes: readonly AnsiCode[],
273
+ ): AnsiCode[] {
274
+ let result = [...codes];
275
+
276
+ for (const code of newCodes) {
277
+ if (code.code === styles.reset.open) {
278
+ // Reset code, disable all codes
279
+ result = [];
280
+ } else if (endCodesSet.has(code.code)) {
281
+ // This is an end code, disable all matching start codes
282
+ result = result.filter((existing) => existing.endCode !== code.code);
283
+ } else if (isIntensityCode(code)) {
284
+ // Intensity codes (1m, 2m) can coexist (both end with 22m). Only add
285
+ // if the exact same code is not already present.
286
+ if (
287
+ !result.some((existing) => existing.code === code.code && existing.endCode === code.endCode)
288
+ ) {
289
+ result.push(code);
290
+ }
291
+ } else {
292
+ // This is a start code. Remove codes it "overrides" (same endCode),
293
+ // then add it.
294
+ result = result.filter((existing) => existing.endCode !== code.endCode);
295
+ result.push(code);
296
+ }
297
+ }
298
+
299
+ return result;
300
+ }
301
+
302
+ /**
303
+ Returns the combination of ANSI codes needed to undo the given ANSI codes.
304
+ */
305
+ export function undoAnsiCodes(codes: readonly AnsiCode[]): AnsiCode[] {
306
+ return reduceAnsiCodes(codes)
307
+ .toReversed()
308
+ .map((code) => ({
309
+ ...code,
310
+ code: code.endCode,
311
+ }));
312
+ }
313
+
314
+ /**
315
+ Returns the minimum amount of ANSI codes necessary to get from the compound
316
+ style `from` to `to`. Both are expected to be reduced.
317
+ */
318
+ export function diffAnsiCodes(from: readonly AnsiCode[], to: readonly AnsiCode[]): AnsiCode[] {
319
+ const endCodesInTo = new Set(to.map((code) => code.endCode));
320
+ const startCodesInTo = new Set(to.map((code) => code.code));
321
+ const startCodesInFrom = new Set(from.map((code) => code.code));
322
+
323
+ return [
324
+ // Disable all styles in `from` that are removed in `to`; keep the rest.
325
+ ...undoAnsiCodes(
326
+ from.filter((code) => {
327
+ // Intensity codes (1m, 2m) can coexist (both end with 22m), so
328
+ // check the start codes for those to not miss a reset.
329
+ if (isIntensityCode(code)) {
330
+ return !startCodesInTo.has(code.code);
331
+ }
332
+
333
+ return !endCodesInTo.has(code.endCode);
334
+ }),
335
+ ),
336
+ // Add all styles in `to` that don't exist in `from`
337
+ ...to.filter((code) => !startCodesInFrom.has(code.code)),
338
+ ];
339
+ }
340
+
341
+ export function styledCharsFromTokens(tokens: readonly Token[]): StyledChar[] {
342
+ let codes: AnsiCode[] = [];
343
+ const result: StyledChar[] = [];
344
+
345
+ for (const token of tokens) {
346
+ if (token.type === "ansi") {
347
+ codes = reduceAnsiCodesIncremental(codes, [token]);
348
+ } else if (token.type === "char") {
349
+ result.push({
350
+ ...token,
351
+ styles: [...codes],
352
+ });
353
+ }
354
+ }
355
+
356
+ return result;
357
+ }
358
+
359
+ export function styledCharsToString(chars: readonly StyledChar[]): string {
360
+ let result = "";
361
+
362
+ for (let index = 0; index < chars.length; index++) {
363
+ const char = chars[index]!;
364
+
365
+ if (index === 0) {
366
+ result += ansiCodesToString(char.styles);
367
+ } else {
368
+ result += ansiCodesToString(diffAnsiCodes(chars[index - 1]!.styles, char.styles));
369
+ }
370
+
371
+ result += char.value;
372
+
373
+ // Reset active styles at the end of the string
374
+ if (index === chars.length - 1) {
375
+ result += ansiCodesToString(diffAnsiCodes(char.styles, []));
376
+ }
377
+ }
378
+
379
+ return result;
380
+ }
@@ -0,0 +1,196 @@
1
+ // Truncate a string to a visible width, ANSI-aware.
2
+ // Ported from `cli-truncate` (MIT, Sindre Sorhus) on top of the shared
3
+ // slice and width modules.
4
+ import { sliceAnsi } from "#/ansi/slice.ts";
5
+ import { stringWidth } from "#/ansi/string-width.ts";
6
+
7
+ export type TruncateOptions = {
8
+ readonly position?: "start" | "middle" | "end";
9
+ readonly space?: boolean;
10
+ readonly preferTruncationOnSpace?: boolean;
11
+ readonly truncationCharacter?: string;
12
+ };
13
+
14
+ function getIndexOfNearestSpace(
15
+ string: string,
16
+ wantedIndex: number,
17
+ shouldSearchRight = false,
18
+ ): number {
19
+ if (string.charAt(wantedIndex) === " ") {
20
+ return wantedIndex;
21
+ }
22
+
23
+ const direction = shouldSearchRight ? 1 : -1;
24
+
25
+ for (let index = 0; index <= 3; index++) {
26
+ const finalIndex = wantedIndex + index * direction;
27
+ if (string.charAt(finalIndex) === " ") {
28
+ return finalIndex;
29
+ }
30
+ }
31
+
32
+ return wantedIndex;
33
+ }
34
+
35
+ const ANSI_ESC = 27;
36
+ const ANSI_LEFT_BRACKET = 91;
37
+ const ANSI_LETTER_M = 109;
38
+
39
+ // 0-9, ; or : (ITU T.416 colon-form parameters)
40
+ const isSgrParameter = (code: number): boolean =>
41
+ (code >= 48 && code <= 57) || code === 59 || code === 58;
42
+
43
+ function leadingSgrSpanEndIndex(string: string): number {
44
+ let index = 0;
45
+ while (
46
+ index + 2 < string.length &&
47
+ string.codePointAt(index) === ANSI_ESC &&
48
+ string.codePointAt(index + 1) === ANSI_LEFT_BRACKET
49
+ ) {
50
+ let scan = index + 2;
51
+ while (scan < string.length && isSgrParameter(string.codePointAt(scan)!)) {
52
+ scan++;
53
+ }
54
+
55
+ if (scan < string.length && string.codePointAt(scan) === ANSI_LETTER_M) {
56
+ index = scan + 1;
57
+ continue;
58
+ }
59
+
60
+ break;
61
+ }
62
+
63
+ return index;
64
+ }
65
+
66
+ function trailingSgrSpanStartIndex(string: string): number {
67
+ let start = string.length;
68
+ while (start > 1 && string.codePointAt(start - 1) === ANSI_LETTER_M) {
69
+ let scan = start - 2;
70
+ while (scan >= 0 && isSgrParameter(string.codePointAt(scan)!)) {
71
+ scan--;
72
+ }
73
+
74
+ if (
75
+ scan >= 1 &&
76
+ string.codePointAt(scan - 1) === ANSI_ESC &&
77
+ string.codePointAt(scan) === ANSI_LEFT_BRACKET
78
+ ) {
79
+ start = scan - 1;
80
+ continue;
81
+ }
82
+
83
+ break;
84
+ }
85
+
86
+ return start;
87
+ }
88
+
89
+ // Insert the truncation character before the trailing SGR close codes, so the
90
+ // visible truncation character inherits the styling of the truncated text.
91
+ function appendWithInheritedStyleFromEnd(visible: string, suffix: string): string {
92
+ const start = trailingSgrSpanStartIndex(visible);
93
+ if (start === visible.length) {
94
+ return visible + suffix;
95
+ }
96
+
97
+ return visible.slice(0, start) + suffix + visible.slice(start);
98
+ }
99
+
100
+ function prependWithInheritedStyleFromStart(prefix: string, visible: string): string {
101
+ const end = leadingSgrSpanEndIndex(visible);
102
+ if (end === 0) {
103
+ return prefix + visible;
104
+ }
105
+
106
+ return visible.slice(0, end) + prefix + visible.slice(end);
107
+ }
108
+
109
+ export function cliTruncate(text: string, columns: number, options: TruncateOptions = {}): string {
110
+ const { position = "end", space = false, preferTruncationOnSpace = false } = options;
111
+
112
+ let { truncationCharacter = "…" } = options;
113
+
114
+ if (columns < 1) {
115
+ return "";
116
+ }
117
+
118
+ const length = stringWidth(text);
119
+
120
+ if (length <= columns) {
121
+ return text;
122
+ }
123
+
124
+ if (columns === 1) {
125
+ return truncationCharacter;
126
+ }
127
+
128
+ if (position === "start") {
129
+ if (preferTruncationOnSpace) {
130
+ const nearestSpace = getIndexOfNearestSpace(
131
+ text,
132
+ length - columns + stringWidth(truncationCharacter),
133
+ true,
134
+ );
135
+ const right = sliceAnsi(text, nearestSpace, length).trim();
136
+ return prependWithInheritedStyleFromStart(truncationCharacter, right);
137
+ }
138
+
139
+ if (space) {
140
+ truncationCharacter += " ";
141
+ }
142
+
143
+ const right = sliceAnsi(text, length - columns + stringWidth(truncationCharacter), length);
144
+ return prependWithInheritedStyleFromStart(truncationCharacter, right);
145
+ }
146
+
147
+ if (position === "middle") {
148
+ if (space) {
149
+ truncationCharacter = ` ${truncationCharacter} `;
150
+
151
+ // Drop the padding spaces if the padded character does not fit, so the
152
+ // truncation character itself never exceeds the budget.
153
+ if (stringWidth(truncationCharacter) >= columns) {
154
+ truncationCharacter = truncationCharacter.trim();
155
+ }
156
+ }
157
+
158
+ const truncationWidth = stringWidth(truncationCharacter);
159
+ // Reserve room for the truncation character before splitting the budget
160
+ // between the two sides, otherwise small budgets overflow.
161
+ const half = Math.min(Math.floor(columns / 2), Math.max(0, columns - truncationWidth));
162
+
163
+ if (preferTruncationOnSpace) {
164
+ const spaceNearFirstBreakPoint = getIndexOfNearestSpace(text, half);
165
+ const spaceNearSecondBreakPoint = getIndexOfNearestSpace(
166
+ text,
167
+ length - (columns - half) + truncationWidth,
168
+ true,
169
+ );
170
+ return (
171
+ sliceAnsi(text, 0, spaceNearFirstBreakPoint) +
172
+ truncationCharacter +
173
+ sliceAnsi(text, spaceNearSecondBreakPoint, length).trim()
174
+ );
175
+ }
176
+
177
+ return (
178
+ sliceAnsi(text, 0, half) +
179
+ truncationCharacter +
180
+ sliceAnsi(text, length - (columns - half) + truncationWidth, length)
181
+ );
182
+ }
183
+
184
+ if (preferTruncationOnSpace) {
185
+ const nearestSpace = getIndexOfNearestSpace(text, columns - stringWidth(truncationCharacter));
186
+ const left = sliceAnsi(text, 0, nearestSpace);
187
+ return appendWithInheritedStyleFromEnd(left, truncationCharacter);
188
+ }
189
+
190
+ if (space) {
191
+ truncationCharacter = ` ${truncationCharacter}`;
192
+ }
193
+
194
+ const left = sliceAnsi(text, 0, columns - stringWidth(truncationCharacter));
195
+ return appendWithInheritedStyleFromEnd(left, truncationCharacter);
196
+ }