Skip to content

Commit 075d196

Browse files
himself65ljharb
authored andcommitted
util: respect nested formats in styleText
Co-authored-by: Jordan Harband <[email protected]> PR-URL: #59098 Fixes: #59035 Reviewed-By: Jordan Harband <[email protected]> Reviewed-By: Marco Ippolito <[email protected]> Reviewed-By: Moshe Atlow <[email protected]>
1 parent 77649ad commit 075d196

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
@@ -25,6 +25,7 @@ const {
2525
ArrayIsArray,
2626
ArrayPrototypePop,
2727
ArrayPrototypePush,
28+
ArrayPrototypeReduce,
2829
Error,
2930
ErrorCaptureStackTrace,
3031
FunctionPrototypeBind,
@@ -36,6 +37,8 @@ const {
3637
ObjectSetPrototypeOf,
3738
ObjectValues,
3839
ReflectApply,
40+
RegExp,
41+
RegExpPrototypeSymbolReplace,
3942
StringPrototypeToWellFormed,
4043
} = primordials;
4144

@@ -137,8 +140,7 @@ function styleText(format, text, { validateStream = true, stream = process.stdou
137140
// If the format is not an array, convert it to an array
138141
const formatArray = ArrayIsArray(format) ? format : [format];
139142

140-
let left = '';
141-
let right = '';
143+
const codes = [];
142144
for (const key of formatArray) {
143145
if (key === 'none') continue;
144146
const formatCodes = inspect.colors[key];
@@ -147,11 +149,56 @@ function styleText(format, text, { validateStream = true, stream = process.stdou
147149
validateOneOf(key, 'format', ObjectKeys(inspect.colors));
148150
}
149151
if (skipColorize) continue;
150-
left += escapeStyleCode(formatCodes[0]);
151-
right = `${escapeStyleCode(formatCodes[1])}${right}`;
152+
ArrayPrototypePush(codes, formatCodes);
152153
}
153154

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

157204
/**

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)