@seedcord/utils 0.6.1-next.0 → 0.7.0-next.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/dist/index.mjs CHANGED
@@ -6,6 +6,19 @@ import * as path from "node:path";
6
6
  * Exhaustiveness guard for discriminated unions. Place in the `default` branch of a `switch` over a
7
7
  * union's discriminant: if a new variant is added without a matching case, the call fails to compile.
8
8
  * Throws at runtime if reached with a value the types said was impossible.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * type Shape = { kind: 'circle' } | { kind: 'square' };
13
+ *
14
+ * function area(shape: Shape): number {
15
+ * switch (shape.kind) {
16
+ * case 'circle': return Math.PI;
17
+ * case 'square': return 1;
18
+ * default: return assertNever(shape); // compile error if a Shape variant is added
19
+ * }
20
+ * }
21
+ * ```
9
22
  */
10
23
  function assertNever(value) {
11
24
  throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);
@@ -28,6 +41,17 @@ function isTsOrJsFile(entry) {
28
41
  * @param dir - The directory path to traverse.
29
42
  * @param callback - A function that will be called for each imported module. It receives the full file path, the file's relative path, and the imported module as arguments.
30
43
  * @returns A Promise that resolves when the traversal is complete.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * await traverseDirectory(
48
+ * './commands',
49
+ * (fullPath, relativePath, imported) => {
50
+ * for (const exported of Object.values(imported)) register(exported);
51
+ * },
52
+ * logger
53
+ * );
54
+ * ```
31
55
  */
32
56
  async function traverseDirectory(dir, callback, logger) {
33
57
  let entries;
@@ -49,6 +73,14 @@ async function traverseDirectory(dir, callback, logger) {
49
73
  * @param filePath - The file path to format.
50
74
  * @param options - Formatting options.
51
75
  * @returns The formatted file path.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * // cwd is /repo
80
+ * formatFilePath('/repo/src/Bot.ts'); // './src/Bot.ts'
81
+ * formatFilePath('/repo/src/Bot.ts', { onlyDir: true }); // './src'
82
+ * formatFilePath('/repo/src/Bot.ts', { prefix: '' }); // 'src/Bot.ts'
83
+ * ```
52
84
  */
53
85
  function formatFilePath(filePath, options = {}) {
54
86
  const { onlyDir = false, prefix = "./" } = options;
@@ -104,6 +136,11 @@ function currentTime() {
104
136
  *
105
137
  * @param digits - The number of digits for the generated code.
106
138
  * @returns A random numeric code with the specified number of digits.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * generateCode(6); // e.g. 482915
143
+ * ```
107
144
  */
108
145
  function generateCode(digits) {
109
146
  const min = Math.pow(10, digits - 1);
@@ -157,6 +194,14 @@ const DURATION_PATTERN = new RegExp(`^(\\d+)(${Object.keys(UNIT_MS).join("|")})$
157
194
  *
158
195
  * @param input - The duration string to parse.
159
196
  * @returns The duration in milliseconds, or `null` if `input` is not a well-formed positive duration.
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * parseDuration('24h'); // 86400000
201
+ * parseDuration('30m'); // 1800000
202
+ * parseDuration('1.5h'); // null
203
+ * parseDuration('foo'); // null
204
+ * ```
160
205
  */
161
206
  function parseDuration(input) {
162
207
  const match = DURATION_PATTERN.exec(input);
@@ -174,6 +219,12 @@ function parseDuration(input) {
174
219
  * @param num2 - The second number.
175
220
  *
176
221
  * @returns The percentage of the first number in the second number with two decimal places.
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * percentage(25, 200); // 12.5
226
+ * percentage(1, 3); // 33.33
227
+ * ```
177
228
  */
178
229
  function percentage(num1, num2) {
179
230
  return Number((num1 / num2 * 100).toFixed(2));
@@ -187,6 +238,12 @@ function percentage(num1, num2) {
187
238
  * @param num - The number to be rounded.
188
239
  * @param precision - The number of decimal places to round to.
189
240
  * @returns The rounded number.
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * round(3.14159, 2); // 3.14
245
+ * round(2.5, 0); // 3
246
+ * ```
190
247
  */
191
248
  function round(num, precision) {
192
249
  const factor = Math.pow(10, precision);
@@ -430,6 +487,11 @@ function keepDefined(source, ...keys) {
430
487
  * Returns the word with its first letter capitalized and the rest in lowercase.
431
488
  * @param word - The word to be formatted.
432
489
  * @returns The formatted word.
490
+ *
491
+ * @example
492
+ * ```ts
493
+ * capitalize('hELLO'); // 'Hello'
494
+ * ```
433
495
  */
434
496
  function capitalize(word) {
435
497
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
@@ -442,55 +504,320 @@ function capitalize(word) {
442
504
  *
443
505
  * @param arr - The array of strings or numbers
444
506
  * @returns The length of the longest element when converted to string
507
+ *
508
+ * @example
509
+ * ```ts
510
+ * longestStringLength(['ab', 12345]); // 5
511
+ * ```
445
512
  */
446
513
  function longestStringLength(arr) {
447
514
  return Math.max(...arr.map((el) => el.toString().length));
448
515
  }
449
516
 
450
517
  //#endregion
451
- //#region src/strings/generateAsciiTable.ts
452
- /**
453
- * Generates an ASCII table from the provided data.
454
- *
455
- * @param data - The data to be displayed in the table.
456
- * @returns The generated ASCII table as a string.
457
- */
458
- function generateAsciiTable(data) {
459
- if (data.length === 0) return "";
460
- const firstRow = data[0];
461
- if (!firstRow || firstRow.length === 0) return "";
462
- let table = "";
463
- const columnWidths = [];
464
- for (let i = 0; i < firstRow.length; i++) {
465
- let maxWidth = 0;
466
- for (const row of data) {
467
- const cell = row[i];
468
- if (cell !== void 0) maxWidth = Math.max(maxWidth, cell.length);
518
+ //#region src/strings/renderTable/borders.ts
519
+ const DOUBLE = {
520
+ top: {
521
+ left: "╔",
522
+ mid: "╦",
523
+ right: "╗",
524
+ fill: "═"
525
+ },
526
+ bottom: {
527
+ left: "╚",
528
+ mid: "",
529
+ right: "",
530
+ fill: "═"
531
+ },
532
+ sep: {
533
+ left: "╟",
534
+ mid: "╫",
535
+ right: "╢",
536
+ fill: "─"
537
+ },
538
+ headerSep: {
539
+ left: "╠",
540
+ mid: "╬",
541
+ right: "╣",
542
+ fill: "═"
543
+ },
544
+ vertical: "║"
545
+ };
546
+ const ROUNDED = {
547
+ top: {
548
+ left: "╭",
549
+ mid: "┬",
550
+ right: "╮",
551
+ fill: "─"
552
+ },
553
+ bottom: {
554
+ left: "╰",
555
+ mid: "┴",
556
+ right: "╯",
557
+ fill: "─"
558
+ },
559
+ sep: {
560
+ left: "├",
561
+ mid: "┼",
562
+ right: "┤",
563
+ fill: "─"
564
+ },
565
+ headerSep: {
566
+ left: "├",
567
+ mid: "┼",
568
+ right: "┤",
569
+ fill: "─"
570
+ },
571
+ vertical: "│"
572
+ };
573
+ const ASCII = {
574
+ top: {
575
+ left: "+",
576
+ mid: "+",
577
+ right: "+",
578
+ fill: "-"
579
+ },
580
+ bottom: {
581
+ left: "+",
582
+ mid: "+",
583
+ right: "+",
584
+ fill: "-"
585
+ },
586
+ sep: {
587
+ left: "+",
588
+ mid: "+",
589
+ right: "+",
590
+ fill: "-"
591
+ },
592
+ headerSep: {
593
+ left: "+",
594
+ mid: "+",
595
+ right: "+",
596
+ fill: "-"
597
+ },
598
+ vertical: "|"
599
+ };
600
+ const BORDERS = {
601
+ double: DOUBLE,
602
+ rounded: ROUNDED,
603
+ ascii: ASCII
604
+ };
605
+
606
+ //#endregion
607
+ //#region src/strings/renderTable/displayWidth.ts
608
+ const segmenter = new Intl.Segmenter();
609
+ const ZERO_WIDTH_RANGES = [
610
+ [8203, 8203],
611
+ [768, 879],
612
+ [6832, 6911],
613
+ [7616, 7679],
614
+ [8400, 8447],
615
+ [65056, 65071]
616
+ ];
617
+ const WIDE_RANGES = [
618
+ [4352, 4447],
619
+ [11904, 12350],
620
+ [12353, 13311],
621
+ [13312, 19903],
622
+ [19968, 40959],
623
+ [40960, 42191],
624
+ [44032, 55203],
625
+ [63744, 64255],
626
+ [65072, 65103],
627
+ [65280, 65376],
628
+ [65504, 65510],
629
+ [127744, 129791],
630
+ [131072, 262141]
631
+ ];
632
+ function inRanges(cp, ranges) {
633
+ return ranges.some(([lo, hi]) => cp >= lo && cp <= hi);
634
+ }
635
+ function segmentWidth(segment) {
636
+ const cp = segment.codePointAt(0) ?? 0;
637
+ if (inRanges(cp, ZERO_WIDTH_RANGES)) return 0;
638
+ return inRanges(cp, WIDE_RANGES) ? 2 : 1;
639
+ }
640
+ function displayWidth(text) {
641
+ let width = 0;
642
+ for (const { segment } of segmenter.segment(text)) width += segmentWidth(segment);
643
+ return width;
644
+ }
645
+ function segments(text) {
646
+ return Array.from(segmenter.segment(text), (entry) => entry.segment);
647
+ }
648
+ function takeWidth(text, maxColumns) {
649
+ let width = 0;
650
+ let taken = "";
651
+ for (const segment of segments(text)) {
652
+ const next = width + segmentWidth(segment);
653
+ if (next > maxColumns) break;
654
+ width = next;
655
+ taken += segment;
656
+ }
657
+ return taken;
658
+ }
659
+ function hardBreak(token, maxColumns) {
660
+ const pieces = [];
661
+ let rest = token;
662
+ while (displayWidth(rest) > maxColumns) {
663
+ const head = takeWidth(rest, maxColumns);
664
+ pieces.push(head);
665
+ rest = rest.slice(head.length);
666
+ }
667
+ if (rest.length > 0) pieces.push(rest);
668
+ return pieces;
669
+ }
670
+ function wrapText(text, maxColumns) {
671
+ const lines = [];
672
+ let current = "";
673
+ for (const token of text.split(" ")) {
674
+ const candidate = current === "" ? token : `${current} ${token}`;
675
+ if (displayWidth(candidate) <= maxColumns) {
676
+ current = candidate;
677
+ continue;
678
+ }
679
+ if (current !== "") lines.push(current);
680
+ if (displayWidth(token) <= maxColumns) {
681
+ current = token;
682
+ continue;
469
683
  }
470
- columnWidths.push(maxWidth);
684
+ const pieces = hardBreak(token, maxColumns);
685
+ current = pieces.pop() ?? "";
686
+ lines.push(...pieces);
471
687
  }
472
- const createLine = (char, left, intersect, right) => {
473
- let line = left;
474
- columnWidths.forEach((width, index) => {
475
- line += char.repeat(width + 2);
476
- if (index < columnWidths.length - 1) line += intersect;
477
- else line += right;
478
- });
479
- line += "\n";
480
- return line;
688
+ if (current !== "" || lines.length === 0) lines.push(current);
689
+ return lines;
690
+ }
691
+
692
+ //#endregion
693
+ //#region src/strings/renderTable/cells.ts
694
+ const ELLIPSIS = "…";
695
+ function wrapFence(content) {
696
+ return `\`\`\`\n${content}\`\`\``;
697
+ }
698
+ function truncate(content, maxWidth) {
699
+ if (displayWidth(content) <= maxWidth) return content;
700
+ return takeWidth(content, maxWidth - displayWidth(ELLIPSIS)) + ELLIPSIS;
701
+ }
702
+ function isNumericColumn(grid, col, header) {
703
+ const cells = (header ? grid.slice(1) : grid).map((row) => row[col] ?? "").filter((cell) => cell !== "");
704
+ return cells.length > 0 && cells.every((cell) => !Number.isNaN(Number(cell)));
705
+ }
706
+ function padCell(content, columnWidth, align) {
707
+ const slack = columnWidth - displayWidth(content);
708
+ if (align === "right") return " ".repeat(slack) + content;
709
+ if (align === "center") {
710
+ const left = Math.floor(slack / 2);
711
+ return " ".repeat(left) + content + " ".repeat(slack - left);
712
+ }
713
+ return content + " ".repeat(slack);
714
+ }
715
+
716
+ //#endregion
717
+ //#region src/strings/renderTable/markdown.ts
718
+ function renderMarkdown(grid, columnCount, alignments, pad) {
719
+ const escapeCell = (value) => value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
720
+ const escaped = grid.map((row) => Array.from({ length: columnCount }, (_, col) => escapeCell(row[col] ?? "")));
721
+ const columnWidths = Array.from({ length: columnCount }, (_, col) => escaped.reduce((max, row) => Math.max(max, displayWidth(row[col] ?? "")), 0));
722
+ const cell = (content, col) => pad + padCell(content, columnWidths[col] ?? 0, alignments[col] ?? "left") + pad;
723
+ const row = (cells) => `|${columnWidths.map((_, col) => cell(cells[col] ?? "", col)).join("|")}|`;
724
+ const delimiterCell = (col) => {
725
+ const align = alignments[col] ?? "left";
726
+ if (align === "center") return " :---: ";
727
+ if (align === "right") return " ---: ";
728
+ return " --- ";
481
729
  };
482
- table += createLine("═", "╔", "╦", "╗");
483
- data.forEach((row, rowIndex) => {
484
- table += "║";
485
- row.forEach((cell, columnIndex) => {
486
- const columnWidth = columnWidths[columnIndex];
487
- if (columnWidth !== void 0) table += ` ${cell.padEnd(columnWidth)} ║`;
488
- });
489
- table += "\n";
490
- if (rowIndex < data.length - 1) table += createLine("─", "╠", "╬", "╣");
491
- else table += createLine("═", "╚", "╩", "╝");
730
+ const [head = [], ...body] = escaped;
731
+ const delimiter = `|${columnWidths.map((_, col) => delimiterCell(col)).join("|")}|`;
732
+ return `${[
733
+ row(head),
734
+ delimiter,
735
+ ...body.map(row)
736
+ ].join("\n")}\n`;
737
+ }
738
+
739
+ //#endregion
740
+ //#region src/strings/renderTable/renderSingle.ts
741
+ function renderSingle(data, options) {
742
+ if (data.length === 0) return "";
743
+ const { align, border = "rounded", header = true, padding = 1, emptyCell = "", numericAlign = false, maxWidth, overflow = "wrap", fence } = options ?? {};
744
+ const columnCount = data.reduce((max, row) => Math.max(max, row.length), 0);
745
+ if (columnCount === 0) return "";
746
+ const grid = data.map((row) => Array.from({ length: columnCount }, (_, i) => {
747
+ const cell = (row[i] ?? "").replace(/\r?\n/g, " ");
748
+ const filled = cell === "" ? emptyCell : cell;
749
+ if (maxWidth === void 0 || overflow !== "truncate") return filled;
750
+ return truncate(filled, maxWidth);
751
+ }));
752
+ const alignments = Array.from({ length: columnCount }, (_, col) => {
753
+ const explicit = typeof align === "string" ? align : align?.[col];
754
+ if (explicit) return explicit;
755
+ if (numericAlign && isNumericColumn(grid, col, header)) return "right";
756
+ return "left";
492
757
  });
493
- return table;
758
+ const pad = " ".repeat(padding);
759
+ if (border === "markdown") {
760
+ const md = renderMarkdown(grid, columnCount, alignments, pad);
761
+ return fence ? wrapFence(md) : md;
762
+ }
763
+ const wrap = maxWidth !== void 0 && overflow === "wrap";
764
+ const rows = grid.map((row) => row.map((cell) => wrap ? wrapText(cell, maxWidth) : [cell]));
765
+ const columnWidths = Array.from({ length: columnCount }, (_, col) => rows.reduce((max, row) => Math.max(max, (row[col] ?? []).reduce((w, line) => Math.max(w, displayWidth(line)), 0)), 0));
766
+ const chars = BORDERS[border];
767
+ function drawLine(part) {
768
+ const segments = columnWidths.map((width) => part.fill.repeat(width + padding * 2));
769
+ return part.left + segments.join(part.mid) + part.right;
770
+ }
771
+ function renderRow(row) {
772
+ const lineCount = row.reduce((max, cellLines) => Math.max(max, cellLines.length), 1);
773
+ return Array.from({ length: lineCount }, (_, line) => columnWidths.map((width, col) => pad + padCell(row[col]?.[line] ?? "", width, alignments[col] ?? "left") + pad).join(chars.vertical)).map((line) => chars.vertical + line + chars.vertical).join("\n");
774
+ }
775
+ const lines = [drawLine(chars.top)];
776
+ rows.forEach((row, rowIndex) => {
777
+ lines.push(renderRow(row));
778
+ if (rowIndex >= rows.length - 1) return;
779
+ lines.push(drawLine(header && rowIndex === 0 ? chars.headerSep : chars.sep));
780
+ });
781
+ lines.push(drawLine(chars.bottom));
782
+ const body = `${lines.join("\n")}\n`;
783
+ return fence ? wrapFence(body) : body;
784
+ }
785
+
786
+ //#endregion
787
+ //#region src/strings/renderTable/pagination.ts
788
+ const DEFAULT_BUDGET = 2e3;
789
+ function paginate(data, options) {
790
+ if (data.length === 0) return [];
791
+ const { budget = DEFAULT_BUDGET, ...tableOptions } = options;
792
+ const header = tableOptions.header === false ? void 0 : data[0];
793
+ const body = header ? data.slice(1) : data;
794
+ const render = (rows) => renderSingle(header ? [header, ...rows] : rows, tableOptions);
795
+ if (body.length === 0) return [render([])];
796
+ const pages = [];
797
+ let current = [];
798
+ for (const row of body) {
799
+ if (current.length === 0 || render([...current, row]).length <= budget) {
800
+ current.push(row);
801
+ continue;
802
+ }
803
+ pages.push(render(current));
804
+ current = [row];
805
+ }
806
+ pages.push(render(current));
807
+ return pages;
808
+ }
809
+
810
+ //#endregion
811
+ //#region src/strings/renderTable/renderTable.ts
812
+ function renderTable(data, options) {
813
+ if (options) validateOptions(options);
814
+ if (options && "budget" in options) return paginate(data, options);
815
+ return renderSingle(data, options);
816
+ }
817
+ function validateOptions(options) {
818
+ const { maxWidth, padding } = options;
819
+ if (maxWidth !== void 0 && (!Number.isInteger(maxWidth) || maxWidth < 1)) throw new RangeError(`renderTable maxWidth must be a positive integer, got ${maxWidth}`);
820
+ if (padding !== void 0 && (!Number.isInteger(padding) || padding < 0)) throw new RangeError(`renderTable padding must be a non-negative integer, got ${padding}`);
494
821
  }
495
822
 
496
823
  //#endregion
@@ -540,8 +867,8 @@ function prettyDifference(numBefore, numAfter) {
540
867
  //#endregion
541
868
  //#region src/index.ts
542
869
  /** Package version */
543
- const version = "0.6.1-next.0";
870
+ const version = "0.7.0-next.0";
544
871
 
545
872
  //#endregion
546
- export { assertNever, capitalize, currentTime, filterCirculars, formatFilePath, fyShuffle, generateAsciiTable, generateCode, hasKeys, isTsOrJsFile, keepDefined, longestStringLength, ordinal, parseDuration, percentage, prettify, prettyDifference, round, roundToDenomination, toEpochSeconds, traverseDirectory, version };
873
+ export { assertNever, capitalize, currentTime, filterCirculars, formatFilePath, fyShuffle, generateCode, hasKeys, isTsOrJsFile, keepDefined, longestStringLength, ordinal, parseDuration, percentage, prettify, prettyDifference, renderTable, round, roundToDenomination, toEpochSeconds, traverseDirectory, version };
547
874
  //# sourceMappingURL=index.mjs.map