diff --git a/lib/util.js b/lib/util.js index a422daa212b855..46296e5b1e916e 100644 --- a/lib/util.js +++ b/lib/util.js @@ -25,6 +25,7 @@ const { ArrayIsArray, ArrayPrototypePop, ArrayPrototypePush, + ArrayPrototypeReduce, Error, ErrorCaptureStackTrace, FunctionPrototypeBind, @@ -36,6 +37,8 @@ const { ObjectSetPrototypeOf, ObjectValues, ReflectApply, + RegExp, + RegExpPrototypeSymbolReplace, StringPrototypeToWellFormed, } = primordials; @@ -137,8 +140,7 @@ function styleText(format, text, { validateStream = true, stream = process.stdou // If the format is not an array, convert it to an array const formatArray = ArrayIsArray(format) ? format : [format]; - let left = ''; - let right = ''; + const codes = []; for (const key of formatArray) { if (key === 'none') continue; const formatCodes = inspect.colors[key]; @@ -147,11 +149,56 @@ function styleText(format, text, { validateStream = true, stream = process.stdou validateOneOf(key, 'format', ObjectKeys(inspect.colors)); } if (skipColorize) continue; - left += escapeStyleCode(formatCodes[0]); - right = `${escapeStyleCode(formatCodes[1])}${right}`; + ArrayPrototypePush(codes, formatCodes); } - return skipColorize ? text : `${left}${text}${right}`; + if (skipColorize) { + return text; + } + + // 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]); + } + + return `${openCodes}${processedText}${closeCodes}`; } /** diff --git a/test/parallel/test-util-styletext.js b/test/parallel/test-util-styletext.js index df2334651cc869..b87c5d7e82c74c 100644 --- a/test/parallel/test-util-styletext.js +++ b/test/parallel/test-util-styletext.js @@ -46,6 +46,83 @@ assert.strictEqual( '\u001b[1m\u001b[31mtest\u001b[39m\u001b[22m', ); +assert.strictEqual( + util.styleText('red', + 'A' + util.styleText('blue', 'B', { validateStream: false }) + 'C', + { validateStream: false }), + '\u001b[31mA\u001b[34mB\u001b[31mC\u001b[39m' +); + +assert.strictEqual( + util.styleText('red', + 'red' + + util.styleText('blue', 'blue', { validateStream: false }) + + 'red' + + util.styleText('blue', 'blue', { validateStream: false }) + + 'red', + { validateStream: false } + ), + '\x1B[31mred\x1B[34mblue\x1B[31mred\x1B[34mblue\x1B[31mred\x1B[39m' +); + +assert.strictEqual( + util.styleText('red', + 'red' + + util.styleText('blue', 'blue', { validateStream: false }) + + 'red' + + util.styleText('red', 'red', { validateStream: false }) + + 'red' + + util.styleText('blue', 'blue', { validateStream: false }), + { validateStream: false } + ), + '\x1b[31mred\x1b[34mblue\x1b[31mred\x1b[31mred\x1b[31mred\x1b[34mblue\x1b[39m\x1b[39m' +); + +assert.strictEqual( + util.styleText('red', + 'A' + util.styleText(['bgRed', 'blue'], 'B', { validateStream: false }) + + 'C', { validateStream: false }), + '\x1B[31mA\x1B[41m\x1B[34mB\x1B[31m\x1B[49mC\x1B[39m' +); + +assert.strictEqual( + util.styleText('dim', + 'dim' + + util.styleText('bold', 'bold', { validateStream: false }) + + 'dim', { validateStream: false }), + '\x1B[2mdim\x1B[1mbold\x1B[22m\x1B[2mdim\x1B[22m' +); + +assert.strictEqual( + util.styleText('blue', + 'blue' + + util.styleText('red', + 'red' + + util.styleText('green', 'green', { validateStream: false }) + + 'red', { validateStream: false }) + + 'blue', { validateStream: false }), + '\x1B[34mblue\x1B[31mred\x1B[32mgreen\x1B[31mred\x1B[34mblue\x1B[39m' +); + +assert.strictEqual( + util.styleText( + 'red', + 'red' + + util.styleText( + 'blue', + 'blue' + util.styleText('red', 'red', { + validateStream: false, + }) + 'blue', + { + validateStream: false, + } + ) + 'red', { + validateStream: false, + } + ), + '\x1b[31mred\x1b[34mblue\x1b[31mred\x1b[34mblue\x1b[31mred\x1b[39m' +); + assert.strictEqual( util.styleText(['bold', 'red'], 'test', { validateStream: false }), util.styleText(