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