@ix-xs/node-comfort 2.1.0 → 2.2.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 +153 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/Arr.js +13 -5
- package/src/Checker.js +56 -8
- package/src/Cli.js +60 -18
- package/src/Color.js +32 -6
- package/src/Crypto.js +429 -78
- package/src/Emitter.js +9 -9
- package/src/Env.js +90 -8
- package/src/FS.js +369 -59
- package/src/Func.js +1 -1
- package/src/Http.js +266 -75
- package/src/Id.js +53 -15
- package/src/Logger.js +100 -22
- package/src/Num.js +3 -2
- package/src/Obj.js +225 -36
- package/src/SQLite.js +155 -98
- package/src/Schema.js +281 -40
- package/src/Str.js +77 -29
- package/src/Sys.js +124 -23
- package/src/Time.js +98 -24
- package/src/Utils.js +18 -3
- package/src/errors.js +75 -4
- package/src/internal/shared.js +68 -14
- package/src/internal/terminal.js +49 -0
- package/types/index.d.ts +28 -5
- package/types/src/Arr.d.ts +4 -2
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Checker.d.ts +29 -3
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +6 -4
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +2 -2
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +126 -28
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +6 -6
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +19 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +30 -23
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +1 -1
- package/types/src/Http.d.ts +45 -8
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +11 -3
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +1 -1
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +15 -2
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +105 -13
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +61 -5
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +32 -7
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +43 -10
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +12 -7
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +16 -6
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +6 -2
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +7 -0
- package/types/src/internal/shared.d.ts.map +1 -1
- package/types/src/internal/terminal.d.ts +13 -0
- package/types/src/internal/terminal.d.ts.map +1 -0
package/src/Logger.js
CHANGED
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
* files if you need to.
|
|
8
8
|
*
|
|
9
9
|
* Markup: `<% red bold Error:%> disk is full`. Any style name works, plus
|
|
10
|
-
* `#ff8800`, `bg#1e1e2e`, `rgb(255, 128, 0)` and `bgRgb(...)`.
|
|
10
|
+
* `#ff8800`, `bg#1e1e2e`, `rgb(255, 128, 0)` and `bgRgb(...)`. Only the
|
|
11
|
+
* first argument, the message, is read as markup: the values after it are
|
|
12
|
+
* printed as they are.
|
|
13
|
+
*
|
|
14
|
+
* What a terminal could act on is shown as text (`\x1b`, `\r`), so a logged
|
|
15
|
+
* value can't move the cursor, change the title or forge a log line. Colors
|
|
16
|
+
* from `nc.color`, links from `color.link()`, new lines and tabs still work,
|
|
17
|
+
* and the lines of a message after its first one are indented, except with
|
|
18
|
+
* `log()` when there is no scope and no timestamp.
|
|
11
19
|
*
|
|
12
20
|
* Levels, from chatty to severe: `trace`, `debug`, `info`, `warn`, `error`,
|
|
13
21
|
* `fatal`. The default is `info`; `LOG_LEVEL` changes it. `DEBUG=1` shows
|
|
@@ -44,6 +52,7 @@ const { timeConfig } = require("./internal/shared.js");
|
|
|
44
52
|
* @property {string} path The file to append to. Folders are created, colors removed.
|
|
45
53
|
* @property {string | number} [maxSize] Rotate past this size, like `"10MB"`.
|
|
46
54
|
* @property {number} [maxFiles] How many rotated files to keep. Defaults to `5`.
|
|
55
|
+
* @property {number} [mode] The permissions a new log file gets, like `0o640`. Defaults to `0o600`: only its owner can read it. A file that already exists keeps its own. Ignored on Windows.
|
|
47
56
|
*/
|
|
48
57
|
|
|
49
58
|
/**
|
|
@@ -78,7 +87,7 @@ const { timeConfig } = require("./internal/shared.js");
|
|
|
78
87
|
* A logger from `createLogger()`. Every method returns the logger, so calls chain.
|
|
79
88
|
*
|
|
80
89
|
* @typedef {object} Logger
|
|
81
|
-
* @property {(...args: unknown[]) => Logger} log A plain message, no badge.
|
|
90
|
+
* @property {(...args: unknown[]) => Logger} log A plain message, no badge. Its first argument supports markup.
|
|
82
91
|
* @property {(...args: unknown[]) => Logger} trace Very detailed diagnostics, hidden unless the level is `trace`.
|
|
83
92
|
* @property {(...args: unknown[]) => Logger} debug Diagnostics, hidden unless the level is `debug` or lower.
|
|
84
93
|
* @property {(...args: unknown[]) => Logger} info An `ℹ INFO` message.
|
|
@@ -124,6 +133,13 @@ const STYLE_NAMES = new Set([
|
|
|
124
133
|
]);
|
|
125
134
|
|
|
126
135
|
const REDACTED = "[redacted]";
|
|
136
|
+
// Shows control characters and escape sequences as text, keeps colors and links.
|
|
137
|
+
const { safe: _safe } = require("./internal/terminal.js");
|
|
138
|
+
// What JSON.stringify leaves as is, but a terminal or a JavaScript parser may act on.
|
|
139
|
+
const JSON_UNSAFE = /[\x7f-\x9f\u{2028}\u{2029}]/u;
|
|
140
|
+
const JSON_UNSAFE_ALL = /[\x7f-\x9f\u{2028}\u{2029}]/gu;
|
|
141
|
+
/** @param {string} char */
|
|
142
|
+
const _jsonEscape = (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
127
143
|
const TRIM_LINES = /^(\s*\r?\n)+|(\r?\n\s*)+$/g;
|
|
128
144
|
const RGB = /(bg)?rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/gi;
|
|
129
145
|
const TOKEN = /^(\S+)(\s+|$)/;
|
|
@@ -227,7 +243,7 @@ const _markup = (input, delimiter, palette) => {
|
|
|
227
243
|
* @returns {unknown}
|
|
228
244
|
*/
|
|
229
245
|
const _redact = (value, secrets, seen) => {
|
|
230
|
-
if (value === null || typeof value !== "object" || value instanceof Date || value instanceof RegExp ||
|
|
246
|
+
if (value === null || typeof value !== "object" || value instanceof Date || value instanceof RegExp || ArrayBuffer.isView(value)) return value;
|
|
231
247
|
const known = seen.get(value);
|
|
232
248
|
if (known !== undefined) return known;
|
|
233
249
|
if (Array.isArray(value)) {
|
|
@@ -237,7 +253,44 @@ const _redact = (value, secrets, seen) => {
|
|
|
237
253
|
for (const item of value) list.push(_redact(item, secrets, seen));
|
|
238
254
|
return list;
|
|
239
255
|
}
|
|
240
|
-
if (value instanceof Map
|
|
256
|
+
if (value instanceof Map) {
|
|
257
|
+
// Same class name in the output, values masked by their key.
|
|
258
|
+
/** @type {Map<unknown, unknown>} */
|
|
259
|
+
const map = Object.setPrototypeOf(new Map(), Object.getPrototypeOf(value));
|
|
260
|
+
seen.set(value, map);
|
|
261
|
+
for (const [key, item] of Map.prototype.entries.call(value)) {
|
|
262
|
+
map.set(_redact(key, secrets, seen), typeof key === "string" && secrets.has(key.toLowerCase()) ? REDACTED : _redact(item, secrets, seen));
|
|
263
|
+
}
|
|
264
|
+
return map;
|
|
265
|
+
}
|
|
266
|
+
if (value instanceof Set) {
|
|
267
|
+
/** @type {Set<unknown>} */
|
|
268
|
+
const set = Object.setPrototypeOf(new Set(), Object.getPrototypeOf(value));
|
|
269
|
+
seen.set(value, set);
|
|
270
|
+
for (const item of Set.prototype.values.call(value)) set.add(_redact(item, secrets, seen));
|
|
271
|
+
return set;
|
|
272
|
+
}
|
|
273
|
+
if (value instanceof Error) {
|
|
274
|
+
// Its own properties too (the cause, a code, a response), stack and message kept as they are.
|
|
275
|
+
/** @type {Record<string | symbol, unknown>} */
|
|
276
|
+
const error = Object.create(Object.getPrototypeOf(value));
|
|
277
|
+
seen.set(value, error);
|
|
278
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
279
|
+
const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(value, key));
|
|
280
|
+
let item;
|
|
281
|
+
try {
|
|
282
|
+
item = /** @type {any} */ (value)[key];
|
|
283
|
+
} catch {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const secret = typeof key === "string" && secrets.has(key.toLowerCase());
|
|
287
|
+
const keep = key === "stack" || key === "message" || key === "name";
|
|
288
|
+
Object.defineProperty(error, key, { value: secret ? REDACTED : keep ? item : _redact(item, secrets, seen), enumerable: descriptor.enumerable, writable: true, configurable: true });
|
|
289
|
+
}
|
|
290
|
+
// The stack lives on the original, not on the copy: carry it over.
|
|
291
|
+
if (!Object.hasOwn(error, "stack")) Object.defineProperty(error, "stack", { value: value.stack, writable: true, configurable: true });
|
|
292
|
+
return error;
|
|
293
|
+
}
|
|
241
294
|
/** @type {Record<string, unknown>} */
|
|
242
295
|
const copy = Object.create(Object.getPrototypeOf(value));
|
|
243
296
|
seen.set(value, copy);
|
|
@@ -260,7 +313,7 @@ const _stringify = (value, colors, secrets) => {
|
|
|
260
313
|
let cause = /** @type {any} */ (value).cause;
|
|
261
314
|
let depth = 0;
|
|
262
315
|
while (cause !== undefined && depth++ < 5) {
|
|
263
|
-
text += `\nCaused by: ${cause instanceof Error ? cause.stack ?? cause.message : util.inspect(cause, { colors })}`;
|
|
316
|
+
text += `\nCaused by: ${cause instanceof Error ? cause.stack ?? cause.message : util.inspect(secrets ? _redact(cause, secrets, new Map()) : cause, { colors })}`;
|
|
264
317
|
cause = cause instanceof Error ? /** @type {any} */ (cause).cause : undefined;
|
|
265
318
|
}
|
|
266
319
|
return text;
|
|
@@ -290,8 +343,9 @@ const _stringify = (value, colors, secrets) => {
|
|
|
290
343
|
* @returns {string}
|
|
291
344
|
*/
|
|
292
345
|
const _json = (level, args, scope, fields, secrets, now) => {
|
|
346
|
+
const time = now.toISOString();
|
|
293
347
|
/** @type {Record<string, unknown>} */
|
|
294
|
-
const record = { time
|
|
348
|
+
const record = { time, level, ...fields };
|
|
295
349
|
if (scope) record.scope = scope;
|
|
296
350
|
const messages = [];
|
|
297
351
|
for (const arg of args) {
|
|
@@ -303,17 +357,25 @@ const _json = (level, args, scope, fields, secrets, now) => {
|
|
|
303
357
|
messages.push(typeof arg === "string" ? color.strip(arg) : _stringify(arg, false, secrets));
|
|
304
358
|
}
|
|
305
359
|
}
|
|
360
|
+
// A logged object may carry a `time`, `level`, `scope` or `msg` of its own: the logger's win.
|
|
361
|
+
record.time = time;
|
|
362
|
+
record.level = level;
|
|
363
|
+
if (scope) record.scope = scope;
|
|
364
|
+
else delete record.scope;
|
|
306
365
|
record.msg = messages.join(" ");
|
|
366
|
+
let line;
|
|
307
367
|
try {
|
|
308
|
-
|
|
368
|
+
line = JSON.stringify(record, (key, v) => (secrets?.has(key.toLowerCase()) ? REDACTED : typeof v === "bigint" ? v.toString() : v));
|
|
309
369
|
} catch {
|
|
310
|
-
|
|
370
|
+
line = JSON.stringify({ time, level, msg: `${record.msg} [unserializable fields]` });
|
|
311
371
|
}
|
|
372
|
+
// C1 controls and line separators stay raw in JSON: escape them too.
|
|
373
|
+
return JSON_UNSAFE.test(line) ? line.replace(JSON_UNSAFE_ALL, _jsonEscape) : line;
|
|
312
374
|
};
|
|
313
375
|
|
|
314
376
|
/**
|
|
315
377
|
* A log file: where it is, how big it may get, how big it is now.
|
|
316
|
-
* @typedef {{ target: string, limit: number | undefined, keep: number, size: number, ready: boolean }} _LogFile
|
|
378
|
+
* @typedef {{ target: string, limit: number | undefined, keep: number, size: number, ready: boolean, options: { mode: number } }} _LogFile
|
|
317
379
|
*/
|
|
318
380
|
|
|
319
381
|
/**
|
|
@@ -326,6 +388,8 @@ const _openFile = (file) => ({
|
|
|
326
388
|
keep: Math.max(1, file.maxFiles ?? 5),
|
|
327
389
|
size: 0,
|
|
328
390
|
ready: false,
|
|
391
|
+
// Logs can hold personal data: a new file is for its owner only.
|
|
392
|
+
options: { mode: file.mode ?? 0o600 },
|
|
329
393
|
});
|
|
330
394
|
|
|
331
395
|
/** @param {string} target */
|
|
@@ -365,11 +429,11 @@ const _writeFile = (file, line) => {
|
|
|
365
429
|
}
|
|
366
430
|
}
|
|
367
431
|
try {
|
|
368
|
-
fs.appendFileSync(file.target, text);
|
|
432
|
+
fs.appendFileSync(file.target, text, file.options);
|
|
369
433
|
} catch {
|
|
370
434
|
// The folder was removed since: make it again, once.
|
|
371
435
|
fs.mkdirSync(path.dirname(file.target), { recursive: true });
|
|
372
|
-
fs.appendFileSync(file.target, text);
|
|
436
|
+
fs.appendFileSync(file.target, text, file.options);
|
|
373
437
|
file.size = 0;
|
|
374
438
|
}
|
|
375
439
|
file.size += bytes;
|
|
@@ -464,7 +528,7 @@ const _create = (options) => {
|
|
|
464
528
|
let face = faces[which];
|
|
465
529
|
if (face) return face;
|
|
466
530
|
const palette = paletteOf(which);
|
|
467
|
-
face = { palette, colors: palette.level > 0, labels: {}, scope: settings.scope ? palette.magenta(`[${settings.scope}]`) : "", stamp: "", stampAt: -1, stampWidth: 0 };
|
|
531
|
+
face = { palette, colors: palette.level > 0, labels: {}, scope: settings.scope ? palette.magenta(`[${_safe(settings.scope, false)}]`) : "", stamp: "", stampAt: -1, stampWidth: 0 };
|
|
468
532
|
for (const kind of Object.keys(BADGES)) face.labels[kind] = BADGES[kind].label ? _style(palette, BADGES[kind].style.split(" "), BADGES[kind].label) : "";
|
|
469
533
|
faces[which] = face;
|
|
470
534
|
return face;
|
|
@@ -493,13 +557,18 @@ const _create = (options) => {
|
|
|
493
557
|
* @param {unknown[]} args
|
|
494
558
|
* @param {Date} now
|
|
495
559
|
* @param {boolean} stamped
|
|
560
|
+
* @param {boolean} block A table or a box, drawn by the CLI: no markup, lines kept in place.
|
|
496
561
|
* @returns {string}
|
|
497
562
|
*/
|
|
498
|
-
const render = (face, kind, args, now, stamped) => {
|
|
563
|
+
const render = (face, kind, args, now, stamped, block) => {
|
|
499
564
|
const { palette } = face;
|
|
500
565
|
let body = "";
|
|
501
566
|
for (let i = 0; i < args.length; i++) {
|
|
502
|
-
const
|
|
567
|
+
const arg = args[i];
|
|
568
|
+
// Whatever is logged, a terminal must not act on it: only colors and links get through.
|
|
569
|
+
let text = _safe(_stringify(arg, face.colors, secrets), face.colors);
|
|
570
|
+
// Markup is for the message the developer wrote, never for the values after it.
|
|
571
|
+
if (i === 0 && !block && typeof arg === "string") text = _markup(text, delimiter, palette);
|
|
503
572
|
body += (i ? " " : "") + (face.colors && (kind === "debug" || kind === "trace") ? palette.gray(text) : text);
|
|
504
573
|
}
|
|
505
574
|
let prefix = "";
|
|
@@ -517,6 +586,12 @@ const _create = (options) => {
|
|
|
517
586
|
if (face.labels[kind]) prefix += `${face.labels[kind]} `;
|
|
518
587
|
if (body.indexOf("\n") === -1) return (prefix + body).trimEnd();
|
|
519
588
|
const lines = body.replace(TRIM_LINES, "").split(/\r?\n/);
|
|
589
|
+
// Without a timestamp, the next lines line up under the message, past the badge and
|
|
590
|
+
// the scope: a new line in a message can't pass for a log line of its own. Without
|
|
591
|
+
// either, like `log()`, there is nothing to forge and nothing is added, as with console.log.
|
|
592
|
+
if (!stamped && !block) {
|
|
593
|
+
padWidth += (face.scope ? color.width(face.scope) + 1 : 0) + (face.labels[kind] ? color.width(face.labels[kind]) + 1 : 0);
|
|
594
|
+
}
|
|
520
595
|
const pad = " ".repeat(padWidth);
|
|
521
596
|
let out = (prefix + lines[0]).trimEnd();
|
|
522
597
|
for (let i = 1; i < lines.length; i++) out += `\n${pad}${lines[i]}`;
|
|
@@ -526,8 +601,9 @@ const _create = (options) => {
|
|
|
526
601
|
/**
|
|
527
602
|
* @param {keyof typeof BADGES} kind
|
|
528
603
|
* @param {unknown[]} args
|
|
604
|
+
* @param {boolean} [block] A table or a box drawn by the CLI.
|
|
529
605
|
*/
|
|
530
|
-
const emit = (kind, args) => {
|
|
606
|
+
const emit = (kind, args, block = false) => {
|
|
531
607
|
if (!on[kind]) return;
|
|
532
608
|
const badge = BADGES[kind];
|
|
533
609
|
const now = new Date();
|
|
@@ -538,15 +614,15 @@ const _create = (options) => {
|
|
|
538
614
|
if (file) _writeFile(file, line);
|
|
539
615
|
} else {
|
|
540
616
|
const face = faceOf(badge.stream);
|
|
541
|
-
const line = render(face, kind, args, now, Boolean(settings.timestamp));
|
|
617
|
+
const line = render(face, kind, args, now, Boolean(settings.timestamp), block);
|
|
542
618
|
stream.write(`${line}\n`);
|
|
543
619
|
// The file gets the same line without colors: the one just made, when it has none.
|
|
544
|
-
if (file) _writeFile(file, face.colors ? render(faceOf("file"), kind, args, now, Boolean(settings.timestamp)) : line);
|
|
620
|
+
if (file) _writeFile(file, face.colors ? render(faceOf("file"), kind, args, now, Boolean(settings.timestamp), block) : line);
|
|
545
621
|
}
|
|
546
622
|
if (settings.onLog) {
|
|
547
623
|
try {
|
|
548
624
|
/** @type {unknown} */
|
|
549
|
-
const pending = settings.onLog({ time: now, level: badge.level, kind: /** @type {LogEntry["kind"]} */ (kind), scope: settings.scope, message: render(faceOf("file"), kind, args, now, false), args });
|
|
625
|
+
const pending = settings.onLog({ time: now, level: badge.level, kind: /** @type {LogEntry["kind"]} */ (kind), scope: settings.scope, message: render(faceOf("file"), kind, args, now, false, block), args });
|
|
550
626
|
// An async hook that fails would otherwise be an unhandled rejection.
|
|
551
627
|
if (pending && typeof (/** @type {any} */ (pending).then) === "function") /** @type {PromiseLike<unknown>} */ (pending).then(undefined, () => {});
|
|
552
628
|
} catch {
|
|
@@ -587,8 +663,9 @@ const _create = (options) => {
|
|
|
587
663
|
const ms = Number(process.hrtime.bigint() - start) / 1e6;
|
|
588
664
|
return logger.log(`${label}: ${ms < 1000 ? `${Math.round(ms * 100) / 100}ms` : require("./Time.js").formatDuration(ms, { units: 2 })}`);
|
|
589
665
|
},
|
|
590
|
-
|
|
591
|
-
|
|
666
|
+
// Drawn by the CLI from values: no markup in them.
|
|
667
|
+
table: (rows, tableOptions) => (on.log && emit("log", [require("./Cli.js").table(rows, { ...tableOptions, colors: faceOf("stdout").colors })], true), logger),
|
|
668
|
+
box: (text, boxOptions) => (on.log && emit("log", [require("./Cli.js").box(text, { ...boxOptions, colors: faceOf("stdout").colors })], true), logger),
|
|
592
669
|
divider: (title) => {
|
|
593
670
|
const columns = /** @type {any} */ (streamOf("stdout")).columns ?? 80;
|
|
594
671
|
const width = Math.max(10, Math.min(columns, 100) - (settings.timestamp ? 11 : 0) - groups.length * 2);
|
|
@@ -651,8 +728,9 @@ function createLogger(options = {}) {
|
|
|
651
728
|
}
|
|
652
729
|
|
|
653
730
|
/**
|
|
654
|
-
* Prints a message without a badge.
|
|
655
|
-
* pretty-printed, and several arguments are
|
|
731
|
+
* Prints a message without a badge. The first argument, when it's a string,
|
|
732
|
+
* supports markup; objects are pretty-printed, and several arguments are
|
|
733
|
+
* joined with spaces.
|
|
656
734
|
*
|
|
657
735
|
* @example
|
|
658
736
|
* nc.log("<% green ✔ Saved%> in <% bold 12ms%>");
|
package/src/Num.js
CHANGED
|
@@ -54,7 +54,7 @@ const _config = { locale: "en" };
|
|
|
54
54
|
* @typedef {object} CurrencyOptions
|
|
55
55
|
* @property {string} [locale] Language. Defaults to the one set with `setLocale()`, or `"en"`.
|
|
56
56
|
* @property {number} [decimals] Defaults to the currency's usual decimals (2 for EUR, 0 for JPY).
|
|
57
|
-
* @property {"symbol"|"narrowSymbol"|"code"|"name"} [display]
|
|
57
|
+
* @property {"symbol"|"narrowSymbol"|"code"|"name"} [display] How the currency is written. For US dollars in Canada: `US$`, `$`, `USD` or `US dollars`. Defaults to `"symbol"`.
|
|
58
58
|
* @property {boolean} [accounting] Show negative amounts in parentheses, like `($5.00)`.
|
|
59
59
|
*/
|
|
60
60
|
|
|
@@ -703,7 +703,8 @@ function parse(value, fallback = NaN) {
|
|
|
703
703
|
*/
|
|
704
704
|
function parseBytes(value, options = {}) {
|
|
705
705
|
if (typeof value === "number") return value;
|
|
706
|
-
|
|
706
|
+
// Trimmed first: spaces at both ends of the pattern would be tried against each other.
|
|
707
|
+
const match = /^([-+]?(?:\d+(?:\.\d*)?|\.\d+))\s*([kmgtpezy]?)(i?)(b?)(?:ytes?)?$/i.exec(String(value).trim());
|
|
707
708
|
if (!match) return NaN;
|
|
708
709
|
const [, amount, prefix, iec] = match;
|
|
709
710
|
const base = iec || options.binary !== false ? 1024 : 1000;
|