@ix-xs/node-comfort 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/Color.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* console.log(color.gradient("node-comfort", ["#ff5f6d", "#ffc371"]));
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
const { segmenter, graphemes } = require("./internal/shared.js");
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Every named style.
|
|
17
19
|
* @typedef {"reset" | "bold" | "dim" | "italic" | "underline" | "overline" | "inverse" | "hidden" | "strikethrough"
|
|
@@ -108,12 +110,13 @@
|
|
|
108
110
|
/**
|
|
109
111
|
* Color helpers.
|
|
110
112
|
* @typedef {object} ColorHelpers
|
|
111
|
-
* @property {ColorLevel} level The current color level. Set it to force one
|
|
113
|
+
* @property {ColorLevel} level The current color level. Set it to force one: `0` turns colors off, `undefined` goes back to what the terminal supports.
|
|
112
114
|
* @property {boolean} enabled Whether colors are being output.
|
|
113
115
|
* @property {(level?: ColorLevel) => ColorModule} create A separate instance with its own level, for another stream or for tests.
|
|
114
116
|
* @property {(stream?: NodeJS.WriteStream | { isTTY?: boolean }) => ColorLevel} detect The color level a stream supports, taking `NO_COLOR` and `FORCE_COLOR` into account.
|
|
115
117
|
* @property {(text: string) => string} strip Removes colors and other terminal escape codes.
|
|
116
118
|
* @property {(text: string) => number} width How many columns the text takes, ignoring colors and counting emoji and CJK as 2.
|
|
119
|
+
* @property {(text: string, columns: number, options?: { ellipsis?: string }) => string} truncate Cuts text to a number of columns and adds `…`. Colors are kept and still closed properly.
|
|
117
120
|
* @property {(text: string, url: string) => string} link A clickable link in terminals that support it, `"text (url)"` elsewhere.
|
|
118
121
|
* @property {(text: string, colors: string[]) => string} gradient Colors the text along a gradient of hex colors.
|
|
119
122
|
*/
|
|
@@ -130,6 +133,10 @@ const _CODES = {
|
|
|
130
133
|
bgBlueBright: [104, 49], bgMagentaBright: [105, 49], bgCyanBright: [106, 49], bgWhiteBright: [107, 49],
|
|
131
134
|
};
|
|
132
135
|
|
|
136
|
+
/** @type {Record<string, { open: string, close: string }>} */
|
|
137
|
+
const _STYLES = {};
|
|
138
|
+
for (const [name, [open, close]] of Object.entries(_CODES)) _STYLES[name] = { open: `\u001b[${open}m`, close: `\u001b[${close}m` };
|
|
139
|
+
|
|
133
140
|
/**
|
|
134
141
|
* A style in a chain: fixed codes, or a color resolved at output time.
|
|
135
142
|
* @typedef {{ open: string, close: string } | { rgb: [number, number, number], bg: boolean } | { ansi256: number, bg: boolean }} StyleEntry
|
|
@@ -163,16 +170,18 @@ const _rgbToAnsi256 = (r, g, b) => {
|
|
|
163
170
|
return 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
|
|
164
171
|
};
|
|
165
172
|
|
|
173
|
+
/** @type {Array<[number, number, number]>} */
|
|
174
|
+
const _ANSI16_RGB = [
|
|
175
|
+
[0, 0, 0], [205, 0, 0], [0, 205, 0], [205, 205, 0], [0, 0, 238], [205, 0, 205], [0, 205, 205], [229, 229, 229],
|
|
176
|
+
[127, 127, 127], [255, 0, 0], [0, 255, 0], [255, 255, 0], [92, 92, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255],
|
|
177
|
+
];
|
|
178
|
+
|
|
166
179
|
/**
|
|
167
180
|
* @param {number} code
|
|
168
181
|
* @returns {[number, number, number]}
|
|
169
182
|
*/
|
|
170
183
|
const _ansi256ToRgb = (code) => {
|
|
171
|
-
if (code < 16)
|
|
172
|
-
const base = [[0, 0, 0], [205, 0, 0], [0, 205, 0], [205, 205, 0], [0, 0, 238], [205, 0, 205], [0, 205, 205], [229, 229, 229],
|
|
173
|
-
[127, 127, 127], [255, 0, 0], [0, 255, 0], [255, 255, 0], [92, 92, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]];
|
|
174
|
-
return /** @type {[number, number, number]} */ (base[code]);
|
|
175
|
-
}
|
|
184
|
+
if (code < 16) return _ANSI16_RGB[code];
|
|
176
185
|
if (code >= 232) {
|
|
177
186
|
const v = (code - 232) * 10 + 8;
|
|
178
187
|
return [v, v, v];
|
|
@@ -214,14 +223,30 @@ const _resolve = (entry, level) => {
|
|
|
214
223
|
return { open: `\u001b[${code}m`, close };
|
|
215
224
|
};
|
|
216
225
|
|
|
226
|
+
// Command line flags don't change while the program runs: read them once.
|
|
227
|
+
/** @type {{ off: boolean, level: ColorLevel | undefined, forced: boolean } | undefined} */
|
|
228
|
+
let _flags;
|
|
229
|
+
const _argvFlags = () => {
|
|
230
|
+
if (_flags) return _flags;
|
|
231
|
+
const argv = process.argv;
|
|
232
|
+
const has = (/** @type {string[]} */ ...names) => names.some((name) => argv.includes(name));
|
|
233
|
+
return (_flags = {
|
|
234
|
+
off: has("--no-color", "--no-colors", "--color=false", "--color=never"),
|
|
235
|
+
level: has("--color=16m", "--color=full", "--color=truecolor") ? 3 : has("--color=256") ? 2 : undefined,
|
|
236
|
+
forced: has("--color", "--colors", "--color=true", "--color=always"),
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const _CI_WITH_COLORS = ["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"];
|
|
241
|
+
|
|
217
242
|
/**
|
|
218
243
|
* @param {NodeJS.WriteStream | { isTTY?: boolean } | undefined} stream
|
|
219
244
|
* @returns {ColorLevel}
|
|
220
245
|
*/
|
|
221
246
|
const _detect = (stream = process.stdout) => {
|
|
222
247
|
const env = process.env;
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
248
|
+
const flags = _argvFlags();
|
|
249
|
+
if (flags.off) return 0;
|
|
225
250
|
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") return 0;
|
|
226
251
|
if (env.NODE_DISABLE_COLORS !== undefined && env.NODE_DISABLE_COLORS !== "") return 0;
|
|
227
252
|
if (env.FORCE_COLOR !== undefined) {
|
|
@@ -231,25 +256,30 @@ const _detect = (stream = process.stdout) => {
|
|
|
231
256
|
if (value === "3") return 3;
|
|
232
257
|
return 1;
|
|
233
258
|
}
|
|
234
|
-
if (
|
|
235
|
-
if (
|
|
236
|
-
const forced = argv.includes("--color") || argv.includes("--colors") || argv.includes("--color=true") || argv.includes("--color=always");
|
|
237
|
-
if (env.TERM === "dumb" && !forced) return 0;
|
|
259
|
+
if (flags.level !== undefined) return flags.level;
|
|
260
|
+
if (env.TERM === "dumb" && !flags.forced) return 0;
|
|
238
261
|
const tty = /** @type {NodeJS.WriteStream | undefined} */ (stream && "isTTY" in stream && stream.isTTY ? stream : undefined);
|
|
239
262
|
if (tty && typeof tty.getColorDepth === "function") {
|
|
240
263
|
const depth = tty.getColorDepth();
|
|
241
264
|
if (depth >= 24) return 3;
|
|
242
265
|
if (depth >= 8) return 2;
|
|
243
266
|
if (depth >= 4) return 1;
|
|
244
|
-
return forced ? 1 : 0;
|
|
267
|
+
return flags.forced ? 1 : 0;
|
|
245
268
|
}
|
|
246
269
|
if (env.CI !== undefined) {
|
|
247
270
|
if (env.GITHUB_ACTIONS || env.GITEA_ACTIONS) return 3;
|
|
248
|
-
if (
|
|
271
|
+
if (_CI_WITH_COLORS.some((name) => name in env)) return 1;
|
|
249
272
|
}
|
|
250
|
-
return forced ? 1 : 0;
|
|
273
|
+
return flags.forced ? 1 : 0;
|
|
251
274
|
};
|
|
252
275
|
|
|
276
|
+
const _PLAIN = /^[\x20-\x7e]*$/;
|
|
277
|
+
// What can turn several code points into one character on screen: accents, joiners, flags, skin tones.
|
|
278
|
+
const _JOINED = /[\p{Mark}\u200d\ufe0e\ufe0f\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}]/u;
|
|
279
|
+
const _MARK = /^\p{Mark}+$/u;
|
|
280
|
+
const _EMOJI = /\p{Emoji_Presentation}/u;
|
|
281
|
+
const _WIDE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰-﹏-⦆¢-₩\u{20000}-\u{3FFFD}]/u;
|
|
282
|
+
|
|
253
283
|
/**
|
|
254
284
|
* Terminal width of one grapheme.
|
|
255
285
|
* @param {string} grapheme
|
|
@@ -258,21 +288,22 @@ const _detect = (stream = process.stdout) => {
|
|
|
258
288
|
const _charWidth = (grapheme) => {
|
|
259
289
|
const cp = /** @type {number} */ (grapheme.codePointAt(0));
|
|
260
290
|
if (cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0;
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
return
|
|
291
|
+
if (cp < 0x300 && grapheme.length === 1) return 1;
|
|
292
|
+
// zero width space and joiners, direction marks, byte order mark
|
|
293
|
+
if ((cp >= 0x200b && cp <= 0x200f) || cp === 0x2060 || cp === 0xfeff) return 0;
|
|
294
|
+
if (_MARK.test(grapheme)) return 0;
|
|
295
|
+
if (_EMOJI.test(grapheme) || grapheme.includes("\ufe0f")) return 2;
|
|
296
|
+
return _WIDE.test(grapheme) ? 2 : 1;
|
|
265
297
|
};
|
|
266
298
|
|
|
267
|
-
/** @type {Intl.Segmenter | undefined} */
|
|
268
|
-
let _segmenter;
|
|
269
|
-
const _graphemeSegmenter = () => (_segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }));
|
|
270
|
-
|
|
271
299
|
/**
|
|
272
300
|
* @param {string} text
|
|
273
301
|
* @returns {string}
|
|
274
302
|
*/
|
|
275
|
-
const strip = (text) =>
|
|
303
|
+
const strip = (text) => {
|
|
304
|
+
const value = typeof text === "string" ? text : String(text);
|
|
305
|
+
return value.indexOf("\u001b") === -1 && value.indexOf("\u009b") === -1 ? value : value.replace(_ANSI_REGEX, "");
|
|
306
|
+
};
|
|
276
307
|
|
|
277
308
|
/**
|
|
278
309
|
* @param {string} text
|
|
@@ -280,12 +311,68 @@ const strip = (text) => String(text).replace(_ANSI_REGEX, "");
|
|
|
280
311
|
*/
|
|
281
312
|
const width = (text) => {
|
|
282
313
|
const plain = strip(text);
|
|
283
|
-
|
|
314
|
+
let i = 0;
|
|
315
|
+
// ASCII and accented Latin letters: one column each.
|
|
316
|
+
for (; i < plain.length; i++) {
|
|
317
|
+
const code = plain.charCodeAt(i);
|
|
318
|
+
if (code < 0x20 || (code > 0x7e && code < 0xa0) || code > 0x2ff) break;
|
|
319
|
+
}
|
|
320
|
+
if (i === plain.length) return i;
|
|
284
321
|
let total = 0;
|
|
285
|
-
|
|
322
|
+
if (_JOINED.test(plain)) {
|
|
323
|
+
for (const { segment } of segmenter().segment(plain)) total += _charWidth(segment);
|
|
324
|
+
} else {
|
|
325
|
+
for (const char of plain) total += _charWidth(char);
|
|
326
|
+
}
|
|
286
327
|
return total;
|
|
287
328
|
};
|
|
288
329
|
|
|
330
|
+
/**
|
|
331
|
+
* @param {string} text
|
|
332
|
+
* @param {number} columns
|
|
333
|
+
* @param {{ ellipsis?: string }} [options]
|
|
334
|
+
* @returns {string}
|
|
335
|
+
*/
|
|
336
|
+
const truncate = (text, columns, options = {}) => {
|
|
337
|
+
const value = typeof text === "string" ? text : String(text);
|
|
338
|
+
const max = Number.isFinite(columns) ? Math.max(0, Math.floor(columns)) : Infinity;
|
|
339
|
+
if (width(value) <= max) return value;
|
|
340
|
+
const mark = options.ellipsis ?? "…";
|
|
341
|
+
const ellipsis = width(mark) <= max ? mark : "";
|
|
342
|
+
const room = max - width(ellipsis);
|
|
343
|
+
if (_PLAIN.test(value)) return value.slice(0, room) + ellipsis;
|
|
344
|
+
let out = "";
|
|
345
|
+
let used = 0;
|
|
346
|
+
let cut = false;
|
|
347
|
+
const take = (/** @type {string} */ plain) => {
|
|
348
|
+
if (cut || !plain) return;
|
|
349
|
+
if (_PLAIN.test(plain) && used + plain.length <= room) {
|
|
350
|
+
out += plain;
|
|
351
|
+
used += plain.length;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
for (const { segment } of segmenter().segment(plain)) {
|
|
355
|
+
const columnsTaken = _charWidth(segment);
|
|
356
|
+
if (used + columnsTaken > room) {
|
|
357
|
+
cut = true;
|
|
358
|
+
out += ellipsis;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
out += segment;
|
|
362
|
+
used += columnsTaken;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
let last = 0;
|
|
366
|
+
// Escape codes past the cut are kept: they are what closes the styles opened before it.
|
|
367
|
+
for (const match of value.matchAll(_ANSI_REGEX)) {
|
|
368
|
+
take(value.slice(last, match.index));
|
|
369
|
+
out += match[0];
|
|
370
|
+
last = match.index + match[0].length;
|
|
371
|
+
}
|
|
372
|
+
take(value.slice(last));
|
|
373
|
+
return out;
|
|
374
|
+
};
|
|
375
|
+
|
|
289
376
|
const _supportsHyperlinks = () => {
|
|
290
377
|
const env = process.env;
|
|
291
378
|
if (env.FORCE_HYPERLINK === "1") return true;
|
|
@@ -297,6 +384,13 @@ const _supportsHyperlinks = () => {
|
|
|
297
384
|
return false;
|
|
298
385
|
};
|
|
299
386
|
|
|
387
|
+
/** @param {number} value */
|
|
388
|
+
const _byte = (value) => (value > 0 ? (value < 255 ? Math.round(value) : 255) : 0);
|
|
389
|
+
|
|
390
|
+
const _NEWLINE = /\r?\n/g;
|
|
391
|
+
const _STACK = Symbol("stack");
|
|
392
|
+
const _CUSTOM = Symbol("custom");
|
|
393
|
+
|
|
300
394
|
/**
|
|
301
395
|
* Builds an independent color instance.
|
|
302
396
|
* @param {ColorLevel | undefined} forcedLevel
|
|
@@ -305,28 +399,10 @@ const _supportsHyperlinks = () => {
|
|
|
305
399
|
const _createModule = (forcedLevel) => {
|
|
306
400
|
/** @type {ColorLevel | undefined} */
|
|
307
401
|
let level = forcedLevel;
|
|
402
|
+
// Goes up when the level changes, so every chain knows its codes are out of date.
|
|
403
|
+
let generation = 0;
|
|
308
404
|
const currentLevel = () => (level === undefined ? (level = _detect()) : level);
|
|
309
405
|
|
|
310
|
-
/**
|
|
311
|
-
* @param {StyleEntry[]} stack
|
|
312
|
-
* @param {string} text
|
|
313
|
-
* @returns {string}
|
|
314
|
-
*/
|
|
315
|
-
const apply = (stack, text) => {
|
|
316
|
-
const lvl = currentLevel();
|
|
317
|
-
if (lvl === 0 || !stack.length || text === "") return text;
|
|
318
|
-
const resolved = stack.map((entry) => _resolve(entry, lvl));
|
|
319
|
-
let result = text;
|
|
320
|
-
for (const { open, close } of resolved) {
|
|
321
|
-
if (close && result.includes(close)) result = result.split(close).join(close + open);
|
|
322
|
-
}
|
|
323
|
-
const opens = resolved.map((s) => s.open).join("");
|
|
324
|
-
const closes = resolved.map((s) => s.close).reverse().join("");
|
|
325
|
-
if (result.includes("\n")) result = result.replace(/\r?\n/g, (nl) => closes + nl + opens);
|
|
326
|
-
return opens + result + closes;
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
const STACK = Symbol("stack");
|
|
330
406
|
/** @type {any} */
|
|
331
407
|
const proto = Object.create(Function.prototype);
|
|
332
408
|
|
|
@@ -335,40 +411,127 @@ const _createModule = (forcedLevel) => {
|
|
|
335
411
|
* @returns {ColorChain}
|
|
336
412
|
*/
|
|
337
413
|
const chain = (stack) => {
|
|
414
|
+
let seen = -1;
|
|
415
|
+
let open = "";
|
|
416
|
+
let close = "";
|
|
417
|
+
/** @type {Array<{ open: string, close: string }>} */
|
|
418
|
+
let pairs = [];
|
|
419
|
+
const refresh = () => {
|
|
420
|
+
seen = generation;
|
|
421
|
+
open = "";
|
|
422
|
+
close = "";
|
|
423
|
+
pairs = [];
|
|
424
|
+
const current = currentLevel();
|
|
425
|
+
if (current === 0) return;
|
|
426
|
+
for (const entry of stack) {
|
|
427
|
+
const pair = _resolve(entry, current);
|
|
428
|
+
pairs.push(pair);
|
|
429
|
+
open += pair.open;
|
|
430
|
+
close = pair.close + close;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
338
433
|
/** @type {any} */
|
|
339
|
-
const fn = (/** @type {unknown[]} */ ...
|
|
434
|
+
const fn = (/** @type {unknown[]} */ ...parts) => {
|
|
435
|
+
const text = parts.length === 1 ? (typeof parts[0] === "string" ? parts[0] : String(parts[0])) : parts.map((part) => String(part)).join(" ");
|
|
436
|
+
if (seen !== generation) refresh();
|
|
437
|
+
if (open === "" || text === "") return text;
|
|
438
|
+
if (text.indexOf("\u001b") === -1 && text.indexOf("\n") === -1) return open + text + close;
|
|
439
|
+
let result = text;
|
|
440
|
+
// A style closed inside the text would also end ours: open it again right after.
|
|
441
|
+
for (const pair of pairs) {
|
|
442
|
+
if (result.includes(pair.close)) result = result.replaceAll(pair.close, pair.close + pair.open);
|
|
443
|
+
}
|
|
444
|
+
// Styles stop at the end of each line, or a background would run to the edge of the terminal.
|
|
445
|
+
if (result.includes("\n")) result = result.replace(_NEWLINE, (newline) => close + newline + open);
|
|
446
|
+
return open + result + close;
|
|
447
|
+
};
|
|
340
448
|
Object.setPrototypeOf(fn, proto);
|
|
341
|
-
fn[
|
|
449
|
+
fn[_STACK] = stack;
|
|
342
450
|
return fn;
|
|
343
451
|
};
|
|
344
452
|
|
|
345
|
-
|
|
453
|
+
// Each chain is built the first time it's read, then kept on its parent:
|
|
454
|
+
// `color.red.bold` costs two property reads from then on.
|
|
455
|
+
for (const name of Object.keys(_STYLES)) {
|
|
346
456
|
Object.defineProperty(proto, name, {
|
|
347
457
|
get() {
|
|
348
|
-
|
|
458
|
+
const next = chain([...this[_STACK], _STYLES[name]]);
|
|
459
|
+
Object.defineProperty(this, name, { value: next });
|
|
460
|
+
return next;
|
|
349
461
|
},
|
|
350
462
|
});
|
|
351
463
|
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @param {any} parent
|
|
467
|
+
* @param {string} key
|
|
468
|
+
* @param {() => StyleEntry} entry
|
|
469
|
+
* @returns {ColorChain}
|
|
470
|
+
*/
|
|
471
|
+
const custom = (parent, key, entry) => {
|
|
472
|
+
/** @type {Map<string, ColorChain>} */
|
|
473
|
+
const known = (parent[_CUSTOM] ??= new Map());
|
|
474
|
+
let next = known.get(key);
|
|
475
|
+
if (!next) {
|
|
476
|
+
if (known.size >= 256) known.clear();
|
|
477
|
+
known.set(key, (next = chain([...parent[_STACK], entry()])));
|
|
478
|
+
}
|
|
479
|
+
return next;
|
|
480
|
+
};
|
|
352
481
|
/** @type {Record<string, (this: any, ...args: any[]) => ColorChain>} */
|
|
353
482
|
const methods = {
|
|
354
|
-
rgb(r, g, b) { return
|
|
355
|
-
bgRgb(r, g, b) { return
|
|
356
|
-
hex(hex) { return
|
|
357
|
-
bgHex(hex) { return
|
|
358
|
-
ansi256(code) { return
|
|
359
|
-
bgAnsi256(code) { return
|
|
483
|
+
rgb(r, g, b) { return custom(this, `f${r},${g},${b}`, () => ({ rgb: [_byte(r), _byte(g), _byte(b)], bg: false })); },
|
|
484
|
+
bgRgb(r, g, b) { return custom(this, `b${r},${g},${b}`, () => ({ rgb: [_byte(r), _byte(g), _byte(b)], bg: true })); },
|
|
485
|
+
hex(hex) { return custom(this, `F${hex}`, () => ({ rgb: _hexToRgb(hex), bg: false })); },
|
|
486
|
+
bgHex(hex) { return custom(this, `B${hex}`, () => ({ rgb: _hexToRgb(hex), bg: true })); },
|
|
487
|
+
ansi256(code) { return custom(this, `a${code}`, () => ({ ansi256: _byte(code), bg: false })); },
|
|
488
|
+
bgAnsi256(code) { return custom(this, `A${code}`, () => ({ ansi256: _byte(code), bg: true })); },
|
|
360
489
|
};
|
|
361
490
|
for (const [name, method] of Object.entries(methods)) Object.defineProperty(proto, name, { value: method });
|
|
362
491
|
|
|
492
|
+
/**
|
|
493
|
+
* @param {string} text
|
|
494
|
+
* @param {string[]} colors
|
|
495
|
+
* @returns {string}
|
|
496
|
+
*/
|
|
497
|
+
const gradient = (text, colors) => {
|
|
498
|
+
const value = String(text);
|
|
499
|
+
const stops = colors.map(_hexToRgb);
|
|
500
|
+
const current = currentLevel();
|
|
501
|
+
if (!stops.length || current === 0) return value;
|
|
502
|
+
const letters = graphemes(value);
|
|
503
|
+
let visible = 0;
|
|
504
|
+
for (const letter of letters) if (letter.trim()) visible++;
|
|
505
|
+
const last = stops.length - 1;
|
|
506
|
+
let index = 0;
|
|
507
|
+
let out = "";
|
|
508
|
+
for (const letter of letters) {
|
|
509
|
+
if (!letter.trim()) {
|
|
510
|
+
out += letter;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const position = visible <= 1 ? 0 : (index++ / (visible - 1)) * last;
|
|
514
|
+
const from = Math.min(Math.floor(position), Math.max(0, last - 1));
|
|
515
|
+
const a = stops[from];
|
|
516
|
+
const b = stops[Math.min(from + 1, last)];
|
|
517
|
+
const share = position - from;
|
|
518
|
+
const pair = _resolve({ rgb: [Math.round(a[0] + (b[0] - a[0]) * share), Math.round(a[1] + (b[1] - a[1]) * share), Math.round(a[2] + (b[2] - a[2]) * share)], bg: false }, current);
|
|
519
|
+
out += pair.open + letter + pair.close;
|
|
520
|
+
}
|
|
521
|
+
return out;
|
|
522
|
+
};
|
|
523
|
+
|
|
363
524
|
/** @type {any} */
|
|
364
525
|
const root = Object.create(proto);
|
|
365
|
-
root[
|
|
526
|
+
root[_STACK] = [];
|
|
366
527
|
Object.defineProperties(root, {
|
|
367
528
|
level: {
|
|
368
529
|
enumerable: true,
|
|
369
530
|
get: currentLevel,
|
|
370
|
-
set(/** @type {ColorLevel} */ value) {
|
|
371
|
-
|
|
531
|
+
set(/** @type {ColorLevel | undefined} */ value) {
|
|
532
|
+
// `undefined` goes back to what the terminal supports.
|
|
533
|
+
level = value === undefined ? undefined : /** @type {ColorLevel} */ (Number.isFinite(value) ? Math.max(0, Math.min(3, Math.floor(value))) : 0);
|
|
534
|
+
generation++;
|
|
372
535
|
},
|
|
373
536
|
},
|
|
374
537
|
enabled: { enumerable: true, get: () => currentLevel() > 0 },
|
|
@@ -376,34 +539,13 @@ const _createModule = (forcedLevel) => {
|
|
|
376
539
|
detect: { enumerable: true, value: _detect },
|
|
377
540
|
strip: { enumerable: true, value: strip },
|
|
378
541
|
width: { enumerable: true, value: width },
|
|
542
|
+
truncate: { enumerable: true, value: truncate },
|
|
379
543
|
link: {
|
|
380
544
|
enumerable: true,
|
|
381
545
|
value: (/** @type {string} */ text, /** @type {string} */ url) =>
|
|
382
546
|
currentLevel() > 0 && _supportsHyperlinks() ? `\u001b]8;;${url}\u0007${text}\u001b]8;;\u0007` : `${text} (${url})`,
|
|
383
547
|
},
|
|
384
|
-
gradient: {
|
|
385
|
-
enumerable: true,
|
|
386
|
-
value: (/** @type {string} */ text, /** @type {string[]} */ colors) => {
|
|
387
|
-
const stops = colors.map(_hexToRgb);
|
|
388
|
-
if (!stops.length) return String(text);
|
|
389
|
-
const graphemes = Array.from(_graphemeSegmenter().segment(String(text)), (s) => s.segment);
|
|
390
|
-
const visible = graphemes.filter((g) => g.trim()).length;
|
|
391
|
-
let index = 0;
|
|
392
|
-
return graphemes
|
|
393
|
-
.map((g) => {
|
|
394
|
-
if (!g.trim()) return g;
|
|
395
|
-
const t = visible <= 1 ? 0 : index++ / (visible - 1);
|
|
396
|
-
const pos = t * (stops.length - 1);
|
|
397
|
-
const i = Math.min(Math.floor(pos), stops.length - 2 < 0 ? 0 : stops.length - 2);
|
|
398
|
-
const a = stops[i];
|
|
399
|
-
const b = stops[Math.min(i + 1, stops.length - 1)];
|
|
400
|
-
const f = pos - i;
|
|
401
|
-
const mix = /** @type {[number, number, number]} */ (a.map((v, k) => Math.round(v + (b[k] - v) * f)));
|
|
402
|
-
return apply([{ rgb: mix, bg: false }], g);
|
|
403
|
-
})
|
|
404
|
-
.join("");
|
|
405
|
-
},
|
|
406
|
-
},
|
|
548
|
+
gradient: { enumerable: true, value: gradient },
|
|
407
549
|
});
|
|
408
550
|
return root;
|
|
409
551
|
};
|
|
@@ -412,4 +554,3 @@ const _createModule = (forcedLevel) => {
|
|
|
412
554
|
const color = _createModule(undefined);
|
|
413
555
|
|
|
414
556
|
module.exports = color;
|
|
415
|
-
|