@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
package/src/ansi/wrap.ts
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
// Word-wrap a string to a column width, ANSI-aware.
|
|
2
|
+
// Ported from `wrap-ansi` (MIT, Sindre Sorhus).
|
|
3
|
+
import { BEL, C1_CSI, ESC } from "./escapes.ts";
|
|
4
|
+
//
|
|
5
|
+
// Supported boundary: semicolon-delimited SGR styling, colon-delimited
|
|
6
|
+
// RGB/indexed colors, and OSC 8 hyperlinks are tracked, while ordinary CSI
|
|
7
|
+
// sequences and other complete 7-bit OSC commands are preserved as opaque
|
|
8
|
+
// zero-width units. This is intentionally not a terminal emulator. Newlines
|
|
9
|
+
// always delimit input lines before ANSI parsing.
|
|
10
|
+
import { codes as sgrCodes } from "./sgr.ts";
|
|
11
|
+
import { stringWidth } from "./string-width.ts";
|
|
12
|
+
|
|
13
|
+
export type WrapOptions = {
|
|
14
|
+
readonly trim?: boolean;
|
|
15
|
+
readonly hard?: boolean;
|
|
16
|
+
readonly wordWrap?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ANSI_ESCAPE = ESC;
|
|
20
|
+
const ANSI_ESCAPE_BELL = BEL;
|
|
21
|
+
const ANSI_CSI = "[";
|
|
22
|
+
const ANSI_OSC = "]";
|
|
23
|
+
const ANSI_SGR_TERMINATOR = "m";
|
|
24
|
+
const ANSI_SGR_RESET = 0;
|
|
25
|
+
const ANSI_SGR_RESET_FOREGROUND = 39;
|
|
26
|
+
const ANSI_SGR_RESET_BACKGROUND = 49;
|
|
27
|
+
const ANSI_SGR_RESET_UNDERLINE_COLOR = 59;
|
|
28
|
+
const ANSI_SGR_FOREGROUND_EXTENDED = 38;
|
|
29
|
+
const ANSI_SGR_BACKGROUND_EXTENDED = 48;
|
|
30
|
+
const ANSI_SGR_UNDERLINE_COLOR_EXTENDED = 58;
|
|
31
|
+
const ANSI_SGR_COLOR_MODE_RGB = 2;
|
|
32
|
+
const ANSI_SGR_COLOR_MODE_256 = 5;
|
|
33
|
+
const ANSI_ESCAPE_LINK = `${ANSI_OSC}8;`;
|
|
34
|
+
|
|
35
|
+
// The first character of every sequence we recognize.
|
|
36
|
+
const ESCAPES = new Set([ANSI_ESCAPE, C1_CSI]);
|
|
37
|
+
const ESCAPE_CHARACTERS = [...ESCAPES].join("");
|
|
38
|
+
|
|
39
|
+
const CSI_INTRODUCER = `(?:${ANSI_ESCAPE}\\${ANSI_CSI}|${C1_CSI})`;
|
|
40
|
+
const CSI_PARAMETERS = "[0-?]*[ -/]*[@-~]";
|
|
41
|
+
const SGR_PARAMETERS = `(?<sgr>[0-9;:]*)${ANSI_SGR_TERMINATOR}`;
|
|
42
|
+
const OSC_STRING_TERMINATOR = `(?:${ANSI_ESCAPE_BELL}|${ANSI_ESCAPE}\\\\)`;
|
|
43
|
+
const OSC_STRING_PAYLOAD = String.raw`[^\u0000-\u001F\u007F-\u009F]*`;
|
|
44
|
+
// A hyperlink is `OSC 8 ; parameters ; URI ST`, where `parameters` is a
|
|
45
|
+
// possibly empty list of `key=value` pairs joined by `:`.
|
|
46
|
+
const LINK_PARAMETERS = String.raw`8;(?<parameters>[^;\u0000-\u001F\u007F-\u009F]*);(?<uri>${OSC_STRING_PAYLOAD})${OSC_STRING_TERMINATOR}`;
|
|
47
|
+
const OSC_STRING = `${OSC_STRING_PAYLOAD}${OSC_STRING_TERMINATOR}`;
|
|
48
|
+
|
|
49
|
+
const ANSI_ESCAPE_REGEX = new RegExp(
|
|
50
|
+
`${CSI_INTRODUCER}(?:${SGR_PARAMETERS}|${CSI_PARAMETERS})` +
|
|
51
|
+
`|${ANSI_ESCAPE}\\${ANSI_OSC}(?:${LINK_PARAMETERS}|${OSC_STRING})`,
|
|
52
|
+
"y",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const ANSI_SGR_MODIFIER_CLOSE_CODES = new Set(sgrCodes.values());
|
|
56
|
+
ANSI_SGR_MODIFIER_CLOSE_CODES.delete(ANSI_SGR_RESET);
|
|
57
|
+
|
|
58
|
+
const segmenter = new Intl.Segmenter();
|
|
59
|
+
// Complete ANSI sequences have already been removed before measuring these
|
|
60
|
+
// strings. Avoid string-width's ANSI scan so malformed sequences are not
|
|
61
|
+
// rescanned.
|
|
62
|
+
const getStringWidth = (string: string): number =>
|
|
63
|
+
stringWidth(string, { countAnsiEscapeCodes: true });
|
|
64
|
+
const TAB_SIZE = 8;
|
|
65
|
+
|
|
66
|
+
// Finds the next character that could introduce a sequence, so plain text is
|
|
67
|
+
// skipped in one native step.
|
|
68
|
+
const ESCAPE_INTRODUCER_REGEX = new RegExp(`[${ESCAPE_CHARACTERS}]`, "g");
|
|
69
|
+
// The final pass only cares about sequences and row boundaries.
|
|
70
|
+
const ROW_BOUNDARY_REGEX = new RegExp(`[\\n${ESCAPE_CHARACTERS}]`, "g");
|
|
71
|
+
// Every printable ASCII character is its own grapheme cluster of width one,
|
|
72
|
+
// which lets the segmenter be skipped.
|
|
73
|
+
const ASCII_PRINTABLE_REGEX = /^[ -~]*$/;
|
|
74
|
+
|
|
75
|
+
const wrapAnsiCode = (code: number | string): string =>
|
|
76
|
+
`${ANSI_ESCAPE}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
|
|
77
|
+
const wrapAnsiHyperlink = (url: string, parameters = ""): string =>
|
|
78
|
+
`${ANSI_ESCAPE}${ANSI_ESCAPE_LINK}${parameters};${url}${ANSI_ESCAPE_BELL}`;
|
|
79
|
+
|
|
80
|
+
// Match a complete escape sequence starting at `index`, or return `undefined`
|
|
81
|
+
// when none starts there.
|
|
82
|
+
const matchAnsiEscape = (string: string, index: number): RegExpExecArray | undefined => {
|
|
83
|
+
if (!ESCAPES.has(string[index]!)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ANSI_ESCAPE_REGEX.lastIndex = index;
|
|
88
|
+
return ANSI_ESCAPE_REGEX.exec(string) ?? undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Walk a string as alternating plain text runs and complete escape sequences.
|
|
92
|
+
// A character that looks like an introducer but does not start a valid
|
|
93
|
+
// sequence stays plain text.
|
|
94
|
+
const forEachSegment = (
|
|
95
|
+
string: string,
|
|
96
|
+
onPlainText: (plainText: string) => void,
|
|
97
|
+
onEscape: (escape: string) => void = () => {},
|
|
98
|
+
): void => {
|
|
99
|
+
let plainStart = 0;
|
|
100
|
+
let index = 0;
|
|
101
|
+
|
|
102
|
+
while (index < string.length) {
|
|
103
|
+
ESCAPE_INTRODUCER_REGEX.lastIndex = index;
|
|
104
|
+
const introducer = ESCAPE_INTRODUCER_REGEX.exec(string);
|
|
105
|
+
|
|
106
|
+
if (!introducer) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const escape = matchAnsiEscape(string, introducer.index);
|
|
111
|
+
|
|
112
|
+
if (!escape) {
|
|
113
|
+
index = introducer.index + 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (introducer.index > plainStart) {
|
|
118
|
+
onPlainText(string.slice(plainStart, introducer.index));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onEscape(escape[0]);
|
|
122
|
+
index = introducer.index + escape[0].length;
|
|
123
|
+
plainStart = index;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (plainStart < string.length) {
|
|
127
|
+
onPlainText(string.slice(plainStart));
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// The visible width of a string, ignoring escape sequences.
|
|
132
|
+
const getWidth = (string: string): number => {
|
|
133
|
+
let plainText = "";
|
|
134
|
+
|
|
135
|
+
forEachSegment(string, (part) => {
|
|
136
|
+
plainText += part;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return getStringWidth(plainText);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type Token = {
|
|
143
|
+
readonly value: string;
|
|
144
|
+
readonly width: number;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Split a string into escape sequences, which are zero width and must never
|
|
148
|
+
// be split, and grapheme clusters.
|
|
149
|
+
const getTokens = (string: string): Token[] => {
|
|
150
|
+
const tokens: Token[] = [];
|
|
151
|
+
|
|
152
|
+
forEachSegment(
|
|
153
|
+
string,
|
|
154
|
+
(plainText) => {
|
|
155
|
+
if (ASCII_PRINTABLE_REGEX.test(plainText)) {
|
|
156
|
+
for (const character of plainText) {
|
|
157
|
+
tokens.push({ value: character, width: 1 });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const { segment } of segmenter.segment(plainText)) {
|
|
164
|
+
tokens.push({ value: segment, width: getStringWidth(segment) });
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
(escape) => {
|
|
168
|
+
tokens.push({ value: escape, width: 0 });
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return tokens;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
type Word = {
|
|
176
|
+
value: string;
|
|
177
|
+
plainText: string;
|
|
178
|
+
width: number;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Split on spaces, ignoring spaces that appear inside a recognized sequence.
|
|
182
|
+
const splitWords = (string: string): Word[] => {
|
|
183
|
+
let currentWord: Word = { value: "", plainText: "", width: 0 };
|
|
184
|
+
const words: Word[] = [currentWord];
|
|
185
|
+
|
|
186
|
+
forEachSegment(
|
|
187
|
+
string,
|
|
188
|
+
(plainText) => {
|
|
189
|
+
const parts = plainText.split(" ");
|
|
190
|
+
currentWord.value += parts[0];
|
|
191
|
+
currentWord.plainText += parts[0];
|
|
192
|
+
|
|
193
|
+
for (let index = 1; index < parts.length; index++) {
|
|
194
|
+
currentWord = { value: parts[index]!, plainText: parts[index]!, width: 0 };
|
|
195
|
+
words.push(currentWord);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
(escape) => {
|
|
199
|
+
currentWord.value += escape;
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Measured once per word rather than per run, so a grapheme cluster split
|
|
204
|
+
// by an escape still counts once.
|
|
205
|
+
for (const word of words) {
|
|
206
|
+
word.width = getStringWidth(word.plainText);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return words;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
type SgrToken = {
|
|
213
|
+
readonly code: number;
|
|
214
|
+
readonly open: string;
|
|
215
|
+
readonly hasArguments: boolean;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
type ActiveStyle = {
|
|
219
|
+
readonly family: string;
|
|
220
|
+
readonly open: number | string;
|
|
221
|
+
readonly close: number;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const getColonColorToken = (parameter: string): SgrToken | undefined => {
|
|
225
|
+
const parts = parameter.split(":");
|
|
226
|
+
const code = Number.parseInt(parts[0]!, 10);
|
|
227
|
+
const mode = Number.parseInt(parts[1]!, 10);
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
![
|
|
231
|
+
ANSI_SGR_FOREGROUND_EXTENDED,
|
|
232
|
+
ANSI_SGR_BACKGROUND_EXTENDED,
|
|
233
|
+
ANSI_SGR_UNDERLINE_COLOR_EXTENDED,
|
|
234
|
+
].includes(code)
|
|
235
|
+
) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (mode === ANSI_SGR_COLOR_MODE_256 && parts.length === 3 && /^\d+$/.test(parts[2]!)) {
|
|
240
|
+
return { code, open: parameter, hasArguments: true };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (mode !== ANSI_SGR_COLOR_MODE_RGB) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const components = parts.length === 6 ? parts.slice(3) : parts.slice(2);
|
|
248
|
+
const colorSpace = parts.length === 6 ? parts[2] : undefined;
|
|
249
|
+
if (
|
|
250
|
+
components.length === 3 &&
|
|
251
|
+
components.every((component) => /^\d+$/.test(component)) &&
|
|
252
|
+
(colorSpace === undefined || /^\d*$/.test(colorSpace))
|
|
253
|
+
) {
|
|
254
|
+
return { code, open: parameter, hasArguments: true };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const getSgrTokens = (sgrParameters: string): SgrToken[] => {
|
|
261
|
+
const parameters = sgrParameters.split(";");
|
|
262
|
+
const sgrTokens: SgrToken[] = [];
|
|
263
|
+
|
|
264
|
+
for (let index = 0; index < parameters.length; index++) {
|
|
265
|
+
const parameter = parameters[index]!;
|
|
266
|
+
if (parameter.includes(":")) {
|
|
267
|
+
const colonColorToken = getColonColorToken(parameter);
|
|
268
|
+
if (colonColorToken) {
|
|
269
|
+
sgrTokens.push(colonColorToken);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const code = parameter === "" ? ANSI_SGR_RESET : Number.parseInt(parameter, 10);
|
|
276
|
+
|
|
277
|
+
if (!Number.isFinite(code)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (
|
|
282
|
+
code === ANSI_SGR_FOREGROUND_EXTENDED ||
|
|
283
|
+
code === ANSI_SGR_BACKGROUND_EXTENDED ||
|
|
284
|
+
code === ANSI_SGR_UNDERLINE_COLOR_EXTENDED
|
|
285
|
+
) {
|
|
286
|
+
if (index + 1 >= parameters.length) {
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const mode = Number.parseInt(parameters[index + 1]!, 10);
|
|
291
|
+
const colorIndex = Number.parseInt(parameters[index + 2] ?? "", 10);
|
|
292
|
+
if (mode === ANSI_SGR_COLOR_MODE_256 && Number.isFinite(colorIndex)) {
|
|
293
|
+
sgrTokens.push({ code, open: [code, mode, colorIndex].join(";"), hasArguments: true });
|
|
294
|
+
index += 2;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const red = Number.parseInt(parameters[index + 2] ?? "", 10);
|
|
299
|
+
const green = Number.parseInt(parameters[index + 3] ?? "", 10);
|
|
300
|
+
const blue = Number.parseInt(parameters[index + 4] ?? "", 10);
|
|
301
|
+
if (
|
|
302
|
+
mode === ANSI_SGR_COLOR_MODE_RGB &&
|
|
303
|
+
Number.isFinite(red) &&
|
|
304
|
+
Number.isFinite(green) &&
|
|
305
|
+
Number.isFinite(blue)
|
|
306
|
+
) {
|
|
307
|
+
sgrTokens.push({
|
|
308
|
+
code,
|
|
309
|
+
open: [code, mode, red, green, blue].join(";"),
|
|
310
|
+
hasArguments: true,
|
|
311
|
+
});
|
|
312
|
+
index += 4;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
sgrTokens.push({ code, open: String(code), hasArguments: false });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return sgrTokens;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const removeActiveStyle = (activeStyles: ActiveStyle[], family: string): void => {
|
|
326
|
+
const activeStyleIndex = activeStyles.findIndex((activeStyle) => activeStyle.family === family);
|
|
327
|
+
|
|
328
|
+
if (activeStyleIndex !== -1) {
|
|
329
|
+
activeStyles.splice(activeStyleIndex, 1);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const upsertActiveStyle = (activeStyles: ActiveStyle[], nextActiveStyle: ActiveStyle): void => {
|
|
334
|
+
removeActiveStyle(activeStyles, nextActiveStyle.family);
|
|
335
|
+
activeStyles.push(nextActiveStyle);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const removeModifierStylesByClose = (activeStyles: ActiveStyle[], closeCode: number): void => {
|
|
339
|
+
for (let index = activeStyles.length - 1; index >= 0; index--) {
|
|
340
|
+
const activeStyle = activeStyles[index]!;
|
|
341
|
+
if (activeStyle.family.startsWith("modifier-") && activeStyle.close === closeCode) {
|
|
342
|
+
activeStyles.splice(index, 1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const getColorStyle = (sgrToken: SgrToken): ActiveStyle | undefined => {
|
|
348
|
+
const { code, open, hasArguments } = sgrToken;
|
|
349
|
+
if (
|
|
350
|
+
(code >= 30 && code <= 37) ||
|
|
351
|
+
(code >= 90 && code <= 97) ||
|
|
352
|
+
(code === ANSI_SGR_FOREGROUND_EXTENDED && hasArguments)
|
|
353
|
+
) {
|
|
354
|
+
return {
|
|
355
|
+
family: "foreground",
|
|
356
|
+
open,
|
|
357
|
+
close: ANSI_SGR_RESET_FOREGROUND,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (
|
|
362
|
+
(code >= 40 && code <= 47) ||
|
|
363
|
+
(code >= 100 && code <= 107) ||
|
|
364
|
+
(code === ANSI_SGR_BACKGROUND_EXTENDED && hasArguments)
|
|
365
|
+
) {
|
|
366
|
+
return {
|
|
367
|
+
family: "background",
|
|
368
|
+
open,
|
|
369
|
+
close: ANSI_SGR_RESET_BACKGROUND,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (code === ANSI_SGR_UNDERLINE_COLOR_EXTENDED && hasArguments) {
|
|
374
|
+
return {
|
|
375
|
+
family: "underlineColor",
|
|
376
|
+
open,
|
|
377
|
+
close: ANSI_SGR_RESET_UNDERLINE_COLOR,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const applySgrResetCode = (code: number, activeStyles: ActiveStyle[]): boolean => {
|
|
385
|
+
if (code === ANSI_SGR_RESET) {
|
|
386
|
+
activeStyles.length = 0;
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (code === ANSI_SGR_RESET_FOREGROUND) {
|
|
391
|
+
removeActiveStyle(activeStyles, "foreground");
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (code === ANSI_SGR_RESET_BACKGROUND) {
|
|
396
|
+
removeActiveStyle(activeStyles, "background");
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (code === ANSI_SGR_RESET_UNDERLINE_COLOR) {
|
|
401
|
+
removeActiveStyle(activeStyles, "underlineColor");
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (ANSI_SGR_MODIFIER_CLOSE_CODES.has(code)) {
|
|
406
|
+
removeModifierStylesByClose(activeStyles, code);
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return false;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const applySgrToken = (sgrToken: SgrToken, activeStyles: ActiveStyle[]): void => {
|
|
414
|
+
const { code } = sgrToken;
|
|
415
|
+
|
|
416
|
+
if (applySgrResetCode(code, activeStyles)) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const colorStyle = getColorStyle(sgrToken);
|
|
421
|
+
if (colorStyle) {
|
|
422
|
+
upsertActiveStyle(activeStyles, colorStyle);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const close = sgrCodes.get(code);
|
|
427
|
+
if (close !== undefined && close !== ANSI_SGR_RESET) {
|
|
428
|
+
upsertActiveStyle(activeStyles, {
|
|
429
|
+
family: `modifier-${code}`,
|
|
430
|
+
open: sgrToken.open,
|
|
431
|
+
close,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const applySgrParameters = (sgrParameters: string, activeStyles: ActiveStyle[]): void => {
|
|
437
|
+
for (const sgrToken of getSgrTokens(sgrParameters)) {
|
|
438
|
+
applySgrToken(sgrToken, activeStyles);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const applySgrResets = (sgrParameters: string, activeStyles: ActiveStyle[]): void => {
|
|
443
|
+
for (const { code } of getSgrTokens(sgrParameters)) {
|
|
444
|
+
applySgrResetCode(code, activeStyles);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const applyLeadingSgrResets = (
|
|
449
|
+
string: string,
|
|
450
|
+
startIndex: number,
|
|
451
|
+
activeStyles: ActiveStyle[],
|
|
452
|
+
): void => {
|
|
453
|
+
let index = startIndex;
|
|
454
|
+
|
|
455
|
+
while (index < string.length) {
|
|
456
|
+
const match = matchAnsiEscape(string, index);
|
|
457
|
+
if (!match) {
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (match.groups?.["sgr"] !== undefined) {
|
|
462
|
+
applySgrResets(match.groups["sgr"], activeStyles);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
index += match[0].length;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const getClosingSgrSequence = (activeStyles: readonly ActiveStyle[]): string =>
|
|
470
|
+
[...activeStyles]
|
|
471
|
+
.reverse()
|
|
472
|
+
.map((activeStyle) => wrapAnsiCode(activeStyle.close))
|
|
473
|
+
.join("");
|
|
474
|
+
|
|
475
|
+
const getOpeningSgrSequence = (activeStyles: readonly ActiveStyle[]): string =>
|
|
476
|
+
activeStyles.map((activeStyle) => wrapAnsiCode(activeStyle.open)).join("");
|
|
477
|
+
|
|
478
|
+
// Wrap a long word across multiple rows. ANSI escape codes do not count
|
|
479
|
+
// towards length. Takes the visible width of the last row and returns the
|
|
480
|
+
// width of the row the word ends on.
|
|
481
|
+
const wrapWord = (rows: string[], word: string, columns: number, rowWidth: number): number => {
|
|
482
|
+
const tokens = getTokens(word);
|
|
483
|
+
|
|
484
|
+
let visible = rowWidth;
|
|
485
|
+
|
|
486
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
487
|
+
const token = tokens[index]!;
|
|
488
|
+
|
|
489
|
+
// Escape sequences and combining marks are zero width, so they always
|
|
490
|
+
// stay on the current row.
|
|
491
|
+
if (token.width > 0 && visible > 0 && visible + token.width > columns) {
|
|
492
|
+
rows.push("");
|
|
493
|
+
visible = 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
rows[rows.length - 1] += token.value;
|
|
497
|
+
visible += token.width;
|
|
498
|
+
|
|
499
|
+
if (visible === columns && index < tokens.length - 1) {
|
|
500
|
+
rows.push("");
|
|
501
|
+
visible = 0;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// It's possible that the last row we copy over is only ANSI escape
|
|
506
|
+
// characters, handle this edge-case
|
|
507
|
+
if (!visible && rows.at(-1)!.length > 0 && rows.length > 1) {
|
|
508
|
+
const lastRow = rows.pop()!;
|
|
509
|
+
rows[rows.length - 1] = rows[rows.length - 1]! + lastRow;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Only the finished row tells the true width when a grapheme cluster was
|
|
513
|
+
// split by an escape sequence, and it is at most one row long to measure.
|
|
514
|
+
return getWidth(rows.at(-1)!);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Trims spaces from a string ignoring invisible sequences
|
|
518
|
+
const stringVisibleTrimSpacesRight = (string: string): string => {
|
|
519
|
+
if (!string.includes(" ")) {
|
|
520
|
+
return string;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const segments: Array<{ value: string; isEscape: boolean }> = [];
|
|
524
|
+
forEachSegment(
|
|
525
|
+
string,
|
|
526
|
+
(plainText) => {
|
|
527
|
+
segments.push({ value: plainText, isEscape: false });
|
|
528
|
+
},
|
|
529
|
+
(escape) => {
|
|
530
|
+
segments.push({ value: escape, isEscape: true });
|
|
531
|
+
},
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Drop the spaces that trail the last visible character, but keep the
|
|
535
|
+
// invisible sequences among them.
|
|
536
|
+
for (let index = segments.length - 1; index >= 0; index--) {
|
|
537
|
+
const segment = segments[index]!;
|
|
538
|
+
|
|
539
|
+
if (segment.isEscape) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Scanned rather than matched with a regex, as a trailing-space pattern
|
|
544
|
+
// backtracks quadratically.
|
|
545
|
+
let end = segment.value.length;
|
|
546
|
+
while (end > 0 && segment.value[end - 1] === " ") {
|
|
547
|
+
end--;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
segment.value = segment.value.slice(0, end);
|
|
551
|
+
|
|
552
|
+
if (getStringWidth(segment.value) > 0) {
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return segments.map((segment) => segment.value).join("");
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const expandTabs = (line: string): string => {
|
|
561
|
+
if (!line.includes("\t")) {
|
|
562
|
+
return line;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let visible = 0;
|
|
566
|
+
let expandedLine = "";
|
|
567
|
+
let plainTextSinceTab = "";
|
|
568
|
+
|
|
569
|
+
const expandPlainText = (plainText: string): void => {
|
|
570
|
+
const segments = plainText.split("\t");
|
|
571
|
+
|
|
572
|
+
for (const [index, segment] of segments.entries()) {
|
|
573
|
+
expandedLine += segment;
|
|
574
|
+
plainTextSinceTab += segment;
|
|
575
|
+
|
|
576
|
+
if (index < segments.length - 1) {
|
|
577
|
+
visible += getStringWidth(plainTextSinceTab);
|
|
578
|
+
plainTextSinceTab = "";
|
|
579
|
+
const spaces = TAB_SIZE - (visible % TAB_SIZE);
|
|
580
|
+
expandedLine += " ".repeat(spaces);
|
|
581
|
+
visible += spaces;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
forEachSegment(line, expandPlainText, (escape) => {
|
|
587
|
+
expandedLine += escape;
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
return expandedLine;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// Close the active styles and hyperlink before every row break and reopen
|
|
594
|
+
// them after, so each row stands on its own.
|
|
595
|
+
const restoreStylesAcrossRows = (preString: string): string => {
|
|
596
|
+
let returnValue = "";
|
|
597
|
+
let activeHyperlink: { parameters: string; uri: string } | undefined;
|
|
598
|
+
const activeStyles: ActiveStyle[] = [];
|
|
599
|
+
let index = 0;
|
|
600
|
+
let copiedIndex = 0;
|
|
601
|
+
|
|
602
|
+
while (index < preString.length) {
|
|
603
|
+
ROW_BOUNDARY_REGEX.lastIndex = index;
|
|
604
|
+
const boundary = ROW_BOUNDARY_REGEX.exec(preString);
|
|
605
|
+
|
|
606
|
+
if (!boundary) {
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
index = boundary.index;
|
|
611
|
+
|
|
612
|
+
if (boundary[0] !== "\n") {
|
|
613
|
+
const escape = matchAnsiEscape(preString, index);
|
|
614
|
+
|
|
615
|
+
if (!escape) {
|
|
616
|
+
index++;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const groups = escape.groups ?? {};
|
|
621
|
+
if (groups["sgr"] !== undefined) {
|
|
622
|
+
applySgrParameters(groups["sgr"], activeStyles);
|
|
623
|
+
} else if (groups["uri"] !== undefined) {
|
|
624
|
+
activeHyperlink =
|
|
625
|
+
groups["uri"].length === 0
|
|
626
|
+
? undefined
|
|
627
|
+
: { parameters: groups["parameters"] ?? "", uri: groups["uri"] };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
index += escape[0].length;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Everything up to the row break is copied verbatim, sequences included.
|
|
635
|
+
returnValue += preString.slice(copiedIndex, index);
|
|
636
|
+
|
|
637
|
+
// An empty row never reopened anything, so there is nothing to close.
|
|
638
|
+
if (index > copiedIndex) {
|
|
639
|
+
if (activeHyperlink) {
|
|
640
|
+
returnValue += wrapAnsiHyperlink("");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
returnValue += getClosingSgrSequence(activeStyles);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
returnValue += "\n";
|
|
647
|
+
index++;
|
|
648
|
+
copiedIndex = index;
|
|
649
|
+
|
|
650
|
+
// An empty row has nothing to style, so the styles stay closed until the
|
|
651
|
+
// next row with content. A trailing row break leaves no row at all.
|
|
652
|
+
if (index < preString.length && preString[index] !== "\n") {
|
|
653
|
+
const openingStyles = [...activeStyles];
|
|
654
|
+
applyLeadingSgrResets(preString, index, openingStyles);
|
|
655
|
+
returnValue += getOpeningSgrSequence(openingStyles);
|
|
656
|
+
|
|
657
|
+
if (activeHyperlink) {
|
|
658
|
+
returnValue += wrapAnsiHyperlink(activeHyperlink.uri, activeHyperlink.parameters);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return returnValue + preString.slice(copiedIndex);
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// 'hard' will never allow a string to take up more than `columns` characters;
|
|
667
|
+
// 'soft' allows long words to expand past the column length.
|
|
668
|
+
// eslint-disable-next-line complexity
|
|
669
|
+
const exec = (string: string, columns: number, options: WrapOptions = {}): string => {
|
|
670
|
+
if (options.trim !== false && string.trim() === "") {
|
|
671
|
+
return "";
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const words = splitWords(string);
|
|
675
|
+
let rows = [""];
|
|
676
|
+
// Tracked as rows are built; remeasuring the row for every word makes
|
|
677
|
+
// wrapping quadratic in the line length.
|
|
678
|
+
let rowLength = 0;
|
|
679
|
+
// Words are only ever appended, so a row that already starts with content
|
|
680
|
+
// can never become trimmable again.
|
|
681
|
+
let trimmedRowIndex = -1;
|
|
682
|
+
|
|
683
|
+
let isFirstWord = true;
|
|
684
|
+
|
|
685
|
+
for (const word of words) {
|
|
686
|
+
const rowIndex = rows.length - 1;
|
|
687
|
+
|
|
688
|
+
if (options.trim !== false && trimmedRowIndex !== rowIndex) {
|
|
689
|
+
const row = rows[rowIndex]!;
|
|
690
|
+
const trimmedRow = row.trimStart();
|
|
691
|
+
|
|
692
|
+
if (trimmedRow.length !== row.length) {
|
|
693
|
+
rows[rowIndex] = trimmedRow;
|
|
694
|
+
rowLength = getWidth(trimmedRow);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (trimmedRow.length > 0) {
|
|
698
|
+
trimmedRowIndex = rowIndex;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (isFirstWord) {
|
|
703
|
+
isFirstWord = false;
|
|
704
|
+
} else {
|
|
705
|
+
if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
|
|
706
|
+
// If we start with a new word but the current row length equals the
|
|
707
|
+
// length of the columns, add a new row
|
|
708
|
+
rows.push("");
|
|
709
|
+
rowLength = 0;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (rowLength > 0 || options.trim === false) {
|
|
713
|
+
rows[rows.length - 1] += " ";
|
|
714
|
+
rowLength++;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// In 'hard' wrap mode, the length of a line is never allowed to extend
|
|
719
|
+
// past 'columns'
|
|
720
|
+
if (options.hard && options.wordWrap !== false && word.width > columns) {
|
|
721
|
+
const remainingColumns = columns - rowLength;
|
|
722
|
+
const breaksStartingThisLine = 1 + Math.floor((word.width - remainingColumns - 1) / columns);
|
|
723
|
+
const breaksStartingNextLine = Math.floor((word.width - 1) / columns);
|
|
724
|
+
if (breaksStartingNextLine < breaksStartingThisLine) {
|
|
725
|
+
rows.push("");
|
|
726
|
+
rowLength = 0;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
rowLength = wrapWord(rows, word.value, columns, rowLength);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (rowLength + word.width > columns && rowLength > 0 && word.width > 0) {
|
|
734
|
+
if (options.wordWrap === false && rowLength < columns) {
|
|
735
|
+
rowLength = wrapWord(rows, word.value, columns, rowLength);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
rows.push("");
|
|
740
|
+
rowLength = 0;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (rowLength + word.width > columns && options.wordWrap === false) {
|
|
744
|
+
rowLength = wrapWord(rows, word.value, columns, rowLength);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
rows[rows.length - 1] += word.value;
|
|
749
|
+
rowLength += word.width;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (options.trim !== false) {
|
|
753
|
+
rows = rows.map((row) => stringVisibleTrimSpacesRight(row));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return restoreStylesAcrossRows(rows.join("\n"));
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
export function wrapAnsi(string: string, columns: number, options?: WrapOptions): string {
|
|
760
|
+
return string
|
|
761
|
+
.normalize()
|
|
762
|
+
.replaceAll("\r\n", "\n")
|
|
763
|
+
.split("\n")
|
|
764
|
+
.map((line) => exec(expandTabs(line), columns, options))
|
|
765
|
+
.join("\n");
|
|
766
|
+
}
|