Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,11 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/59710
description: The util.inspect.styles.regexp style is now a method that is
invoked for coloring the stringified regular expression.
- version:
- v17.3.0
- v16.14.0
Expand Down Expand Up @@ -1295,7 +1300,12 @@ The default styles and associated colors are:
* `name`: (no styling)
* `null`: `bold`
* `number`: `yellow`
* `regexp`: `red`
* `regexp`: A method that colors character classes, groups, assertions, and
other parts for improved readability. To customize the coloring, change the
`colors` property. It is set to
`['red', 'green', 'yellow', 'cyan', 'magenta']` by default and may be
adjusted as needed. The array is repetitively iterated through depending on
the "depth".
* `special`: `cyan` (e.g., `Proxies`)
* `string`: `green`
* `symbol`: `green`
Expand All @@ -1307,6 +1317,17 @@ terminals. To verify color support use [`tty.hasColors()`][].
Predefined control codes are listed below (grouped as "Modifiers", "Foreground
colors", and "Background colors").

#### Complex custom coloring

It is possible to define a method as style. It receives the stringified value
of the input. It is invoked in case coloring is active and the type is
inspected.

Example: `util.inspect.styles.regexp(value)`

* `value` {string} The string representation of the input type.
* Returns: {string} The adjusted representation of `object`.

#### Modifiers

Modifier support varies throughout different terminals. They will mostly be
Expand Down
262 changes: 257 additions & 5 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,6 @@ defineColorAlias('inverse', 'swapColors');
defineColorAlias('inverse', 'swapcolors');
defineColorAlias('doubleunderline', 'doubleUnderline');

// TODO(BridgeAR): Add function style support for more complex styles.
// Don't use 'blue' not visible on cmd.exe
inspect.styles = ObjectAssign({ __proto__: null }, {
special: 'cyan',
Expand All @@ -510,11 +509,259 @@ inspect.styles = ObjectAssign({ __proto__: null }, {
symbol: 'green',
date: 'magenta',
// "name": intentionally not styling
// TODO(BridgeAR): Highlight regular expressions properly.
regexp: 'red',
regexp: highlightRegExp,
module: 'underline',
});

// Define the palette for RegExp group depth highlighting. Can be changed by users.
inspect.styles.regexp.colors = ['green', 'red', 'yellow', 'cyan', 'magenta'];

const highlightRegExpColors = inspect.styles.regexp.colors.slice();

/**
* Colorize a JavaScript RegExp pattern per ECMAScript grammar.
* This is a tolerant single-pass highlighter using heuristics in some cases.
* It supports: groups (named/unnamed, lookaround), assertions, alternation,
* quantifiers, escapes (incl. Unicode properties), character classes and
* backreferences.
* @param {string} regexpString
* @returns {string}
*/
function highlightRegExp(regexpString) {
let out = '';
let i = 0;
let depth = 0;
let inClass = false;

// TODO(BridgeAR): Add group type tracking. That allows to increase the depth
// in case the same type is next to each other.
// let groupType = 0;

// Verify palette and update cache if user changed colors
const paletteNames = highlightRegExp.colors?.length > 0 ?
highlightRegExp.colors :
highlightRegExpColors;

const palette = paletteNames.reduce((acc, name) => {
const color = inspect.colors[name];
if (color) acc.push([`\u001b[${color[0]}m`, `\u001b[${color[1]}m`]);
return acc;
}, []);

function writeGroup(start, end, decreaseDepth = 1) {
let seq = '';
i++;
// Only checking for the closing delimiter is a fast heuristic for regular
// expressions without the u or v flag. A safer check would verify that the
// read characters are all alphanumeric.
while (i < regexpString.length && regexpString[i] !== end) {
seq += regexpString[i++];
}
if (i < regexpString.length) {
depth -= decreaseDepth;
write(start);
writeDepth(seq, 1, 1);
write(end);
depth += decreaseDepth;
} else {
// The group is not closed which would lead to mistakes in the output.
// This is a workaround to prevent output from being corrupted.
writeDepth(start, 1, -seq.length);
}
}

const write = (str) => {
const idx = depth % palette.length;
// Safeguard against bugs in the implementation.
const color = palette[idx] ?? palette[0];
out += color[0] + str + color[1];
return idx;
};

function writeDepth(str, incDepth, incI) {
depth += incDepth;
write(str);
depth -= incDepth;
i += incI;
}

// Opening '/'
write('/');
depth++;
i = 1;

// Parse pattern until next unescaped '/'
while (i < regexpString.length) {
const ch = regexpString[i];

if (inClass) {
if (ch === '\\') {
let seq = '\\';
i++;
if (i < regexpString.length) {
seq += regexpString[i++];
const next = seq[1];
if (next === 'u' && regexpString[i] === '{') {
writeGroup(`${seq}{`, '}', 0);
continue;
} else if ((next === 'p' || next === 'P') && regexpString[i] === '{') {
writeGroup(`${seq}{`, '}', 0);
continue;
} else if (seq[1] === 'x') {
seq += regexpString.slice(i, i + 2);
i += 2;
}
}
write(seq);
} else if (ch === ']') {
depth--;
write(']');
i++;
inClass = false;
} else if (ch === '-' && regexpString[i - 1] !== '[' &&
i + 1 < regexpString.length && regexpString[i + 1] !== ']') {
writeDepth('-', 1, 1);
} else {
write(ch);
i++;
}
} else if (ch === '[') {
// Enter class
write('[');
depth++;
i++;
inClass = true;
} else if (ch === '(') {
write('(');
depth++;
i++;
if (i < regexpString.length && regexpString[i] === '?') {
// Assertions and named groups
i++;
const a = i < regexpString.length ? regexpString[i] : '';
if (a === ':' || a === '=' || a === '!') {
writeDepth(`?${a}`, -1, 1);
} else {
const b = i + 1 < regexpString.length ? regexpString[i + 1] : '';
if (a === '<' && (b === '=' || b === '!')) {
writeDepth(`?<${b}`, -1, 2);
} else if (a === '<') {
// Named capture: write '?<name>' as a single colored token
i++; // consume '<'
const start = i;
while (i < regexpString.length && regexpString[i] !== '>') {
i++;
}
const name = regexpString.slice(start, i);
if (i < regexpString.length && regexpString[i] === '>') {
depth--;
write('?<');
writeDepth(name, 1, 0);
write('>');
depth++;
i++;
} else {
writeDepth('?<', -1, 0);
write(name);
}
} else {
write('?');
}
}
}
} else if (ch === ')') {
depth--;
write(')');
i++;
} else if (ch === '\\') {
let seq = '\\';
i++;
if (i < regexpString.length) {
seq += regexpString[i++];
const next = seq[1];
if (i < regexpString.length) {
if (next === 'u' && regexpString[i] === '{') {
writeGroup(`${seq}{`, '}', 0);
continue;
} else if (next === 'x') {
seq += regexpString.slice(i, i + 2);
i += 2;
} else if (next >= '0' && next <= '9') {
while (i < regexpString.length && regexpString[i] >= '0' && regexpString[i] <= '9') {
seq += regexpString[i++];
}
} else if (next === 'k' && regexpString[i] === '<') {
writeGroup(`${seq}<`, '>');
continue;
} else if ((next === 'p' || next === 'P') && regexpString[i] === '{') {
// Unicode properties
writeGroup(`${seq}{`, '}', 0);
continue;
}
}
}
writeDepth(seq, 1, 0);
} else if (ch === '|' || ch === '+' || ch === '*' || ch === '?' || ch === ',' || ch === '^' || ch === '$') {
writeDepth(ch, 3, 1);
} else if (ch === '{') {
i++;
let digits = '';
while (i < regexpString.length && regexpString[i] >= '0' && regexpString[i] <= '9') {
digits += regexpString[i++];
}
if (digits) {
write('{');
depth++;
writeDepth(digits, 1, 0);
}
if (i < regexpString.length) {
if (regexpString[i] === ',') {
if (!digits) {
write('{');
depth++;
}
write(',');
i++;
} else if (!digits) {
depth += 1;
write('{');
depth -= 1;
continue;
}
}
let digits2 = '';
while (i < regexpString.length && regexpString[i] >= '0' && regexpString[i] <= '9') {
digits2 += regexpString[i++];
}
if (digits2) {
writeDepth(digits2, 1, 0);
}
if (i < regexpString.length && regexpString[i] === '}') {
depth--;
write('}');
i++;
}
if (i < regexpString.length && regexpString[i] === '?') {
writeDepth('?', 3, 1);
}
} else if (ch === '.') {
writeDepth(ch, 2, 1);
} else if (ch === '/') {
// Stop at closing delimiter (unescaped, outside of character class)
break;
} else {
writeDepth(ch, 1, 1);
}
}

// Closing delimiter and flags
writeDepth('/', -1, 1);
if (i < regexpString.length) {
write(regexpString.slice(i));
}
return out;
}

function addQuotes(str, quotes) {
if (quotes === -1) {
return `"${str}"`;
Expand Down Expand Up @@ -601,8 +848,12 @@ function stylizeWithColor(str, styleType) {
const style = inspect.styles[styleType];
if (style !== undefined) {
const color = inspect.colors[style];
if (color !== undefined)
if (color !== undefined) {
return `\u001b[${color[0]}m${str}\u001b[${color[1]}m`;
}
if (typeof style === 'function') {
return style(str);
}
}
return str;
}
Expand Down Expand Up @@ -1038,9 +1289,10 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
const prefix = getPrefix(constructor, tag, 'RegExp');
if (prefix !== 'RegExp ')
base = `${prefix}${base}`;
base = ctx.stylize(base, 'regexp');
if ((keys.length === 0 && protoProps === undefined) ||
(recurseTimes > ctx.depth && ctx.depth !== null)) {
return ctx.stylize(base, 'regexp');
return base;
}
} else if (isDate(value)) {
// Make dates with properties first say the date
Expand Down
Loading
Loading