Skip to content

Commit 37ddc2a

Browse files
committed
util: support background colors with hex codes
Signed-off-by: carlosnaico77 <carlosfibex@gmail.com>
1 parent f9715fc commit 37ddc2a

3 files changed

Lines changed: 81 additions & 21 deletions

File tree

doc/api/util.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2638,26 +2638,33 @@ The special format value `none` applies no additional styling to the text.
26382638
26392639
In addition to predefined color names, `util.styleText()` supports hex color
26402640
strings using ANSI TrueColor (24-bit) escape sequences. Hex colors can be
2641-
specified in either 3-digit (`#RGB`) or 6-digit (`#RRGGBB`) format:
2641+
specified in either 3-digit (`#RGB`) or 6-digit (`#RRGGBB`) format for foreground,
2642+
or prefixed with `bg` (e.g., `bg#RGB`, `bg#RRGGBB`) for background:
26422643
26432644
```mjs
26442645
import { styleText } from 'node:util';
26452646

2646-
// 6-digit hex color
2647+
// 6-digit hex color (foreground)
26472648
console.log(styleText('#ff5733', 'Orange text'));
26482649

2649-
// 3-digit hex color (shorthand)
2650+
// 3-digit hex color (shorthand) (foreground)
26502651
console.log(styleText('#f00', 'Red text'));
2652+
2653+
// Hex color for background
2654+
console.log(styleText('bg#ff5733', 'Text with orange background'));
26512655
```
26522656
26532657
```cjs
26542658
const { styleText } = require('node:util');
26552659

2656-
// 6-digit hex color
2660+
// 6-digit hex color (foreground)
26572661
console.log(styleText('#ff5733', 'Orange text'));
26582662

2659-
// 3-digit hex color (shorthand)
2663+
// 3-digit hex color (shorthand) (foreground)
26602664
console.log(styleText('#f00', 'Red text'));
2665+
2666+
// Hex color for background
2667+
console.log(styleText('bg#ff5733', 'Text with orange background'));
26612668
```
26622669
26632670
The full list of formats can be found in [modifiers][].

lib/util.js

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const {
4040
RegExpPrototypeExec,
4141
SafeMap,
4242
StringPrototypeSlice,
43+
StringPrototypeStartsWith,
4344
StringPrototypeToWellFormed,
4445
} = primordials;
4546

@@ -118,6 +119,8 @@ const kBoldCode = 1;
118119

119120
// Close sequence for 24-bit foreground colors (reset to default foreground)
120121
const kHexCloseSeq = kEscape + '39' + kEscapeEnd;
122+
// Close sequence for 24-bit background colors (reset to default background)
123+
const kBgHexCloseSeq = kEscape + '49' + kEscapeEnd;
121124

122125
let styleCache;
123126

@@ -155,20 +158,27 @@ function getStyleCache() {
155158
}
156159

157160
/**
158-
* Returns the cached ANSI escape sequences for a hex color.
161+
* Returns the cached ANSI escape sequences for a hex color (foreground or background).
159162
* Computes and caches on first use to avoid repeated Buffer allocations.
160-
* @param {string} hex A valid hex color string (#RGB or #RRGGBB)
163+
* @param {string} hex A valid hex color string (#RGB or #RRGGBB) or background (bg#RGB or bg#RRGGBB)
161164
* @returns {{openSeq: string, closeSeq: string}}
162165
*/
163166
function getHexStyle(hex) {
164167
const cache = getHexStyleCache();
165168
const cached = cache.get(hex);
166169
if (cached !== undefined) return cached;
167-
const { 0: r, 1: g, 2: b } = hexToRgb(hex);
170+
171+
// Check if this is a background hex color prefixed with 'bg#'
172+
const isBg = StringPrototypeStartsWith(hex, 'bg#');
173+
// Strip 'bg' prefix to extract the raw hex color string (#RGB or #RRGGBB)
174+
const cleanHex = isBg ? StringPrototypeSlice(hex, 2) : hex;
175+
const { 0: r, 1: g, 2: b } = hexToRgb(cleanHex);
168176
const style = {
169177
__proto__: null,
170-
openSeq: kEscape + rgbToAnsi24Bit(r, g, b) + kEscapeEnd,
171-
closeSeq: kHexCloseSeq,
178+
// 38 represents foreground TrueColor SGR parameter, while 48 represents background
179+
openSeq: kEscape + (isBg ? bgRgbToAnsi24Bit(r, g, b) : rgbToAnsi24Bit(r, g, b)) + kEscapeEnd,
180+
// 39 resets foreground to default, while 49 resets background to default
181+
closeSeq: isBg ? kBgHexCloseSeq : kHexCloseSeq,
172182
};
173183
if (cache.size >= kHexStyleCacheMax)
174184
cache.delete(cache.keys().next().value);
@@ -201,6 +211,8 @@ function replaceCloseCode(str, closeSeq, openSeq, keepClose) {
201211

202212
// Matches #RGB or #RRGGBB
203213
const hexColorRegExp = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
214+
// Matches bg#RGB or bg#RRGGBB
215+
const bgHexColorRegExp = /^bg#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
204216

205217
/**
206218
* Parses a hex color string into RGB components.
@@ -235,6 +247,17 @@ function rgbToAnsi24Bit(r, g, b) {
235247
return `38;2;${r};${g};${b}`;
236248
}
237249

250+
/**
251+
* Generates the ANSI TrueColor (24-bit) escape sequence for a background color.
252+
* @param {number} r Red component (0-255)
253+
* @param {number} g Green component (0-255)
254+
* @param {number} b Blue component (0-255)
255+
* @returns {string} The ANSI escape sequence
256+
*/
257+
function bgRgbToAnsi24Bit(r, g, b) {
258+
return `48;2;${r};${g};${b}`;
259+
}
260+
238261
/**
239262
* @param {string | string[]} format
240263
* @param {string} text
@@ -256,10 +279,15 @@ function styleText(format, text, options) {
256279
return style.openSeq + processed + style.closeSeq;
257280
}
258281

259-
if (format[0] === '#') {
282+
const isHex = format[0] === '#';
283+
const isBgHex = !isHex && StringPrototypeStartsWith(format, 'bg#');
284+
if (isHex || isBgHex) {
260285
let hexStyle = getHexStyleCache().get(format);
261-
if (hexStyle === undefined && RegExpPrototypeExec(hexColorRegExp, format) !== null) {
262-
hexStyle = getHexStyle(format);
286+
if (hexStyle === undefined) {
287+
const regExp = isHex ? hexColorRegExp : bgHexColorRegExp;
288+
if (RegExpPrototypeExec(regExp, format) !== null) {
289+
hexStyle = getHexStyle(format);
290+
}
263291
}
264292
if (hexStyle !== undefined) {
265293
const processed = replaceCloseCode(text, hexStyle.closeSeq, hexStyle.openSeq, false);
@@ -297,17 +325,25 @@ function styleText(format, text, options) {
297325
for (const key of formatArray) {
298326
if (key === 'none') continue;
299327

300-
if (typeof key === 'string' && key[0] === '#') {
301-
if (RegExpPrototypeExec(hexColorRegExp, key) === null) {
302-
throw new ERR_INVALID_ARG_VALUE('format', key,
303-
'must be a valid hex color (#RGB or #RRGGBB)');
328+
if (typeof key === 'string' && (key[0] === '#' || StringPrototypeStartsWith(key, 'bg#'))) {
329+
const isBg = key[0] === 'b';
330+
const regExp = isBg ? bgHexColorRegExp : hexColorRegExp;
331+
if (RegExpPrototypeExec(regExp, key) === null) {
332+
throw new ERR_INVALID_ARG_VALUE(
333+
'format',
334+
key,
335+
'must be a valid hex color (#RGB or #RRGGBB) or ' +
336+
'background hex color (bg#RGB or bg#RRGGBB)',
337+
);
304338
}
305339
if (skipColorize) continue;
306-
const { 0: r, 1: g, 2: b } = hexToRgb(key);
307-
const hexOpenSeq = kEscape + rgbToAnsi24Bit(r, g, b) + kEscapeEnd;
340+
const cleanHex = isBg ? StringPrototypeSlice(key, 2) : key;
341+
const { 0: r, 1: g, 2: b } = hexToRgb(cleanHex);
342+
const hexOpenSeq = kEscape + (isBg ? bgRgbToAnsi24Bit(r, g, b) : rgbToAnsi24Bit(r, g, b)) + kEscapeEnd;
343+
const closeSeq = isBg ? kBgHexCloseSeq : kHexCloseSeq;
308344
openCodes += hexOpenSeq;
309-
closeCodes = kHexCloseSeq + closeCodes;
310-
processedText = replaceCloseCode(processedText, kHexCloseSeq, hexOpenSeq, false);
345+
closeCodes = closeSeq + closeCodes;
346+
processedText = replaceCloseCode(processedText, closeSeq, hexOpenSeq, false);
311347
continue;
312348
}
313349

test/parallel/test-util-styletext-hex.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,21 @@ describe('util.styleText hex color support', () => {
245245
);
246246
});
247247
});
248+
249+
describe('valid background hex colors', () => {
250+
it('should parse bg#ffcc00 as RGB(255, 204, 0) background', () => {
251+
const styled = util.styleText('bg#ffcc00', 'test', { validateStream: false });
252+
assert.strictEqual(styled, '\u001b[48;2;255;204;0mtest\u001b[49m');
253+
});
254+
255+
it('should expand bg#fc0 to bg#ffcc00 -> RGB(255, 204, 0) background', () => {
256+
const styled = util.styleText('bg#fc0', 'test', { validateStream: false });
257+
assert.strictEqual(styled, '\u001b[48;2;255;204;0mtest\u001b[49m');
258+
});
259+
260+
it('should combine foreground and background hex colors', () => {
261+
const styled = util.styleText(['#ffffff', 'bg#ff5733'], 'test', { validateStream: false });
262+
assert.strictEqual(styled, '\u001b[38;2;255;255;255m\u001b[48;2;255;87;51mtest\u001b[49m\u001b[39m');
263+
});
264+
});
248265
});

0 commit comments

Comments
 (0)