@depup/wrap-ansi 10.0.0-depup.0 → 10.0.2-depup.0
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 +5 -4
- package/changes.json +9 -5
- package/index.js +391 -197
- package/package.json +15 -14
package/README.md
CHANGED
|
@@ -13,16 +13,17 @@ npm install @depup/wrap-ansi
|
|
|
13
13
|
|
|
14
14
|
| Field | Value |
|
|
15
15
|
|-------|-------|
|
|
16
|
-
| Original | [wrap-ansi](https://www.npmjs.com/package/wrap-ansi) @ 10.0.
|
|
17
|
-
| Processed | 2026-
|
|
16
|
+
| Original | [wrap-ansi](https://www.npmjs.com/package/wrap-ansi) @ 10.0.2 |
|
|
17
|
+
| Processed | 2026-09-27 |
|
|
18
18
|
| Smoke test | passed |
|
|
19
|
-
| Deps updated |
|
|
19
|
+
| Deps updated | 2 |
|
|
20
20
|
|
|
21
21
|
## Dependency Changes
|
|
22
22
|
|
|
23
23
|
| Dependency | From | To |
|
|
24
24
|
|------------|------|-----|
|
|
25
|
-
|
|
|
25
|
+
| ansi-styles | ^6.2.3 | ^7.0.0 |
|
|
26
|
+
| string-width | ^8.2.0 | ^8.3.0 |
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
package/changes.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bumped": {
|
|
3
|
-
"
|
|
4
|
-
"from": "^
|
|
5
|
-
"to": "^7.
|
|
3
|
+
"ansi-styles": {
|
|
4
|
+
"from": "^6.2.3",
|
|
5
|
+
"to": "^7.0.0"
|
|
6
|
+
},
|
|
7
|
+
"string-width": {
|
|
8
|
+
"from": "^8.2.0",
|
|
9
|
+
"to": "^8.3.0"
|
|
6
10
|
}
|
|
7
11
|
},
|
|
8
|
-
"timestamp": "2026-
|
|
9
|
-
"totalUpdated":
|
|
12
|
+
"timestamp": "2026-09-27T01:05:09.603Z",
|
|
13
|
+
"totalUpdated": 2
|
|
10
14
|
}
|
package/index.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import stringWidth from 'string-width';
|
|
2
|
-
import stripAnsi from 'strip-ansi';
|
|
3
2
|
import ansiStyles from 'ansi-styles';
|
|
4
3
|
|
|
5
4
|
const ANSI_ESCAPE = '\u001B';
|
|
6
|
-
const ANSI_ESCAPE_CSI = '\u009B';
|
|
7
|
-
const ESCAPES = new Set([
|
|
8
|
-
ANSI_ESCAPE,
|
|
9
|
-
ANSI_ESCAPE_CSI,
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
5
|
const ANSI_ESCAPE_BELL = '\u0007';
|
|
6
|
+
const C1_CSI = '\u009B';
|
|
13
7
|
const ANSI_CSI = '[';
|
|
14
8
|
const ANSI_OSC = ']';
|
|
15
9
|
const ANSI_SGR_TERMINATOR = 'm';
|
|
@@ -20,61 +14,219 @@ const ANSI_SGR_RESET_UNDERLINE_COLOR = 59;
|
|
|
20
14
|
const ANSI_SGR_FOREGROUND_EXTENDED = 38;
|
|
21
15
|
const ANSI_SGR_BACKGROUND_EXTENDED = 48;
|
|
22
16
|
const ANSI_SGR_UNDERLINE_COLOR_EXTENDED = 58;
|
|
23
|
-
const ANSI_SGR_COLOR_MODE_256 = 5;
|
|
24
17
|
const ANSI_SGR_COLOR_MODE_RGB = 2;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
18
|
+
const ANSI_SGR_COLOR_MODE_256 = 5;
|
|
19
|
+
const ANSI_ESCAPE_LINK = `${ANSI_OSC}8;`;
|
|
20
|
+
// The first character of every sequence we recognize.
|
|
21
|
+
const ESCAPES = new Set([
|
|
22
|
+
ANSI_ESCAPE,
|
|
23
|
+
C1_CSI,
|
|
24
|
+
]);
|
|
25
|
+
const ESCAPE_CHARACTERS = [...ESCAPES].join('');
|
|
26
|
+
|
|
27
|
+
const CSI_INTRODUCER = `(?:${ANSI_ESCAPE}\\${ANSI_CSI}|${C1_CSI})`;
|
|
28
|
+
const CSI_PARAMETERS = '[0-?]*[ -/]*[@-~]';
|
|
29
|
+
const SGR_PARAMETERS = `(?<sgr>[0-9;:]*)${ANSI_SGR_TERMINATOR}`;
|
|
30
|
+
const OSC_STRING_TERMINATOR = `(?:${ANSI_ESCAPE_BELL}|${ANSI_ESCAPE}\\\\)`;
|
|
31
|
+
const OSC_STRING_PAYLOAD = String.raw`[^\u0000-\u001F\u007F-\u009F]*`;
|
|
32
|
+
// A hyperlink is `OSC 8 ; parameters ; URI ST`, where `parameters` is a possibly empty list of `key=value` pairs joined by `:`.
|
|
33
|
+
const LINK_PARAMETERS = String.raw`8;(?<parameters>[^;\u0000-\u001F\u007F-\u009F]*);(?<uri>${OSC_STRING_PAYLOAD})${OSC_STRING_TERMINATOR}`;
|
|
34
|
+
const OSC_STRING = `${OSC_STRING_PAYLOAD}${OSC_STRING_TERMINATOR}`;
|
|
35
|
+
|
|
36
|
+
// Supported boundary: semicolon-delimited SGR styling, colon-delimited RGB/indexed colors, and OSC 8 hyperlinks are tracked, while ordinary CSI sequences and other complete 7-bit OSC commands are preserved as opaque zero-width units. This is intentionally not a terminal emulator, so other colon-delimited SGR semantics, C0 bytes inside sequences, DCS/SOS/PM/APC control strings, cancellations, generic ESC sequences, and 8-bit control-string forms are unsupported. Newlines always delimit input lines before ANSI parsing.
|
|
37
|
+
const ANSI_ESCAPE_REGEX = new RegExp(
|
|
38
|
+
`${CSI_INTRODUCER}(?:${SGR_PARAMETERS}|${CSI_PARAMETERS})`
|
|
39
|
+
+ `|${ANSI_ESCAPE}\\${ANSI_OSC}(?:${LINK_PARAMETERS}|${OSC_STRING})`,
|
|
40
|
+
'y',
|
|
41
|
+
);
|
|
42
|
+
|
|
28
43
|
const ANSI_SGR_MODIFIER_CLOSE_CODES = new Set(ansiStyles.codes.values());
|
|
29
44
|
ANSI_SGR_MODIFIER_CLOSE_CODES.delete(ANSI_SGR_RESET);
|
|
30
45
|
|
|
31
46
|
const segmenter = new Intl.Segmenter();
|
|
32
|
-
|
|
47
|
+
// Complete ANSI sequences have already been removed before measuring these strings. Avoid string-width's ANSI scan so malformed sequences are not rescanned.
|
|
48
|
+
const getStringWidth = string => stringWidth(string, {countAnsiEscapeCodes: true});
|
|
33
49
|
const TAB_SIZE = 8;
|
|
34
50
|
|
|
51
|
+
// Finds the next character that could introduce a sequence, so plain text is skipped in one native step.
|
|
52
|
+
const ESCAPE_INTRODUCER_REGEX = new RegExp(`[${ESCAPE_CHARACTERS}]`, 'g');
|
|
53
|
+
// The final pass only cares about sequences and row boundaries, so it skips everything else in one native step.
|
|
54
|
+
const ROW_BOUNDARY_REGEX = new RegExp(`[\\n${ESCAPE_CHARACTERS}]`, 'g');
|
|
55
|
+
// Every printable ASCII character is its own grapheme cluster of width one, which lets the segmenter be skipped.
|
|
56
|
+
const ASCII_PRINTABLE_REGEX = /^[ -~]*$/;
|
|
57
|
+
|
|
35
58
|
const wrapAnsiCode = code => `${ANSI_ESCAPE}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
|
|
36
|
-
const wrapAnsiHyperlink = url => `${ANSI_ESCAPE}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
|
|
59
|
+
const wrapAnsiHyperlink = (url, parameters = '') => `${ANSI_ESCAPE}${ANSI_ESCAPE_LINK}${parameters};${url}${ANSI_ESCAPE_BELL}`;
|
|
60
|
+
|
|
61
|
+
// Match a complete escape sequence starting at `index`, or return `undefined` when none starts there.
|
|
62
|
+
const matchAnsiEscape = (string, index) => {
|
|
63
|
+
if (!ESCAPES.has(string[index])) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ANSI_ESCAPE_REGEX.lastIndex = index;
|
|
68
|
+
return ANSI_ESCAPE_REGEX.exec(string) ?? undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Walk a string as alternating plain text runs and complete escape sequences.
|
|
72
|
+
// A character that looks like an introducer but does not start a valid sequence stays plain text.
|
|
73
|
+
const forEachSegment = (string, onPlainText, onEscape = () => {}) => {
|
|
74
|
+
let plainStart = 0;
|
|
75
|
+
let index = 0;
|
|
76
|
+
|
|
77
|
+
while (index < string.length) {
|
|
78
|
+
ESCAPE_INTRODUCER_REGEX.lastIndex = index;
|
|
79
|
+
const introducer = ESCAPE_INTRODUCER_REGEX.exec(string);
|
|
80
|
+
|
|
81
|
+
if (!introducer) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const escape = matchAnsiEscape(string, introducer.index);
|
|
86
|
+
|
|
87
|
+
if (!escape) {
|
|
88
|
+
index = introducer.index + 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (introducer.index > plainStart) {
|
|
93
|
+
onPlainText(string.slice(plainStart, introducer.index));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onEscape(escape[0]);
|
|
97
|
+
index = introducer.index + escape[0].length;
|
|
98
|
+
plainStart = index;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (plainStart < string.length) {
|
|
102
|
+
onPlainText(string.slice(plainStart));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// The visible width of a string, ignoring escape sequences.
|
|
107
|
+
const getWidth = string => {
|
|
108
|
+
let plainText = '';
|
|
109
|
+
|
|
110
|
+
forEachSegment(string, part => {
|
|
111
|
+
plainText += part;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return getStringWidth(plainText);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Split a string into escape sequences, which are zero width and must never be split, and grapheme clusters.
|
|
118
|
+
// The supported boundary is between grapheme clusters and ANSI sequences; ANSI inserted inside a cluster is treated as separate segments.
|
|
119
|
+
const getTokens = string => {
|
|
120
|
+
const tokens = [];
|
|
121
|
+
|
|
122
|
+
forEachSegment(string, plainText => {
|
|
123
|
+
if (ASCII_PRINTABLE_REGEX.test(plainText)) {
|
|
124
|
+
for (const character of plainText) {
|
|
125
|
+
tokens.push({value: character, width: 1});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const {segment} of segmenter.segment(plainText)) {
|
|
132
|
+
tokens.push({value: segment, width: getStringWidth(segment)});
|
|
133
|
+
}
|
|
134
|
+
}, escape => {
|
|
135
|
+
tokens.push({value: escape, width: 0});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return tokens;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Split on spaces, ignoring spaces that appear inside a recognized sequence.
|
|
142
|
+
const splitWords = string => {
|
|
143
|
+
let currentWord = {value: '', plainText: ''};
|
|
144
|
+
const words = [currentWord];
|
|
145
|
+
|
|
146
|
+
forEachSegment(string, plainText => {
|
|
147
|
+
const parts = plainText.split(' ');
|
|
148
|
+
currentWord.value += parts[0];
|
|
149
|
+
currentWord.plainText += parts[0];
|
|
150
|
+
|
|
151
|
+
for (let index = 1; index < parts.length; index++) {
|
|
152
|
+
currentWord = {value: parts[index], plainText: parts[index]};
|
|
153
|
+
words.push(currentWord);
|
|
154
|
+
}
|
|
155
|
+
}, escape => {
|
|
156
|
+
currentWord.value += escape;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Measured once per word rather than per run, so a grapheme cluster split by an escape still counts once.
|
|
160
|
+
for (const word of words) {
|
|
161
|
+
word.width = getStringWidth(word.plainText);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return words;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const getColonColorToken = parameter => {
|
|
168
|
+
const parts = parameter.split(':');
|
|
169
|
+
const code = Number.parseInt(parts[0], 10);
|
|
170
|
+
const mode = Number.parseInt(parts[1], 10);
|
|
171
|
+
|
|
172
|
+
if (![ANSI_SGR_FOREGROUND_EXTENDED, ANSI_SGR_BACKGROUND_EXTENDED, ANSI_SGR_UNDERLINE_COLOR_EXTENDED].includes(code)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (mode === ANSI_SGR_COLOR_MODE_256 && parts.length === 3 && /^\d+$/.test(parts[2])) {
|
|
177
|
+
return {code, open: parameter, hasArguments: true};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (mode !== ANSI_SGR_COLOR_MODE_RGB) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const components = parts.length === 6 ? parts.slice(3) : parts.slice(2);
|
|
185
|
+
const colorSpace = parts.length === 6 ? parts[2] : undefined;
|
|
186
|
+
if (components.length === 3 && components.every(component => /^\d+$/.test(component)) && (colorSpace === undefined || /^\d*$/.test(colorSpace))) {
|
|
187
|
+
return {code, open: parameter, hasArguments: true};
|
|
188
|
+
}
|
|
189
|
+
};
|
|
37
190
|
|
|
38
191
|
const getSgrTokens = sgrParameters => {
|
|
39
|
-
const
|
|
192
|
+
const parameters = sgrParameters.split(';');
|
|
40
193
|
const sgrTokens = [];
|
|
41
194
|
|
|
42
|
-
for (let index = 0; index <
|
|
43
|
-
const
|
|
195
|
+
for (let index = 0; index < parameters.length; index++) {
|
|
196
|
+
const parameter = parameters[index];
|
|
197
|
+
if (parameter.includes(':')) {
|
|
198
|
+
const colonColorToken = getColonColorToken(parameter);
|
|
199
|
+
if (colonColorToken) {
|
|
200
|
+
sgrTokens.push(colonColorToken);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const code = parameter === '' ? ANSI_SGR_RESET : Number.parseInt(parameter, 10);
|
|
44
207
|
|
|
45
208
|
if (!Number.isFinite(code)) {
|
|
46
209
|
continue;
|
|
47
210
|
}
|
|
48
211
|
|
|
49
|
-
if (
|
|
50
|
-
(
|
|
51
|
-
code === ANSI_SGR_FOREGROUND_EXTENDED
|
|
52
|
-
|| code === ANSI_SGR_BACKGROUND_EXTENDED
|
|
53
|
-
|| code === ANSI_SGR_UNDERLINE_COLOR_EXTENDED
|
|
54
|
-
)
|
|
55
|
-
) {
|
|
56
|
-
if (index + 1 >= codes.length) {
|
|
212
|
+
if (code === ANSI_SGR_FOREGROUND_EXTENDED || code === ANSI_SGR_BACKGROUND_EXTENDED || code === ANSI_SGR_UNDERLINE_COLOR_EXTENDED) {
|
|
213
|
+
if (index + 1 >= parameters.length) {
|
|
57
214
|
break;
|
|
58
215
|
}
|
|
59
216
|
|
|
60
|
-
const mode =
|
|
61
|
-
|
|
62
|
-
if (mode === ANSI_SGR_COLOR_MODE_256 && Number.isFinite(
|
|
63
|
-
sgrTokens.push([code, mode,
|
|
217
|
+
const mode = Number.parseInt(parameters[index + 1], 10);
|
|
218
|
+
const colorIndex = Number.parseInt(parameters[index + 2], 10);
|
|
219
|
+
if (mode === ANSI_SGR_COLOR_MODE_256 && Number.isFinite(colorIndex)) {
|
|
220
|
+
sgrTokens.push({code, open: [code, mode, colorIndex].join(';'), hasArguments: true});
|
|
64
221
|
index += 2;
|
|
65
222
|
continue;
|
|
66
223
|
}
|
|
67
224
|
|
|
68
|
-
const red =
|
|
69
|
-
const green =
|
|
70
|
-
const blue =
|
|
71
|
-
if (
|
|
72
|
-
mode
|
|
73
|
-
&& Number.isFinite(red)
|
|
74
|
-
&& Number.isFinite(green)
|
|
75
|
-
&& Number.isFinite(blue)
|
|
76
|
-
) {
|
|
77
|
-
sgrTokens.push([code, mode, red, green, blue]);
|
|
225
|
+
const red = Number.parseInt(parameters[index + 2], 10);
|
|
226
|
+
const green = Number.parseInt(parameters[index + 3], 10);
|
|
227
|
+
const blue = Number.parseInt(parameters[index + 4], 10);
|
|
228
|
+
if (mode === ANSI_SGR_COLOR_MODE_RGB && Number.isFinite(red) && Number.isFinite(green) && Number.isFinite(blue)) {
|
|
229
|
+
sgrTokens.push({code, open: [code, mode, red, green, blue].join(';'), hasArguments: true});
|
|
78
230
|
index += 4;
|
|
79
231
|
continue;
|
|
80
232
|
}
|
|
@@ -82,7 +234,7 @@ const getSgrTokens = sgrParameters => {
|
|
|
82
234
|
break;
|
|
83
235
|
}
|
|
84
236
|
|
|
85
|
-
sgrTokens.push(
|
|
237
|
+
sgrTokens.push({code, open: String(code), hasArguments: false});
|
|
86
238
|
}
|
|
87
239
|
|
|
88
240
|
return sgrTokens;
|
|
@@ -110,27 +262,28 @@ const removeModifierStylesByClose = (activeStyles, closeCode) => {
|
|
|
110
262
|
}
|
|
111
263
|
};
|
|
112
264
|
|
|
113
|
-
const getColorStyle =
|
|
114
|
-
|
|
265
|
+
const getColorStyle = sgrToken => {
|
|
266
|
+
const {code, open, hasArguments} = sgrToken;
|
|
267
|
+
if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97) || (code === ANSI_SGR_FOREGROUND_EXTENDED && hasArguments)) {
|
|
115
268
|
return {
|
|
116
269
|
family: 'foreground',
|
|
117
|
-
open
|
|
270
|
+
open,
|
|
118
271
|
close: ANSI_SGR_RESET_FOREGROUND,
|
|
119
272
|
};
|
|
120
273
|
}
|
|
121
274
|
|
|
122
|
-
if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107) || (code === ANSI_SGR_BACKGROUND_EXTENDED &&
|
|
275
|
+
if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107) || (code === ANSI_SGR_BACKGROUND_EXTENDED && hasArguments)) {
|
|
123
276
|
return {
|
|
124
277
|
family: 'background',
|
|
125
|
-
open
|
|
278
|
+
open,
|
|
126
279
|
close: ANSI_SGR_RESET_BACKGROUND,
|
|
127
280
|
};
|
|
128
281
|
}
|
|
129
282
|
|
|
130
|
-
if (code === ANSI_SGR_UNDERLINE_COLOR_EXTENDED &&
|
|
283
|
+
if (code === ANSI_SGR_UNDERLINE_COLOR_EXTENDED && hasArguments) {
|
|
131
284
|
return {
|
|
132
285
|
family: 'underlineColor',
|
|
133
|
-
open
|
|
286
|
+
open,
|
|
134
287
|
close: ANSI_SGR_RESET_UNDERLINE_COLOR,
|
|
135
288
|
};
|
|
136
289
|
}
|
|
@@ -166,13 +319,13 @@ const applySgrResetCode = (code, activeStyles) => {
|
|
|
166
319
|
};
|
|
167
320
|
|
|
168
321
|
const applySgrToken = (sgrToken, activeStyles) => {
|
|
169
|
-
const
|
|
322
|
+
const {code} = sgrToken;
|
|
170
323
|
|
|
171
324
|
if (applySgrResetCode(code, activeStyles)) {
|
|
172
325
|
return;
|
|
173
326
|
}
|
|
174
327
|
|
|
175
|
-
const colorStyle = getColorStyle(
|
|
328
|
+
const colorStyle = getColorStyle(sgrToken);
|
|
176
329
|
if (colorStyle) {
|
|
177
330
|
upsertActiveStyle(activeStyles, colorStyle);
|
|
178
331
|
return;
|
|
@@ -182,7 +335,7 @@ const applySgrToken = (sgrToken, activeStyles) => {
|
|
|
182
335
|
if (close !== undefined && close !== ANSI_SGR_RESET) {
|
|
183
336
|
upsertActiveStyle(activeStyles, {
|
|
184
337
|
family: `modifier-${code}`,
|
|
185
|
-
open: sgrToken.
|
|
338
|
+
open: sgrToken.open,
|
|
186
339
|
close,
|
|
187
340
|
});
|
|
188
341
|
}
|
|
@@ -195,97 +348,52 @@ const applySgrParameters = (sgrParameters, activeStyles) => {
|
|
|
195
348
|
};
|
|
196
349
|
|
|
197
350
|
const applySgrResets = (sgrParameters, activeStyles) => {
|
|
198
|
-
for (const
|
|
199
|
-
const [code] = sgrToken;
|
|
351
|
+
for (const {code} of getSgrTokens(sgrParameters)) {
|
|
200
352
|
applySgrResetCode(code, activeStyles);
|
|
201
353
|
}
|
|
202
354
|
};
|
|
203
355
|
|
|
204
|
-
const applyLeadingSgrResets = (string, activeStyles) => {
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
while (remainder.length > 0) {
|
|
208
|
-
if (remainder.startsWith(ANSI_ESCAPE) && remainder[1] !== '\\') {
|
|
209
|
-
const match = ANSI_ESCAPE_REGEX.exec(remainder);
|
|
210
|
-
if (!match) {
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
356
|
+
const applyLeadingSgrResets = (string, startIndex, activeStyles) => {
|
|
357
|
+
let index = startIndex;
|
|
213
358
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
remainder = remainder.slice(match[0].length);
|
|
219
|
-
continue;
|
|
359
|
+
while (index < string.length) {
|
|
360
|
+
const match = matchAnsiEscape(string, index);
|
|
361
|
+
if (!match) {
|
|
362
|
+
break;
|
|
220
363
|
}
|
|
221
364
|
|
|
222
|
-
if (
|
|
223
|
-
const match = ANSI_ESCAPE_CSI_REGEX.exec(remainder);
|
|
224
|
-
if (!match || match.groups.sgr === undefined) {
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
|
|
365
|
+
if (match.groups.sgr !== undefined) {
|
|
228
366
|
applySgrResets(match.groups.sgr, activeStyles);
|
|
229
|
-
remainder = remainder.slice(match[0].length);
|
|
230
|
-
continue;
|
|
231
367
|
}
|
|
232
368
|
|
|
233
|
-
|
|
369
|
+
index += match[0].length;
|
|
234
370
|
}
|
|
235
371
|
};
|
|
236
372
|
|
|
237
373
|
const getClosingSgrSequence = activeStyles => [...activeStyles].reverse().map(activeStyle => wrapAnsiCode(activeStyle.close)).join('');
|
|
238
374
|
const getOpeningSgrSequence = activeStyles => activeStyles.map(activeStyle => wrapAnsiCode(activeStyle.open)).join('');
|
|
239
375
|
|
|
240
|
-
// Calculate the length of words split on ' ', ignoring
|
|
241
|
-
// the extra characters added by ANSI escape codes
|
|
242
|
-
const wordLengths = string => string.split(' ').map(word => stringWidth(word));
|
|
243
|
-
|
|
244
376
|
// Wrap a long word across multiple rows
|
|
245
377
|
// ANSI escape codes do not count towards length
|
|
246
|
-
|
|
247
|
-
|
|
378
|
+
// Takes the visible width of the last row and returns the width of the row the word ends on, so callers never have to measure the rows themselves.
|
|
379
|
+
const wrapWord = (rows, word, columns, rowWidth) => {
|
|
380
|
+
const tokens = getTokens(word);
|
|
248
381
|
|
|
249
|
-
let
|
|
250
|
-
let isInsideLinkEscape = false;
|
|
251
|
-
let visible = stringWidth(stripAnsi(rows.at(-1)));
|
|
382
|
+
let visible = rowWidth;
|
|
252
383
|
|
|
253
|
-
for (
|
|
254
|
-
const
|
|
384
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
385
|
+
const token = tokens[index];
|
|
255
386
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
rows.push(character);
|
|
387
|
+
// Escape sequences and combining marks are zero width, so they always stay on the current row.
|
|
388
|
+
if (token.width > 0 && visible > 0 && visible + token.width > columns) {
|
|
389
|
+
rows.push('');
|
|
260
390
|
visible = 0;
|
|
261
391
|
}
|
|
262
392
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const ansiEscapeLinkCandidate = characters.slice(index + 1, index + 1 + ANSI_ESCAPE_LINK.length).join('');
|
|
267
|
-
isInsideLinkEscape = ansiEscapeLinkCandidate === ANSI_ESCAPE_LINK;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (isInsideEscape) {
|
|
271
|
-
if (isInsideLinkEscape) {
|
|
272
|
-
if (
|
|
273
|
-
character === ANSI_ESCAPE_BELL
|
|
274
|
-
|| (character === '\\' && index > 0 && characters[index - 1] === ANSI_ESCAPE) // ST terminator (ESC \)
|
|
275
|
-
) {
|
|
276
|
-
isInsideEscape = false;
|
|
277
|
-
isInsideLinkEscape = false;
|
|
278
|
-
}
|
|
279
|
-
} else if (character === ANSI_SGR_TERMINATOR) {
|
|
280
|
-
isInsideEscape = false;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
visible += characterLength;
|
|
393
|
+
rows[rows.length - 1] += token.value;
|
|
394
|
+
visible += token.width;
|
|
287
395
|
|
|
288
|
-
if (visible === columns && index <
|
|
396
|
+
if (visible === columns && index < tokens.length - 1) {
|
|
289
397
|
rows.push('');
|
|
290
398
|
visible = 0;
|
|
291
399
|
}
|
|
@@ -296,26 +404,46 @@ const wrapWord = (rows, word, columns) => {
|
|
|
296
404
|
if (!visible && rows.at(-1).length > 0 && rows.length > 1) {
|
|
297
405
|
rows[rows.length - 2] += rows.pop();
|
|
298
406
|
}
|
|
407
|
+
|
|
408
|
+
// The tokens are measured one by one, so a grapheme cluster that an escape sequence splits is counted once per part rather than once as a whole. Only the finished row tells the true width, and it is at most one row long to measure.
|
|
409
|
+
return getWidth(rows.at(-1));
|
|
299
410
|
};
|
|
300
411
|
|
|
301
412
|
// Trims spaces from a string ignoring invisible sequences
|
|
302
413
|
const stringVisibleTrimSpacesRight = string => {
|
|
303
|
-
|
|
304
|
-
|
|
414
|
+
if (!string.includes(' ')) {
|
|
415
|
+
return string;
|
|
416
|
+
}
|
|
305
417
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
418
|
+
const segments = [];
|
|
419
|
+
forEachSegment(string, plainText => {
|
|
420
|
+
segments.push({value: plainText, isEscape: false});
|
|
421
|
+
}, escape => {
|
|
422
|
+
segments.push({value: escape, isEscape: true});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Drop the spaces that trail the last visible character, but keep the invisible sequences among them.
|
|
426
|
+
for (let index = segments.length - 1; index >= 0; index--) {
|
|
427
|
+
const segment = segments[index];
|
|
428
|
+
|
|
429
|
+
if (segment.isEscape) {
|
|
430
|
+
continue;
|
|
309
431
|
}
|
|
310
432
|
|
|
311
|
-
|
|
312
|
-
|
|
433
|
+
// Scanned rather than matched with a regex, as a trailing-space pattern backtracks quadratically.
|
|
434
|
+
let end = segment.value.length;
|
|
435
|
+
while (end > 0 && segment.value[end - 1] === ' ') {
|
|
436
|
+
end--;
|
|
437
|
+
}
|
|
313
438
|
|
|
314
|
-
|
|
315
|
-
|
|
439
|
+
segment.value = segment.value.slice(0, end);
|
|
440
|
+
|
|
441
|
+
if (getStringWidth(segment.value) > 0) {
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
316
444
|
}
|
|
317
445
|
|
|
318
|
-
return
|
|
446
|
+
return segments.map(segment => segment.value).join('');
|
|
319
447
|
};
|
|
320
448
|
|
|
321
449
|
const expandTabs = line => {
|
|
@@ -323,24 +451,103 @@ const expandTabs = line => {
|
|
|
323
451
|
return line;
|
|
324
452
|
}
|
|
325
453
|
|
|
326
|
-
const segments = line.split('\t');
|
|
327
454
|
let visible = 0;
|
|
328
455
|
let expandedLine = '';
|
|
456
|
+
let plainTextSinceTab = '';
|
|
457
|
+
|
|
458
|
+
const expandPlainText = plainText => {
|
|
459
|
+
const segments = plainText.split('\t');
|
|
329
460
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
461
|
+
for (const [index, segment] of segments.entries()) {
|
|
462
|
+
expandedLine += segment;
|
|
463
|
+
plainTextSinceTab += segment;
|
|
333
464
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
465
|
+
if (index < segments.length - 1) {
|
|
466
|
+
visible += getStringWidth(plainTextSinceTab);
|
|
467
|
+
plainTextSinceTab = '';
|
|
468
|
+
const spaces = TAB_SIZE - (visible % TAB_SIZE);
|
|
469
|
+
expandedLine += ' '.repeat(spaces);
|
|
470
|
+
visible += spaces;
|
|
471
|
+
}
|
|
338
472
|
}
|
|
339
|
-
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
forEachSegment(line, expandPlainText, escape => {
|
|
476
|
+
expandedLine += escape;
|
|
477
|
+
});
|
|
340
478
|
|
|
341
479
|
return expandedLine;
|
|
342
480
|
};
|
|
343
481
|
|
|
482
|
+
// Close the active styles and hyperlink before every row break and reopen them after, so each row stands on its own.
|
|
483
|
+
// Only sequences and newlines matter here, so the string is scanned directly rather than split into grapheme clusters.
|
|
484
|
+
const restoreStylesAcrossRows = preString => {
|
|
485
|
+
let returnValue = '';
|
|
486
|
+
let activeHyperlink;
|
|
487
|
+
const activeStyles = [];
|
|
488
|
+
let index = 0;
|
|
489
|
+
let copiedIndex = 0;
|
|
490
|
+
|
|
491
|
+
while (index < preString.length) {
|
|
492
|
+
ROW_BOUNDARY_REGEX.lastIndex = index;
|
|
493
|
+
const boundary = ROW_BOUNDARY_REGEX.exec(preString);
|
|
494
|
+
|
|
495
|
+
if (!boundary) {
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
index = boundary.index;
|
|
500
|
+
|
|
501
|
+
if (boundary[0] !== '\n') {
|
|
502
|
+
const escape = matchAnsiEscape(preString, index);
|
|
503
|
+
|
|
504
|
+
if (!escape) {
|
|
505
|
+
index++;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const {groups} = escape;
|
|
510
|
+
if (groups.sgr !== undefined) {
|
|
511
|
+
applySgrParameters(groups.sgr, activeStyles);
|
|
512
|
+
} else if (groups.uri !== undefined) {
|
|
513
|
+
activeHyperlink = groups.uri.length === 0 ? undefined : {parameters: groups.parameters, uri: groups.uri};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
index += escape[0].length;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Everything up to the row break is copied verbatim, sequences included.
|
|
521
|
+
returnValue += preString.slice(copiedIndex, index);
|
|
522
|
+
|
|
523
|
+
// An empty row never reopened anything, so there is nothing to close.
|
|
524
|
+
if (index > copiedIndex) {
|
|
525
|
+
if (activeHyperlink) {
|
|
526
|
+
returnValue += wrapAnsiHyperlink('');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
returnValue += getClosingSgrSequence(activeStyles);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
returnValue += '\n';
|
|
533
|
+
index++;
|
|
534
|
+
copiedIndex = index;
|
|
535
|
+
|
|
536
|
+
// An empty row has nothing to style, so the styles stay closed until the next row with content. A trailing row break leaves no row at all.
|
|
537
|
+
if (index < preString.length && preString[index] !== '\n') {
|
|
538
|
+
const openingStyles = [...activeStyles];
|
|
539
|
+
applyLeadingSgrResets(preString, index, openingStyles);
|
|
540
|
+
returnValue += getOpeningSgrSequence(openingStyles);
|
|
541
|
+
|
|
542
|
+
if (activeHyperlink) {
|
|
543
|
+
returnValue += wrapAnsiHyperlink(activeHyperlink.uri, activeHyperlink.parameters);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return returnValue + preString.slice(copiedIndex);
|
|
549
|
+
};
|
|
550
|
+
|
|
344
551
|
// The wrap-ansi module can be invoked in either 'hard' or 'soft' wrap mode.
|
|
345
552
|
//
|
|
346
553
|
// 'hard' will never allow a string to take up more than columns characters.
|
|
@@ -351,21 +558,35 @@ const exec = (string, columns, options = {}) => {
|
|
|
351
558
|
return '';
|
|
352
559
|
}
|
|
353
560
|
|
|
354
|
-
|
|
355
|
-
let escapeUrl;
|
|
356
|
-
const activeStyles = [];
|
|
357
|
-
|
|
358
|
-
const lengths = wordLengths(string);
|
|
561
|
+
const words = splitWords(string);
|
|
359
562
|
let rows = [''];
|
|
563
|
+
// Tracked as rows are built. Remeasuring the row for every word makes wrapping quadratic in the line length.
|
|
564
|
+
let rowLength = 0;
|
|
565
|
+
// Words are only ever appended, so a row that already starts with content can never become trimmable again. Retrimming it for every word makes wrapping quadratic in the line length.
|
|
566
|
+
let trimmedRowIndex = -1;
|
|
360
567
|
|
|
361
|
-
|
|
362
|
-
if (options.trim !== false) {
|
|
363
|
-
rows[rows.length - 1] = rows.at(-1).trimStart();
|
|
364
|
-
}
|
|
568
|
+
let isFirstWord = true;
|
|
365
569
|
|
|
366
|
-
|
|
570
|
+
for (const word of words) {
|
|
571
|
+
const rowIndex = rows.length - 1;
|
|
572
|
+
|
|
573
|
+
if (options.trim !== false && trimmedRowIndex !== rowIndex) {
|
|
574
|
+
const row = rows[rowIndex];
|
|
575
|
+
const trimmedRow = row.trimStart();
|
|
576
|
+
|
|
577
|
+
if (trimmedRow.length !== row.length) {
|
|
578
|
+
rows[rowIndex] = trimmedRow;
|
|
579
|
+
rowLength = getWidth(trimmedRow);
|
|
580
|
+
}
|
|
367
581
|
|
|
368
|
-
|
|
582
|
+
if (trimmedRow.length > 0) {
|
|
583
|
+
trimmedRowIndex = rowIndex;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (isFirstWord) {
|
|
588
|
+
isFirstWord = false;
|
|
589
|
+
} else {
|
|
369
590
|
if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
|
|
370
591
|
// If we start with a new word but the current row length equals the length of the columns, add a new row
|
|
371
592
|
rows.push('');
|
|
@@ -379,88 +600,61 @@ const exec = (string, columns, options = {}) => {
|
|
|
379
600
|
}
|
|
380
601
|
|
|
381
602
|
// In 'hard' wrap mode, the length of a line is never allowed to extend past 'columns'
|
|
382
|
-
if (options.hard && options.wordWrap !== false &&
|
|
603
|
+
if (options.hard && options.wordWrap !== false && word.width > columns) {
|
|
383
604
|
const remainingColumns = columns - rowLength;
|
|
384
|
-
const breaksStartingThisLine = 1 + Math.floor((
|
|
385
|
-
const breaksStartingNextLine = Math.floor((
|
|
605
|
+
const breaksStartingThisLine = 1 + Math.floor((word.width - remainingColumns - 1) / columns);
|
|
606
|
+
const breaksStartingNextLine = Math.floor((word.width - 1) / columns);
|
|
386
607
|
if (breaksStartingNextLine < breaksStartingThisLine) {
|
|
387
608
|
rows.push('');
|
|
609
|
+
rowLength = 0;
|
|
388
610
|
}
|
|
389
611
|
|
|
390
|
-
wrapWord(rows, word, columns);
|
|
612
|
+
rowLength = wrapWord(rows, word.value, columns, rowLength);
|
|
391
613
|
continue;
|
|
392
614
|
}
|
|
393
615
|
|
|
394
|
-
if (rowLength +
|
|
616
|
+
if (rowLength + word.width > columns && rowLength > 0 && word.width > 0) {
|
|
395
617
|
if (options.wordWrap === false && rowLength < columns) {
|
|
396
|
-
wrapWord(rows, word, columns);
|
|
618
|
+
rowLength = wrapWord(rows, word.value, columns, rowLength);
|
|
397
619
|
continue;
|
|
398
620
|
}
|
|
399
621
|
|
|
400
622
|
rows.push('');
|
|
623
|
+
rowLength = 0;
|
|
401
624
|
}
|
|
402
625
|
|
|
403
|
-
if (rowLength +
|
|
404
|
-
wrapWord(rows, word, columns);
|
|
626
|
+
if (rowLength + word.width > columns && options.wordWrap === false) {
|
|
627
|
+
rowLength = wrapWord(rows, word.value, columns, rowLength);
|
|
405
628
|
continue;
|
|
406
629
|
}
|
|
407
630
|
|
|
408
|
-
rows[rows.length - 1] += word;
|
|
631
|
+
rows[rows.length - 1] += word.value;
|
|
632
|
+
rowLength += word.width;
|
|
409
633
|
}
|
|
410
634
|
|
|
411
635
|
if (options.trim !== false) {
|
|
412
636
|
rows = rows.map(row => stringVisibleTrimSpacesRight(row));
|
|
413
637
|
}
|
|
414
638
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
// We need to keep a separate index as `String#slice()` works on Unicode code units, while `pre` is an array of grapheme clusters.
|
|
419
|
-
let preStringIndex = 0;
|
|
420
|
-
|
|
421
|
-
for (const [index, character] of pre.entries()) {
|
|
422
|
-
returnValue += character;
|
|
423
|
-
|
|
424
|
-
if (character === ANSI_ESCAPE && pre[index + 1] !== '\\') {
|
|
425
|
-
const {groups} = ANSI_ESCAPE_REGEX.exec(preString.slice(preStringIndex)) || {groups: {}};
|
|
426
|
-
if (groups.sgr !== undefined) {
|
|
427
|
-
applySgrParameters(groups.sgr, activeStyles);
|
|
428
|
-
} else if (groups.uri !== undefined) {
|
|
429
|
-
escapeUrl = groups.uri.length === 0 ? undefined : groups.uri;
|
|
430
|
-
}
|
|
431
|
-
} else if (character === ANSI_ESCAPE_CSI) {
|
|
432
|
-
const {groups} = ANSI_ESCAPE_CSI_REGEX.exec(preString.slice(preStringIndex)) || {groups: {}};
|
|
433
|
-
if (groups.sgr !== undefined) {
|
|
434
|
-
applySgrParameters(groups.sgr, activeStyles);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (pre[index + 1] === '\n') {
|
|
439
|
-
if (escapeUrl) {
|
|
440
|
-
returnValue += wrapAnsiHyperlink('');
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
returnValue += getClosingSgrSequence(activeStyles);
|
|
444
|
-
} else if (character === '\n') {
|
|
445
|
-
const openingStyles = [...activeStyles];
|
|
446
|
-
applyLeadingSgrResets(preString.slice(preStringIndex + 1), openingStyles);
|
|
447
|
-
returnValue += getOpeningSgrSequence(openingStyles);
|
|
639
|
+
return restoreStylesAcrossRows(rows.join('\n'));
|
|
640
|
+
};
|
|
448
641
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
642
|
+
// Normalize the text, but not the sequences: a combining mark that follows a sequence composes with its last character and destroys it.
|
|
643
|
+
const normalizeText = string => {
|
|
644
|
+
let normalizedString = '';
|
|
453
645
|
|
|
454
|
-
|
|
455
|
-
|
|
646
|
+
forEachSegment(string, plainText => {
|
|
647
|
+
normalizedString += plainText.normalize();
|
|
648
|
+
}, escape => {
|
|
649
|
+
normalizedString += escape;
|
|
650
|
+
});
|
|
456
651
|
|
|
457
|
-
return
|
|
652
|
+
return normalizedString;
|
|
458
653
|
};
|
|
459
654
|
|
|
460
|
-
// For each newline, invoke the method separately
|
|
655
|
+
// For each newline, invoke the method separately.
|
|
461
656
|
export default function wrapAnsi(string, columns, options) {
|
|
462
|
-
return String(string)
|
|
463
|
-
.normalize()
|
|
657
|
+
return normalizeText(String(string))
|
|
464
658
|
.replaceAll('\r\n', '\n')
|
|
465
659
|
.split('\n')
|
|
466
660
|
.map(line => exec(expandTabs(line), columns, options))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@depup/wrap-ansi",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.2-depup.0",
|
|
4
4
|
"description": "Wordwrap a string with ANSI escape codes (with updated dependencies)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "chalk/wrap-ansi",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"node": ">=20"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test": "xo &&
|
|
22
|
+
"test": "xo && node --test && tsd"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"index.js",
|
|
@@ -61,30 +61,31 @@
|
|
|
61
61
|
"text"
|
|
62
62
|
],
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"ansi-styles": "^
|
|
65
|
-
"string-width": "^8.
|
|
66
|
-
"strip-ansi": "^7.2.0"
|
|
64
|
+
"ansi-styles": "^7.0.0",
|
|
65
|
+
"string-width": "^8.3.0"
|
|
67
66
|
},
|
|
68
67
|
"devDependencies": {
|
|
69
|
-
"ava": "^6.4.1",
|
|
70
68
|
"chalk": "^5.6.2",
|
|
71
|
-
"coveralls": "^3.1.1",
|
|
72
69
|
"has-ansi": "^6.0.2",
|
|
73
|
-
"
|
|
70
|
+
"strip-ansi": "^7.1.2",
|
|
74
71
|
"tsd": "^0.33.0",
|
|
75
72
|
"xo": "^1.2.3"
|
|
76
73
|
},
|
|
77
74
|
"depup": {
|
|
78
75
|
"changes": {
|
|
79
|
-
"
|
|
80
|
-
"from": "^
|
|
81
|
-
"to": "^7.
|
|
76
|
+
"ansi-styles": {
|
|
77
|
+
"from": "^6.2.3",
|
|
78
|
+
"to": "^7.0.0"
|
|
79
|
+
},
|
|
80
|
+
"string-width": {
|
|
81
|
+
"from": "^8.2.0",
|
|
82
|
+
"to": "^8.3.0"
|
|
82
83
|
}
|
|
83
84
|
},
|
|
84
|
-
"depsUpdated":
|
|
85
|
+
"depsUpdated": 2,
|
|
85
86
|
"originalPackage": "wrap-ansi",
|
|
86
|
-
"originalVersion": "10.0.
|
|
87
|
-
"processedAt": "2026-
|
|
87
|
+
"originalVersion": "10.0.2",
|
|
88
|
+
"processedAt": "2026-09-27T01:05:14.220Z",
|
|
88
89
|
"smokeTest": "passed"
|
|
89
90
|
}
|
|
90
91
|
}
|