@mmutils/logger 0.0.0-stage → 0.1.1

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1038 -2
  3. package/dist/cjs/index.cjs +72 -0
  4. package/dist/cjs/index.cjs.map +1 -0
  5. package/dist/cjs/levels.cjs +82 -0
  6. package/dist/cjs/levels.cjs.map +1 -0
  7. package/dist/cjs/logger.cjs +576 -0
  8. package/dist/cjs/logger.cjs.map +1 -0
  9. package/dist/cjs/outputs.cjs +24 -0
  10. package/dist/cjs/outputs.cjs.map +1 -0
  11. package/dist/cjs/plugins/columns.cjs +782 -0
  12. package/dist/cjs/plugins/columns.cjs.map +1 -0
  13. package/dist/cjs/plugins/graph.cjs +578 -0
  14. package/dist/cjs/plugins/graph.cjs.map +1 -0
  15. package/dist/cjs/plugins/json.cjs +644 -0
  16. package/dist/cjs/plugins/json.cjs.map +1 -0
  17. package/dist/cjs/plugins/message.cjs +108 -0
  18. package/dist/cjs/plugins/message.cjs.map +1 -0
  19. package/dist/cjs/plugins/shared.cjs +194 -0
  20. package/dist/cjs/plugins/shared.cjs.map +1 -0
  21. package/dist/cjs/time.cjs +234 -0
  22. package/dist/cjs/time.cjs.map +1 -0
  23. package/dist/cjs/utils/text.cjs +646 -0
  24. package/dist/cjs/utils/text.cjs.map +1 -0
  25. package/dist/esm/index.js +11 -0
  26. package/dist/esm/index.js.map +1 -0
  27. package/dist/esm/levels.js +72 -0
  28. package/dist/esm/levels.js.map +1 -0
  29. package/dist/esm/logger.js +572 -0
  30. package/dist/esm/logger.js.map +1 -0
  31. package/dist/esm/outputs.js +21 -0
  32. package/dist/esm/outputs.js.map +1 -0
  33. package/dist/esm/plugins/columns.js +778 -0
  34. package/dist/esm/plugins/columns.js.map +1 -0
  35. package/dist/esm/plugins/graph.js +572 -0
  36. package/dist/esm/plugins/graph.js.map +1 -0
  37. package/dist/esm/plugins/json.js +641 -0
  38. package/dist/esm/plugins/json.js.map +1 -0
  39. package/dist/esm/plugins/message.js +105 -0
  40. package/dist/esm/plugins/message.js.map +1 -0
  41. package/dist/esm/plugins/shared.js +187 -0
  42. package/dist/esm/plugins/shared.js.map +1 -0
  43. package/dist/esm/time.js +231 -0
  44. package/dist/esm/time.js.map +1 -0
  45. package/dist/esm/utils/text.js +621 -0
  46. package/dist/esm/utils/text.js.map +1 -0
  47. package/dist/types/index.d.ts +11 -0
  48. package/dist/types/levels.d.ts +50 -0
  49. package/dist/types/logger.d.ts +245 -0
  50. package/dist/types/outputs.d.ts +12 -0
  51. package/dist/types/plugins/columns.d.ts +151 -0
  52. package/dist/types/plugins/graph.d.ts +243 -0
  53. package/dist/types/plugins/json.d.ts +169 -0
  54. package/dist/types/plugins/message.d.ts +181 -0
  55. package/dist/types/plugins/shared.d.ts +74 -0
  56. package/dist/types/time.d.ts +48 -0
  57. package/dist/types/types.d.ts +176 -0
  58. package/dist/types/utils/text.d.ts +231 -0
  59. package/package.json +85 -4
@@ -0,0 +1,621 @@
1
+ /**
2
+ * Centers `text` by padding it left and right. Text longer than `length` is
3
+ * returned unchanged.
4
+ */
5
+ const padTextMiddle = (text, length, padString = ' ') => text
6
+ .padStart(text.length + Math.floor((length - text.length) / 2), padString)
7
+ .padEnd(length, padString);
8
+ /**
9
+ * Justifies two strings to the left and right of a column. If the column is
10
+ * too narrow, each text is placed on its own line.
11
+ */
12
+ const justifyLeftRight = (leftText, rightText, columnWidth) => columnWidth < leftText.length + rightText.length ?
13
+ `${leftText}\n${rightText}`
14
+ : `${leftText.padEnd(columnWidth - rightText.length)}${rightText}`;
15
+ /* eslint-disable no-control-regex -- matching control characters is the point */
16
+ /**
17
+ * SGR (color and text style) sequence.
18
+ */
19
+ const SGR = /\x1B\[[0-9;:]*m/g;
20
+ /* eslint-enable no-control-regex */
21
+ const SGR_RESET = '\x1b[0m';
22
+ const INDENT = ' ';
23
+ /**
24
+ * Source of a character class with the control characters to replace.
25
+ *
26
+ * @param newlines Include `\n`.
27
+ * @param esc Include ESC.
28
+ */
29
+ const controlClass = (newlines, esc) => `[\\x00-\\x08${newlines ? '\\x0A' : ''}\\x0B-${esc ? '\\x1F' : '\\x1A\\x1C-\\x1F'}\\x7F-\\x9F]`;
30
+ /**
31
+ * Replaces every `\n`. Faster than `replaceAll`, which matters for multi-line
32
+ * messages.
33
+ */
34
+ const replaceNewlines = (text, newline) => {
35
+ let index = text.indexOf('\n');
36
+ if (index === -1)
37
+ return text;
38
+ let result = '';
39
+ let start = 0;
40
+ while (index !== -1) {
41
+ result += `${text.slice(start, index)}${newline}`;
42
+ start = index + 1;
43
+ index = text.indexOf('\n', start);
44
+ }
45
+ return `${result}${text.slice(start)}`;
46
+ };
47
+ const toHexEscape = (char) => `\\x${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`;
48
+ /**
49
+ * Builds the regular expressions once and returns the escape function. Clean
50
+ * text costs one regex scan. Text with only kept colors and newlines is
51
+ * handled natively; the replacer runs only for other control characters.
52
+ */
53
+ const createEscape = (keepColors, newlines) => {
54
+ const matchNewlines = newlines !== 'keep';
55
+ const control = controlClass(matchNewlines, true);
56
+ const test = new RegExp(control);
57
+ // control characters the native fast path can't handle
58
+ const unsafe = new RegExp(keepColors ?
59
+ `${controlClass(false, false)}|\\x1B(?!\\[[0-9;:]*m)`
60
+ : controlClass(false, true));
61
+ const newline = newlines === 'indent' ? `\n${INDENT}` : '\\x0A';
62
+ const replaceControl = (char) => char === '\n' ? newline : toHexEscape(char);
63
+ if (!keepColors) {
64
+ const controls = new RegExp(control, 'g');
65
+ return (text) => {
66
+ if (!test.test(text))
67
+ return text;
68
+ // only newlines
69
+ if (!unsafe.test(text))
70
+ return replaceNewlines(text, newline);
71
+ return text.replace(controls, replaceControl);
72
+ };
73
+ }
74
+ const sgrOrControls = new RegExp(`${SGR.source}|${control}`, 'g');
75
+ return (text) => {
76
+ if (!test.test(text))
77
+ return text;
78
+ // only colors and newlines, e.g. multi-line text with baked styles
79
+ if (!unsafe.test(text)) {
80
+ const result = matchNewlines ? replaceNewlines(text, newline) : text;
81
+ return result.includes('\x1b') ? `${result}${SGR_RESET}` : result;
82
+ }
83
+ // set by the replacer, which control flow analysis doesn't follow
84
+ let colored = false;
85
+ const escaped = text.replace(sgrOrControls, (match) => {
86
+ if (match.length === 1)
87
+ return replaceControl(match);
88
+ colored = true;
89
+ return match;
90
+ });
91
+ return colored ? `${escaped}${SGR_RESET}` : escaped;
92
+ };
93
+ };
94
+ const keepText = (text) => text;
95
+ const NO_SANITIZER = { text: keepText, label: keepText };
96
+ /**
97
+ * Creates the sanitizing functions of a given setting. Create it once (e.g. in
98
+ * a plugin factory) and reuse it for every log.
99
+ *
100
+ * @param sanitize Preset or custom rules. Default: `'safe'`.
101
+ */
102
+ const createSanitizer = (sanitize = 'safe') => {
103
+ if (sanitize === 'none')
104
+ return NO_SANITIZER;
105
+ const rules = sanitize === 'safe' ? {}
106
+ : sanitize === 'strict' ? { colors: 'escape', newlines: 'escape' }
107
+ : sanitize;
108
+ const { colors = 'keep', newlines = 'indent' } = rules;
109
+ // the functions keep no state, so equal rules share them and their regexes
110
+ const key = `${colors}:${newlines}`;
111
+ const cached = sanitizers.get(key);
112
+ if (cached !== undefined)
113
+ return cached;
114
+ const label = createEscape(colors === 'keep', 'escape');
115
+ const sanitizer = {
116
+ text: newlines === 'escape' ? label : createEscape(colors === 'keep', newlines),
117
+ label,
118
+ };
119
+ sanitizers.set(key, sanitizer);
120
+ return sanitizer;
121
+ };
122
+ /**
123
+ * Sanitizers by resolved rules: building one compiles several regular
124
+ * expressions (about 2 µs).
125
+ */
126
+ const sanitizers = new Map();
127
+ /**
128
+ * Length of text without SGR sequences: the columns it takes on a terminal,
129
+ * for single-width characters. Sanitized text contains no other sequences.
130
+ * `textWidth` also counts wide characters as two columns.
131
+ */
132
+ const visibleLength = (text) => {
133
+ if (!text.includes('\x1b'))
134
+ return text.length;
135
+ let length = text.length;
136
+ for (const [sequence] of text.matchAll(SGR))
137
+ length -= sequence.length;
138
+ return length;
139
+ };
140
+ /* eslint-disable no-control-regex -- matching control characters is the point */
141
+ const SGR_AT = /\x1B\[[0-9;:]*m/y;
142
+ /* eslint-enable no-control-regex */
143
+ /**
144
+ * Characters that are not one terminal column wide, escape codes and tabs:
145
+ * lines without them are wrapped by slicing. Box drawing and block
146
+ * characters (graphs) are not in it.
147
+ */
148
+ /* eslint-disable no-misleading-character-class -- ranges of combining marks, matched one code unit at a time on purpose */
149
+ const SLOW_CHARS = new RegExp('[\\x00-\\x1f\\x7f-\\x9f\\u0300-\\u036f\\u1100-\\u115f\\u200b-\\u200f' +
150
+ '\\u231a-\\u23f3\\u25fd-\\u27bf\\u2b1b-\\u2b55\\u2e80-\\ua4cf' +
151
+ '\\ua960-\\ua97f\\uac00-\\ud7a3\\ud800-\\udfff\\uf900-\\ufaff' +
152
+ '\\ufe00-\\ufe6f\\uff00-\\uff60\\uffe0-\\uffe6]');
153
+ /* eslint-enable no-misleading-character-class */
154
+ /**
155
+ * East Asian Wide and Fullwidth ranges and emoji shown as two columns, as
156
+ * `first, last` pairs in ascending order. An approximation: emoji
157
+ * sequences (ZWJ, flags) are measured per code point.
158
+ */
159
+ const WIDE_RANGES = [
160
+ 0x1100, 0x115f, 0x231a, 0x231b, 0x2329, 0x232a, 0x23e9, 0x23ec, 0x23f0,
161
+ 0x23f0, 0x23f3, 0x23f3, 0x25fd, 0x25fe, 0x2614, 0x2615, 0x2648, 0x2653,
162
+ 0x267f, 0x267f, 0x2693, 0x2693, 0x26a1, 0x26a1, 0x26aa, 0x26ab, 0x26bd,
163
+ 0x26be, 0x26c4, 0x26c5, 0x26ce, 0x26ce, 0x26d4, 0x26d4, 0x26ea, 0x26ea,
164
+ 0x26f2, 0x26f3, 0x26f5, 0x26f5, 0x26fa, 0x26fa, 0x26fd, 0x26fd, 0x2705,
165
+ 0x2705, 0x270a, 0x270b, 0x2728, 0x2728, 0x274c, 0x274c, 0x274e, 0x274e,
166
+ 0x2753, 0x2755, 0x2757, 0x2757, 0x2795, 0x2797, 0x27b0, 0x27b0, 0x27bf,
167
+ 0x27bf, 0x2b1b, 0x2b1c, 0x2b50, 0x2b50, 0x2b55, 0x2b55, 0x2e80, 0x303e,
168
+ 0x3041, 0x33ff, 0x3400, 0x4dbf, 0x4e00, 0x9fff, 0xa000, 0xa4cf, 0xa960,
169
+ 0xa97f, 0xac00, 0xd7a3, 0xf900, 0xfaff, 0xfe10, 0xfe19, 0xfe30, 0xfe6f,
170
+ 0xff00, 0xff60, 0xffe0, 0xffe6, 0x16fe0, 0x18aff, 0x1b000, 0x1b2ff, 0x1f004,
171
+ 0x1f004, 0x1f0cf, 0x1f0cf, 0x1f18e, 0x1f18e, 0x1f191, 0x1f19a, 0x1f200,
172
+ 0x1f251, 0x1f300, 0x1f320, 0x1f32d, 0x1f335, 0x1f337, 0x1f37c, 0x1f37e,
173
+ 0x1f393, 0x1f3a0, 0x1f3ca, 0x1f3cf, 0x1f3d3, 0x1f3e0, 0x1f3f0, 0x1f3f4,
174
+ 0x1f3f4, 0x1f3f8, 0x1f43e, 0x1f440, 0x1f440, 0x1f442, 0x1f4fc, 0x1f4ff,
175
+ 0x1f53d, 0x1f54b, 0x1f54e, 0x1f550, 0x1f567, 0x1f57a, 0x1f57a, 0x1f595,
176
+ 0x1f596, 0x1f5a4, 0x1f5a4, 0x1f5fb, 0x1f64f, 0x1f680, 0x1f6c5, 0x1f6cc,
177
+ 0x1f6cc, 0x1f6d0, 0x1f6d2, 0x1f6d5, 0x1f6d7, 0x1f6eb, 0x1f6ec, 0x1f6f4,
178
+ 0x1f6fc, 0x1f7e0, 0x1f7eb, 0x1f90c, 0x1f93a, 0x1f93c, 0x1f945, 0x1f947,
179
+ 0x1f9ff, 0x1fa70, 0x1faff, 0x20000, 0x3fffd,
180
+ ];
181
+ const isWide = (codePoint) => {
182
+ // binary search over the ranges
183
+ let low = 0;
184
+ let high = WIDE_RANGES.length / 2 - 1;
185
+ while (low <= high) {
186
+ const middle = (low + high) >> 1;
187
+ if (codePoint < (WIDE_RANGES[middle * 2] ?? 0))
188
+ high = middle - 1;
189
+ else if (codePoint > (WIDE_RANGES[middle * 2 + 1] ?? 0))
190
+ low = middle + 1;
191
+ else
192
+ return true;
193
+ }
194
+ return false;
195
+ };
196
+ const isZeroWidth = (codePoint) => codePoint < 0x20 ||
197
+ (codePoint >= 0x7f && codePoint <= 0x9f) ||
198
+ (codePoint >= 0x300 && codePoint <= 0x36f) ||
199
+ (codePoint >= 0x200b && codePoint <= 0x200f) ||
200
+ (codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
201
+ (codePoint >= 0x1f3fb && codePoint <= 0x1f3ff) ||
202
+ (codePoint >= 0xe0100 && codePoint <= 0xe01ef);
203
+ /**
204
+ * Terminal columns a code point takes: 0 for controls, combining marks,
205
+ * zero-width characters, variation selectors and skin tones, 2 for East
206
+ * Asian wide characters and most emoji, 1 otherwise.
207
+ */
208
+ const charWidth = (codePoint) => isZeroWidth(codePoint) ? 0
209
+ : isWide(codePoint) ? 2
210
+ : 1;
211
+ /**
212
+ * Terminal columns a text takes: SGR sequences take none, wide characters
213
+ * two. Tabs count as one column; `wrapText` expands them.
214
+ */
215
+ const textWidth = (text) => {
216
+ if (!SLOW_CHARS.test(text))
217
+ return text.length;
218
+ let width = 0;
219
+ for (let index = 0; index < text.length;) {
220
+ if (text.charCodeAt(index) === 0x1b) {
221
+ SGR_AT.lastIndex = index;
222
+ const sgr = SGR_AT.exec(text);
223
+ if (sgr !== null) {
224
+ index += sgr[0].length;
225
+ continue;
226
+ }
227
+ }
228
+ const codePoint = text.codePointAt(index) ?? 0;
229
+ width += codePoint === 0x09 ? 1 : charWidth(codePoint);
230
+ index += codePoint > 0xffff ? 2 : 1;
231
+ }
232
+ return width;
233
+ };
234
+ /**
235
+ * Describes a thrown value as `Name: message`. Never throws, even for thrown
236
+ * values with throwing getters or revoked proxies.
237
+ */
238
+ const describeThrown = (thrown) => {
239
+ try {
240
+ return thrown instanceof Error ?
241
+ `${thrown.name}: ${thrown.message}`
242
+ : String(thrown);
243
+ }
244
+ catch {
245
+ return 'unknown error';
246
+ }
247
+ };
248
+ /**
249
+ * Splits an error to its `name: message` head, which can span multiple
250
+ * lines, and its stack frames. Never throws: a head or stack that can't be
251
+ * read (throwing getters, `Error.prepareStackTrace`) becomes a placeholder.
252
+ */
253
+ const splitError = (error) => {
254
+ let head;
255
+ try {
256
+ head = String(error);
257
+ }
258
+ catch (thrown) {
259
+ head = `[Unreadable error: ${describeThrown(thrown)}]`;
260
+ }
261
+ let stack;
262
+ try {
263
+ stack = error.stack;
264
+ }
265
+ catch (thrown) {
266
+ return [head, [`[Unreadable stack: ${describeThrown(thrown)}]`]];
267
+ }
268
+ if (typeof stack !== 'string')
269
+ return [head, []];
270
+ const frames = stack.startsWith(head) ? stack.slice(head.length) : stack;
271
+ return [head, frames.split('\n').filter(Boolean)];
272
+ };
273
+ const DEFAULT_OUTPUT_LIMITS = {
274
+ items: 100,
275
+ stringLength: 10_000,
276
+ };
277
+ /**
278
+ * No output limits: the default of the text utilities, which only truncate
279
+ * when given limits.
280
+ */
281
+ const NO_OUTPUT_LIMITS = {
282
+ items: Infinity,
283
+ stringLength: Infinity,
284
+ };
285
+ /**
286
+ * Fills in the default output limits and validates them.
287
+ *
288
+ * @param what Prefix of error messages, e.g. `messagePlugin`.
289
+ * @throws RangeError if a limit is not a positive integer or `Infinity`.
290
+ */
291
+ const resolveOutputLimits = (limits, what) => {
292
+ const resolved = {
293
+ items: limits?.items ?? DEFAULT_OUTPUT_LIMITS.items,
294
+ stringLength: limits?.stringLength ?? DEFAULT_OUTPUT_LIMITS.stringLength,
295
+ };
296
+ for (const [name, value] of Object.entries(resolved))
297
+ if (value !== Infinity && !(Number.isInteger(value) && value >= 1))
298
+ throw new RangeError(`${what}: limits.${name} must be a positive integer or Infinity, got ${String(value)}.`);
299
+ return resolved;
300
+ };
301
+ /**
302
+ * Note on what output limits left out: `… 3 more items`, `… 1 more item`.
303
+ */
304
+ const describeLeftOut = (count, unit) => `… ${String(count)} more ${count === 1 ? unit
305
+ : unit === 'entry' ? 'entries'
306
+ : `${unit}s`}`;
307
+ /**
308
+ * Cuts text to `maxLength` characters, never inside a surrogate pair, and
309
+ * tells how many were left out: `xxx… 1000 more characters`. Shorter text is
310
+ * returned as is.
311
+ */
312
+ const truncate = (text, maxLength) => {
313
+ if (text.length <= maxLength)
314
+ return text;
315
+ const end = cutIndex(text, maxLength);
316
+ return `${text.slice(0, end)}${describeLeftOut(text.length - end, 'character')}`;
317
+ };
318
+ /**
319
+ * Where to cut text longer than `maxLength`, so a surrogate pair stays whole.
320
+ */
321
+ const cutIndex = (text, maxLength) => {
322
+ const code = text.charCodeAt(maxLength - 1);
323
+ return code >= 0xd800 && code <= 0xdbff ? maxLength - 1 : maxLength;
324
+ };
325
+ /**
326
+ * Keys of an object to render, at most `items`. Large arrays are indexed
327
+ * without listing all their keys first.
328
+ *
329
+ * @returns The keys and the number of keys left out.
330
+ */
331
+ const limitKeys = (data, items) => {
332
+ if (Array.isArray(data) && data.length > items) {
333
+ const keys = [];
334
+ for (let index = 0; index < items; index++)
335
+ keys.push(String(index));
336
+ return [keys, data.length - items];
337
+ }
338
+ const keys = Object.keys(data);
339
+ return keys.length > items ?
340
+ [keys.slice(0, items), keys.length - items]
341
+ : [keys, 0];
342
+ };
343
+ /**
344
+ * JSON of an object property. Never throws: a getter that throws gives a
345
+ * placeholder, so one bad property doesn't hide the others.
346
+ */
347
+ const stringifyProperty = (data, key, limits = NO_OUTPUT_LIMITS) => {
348
+ let value;
349
+ try {
350
+ value = data[key];
351
+ }
352
+ catch (thrown) {
353
+ return `[Getter threw: ${describeThrown(thrown)}]`;
354
+ }
355
+ return safeStringify(value, limits);
356
+ };
357
+ /**
358
+ * Short description of a function, like `util.inspect`: `[Function: name]`
359
+ * or `[class Name]`, never its source code.
360
+ */
361
+ const describeFunction = (fn) => {
362
+ const { name } = fn;
363
+ const label = typeof name === 'string' && name ? name : '(anonymous)';
364
+ // V8 returns a slice of the cached source: checking the prefix copies nothing
365
+ return Function.prototype.toString.call(fn).startsWith('class') ?
366
+ `[class ${label}]`
367
+ : `[Function: ${label}]`;
368
+ };
369
+ /**
370
+ * Describes buffers and typed arrays in one line, like `util.inspect`:
371
+ * `<Buffer 68 69 … 998 more bytes>`, `Uint8Array(3) [1, 2, 3]`. Their JSON
372
+ * (and their keys) would list every element.
373
+ */
374
+ const describeBinary = (view, items) => {
375
+ if (view instanceof DataView)
376
+ return `DataView(${String(view.byteLength)})`;
377
+ const elements = view;
378
+ const { name } = view.constructor ?? {};
379
+ const isBuffer = name === 'Buffer';
380
+ const shown = Math.min(elements.length, items);
381
+ let text = '';
382
+ for (let index = 0; index < shown; index++) {
383
+ const element = elements[index] ?? 0;
384
+ text +=
385
+ isBuffer ?
386
+ `${index ? ' ' : ''}${element.toString(16).padStart(2, '0')}`
387
+ : `${index ? ', ' : ''}${String(element)}${typeof element === 'bigint' ? 'n' : ''}`;
388
+ }
389
+ const more = elements.length - shown;
390
+ if (isBuffer)
391
+ return `<Buffer${text ? ` ${text}` : ''}${more ? ` ${describeLeftOut(more, 'byte')}` : ''}>`;
392
+ return `${typeof name === 'string' ? name : 'TypedArray'}(${String(elements.length)}) [${text}${more ? `, ${describeLeftOut(more, 'item')}` : ''}]`;
393
+ };
394
+ /**
395
+ * Checks for errors of other realms (`vm` contexts, workers), which fail
396
+ * `instanceof Error`.
397
+ */
398
+ const isForeignError = (value) => Object.prototype.toString.call(value) === '[object Error]';
399
+ /**
400
+ * `Name: message` of an error, followed by the JSON of its own enumerable
401
+ * properties, if any, e.g. `Error: refused {"code":"ECONNREFUSED"}`.
402
+ */
403
+ const describeErrorValue = (error) => {
404
+ const head = describeThrown(error);
405
+ let props;
406
+ try {
407
+ props = JSON.stringify(error);
408
+ }
409
+ catch {
410
+ // properties that can't be serialized are left out
411
+ }
412
+ return props === undefined || props === '{}' ? head : `${head} ${props}`;
413
+ };
414
+ /**
415
+ * Entries of a map or set, at most `items`, stopping early once longer than
416
+ * `stringLength`.
417
+ */
418
+ const describeEntries = (entries, size, limits, describeEntry) => {
419
+ let text = '';
420
+ let shown = 0;
421
+ for (const entry of entries) {
422
+ if (shown === limits.items || text.length > limits.stringLength)
423
+ break;
424
+ text += `${shown ? ', ' : ''}${describeEntry(entry)}`;
425
+ shown++;
426
+ }
427
+ const more = size - shown;
428
+ return more ? `${text}, ${describeLeftOut(more, 'entry')}` : text;
429
+ };
430
+ /**
431
+ * Describes objects JSON shows as `{}`: dates (ISO), maps, sets, regular
432
+ * expressions and errors of other realms, like `util.inspect`. Entries of
433
+ * maps and sets are JSON, so nesting can't recurse.
434
+ *
435
+ * @returns `undefined` for other objects.
436
+ */
437
+ const describeObject = (value, limits = NO_OUTPUT_LIMITS) => {
438
+ if (value instanceof Date)
439
+ return Number.isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString();
440
+ if (value instanceof Map)
441
+ return `Map(${String(value.size)}) {${describeEntries(value, value.size, limits, (entry) => {
442
+ const [key, item] = entry;
443
+ return `${stringifyValue(key, false, limits)} => ${stringifyValue(item, false, limits)}`;
444
+ })}}`;
445
+ if (value instanceof Set)
446
+ return `Set(${String(value.size)}) {${describeEntries(value, value.size, limits, (item) => stringifyValue(item, false, limits))}}`;
447
+ if (value instanceof RegExp)
448
+ return String(value);
449
+ if (isForeignError(value))
450
+ return describeErrorValue(value);
451
+ return undefined;
452
+ };
453
+ /**
454
+ * @param describe Describe objects JSON shows as `{}` (see `describeObject`).
455
+ */
456
+ const stringifyValue = (value, describe, limits) => {
457
+ let text;
458
+ try {
459
+ // long strings are cut before JSON, which would copy them whole
460
+ if (typeof value === 'string' && value.length > limits.stringLength) {
461
+ const end = cutIndex(value, limits.stringLength);
462
+ return `${JSON.stringify(value.slice(0, end))}${describeLeftOut(value.length - end, 'character')}`;
463
+ }
464
+ if (typeof value === 'function')
465
+ text = describeFunction(value);
466
+ else if (typeof value === 'bigint')
467
+ text = `${String(value)}n`;
468
+ else if (value instanceof Error)
469
+ text = describeErrorValue(value);
470
+ else if (ArrayBuffer.isView(value))
471
+ text = describeBinary(value, limits.items);
472
+ else {
473
+ // undefined for symbols and undefined, despite the lib typing
474
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
475
+ text = JSON.stringify(value) ?? String(value);
476
+ if (describe &&
477
+ text === '{}' &&
478
+ typeof value === 'object' &&
479
+ value !== null)
480
+ text = describeObject(value, limits) ?? text;
481
+ }
482
+ }
483
+ catch {
484
+ return '"[Unserializable]"';
485
+ }
486
+ return truncate(text, limits.stringLength);
487
+ };
488
+ /**
489
+ * `JSON.stringify` that never throws and describes what JSON can't:
490
+ * - functions and classes: `[Function: name]`, `[class Name]`, no source;
491
+ * - bigints: `1n`;
492
+ * - errors: `Name: message` and their own properties' JSON;
493
+ * - buffers and typed arrays: see `describeBinary`;
494
+ * - dates, maps, sets, regular expressions and errors of other realms that
495
+ * JSON shows as `{}`: see `describeObject`;
496
+ * - symbols and `undefined`: `String`;
497
+ * - circular structures: `"[Unserializable]"`.
498
+ *
499
+ * Nested values are serialized whole by the native `JSON.stringify` and the
500
+ * result is cut to `limits.stringLength`.
501
+ *
502
+ * @param limits Output limits. Default: none.
503
+ */
504
+ const safeStringify = (value, limits = NO_OUTPUT_LIMITS) => stringifyValue(value, true, limits);
505
+ /**
506
+ * Text of a top-level value that is neither a string nor an object:
507
+ * functions and classes are described, bigints get their `n` suffix.
508
+ */
509
+ const stringifyPrimitive = (value) => typeof value === 'function' ? describeFunction(value)
510
+ : typeof value === 'bigint' ? `${String(value)}n`
511
+ : String(value);
512
+ const stringifyError = (error, { text, label }, { items, stringLength }) => {
513
+ const [head, frames] = splitError(error);
514
+ let result = text(truncate(head, stringLength));
515
+ const shown = Math.min(frames.length, items);
516
+ for (let index = 0; index < shown; index++)
517
+ result += `\n${label(truncate(frames[index] ?? '', stringLength))}`;
518
+ if (frames.length > shown)
519
+ result += `\n${describeLeftOut(frames.length - shown, 'frame')}`;
520
+ return result;
521
+ };
522
+ /**
523
+ * Converts any log data to text:
524
+ * - strings are returned as is;
525
+ * - errors are represented by their `name: message` and stack frames;
526
+ * - buffers and typed arrays are described in one line;
527
+ * - objects and arrays print each own property as `key: JSON` on a new line;
528
+ * - everything else is converted with `String`.
529
+ *
530
+ * Never throws: data that can't be read (throwing getters, revoked proxies,
531
+ * broken errors) gives placeholders.
532
+ *
533
+ * @param sanitizer Sanitizes strings (`text`), keys, values and stack frames
534
+ * (`label`). Default: none.
535
+ * @param limits Output limits. Default: none.
536
+ */
537
+ const stringifyData = (data, sanitizer = NO_SANITIZER, limits = NO_OUTPUT_LIMITS) => {
538
+ const { text, label } = sanitizer;
539
+ const { items, stringLength } = limits;
540
+ try {
541
+ if (typeof data === 'string')
542
+ return text(truncate(data, stringLength));
543
+ if (data instanceof Error)
544
+ return stringifyError(data, sanitizer, limits);
545
+ if (typeof data === 'object' && data !== null) {
546
+ if (ArrayBuffer.isView(data))
547
+ return label(describeBinary(data, items));
548
+ const [keys, more] = limitKeys(data, items);
549
+ if (!keys.length) {
550
+ // errors of other realms have no enumerable keys either
551
+ if (isForeignError(data))
552
+ return stringifyError(data, sanitizer, limits);
553
+ return label(truncate(describeObject(data, limits) ?? safeStringify(data), stringLength));
554
+ }
555
+ const lines = [];
556
+ for (const key of keys)
557
+ lines.push(`${label(truncate(key, stringLength))}: ${label(stringifyProperty(data, key, limits))}`);
558
+ if (more)
559
+ lines.push(describeLeftOut(more, 'item'));
560
+ return lines.join('\n');
561
+ }
562
+ return text(truncate(stringifyPrimitive(data), stringLength));
563
+ }
564
+ catch (thrown) {
565
+ return `[Unrenderable: ${label(describeThrown(thrown))}]`;
566
+ }
567
+ };
568
+ /**
569
+ * Overlaps below this are floating point noise at bucket edges, not real
570
+ * overlaps, and must not leak a neighbour's peak into `'max'`/`'min'`.
571
+ */
572
+ const MIN_OVERLAP = 1e-9;
573
+ /**
574
+ * Scales an array to a new length by combining the overlapping source cells
575
+ * of each new cell.
576
+ *
577
+ * @param data Values to scale.
578
+ * @param newLength Length of the result.
579
+ * @param aggregate How overlapping cells are combined. Default: `'average'`.
580
+ * @throws TypeError if `newLength` is not a non-negative integer.
581
+ */
582
+ const scaleArray = (data, newLength, aggregate = 'average') => {
583
+ if (!Number.isInteger(newLength) || newLength < 0)
584
+ throw new TypeError('scaleArray: new length is not a positive integer.');
585
+ const dataLength = data.length;
586
+ if (dataLength === newLength)
587
+ return [...data];
588
+ if (dataLength === 0)
589
+ return Array(newLength).fill(0);
590
+ const bucketSize = dataLength / newLength;
591
+ const result = [];
592
+ for (let i = 0; i < newLength; i++) {
593
+ const bucketStart = i * bucketSize;
594
+ const bucketEnd = bucketStart + bucketSize;
595
+ const indexEnd = Math.min(dataLength, Math.ceil(bucketEnd));
596
+ let sum = 0;
597
+ let totalWeight = 0;
598
+ let max = -Infinity;
599
+ let min = Infinity;
600
+ for (let n = Math.floor(bucketStart); n < indexEnd; n++) {
601
+ const weight = Math.min(bucketEnd, n + 1) - Math.max(bucketStart, n);
602
+ if (weight < MIN_OVERLAP)
603
+ continue;
604
+ const value = data[n] ?? 0;
605
+ sum += value * weight;
606
+ totalWeight += weight;
607
+ if (value > max)
608
+ max = value;
609
+ if (value < min)
610
+ min = value;
611
+ }
612
+ result.push(!totalWeight ? 0
613
+ : aggregate === 'max' ? max
614
+ : aggregate === 'min' ? min
615
+ : sum / totalWeight);
616
+ }
617
+ return result;
618
+ };
619
+
620
+ export { DEFAULT_OUTPUT_LIMITS, NO_OUTPUT_LIMITS, SLOW_CHARS, charWidth, createSanitizer, describeBinary, describeFunction, describeLeftOut, describeObject, describeThrown, isForeignError, justifyLeftRight, limitKeys, padTextMiddle, resolveOutputLimits, safeStringify, scaleArray, splitError, stringifyData, stringifyPrimitive, stringifyProperty, textWidth, truncate, visibleLength };
621
+ //# sourceMappingURL=text.js.map