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

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 (121) hide show
  1. package/README.md +299 -299
  2. package/dist/ansi.d.ts +223 -0
  3. package/dist/ansi.js +2 -0
  4. package/dist/{devtools-QpCMm9JH.mjs → devtools-BhYGjb7h.js} +1 -1
  5. package/dist/index-DDVME65c.d.ts +919 -0
  6. package/dist/index.d.ts +1657 -0
  7. package/dist/index.js +4643 -0
  8. package/dist/sgr-CMfEpjSk.d.ts +91 -0
  9. package/dist/truncate-CBiyyZzw.js +2156 -0
  10. package/dist/yoga-5jKhYCJC.js +3465 -0
  11. package/dist/yoga.d.ts +2 -0
  12. package/dist/yoga.js +2 -0
  13. package/package.json +37 -17
  14. package/src/ansi/chalk.ts +179 -0
  15. package/src/ansi/cursor.ts +48 -0
  16. package/src/ansi/east-asian-width.ts +215 -0
  17. package/src/ansi/escapes.ts +128 -0
  18. package/src/ansi/index.ts +27 -0
  19. package/src/ansi/sgr.ts +237 -0
  20. package/src/ansi/slice.ts +43 -0
  21. package/src/ansi/string-width.ts +236 -0
  22. package/src/ansi/strip.ts +33 -0
  23. package/src/ansi/supports-color.ts +213 -0
  24. package/src/ansi/tokenize.ts +453 -0
  25. package/src/ansi/truncate.ts +194 -0
  26. package/src/ansi/widest-line.ts +12 -0
  27. package/src/ansi/wrap.ts +766 -0
  28. package/src/ansi-tokenizer.ts +510 -0
  29. package/src/auto-bind.ts +41 -0
  30. package/src/boxes.ts +100 -0
  31. package/src/code-excerpt.ts +39 -0
  32. package/src/colorize.ts +60 -0
  33. package/src/components/AccessibilityContext.ts +5 -0
  34. package/src/components/AnimationContext.ts +24 -0
  35. package/src/components/App.tsx +781 -0
  36. package/src/components/AppContext.ts +111 -0
  37. package/src/components/BackgroundContext.ts +8 -0
  38. package/src/components/Box.tsx +116 -0
  39. package/src/components/CursorContext.ts +19 -0
  40. package/src/components/ErrorBoundary.tsx +38 -0
  41. package/src/components/ErrorOverview.tsx +133 -0
  42. package/src/components/FocusContext.ts +30 -0
  43. package/src/components/Newline.tsx +15 -0
  44. package/src/components/Spacer.tsx +10 -0
  45. package/src/components/Static.tsx +59 -0
  46. package/src/components/StderrContext.ts +26 -0
  47. package/src/components/StdinContext.ts +49 -0
  48. package/src/components/StdoutContext.ts +28 -0
  49. package/src/components/Text.tsx +144 -0
  50. package/src/components/Transform.tsx +37 -0
  51. package/src/cursor-position.ts +103 -0
  52. package/src/devtools-window-polyfill.ts +73 -0
  53. package/src/devtools.ts +43 -0
  54. package/src/dom.ts +292 -0
  55. package/src/get-max-width.ts +11 -0
  56. package/src/global.d.ts +36 -0
  57. package/src/hooks/use-animation.ts +142 -0
  58. package/src/hooks/use-app.ts +8 -0
  59. package/src/hooks/use-box-metrics.ts +134 -0
  60. package/src/hooks/use-cursor.ts +33 -0
  61. package/src/hooks/use-focus-manager.ts +62 -0
  62. package/src/hooks/use-focus.ts +83 -0
  63. package/src/hooks/use-input.ts +267 -0
  64. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  65. package/src/hooks/use-paste.ts +78 -0
  66. package/src/hooks/use-stderr.ts +8 -0
  67. package/src/hooks/use-stdin.ts +10 -0
  68. package/src/hooks/use-stdout.ts +8 -0
  69. package/src/hooks/use-window-size.ts +41 -0
  70. package/src/indent-string.ts +16 -0
  71. package/src/index.ts +44 -0
  72. package/src/ink.tsx +1506 -0
  73. package/src/input-parser.ts +283 -0
  74. package/src/instances.ts +9 -0
  75. package/src/is-in-ci.ts +7 -0
  76. package/src/kitty-keyboard.ts +57 -0
  77. package/src/log-update.ts +370 -0
  78. package/src/measure-element.ts +62 -0
  79. package/src/measure-text.ts +31 -0
  80. package/src/output.ts +308 -0
  81. package/src/parse-keypress.ts +516 -0
  82. package/src/parse-stack-line.ts +139 -0
  83. package/src/patch-console.ts +62 -0
  84. package/src/quick-lru.ts +85 -0
  85. package/src/reconciler.ts +451 -0
  86. package/src/render-background.ts +38 -0
  87. package/src/render-border.ts +134 -0
  88. package/src/render-node-to-output.ts +191 -0
  89. package/src/render-to-string.ts +131 -0
  90. package/src/render.ts +276 -0
  91. package/src/renderer.ts +73 -0
  92. package/src/sanitize-ansi.ts +33 -0
  93. package/src/signal-exit.ts +107 -0
  94. package/src/squash-text-nodes.ts +40 -0
  95. package/src/stream.ts +30 -0
  96. package/src/styles.ts +748 -0
  97. package/src/terminal-size.ts +57 -0
  98. package/src/throttle.ts +73 -0
  99. package/src/types.ts +15 -0
  100. package/src/utils.ts +40 -0
  101. package/src/wrap-text.ts +50 -0
  102. package/src/write-synchronized.ts +9 -0
  103. package/src/yoga/config.ts +57 -0
  104. package/src/yoga/core/absoluteLayout.ts +626 -0
  105. package/src/yoga/core/baseline.ts +66 -0
  106. package/src/yoga/core/cache.ts +136 -0
  107. package/src/yoga/core/calculateLayout.ts +2920 -0
  108. package/src/yoga/core/config.ts +104 -0
  109. package/src/yoga/core/flexLine.ts +177 -0
  110. package/src/yoga/core/helpers.ts +293 -0
  111. package/src/yoga/core/layoutResults.ts +167 -0
  112. package/src/yoga/core/node.ts +611 -0
  113. package/src/yoga/core/numeric.ts +44 -0
  114. package/src/yoga/core/pixelGrid.ts +151 -0
  115. package/src/yoga/core/style.ts +887 -0
  116. package/src/yoga/core/types.ts +224 -0
  117. package/src/yoga/generated/YGEnums.ts +263 -0
  118. package/src/yoga/index.ts +19 -0
  119. package/src/yoga/node.ts +1140 -0
  120. package/dist/index.d.mts +0 -2379
  121. package/dist/index.mjs +0 -10072
@@ -0,0 +1,27 @@
1
+ // Standalone ANSI toolkit — styling, measurement, and manipulation of
2
+ // terminal strings. Importable without the React renderer via
3
+ // `@alchemy.run/sigil/ansi`.
4
+
5
+ // Styling
6
+ export { chalk } from "./chalk.ts";
7
+ export { supportsColor } from "./supports-color.ts";
8
+ export * from "./sgr.ts";
9
+
10
+ // Escape sequences (raw building blocks + named sequences)
11
+ export { ansiEscapes } from "./escapes.ts";
12
+ export * from "./escapes.ts";
13
+ export { cliCursor } from "./cursor.ts";
14
+
15
+ // Measurement
16
+ export { stringWidth } from "./string-width.ts";
17
+ export { widestLine } from "./widest-line.ts";
18
+ export * from "./east-asian-width.ts";
19
+
20
+ // String manipulation
21
+ export { stripAnsi, ansiRegex } from "./strip.ts";
22
+ export { sliceAnsi } from "./slice.ts";
23
+ export { cliTruncate as truncateAnsi } from "./truncate.ts";
24
+ export { wrapAnsi } from "./wrap.ts";
25
+
26
+ // Tokenizer: style-aware parsing and minimal re-emission
27
+ export * from "./tokenize.ts";
@@ -0,0 +1,237 @@
1
+ // ANSI SGR style tables and color-space conversions.
2
+ // Ported from `ansi-styles` (MIT, Sindre Sorhus).
3
+ import { CSI } from "./escapes.ts";
4
+
5
+ const ANSI_BACKGROUND_OFFSET = 10;
6
+
7
+ export type StylePair = {
8
+ readonly open: string;
9
+ readonly close: string;
10
+ };
11
+
12
+ const wrapAnsi16 =
13
+ (offset = 0) =>
14
+ (code: number): string =>
15
+ `${CSI}${code + offset}m`;
16
+
17
+ const wrapAnsi256 =
18
+ (offset = 0) =>
19
+ (code: number): string =>
20
+ `${CSI}${38 + offset};5;${code}m`;
21
+
22
+ const wrapAnsi16m =
23
+ (offset = 0) =>
24
+ (red: number, green: number, blue: number): string =>
25
+ `${CSI}${38 + offset};2;${red};${green};${blue}m`;
26
+
27
+ const modifierCodes = {
28
+ reset: [0, 0],
29
+ // 21 isn't widely supported and 22 does the same thing
30
+ bold: [1, 22],
31
+ dim: [2, 22],
32
+ italic: [3, 23],
33
+ underline: [4, 24],
34
+ overline: [53, 55],
35
+ inverse: [7, 27],
36
+ hidden: [8, 28],
37
+ strikethrough: [9, 29],
38
+ } as const;
39
+
40
+ const colorCodes = {
41
+ black: [30, 39],
42
+ red: [31, 39],
43
+ green: [32, 39],
44
+ yellow: [33, 39],
45
+ blue: [34, 39],
46
+ magenta: [35, 39],
47
+ cyan: [36, 39],
48
+ white: [37, 39],
49
+
50
+ // Bright colors
51
+ blackBright: [90, 39],
52
+ gray: [90, 39], // Alias of `blackBright`
53
+ grey: [90, 39], // Alias of `blackBright`
54
+ redBright: [91, 39],
55
+ greenBright: [92, 39],
56
+ yellowBright: [93, 39],
57
+ blueBright: [94, 39],
58
+ magentaBright: [95, 39],
59
+ cyanBright: [96, 39],
60
+ whiteBright: [97, 39],
61
+ } as const;
62
+
63
+ const bgColorCodes = {
64
+ bgBlack: [40, 49],
65
+ bgRed: [41, 49],
66
+ bgGreen: [42, 49],
67
+ bgYellow: [43, 49],
68
+ bgBlue: [44, 49],
69
+ bgMagenta: [45, 49],
70
+ bgCyan: [46, 49],
71
+ bgWhite: [47, 49],
72
+
73
+ // Bright colors
74
+ bgBlackBright: [100, 49],
75
+ bgGray: [100, 49], // Alias of `bgBlackBright`
76
+ bgGrey: [100, 49], // Alias of `bgBlackBright`
77
+ bgRedBright: [101, 49],
78
+ bgGreenBright: [102, 49],
79
+ bgYellowBright: [103, 49],
80
+ bgBlueBright: [104, 49],
81
+ bgMagentaBright: [105, 49],
82
+ bgCyanBright: [106, 49],
83
+ bgWhiteBright: [107, 49],
84
+ } as const;
85
+
86
+ export type ModifierName = keyof typeof modifierCodes;
87
+ export type ForegroundColorName = keyof typeof colorCodes;
88
+ export type BackgroundColorName = keyof typeof bgColorCodes;
89
+ export type StyleName = ModifierName | ForegroundColorName | BackgroundColorName;
90
+
91
+ export const modifierNames = Object.keys(modifierCodes) as ModifierName[];
92
+ export const foregroundColorNames = Object.keys(colorCodes) as ForegroundColorName[];
93
+ export const backgroundColorNames = Object.keys(bgColorCodes) as BackgroundColorName[];
94
+
95
+ const toPairs = <Name extends string>(
96
+ codes: Record<Name, readonly [number, number]>,
97
+ ): Record<Name, StylePair> => {
98
+ const result = {} as Record<Name, StylePair>;
99
+
100
+ for (const [name, [open, close]] of Object.entries(codes) as Array<
101
+ [Name, readonly [number, number]]
102
+ >) {
103
+ result[name] = { open: `${CSI}${open}m`, close: `${CSI}${close}m` };
104
+ }
105
+
106
+ return result;
107
+ };
108
+
109
+ /**
110
+ Named SGR styles as `{open, close}` escape sequence pairs.
111
+ */
112
+ export const styles: Record<StyleName, StylePair> = {
113
+ ...toPairs(modifierCodes),
114
+ ...toPairs(colorCodes),
115
+ ...toPairs(bgColorCodes),
116
+ };
117
+
118
+ /**
119
+ Raw SGR code numbers: open code → close code.
120
+ */
121
+ export const codes: ReadonlyMap<number, number> = new Map(
122
+ [
123
+ ...Object.values<readonly [number, number]>(modifierCodes),
124
+ ...Object.values<readonly [number, number]>(colorCodes),
125
+ ...Object.values<readonly [number, number]>(bgColorCodes),
126
+ ].map(([open, close]) => [open, close]),
127
+ );
128
+
129
+ export const foreground = {
130
+ close: `${CSI}39m`,
131
+ ansi: wrapAnsi16(),
132
+ ansi256: wrapAnsi256(),
133
+ ansi16m: wrapAnsi16m(),
134
+ };
135
+
136
+ export const background = {
137
+ close: `${CSI}49m`,
138
+ ansi: wrapAnsi16(ANSI_BACKGROUND_OFFSET),
139
+ ansi256: wrapAnsi256(ANSI_BACKGROUND_OFFSET),
140
+ ansi16m: wrapAnsi16m(ANSI_BACKGROUND_OFFSET),
141
+ };
142
+
143
+ // Conversions from https://github.com/Qix-/color-convert
144
+ export const rgbToAnsi256 = (red: number, green: number, blue: number): number => {
145
+ // We use the extended greyscale palette here, with the exception of
146
+ // black and white. The normal palette only has 4 greyscale shades.
147
+ if (red === green && green === blue) {
148
+ if (red < 8) {
149
+ return 16;
150
+ }
151
+
152
+ if (red > 248) {
153
+ return 231;
154
+ }
155
+
156
+ return Math.round(((red - 8) / 247) * 24) + 232;
157
+ }
158
+
159
+ return (
160
+ 16 +
161
+ 36 * Math.round((red / 255) * 5) +
162
+ 6 * Math.round((green / 255) * 5) +
163
+ Math.round((blue / 255) * 5)
164
+ );
165
+ };
166
+
167
+ export const hexToRgb = (hex: string): [number, number, number] => {
168
+ const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex);
169
+ if (!matches) {
170
+ return [0, 0, 0];
171
+ }
172
+
173
+ let [colorString] = matches;
174
+
175
+ if (colorString.length === 3) {
176
+ colorString = colorString
177
+ .split("")
178
+ .map((character) => character + character)
179
+ .join("");
180
+ }
181
+
182
+ const integer = Number.parseInt(colorString, 16);
183
+
184
+ /* eslint-disable no-bitwise */
185
+ return [(integer >> 16) & 0xff, (integer >> 8) & 0xff, integer & 0xff];
186
+ /* eslint-enable no-bitwise */
187
+ };
188
+
189
+ export const hexToAnsi256 = (hex: string): number => rgbToAnsi256(...hexToRgb(hex));
190
+
191
+ export const ansi256ToAnsi = (code: number): number => {
192
+ if (code < 8) {
193
+ return 30 + code;
194
+ }
195
+
196
+ if (code < 16) {
197
+ return 90 + (code - 8);
198
+ }
199
+
200
+ let red;
201
+ let green;
202
+ let blue;
203
+
204
+ if (code >= 232) {
205
+ red = ((code - 232) * 10 + 8) / 255;
206
+ green = red;
207
+ blue = red;
208
+ } else {
209
+ code -= 16;
210
+
211
+ const remainder = code % 36;
212
+
213
+ red = Math.floor(code / 36) / 5;
214
+ green = Math.floor(remainder / 6) / 5;
215
+ blue = (remainder % 6) / 5;
216
+ }
217
+
218
+ const value = Math.max(red, green, blue) * 2;
219
+
220
+ if (value === 0) {
221
+ return 30;
222
+ }
223
+
224
+ // eslint-disable-next-line no-bitwise
225
+ let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red));
226
+
227
+ if (value === 2) {
228
+ result += 60;
229
+ }
230
+
231
+ return result;
232
+ };
233
+
234
+ export const rgbToAnsi = (red: number, green: number, blue: number): number =>
235
+ ansi256ToAnsi(rgbToAnsi256(red, green, blue));
236
+
237
+ export const hexToAnsi = (hex: string): number => ansi256ToAnsi(hexToAnsi256(hex));
@@ -0,0 +1,43 @@
1
+ // Slice a string by visible column positions, preserving active ANSI styles.
2
+ // Replaces `slice-ansi`, built on the shared tokenizer instead of a second
3
+ // escape parser: characters keep their computed style lists, and the result
4
+ // is re-emitted with minimal escape codes. A full-width character that would
5
+ // be cut in half at either boundary is dropped, like `slice-ansi` does.
6
+ import {
7
+ styledCharsFromTokens,
8
+ styledCharsToString,
9
+ tokenize,
10
+ type StyledChar,
11
+ } from "./tokenize.ts";
12
+
13
+ export function sliceAnsi(string: string, start: number, end?: number): string {
14
+ const sliceEnd = end ?? Number.POSITIVE_INFINITY;
15
+
16
+ if (start >= sliceEnd || string === "") {
17
+ return "";
18
+ }
19
+
20
+ if (start === 0 && sliceEnd === Number.POSITIVE_INFINITY) {
21
+ return string;
22
+ }
23
+
24
+ const chars = styledCharsFromTokens(tokenize(string));
25
+ const included: StyledChar[] = [];
26
+ let column = 0;
27
+
28
+ for (const char of chars) {
29
+ const width = char.fullWidth ? 2 : 1;
30
+
31
+ if (column + width > sliceEnd) {
32
+ break;
33
+ }
34
+
35
+ if (column >= start) {
36
+ included.push(char);
37
+ }
38
+
39
+ column += width;
40
+ }
41
+
42
+ return styledCharsToString(included);
43
+ }
@@ -0,0 +1,236 @@
1
+ import { eastAsianWidth } from "./east-asian-width.ts";
2
+ // Visual width of a string: how many terminal columns it occupies.
3
+ // Ported from `string-width` (MIT, Sindre Sorhus).
4
+ //
5
+ // Logic:
6
+ // - Segment graphemes to match how terminals render clusters.
7
+ // - Width rules:
8
+ // 1. Skip non-printing clusters (Default_Ignorable, Control, pure
9
+ // nonspacing/enclosing Mark, lone Surrogates). Tabs are ignored by design.
10
+ // 2. RGI emoji clusters (\p{RGI_Emoji}) are double-width.
11
+ // 3. Minimally-qualified/unqualified emoji clusters (ZWJ sequences with 2+
12
+ // Extended_Pictographic, or keycap sequences) are double-width.
13
+ // 4. Hangul jamo collapse each standard modern Hangul L+V or L+V+T syllable
14
+ // piece to width 2. Unmatched repeated leading/vowel/trailing jamo stay
15
+ // additive because that matches how the terminals we target render them.
16
+ // 5. Otherwise use East Asian Width of the cluster's first visible code
17
+ // point, and add widths for trailing spacing marks and Halfwidth/Fullwidth
18
+ // Forms within the same cluster (e.g., dakuten/handakuten).
19
+ import { C1_CSI, ESC } from "./escapes.ts";
20
+ import { stripAnsi } from "./strip.ts";
21
+
22
+ export type StringWidthOptions = {
23
+ readonly ambiguousIsNarrow?: boolean;
24
+ readonly countAnsiEscapeCodes?: boolean;
25
+ };
26
+
27
+ type EastAsianWidthOptions = {
28
+ readonly ambiguousAsWide: boolean;
29
+ };
30
+
31
+ const segmenter = new Intl.Segmenter();
32
+
33
+ // Whole-cluster zero-width
34
+ const zeroWidthClusterRegex =
35
+ /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Format}|\p{Nonspacing_Mark}|\p{Enclosing_Mark}|\p{Surrogate})+$/v;
36
+
37
+ // Pick the base scalar if the cluster starts with Prepend/Format/Marks
38
+ const leadingNonPrintingRegex =
39
+ /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Nonspacing_Mark}\p{Enclosing_Mark}\p{Surrogate}]+/v;
40
+ const spacingMarkRegex = /\p{Spacing_Mark}/v;
41
+
42
+ // RGI emoji sequences
43
+ const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
44
+
45
+ // Detect minimally-qualified/unqualified emoji sequences (missing VS16 but
46
+ // still rendering as double-width)
47
+ const unqualifiedKeycapRegex = /^[\d#*]\u20E3$/;
48
+ const extendedPictographicRegex = /\p{Extended_Pictographic}/gu;
49
+
50
+ function isDoubleWidthNonRgiEmojiSequence(segment: string): boolean {
51
+ // Real emoji clusters are < 30 chars; guard against pathological input
52
+ if (segment.length > 50) {
53
+ return false;
54
+ }
55
+
56
+ if (unqualifiedKeycapRegex.test(segment)) {
57
+ return true;
58
+ }
59
+
60
+ // ZWJ sequences with 2+ Extended_Pictographic
61
+ if (segment.includes("\u200D")) {
62
+ const pictographics = segment.match(extendedPictographicRegex);
63
+ return pictographics !== null && pictographics.length >= 2;
64
+ }
65
+
66
+ return false;
67
+ }
68
+
69
+ function baseVisible(segment: string): string {
70
+ return segment.replace(leadingNonPrintingRegex, "");
71
+ }
72
+
73
+ function isZeroWidthCluster(segment: string): boolean {
74
+ return zeroWidthClusterRegex.test(segment);
75
+ }
76
+
77
+ function isHangulLeadingJamo(codePoint: number | undefined): boolean {
78
+ return (
79
+ codePoint !== undefined &&
80
+ ((codePoint >= 0x11_00 && codePoint <= 0x11_5f) ||
81
+ (codePoint >= 0xa9_60 && codePoint <= 0xa9_7c))
82
+ );
83
+ }
84
+
85
+ function isHangulVowelJamo(codePoint: number | undefined): boolean {
86
+ return (
87
+ codePoint !== undefined &&
88
+ ((codePoint >= 0x11_60 && codePoint <= 0x11_a7) ||
89
+ (codePoint >= 0xd7_b0 && codePoint <= 0xd7_c6))
90
+ );
91
+ }
92
+
93
+ function isHangulTrailingJamo(codePoint: number | undefined): boolean {
94
+ return (
95
+ codePoint !== undefined &&
96
+ ((codePoint >= 0x11_a8 && codePoint <= 0x11_ff) ||
97
+ (codePoint >= 0xd7_cb && codePoint <= 0xd7_fb))
98
+ );
99
+ }
100
+
101
+ function isHangulJamo(codePoint: number): boolean {
102
+ return (
103
+ isHangulLeadingJamo(codePoint) ||
104
+ isHangulVowelJamo(codePoint) ||
105
+ isHangulTrailingJamo(codePoint)
106
+ );
107
+ }
108
+
109
+ function hangulClusterWidth(
110
+ visibleSegment: string,
111
+ eastAsianWidthOptions: EastAsianWidthOptions,
112
+ ): number | undefined {
113
+ const codePoints: number[] = [];
114
+
115
+ for (const character of visibleSegment) {
116
+ if (zeroWidthClusterRegex.test(character)) {
117
+ continue;
118
+ }
119
+
120
+ codePoints.push(character.codePointAt(0)!);
121
+ }
122
+
123
+ if (codePoints.length === 0) {
124
+ return;
125
+ }
126
+
127
+ let width = 0;
128
+
129
+ for (let index = 0; index < codePoints.length; index++) {
130
+ const codePoint = codePoints[index]!;
131
+ if (!isHangulJamo(codePoint)) {
132
+ if (width === 0) {
133
+ return;
134
+ }
135
+
136
+ // Mixed cluster (e.g., L + precomposed syllable): use EAW for the
137
+ // non-jamo remainder
138
+ for (let remaining = index; remaining < codePoints.length; remaining++) {
139
+ width += eastAsianWidth(codePoints[remaining]!, eastAsianWidthOptions);
140
+ }
141
+
142
+ return width;
143
+ }
144
+
145
+ // Modern Hangul L+V(+T) shapes as one syllable block. Unmatched jamo stay
146
+ // additive: U+1100 U+1100 U+1161 => U+1100 + (U+1100 U+1161) => 2 + 2.
147
+ if (isHangulLeadingJamo(codePoint) && isHangulVowelJamo(codePoints[index + 1])) {
148
+ width += 2;
149
+ index += isHangulTrailingJamo(codePoints[index + 2]) ? 2 : 1;
150
+ continue;
151
+ }
152
+
153
+ width += eastAsianWidth(codePoint, eastAsianWidthOptions);
154
+ }
155
+
156
+ return width;
157
+ }
158
+
159
+ function trailingWidth(
160
+ visibleSegment: string,
161
+ eastAsianWidthOptions: EastAsianWidthOptions,
162
+ ): number {
163
+ let extra = 0;
164
+ let first = true;
165
+
166
+ for (const character of visibleSegment) {
167
+ if (first) {
168
+ first = false;
169
+ continue;
170
+ }
171
+
172
+ if (spacingMarkRegex.test(character) || (character >= "\uFF00" && character <= "\uFFEF")) {
173
+ extra += eastAsianWidth(character.codePointAt(0)!, eastAsianWidthOptions);
174
+ }
175
+ }
176
+
177
+ return extra;
178
+ }
179
+
180
+ export function stringWidth(input: string, options: StringWidthOptions = {}): number {
181
+ if (typeof input !== "string" || input.length === 0) {
182
+ return 0;
183
+ }
184
+
185
+ const { ambiguousIsNarrow = true, countAnsiEscapeCodes = false } = options;
186
+
187
+ let string = input;
188
+
189
+ // Avoid calling stripAnsi when there are no ANSI escape sequences
190
+ // (ESC = 0x1B, CSI = 0x9B)
191
+ if (!countAnsiEscapeCodes && (string.includes(ESC) || string.includes(C1_CSI))) {
192
+ string = stripAnsi(string);
193
+ }
194
+
195
+ if (string.length === 0) {
196
+ return 0;
197
+ }
198
+
199
+ // Fast path: printable ASCII (0x20–0x7E) needs no segmenter, regex, or EAW
200
+ // lookup — width equals length.
201
+ if (/^[ -~]*$/.test(string)) {
202
+ return string.length;
203
+ }
204
+
205
+ let width = 0;
206
+ const eastAsianWidthOptions: EastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
207
+
208
+ for (const { segment } of segmenter.segment(string)) {
209
+ // Zero-width / non-printing clusters
210
+ if (isZeroWidthCluster(segment)) {
211
+ continue;
212
+ }
213
+
214
+ // Emoji width logic
215
+ if (rgiEmojiRegex.test(segment) || isDoubleWidthNonRgiEmojiSequence(segment)) {
216
+ width += 2;
217
+ continue;
218
+ }
219
+
220
+ const visibleSegment = baseVisible(segment);
221
+ const hangulWidth = hangulClusterWidth(visibleSegment, eastAsianWidthOptions);
222
+ if (hangulWidth !== undefined) {
223
+ width += hangulWidth;
224
+ continue;
225
+ }
226
+
227
+ // Everything else: EAW of the cluster's first visible scalar
228
+ const codePoint = visibleSegment.codePointAt(0)!;
229
+ width += eastAsianWidth(codePoint, eastAsianWidthOptions);
230
+
231
+ // Add width for trailing spacing marks and Halfwidth/Fullwidth Forms
232
+ width += trailingWidth(visibleSegment, eastAsianWidthOptions);
233
+ }
234
+
235
+ return width;
236
+ }
@@ -0,0 +1,33 @@
1
+ // ANSI escape sequence matching and removal.
2
+ // Ported from `ansi-regex` and `strip-ansi` (MIT, Sindre Sorhus).
3
+ import { BEL, C1_CSI, C1_ST, ESC } from "./escapes.ts";
4
+
5
+ export function ansiRegex({ onlyFirst = false }: { onlyFirst?: boolean } = {}): RegExp {
6
+ // Valid string terminator sequences are BEL, ESC\, and C1 ST
7
+ const st = `(?:${BEL}|${ESC}\\\\|${C1_ST})`;
8
+
9
+ // OSC sequences only: ESC ] ... ST
10
+ // The payload stops at the first terminator character rather than scanning
11
+ // ahead for one, so an unterminated `ESC ]` cannot rescan the rest of the
12
+ // input. Terminals likewise abort a control string on an unexpected ESC.
13
+ const osc = `(?:${ESC}\\][^${BEL}${ESC}${C1_ST}]*${st})`;
14
+
15
+ // CSI and related: ESC/C1, optional intermediates, optional params
16
+ // (supports ; and :) then final byte
17
+ const csi = `[${ESC}${C1_CSI}][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]`;
18
+
19
+ return new RegExp(`${osc}|${csi}`, onlyFirst ? undefined : "g");
20
+ }
21
+
22
+ const regex = ansiRegex();
23
+
24
+ export function stripAnsi(string: string): string {
25
+ // Fast path: ANSI codes require ESC (7-bit) or CSI (8-bit) introducer
26
+ if (!string.includes(ESC) && !string.includes(C1_CSI)) {
27
+ return string;
28
+ }
29
+
30
+ // Even though the regex is global, we don't need to reset `.lastIndex`
31
+ // because `.replace()` does it automatically.
32
+ return string.replace(regex, "");
33
+ }