Skip to content

Commit 6bb2f8e

Browse files
committed
util: strip whole CSI sequences per ECMA-48
stripVTControlCharacters() recognises a control sequence by a hand written list of final bytes, inherited from the bundled copy of ansi-regex. The list covers neither parameter bytes such as `<` and `:`, used by mouse reports and by sub parameters, nor final bytes such as `@`, `X`, `d`, `a` and `b`. A sequence that uses one of them matches only in part, so the rest of it is left in the string rather than removed. getStringWidth() strips before measuring and readline places the cursor from that width, so the leftovers are counted as printable columns. Add an alternative built from the control sequence structure in ECMA-48 5.4: any number of parameter bytes (0x30-0x3F), any number of intermediate bytes (0x20-0x2F), then a single final byte (0x40-0x7E). Both existing alternatives are kept and the new one is only reached where they do not match, so a match can be extended but never shortened. The new alternative carries its own introducer instead of sharing the existing prefix, because the shared prefix also consumes `;` and the two competing for the same run made the match quadratic. Signed-off-by: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com>
1 parent e2b33e2 commit 6bb2f8e

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

‎lib/internal/util/inspect.js‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,24 @@ const meta = [
286286
// License: MIT by Sindre Sorhus <sindresorhus@gmail.com>
287287
// Matches all ansi escape code sequences in a string
288288
const ansi = new RegExp(
289+
// Sequences that run until a string terminator, such as OSC 8 hyperlinks.
289290
'[\\u001B\\u009B][[\\]()#;?]*' +
290-
'(?:(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*' +
291+
'(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*' +
291292
'|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]*)*)?' +
292293
'(?:\\u0007|\\u001B\\u005C|\\u009C))' +
293-
'|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?' +
294-
'[\\dA-PR-TZcf-nq-uy=><~]))', 'g',
294+
// Control sequences, using the structure ECMA-48 5.4 defines: any number
295+
// of parameter bytes (0x30-0x3F), any number of intermediate bytes
296+
// (0x20-0x2F), then a single final byte (0x40-0x7E). The list of final
297+
// bytes below covers neither parameter bytes such as `<`, used by mouse
298+
// reports, nor final bytes such as `@`, so it stops in the middle of the
299+
// sequence and leaves the rest in the output.
300+
'|(?:\\u001B\\[|\\u009B)[[\\]()#]*' +
301+
'[\\u0030-\\u003F]*[\\u0020-\\u002F]*[\\u0040-\\u007E]' +
302+
// Everything else the reference implementation matched, including
303+
// sequences whose last byte is a digit, such as `ESC 7`.
304+
'|[\\u001B\\u009B][[\\]()#;?]*' +
305+
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?' +
306+
'[\\dA-PR-TZcf-nq-uy=><~])', 'g',
295307
);
296308

297309
let getStringWidth;

‎test/parallel/test-util-stripvtcontrolcharacters.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ for (const ST of ['\u0007', '\u001B\u005C', '\u009C']) {
2121
);
2222
}
2323

24+
// Ref: ECMA-48 5.4. A control sequence is CSI, then any number of parameter
25+
// bytes (0x30-0x3F), then any number of intermediate bytes (0x20-0x2F), then a
26+
// single final byte (0x40-0x7E). Every sequence below is well formed and in use
27+
// by terminals today, and every one of them used to have its tail left behind.
28+
tests.push(
29+
// Parameter bytes other than digits and `;`.
30+
['a\u001B[<35;10;20Mb', 'ab'], // SGR mouse report
31+
['a\u001B[>1;2cb', 'ab'], // Secondary device attributes
32+
['a\u001B[=5hb', 'ab'],
33+
['a\u009B<35;10;20Mb', 'ab'], // Same, with the 8 bit CSI introducer
34+
// Final bytes that the reference implementation does not list.
35+
['a\u001B[3@b', 'ab'], // ICH, insert character
36+
['a\u001B[3Xb', 'ab'], // ECH, erase character
37+
['a\u001B[5db', 'ab'], // VPA, line position absolute
38+
['a\u001B[3bb', 'ab'], // REP, repeat preceding character
39+
['a\u001B[2ab', 'ab'], // HPR, character position forward
40+
// Sub parameters are separated by `:`, which is a parameter byte as well.
41+
['a\u001B[38:2:255:0:0mb', 'ab'], // Truecolour in its colon form
42+
['a\u001B[4:3mb', 'ab'], // Curly underline
43+
// Intermediate bytes.
44+
['a\u001B[2 qb', 'ab'], // DECSCUSR, set cursor style
45+
['a\u001B[!pb', 'ab'], // DECSTR, soft terminal reset
46+
);
47+
2448
test('util.stripVTControlCharacters', (t) => {
2549
for (const [before, expected] of tests) {
2650
t.assert.strictEqual(util.stripVTControlCharacters(before), expected);

0 commit comments

Comments
 (0)