Skip to content

Commit 00705a4

Browse files
araujoguiaduh95
authored andcommitted
util: colorize text with hex colors
PR-URL: #61556 Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com> Reviewed-By: René <contact.9a5d6388@renegade334.me.uk> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Claudio Wunder <cwunder@gnome.org> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent c128942 commit 00705a4

4 files changed

Lines changed: 344 additions & 2 deletions

File tree

‎benchmark/util/style-text.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const assert = require('node:assert');
77

88
constbench=common.createBenchmark(main,{
99
messageType: ['string','number','boolean','invalid'],
10-
format: ['red','italic','invalid'],
10+
format: ['red','italic','invalid','#ff0000'],
1111
validateStream: [1,0],
1212
n: [1e3],
1313
});

‎doc/api/util.md‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2520,6 +2520,9 @@ added:
25202520
- v21.7.0
25212521
- v20.12.0
25222522
changes:
2523+
- version: REPLACEME
2524+
pr-url: https://github.com/nodejs/node/pull/61556
2525+
description: Add support for hexadecimal colors.
25232526
- version: v24.2.0
25242527
pr-url: https://github.com/nodejs/node/pull/58437
25252528
description: Added the `'none'` format as a non-op format.
@@ -2537,7 +2540,8 @@ changes:
25372540
-->
25382541
25392542
* `format` {string | Array} A text format or an Array
2540-
of text formats defined in `util.inspect.colors`.
2543+
of text formats defined in `util.inspect.colors`, or a hex color in `#RGB`
2544+
or `#RRGGBB` form.
25412545
* `text` {string} The text to to be formatted.
25422546
* `options` {Object}
25432547
* `validateStream` {boolean} When true, `stream` is checked to see if it can handle colors. **Default:** `true`.
@@ -2600,6 +2604,30 @@ console.log(
26002604
26012605
The special format value `none` applies no additional styling to the text.
26022606
2607+
In addition to predefined color names, `util.styleText()` supports hex color
2608+
strings using ANSI TrueColor (24-bit) escape sequences. Hex colors can be
2609+
specified in either 3-digit (`#RGB`) or 6-digit (`#RRGGBB`) format:
2610+
2611+
```mjs
2612+
import { styleText } from'node:util';
2613+
2614+
// 6-digit hex color
2615+
console.log(styleText('#ff5733', 'Orange text'));
2616+
2617+
// 3-digit hex color (shorthand)
2618+
console.log(styleText('#f00', 'Red text'));
2619+
```
2620+
2621+
```cjs
2622+
const { styleText } =require('node:util');
2623+
2624+
// 6-digit hex color
2625+
console.log(styleText('#ff5733', 'Orange text'));
2626+
2627+
// 3-digit hex color (shorthand)
2628+
console.log(styleText('#f00', 'Red text'));
2629+
```
2630+
26032631
The full list of formats can be found in [modifiers][].
26042632
26052633
## Class: `util.TextDecoder`

‎lib/util.js‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const {
3737
ObjectSetPrototypeOf,
3838
ObjectValues,
3939
ReflectApply,
40+
RegExpPrototypeExec,
41+
StringPrototypeSlice,
4042
StringPrototypeToWellFormed,
4143
}=primordials;
4244

@@ -46,10 +48,12 @@ const {
4648
codes: {
4749
ERR_FALSY_VALUE_REJECTION,
4850
ERR_INVALID_ARG_TYPE,
51+
ERR_INVALID_ARG_VALUE,
4952
ERR_OUT_OF_RANGE,
5053
},
5154
isErrorStackTraceLimitWritable,
5255
}=require('internal/errors');
56+
const{ Buffer }=require('buffer');
5357
const{
5458
format,
5559
formatWithOptions,
@@ -156,6 +160,51 @@ function replaceCloseCode(str, closeSeq, openSeq, keepClose) {
156160
returnresult+str.slice(lastIndex);
157161
}
158162

163+
// Matches #RGB or #RRGGBB
164+
consthexColorRegExp=/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
165+
166+
/**
167+
* Validates whether a string is a valid hex color code.
168+
* @param {string} hex The hex string to validate (e.g., '#fff' or '#ffffff')
169+
* @returns {boolean} True if valid hex color, false otherwise
170+
*/
171+
functionisValidHexColor(hex){
172+
returntypeofhex==='string'&&RegExpPrototypeExec(hexColorRegExp,hex)!==null;
173+
}
174+
175+
/**
176+
* Parses a hex color string into RGB components.
177+
* Supports both 3-digit (#RGB) and 6-digit (#RRGGBB) formats.
178+
* @param {string} hex A valid hex color string
179+
* @returns {Buffer} The RGB components
180+
*/
181+
functionhexToRgb(hex){
182+
// Normalize to 6 digits
183+
lethexStr;
184+
if(hex.length===4){
185+
// Expand #RGB to #RRGGBB
186+
hexStr=hex[1]+hex[1]+hex[2]+hex[2]+hex[3]+hex[3];
187+
}elseif(hex.length===7){
188+
hexStr=StringPrototypeSlice(hex,1);
189+
}else{
190+
thrownewERR_OUT_OF_RANGE('hex','#RGB or #RRGGBB',hex);
191+
}
192+
193+
// TODO(araujogui): use Uint8Array.fromHex
194+
returnBuffer.from(hexStr,'hex');
195+
}
196+
197+
/**
198+
* Generates the ANSI TrueColor (24-bit) escape sequence for a foreground color.
199+
* @param {number} r Red component (0-255)
200+
* @param {number} g Green component (0-255)
201+
* @param {number} b Blue component (0-255)
202+
* @returns {string} The ANSI escape sequence
203+
*/
204+
functionrgbToAnsi24Bit(r,g,b){
205+
return`38;2;${r};${g};${b}`;
206+
}
207+
159208
/**
160209
* @param {string | string[]} format
161210
* @param {string} text
@@ -205,8 +254,25 @@ function styleText(format, text, options) {
205254

206255
for(constkeyofformatArray){
207256
if(key==='none')continue;
257+
258+
if(isValidHexColor(key)){
259+
if(skipColorize)continue;
260+
const{0: r,1: g,2: b}=hexToRgb(key);
261+
constopenSeq=kEscape+rgbToAnsi24Bit(r,g,b)+kEscapeEnd;
262+
constcloseSeq=kEscape+'39'+kEscapeEnd;
263+
openCodes+=openSeq;
264+
closeCodes=closeSeq+closeCodes;
265+
processedText=replaceCloseCode(processedText,closeSeq,openSeq,false);
266+
continue;
267+
}
268+
208269
conststyle=cache[key];
209270
if(style===undefined){
271+
// Check if it looks like an invalid hex color (starts with #)
272+
if(typeofkey==='string'&&key[0]==='#'){
273+
thrownewERR_INVALID_ARG_VALUE('format',key,
274+
'must be a valid hex color (#RGB or #RRGGBB)');
275+
}
210276
validateOneOf(key,'format',ObjectGetOwnPropertyNames(inspect.colors));
211277
}
212278
openCodes+=style.openSeq;

0 commit comments

Comments
 (0)