Skip to content
Open
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
155 changes: 85 additions & 70 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const {
ArrayIsArray,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeReduce,
Error,
ErrorCaptureStackTrace,
FunctionPrototypeBind,
Expand All @@ -37,8 +36,6 @@ const {
ObjectSetPrototypeOf,
ObjectValues,
ReflectApply,
RegExp,
RegExpPrototypeSymbolReplace,
StringPrototypeToWellFormed,
} = primordials;

Expand Down Expand Up @@ -104,13 +101,58 @@ function lazyAbortController() {

let internalDeepEqual;

/**
* @param {string} [code]
* @returns {string}
*/
function escapeStyleCode(code) {
if (code === undefined) return '';
return `\u001b[${code}m`;
// Pre-computed ANSI escape code constants
const kEscape = '\u001b[';
const kEscapeEnd = 'm';

// Codes for dim (2) and bold (1) - these share close code 22
const kDimCode = 2;
const kBoldCode = 1;

let styleCache;

function getStyleCache() {
if (styleCache === undefined) {
styleCache = { __proto__: null };
const colors = inspect.colors;
for (const key of ObjectKeys(colors)) {
const codes = colors[key];
if (codes) {
const openNum = codes[0];
const closeNum = codes[1];
styleCache[key] = {
__proto__: null,
openSeq: kEscape + openNum + kEscapeEnd,
closeSeq: kEscape + closeNum + kEscapeEnd,
keepClose: openNum === kDimCode || openNum === kBoldCode,
};
}
}
}
return styleCache;
}

function replaceCloseCode(str, closeSeq, openSeq, keepClose) {
const closeLen = closeSeq.length;
let index = str.indexOf(closeSeq);
if (index === -1) return str;

let result = '';
let lastIndex = 0;
const replacement = keepClose ? closeSeq + openSeq : openSeq;

do {
const afterClose = index + closeLen;
if (afterClose < str.length) {
result += str.slice(lastIndex, index) + replacement;
lastIndex = afterClose;
} else {
break;
}
index = str.indexOf(closeSeq, lastIndex);
} while (index !== -1);

return result + str.slice(lastIndex);
}

/**
Expand All @@ -121,83 +163,56 @@ function escapeStyleCode(code) {
* @param {Stream} [options.stream] - The stream used for validation.
* @returns {string}
*/
function styleText(format, text, { validateStream = true, stream = process.stdout } = {}) {
function styleText(format, text, options) {
const validateStream = options?.validateStream ?? true;
const cache = getStyleCache();

// Fast path: single format string with validateStream=false
if (!validateStream && typeof format === 'string' && typeof text === 'string') {
if (format === 'none') return text;
const style = cache[format];
if (style !== undefined) {
const processed = replaceCloseCode(text, style.closeSeq, style.openSeq, style.keepClose);
return style.openSeq + processed + style.closeSeq;
}
}

validateString(text, 'text');
if (options !== undefined) {
validateObject(options, 'options');
}
validateBoolean(validateStream, 'options.validateStream');

let skipColorize;
if (validateStream) {
const stream = options?.stream ?? process.stdout;
if (
!isReadableStream(stream) &&
!isWritableStream(stream) &&
!isNodeStream(stream)
) {
throw new ERR_INVALID_ARG_TYPE('stream', ['ReadableStream', 'WritableStream', 'Stream'], stream);
}

// If the stream is falsy or should not be colorized, set skipColorize to true
skipColorize = !lazyUtilColors().shouldColorize(stream);
const skipColorize = !lazyUtilColors().shouldColorize(stream);
if (skipColorize) return text;
}

// If the format is not an array, convert it to an array
const formatArray = ArrayIsArray(format) ? format : [format];
const isArray = ArrayIsArray(format);
const formatArray = isArray ? format : [format];

let openCodes = '';
let closeCodes = '';
let processedText = text;

const codes = [];
for (const key of formatArray) {
for (let i = 0; i < formatArray.length; i++) {
const key = formatArray[i];
if (key === 'none') continue;
const formatCodes = inspect.colors[key];
// If the format is not a valid style, throw an error
if (formatCodes == null) {
const style = cache[key];
if (style === undefined) {
validateOneOf(key, 'format', ObjectKeys(inspect.colors));
}
if (skipColorize) continue;
ArrayPrototypePush(codes, formatCodes);
}

if (skipColorize) {
return text;
}

// Build opening codes
let openCodes = '';
for (let i = 0; i < codes.length; i++) {
openCodes += escapeStyleCode(codes[i][0]);
}

// Process the text to handle nested styles
let processedText;
if (codes.length > 0) {
processedText = ArrayPrototypeReduce(
codes,
(text, code) => RegExpPrototypeSymbolReplace(
// Find the reset code
new RegExp(`\\u001b\\[${code[1]}m`, 'g'),
text,
(match, offset) => {
// Check if there's more content after this reset
if (offset + match.length < text.length) {
if (
code[0] === inspect.colors.dim[0] ||
code[0] === inspect.colors.bold[0]
) {
// Dim and bold are not mutually exclusive, so we need to reapply
return `${match}${escapeStyleCode(code[0])}`;
}
return escapeStyleCode(code[0]);
}
return match;
},
),
text,
);
} else {
processedText = text;
}

// Build closing codes in reverse order
let closeCodes = '';
for (let i = codes.length - 1; i >= 0; i--) {
closeCodes += escapeStyleCode(codes[i][1]);
openCodes += style.openSeq;
closeCodes = style.closeSeq + closeCodes;
processedText = replaceCloseCode(processedText, style.closeSeq, style.openSeq, style.keepClose);
}

return `${openCodes}${processedText}${closeCodes}`;
Expand Down
4 changes: 2 additions & 2 deletions test/parallel/test-util-styletext.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
Symbol(),
() => {},
{},
].forEach((invalidOption) => {

Check failure on line 20 in test/parallel/test-util-styletext.js

View workflow job for this annotation

GitHub Actions / test-linux (ubuntu-24.04-arm)

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Missing expected exception. at /home/runner/work/node/node/node/test/parallel/test-util-styletext.js:21:10 at Array.forEach (<anonymous>) at Object.<anonymous> (/home/runner/work/node/node/node/test/parallel/test-util-styletext.js:20:3) at Module._compile (node:internal/modules/cjs/loader:1811:14) at Object..js (node:internal/modules/cjs/loader:1942:10) at Module.load (node:internal/modules/cjs/loader:1532:32) at Module._load (node:internal/modules/cjs/loader:1334:12) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: { code: 'ERR_INVALID_ARG_VALUE' }, operator: 'throws', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /home/runner/work/node/node/node/test/parallel/test-util-styletext.js

Check failure on line 20 in test/parallel/test-util-styletext.js

View workflow job for this annotation

GitHub Actions / test-linux (ubuntu-24.04)

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Missing expected exception. at /home/runner/work/node/node/node/test/parallel/test-util-styletext.js:21:10 at Array.forEach (<anonymous>) at Object.<anonymous> (/home/runner/work/node/node/node/test/parallel/test-util-styletext.js:20:3) at Module._compile (node:internal/modules/cjs/loader:1811:14) at Object..js (node:internal/modules/cjs/loader:1942:10) at Module.load (node:internal/modules/cjs/loader:1532:32) at Module._load (node:internal/modules/cjs/loader:1334:12) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: { code: 'ERR_INVALID_ARG_VALUE' }, operator: 'throws', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /home/runner/work/node/node/node/test/parallel/test-util-styletext.js

Check failure on line 20 in test/parallel/test-util-styletext.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Missing expected exception. at /Users/runner/work/node/node/node/test/parallel/test-util-styletext.js:21:10 at Array.forEach (<anonymous>) at Object.<anonymous> (/Users/runner/work/node/node/node/test/parallel/test-util-styletext.js:20:3) at Module._compile (node:internal/modules/cjs/loader:1811:14) at Object..js (node:internal/modules/cjs/loader:1942:10) at Module.load (node:internal/modules/cjs/loader:1532:32) at Module._load (node:internal/modules/cjs/loader:1334:12) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: { code: 'ERR_INVALID_ARG_VALUE' }, operator: 'throws', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /Users/runner/work/node/node/node/test/parallel/test-util-styletext.js

Check failure on line 20 in test/parallel/test-util-styletext.js

View workflow job for this annotation

GitHub Actions / x86_64-darwin: with shared libraries

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Missing expected exception. at /Users/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js:21:10 at Array.forEach (<anonymous>) at Object.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js:20:3) at Module._compile (node:internal/modules/cjs/loader:1811:14) at Object..js (node:internal/modules/cjs/loader:1942:10) at Module.load (node:internal/modules/cjs/loader:1532:32) at Module._load (node:internal/modules/cjs/loader:1334:12) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: { code: 'ERR_INVALID_ARG_VALUE' }, operator: 'throws', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /Users/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js

Check failure on line 20 in test/parallel/test-util-styletext.js

View workflow job for this annotation

GitHub Actions / aarch64-darwin: with shared libraries

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Missing expected exception. at /Users/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js:21:10 at Array.forEach (<anonymous>) at Object.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js:20:3) at Module._compile (node:internal/modules/cjs/loader:1811:14) at Object..js (node:internal/modules/cjs/loader:1942:10) at Module.load (node:internal/modules/cjs/loader:1532:32) at Module._load (node:internal/modules/cjs/loader:1334:12) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: { code: 'ERR_INVALID_ARG_VALUE' }, operator: 'throws', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /Users/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js

Check failure on line 20 in test/parallel/test-util-styletext.js

View workflow job for this annotation

GitHub Actions / aarch64-linux: with shared libraries

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Missing expected exception. at /home/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js:21:10 at Array.forEach (<anonymous>) at Object.<anonymous> (/home/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js:20:3) at Module._compile (node:internal/modules/cjs/loader:1811:14) at Object..js (node:internal/modules/cjs/loader:1942:10) at Module.load (node:internal/modules/cjs/loader:1532:32) at Module._load (node:internal/modules/cjs/loader:1334:12) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: { code: 'ERR_INVALID_ARG_VALUE' }, operator: 'throws', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /home/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js

Check failure on line 20 in test/parallel/test-util-styletext.js

View workflow job for this annotation

GitHub Actions / x86_64-linux: with shared libraries

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Missing expected exception. at /home/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js:21:10 at Array.forEach (<anonymous>) at Object.<anonymous> (/home/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js:20:3) at Module._compile (node:internal/modules/cjs/loader:1811:14) at Object..js (node:internal/modules/cjs/loader:1942:10) at Module.load (node:internal/modules/cjs/loader:1532:32) at Module._load (node:internal/modules/cjs/loader:1334:12) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: { code: 'ERR_INVALID_ARG_VALUE' }, operator: 'throws', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /home/runner/work/_temp/node-v26.0.0-nightly2026-02-12307a094dea-slim/test/parallel/test-util-styletext.js
assert.throws(() => {
util.styleText(invalidOption, 'test');
}, {
code: 'ERR_INVALID_ARG_VALUE',
});
}, invalidOption);
assert.throws(() => {
util.styleText('red', invalidOption);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
}, invalidOption);
});

assert.throws(() => {
Expand Down
Loading