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
145 changes: 80 additions & 65 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,84 +163,57 @@ 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) {
Comment on lines -124 to +166
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why getting rid of the default values here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process.stdout doesn't need to be evaluated if validateStream is false. Just moved it to the runtime opts check.

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);
}

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

const codes = [];
let openCodes = '';
let closeCodes = '';
let processedText = text;

for (const key of formatArray) {
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;
openCodes += style.openSeq;
closeCodes = style.closeSeq + closeCodes;
processedText = replaceCloseCode(processedText, style.closeSeq, style.openSeq, style.keepClose);
}

// 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]);
}
if (skipColorize) return text;

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 @@ -22,12 +22,12 @@ const noChange = 'test';
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