@ix-xs/node-comfort 1.1.31 → 1.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/src/Str.js ADDED
@@ -0,0 +1,452 @@
1
+ /**
2
+ * String utilities.
3
+ *
4
+ * A curated set of the string helpers you end up rewriting in every project:
5
+ * case conversion, slugs, truncation, padding, template rendering, and more.
6
+ * Zero dependencies, Unicode-aware where it matters.
7
+ */
8
+
9
+ /**
10
+ * Splits a string into its "words", handling camelCase, PascalCase,
11
+ * snake_case, kebab-case, spaces and digits.
12
+ * @private
13
+ * @param {string} str
14
+ * @returns {string[]}
15
+ */
16
+ const _words = (str) => {
17
+ return String(str)
18
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
19
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
20
+ .replace(/[_\-.]+/g, " ")
21
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
22
+ .trim()
23
+ .split(/\s+/)
24
+ .filter(Boolean);
25
+ };
26
+
27
+ module.exports = {
28
+ /**
29
+ * Capitalizes the first character of a string and lowercases the rest.
30
+ *
31
+ * @example
32
+ * nodeComfort.str.capitalize("hELLO"); // "Hello"
33
+ *
34
+ * @param {string} str - Input string.
35
+ * @returns {string} The capitalized string.
36
+ */
37
+ capitalize(str) {
38
+ str = String(str);
39
+ if (!str) return str;
40
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
41
+ },
42
+
43
+ /**
44
+ * Uppercases the first character of each word (Title Case), leaving the rest untouched.
45
+ *
46
+ * @example
47
+ * nodeComfort.str.titleCase("the quick brown fox"); // "The Quick Brown Fox"
48
+ *
49
+ * @param {string} str - Input string.
50
+ * @returns {string} The title-cased string.
51
+ */
52
+ titleCase(str) {
53
+ return String(str).replace(/\b\p{L}/gu, (c) => c.toUpperCase());
54
+ },
55
+
56
+ /**
57
+ * Converts a string to `camelCase`.
58
+ *
59
+ * @example
60
+ * nodeComfort.str.camelCase("hello world"); // "helloWorld"
61
+ * nodeComfort.str.camelCase("foo-bar_baz"); // "fooBarBaz"
62
+ *
63
+ * @param {string} str - Input string.
64
+ * @returns {string} The camelCased string.
65
+ */
66
+ camelCase(str) {
67
+ return _words(str)
68
+ .map((word, i) =>
69
+ i === 0
70
+ ? word.toLowerCase()
71
+ : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
72
+ )
73
+ .join("");
74
+ },
75
+
76
+ /**
77
+ * Converts a string to `PascalCase`.
78
+ *
79
+ * @example
80
+ * nodeComfort.str.pascalCase("hello world"); // "HelloWorld"
81
+ *
82
+ * @param {string} str - Input string.
83
+ * @returns {string} The PascalCased string.
84
+ */
85
+ pascalCase(str) {
86
+ return _words(str)
87
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
88
+ .join("");
89
+ },
90
+
91
+ /**
92
+ * Converts a string to `snake_case`.
93
+ *
94
+ * @example
95
+ * nodeComfort.str.snakeCase("helloWorld"); // "hello_world"
96
+ *
97
+ * @param {string} str - Input string.
98
+ * @returns {string} The snake_cased string.
99
+ */
100
+ snakeCase(str) {
101
+ return _words(str).map((word) => word.toLowerCase()).join("_");
102
+ },
103
+
104
+ /**
105
+ * Converts a string to `kebab-case`.
106
+ *
107
+ * @example
108
+ * nodeComfort.str.kebabCase("helloWorld"); // "hello-world"
109
+ *
110
+ * @param {string} str - Input string.
111
+ * @returns {string} The kebab-cased string.
112
+ */
113
+ kebabCase(str) {
114
+ return _words(str).map((word) => word.toLowerCase()).join("-");
115
+ },
116
+
117
+ /**
118
+ * Converts a string to `CONSTANT_CASE`.
119
+ *
120
+ * @example
121
+ * nodeComfort.str.constantCase("helloWorld"); // "HELLO_WORLD"
122
+ *
123
+ * @param {string} str - Input string.
124
+ * @returns {string} The CONSTANT_CASED string.
125
+ */
126
+ constantCase(str) {
127
+ return _words(str).map((word) => word.toUpperCase()).join("_");
128
+ },
129
+
130
+ /**
131
+ * Converts a string into a URL-friendly slug.
132
+ *
133
+ * Accents/diacritics are stripped, non-alphanumeric runs become the separator.
134
+ *
135
+ * @example
136
+ * nodeComfort.str.slugify("Héllo, World!"); // "hello-world"
137
+ * nodeComfort.str.slugify("A B C", { separator: "_" }); // "a_b_c"
138
+ *
139
+ * @param {string} str - Input string.
140
+ * @param {{ separator?: string, lower?: boolean }} [options] - Slug options.
141
+ * @returns {string} The slug.
142
+ */
143
+ slugify(str, options = {}) {
144
+ const separator = options.separator ?? "-";
145
+ const lower = options.lower ?? true;
146
+ const result = String(str)
147
+ .normalize("NFKD")
148
+ .replace(/[̀-ͯ]/g, "")
149
+ .replace(/[^\p{L}\p{N}]+/gu, separator)
150
+ .replace(new RegExp(`\\${separator}{2,}`, "g"), separator)
151
+ .replace(new RegExp(`^\\${separator}|\\${separator}$`, "g"), "");
152
+ return lower ? result.toLowerCase() : result;
153
+ },
154
+
155
+ /**
156
+ * Truncates a string to a maximum length, appending an ellipsis if cut.
157
+ *
158
+ * @example
159
+ * nodeComfort.str.truncate("Hello world", 8); // "Hello…"
160
+ * nodeComfort.str.truncate("Hello world", 8, { omission: "..." }); // "Hello..."
161
+ *
162
+ * @param {string} str - Input string.
163
+ * @param {number} length - Maximum length of the result (including the omission).
164
+ * @param {{ omission?: string, words?: boolean }} [options] - Truncation options.
165
+ * @returns {string} The truncated string.
166
+ */
167
+ truncate(str, length, options = {}) {
168
+ str = String(str);
169
+ const omission = options.omission ?? "…";
170
+ if (str.length <= length) return str;
171
+
172
+ const cut = Math.max(0, length - omission.length);
173
+ let sliced = str.slice(0, cut);
174
+
175
+ if (options.words) {
176
+ const lastSpace = sliced.lastIndexOf(" ");
177
+ if (lastSpace > 0) sliced = sliced.slice(0, lastSpace);
178
+ }
179
+ return sliced.replace(/\s+$/, "") + omission;
180
+ },
181
+
182
+ /**
183
+ * Pads a string on the left (start) to a target length.
184
+ *
185
+ * @param {string} str - Input string.
186
+ * @param {number} length - Target total length.
187
+ * @param {string} [char=" "] - Padding character(s).
188
+ * @returns {string} The padded string.
189
+ */
190
+ padStart(str, length, char = " ") {
191
+ return String(str).padStart(length, char);
192
+ },
193
+
194
+ /**
195
+ * Pads a string on the right (end) to a target length.
196
+ *
197
+ * @param {string} str - Input string.
198
+ * @param {number} length - Target total length.
199
+ * @param {string} [char=" "] - Padding character(s).
200
+ * @returns {string} The padded string.
201
+ */
202
+ padEnd(str, length, char = " ") {
203
+ return String(str).padEnd(length, char);
204
+ },
205
+
206
+ /**
207
+ * Centers a string within a target length by padding both sides.
208
+ *
209
+ * @example
210
+ * nodeComfort.str.center("hi", 6); // " hi "
211
+ * nodeComfort.str.center("hi", 6, "*"); // "**hi**"
212
+ *
213
+ * @param {string} str - Input string.
214
+ * @param {number} length - Target total length.
215
+ * @param {string} [char=" "] - Padding character.
216
+ * @returns {string} The centered string.
217
+ */
218
+ center(str, length, char = " ") {
219
+ str = String(str);
220
+ if (str.length >= length) return str;
221
+ const total = length - str.length;
222
+ const left = Math.floor(total / 2);
223
+ const right = total - left;
224
+ return char.repeat(left) + str + char.repeat(right);
225
+ },
226
+
227
+ /**
228
+ * Reverses a string (Unicode code-point aware, so emojis stay intact).
229
+ *
230
+ * @example
231
+ * nodeComfort.str.reverse("abc"); // "cba"
232
+ *
233
+ * @param {string} str - Input string.
234
+ * @returns {string} The reversed string.
235
+ */
236
+ reverse(str) {
237
+ return [...String(str)].reverse().join("");
238
+ },
239
+
240
+ /**
241
+ * Collapses all runs of whitespace into single spaces and trims the ends.
242
+ *
243
+ * @example
244
+ * nodeComfort.str.squish(" hello world \n"); // "hello world"
245
+ *
246
+ * @param {string} str - Input string.
247
+ * @returns {string} The normalized string.
248
+ */
249
+ squish(str) {
250
+ return String(str).replace(/\s+/g, " ").trim();
251
+ },
252
+
253
+ /**
254
+ * Removes all HTML tags from a string.
255
+ *
256
+ * @example
257
+ * nodeComfort.str.stripTags("<b>Hi</b> there"); // "Hi there"
258
+ *
259
+ * @param {string} str - Input string.
260
+ * @returns {string} The string with tags removed.
261
+ */
262
+ stripTags(str) {
263
+ return String(str).replace(/<\/?[^>]+(>|$)/g, "");
264
+ },
265
+
266
+ /**
267
+ * Escapes HTML-sensitive characters (`& < > " '`).
268
+ *
269
+ * @example
270
+ * nodeComfort.str.escapeHTML('<a href="x">'); // "&lt;a href=&quot;x&quot;&gt;"
271
+ *
272
+ * @param {string} str - Input string.
273
+ * @returns {string} The escaped string.
274
+ */
275
+ escapeHTML(str) {
276
+ const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" };
277
+ return String(str).replace(/[&<>"']/g, (c) => map[c]);
278
+ },
279
+
280
+ /**
281
+ * Reverses {@link escapeHTML}: turns HTML entities back into characters.
282
+ *
283
+ * @param {string} str - Input string.
284
+ * @returns {string} The unescaped string.
285
+ */
286
+ unescapeHTML(str) {
287
+ const map = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": "\"", "&#39;": "'", "&#x27;": "'" };
288
+ return String(str).replace(/&(amp|lt|gt|quot|#39|#x27);/g, (m) => map[m]);
289
+ },
290
+
291
+ /**
292
+ * Escapes a string for safe use inside a `RegExp`.
293
+ *
294
+ * @example
295
+ * new RegExp(nodeComfort.str.escapeRegExp("a.b")); // matches literal "a.b"
296
+ *
297
+ * @param {string} str - Input string.
298
+ * @returns {string} The escaped string.
299
+ */
300
+ escapeRegExp(str) {
301
+ return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
302
+ },
303
+
304
+ /**
305
+ * Counts the number of non-overlapping occurrences of a substring.
306
+ *
307
+ * @example
308
+ * nodeComfort.str.count("banana", "a"); // 3
309
+ *
310
+ * @param {string} str - Input string.
311
+ * @param {string} sub - Substring to count.
312
+ * @returns {number} Number of occurrences.
313
+ */
314
+ count(str, sub) {
315
+ str = String(str);
316
+ if (!sub) return 0;
317
+ let count = 0;
318
+ let pos = str.indexOf(sub);
319
+ while (pos !== -1) {
320
+ count++;
321
+ pos = str.indexOf(sub, pos + sub.length);
322
+ }
323
+ return count;
324
+ },
325
+
326
+ /**
327
+ * Ensures a string starts with a given prefix (adds it only if missing).
328
+ *
329
+ * @example
330
+ * nodeComfort.str.ensurePrefix("example.com", "https://"); // "https://example.com"
331
+ *
332
+ * @param {string} str - Input string.
333
+ * @param {string} prefix - Prefix to ensure.
334
+ * @returns {string} The string with the prefix guaranteed.
335
+ */
336
+ ensurePrefix(str, prefix) {
337
+ str = String(str);
338
+ return str.startsWith(prefix) ? str : prefix + str;
339
+ },
340
+
341
+ /**
342
+ * Ensures a string ends with a given suffix (adds it only if missing).
343
+ *
344
+ * @example
345
+ * nodeComfort.str.ensureSuffix("path", "/"); // "path/"
346
+ *
347
+ * @param {string} str - Input string.
348
+ * @param {string} suffix - Suffix to ensure.
349
+ * @returns {string} The string with the suffix guaranteed.
350
+ */
351
+ ensureSuffix(str, suffix) {
352
+ str = String(str);
353
+ return str.endsWith(suffix) ? str : str + suffix;
354
+ },
355
+
356
+ /**
357
+ * Removes a prefix from a string if present.
358
+ *
359
+ * @param {string} str - Input string.
360
+ * @param {string} prefix - Prefix to remove.
361
+ * @returns {string} The string without the prefix.
362
+ */
363
+ removePrefix(str, prefix) {
364
+ str = String(str);
365
+ return str.startsWith(prefix) ? str.slice(prefix.length) : str;
366
+ },
367
+
368
+ /**
369
+ * Removes a suffix from a string if present.
370
+ *
371
+ * @param {string} str - Input string.
372
+ * @param {string} suffix - Suffix to remove.
373
+ * @returns {string} The string without the suffix.
374
+ */
375
+ removeSuffix(str, suffix) {
376
+ str = String(str);
377
+ return suffix && str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
378
+ },
379
+
380
+ /**
381
+ * Renders a template, replacing `{key}` placeholders with values from `data`.
382
+ *
383
+ * Missing keys are left as-is by default. Nested paths (`{user.name}`) are supported.
384
+ *
385
+ * @example
386
+ * nodeComfort.str.template("Hi {name}, you have {count} msgs", { name: "Jo", count: 3 });
387
+ * // "Hi Jo, you have 3 msgs"
388
+ *
389
+ * @param {string} str - Template string.
390
+ * @param {Record<string, any>} data - Replacement values.
391
+ * @param {{ open?: string, close?: string, fallback?: string }} [options] - Delimiters / fallback.
392
+ * @returns {string} The rendered string.
393
+ */
394
+ template(str, data, options = {}) {
395
+ const open = options.open ?? "{";
396
+ const close = options.close ?? "}";
397
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
398
+ const regex = new RegExp(`${esc(open)}\\s*([\\w.]+)\\s*${esc(close)}`, "g");
399
+ return String(str).replace(regex, (match, key) => {
400
+ const value = key.split(".").reduce((acc, k) => (acc == null ? acc : acc[k]), data);
401
+ if (value === undefined || value === null) {
402
+ return options.fallback !== undefined ? options.fallback : match;
403
+ }
404
+ return String(value);
405
+ });
406
+ },
407
+
408
+ /**
409
+ * Splits a string into an array of "words" (camelCase / snake / kebab aware).
410
+ *
411
+ * @example
412
+ * nodeComfort.str.words("helloWorld-foo_bar"); // ["hello", "World", "foo", "bar"]
413
+ *
414
+ * @param {string} str - Input string.
415
+ * @returns {string[]} The words.
416
+ */
417
+ words(str) {
418
+ return _words(str);
419
+ },
420
+
421
+ /**
422
+ * Returns the visible length of a string in code points (emoji-safe).
423
+ *
424
+ * @example
425
+ * "😀".length; // 2
426
+ * nodeComfort.str.length("😀"); // 1
427
+ *
428
+ * @param {string} str - Input string.
429
+ * @returns {number} The number of code points.
430
+ */
431
+ length(str) {
432
+ return [...String(str)].length;
433
+ },
434
+
435
+ /**
436
+ * Generates a random alphanumeric string of a given length.
437
+ *
438
+ * Note: uses `Math.random` and is **not** cryptographically secure.
439
+ * For tokens/IDs use `nodeComfort.id.token()` instead.
440
+ *
441
+ * @param {number} [length=16] - Desired length.
442
+ * @param {string} [charset] - Characters to pick from.
443
+ * @returns {string} The random string.
444
+ */
445
+ random(length = 16, charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") {
446
+ let out = "";
447
+ for (let i = 0; i < length; i++) {
448
+ out += charset.charAt(Math.floor(Math.random() * charset.length));
449
+ }
450
+ return out;
451
+ },
452
+ };