Skip to content

Commit b1c1ddf

Browse files
RafaelGSSbrunocroh
authored andcommitted
lib: optimize styleText when validateStream is false
This commit optimizes the util.styleText when validateStream is false Co-Authored-By: Bruno Rodrigues <swe@brunocroh.com> Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> PR-URL: #61792 Reviewed-By: Santiago Gimeno <santiago.gimeno@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent 2c94967 commit b1c1ddf

File tree

2 files changed

+82
-67
lines changed

2 files changed

+82
-67
lines changed

lib/util.js

Lines changed: 80 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ const {
2525
ArrayIsArray,
2626
ArrayPrototypePop,
2727
ArrayPrototypePush,
28-
ArrayPrototypeReduce,
2928
Error,
3029
ErrorCaptureStackTrace,
3130
FunctionPrototypeBind,
@@ -37,8 +36,6 @@ const {
3736
ObjectSetPrototypeOf,
3837
ObjectValues,
3938
ReflectApply,
40-
RegExp,
41-
RegExpPrototypeSymbolReplace,
4239
StringPrototypeToWellFormed,
4340
} = primordials;
4441

@@ -104,13 +101,58 @@ function lazyAbortController() {
104101

105102
let internalDeepEqual;
106103

107-
/**
108-
* @param {string} [code]
109-
* @returns {string}
110-
*/
111-
function escapeStyleCode(code) {
112-
if (code === undefined) return '';
113-
return `\u001b[${code}m`;
104+
// Pre-computed ANSI escape code constants
105+
const kEscape = '\u001b[';
106+
const kEscapeEnd = 'm';
107+
108+
// Codes for dim (2) and bold (1) - these share close code 22
109+
const kDimCode = 2;
110+
const kBoldCode = 1;
111+
112+
let styleCache;
113+
114+
function getStyleCache() {
115+
if (styleCache === undefined) {
116+
styleCache = { __proto__: null };
117+
const colors = inspect.colors;
118+
for (const key of ObjectKeys(colors)) {
119+
const codes = colors[key];
120+
if (codes) {
121+
const openNum = codes[0];
122+
const closeNum = codes[1];
123+
styleCache[key] = {
124+
__proto__: null,
125+
openSeq: kEscape + openNum + kEscapeEnd,
126+
closeSeq: kEscape + closeNum + kEscapeEnd,
127+
keepClose: openNum === kDimCode || openNum === kBoldCode,
128+
};
129+
}
130+
}
131+
}
132+
return styleCache;
133+
}
134+
135+
function replaceCloseCode(str, closeSeq, openSeq, keepClose) {
136+
const closeLen = closeSeq.length;
137+
let index = str.indexOf(closeSeq);
138+
if (index === -1) return str;
139+
140+
let result = '';
141+
let lastIndex = 0;
142+
const replacement = keepClose ? closeSeq + openSeq : openSeq;
143+
144+
do {
145+
const afterClose = index + closeLen;
146+
if (afterClose < str.length) {
147+
result += str.slice(lastIndex, index) + replacement;
148+
lastIndex = afterClose;
149+
} else {
150+
break;
151+
}
152+
index = str.indexOf(closeSeq, lastIndex);
153+
} while (index !== -1);
154+
155+
return result + str.slice(lastIndex);
114156
}
115157

116158
/**
@@ -121,84 +163,57 @@ function escapeStyleCode(code) {
121163
* @param {Stream} [options.stream] - The stream used for validation.
122164
* @returns {string}
123165
*/
124-
function styleText(format, text, { validateStream = true, stream = process.stdout } = {}) {
166+
function styleText(format, text, options) {
167+
const validateStream = options?.validateStream ?? true;
168+
const cache = getStyleCache();
169+
170+
// Fast path: single format string with validateStream=false
171+
if (!validateStream && typeof format === 'string' && typeof text === 'string') {
172+
if (format === 'none') return text;
173+
const style = cache[format];
174+
if (style !== undefined) {
175+
const processed = replaceCloseCode(text, style.closeSeq, style.openSeq, style.keepClose);
176+
return style.openSeq + processed + style.closeSeq;
177+
}
178+
}
179+
125180
validateString(text, 'text');
181+
if (options !== undefined) {
182+
validateObject(options, 'options');
183+
}
126184
validateBoolean(validateStream, 'options.validateStream');
127185

128186
let skipColorize;
129187
if (validateStream) {
188+
const stream = options?.stream ?? process.stdout;
130189
if (
131190
!isReadableStream(stream) &&
132191
!isWritableStream(stream) &&
133192
!isNodeStream(stream)
134193
) {
135194
throw new ERR_INVALID_ARG_TYPE('stream', ['ReadableStream', 'WritableStream', 'Stream'], stream);
136195
}
137-
138-
// If the stream is falsy or should not be colorized, set skipColorize to true
139196
skipColorize = !lazyUtilColors().shouldColorize(stream);
140197
}
141198

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

145-
const codes = [];
201+
let openCodes = '';
202+
let closeCodes = '';
203+
let processedText = text;
204+
146205
for (const key of formatArray) {
147206
if (key === 'none') continue;
148-
const formatCodes = inspect.colors[key];
149-
// If the format is not a valid style, throw an error
150-
if (formatCodes == null) {
207+
const style = cache[key];
208+
if (style === undefined) {
151209
validateOneOf(key, 'format', ObjectKeys(inspect.colors));
152210
}
153-
if (skipColorize) continue;
154-
ArrayPrototypePush(codes, formatCodes);
155-
}
156-
157-
if (skipColorize) {
158-
return text;
211+
openCodes += style.openSeq;
212+
closeCodes = style.closeSeq + closeCodes;
213+
processedText = replaceCloseCode(processedText, style.closeSeq, style.openSeq, style.keepClose);
159214
}
160215

161-
// Build opening codes
162-
let openCodes = '';
163-
for (let i = 0; i < codes.length; i++) {
164-
openCodes += escapeStyleCode(codes[i][0]);
165-
}
166-
167-
// Process the text to handle nested styles
168-
let processedText;
169-
if (codes.length > 0) {
170-
processedText = ArrayPrototypeReduce(
171-
codes,
172-
(text, code) => RegExpPrototypeSymbolReplace(
173-
// Find the reset code
174-
new RegExp(`\\u001b\\[${code[1]}m`, 'g'),
175-
text,
176-
(match, offset) => {
177-
// Check if there's more content after this reset
178-
if (offset + match.length < text.length) {
179-
if (
180-
code[0] === inspect.colors.dim[0] ||
181-
code[0] === inspect.colors.bold[0]
182-
) {
183-
// Dim and bold are not mutually exclusive, so we need to reapply
184-
return `${match}${escapeStyleCode(code[0])}`;
185-
}
186-
return escapeStyleCode(code[0]);
187-
}
188-
return match;
189-
},
190-
),
191-
text,
192-
);
193-
} else {
194-
processedText = text;
195-
}
196-
197-
// Build closing codes in reverse order
198-
let closeCodes = '';
199-
for (let i = codes.length - 1; i >= 0; i--) {
200-
closeCodes += escapeStyleCode(codes[i][1]);
201-
}
216+
if (skipColorize) return text;
202217

203218
return `${openCodes}${processedText}${closeCodes}`;
204219
}

test/parallel/test-util-styletext.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ const noChange = 'test';
2222
util.styleText(invalidOption, 'test');
2323
}, {
2424
code: 'ERR_INVALID_ARG_VALUE',
25-
});
25+
}, invalidOption);
2626
assert.throws(() => {
2727
util.styleText('red', invalidOption);
2828
}, {
2929
code: 'ERR_INVALID_ARG_TYPE'
30-
});
30+
}, invalidOption);
3131
});
3232

3333
assert.throws(() => {

0 commit comments

Comments
 (0)