@alchemy.run/sigil 0.0.0-alpha.2 → 0.0.0-alpha.4
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 +2 -4
- package/THIRD_PARTY_NOTICES.md +31 -22
- package/dist/ansi.d.ts +17 -14
- package/dist/ansi.js +7 -2
- package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
- package/dist/index.d.ts +125 -288
- package/dist/index.js +2460 -2864
- package/dist/{truncate-CBiyyZzw.js → truncate-Cr6xVFMa.js} +447 -273
- package/package.json +3 -9
- package/src/ansi/chalk.ts +8 -51
- package/src/ansi/cursor.ts +2 -4
- package/src/ansi/east-asian-width.ts +44 -0
- package/src/ansi/escapes.ts +4 -26
- package/src/ansi/index.ts +12 -14
- package/src/ansi/sgr.ts +2 -5
- package/src/ansi/slice.ts +1 -1
- package/src/ansi/string-width.ts +91 -206
- package/src/ansi/strip.ts +13 -33
- package/src/ansi/supports-color.ts +8 -14
- package/src/ansi/tokenize.ts +94 -166
- package/src/ansi/truncate.ts +5 -3
- package/src/ansi/wrap.ts +13 -14
- package/src/ansi-tokenizer.ts +1 -1
- package/src/colorize.ts +1 -1
- package/src/components/App.tsx +14 -14
- package/src/components/BackgroundContext.ts +2 -2
- package/src/components/Box.tsx +51 -59
- package/src/components/CursorContext.ts +1 -1
- package/src/components/ErrorBoundary.tsx +2 -1
- package/src/components/ErrorOverview.tsx +9 -7
- package/src/components/Newline.tsx +1 -0
- package/src/components/Spacer.tsx +2 -1
- package/src/components/Static.tsx +2 -1
- package/src/components/StderrContext.ts +0 -2
- package/src/components/StdinContext.ts +0 -1
- package/src/components/StdoutContext.ts +1 -3
- package/src/components/Text.tsx +7 -6
- package/src/components/Transform.tsx +2 -1
- package/src/cursor-position.ts +1 -1
- package/src/devtools.ts +84 -24
- package/src/dom.ts +7 -7
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +1 -1
- package/src/global.d.ts +3 -4
- package/src/{boxes.ts → glyphs.ts} +17 -18
- package/src/hooks/use-animation.ts +1 -1
- package/src/hooks/use-app.ts +1 -1
- package/src/hooks/use-box-metrics.ts +1 -1
- package/src/hooks/use-cursor.ts +2 -2
- package/src/hooks/use-focus-manager.ts +1 -1
- package/src/hooks/use-focus.ts +5 -6
- package/src/hooks/use-input.ts +4 -4
- package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
- package/src/hooks/use-paste.ts +2 -2
- package/src/hooks/use-stderr.ts +1 -1
- package/src/hooks/use-stdin.ts +1 -1
- package/src/hooks/use-stdout.ts +1 -1
- package/src/hooks/use-window-size.ts +2 -2
- package/src/index.ts +44 -44
- package/src/ink.tsx +536 -720
- package/src/input-parser.ts +1 -1
- package/src/instances.ts +1 -1
- package/src/kitty-keyboard.ts +128 -0
- package/src/log-update.ts +4 -4
- package/src/measure-element.ts +1 -1
- package/src/measure-text.ts +2 -2
- package/src/output.ts +4 -4
- package/src/parse-keypress.ts +4 -5
- package/src/parse-stack-line.ts +0 -1
- package/src/patch-console.ts +44 -0
- package/src/reconciler.ts +13 -60
- package/src/render-background.ts +3 -3
- package/src/render-border.ts +6 -6
- package/src/render-node-to-output.ts +13 -11
- package/src/render-to-string.ts +4 -4
- package/src/render.ts +9 -10
- package/src/renderer.ts +3 -3
- package/src/sanitize-ansi.ts +1 -1
- package/src/signal-exit.ts +4 -5
- package/src/squash-text-nodes.ts +2 -2
- package/src/stream.ts +2 -4
- package/src/styles.ts +6 -6
- package/src/terminal-size.ts +9 -8
- package/src/types.ts +1 -6
- package/src/utils.ts +2 -2
- package/src/wrap-text.ts +4 -4
- package/src/yoga/config.ts +2 -2
- package/src/yoga/core/absoluteLayout.ts +15 -15
- package/src/yoga/core/baseline.ts +3 -3
- package/src/yoga/core/cache.ts +4 -4
- package/src/yoga/core/calculateLayout.ts +33 -27
- package/src/yoga/core/config.ts +1 -1
- package/src/yoga/core/flexLine.ts +3 -3
- package/src/yoga/core/helpers.ts +3 -3
- package/src/yoga/core/layoutResults.ts +4 -4
- package/src/yoga/core/node.ts +16 -16
- package/src/yoga/core/pixelGrid.ts +4 -4
- package/src/yoga/core/style.ts +11 -11
- package/src/yoga/core/types.ts +2 -2
- package/src/yoga/index.ts +6 -6
- package/src/yoga/node.ts +8 -8
- package/src/ansi/widest-line.ts +0 -12
- package/src/auto-bind.ts +0 -41
- package/src/devtools-window-polyfill.ts +0 -73
- package/src/indent-string.ts +0 -16
- package/src/is-in-ci.ts +0 -7
- package/src/write-synchronized.ts +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alchemy.run/sigil",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.4",
|
|
4
4
|
"description": "React for CLIs. A self-contained fork of Ink with an integrated TypeScript Yoga layout engine.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"type": "module",
|
|
35
35
|
"imports": {
|
|
36
|
-
"
|
|
36
|
+
"#/*": "./src/*"
|
|
37
37
|
},
|
|
38
38
|
"exports": {
|
|
39
39
|
".": {
|
|
@@ -70,7 +70,6 @@
|
|
|
70
70
|
"@types/react": "^19.2.18",
|
|
71
71
|
"@types/react-reconciler": "^0.33.0",
|
|
72
72
|
"@types/scheduler": "^0.26.0",
|
|
73
|
-
"@types/ws": "^8.18.1",
|
|
74
73
|
"react": "^19.2.8",
|
|
75
74
|
"react-devtools-core": "^7.0.1",
|
|
76
75
|
"react-router": "^8.3.0",
|
|
@@ -78,14 +77,12 @@
|
|
|
78
77
|
"typescript": "^7.0.2",
|
|
79
78
|
"vite-plus": "^0.2.9",
|
|
80
79
|
"vitest": "4.1.11",
|
|
81
|
-
"ws": "^8.20.0",
|
|
82
80
|
"zigpty": "^0.2.1"
|
|
83
81
|
},
|
|
84
82
|
"peerDependencies": {
|
|
85
83
|
"@types/react": ">=19.2.0",
|
|
86
84
|
"react": ">=19.2.0",
|
|
87
|
-
"react-devtools-core": ">=6.1.2"
|
|
88
|
-
"ws": ">=8.20.0"
|
|
85
|
+
"react-devtools-core": ">=6.1.2"
|
|
89
86
|
},
|
|
90
87
|
"peerDependenciesMeta": {
|
|
91
88
|
"@types/react": {
|
|
@@ -93,9 +90,6 @@
|
|
|
93
90
|
},
|
|
94
91
|
"react-devtools-core": {
|
|
95
92
|
"optional": true
|
|
96
|
-
},
|
|
97
|
-
"ws": {
|
|
98
|
-
"optional": true
|
|
99
93
|
}
|
|
100
94
|
},
|
|
101
95
|
"engines": {
|
package/src/ansi/chalk.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Terminal string styling with a chalk-compatible API for the styles Ink uses.
|
|
2
2
|
// Ported from `chalk` (MIT, Sindre Sorhus) without the chaining builder —
|
|
3
3
|
// Ink only ever applies one style per call.
|
|
4
|
-
import { ESC } from "
|
|
4
|
+
import { ESC } from "#/ansi/escapes.ts";
|
|
5
5
|
import {
|
|
6
6
|
background,
|
|
7
7
|
backgroundColorNames,
|
|
@@ -15,12 +15,12 @@ import {
|
|
|
15
15
|
type BackgroundColorName,
|
|
16
16
|
type ForegroundColorName,
|
|
17
17
|
type ModifierName,
|
|
18
|
-
} from "
|
|
18
|
+
} from "#/ansi/sgr.ts";
|
|
19
19
|
import {
|
|
20
20
|
supportsColor as supportsColorDetection,
|
|
21
21
|
type ColorInfo,
|
|
22
22
|
type ColorSupportLevel,
|
|
23
|
-
} from "
|
|
23
|
+
} from "#/ansi/supports-color.ts";
|
|
24
24
|
|
|
25
25
|
export type { BackgroundColorName, ForegroundColorName, ModifierName };
|
|
26
26
|
|
|
@@ -38,50 +38,6 @@ const state = {
|
|
|
38
38
|
|
|
39
39
|
// Replace every occurrence of `substring` with `substring + replacer`,
|
|
40
40
|
// re-opening a style after a nested close code would have ended it.
|
|
41
|
-
const stringReplaceAll = (string: string, substring: string, replacer: string): string => {
|
|
42
|
-
let index = string.indexOf(substring);
|
|
43
|
-
if (index === -1) {
|
|
44
|
-
return string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const substringLength = substring.length;
|
|
48
|
-
let endIndex = 0;
|
|
49
|
-
let returnValue = "";
|
|
50
|
-
do {
|
|
51
|
-
returnValue += string.slice(endIndex, index) + substring + replacer;
|
|
52
|
-
endIndex = index + substringLength;
|
|
53
|
-
index = string.indexOf(substring, endIndex);
|
|
54
|
-
} while (index !== -1);
|
|
55
|
-
|
|
56
|
-
returnValue += string.slice(endIndex);
|
|
57
|
-
return returnValue;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Close the style before every line break and reopen it after, to prevent
|
|
61
|
-
// background bleed across lines (https://github.com/chalk/chalk/pull/92).
|
|
62
|
-
const stringEncaseCRLFWithFirstIndex = (
|
|
63
|
-
string: string,
|
|
64
|
-
prefix: string,
|
|
65
|
-
postfix: string,
|
|
66
|
-
index: number,
|
|
67
|
-
): string => {
|
|
68
|
-
let endIndex = 0;
|
|
69
|
-
let returnValue = "";
|
|
70
|
-
do {
|
|
71
|
-
const gotCR = string[index - 1] === "\r";
|
|
72
|
-
returnValue +=
|
|
73
|
-
string.slice(endIndex, gotCR ? index - 1 : index) +
|
|
74
|
-
prefix +
|
|
75
|
-
(gotCR ? "\r\n" : "\n") +
|
|
76
|
-
postfix;
|
|
77
|
-
endIndex = index + 1;
|
|
78
|
-
index = string.indexOf("\n", endIndex);
|
|
79
|
-
} while (index !== -1);
|
|
80
|
-
|
|
81
|
-
returnValue += string.slice(endIndex);
|
|
82
|
-
return returnValue;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
41
|
const applyStyle = (open: string, close: string, text: string): string => {
|
|
86
42
|
if (state.level <= 0 || !text) {
|
|
87
43
|
return text;
|
|
@@ -92,12 +48,13 @@ const applyStyle = (open: string, close: string, text: string): string => {
|
|
|
92
48
|
if (string.includes(ESC)) {
|
|
93
49
|
// Re-open the style wherever the text already contains its close code,
|
|
94
50
|
// otherwise only the part before that code would stay styled.
|
|
95
|
-
string =
|
|
51
|
+
string = string.replaceAll(close, close + open);
|
|
96
52
|
}
|
|
97
53
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
54
|
+
if (string.includes("\n")) {
|
|
55
|
+
// Close the style before every line break and reopen it after, to prevent
|
|
56
|
+
// background bleed across lines (https://github.com/chalk/chalk/pull/92).
|
|
57
|
+
string = string.replaceAll(/\r?\n/g, `${close}$&${open}`);
|
|
101
58
|
}
|
|
102
59
|
|
|
103
60
|
return open + string + close;
|
package/src/ansi/cursor.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { cursorHide, cursorShow } from "#/ansi/escapes.ts";
|
|
1
2
|
// Derived from `cli-cursor` + `restore-cursor` (MIT, Sindre Sorhus),
|
|
2
3
|
// collapsed into one module. Restores the cursor on process exit once
|
|
3
4
|
// `hide()` has been called, like `restore-cursor` did.
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import { signalExit } from "../signal-exit.ts";
|
|
7
|
-
import { cursorHide, cursorShow } from "./escapes.ts";
|
|
5
|
+
import { signalExit } from "#/signal-exit.ts";
|
|
8
6
|
|
|
9
7
|
type CursorStream = {
|
|
10
8
|
isTTY?: boolean;
|
|
@@ -206,6 +206,50 @@ export function eastAsianWidth(
|
|
|
206
206
|
return 1;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/**
|
|
210
|
+
Grapheme-cluster width test shared by the tokenizer (slice/clip path): the
|
|
211
|
+
base code point's East Asian Width, plus emoji presentation rules.
|
|
212
|
+
*/
|
|
213
|
+
export function isFullwidthGrapheme(grapheme: string, baseCodePoint: number): boolean {
|
|
214
|
+
if (isFullwidthCodePoint(baseCodePoint)) return true;
|
|
215
|
+
// Variation Selector 16 forces emoji presentation (2 columns wide)
|
|
216
|
+
if (grapheme.includes("\uFE0F")) return true;
|
|
217
|
+
// Regional indicator pairs form flag emoji (2 columns wide)
|
|
218
|
+
if (baseCodePoint >= 0x1f1e6 && baseCodePoint <= 0x1f1ff) return true;
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
Wide code points not covered by string-width's CJKT script regex or emoji
|
|
224
|
+
regex — the fallback bucket of its block parser. A disjoint decomposition of
|
|
225
|
+
the `wide` table above, kept alongside it so the width data lives in one
|
|
226
|
+
module.
|
|
227
|
+
*/
|
|
228
|
+
export const isWideNotCJKTNotEmoji = (x: number): boolean => {
|
|
229
|
+
return (
|
|
230
|
+
x === 0x231b ||
|
|
231
|
+
x === 0x2329 ||
|
|
232
|
+
(x >= 0x2ff0 && x <= 0x2fff) ||
|
|
233
|
+
(x >= 0x3001 && x <= 0x303e) ||
|
|
234
|
+
(x >= 0x3099 && x <= 0x30ff) ||
|
|
235
|
+
(x >= 0x3105 && x <= 0x312f) ||
|
|
236
|
+
(x >= 0x3131 && x <= 0x318e) ||
|
|
237
|
+
(x >= 0x3190 && x <= 0x31e3) ||
|
|
238
|
+
(x >= 0x31ef && x <= 0x321e) ||
|
|
239
|
+
(x >= 0x3220 && x <= 0x3247) ||
|
|
240
|
+
(x >= 0x3250 && x <= 0x4dbf) ||
|
|
241
|
+
(x >= 0xfe10 && x <= 0xfe19) ||
|
|
242
|
+
(x >= 0xfe30 && x <= 0xfe52) ||
|
|
243
|
+
(x >= 0xfe54 && x <= 0xfe66) ||
|
|
244
|
+
(x >= 0xfe68 && x <= 0xfe6b) ||
|
|
245
|
+
(x >= 0x1f200 && x <= 0x1f202) ||
|
|
246
|
+
(x >= 0x1f210 && x <= 0x1f23b) ||
|
|
247
|
+
(x >= 0x1f240 && x <= 0x1f248) ||
|
|
248
|
+
(x >= 0x20000 && x <= 0x2fffd) ||
|
|
249
|
+
(x >= 0x30000 && x <= 0x3fffd)
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
|
|
209
253
|
export function isFullwidthCodePoint(codePoint: number): boolean {
|
|
210
254
|
if (!Number.isInteger(codePoint)) {
|
|
211
255
|
return false;
|
package/src/ansi/escapes.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
1
|
// Derived from `ansi-escapes` (MIT, Sindre Sorhus), reduced to the escapes Ink uses.
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
|
|
5
2
|
// Control characters and sequence introducers. This module is the single
|
|
6
3
|
// home for escape sequences: everything else imports these instead of
|
|
7
4
|
// spelling out `\u001B[...` inline.
|
|
@@ -51,29 +48,10 @@ export const eraseLines = (count: number): string => {
|
|
|
51
48
|
return clear;
|
|
52
49
|
};
|
|
53
50
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const parts = os.release().split(".");
|
|
61
|
-
const major = Number(parts[0]);
|
|
62
|
-
const build = Number(parts[2] ?? 0);
|
|
63
|
-
|
|
64
|
-
if (major < 10) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return major === 10 && build < 10_586;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export const clearTerminal = isOldWindows()
|
|
72
|
-
? `${eraseScreen}${CSI}0f`
|
|
73
|
-
: // 1. Erases the screen (only done in case `2` is not supported)
|
|
74
|
-
// 2. Erases the whole screen including scrollback buffer
|
|
75
|
-
// 3. Moves cursor to the top-left position
|
|
76
|
-
`${eraseScreen}${CSI}3J${CSI}H`;
|
|
51
|
+
// 1. Erases the screen (only done in case `2` is not supported)
|
|
52
|
+
// 2. Erases the whole screen including scrollback buffer
|
|
53
|
+
// 3. Moves cursor to the top-left position
|
|
54
|
+
export const clearTerminal = `${eraseScreen}${CSI}3J${CSI}H`;
|
|
77
55
|
|
|
78
56
|
export const enterAlternativeScreen = CSI + "?1049h";
|
|
79
57
|
export const exitAlternativeScreen = CSI + "?1049l";
|
package/src/ansi/index.ts
CHANGED
|
@@ -3,25 +3,23 @@
|
|
|
3
3
|
// `@alchemy.run/sigil/ansi`.
|
|
4
4
|
|
|
5
5
|
// Styling
|
|
6
|
-
export { chalk } from "
|
|
7
|
-
export { supportsColor } from "
|
|
8
|
-
export * from "
|
|
6
|
+
export { chalk } from "#/ansi/chalk.ts";
|
|
7
|
+
export { supportsColor } from "#/ansi/supports-color.ts";
|
|
8
|
+
export * from "#/ansi/sgr.ts";
|
|
9
9
|
|
|
10
10
|
// Escape sequences (raw building blocks + named sequences)
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
export { cliCursor } from "./cursor.ts";
|
|
11
|
+
export * from "#/ansi/escapes.ts";
|
|
12
|
+
export { cliCursor } from "#/ansi/cursor.ts";
|
|
14
13
|
|
|
15
14
|
// Measurement
|
|
16
|
-
export { stringWidth } from "
|
|
17
|
-
export
|
|
18
|
-
export * from "./east-asian-width.ts";
|
|
15
|
+
export { stringWidth, widestLine } from "#/ansi/string-width.ts";
|
|
16
|
+
export * from "#/ansi/east-asian-width.ts";
|
|
19
17
|
|
|
20
18
|
// String manipulation
|
|
21
|
-
export { stripAnsi
|
|
22
|
-
export { sliceAnsi } from "
|
|
23
|
-
export { cliTruncate as truncateAnsi } from "
|
|
24
|
-
export { wrapAnsi } from "
|
|
19
|
+
export { stripAnsi } from "#/ansi/strip.ts";
|
|
20
|
+
export { sliceAnsi } from "#/ansi/slice.ts";
|
|
21
|
+
export { cliTruncate as truncateAnsi } from "#/ansi/truncate.ts";
|
|
22
|
+
export { wrapAnsi } from "#/ansi/wrap.ts";
|
|
25
23
|
|
|
26
24
|
// Tokenizer: style-aware parsing and minimal re-emission
|
|
27
|
-
export * from "
|
|
25
|
+
export * from "#/ansi/tokenize.ts";
|
package/src/ansi/sgr.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ANSI SGR style tables and color-space conversions.
|
|
2
2
|
// Ported from `ansi-styles` (MIT, Sindre Sorhus).
|
|
3
|
-
import { CSI } from "
|
|
3
|
+
import { CSI } from "#/ansi/escapes.ts";
|
|
4
4
|
|
|
5
5
|
const ANSI_BACKGROUND_OFFSET = 10;
|
|
6
6
|
|
|
@@ -173,10 +173,7 @@ export const hexToRgb = (hex: string): [number, number, number] => {
|
|
|
173
173
|
let [colorString] = matches;
|
|
174
174
|
|
|
175
175
|
if (colorString.length === 3) {
|
|
176
|
-
colorString = colorString
|
|
177
|
-
.split("")
|
|
178
|
-
.map((character) => character + character)
|
|
179
|
-
.join("");
|
|
176
|
+
colorString = colorString.replaceAll(/./g, "$&$&");
|
|
180
177
|
}
|
|
181
178
|
|
|
182
179
|
const integer = Number.parseInt(colorString, 16);
|
package/src/ansi/slice.ts
CHANGED
package/src/ansi/string-width.ts
CHANGED
|
@@ -1,236 +1,121 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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;
|
|
1
|
+
// Derived from `fast-string-truncated-width` (MIT, Fabio Spampinato),
|
|
2
|
+
// reduced to plain width measurement: Sigil truncates via `truncate.ts`, and
|
|
3
|
+
// no caller customizes per-class widths, so those are constants here.
|
|
4
|
+
// Width tables live in east-asian-width.ts, the single width-data module.
|
|
36
5
|
|
|
37
|
-
|
|
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;
|
|
6
|
+
import { isFullWidth, isWideNotCJKTNotEmoji } from "#/ansi/east-asian-width.ts";
|
|
41
7
|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
}
|
|
8
|
+
const getCodePointsLength = (input: string): number => {
|
|
9
|
+
let length = 0;
|
|
59
10
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const pictographics = segment.match(extendedPictographicRegex);
|
|
63
|
-
return pictographics !== null && pictographics.length >= 2;
|
|
11
|
+
for (const _ of input) {
|
|
12
|
+
length += 1;
|
|
64
13
|
}
|
|
65
14
|
|
|
66
|
-
return
|
|
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
|
-
}
|
|
15
|
+
return length;
|
|
16
|
+
};
|
|
119
17
|
|
|
120
|
-
|
|
121
|
-
|
|
18
|
+
// Unlike the original port, CSI parameters also allow `:` so ITU T.416
|
|
19
|
+
// colon-parameter SGR sequences (which Sigil preserves) measure as width 0.
|
|
20
|
+
const ANSI_RE =
|
|
21
|
+
/[\u001b\u009b][[()#;?]*[0-9;:]*[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y;
|
|
22
|
+
const CONTROL_RE = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y;
|
|
23
|
+
const CJKT_WIDE_RE =
|
|
24
|
+
/(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/uy;
|
|
25
|
+
const TAB_RE = /\t{1,1000}/y;
|
|
26
|
+
const EMOJI_RE =
|
|
27
|
+
/[\u{1F1E6}-\u{1F1FF}]{2}|\u{1F3F4}[\u{E0061}-\u{E007A}]{2}[\u{E0030}-\u{E0039}\u{E0061}-\u{E007A}]{1,3}\u{E007F}|(?:\p{Emoji}\uFE0F\u20E3?|\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation})(?:\u200D(?:\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F\u20E3?))*/uy;
|
|
28
|
+
const LATIN_RE = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y;
|
|
29
|
+
const MODIFIER_RE = /\p{M}+/gu;
|
|
30
|
+
|
|
31
|
+
const ANSI_WIDTH = 0;
|
|
32
|
+
const CONTROL_WIDTH = 0;
|
|
33
|
+
const TAB_WIDTH = 8;
|
|
34
|
+
const EMOJI_WIDTH = 2;
|
|
35
|
+
const FULL_WIDTH_WIDTH = 2;
|
|
36
|
+
const REGULAR_WIDTH = 1;
|
|
37
|
+
const WIDE_WIDTH = 2;
|
|
38
|
+
|
|
39
|
+
const PARSE_BLOCKS: [RegExp, number][] = [
|
|
40
|
+
[LATIN_RE, REGULAR_WIDTH],
|
|
41
|
+
[ANSI_RE, ANSI_WIDTH],
|
|
42
|
+
[CONTROL_RE, CONTROL_WIDTH],
|
|
43
|
+
[TAB_RE, TAB_WIDTH],
|
|
44
|
+
[EMOJI_RE, EMOJI_WIDTH],
|
|
45
|
+
[CJKT_WIDE_RE, WIDE_WIDTH],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export const stringWidth = (input: string): number => {
|
|
49
|
+
let indexPrev = 0;
|
|
50
|
+
let index = 0;
|
|
51
|
+
const length = input.length;
|
|
52
|
+
let unmatchedStart = 0;
|
|
53
|
+
let unmatchedEnd = 0;
|
|
54
|
+
let width = 0;
|
|
122
55
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
56
|
+
outer: while (true) {
|
|
57
|
+
/* UNMATCHED */
|
|
126
58
|
|
|
127
|
-
|
|
59
|
+
if (unmatchedEnd > unmatchedStart || (index >= length && index > indexPrev)) {
|
|
60
|
+
const unmatched = input.slice(unmatchedStart, unmatchedEnd) || input.slice(indexPrev, index);
|
|
128
61
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!isHangulJamo(codePoint)) {
|
|
132
|
-
if (width === 0) {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
62
|
+
for (const char of unmatched.replaceAll(MODIFIER_RE, "")) {
|
|
63
|
+
const codePoint = char.codePointAt(0) || 0;
|
|
135
64
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
65
|
+
width += isFullWidth(codePoint)
|
|
66
|
+
? FULL_WIDTH_WIDTH
|
|
67
|
+
: isWideNotCJKTNotEmoji(codePoint)
|
|
68
|
+
? WIDE_WIDTH
|
|
69
|
+
: REGULAR_WIDTH;
|
|
140
70
|
}
|
|
141
71
|
|
|
142
|
-
|
|
72
|
+
unmatchedStart = unmatchedEnd = 0;
|
|
143
73
|
}
|
|
144
74
|
|
|
145
|
-
|
|
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
|
-
}
|
|
75
|
+
/* EXITING */
|
|
155
76
|
|
|
156
|
-
|
|
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;
|
|
77
|
+
if (index >= length) {
|
|
78
|
+
break;
|
|
170
79
|
}
|
|
171
80
|
|
|
172
|
-
|
|
173
|
-
extra += eastAsianWidth(character.codePointAt(0)!, eastAsianWidthOptions);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
81
|
+
/* PARSE BLOCKS */
|
|
176
82
|
|
|
177
|
-
|
|
178
|
-
|
|
83
|
+
for (let i = 0, l = PARSE_BLOCKS.length; i < l; i++) {
|
|
84
|
+
const [BLOCK_RE, BLOCK_WIDTH] = PARSE_BLOCKS[i]!;
|
|
179
85
|
|
|
180
|
-
|
|
181
|
-
if (typeof input !== "string" || input.length === 0) {
|
|
182
|
-
return 0;
|
|
183
|
-
}
|
|
86
|
+
BLOCK_RE.lastIndex = index;
|
|
184
87
|
|
|
185
|
-
|
|
88
|
+
if (BLOCK_RE.test(input)) {
|
|
89
|
+
const lengthExtra =
|
|
90
|
+
BLOCK_RE === CJKT_WIDE_RE
|
|
91
|
+
? getCodePointsLength(input.slice(index, BLOCK_RE.lastIndex))
|
|
92
|
+
: BLOCK_RE === EMOJI_RE
|
|
93
|
+
? 1
|
|
94
|
+
: BLOCK_RE.lastIndex - index;
|
|
186
95
|
|
|
187
|
-
|
|
96
|
+
width += lengthExtra * BLOCK_WIDTH;
|
|
97
|
+
unmatchedStart = indexPrev;
|
|
98
|
+
unmatchedEnd = index;
|
|
99
|
+
index = indexPrev = BLOCK_RE.lastIndex;
|
|
188
100
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
string = stripAnsi(string);
|
|
193
|
-
}
|
|
101
|
+
continue outer;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
194
104
|
|
|
195
|
-
|
|
196
|
-
return 0;
|
|
197
|
-
}
|
|
105
|
+
/* UNMATCHED INDEX */
|
|
198
106
|
|
|
199
|
-
|
|
200
|
-
// lookup — width equals length.
|
|
201
|
-
if (/^[ -~]*$/.test(string)) {
|
|
202
|
-
return string.length;
|
|
107
|
+
index += 1;
|
|
203
108
|
}
|
|
204
109
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
}
|
|
110
|
+
return width;
|
|
111
|
+
};
|
|
226
112
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
width += eastAsianWidth(codePoint, eastAsianWidthOptions);
|
|
113
|
+
export const widestLine = (text: string): number => {
|
|
114
|
+
let lineWidth = 0;
|
|
230
115
|
|
|
231
|
-
|
|
232
|
-
|
|
116
|
+
for (const line of text.split("\n")) {
|
|
117
|
+
lineWidth = Math.max(lineWidth, stringWidth(line));
|
|
233
118
|
}
|
|
234
119
|
|
|
235
|
-
return
|
|
236
|
-
}
|
|
120
|
+
return lineWidth;
|
|
121
|
+
};
|