@alchemy.run/sigil 0.0.0-alpha.3 → 0.0.0-alpha.5

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 (207) hide show
  1. package/README.md +23 -13
  2. package/THIRD_PARTY_NOTICES.md +70 -23
  3. package/dist/Text-D5HUf3Fj.d.ts +452 -0
  4. package/dist/ansi.d.ts +137 -143
  5. package/dist/ansi.js +87 -2
  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-BMzMwV7Q.d.ts +22 -0
  11. package/dist/color-policy-SVj1pYTA.js +560 -0
  12. package/dist/color-profile-DHhQHY55.js +36 -0
  13. package/dist/color-profile-u0Nhe9Nv.d.ts +97 -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-Bh4yGP6w.d.ts +186 -0
  18. package/dist/detect-BuTXtY6e.js +373 -0
  19. package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
  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-Bmc2tRPk.d.ts +21 -0
  24. package/dist/index.d.ts +235 -754
  25. package/dist/index.js +1362 -3001
  26. package/dist/osc-CCH7xDoS.js +71 -0
  27. package/dist/osc-Cn0fw77g.d.ts +23 -0
  28. package/dist/paint-C19minOS.d.ts +81 -0
  29. package/dist/query-vaIeGOkH.d.ts +152 -0
  30. package/dist/router.d.ts +392 -0
  31. package/dist/router.js +709 -0
  32. package/dist/sample-Cqw1bjUL.js +445 -0
  33. package/dist/screen-BOh__-65.js +324 -0
  34. package/dist/screen-BReKIheE.d.ts +40 -0
  35. package/dist/screen.d.ts +5 -0
  36. package/dist/screen.js +5 -0
  37. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  38. package/dist/serialize-BTkAZgw1.js +79 -0
  39. package/dist/session-BJmzX2NJ.js +664 -0
  40. package/dist/sgr-BhwaWAJB.js +246 -0
  41. package/dist/store-CgrG9K4y.d.ts +72 -0
  42. package/dist/string-width-CijQwpIk.js +69 -0
  43. package/dist/strip-BvU4toXG.js +6 -0
  44. package/dist/terminal.d.ts +118 -0
  45. package/dist/terminal.js +2 -0
  46. package/dist/tokenize-AjqbvtiT.js +1242 -0
  47. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  48. package/dist/truncate-D31fhU6i.js +562 -0
  49. package/dist/use-focus-C2sciZOo.js +1335 -0
  50. package/package.json +43 -17
  51. package/src/ansi/chalk.ts +12 -53
  52. package/src/ansi/cursor.ts +2 -4
  53. package/src/ansi/east-asian-width.ts +44 -0
  54. package/src/ansi/escapes.ts +18 -26
  55. package/src/ansi/graphemes.ts +8 -0
  56. package/src/ansi/hyperlink.ts +44 -0
  57. package/src/ansi/index.ts +14 -14
  58. package/src/ansi/osc.ts +77 -0
  59. package/src/ansi/sgr.ts +2 -5
  60. package/src/ansi/slice.ts +1 -1
  61. package/src/ansi/string-width.ts +91 -206
  62. package/src/ansi/strip.ts +13 -33
  63. package/src/ansi/tokenize.ts +96 -169
  64. package/src/ansi/truncate.ts +5 -3
  65. package/src/ansi/wrap.ts +13 -14
  66. package/src/ansi-tokenizer.ts +1 -1
  67. package/src/capabilities/color-policy.ts +34 -0
  68. package/src/capabilities/detect.ts +594 -0
  69. package/src/capabilities/index.ts +37 -0
  70. package/src/capabilities/query.ts +657 -0
  71. package/src/capabilities/store.ts +379 -0
  72. package/src/color/index.ts +3 -0
  73. package/src/color/paint.ts +169 -0
  74. package/src/color/palette.ts +48 -0
  75. package/src/color/sample.ts +323 -0
  76. package/src/color.ts +1 -0
  77. package/src/components/AnsiText.tsx +42 -0
  78. package/src/components/App.tsx +109 -22
  79. package/src/components/BackgroundContext.ts +2 -3
  80. package/src/components/Box.tsx +43 -60
  81. package/src/components/CursorContext.ts +1 -1
  82. package/src/components/ErrorBoundary.tsx +1 -1
  83. package/src/components/ErrorOverview.tsx +8 -7
  84. package/src/components/Hyperlink.tsx +56 -0
  85. package/src/components/Spacer.tsx +1 -1
  86. package/src/components/Static.tsx +1 -1
  87. package/src/components/StderrContext.ts +0 -2
  88. package/src/components/StdinContext.ts +0 -1
  89. package/src/components/StdoutContext.ts +1 -3
  90. package/src/components/TerminalOscContext.ts +25 -0
  91. package/src/components/Text.tsx +23 -46
  92. package/src/components/Transform.tsx +2 -2
  93. package/src/cursor-position.ts +1 -1
  94. package/src/devtools.ts +84 -24
  95. package/src/dom.ts +11 -8
  96. package/src/env.ts +12 -0
  97. package/src/get-max-width.ts +1 -1
  98. package/src/global.d.ts +6 -4
  99. package/src/{boxes.ts → glyphs.ts} +17 -18
  100. package/src/hooks/use-animation.ts +1 -1
  101. package/src/hooks/use-app.ts +1 -1
  102. package/src/hooks/use-box-metrics.ts +1 -1
  103. package/src/hooks/use-capabilities.ts +73 -0
  104. package/src/hooks/use-cursor.ts +3 -3
  105. package/src/hooks/use-focus-manager.ts +1 -1
  106. package/src/hooks/use-focus.ts +5 -6
  107. package/src/hooks/use-input.ts +4 -4
  108. package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
  109. package/src/hooks/use-paste.ts +2 -2
  110. package/src/hooks/use-stderr.ts +1 -1
  111. package/src/hooks/use-stdin.ts +1 -1
  112. package/src/hooks/use-stdout.ts +1 -1
  113. package/src/hooks/use-terminal-osc.ts +59 -0
  114. package/src/hooks/use-window-size.ts +2 -2
  115. package/src/index.ts +88 -44
  116. package/src/ink.tsx +620 -745
  117. package/src/input-parser.ts +1 -1
  118. package/src/instances.ts +1 -1
  119. package/src/kitty-keyboard.ts +128 -0
  120. package/src/measure-element.ts +1 -1
  121. package/src/measure-text.ts +2 -2
  122. package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
  123. package/src/parse-keypress.ts +4 -5
  124. package/src/parse-stack-line.ts +0 -1
  125. package/src/patch-console.ts +44 -0
  126. package/src/reconciler.ts +36 -62
  127. package/src/render-background.ts +37 -16
  128. package/src/render-border.ts +97 -64
  129. package/src/render-frame.ts +83 -0
  130. package/src/render-to-string.ts +24 -9
  131. package/src/render.ts +24 -16
  132. package/src/router/components.tsx +343 -0
  133. package/src/router/context.ts +41 -0
  134. package/src/router/history.ts +194 -0
  135. package/src/router/hooks.tsx +391 -0
  136. package/src/router/index.ts +34 -0
  137. package/src/router/matcher.ts +571 -0
  138. package/src/sanitize-ansi.ts +1 -1
  139. package/src/screen/ansi.ts +184 -0
  140. package/src/screen/canvas.ts +137 -0
  141. package/src/screen/cell.ts +138 -0
  142. package/src/screen/color-profile.ts +47 -0
  143. package/src/screen/geometry.ts +9 -0
  144. package/src/screen/index.ts +6 -0
  145. package/src/screen/screen.ts +272 -0
  146. package/src/screen/serialize.ts +129 -0
  147. package/src/screen.ts +1 -0
  148. package/src/semantic-text-style.ts +118 -0
  149. package/src/signal-exit.ts +4 -5
  150. package/src/squash-text-nodes.ts +4 -7
  151. package/src/stream.ts +2 -4
  152. package/src/structured-text.ts +321 -0
  153. package/src/styles.ts +16 -17
  154. package/src/terminal/index.ts +2 -0
  155. package/src/terminal/inline-presenter.ts +120 -0
  156. package/src/terminal/input.ts +86 -0
  157. package/src/terminal/render-scheduler.ts +37 -0
  158. package/src/terminal/screen-presenter.ts +188 -0
  159. package/src/terminal/session.ts +407 -0
  160. package/src/terminal-size.ts +9 -8
  161. package/src/terminal.ts +1 -0
  162. package/src/testing/browser.ts +588 -0
  163. package/src/testing/emulators.ts +205 -0
  164. package/src/testing/explorer-app/index.html +12 -0
  165. package/src/testing/explorer-app/main.ts +381 -0
  166. package/src/testing/explorer-app/style.css +194 -0
  167. package/src/testing/explorer-app/tsconfig.json +15 -0
  168. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  169. package/src/testing/index.ts +26 -0
  170. package/src/testing/keys.ts +56 -0
  171. package/src/testing/live.ts +85 -0
  172. package/src/testing/matchers.ts +70 -0
  173. package/src/testing/public.ts +94 -0
  174. package/src/testing/terminal.ts +349 -0
  175. package/src/testing/vitest.ts +157 -0
  176. package/src/transform-adapter.ts +14 -0
  177. package/src/types.ts +1 -6
  178. package/src/utils.ts +2 -2
  179. package/src/wrap-text.ts +4 -4
  180. package/src/yoga/config.ts +2 -2
  181. package/src/yoga/core/absoluteLayout.ts +15 -15
  182. package/src/yoga/core/baseline.ts +3 -3
  183. package/src/yoga/core/cache.ts +4 -4
  184. package/src/yoga/core/calculateLayout.ts +33 -27
  185. package/src/yoga/core/config.ts +1 -1
  186. package/src/yoga/core/flexLine.ts +3 -3
  187. package/src/yoga/core/helpers.ts +3 -3
  188. package/src/yoga/core/layoutResults.ts +4 -4
  189. package/src/yoga/core/node.ts +16 -16
  190. package/src/yoga/core/pixelGrid.ts +4 -4
  191. package/src/yoga/core/style.ts +11 -11
  192. package/src/yoga/core/types.ts +2 -2
  193. package/src/yoga/index.ts +6 -6
  194. package/src/yoga/node.ts +8 -8
  195. package/dist/sgr-CMfEpjSk.d.ts +0 -91
  196. package/dist/truncate-CBiyyZzw.js +0 -2156
  197. package/src/ansi/supports-color.ts +0 -213
  198. package/src/ansi/widest-line.ts +0 -12
  199. package/src/auto-bind.ts +0 -41
  200. package/src/colorize.ts +0 -60
  201. package/src/devtools-window-polyfill.ts +0 -73
  202. package/src/indent-string.ts +0 -16
  203. package/src/is-in-ci.ts +0 -7
  204. package/src/log-update.ts +0 -370
  205. package/src/output.ts +0 -308
  206. package/src/renderer.ts +0 -73
  207. package/src/write-synchronized.ts +0 -9
@@ -1,34 +1,23 @@
1
- import { isFullwidthCodePoint } from "./east-asian-width.ts";
2
- // ANSI-aware tokenizer: splits a string into grapheme clusters and ANSI
3
- // codes, tracks active styles per character, and re-emits minimal escape
4
- // sequences. This is the style model Ink's renderer is built on.
5
- // Ported from `@alcalzone/ansi-tokenize` (MIT, AlCalzone).
6
- import { BEL, C1_ST, ESC } from "./escapes.ts";
7
- import { codes as sgrCodes, foreground, background, styles } from "./sgr.ts";
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";
8
13
 
9
14
  // Single-character introducer suffixes (`ESC [` = CSI, `ESC ]` = OSC).
10
15
  const BACKSLASH = "\\";
11
16
  const CSI = "[";
12
17
  const OSC = "]";
13
18
 
14
- // Char codes
15
- const CC_BEL = BEL.charCodeAt(0);
16
- const CC_ESC = ESC.charCodeAt(0);
17
- const CC_BACKSLASH = BACKSLASH.charCodeAt(0);
18
- const CC_CSI = CSI.charCodeAt(0);
19
- const CC_OSC = OSC.charCodeAt(0);
20
- const CC_C1_ST = C1_ST.charCodeAt(0);
21
- const CC_0 = "0".charCodeAt(0);
22
- const CC_9 = "9".charCodeAt(0);
23
- const CC_SEMI = ";".charCodeAt(0);
24
- const CC_M = "m".charCodeAt(0);
25
-
26
- // Escape code points: \u001B and \u009B
27
- const ESCAPES = new Set([CC_ESC, 0x9b]);
28
-
29
19
  // OSC 8 hyperlink constants
30
20
  const linkCodePrefix = `${ESC}${OSC}8;`;
31
- const linkCodePrefixCharCodes = linkCodePrefix.split("").map((char) => char.charCodeAt(0));
32
21
  const linkCodeSuffix = BEL;
33
22
  const linkEndCode = `${ESC}${OSC}8;;${BEL}`;
34
23
  const linkEndCodeST = `${ESC}${OSC}8;;${ESC}${BACKSLASH}`;
@@ -103,6 +92,11 @@ export function getEndCode(code: string): string {
103
92
  return background.close;
104
93
  }
105
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
+
106
100
  // Otherwise find the reset code in the SGR code map
107
101
  const endCode = sgrCodes.get(Number.parseInt(code, 10));
108
102
  if (endCode !== undefined) {
@@ -126,97 +120,6 @@ export function isIntensityCode(code: AnsiCode): boolean {
126
120
  return code.code === styles.bold.open || code.code === styles.dim.open;
127
121
  }
128
122
 
129
- const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
130
-
131
- function isFullwidthGrapheme(grapheme: string, baseCodePoint: number): boolean {
132
- if (isFullwidthCodePoint(baseCodePoint)) return true;
133
- // Variation Selector 16 forces emoji presentation (2 columns wide)
134
- if (grapheme.includes("\uFE0F")) return true;
135
- // Regional indicator pairs form flag emoji (2 columns wide)
136
- if (baseCodePoint >= 0x1f1e6 && baseCodePoint <= 0x1f1ff) return true;
137
- return false;
138
- }
139
-
140
- // HOT PATH: Use only basic string/char code operations for maximum performance
141
- function parseLinkCode(string: string, offset: number): string | undefined {
142
- string = string.slice(offset);
143
- for (let index = 1; index < linkCodePrefixCharCodes.length; index++) {
144
- if (string.charCodeAt(index) !== linkCodePrefixCharCodes[index]) {
145
- return;
146
- }
147
- }
148
-
149
- // Find the semicolon that ends params
150
- const paramsEndIndex = string.indexOf(";", linkCodePrefix.length);
151
- if (paramsEndIndex === -1) return;
152
-
153
- // This is a link code (with or without the URL part). Find the end of it.
154
- const endIndex = findOscTerminatorIndex(string, paramsEndIndex + 1);
155
- if (endIndex === -1) return;
156
-
157
- return string.slice(0, endIndex + 1);
158
- }
159
-
160
- // HOT PATH: Generic fallback for non-link OSC sequences (window title, etc.)
161
- function parseOscSequence(string: string, offset: number): string | undefined {
162
- string = string.slice(offset);
163
- // Find the OSC terminator (starting after "ESC ]")
164
- const endIndex = findOscTerminatorIndex(string, 2);
165
- if (endIndex === -1) return;
166
- return string.slice(0, endIndex + 1);
167
- }
168
-
169
- /**
170
- Finds the index of the last character of the first OSC terminator at or after
171
- `startIndex`. Recognizes BEL (\u0007), C1 ST (\u009C), and ESC+backslash.
172
- Returns -1 if no terminator is found.
173
- */
174
- function findOscTerminatorIndex(string: string, startIndex: number): number {
175
- for (let index = startIndex; index < string.length; index++) {
176
- const charCode = string.charCodeAt(index);
177
- if (charCode === CC_BEL) return index;
178
- if (charCode === CC_C1_ST) return index;
179
- if (
180
- charCode === CC_ESC &&
181
- index + 1 < string.length &&
182
- string.charCodeAt(index + 1) === CC_BACKSLASH
183
- ) {
184
- return index + 1;
185
- }
186
- }
187
-
188
- return -1;
189
- }
190
-
191
- /**
192
- Scans through the given string and finds the index of the last character of an
193
- SGR sequence like `\u001B[38;2;123;123;123m`. This assumes that the string has
194
- been checked to start with `\u001B[`. Returns -1 if no valid SGR sequence is
195
- found.
196
- */
197
- function findSgrSequenceEndIndex(string: string): number {
198
- for (let index = 2; index < string.length; index++) {
199
- const charCode = string.charCodeAt(index);
200
- // m marks the end of the SGR sequence
201
- if (charCode === CC_M) return index;
202
- // Digits and semicolons are valid
203
- if (charCode === CC_SEMI) continue;
204
- if (charCode >= CC_0 && charCode <= CC_9) continue;
205
- // Everything else is invalid
206
- break;
207
- }
208
-
209
- return -1;
210
- }
211
-
212
- // HOT PATH: Use only basic string/char code operations for maximum performance
213
- function parseSgrSequence(string: string, offset: number): string | undefined {
214
- string = string.slice(offset);
215
- const endIndex = findSgrSequenceEndIndex(string);
216
- if (endIndex === -1) return;
217
- return string.slice(0, endIndex + 1);
218
- }
219
-
220
123
  /**
221
124
  Splits compound SGR sequences like `\u001B[1;3;31m` into individual components.
222
125
  */
@@ -235,7 +138,7 @@ function splitCompoundSgrSequences(code: string): string[] {
235
138
  for (let index = 0; index < codeParts.length; index++) {
236
139
  const rawCode = codeParts[index]!;
237
140
  // Keep 8-bit and 24-bit color codes (containing multiple ";") together
238
- if (rawCode === "38" || rawCode === "48") {
141
+ if (rawCode === "38" || rawCode === "48" || rawCode === "58") {
239
142
  if (index + 2 < codeParts.length && codeParts[index + 1] === "5") {
240
143
  // 8-bit color, followed by another number
241
144
  result.push(codeParts.slice(index, index + 3).join(";"));
@@ -256,73 +159,97 @@ function splitCompoundSgrSequences(code: string): string[] {
256
159
  return result.map((part) => `${ESC}[${part}m`);
257
160
  }
258
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
+
259
167
  export function tokenize(string: string, endChar = Number.POSITIVE_INFINITY): Token[] {
260
168
  const result: Token[] = [];
261
169
  let visible = 0;
262
- let codeEndIndex = 0;
263
-
264
- for (const { segment, index } of segmenter.segment(string)) {
265
- // Skip segments consumed as part of an ANSI sequence
266
- if (index < codeEndIndex) continue;
267
-
268
- const codePoint = segment.codePointAt(0)!;
269
- if (ESCAPES.has(codePoint)) {
270
- let code: string | undefined;
271
- // Peek the next code point to determine the type of ANSI sequence
272
- const nextCodePoint = string.codePointAt(index + 1);
273
- if (nextCodePoint === CC_OSC) {
274
- // ] = operating system commands
275
- code = parseLinkCode(string, index);
276
- if (code) {
277
- // OSC 8 hyperlinks are paired codes with an endCode
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)) {
278
201
  result.push({
279
202
  type: "ansi",
280
- code,
281
- endCode: getEndCode(code),
203
+ code: individualCode,
204
+ endCode: getEndCode(individualCode),
282
205
  });
283
- } else {
284
- // Other OSC sequences (window title, etc.) are self-contained
285
- // control codes with no endCode.
286
- code = parseOscSequence(string, index);
287
- if (code) {
288
- result.push({
289
- type: "control",
290
- code,
291
- });
292
- }
293
- }
294
- } else if (nextCodePoint === CC_CSI) {
295
- // [ = control sequence introducer, like SGR sequences [...m
296
- code = parseSgrSequence(string, index);
297
- if (code) {
298
- // Split compound codes into individual tokens
299
- for (const individualCode of splitCompoundSgrSequences(code)) {
300
- result.push({
301
- type: "ansi",
302
- code: individualCode,
303
- endCode: getEndCode(individualCode),
304
- });
305
- }
306
206
  }
207
+ } else {
208
+ result.push({
209
+ type: "control",
210
+ code: ansiToken.value,
211
+ });
307
212
  }
308
213
 
309
- if (code) {
310
- codeEndIndex = index + code.length;
311
- continue;
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
+ });
312
241
  }
242
+
243
+ continue;
313
244
  }
314
245
 
315
- const fullWidth = isFullwidthGrapheme(segment, codePoint);
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.
316
249
  result.push({
317
- type: "char",
318
- value: segment,
319
- fullWidth,
250
+ type: "control",
251
+ code: ansiToken.value,
320
252
  });
321
-
322
- visible += fullWidth ? 2 : 1;
323
- if (visible >= endChar) {
324
- break;
325
- }
326
253
  }
327
254
 
328
255
  return result;
@@ -377,7 +304,7 @@ Returns the combination of ANSI codes needed to undo the given ANSI codes.
377
304
  */
378
305
  export function undoAnsiCodes(codes: readonly AnsiCode[]): AnsiCode[] {
379
306
  return reduceAnsiCodes(codes)
380
- .reverse()
307
+ .toReversed()
381
308
  .map((code) => ({
382
309
  ...code,
383
310
  code: code.endCode,
@@ -1,8 +1,8 @@
1
1
  // Truncate a string to a visible width, ANSI-aware.
2
2
  // Ported from `cli-truncate` (MIT, Sindre Sorhus) on top of the shared
3
3
  // slice and width modules.
4
- import { sliceAnsi } from "./slice.ts";
5
- import { stringWidth } from "./string-width.ts";
4
+ import { sliceAnsi } from "#/ansi/slice.ts";
5
+ import { stringWidth } from "#/ansi/string-width.ts";
6
6
 
7
7
  export type TruncateOptions = {
8
8
  readonly position?: "start" | "middle" | "end";
@@ -36,7 +36,9 @@ const ANSI_ESC = 27;
36
36
  const ANSI_LEFT_BRACKET = 91;
37
37
  const ANSI_LETTER_M = 109;
38
38
 
39
- const isSgrParameter = (code: number): boolean => (code >= 48 && code <= 57) || code === 59; // 0-9 or ;
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;
40
42
 
41
43
  function leadingSgrSpanEndIndex(string: string): number {
42
44
  let index = 0;
package/src/ansi/wrap.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  // Word-wrap a string to a column width, ANSI-aware.
2
2
  // Ported from `wrap-ansi` (MIT, Sindre Sorhus).
3
- import { BEL, C1_CSI, ESC } from "./escapes.ts";
3
+ import { BEL, C1_CSI, ESC } from "#/ansi/escapes.ts";
4
4
  //
5
5
  // Supported boundary: semicolon-delimited SGR styling, colon-delimited
6
6
  // RGB/indexed colors, and OSC 8 hyperlinks are tracked, while ordinary CSI
7
7
  // sequences and other complete 7-bit OSC commands are preserved as opaque
8
8
  // zero-width units. This is intentionally not a terminal emulator. Newlines
9
9
  // always delimit input lines before ANSI parsing.
10
- import { codes as sgrCodes } from "./sgr.ts";
11
- import { stringWidth } from "./string-width.ts";
10
+ import { codes as sgrCodes } from "#/ansi/sgr.ts";
11
+ import { stringWidth } from "#/ansi/string-width.ts";
12
12
 
13
13
  export type WrapOptions = {
14
14
  readonly trim?: boolean;
@@ -59,8 +59,7 @@ const segmenter = new Intl.Segmenter();
59
59
  // Complete ANSI sequences have already been removed before measuring these
60
60
  // strings. Avoid string-width's ANSI scan so malformed sequences are not
61
61
  // rescanned.
62
- const getStringWidth = (string: string): number =>
63
- stringWidth(string, { countAnsiEscapeCodes: true });
62
+ const getStringWidth = (string: string): number => stringWidth(string);
64
63
  const TAB_SIZE = 8;
65
64
 
66
65
  // Finds the next character that could introduce a sequence, so plain text is
@@ -221,18 +220,18 @@ type ActiveStyle = {
221
220
  readonly close: number;
222
221
  };
223
222
 
223
+ const EXTENDED_COLOR_CODES = new Set([
224
+ ANSI_SGR_FOREGROUND_EXTENDED,
225
+ ANSI_SGR_BACKGROUND_EXTENDED,
226
+ ANSI_SGR_UNDERLINE_COLOR_EXTENDED,
227
+ ]);
228
+
224
229
  const getColonColorToken = (parameter: string): SgrToken | undefined => {
225
230
  const parts = parameter.split(":");
226
231
  const code = Number.parseInt(parts[0]!, 10);
227
232
  const mode = Number.parseInt(parts[1]!, 10);
228
233
 
229
- if (
230
- ![
231
- ANSI_SGR_FOREGROUND_EXTENDED,
232
- ANSI_SGR_BACKGROUND_EXTENDED,
233
- ANSI_SGR_UNDERLINE_COLOR_EXTENDED,
234
- ].includes(code)
235
- ) {
234
+ if (!EXTENDED_COLOR_CODES.has(code)) {
236
235
  return;
237
236
  }
238
237
 
@@ -467,8 +466,8 @@ const applyLeadingSgrResets = (
467
466
  };
468
467
 
469
468
  const getClosingSgrSequence = (activeStyles: readonly ActiveStyle[]): string =>
470
- [...activeStyles]
471
- .reverse()
469
+ activeStyles
470
+ .toReversed()
472
471
  .map((activeStyle) => wrapAnsiCode(activeStyle.close))
473
472
  .join("");
474
473
 
@@ -1,4 +1,4 @@
1
- import { BEL, C1_CSI, C1_ST, ESC } from "./ansi/escapes.ts";
1
+ import { BEL, C1_CSI, C1_ST, ESC } from "#/ansi/escapes.ts";
2
2
 
3
3
  const bellCharacter = BEL;
4
4
  const escapeCharacter = ESC;
@@ -0,0 +1,34 @@
1
+ import type { ColorSupportLevel } from "#/capabilities/detect.ts";
2
+ import { colorProfileFromLevel, type ColorProfile } from "#/screen/color-profile.ts";
3
+
4
+ /** A user-selected output policy. `auto` follows the target stream. */
5
+ export type ColorPolicy = "auto" | ColorProfile;
6
+
7
+ /** The three distinct inputs and result of terminal color negotiation. */
8
+ export type ColorState = {
9
+ /** The stream/terminal fact after environment and query detection. */
10
+ readonly detected: ColorProfile;
11
+ /** The application's policy for this render instance. */
12
+ readonly policy: ColorPolicy;
13
+ /** The profile the serializer must actually emit. */
14
+ readonly effective: ColorProfile;
15
+ };
16
+
17
+ export function resolveColorProfile(
18
+ detectedLevel: ColorSupportLevel,
19
+ policy: ColorPolicy = "auto",
20
+ ): ColorProfile {
21
+ return policy === "auto" ? colorProfileFromLevel(detectedLevel) : policy;
22
+ }
23
+
24
+ export function colorState(
25
+ capabilities: { readonly color: { readonly level: ColorSupportLevel } },
26
+ policy: ColorPolicy = "auto",
27
+ ): ColorState {
28
+ const detected = colorProfileFromLevel(capabilities.color.level);
29
+ return {
30
+ detected,
31
+ policy,
32
+ effective: policy === "auto" ? detected : policy,
33
+ };
34
+ }