@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +32 -30
  3. package/index.js +3 -3
  4. package/package.json +8 -7
  5. package/src/Arr.js +222 -58
  6. package/src/Async.js +322 -90
  7. package/src/Cache.js +148 -44
  8. package/src/Checker.js +257 -26
  9. package/src/Cli.js +404 -145
  10. package/src/Color.js +224 -83
  11. package/src/Crypto.js +481 -106
  12. package/src/Emitter.js +104 -37
  13. package/src/Env.js +176 -42
  14. package/src/FS.js +203 -60
  15. package/src/Func.js +226 -76
  16. package/src/Http.js +311 -149
  17. package/src/Id.js +281 -96
  18. package/src/Logger.js +302 -78
  19. package/src/Num.js +182 -60
  20. package/src/Obj.js +340 -125
  21. package/src/SQLite.js +426 -105
  22. package/src/Schema.js +602 -87
  23. package/src/Str.js +316 -86
  24. package/src/Sys.js +197 -54
  25. package/src/Time.js +572 -317
  26. package/src/Utils.js +62 -41
  27. package/src/errors.js +54 -11
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +59 -17
  30. package/types/src/Arr.d.ts +78 -0
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +52 -1
  33. package/types/src/Async.d.ts.map +1 -1
  34. package/types/src/Cache.d.ts +33 -7
  35. package/types/src/Cache.d.ts.map +1 -1
  36. package/types/src/Checker.d.ts +155 -2
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +97 -15
  39. package/types/src/Cli.d.ts.map +1 -1
  40. package/types/src/Color.d.ts +7 -1
  41. package/types/src/Color.d.ts.map +1 -1
  42. package/types/src/Crypto.d.ts +99 -13
  43. package/types/src/Crypto.d.ts.map +1 -1
  44. package/types/src/Emitter.d.ts +36 -17
  45. package/types/src/Emitter.d.ts.map +1 -1
  46. package/types/src/Env.d.ts +31 -1
  47. package/types/src/Env.d.ts.map +1 -1
  48. package/types/src/FS.d.ts +36 -5
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +147 -19
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +113 -43
  53. package/types/src/Http.d.ts.map +1 -1
  54. package/types/src/Id.d.ts +60 -8
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +53 -2
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +48 -7
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +67 -4
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +123 -14
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +256 -7
  65. package/types/src/Schema.d.ts.map +1 -1
  66. package/types/src/Str.d.ts +49 -1
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +51 -6
  69. package/types/src/Sys.d.ts.map +1 -1
  70. package/types/src/Time.d.ts +130 -34
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +15 -11
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +27 -3
  75. package/types/src/errors.d.ts.map +1 -1
  76. package/types/src/internal/shared.d.ts +123 -0
  77. 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; `0` turns colors off.
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 argv = process.argv;
224
- if (argv.includes("--no-color") || argv.includes("--no-colors") || argv.includes("--color=false")) return 0;
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 (argv.includes("--color=16m") || argv.includes("--color=full") || argv.includes("--color=truecolor")) return 3;
235
- if (argv.includes("--color=256")) return 2;
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 (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((name) => name in env)) return 1;
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 (/^\p{Mark}+$/u.test(grapheme)) return 0;
262
- if (/\p{Emoji_Presentation}/u.test(grapheme) || grapheme.includes("\ufe0f")) return 2;
263
- if (/[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩\u{20000}-\u{3FFFD}]/u.test(grapheme)) return 2;
264
- return 1;
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) => String(text).replace(_ANSI_REGEX, "");
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
- if (/^[\x20-\x7e]*$/.test(plain)) return plain.length;
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
- for (const { segment } of _graphemeSegmenter().segment(plain)) total += _charWidth(segment);
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[]} */ ...text) => apply(stack, text.map((t) => String(t)).join(" "));
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[STACK] = stack;
449
+ fn[_STACK] = stack;
342
450
  return fn;
343
451
  };
344
452
 
345
- for (const [name, [open, close]] of Object.entries(_CODES)) {
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
- return chain([...(this[STACK] ?? []), { open: `\u001b[${open}m`, close: `\u001b[${close}m` }]);
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 chain([...(this[STACK] ?? []), { rgb: [r, g, b], bg: false }]); },
355
- bgRgb(r, g, b) { return chain([...(this[STACK] ?? []), { rgb: [r, g, b], bg: true }]); },
356
- hex(hex) { return chain([...(this[STACK] ?? []), { rgb: _hexToRgb(hex), bg: false }]); },
357
- bgHex(hex) { return chain([...(this[STACK] ?? []), { rgb: _hexToRgb(hex), bg: true }]); },
358
- ansi256(code) { return chain([...(this[STACK] ?? []), { ansi256: code, bg: false }]); },
359
- bgAnsi256(code) { return chain([...(this[STACK] ?? []), { ansi256: code, bg: true }]); },
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[STACK] = [];
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
- level = /** @type {ColorLevel} */ (Math.max(0, Math.min(3, Math.floor(value))));
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
-