Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmark/util/style-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const assert = require('node:assert');

const bench = common.createBenchmark(main, {
messageType: ['string', 'number', 'boolean', 'invalid'],
format: ['red', 'italic', 'invalid'],
format: ['red', 'italic', 'invalid', '#ff0000'],
validateStream: [1, 0],
n: [1e3],
});
Expand Down
30 changes: 29 additions & 1 deletion doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -2529,6 +2529,9 @@ added:
- v21.7.0
- v20.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/61556
description: Add support for hexadecimal colors.
- version:
- v24.2.0
- v22.17.0
Expand All @@ -2548,7 +2551,8 @@ changes:
-->

* `format` {string | Array} A text format or an Array
of text formats defined in `util.inspect.colors`.
of text formats defined in `util.inspect.colors`, or a hex color in `#RGB`
or `#RRGGBB` form.
* `text` {string} The text to to be formatted.
* `options` {Object}
* `validateStream` {boolean} When true, `stream` is checked to see if it can handle colors. **Default:** `true`.
Expand Down Expand Up @@ -2611,6 +2615,30 @@ console.log(

The special format value `none` applies no additional styling to the text.

In addition to predefined color names, `util.styleText()` supports hex color
strings using ANSI TrueColor (24-bit) escape sequences. Hex colors can be
specified in either 3-digit (`#RGB`) or 6-digit (`#RRGGBB`) format:

```mjs
import { styleText } from 'node:util';

// 6-digit hex color
console.log(styleText('#ff5733', 'Orange text'));

// 3-digit hex color (shorthand)
console.log(styleText('#f00', 'Red text'));
```

```cjs
const { styleText } = require('node:util');

// 6-digit hex color
console.log(styleText('#ff5733', 'Orange text'));

// 3-digit hex color (shorthand)
console.log(styleText('#f00', 'Red text'));
```

The full list of formats can be found in [modifiers][].

## Class: `util.TextDecoder`
Expand Down
66 changes: 66 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const {
ObjectSetPrototypeOf,
ObjectValues,
ReflectApply,
RegExpPrototypeExec,
StringPrototypeSlice,
StringPrototypeToWellFormed,
} = primordials;

Expand All @@ -45,10 +47,12 @@ const {
codes: {
ERR_FALSY_VALUE_REJECTION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE,
},
isErrorStackTraceLimitWritable,
} = require('internal/errors');
const { Buffer } = require('buffer');
const {
format,
formatWithOptions,
Expand Down Expand Up @@ -155,6 +159,51 @@ function replaceCloseCode(str, closeSeq, openSeq, keepClose) {
return result + str.slice(lastIndex);
}

// Matches #RGB or #RRGGBB
const hexColorRegExp = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;

/**
* Validates whether a string is a valid hex color code.
* @param {string} hex The hex string to validate (e.g., '#fff' or '#ffffff')
* @returns {boolean} True if valid hex color, false otherwise
*/
function isValidHexColor(hex) {
return typeof hex === 'string' && RegExpPrototypeExec(hexColorRegExp, hex) !== null;
}

/**
* Parses a hex color string into RGB components.
* Supports both 3-digit (#RGB) and 6-digit (#RRGGBB) formats.
* @param {string} hex A valid hex color string
* @returns {Buffer} The RGB components
*/
function hexToRgb(hex) {
// Normalize to 6 digits
let hexStr;
if (hex.length === 4) {
// Expand #RGB to #RRGGBB
hexStr = hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
} else if (hex.length === 7) {
hexStr = StringPrototypeSlice(hex, 1);
} else {
throw new ERR_OUT_OF_RANGE('hex', '#RGB or #RRGGBB', hex);
}

// TODO(araujogui): use Uint8Array.fromHex
return Buffer.from(hexStr, 'hex');
}

/**
* Generates the ANSI TrueColor (24-bit) escape sequence for a foreground color.
* @param {number} r Red component (0-255)
* @param {number} g Green component (0-255)
* @param {number} b Blue component (0-255)
* @returns {string} The ANSI escape sequence
*/
function rgbToAnsi24Bit(r, g, b) {
return `38;2;${r};${g};${b}`;
}

/**
* @param {string | string[]} format
* @param {string} text
Expand Down Expand Up @@ -204,8 +253,25 @@ function styleText(format, text, options) {

for (const key of formatArray) {
if (key === 'none') continue;

if (isValidHexColor(key)) {
if (skipColorize) continue;
const { 0: r, 1: g, 2: b } = hexToRgb(key);
const openSeq = kEscape + rgbToAnsi24Bit(r, g, b) + kEscapeEnd;
const closeSeq = kEscape + '39' + kEscapeEnd;
openCodes += openSeq;
closeCodes = closeSeq + closeCodes;
processedText = replaceCloseCode(processedText, closeSeq, openSeq, false);
continue;
}

const style = cache[key];
if (style === undefined) {
// Check if it looks like an invalid hex color (starts with #)
if (typeof key === 'string' && key[0] === '#') {
throw new ERR_INVALID_ARG_VALUE('format', key,
'must be a valid hex color (#RGB or #RRGGBB)');
}
validateOneOf(key, 'format', ObjectKeys(inspect.colors));
}
openCodes += style.openSeq;
Expand Down
Loading