@ix-xs/node-comfort 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/Str.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* Options for `truncate()`.
|
|
25
25
|
* @typedef {object} TruncateOptions
|
|
26
26
|
* @property {string} [omission] Added where the text is cut, and counted in the length. Defaults to `"…"`.
|
|
27
|
-
* @property {boolean} [words] Cut at the previous space rather than inside a word.
|
|
27
|
+
* @property {boolean} [words] Cut at the previous space rather than inside a word, when the end is what gets cut.
|
|
28
|
+
* @property {"end" | "middle" | "start"} [position] Where the text is cut. `"middle"` keeps both ends, which suits paths and ids. Defaults to `"end"`.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -94,22 +95,30 @@
|
|
|
94
95
|
* @property {string} [charset] Characters to pick from. Defaults to letters and digits.
|
|
95
96
|
*/
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const shared = require("./internal/shared.js");
|
|
99
|
+
|
|
100
|
+
const { intl, graphemes: _graphemes, graphemeCount: _count } = shared;
|
|
101
|
+
|
|
100
102
|
/** @type {Intl.Collator | undefined} */
|
|
101
103
|
let _defaultCollator;
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
const WORD = /[\p{L}\p{N}]+/gu;
|
|
106
|
+
const COMBINING_MARKS = /[\u0300-\u036f]/g;
|
|
107
|
+
const SPECIAL_LATIN = /[\u00df\u1e9e\u00e6\u00c6\u0153\u0152\u00f8\u00d8\u0111\u0110\u00f0\u00d0\u0142\u0141\u00fe\u00de\u0127\u0126\u0131]/g;
|
|
108
|
+
const HTML_UNSAFE = /[&<>"']/;
|
|
109
|
+
const HTML_UNSAFE_ALL = /[&<>"']/g;
|
|
110
|
+
const HTML_ESCAPES = /** @type {Record<string, string>} */ ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" });
|
|
111
|
+
const HTML_ENTITIES = /** @type {Record<string, string>} */ ({
|
|
112
|
+
amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\u00a0",
|
|
113
|
+
copy: "\u00a9", reg: "\u00ae", trade: "\u2122", hellip: "\u2026", mdash: "\u2014", ndash: "\u2013",
|
|
114
|
+
lsquo: "\u2018", rsquo: "\u2019", ldquo: "\u201c", rdquo: "\u201d", laquo: "\u00ab", raquo: "\u00bb",
|
|
115
|
+
euro: "\u20ac", pound: "\u00a3", yen: "\u00a5", cent: "\u00a2", deg: "\u00b0", middot: "\u00b7",
|
|
116
|
+
bull: "\u2022", times: "\u00d7", divide: "\u00f7", sect: "\u00a7", para: "\u00b6",
|
|
117
|
+
});
|
|
118
|
+
const TEMPLATE_DEFAULT = /\{\s*([\w$.-]+)\s*\}/g;
|
|
119
|
+
/** @type {Map<string, RegExp>} */
|
|
120
|
+
const _templateRegexes = new Map();
|
|
121
|
+
const ORDINAL = { type: "ordinal" };
|
|
113
122
|
|
|
114
123
|
/** Letters that Unicode normalization doesn't decompose. */
|
|
115
124
|
const _SPECIAL_LATIN = /** @type {Record<string, string>} */ ({
|
|
@@ -130,8 +139,96 @@ const _words = (input) =>
|
|
|
130
139
|
.split(/\s+/)
|
|
131
140
|
.filter(Boolean);
|
|
132
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Length of the first character in UTF-16 units: 2 for letters outside the
|
|
144
|
+
* basic plane, like mathematical alphabets.
|
|
145
|
+
* @param {string} text
|
|
146
|
+
*/
|
|
147
|
+
const _headLength = (text) => ((text.codePointAt(0) ?? 0) > 0xffff ? 2 : 1);
|
|
148
|
+
|
|
133
149
|
/** @param {string} word */
|
|
134
|
-
const _upperFirst = (word) =>
|
|
150
|
+
const _upperFirst = (word) => {
|
|
151
|
+
const n = _headLength(word);
|
|
152
|
+
return word.slice(0, n).toUpperCase() + word.slice(n);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Repeats `char` to fill `count` visible characters.
|
|
157
|
+
* @param {string} char
|
|
158
|
+
* @param {number} count
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
const _padding = (char, count) => {
|
|
162
|
+
if (!(count > 0) || !char) return "";
|
|
163
|
+
if (char.length === 1) return char.repeat(count);
|
|
164
|
+
const units = _graphemes(char);
|
|
165
|
+
let out = "";
|
|
166
|
+
for (let i = 0; i < count; i++) out += units[i % units.length];
|
|
167
|
+
return out;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reads a template value. Members inherited from `Object.prototype`
|
|
172
|
+
* (`constructor`, `toString`...) are not data.
|
|
173
|
+
* @param {any} value
|
|
174
|
+
* @param {string} key
|
|
175
|
+
* @returns {unknown}
|
|
176
|
+
*/
|
|
177
|
+
const _templateValue = (value, key) => {
|
|
178
|
+
const object = Object(value);
|
|
179
|
+
if (!(key in object)) return undefined;
|
|
180
|
+
if (!Object.prototype.hasOwnProperty.call(object, key) && key in Object.prototype) return undefined;
|
|
181
|
+
const found = object[key];
|
|
182
|
+
return typeof found === "function" ? undefined : found;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** One row of the edit-distance table, reused between calls. */
|
|
186
|
+
let _row = new Uint32Array(64);
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Levenshtein distance between two sequences of characters.
|
|
190
|
+
* @param {ArrayLike<string>} s
|
|
191
|
+
* @param {ArrayLike<string>} t
|
|
192
|
+
* @returns {number}
|
|
193
|
+
*/
|
|
194
|
+
const _distance = (s, t) => {
|
|
195
|
+
let sLength = s.length;
|
|
196
|
+
let tLength = t.length;
|
|
197
|
+
let start = 0;
|
|
198
|
+
while (start < sLength && start < tLength && s[start] === t[start]) start++;
|
|
199
|
+
while (sLength > start && tLength > start && s[sLength - 1] === t[tLength - 1]) {
|
|
200
|
+
sLength--;
|
|
201
|
+
tLength--;
|
|
202
|
+
}
|
|
203
|
+
sLength -= start;
|
|
204
|
+
tLength -= start;
|
|
205
|
+
if (sLength === 0) return tLength;
|
|
206
|
+
if (tLength === 0) return sLength;
|
|
207
|
+
if (_row.length <= tLength) _row = new Uint32Array(tLength * 2);
|
|
208
|
+
const row = _row;
|
|
209
|
+
for (let j = 0; j <= tLength; j++) row[j] = j;
|
|
210
|
+
for (let i = 1; i <= sLength; i++) {
|
|
211
|
+
const char = s[start + i - 1];
|
|
212
|
+
let diagonal = row[0];
|
|
213
|
+
row[0] = i;
|
|
214
|
+
for (let j = 1; j <= tLength; j++) {
|
|
215
|
+
const above = row[j];
|
|
216
|
+
let best = char === t[start + j - 1] ? diagonal : diagonal + 1;
|
|
217
|
+
if (above + 1 < best) best = above + 1;
|
|
218
|
+
if (row[j - 1] + 1 < best) best = row[j - 1] + 1;
|
|
219
|
+
row[j] = best;
|
|
220
|
+
diagonal = above;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return row[tLength];
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A string as a sequence of visible characters. ASCII strings are already one.
|
|
228
|
+
* @param {string} text
|
|
229
|
+
* @returns {ArrayLike<string>}
|
|
230
|
+
*/
|
|
231
|
+
const _units = (text) => (shared.ASCII.test(text) ? text : _graphemes(text));
|
|
135
232
|
|
|
136
233
|
/** @param {string} value */
|
|
137
234
|
const _escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -147,7 +244,60 @@ const _escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
147
244
|
*/
|
|
148
245
|
function capitalize(input) {
|
|
149
246
|
const s = String(input);
|
|
150
|
-
|
|
247
|
+
if (!s) return s;
|
|
248
|
+
const n = _headLength(s);
|
|
249
|
+
return s.slice(0, n).toUpperCase() + s.slice(n).toLowerCase();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Uppercases the first character and leaves the rest alone.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* str.upperFirst("userName"); // "UserName"
|
|
257
|
+
*
|
|
258
|
+
* @param {unknown} input
|
|
259
|
+
* @returns {string}
|
|
260
|
+
*/
|
|
261
|
+
function upperFirst(input) {
|
|
262
|
+
return _upperFirst(String(input));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Lowercases the first character and leaves the rest alone.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* str.lowerFirst("UserName"); // "userName"
|
|
270
|
+
*
|
|
271
|
+
* @param {unknown} input
|
|
272
|
+
* @returns {string}
|
|
273
|
+
*/
|
|
274
|
+
function lowerFirst(input) {
|
|
275
|
+
const s = String(input);
|
|
276
|
+
const n = _headLength(s);
|
|
277
|
+
return s.slice(0, n).toLowerCase() + s.slice(n);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Turns an identifier into a label people can read. Acronyms stay as they are.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* str.humanize("user_first_name"); // "User first name"
|
|
285
|
+
* str.humanize("createdAt"); // "Created at"
|
|
286
|
+
* str.humanize("userID"); // "User ID"
|
|
287
|
+
*
|
|
288
|
+
* @param {unknown} input
|
|
289
|
+
* @returns {string}
|
|
290
|
+
*/
|
|
291
|
+
function humanize(input) {
|
|
292
|
+
const list = _words(input);
|
|
293
|
+
const shouting = list.every((word) => word === word.toUpperCase());
|
|
294
|
+
return list
|
|
295
|
+
.map((word, i) => {
|
|
296
|
+
if (!shouting && word.length > 1 && word === word.toUpperCase() && /\p{Lu}/u.test(word)) return word;
|
|
297
|
+
const lower = word.toLowerCase();
|
|
298
|
+
return i === 0 ? _upperFirst(lower) : lower;
|
|
299
|
+
})
|
|
300
|
+
.join(" ");
|
|
151
301
|
}
|
|
152
302
|
|
|
153
303
|
/**
|
|
@@ -286,10 +436,12 @@ function words(input) {
|
|
|
286
436
|
* @returns {string}
|
|
287
437
|
*/
|
|
288
438
|
function deburr(input) {
|
|
289
|
-
|
|
439
|
+
const s = String(input);
|
|
440
|
+
if (shared.ASCII.test(s)) return s;
|
|
441
|
+
return s
|
|
290
442
|
.normalize("NFKD")
|
|
291
|
-
.replace(
|
|
292
|
-
.replace(
|
|
443
|
+
.replace(COMBINING_MARKS, "")
|
|
444
|
+
.replace(SPECIAL_LATIN, (c) => _SPECIAL_LATIN[c] ?? c);
|
|
293
445
|
}
|
|
294
446
|
|
|
295
447
|
/**
|
|
@@ -306,18 +458,21 @@ function deburr(input) {
|
|
|
306
458
|
*/
|
|
307
459
|
function slugify(input, options = {}) {
|
|
308
460
|
const separator = options.separator ?? "-";
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
461
|
+
let text = deburr(input);
|
|
462
|
+
if (options.lower ?? true) text = text.toLowerCase();
|
|
463
|
+
const list = text.match(WORD);
|
|
464
|
+
if (!list) return "";
|
|
465
|
+
const slug = list.join(separator);
|
|
466
|
+
const max = options.maxLength;
|
|
467
|
+
if (max === undefined || slug.length <= max) return slug;
|
|
468
|
+
// Keep whole words while they fit; a first word that is too long is cut.
|
|
469
|
+
let length = 0;
|
|
470
|
+
for (let i = 0; i < list.length; i++) {
|
|
471
|
+
const next = length + (i ? separator.length : 0) + list[i].length;
|
|
472
|
+
if (next > max) break;
|
|
473
|
+
length = next;
|
|
319
474
|
}
|
|
320
|
-
return slug;
|
|
475
|
+
return slug.slice(0, length || Math.max(0, max));
|
|
321
476
|
}
|
|
322
477
|
|
|
323
478
|
/**
|
|
@@ -344,7 +499,7 @@ function squish(input) {
|
|
|
344
499
|
* @returns {string}
|
|
345
500
|
*/
|
|
346
501
|
function stripTags(input) {
|
|
347
|
-
return String(input).replace(
|
|
502
|
+
return String(input).replace(/<!--[\s\S]*?(?:-->|$)|<[/!]?[a-zA-Z][^>]*(?:>|$)/g, "");
|
|
348
503
|
}
|
|
349
504
|
|
|
350
505
|
/**
|
|
@@ -358,8 +513,8 @@ function stripTags(input) {
|
|
|
358
513
|
* @returns {string}
|
|
359
514
|
*/
|
|
360
515
|
function escapeHTML(input) {
|
|
361
|
-
const
|
|
362
|
-
return
|
|
516
|
+
const s = String(input);
|
|
517
|
+
return HTML_UNSAFE.test(s) ? s.replace(HTML_UNSAFE_ALL, (c) => HTML_ESCAPES[c]) : s;
|
|
363
518
|
}
|
|
364
519
|
|
|
365
520
|
/**
|
|
@@ -373,12 +528,16 @@ function escapeHTML(input) {
|
|
|
373
528
|
* @returns {string}
|
|
374
529
|
*/
|
|
375
530
|
function unescapeHTML(input) {
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
531
|
+
const s = String(input);
|
|
532
|
+
if (!s.includes("&")) return s;
|
|
533
|
+
return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]*);/gi, (match, entity) => {
|
|
534
|
+
if (entity.charCodeAt(0) === 35) {
|
|
535
|
+
const hex = entity.charCodeAt(1) === 120 || entity.charCodeAt(1) === 88;
|
|
536
|
+
const code = parseInt(entity.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
537
|
+
// Zero, out-of-range numbers and lone surrogates are not characters: leave them as written.
|
|
538
|
+
return code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff) || Number.isNaN(code) ? match : String.fromCodePoint(code);
|
|
539
|
+
}
|
|
540
|
+
return HTML_ENTITIES[entity] ?? HTML_ENTITIES[entity.toLowerCase()] ?? match;
|
|
382
541
|
});
|
|
383
542
|
}
|
|
384
543
|
|
|
@@ -407,7 +566,7 @@ function escapeRegExp(input) {
|
|
|
407
566
|
* @returns {number}
|
|
408
567
|
*/
|
|
409
568
|
function length(input) {
|
|
410
|
-
return
|
|
569
|
+
return _count(String(input));
|
|
411
570
|
}
|
|
412
571
|
|
|
413
572
|
/**
|
|
@@ -463,10 +622,20 @@ function count(input, search) {
|
|
|
463
622
|
* @returns {string}
|
|
464
623
|
*/
|
|
465
624
|
function truncate(input, length, options = {}) {
|
|
625
|
+
const s = String(input);
|
|
626
|
+
// A string never has more visible characters than UTF-16 units.
|
|
627
|
+
if (s.length <= length) return s;
|
|
628
|
+
const chars = _graphemes(s);
|
|
629
|
+
if (chars.length <= length) return s;
|
|
466
630
|
const omission = options.omission ?? "…";
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
631
|
+
const keep = length - _count(omission);
|
|
632
|
+
// No room for anything but the omission, and maybe not all of it.
|
|
633
|
+
if (keep <= 0) return _graphemes(omission).slice(0, Math.max(0, length)).join("");
|
|
634
|
+
if (options.position === "start") return omission + chars.slice(chars.length - keep).join("").replace(/^\s+/, "");
|
|
635
|
+
if (options.position === "middle") {
|
|
636
|
+
const head = Math.ceil(keep / 2);
|
|
637
|
+
return chars.slice(0, head).join("") + omission + chars.slice(chars.length - (keep - head)).join("");
|
|
638
|
+
}
|
|
470
639
|
let sliced = chars.slice(0, keep).join("");
|
|
471
640
|
if (options.words) {
|
|
472
641
|
const nextIsSpace = /\s/.test(chars[keep] ?? " ");
|
|
@@ -476,6 +645,32 @@ function truncate(input, length, options = {}) {
|
|
|
476
645
|
return sliced.replace(/\s+$/, "") + omission;
|
|
477
646
|
}
|
|
478
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Splits text into pieces of `size` visible characters. The last one can be
|
|
650
|
+
* shorter.
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* str.chunk("4242424242424242", 4).join(" "); // "4242 4242 4242 4242"
|
|
654
|
+
*
|
|
655
|
+
* @param {unknown} input
|
|
656
|
+
* @param {number} size
|
|
657
|
+
* @returns {string[]}
|
|
658
|
+
*/
|
|
659
|
+
function chunk(input, size) {
|
|
660
|
+
const n = Math.floor(size);
|
|
661
|
+
/** @type {string[]} */
|
|
662
|
+
const out = [];
|
|
663
|
+
if (!(n >= 1)) return out;
|
|
664
|
+
const s = String(input);
|
|
665
|
+
if (shared.ASCII.test(s)) {
|
|
666
|
+
for (let i = 0; i < s.length; i += n) out.push(s.slice(i, i + n));
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
const chars = _graphemes(s);
|
|
670
|
+
for (let i = 0; i < chars.length; i += n) out.push(chars.slice(i, i + n).join(""));
|
|
671
|
+
return out;
|
|
672
|
+
}
|
|
673
|
+
|
|
479
674
|
/**
|
|
480
675
|
* Returns the text between `start` and the next `end`, or `undefined` if a
|
|
481
676
|
* marker is missing.
|
|
@@ -538,7 +733,8 @@ function lines(input) {
|
|
|
538
733
|
* @returns {string}
|
|
539
734
|
*/
|
|
540
735
|
function padStart(input, length, char = " ") {
|
|
541
|
-
|
|
736
|
+
const s = String(input);
|
|
737
|
+
return _padding(char, length - _count(s)) + s;
|
|
542
738
|
}
|
|
543
739
|
|
|
544
740
|
/**
|
|
@@ -553,7 +749,8 @@ function padStart(input, length, char = " ") {
|
|
|
553
749
|
* @returns {string}
|
|
554
750
|
*/
|
|
555
751
|
function padEnd(input, length, char = " ") {
|
|
556
|
-
|
|
752
|
+
const s = String(input);
|
|
753
|
+
return s + _padding(char, length - _count(s));
|
|
557
754
|
}
|
|
558
755
|
|
|
559
756
|
/**
|
|
@@ -569,11 +766,10 @@ function padEnd(input, length, char = " ") {
|
|
|
569
766
|
*/
|
|
570
767
|
function center(input, length, char = " ") {
|
|
571
768
|
const s = String(input);
|
|
572
|
-
const
|
|
573
|
-
if (
|
|
574
|
-
const total = length - visible;
|
|
769
|
+
const total = length - _count(s);
|
|
770
|
+
if (!(total > 0) || !char) return s;
|
|
575
771
|
const left = Math.floor(total / 2);
|
|
576
|
-
return char
|
|
772
|
+
return _padding(char, left) + s + _padding(char, total - left);
|
|
577
773
|
}
|
|
578
774
|
|
|
579
775
|
/**
|
|
@@ -620,8 +816,13 @@ function dedent(input, ...values) {
|
|
|
620
816
|
const all = text.replace(/\r\n?/g, "\n").split("\n");
|
|
621
817
|
if (all.length && all[0].trim() === "") all.shift();
|
|
622
818
|
if (all.length && all[all.length - 1].trim() === "") all.pop();
|
|
623
|
-
|
|
624
|
-
const
|
|
819
|
+
let min = Infinity;
|
|
820
|
+
for (const line of all) {
|
|
821
|
+
if (!line.trim()) continue;
|
|
822
|
+
const indentation = /^[ \t]*/.exec(line)?.[0].length ?? 0;
|
|
823
|
+
if (indentation < min) min = indentation;
|
|
824
|
+
}
|
|
825
|
+
if (min === Infinity || min === 0) return all.join("\n");
|
|
625
826
|
return all.map((line) => line.slice(min)).join("\n");
|
|
626
827
|
}
|
|
627
828
|
|
|
@@ -646,28 +847,38 @@ function wrap(input, options = {}) {
|
|
|
646
847
|
const out = [];
|
|
647
848
|
for (const paragraph of String(input).split(/\r\n|\r|\n/)) {
|
|
648
849
|
let line = "";
|
|
649
|
-
|
|
650
|
-
|
|
850
|
+
let lineWidth = 0;
|
|
851
|
+
for (let word of paragraph.split(/\s+/)) {
|
|
852
|
+
if (!word) continue;
|
|
853
|
+
let wordWidth = _count(word);
|
|
854
|
+
if (opts.cut && wordWidth > width) {
|
|
651
855
|
if (line) {
|
|
652
856
|
out.push(line);
|
|
653
857
|
line = "";
|
|
858
|
+
lineWidth = 0;
|
|
654
859
|
}
|
|
655
860
|
const chars = _graphemes(word);
|
|
656
|
-
|
|
657
|
-
|
|
861
|
+
let start = 0;
|
|
862
|
+
while (chars.length - start > width) {
|
|
863
|
+
out.push(chars.slice(start, start + width).join(""));
|
|
864
|
+
start += width;
|
|
865
|
+
}
|
|
866
|
+
word = chars.slice(start).join("");
|
|
867
|
+
wordWidth = chars.length - start;
|
|
868
|
+
if (!word) continue;
|
|
658
869
|
}
|
|
659
|
-
if (
|
|
660
|
-
const candidate = line ? `${line} ${word}` : word;
|
|
661
|
-
if (_graphemes(candidate).length > width && line) {
|
|
870
|
+
if (line && lineWidth + 1 + wordWidth > width) {
|
|
662
871
|
out.push(line);
|
|
663
872
|
line = word;
|
|
873
|
+
lineWidth = wordWidth;
|
|
664
874
|
} else {
|
|
665
|
-
line
|
|
875
|
+
lineWidth += (line ? 1 : 0) + wordWidth;
|
|
876
|
+
line = line ? `${line} ${word}` : word;
|
|
666
877
|
}
|
|
667
878
|
}
|
|
668
879
|
out.push(line);
|
|
669
880
|
}
|
|
670
|
-
return out.map((l) => prefix + l).join(newline);
|
|
881
|
+
return prefix ? out.map((l) => prefix + l).join(newline) : out.join(newline);
|
|
671
882
|
}
|
|
672
883
|
|
|
673
884
|
/**
|
|
@@ -746,10 +957,10 @@ function initials(input, max = 2) {
|
|
|
746
957
|
function plural(value, forms, options = {}) {
|
|
747
958
|
const locale = options.locale ?? "en";
|
|
748
959
|
if (value === 0 && forms.zero !== undefined) return forms.zero;
|
|
749
|
-
const category =
|
|
960
|
+
const category = intl.plural(locale, options.ordinal ? ORDINAL : undefined).select(value);
|
|
750
961
|
const word = forms[category] ?? forms.other;
|
|
751
962
|
if (options.includeCount === false) return word;
|
|
752
|
-
return `${
|
|
963
|
+
return `${intl.number(locale).format(value)} ${word}`;
|
|
753
964
|
}
|
|
754
965
|
|
|
755
966
|
/**
|
|
@@ -768,13 +979,30 @@ function plural(value, forms, options = {}) {
|
|
|
768
979
|
* @returns {string}
|
|
769
980
|
*/
|
|
770
981
|
function template(input, data, options = {}) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
982
|
+
let regex = TEMPLATE_DEFAULT;
|
|
983
|
+
if (options.open !== undefined || options.close !== undefined) {
|
|
984
|
+
const open = options.open ?? "{";
|
|
985
|
+
const close = options.close ?? "}";
|
|
986
|
+
const key = `${open}\u0000${close}`;
|
|
987
|
+
const cached = _templateRegexes.get(key);
|
|
988
|
+
if (cached) regex = cached;
|
|
989
|
+
else {
|
|
990
|
+
regex = new RegExp(`${_escapeRegExp(open)}\\s*([\\w$.-]+)\\s*${_escapeRegExp(close)}`, "g");
|
|
991
|
+
if (_templateRegexes.size >= 50) _templateRegexes.clear();
|
|
992
|
+
_templateRegexes.set(key, regex);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
774
995
|
return String(input).replace(regex, (match, key) => {
|
|
775
|
-
/** @type {
|
|
996
|
+
/** @type {unknown} */
|
|
776
997
|
let value = data;
|
|
777
|
-
|
|
998
|
+
if (key.includes(".")) {
|
|
999
|
+
for (const part of key.split(".")) {
|
|
1000
|
+
if (value === undefined || value === null) break;
|
|
1001
|
+
value = _templateValue(value, part);
|
|
1002
|
+
}
|
|
1003
|
+
} else if (value !== undefined && value !== null) {
|
|
1004
|
+
value = _templateValue(value, key);
|
|
1005
|
+
}
|
|
778
1006
|
if (value === undefined || value === null) return options.fallback ?? match;
|
|
779
1007
|
return String(value);
|
|
780
1008
|
});
|
|
@@ -856,7 +1084,7 @@ function removeSuffix(input, suffix) {
|
|
|
856
1084
|
function compare(a, b, options = {}) {
|
|
857
1085
|
const collator =
|
|
858
1086
|
options && typeof options === "object" && (options.locale || options.sensitivity || options.numeric === false)
|
|
859
|
-
?
|
|
1087
|
+
? intl.collator(options.locale, { numeric: options.numeric ?? true, sensitivity: options.sensitivity ?? "variant" })
|
|
860
1088
|
: (_defaultCollator ??= new Intl.Collator(undefined, { numeric: true, sensitivity: "variant" }));
|
|
861
1089
|
return collator.compare(String(a), String(b));
|
|
862
1090
|
}
|
|
@@ -872,20 +1100,9 @@ function compare(a, b, options = {}) {
|
|
|
872
1100
|
* @returns {number}
|
|
873
1101
|
*/
|
|
874
1102
|
function levenshtein(a, b) {
|
|
875
|
-
const
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
if (!t.length) return s.length;
|
|
879
|
-
let previous = Array.from({ length: t.length + 1 }, (_, i) => i);
|
|
880
|
-
for (let i = 1; i <= s.length; i++) {
|
|
881
|
-
const current = [i];
|
|
882
|
-
for (let j = 1; j <= t.length; j++) {
|
|
883
|
-
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
884
|
-
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
|
|
885
|
-
}
|
|
886
|
-
previous = current;
|
|
887
|
-
}
|
|
888
|
-
return previous[t.length];
|
|
1103
|
+
const x = String(a);
|
|
1104
|
+
const y = String(b);
|
|
1105
|
+
return x === y ? 0 : _distance(_units(x), _units(y));
|
|
889
1106
|
}
|
|
890
1107
|
|
|
891
1108
|
/**
|
|
@@ -899,8 +1116,13 @@ function levenshtein(a, b) {
|
|
|
899
1116
|
* @returns {number}
|
|
900
1117
|
*/
|
|
901
1118
|
function similarity(a, b) {
|
|
902
|
-
const
|
|
903
|
-
|
|
1119
|
+
const x = String(a);
|
|
1120
|
+
const y = String(b);
|
|
1121
|
+
if (x === y) return 1;
|
|
1122
|
+
const s = _units(x);
|
|
1123
|
+
const t = _units(y);
|
|
1124
|
+
const longest = Math.max(s.length, t.length);
|
|
1125
|
+
return longest === 0 ? 1 : 1 - _distance(s, t) / longest;
|
|
904
1126
|
}
|
|
905
1127
|
|
|
906
1128
|
/**
|
|
@@ -919,11 +1141,15 @@ function similarity(a, b) {
|
|
|
919
1141
|
function closest(input, candidates, options = {}) {
|
|
920
1142
|
const threshold = options.threshold ?? 0.4;
|
|
921
1143
|
const norm = (/** @type {string} */ s) => (options.caseSensitive ? s : s.toLowerCase());
|
|
922
|
-
const needle = norm(String(input));
|
|
1144
|
+
const needle = _units(norm(String(input)));
|
|
923
1145
|
let best;
|
|
924
1146
|
let bestScore = -1;
|
|
925
1147
|
for (const candidate of candidates) {
|
|
926
|
-
const
|
|
1148
|
+
const units = _units(norm(candidate));
|
|
1149
|
+
const longest = Math.max(needle.length, units.length);
|
|
1150
|
+
// The length gap alone bounds the score: skip candidates that can't win.
|
|
1151
|
+
if (longest > 0 && 1 - Math.abs(needle.length - units.length) / longest <= bestScore) continue;
|
|
1152
|
+
const score = longest === 0 ? 1 : 1 - _distance(needle, units) / longest;
|
|
927
1153
|
if (score > bestScore) {
|
|
928
1154
|
bestScore = score;
|
|
929
1155
|
best = candidate;
|
|
@@ -953,6 +1179,9 @@ function random(size = 16, options = {}) {
|
|
|
953
1179
|
|
|
954
1180
|
module.exports = {
|
|
955
1181
|
capitalize,
|
|
1182
|
+
upperFirst,
|
|
1183
|
+
lowerFirst,
|
|
1184
|
+
humanize,
|
|
956
1185
|
titleCase,
|
|
957
1186
|
sentenceCase,
|
|
958
1187
|
camelCase,
|
|
@@ -973,6 +1202,7 @@ module.exports = {
|
|
|
973
1202
|
byteLength,
|
|
974
1203
|
count,
|
|
975
1204
|
truncate,
|
|
1205
|
+
chunk,
|
|
976
1206
|
between,
|
|
977
1207
|
splitOnce,
|
|
978
1208
|
lines,
|