util: colorize text with hex colors · nodejs/node@00705a4
@@ -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 {
4648codes: {
4749ERR_FALSY_VALUE_REJECTION,
4850ERR_INVALID_ARG_TYPE,
51+ERR_INVALID_ARG_VALUE,
4952ERR_OUT_OF_RANGE,
5053},
5154 isErrorStackTraceLimitWritable,
5255} = require('internal/errors');
56+const { Buffer } = require('buffer');
5357const {
5458 format,
5559 formatWithOptions,
@@ -156,6 +160,51 @@ function replaceCloseCode(str, closeSeq, openSeq, keepClose) {
156160return result + str.slice(lastIndex);
157161}
158162163+// 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) {
205254206255for (const key of formatArray) {
207256if (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+208269const style = cache[key];
209270if (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+}
210276validateOneOf(key, 'format', ObjectGetOwnPropertyNames(inspect.colors));
211277}
212278openCodes += style.openSeq;