Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 00705a4

Browse filesBrowse 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
Copy full SHA for 00705a4

4 files changed

+344-2Lines changed: 344 additions & 2 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎benchmark/util/style-text.js‎

Copy file name to clipboardExpand all lines: benchmark/util/style-text.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const assert = require('node:assert');
77

88
const bench = 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
});
Collapse file

‎doc/api/util.md‎

Copy file name to clipboardExpand all lines: doc/api/util.md
+29-1Lines changed: 29 additions & 1 deletion
  • Display the source diff
  • Display the rich diff
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`
Collapse file

‎lib/util.js‎

Copy file name to clipboardExpand all lines: lib/util.js
+66Lines 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
return result + str.slice(lastIndex);
157161
}
158162

163+
// Matches #RGB or #RRGGBB
164+
const hexColorRegExp = /^#(?:[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+
function isValidHexColor(hex) {
172+
return typeof hex === '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+
function hexToRgb(hex) {
182+
// Normalize to 6 digits
183+
let hexStr;
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+
} else if (hex.length === 7) {
188+
hexStr = StringPrototypeSlice(hex, 1);
189+
} else {
190+
throw new ERR_OUT_OF_RANGE('hex', '#RGB or #RRGGBB', hex);
191+
}
192+
193+
// TODO(araujogui): use Uint8Array.fromHex
194+
return Buffer.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+
function rgbToAnsi24Bit(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 (const key of formatArray) {
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+
const openSeq = kEscape + rgbToAnsi24Bit(r, g, b) + kEscapeEnd;
262+
const closeSeq = kEscape + '39' + kEscapeEnd;
263+
openCodes += openSeq;
264+
closeCodes = closeSeq + closeCodes;
265+
processedText = replaceCloseCode(processedText, closeSeq, openSeq, false);
266+
continue;
267+
}
268+
208269
const style = cache[key];
209270
if (style === undefined) {
271+
// Check if it looks like an invalid hex color (starts with #)
272+
if (typeof key === 'string' && key[0] === '#') {
273+
throw new ERR_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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.