Skip to content

Commit 5b6a943

Browse files
committed
util: colorize text with hex colors
1 parent 150d154 commit 5b6a943

File tree

2 files changed

+97
-0
lines changed

2 files changed

+97
-0
lines changed

lib/util.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,55 @@ function escapeStyleCode(code) {
112112
return `\u001b[${code}m`;
113113
}
114114

115+
/**
116+
* @param {number} code
117+
* @returns {number}
118+
*/
119+
function hexToNibble(code) {
120+
// '0'-'9'
121+
if (code >= 48 && code <= 57) return code - 48;
122+
// 'A'-'F'
123+
if (code >= 65 && code <= 70) return code - 55;
124+
// 'a'-'f'
125+
if (code >= 97 && code <= 102) return code - 87;
126+
return -1;
127+
}
128+
129+
const hastagCharcode = 35
130+
131+
/**
132+
* Parses a hex color in #RGB or #RRGGBB form.
133+
* Returns RGB bytes on success, otherwise undefined
134+
* .
135+
* @param {string} format
136+
* @returns {[number, number, number] | undefined}
137+
*/
138+
function parseHexColor(format) {
139+
if (format.length !== 4 && format.length !== 7) return;
140+
141+
if (format.length === 4) {
142+
const r = hexToNibble(format.charCodeAt(1));
143+
const g = hexToNibble(format.charCodeAt(2));
144+
const b = hexToNibble(format.charCodeAt(3));
145+
146+
if ((r | g | b) < 0) return;
147+
148+
// Duplicate nibbles: #RGB -> #RRGGBB
149+
return [(r << 4) | r, (g << 4) | g, (b << 4) | b];
150+
}
151+
152+
const r1 = hexToNibble(format.charCodeAt(1));
153+
const r2 = hexToNibble(format.charCodeAt(2));
154+
const g1 = hexToNibble(format.charCodeAt(3));
155+
const g2 = hexToNibble(format.charCodeAt(4));
156+
const b1 = hexToNibble(format.charCodeAt(5));
157+
const b2 = hexToNibble(format.charCodeAt(6));
158+
159+
if ((r1 | r2 | g1 | g2 | b1 | b2) < 0) return;
160+
161+
return [(r1 << 4) | r2, (g1 << 4) | g2, (b1 << 4) | b2];
162+
}
163+
115164
/**
116165
* @param {string | string[]} format
117166
* @param {string} text
@@ -144,6 +193,15 @@ function styleText(format, text, { validateStream = true, stream = process.stdou
144193
const codes = [];
145194
for (const key of formatArray) {
146195
if (key === 'none') continue;
196+
197+
if (typeof key === 'string' && key.charCodeAt(0) === hastagCharcode) {
198+
const rgb = parseHexColor(key);
199+
if (rgb !== undefined && !skipColorize) {
200+
ArrayPrototypePush(codes, [`38;2;${rgb[0]};${rgb[1]};${rgb[2]}`, 39]);
201+
}
202+
continue;
203+
}
204+
147205
const formatCodes = inspect.colors[key];
148206
// If the format is not a valid style, throw an error
149207
if (formatCodes == null) {

test/parallel/test-util-styletext.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,37 @@ assert.throws(() => {
3636
code: 'ERR_INVALID_ARG_VALUE',
3737
});
3838

39+
assert.strictEqual(
40+
util.styleText('#fff', 'test', { validateStream: false }),
41+
'\u001b[38;2;255;255;255mtest\u001b[39m',
42+
);
43+
44+
assert.strictEqual(
45+
util.styleText('#ffcc00', 'test', { validateStream: false }),
46+
'\u001b[38;2;255;204;0mtest\u001b[39m',
47+
);
48+
49+
assert.strictEqual(
50+
util.styleText(['#ffcc00', 'bold'], 'test', { validateStream: false }),
51+
'\x1B[38;2;255;204;0m\x1B[1mtest\x1B[22m\x1B[39m',
52+
);
53+
54+
55+
['#ff', '#zzzzzz', '#12345g'].forEach((invalidHex) => {
56+
assert.doesNotThrow(() => {
57+
util.styleText(invalidHex, 'test', { validateStream: false });
58+
});
59+
assert.strictEqual(
60+
util.styleText(invalidHex, 'test', { validateStream: false }),
61+
noChange,
62+
);
63+
});
64+
65+
assert.strictEqual(
66+
util.styleText(['red', '#zzzzzz'], 'test', { validateStream: false }),
67+
styled,
68+
);
69+
3970
assert.strictEqual(
4071
util.styleText('red', 'test', { validateStream: false }),
4172
'\u001b[31mtest\u001b[39m',
@@ -183,6 +214,14 @@ if (fd !== -1) {
183214
const output = util.styleText(['red'], 'test', { stream: writeStream });
184215
assert.strictEqual(output, testCase.expected);
185216
}
217+
218+
// When colors are disabled, hex colors should also be disabled.
219+
if (testCase.expected === noChange) {
220+
const output = util.styleText('#ffcc00', 'test', { stream: writeStream });
221+
assert.strictEqual(output, noChange);
222+
const outputArray = util.styleText(['#ffcc00'], 'test', { stream: writeStream });
223+
assert.strictEqual(outputArray, noChange);
224+
}
186225
process.env = originalEnv;
187226
});
188227
} else {

0 commit comments

Comments
 (0)