Skip to content

Commit e22dddf

Browse files
himself65ljharb
authored andcommitted
util: respect nested formats in styleText
Co-authored-by: Jordan Harband <ljharb@gmail.com> PR-URL: #59098 Fixes: #59035 Reviewed-By: Jordan Harband <ljharb@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent 1e7639e commit e22dddf

File tree

2 files changed

+129
-5
lines changed

2 files changed

+129
-5
lines changed

β€Žlib/util.js

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const {
2626
ArrayPrototypeJoin,
2727
ArrayPrototypePop,
2828
ArrayPrototypePush,
29+
ArrayPrototypeReduce,
2930
Date,
3031
DatePrototypeGetDate,
3132
DatePrototypeGetHours,
@@ -44,6 +45,8 @@ const {
4445
ObjectSetPrototypeOf,
4546
ObjectValues,
4647
ReflectApply,
48+
RegExp,
49+
RegExpPrototypeSymbolReplace,
4750
StringPrototypePadStart,
4851
StringPrototypeToWellFormed,
4952
} = primordials;
@@ -254,8 +257,7 @@ function styleText(format, text, { validateStream = true, stream = process.stdou
254257
// If the format is not an array, convert it to an array
255258
const formatArray = ArrayIsArray(format) ? format : [format];
256259

257-
let left = '';
258-
let right = '';
260+
const codes = [];
259261
for (const key of formatArray) {
260262
if (key === 'none') continue;
261263
const formatCodes = inspect.colors[key];
@@ -264,11 +266,56 @@ function styleText(format, text, { validateStream = true, stream = process.stdou
264266
validateOneOf(key, 'format', ObjectKeys(inspect.colors));
265267
}
266268
if (skipColorize) continue;
267-
left += escapeStyleCode(formatCodes[0]);
268-
right = `${escapeStyleCode(formatCodes[1])}${right}`;
269+
ArrayPrototypePush(codes, formatCodes);
269270
}
270271

271-
return skipColorize ? text : `${left}${text}${right}`;
272+
if (skipColorize) {
273+
return text;
274+
}
275+
276+
// Build opening codes
277+
let openCodes = '';
278+
for (let i = 0; i < codes.length; i++) {
279+
openCodes += escapeStyleCode(codes[i][0]);
280+
}
281+
282+
// Process the text to handle nested styles
283+
let processedText;
284+
if (codes.length > 0) {
285+
processedText = ArrayPrototypeReduce(
286+
codes,
287+
(text, code) => RegExpPrototypeSymbolReplace(
288+
// Find the reset code
289+
new RegExp(`\\u001b\\[${code[1]}m`, 'g'),
290+
text,
291+
(match, offset) => {
292+
// Check if there's more content after this reset
293+
if (offset + match.length < text.length) {
294+
if (
295+
code[0] === inspect.colors.dim[0] ||
296+
code[0] === inspect.colors.bold[0]
297+
) {
298+
// Dim and bold are not mutually exclusive, so we need to reapply
299+
return `${match}${escapeStyleCode(code[0])}`;
300+
}
301+
return `${escapeStyleCode(code[0])}`;
302+
}
303+
return match;
304+
},
305+
),
306+
text,
307+
);
308+
} else {
309+
processedText = text;
310+
}
311+
312+
// Build closing codes in reverse order
313+
let closeCodes = '';
314+
for (let i = codes.length - 1; i >= 0; i--) {
315+
closeCodes += escapeStyleCode(codes[i][1]);
316+
}
317+
318+
return `${openCodes}${processedText}${closeCodes}`;
272319
}
273320

274321
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',

β€Žtest/parallel/test-util-styletext.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,83 @@ assert.strictEqual(
4646
'\u001b[1m\u001b[31mtest\u001b[39m\u001b[22m',
4747
);
4848

49+
assert.strictEqual(
50+
util.styleText('red',
51+
'A' + util.styleText('blue', 'B', { validateStream: false }) + 'C',
52+
{ validateStream: false }),
53+
'\u001b[31mA\u001b[34mB\u001b[31mC\u001b[39m'
54+
);
55+
56+
assert.strictEqual(
57+
util.styleText('red',
58+
'red' +
59+
util.styleText('blue', 'blue', { validateStream: false }) +
60+
'red' +
61+
util.styleText('blue', 'blue', { validateStream: false }) +
62+
'red',
63+
{ validateStream: false }
64+
),
65+
'\x1B[31mred\x1B[34mblue\x1B[31mred\x1B[34mblue\x1B[31mred\x1B[39m'
66+
);
67+
68+
assert.strictEqual(
69+
util.styleText('red',
70+
'red' +
71+
util.styleText('blue', 'blue', { validateStream: false }) +
72+
'red' +
73+
util.styleText('red', 'red', { validateStream: false }) +
74+
'red' +
75+
util.styleText('blue', 'blue', { validateStream: false }),
76+
{ validateStream: false }
77+
),
78+
'\x1b[31mred\x1b[34mblue\x1b[31mred\x1b[31mred\x1b[31mred\x1b[34mblue\x1b[39m\x1b[39m'
79+
);
80+
81+
assert.strictEqual(
82+
util.styleText('red',
83+
'A' + util.styleText(['bgRed', 'blue'], 'B', { validateStream: false }) +
84+
'C', { validateStream: false }),
85+
'\x1B[31mA\x1B[41m\x1B[34mB\x1B[31m\x1B[49mC\x1B[39m'
86+
);
87+
88+
assert.strictEqual(
89+
util.styleText('dim',
90+
'dim' +
91+
util.styleText('bold', 'bold', { validateStream: false }) +
92+
'dim', { validateStream: false }),
93+
'\x1B[2mdim\x1B[1mbold\x1B[22m\x1B[2mdim\x1B[22m'
94+
);
95+
96+
assert.strictEqual(
97+
util.styleText('blue',
98+
'blue' +
99+
util.styleText('red',
100+
'red' +
101+
util.styleText('green', 'green', { validateStream: false }) +
102+
'red', { validateStream: false }) +
103+
'blue', { validateStream: false }),
104+
'\x1B[34mblue\x1B[31mred\x1B[32mgreen\x1B[31mred\x1B[34mblue\x1B[39m'
105+
);
106+
107+
assert.strictEqual(
108+
util.styleText(
109+
'red',
110+
'red' +
111+
util.styleText(
112+
'blue',
113+
'blue' + util.styleText('red', 'red', {
114+
validateStream: false,
115+
}) + 'blue',
116+
{
117+
validateStream: false,
118+
}
119+
) + 'red', {
120+
validateStream: false,
121+
}
122+
),
123+
'\x1b[31mred\x1b[34mblue\x1b[31mred\x1b[34mblue\x1b[31mred\x1b[39m'
124+
);
125+
49126
assert.strictEqual(
50127
util.styleText(['bold', 'red'], 'test', { validateStream: false }),
51128
util.styleText(

0 commit comments

Comments
 (0)