◐ Shell
clean mode source ↗

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 {

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

}

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) {

205254206255

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;