diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 193ae8524632..df83ea736450 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -286,12 +286,24 @@ const meta = [ // License: MIT by Sindre Sorhus // Matches all ansi escape code sequences in a string const ansi = new RegExp( + // Sequences that run until a string terminator, such as OSC 8 hyperlinks. '[\\u001B\\u009B][[\\]()#;?]*' + - '(?:(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*' + + '(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*' + '|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]*)*)?' + '(?:\\u0007|\\u001B\\u005C|\\u009C))' + - '|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?' + - '[\\dA-PR-TZcf-nq-uy=><~]))', 'g', + // Control sequences, using the structure ECMA-48 5.4 defines: any number + // of parameter bytes (0x30-0x3F), any number of intermediate bytes + // (0x20-0x2F), then a single final byte (0x40-0x7E). The list of final + // bytes below covers neither parameter bytes such as `<`, used by mouse + // reports, nor final bytes such as `@`, so it stops in the middle of the + // sequence and leaves the rest in the output. + '|(?:\\u001B\\[|\\u009B)[[\\]()#]*' + + '[\\u0030-\\u003F]*[\\u0020-\\u002F]*[\\u0040-\\u007E]' + + // Everything else the reference implementation matched, including + // sequences whose last byte is a digit, such as `ESC 7`. + '|[\\u001B\\u009B][[\\]()#;?]*' + + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?' + + '[\\dA-PR-TZcf-nq-uy=><~])', 'g', ); let getStringWidth; diff --git a/test/parallel/test-util-stripvtcontrolcharacters.js b/test/parallel/test-util-stripvtcontrolcharacters.js index a33d18d26dbc..853cda998ddb 100644 --- a/test/parallel/test-util-stripvtcontrolcharacters.js +++ b/test/parallel/test-util-stripvtcontrolcharacters.js @@ -21,6 +21,30 @@ for (const ST of ['\u0007', '\u001B\u005C', '\u009C']) { ); } +// Ref: ECMA-48 5.4. A control sequence is CSI, then any number of parameter +// bytes (0x30-0x3F), then any number of intermediate bytes (0x20-0x2F), then a +// single final byte (0x40-0x7E). Every sequence below is well formed and in use +// by terminals today, and every one of them used to have its tail left behind. +tests.push( + // Parameter bytes other than digits and `;`. + ['a\u001B[<35;10;20Mb', 'ab'], // SGR mouse report + ['a\u001B[>1;2cb', 'ab'], // Secondary device attributes + ['a\u001B[=5hb', 'ab'], + ['a\u009B<35;10;20Mb', 'ab'], // Same, with the 8 bit CSI introducer + // Final bytes that the reference implementation does not list. + ['a\u001B[3@b', 'ab'], // ICH, insert character + ['a\u001B[3Xb', 'ab'], // ECH, erase character + ['a\u001B[5db', 'ab'], // VPA, line position absolute + ['a\u001B[3bb', 'ab'], // REP, repeat preceding character + ['a\u001B[2ab', 'ab'], // HPR, character position forward + // Sub parameters are separated by `:`, which is a parameter byte as well. + ['a\u001B[38:2:255:0:0mb', 'ab'], // Truecolour in its colon form + ['a\u001B[4:3mb', 'ab'], // Curly underline + // Intermediate bytes. + ['a\u001B[2 qb', 'ab'], // DECSCUSR, set cursor style + ['a\u001B[!pb', 'ab'], // DECSTR, soft terminal reset +); + test('util.stripVTControlCharacters', (t) => { for (const [before, expected] of tests) { t.assert.strictEqual(util.stripVTControlCharacters(before), expected);