@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.
- package/README.md +299 -299
- package/dist/ansi.d.ts +223 -0
- package/dist/ansi.js +2 -0
- package/dist/{devtools-QpCMm9JH.mjs → devtools-BhYGjb7h.js} +1 -1
- package/dist/index-DDVME65c.d.ts +919 -0
- package/dist/index.d.ts +1657 -0
- package/dist/index.js +4643 -0
- package/dist/sgr-CMfEpjSk.d.ts +91 -0
- package/dist/truncate-CBiyyZzw.js +2156 -0
- package/dist/yoga-5jKhYCJC.js +3465 -0
- package/dist/yoga.d.ts +2 -0
- package/dist/yoga.js +2 -0
- package/package.json +37 -17
- package/src/ansi/chalk.ts +179 -0
- package/src/ansi/cursor.ts +48 -0
- package/src/ansi/east-asian-width.ts +215 -0
- package/src/ansi/escapes.ts +128 -0
- package/src/ansi/index.ts +27 -0
- package/src/ansi/sgr.ts +237 -0
- package/src/ansi/slice.ts +43 -0
- package/src/ansi/string-width.ts +236 -0
- package/src/ansi/strip.ts +33 -0
- package/src/ansi/supports-color.ts +213 -0
- package/src/ansi/tokenize.ts +453 -0
- package/src/ansi/truncate.ts +194 -0
- package/src/ansi/widest-line.ts +12 -0
- package/src/ansi/wrap.ts +766 -0
- package/src/ansi-tokenizer.ts +510 -0
- package/src/auto-bind.ts +41 -0
- package/src/boxes.ts +100 -0
- package/src/code-excerpt.ts +39 -0
- package/src/colorize.ts +60 -0
- package/src/components/AccessibilityContext.ts +5 -0
- package/src/components/AnimationContext.ts +24 -0
- package/src/components/App.tsx +781 -0
- package/src/components/AppContext.ts +111 -0
- package/src/components/BackgroundContext.ts +8 -0
- package/src/components/Box.tsx +116 -0
- package/src/components/CursorContext.ts +19 -0
- package/src/components/ErrorBoundary.tsx +38 -0
- package/src/components/ErrorOverview.tsx +133 -0
- package/src/components/FocusContext.ts +30 -0
- package/src/components/Newline.tsx +15 -0
- package/src/components/Spacer.tsx +10 -0
- package/src/components/Static.tsx +59 -0
- package/src/components/StderrContext.ts +26 -0
- package/src/components/StdinContext.ts +49 -0
- package/src/components/StdoutContext.ts +28 -0
- package/src/components/Text.tsx +144 -0
- package/src/components/Transform.tsx +37 -0
- package/src/cursor-position.ts +103 -0
- package/src/devtools-window-polyfill.ts +73 -0
- package/src/devtools.ts +43 -0
- package/src/dom.ts +292 -0
- package/src/get-max-width.ts +11 -0
- package/src/global.d.ts +36 -0
- package/src/hooks/use-animation.ts +142 -0
- package/src/hooks/use-app.ts +8 -0
- package/src/hooks/use-box-metrics.ts +134 -0
- package/src/hooks/use-cursor.ts +33 -0
- package/src/hooks/use-focus-manager.ts +62 -0
- package/src/hooks/use-focus.ts +83 -0
- package/src/hooks/use-input.ts +267 -0
- package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
- package/src/hooks/use-paste.ts +78 -0
- package/src/hooks/use-stderr.ts +8 -0
- package/src/hooks/use-stdin.ts +10 -0
- package/src/hooks/use-stdout.ts +8 -0
- package/src/hooks/use-window-size.ts +41 -0
- package/src/indent-string.ts +16 -0
- package/src/index.ts +44 -0
- package/src/ink.tsx +1506 -0
- package/src/input-parser.ts +283 -0
- package/src/instances.ts +9 -0
- package/src/is-in-ci.ts +7 -0
- package/src/kitty-keyboard.ts +57 -0
- package/src/log-update.ts +370 -0
- package/src/measure-element.ts +62 -0
- package/src/measure-text.ts +31 -0
- package/src/output.ts +308 -0
- package/src/parse-keypress.ts +516 -0
- package/src/parse-stack-line.ts +139 -0
- package/src/patch-console.ts +62 -0
- package/src/quick-lru.ts +85 -0
- package/src/reconciler.ts +451 -0
- package/src/render-background.ts +38 -0
- package/src/render-border.ts +134 -0
- package/src/render-node-to-output.ts +191 -0
- package/src/render-to-string.ts +131 -0
- package/src/render.ts +276 -0
- package/src/renderer.ts +73 -0
- package/src/sanitize-ansi.ts +33 -0
- package/src/signal-exit.ts +107 -0
- package/src/squash-text-nodes.ts +40 -0
- package/src/stream.ts +30 -0
- package/src/styles.ts +748 -0
- package/src/terminal-size.ts +57 -0
- package/src/throttle.ts +73 -0
- package/src/types.ts +15 -0
- package/src/utils.ts +40 -0
- package/src/wrap-text.ts +50 -0
- package/src/write-synchronized.ts +9 -0
- package/src/yoga/config.ts +57 -0
- package/src/yoga/core/absoluteLayout.ts +626 -0
- package/src/yoga/core/baseline.ts +66 -0
- package/src/yoga/core/cache.ts +136 -0
- package/src/yoga/core/calculateLayout.ts +2920 -0
- package/src/yoga/core/config.ts +104 -0
- package/src/yoga/core/flexLine.ts +177 -0
- package/src/yoga/core/helpers.ts +293 -0
- package/src/yoga/core/layoutResults.ts +167 -0
- package/src/yoga/core/node.ts +611 -0
- package/src/yoga/core/numeric.ts +44 -0
- package/src/yoga/core/pixelGrid.ts +151 -0
- package/src/yoga/core/style.ts +887 -0
- package/src/yoga/core/types.ts +224 -0
- package/src/yoga/generated/YGEnums.ts +263 -0
- package/src/yoga/index.ts +19 -0
- package/src/yoga/node.ts +1140 -0
- package/dist/index.d.mts +0 -2379
- 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";
|
package/src/ansi/sgr.ts
ADDED
|
@@ -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
|
+
}
|