@@ -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
105102let 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}
0 commit comments