1/**
2 * helpers for strings, escaping, and encoding.
3 *
4 * @module
5 */
6
7/** count the number of `\n` characters are in the string */
8export function countNewlines(str: string): number {
9 let count = 0;
10 for (let i = 0, { length } = str; i < length; i += 1) {
11 if (str[i] === "\n") count += 1;
12 }
13 return count;
14}
15
16/**
17 * escape a string for use in a regular expression.
18 * @deprecated use `RegExp.escape` for browsers past May 2025
19 */
20export const escapeRegExp: (unsafeText: string) => string = RegExp.escape
21 ?? ((unsafeText: string) => unsafeText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
22
23/** escape a string for use in a regular expression. */
24export function escapeRegExpReplacement(unsafeText: string): string {
25 return unsafeText.replace(/\$/g, "$$$$");
26}
27
28export function escapeHtmlContent(unsafeText: string): string {
29 return unsafeText
30 .replace(/&/g, "&amp;")
31 .replace(/</g, "&lt;")
32 .replace(/>/g, "&gt;");
33}
34
35export function escapeHtmlAttr(unsafeText: string): string {
36 return escapeHtmlContent(unsafeText)
37 .replace(/'/g, "&#x27;")
38 .replace(/"/g, "&quot;");
39}
40
41/**
42 * Escape a string for use in a POSIX shell. Prefers easier to read output vs
43 * simpler implementation.
44 */
45export function escapeShellArgument(s: string): string {
46 if (s.length === 0) return "''";
47 if (/^[a-zA-Z0-9_\-./@,+=%:]+$/.test(s) && s[0] !== ":") return s;
48 if (!s.includes("'")) return `'${s}'`;
49 if (!/["$`\\]/.test(s)) return `"${s}"`;
50 let complex = `'${s.replace(/'/g, "'\\''")}'`;
51 if (s.endsWith("'")) complex = complex.slice(0, -2);
52 if (s.startsWith("'")) complex = complex.slice(2);
53 return complex;
54}
55
56const te = new TextEncoder();
57export function encodeUtf8(input: string): Uint8Array<ArrayBuffer> {
58 return te.encode(input) as Uint8Array<ArrayBuffer>;
59}
60
61const td = new TextDecoder();
62export function decodeUtf8(input: Uint8Array): string {
63 return td.decode(input);
64}
65
66const byteUnits = "kMGTPEZYRQ"; // kilo, mega, giga, etc
67export function formatByteSize(bytes: number): string {
68 if (!Number.isFinite(bytes)) return bytes.toString();
69 let prefix = "";
70 if (bytes < 0) bytes = -bytes, prefix = "-";
71 if (bytes < 1000) return (Math.round(bytes * 100) / 100) + "B";
72 let unit = 0;
73 do {
74 bytes /= 1000;
75 unit += 1;
76 } while (bytes >= 1000);
77 return prefix + (unit === 1
78 ? String(+bytes.toFixed(bytes > 100 ? 1 : 2))
79 : bytes.toFixed(
80 Math.floor(bytes) === bytes ? 0 : bytes > 100 ? 1 : 2,
81 ))
82 + byteUnits.charAt(unit - 1) + "B";
83}
84
85export function formatBinaryByteSize(bytes: number): string {
86 let prefix = "";
87 if (bytes < 0) bytes = -bytes, prefix = "-";
88 if (bytes < 1024) return (Math.round(bytes * 100) / 100) + "B";
89 let unit = 0;
90 do {
91 bytes /= 1024;
92 unit += 1;
93 } while (bytes >= 1024);
94 return prefix + (Math.round(bytes * 100) / 100)
95 + byteUnits.charAt(unit - 1) + "iB";
96}
97
98export function formatDurationLetters(seconds: number): string {
99 if (seconds === 0) return "now";
100 if (!Number.isFinite(seconds)) {
101 if (seconds === Infinity) return "forever";
102 if (seconds === -Infinity) return "forever ago";
103 return "NaN";
104 }
105 let ago = "";
106 if (seconds < 0) seconds = -seconds, ago = " ago";
107 const units = [
108 [31536000, "y"],
109 [86400, "d"],
110 [3600, "h"],
111 [60, "m"],
112 ] as const;
113 let out = "";
114 let remain = seconds;
115 let u = 0;
116 for (const [size, label] of units) {
117 if (remain < size) continue;
118 const value = Math.floor(remain / size);
119 remain -= value * size;
120 out += `${value}${label}`;
121 if ((u += 1) === 2) return out + ago;
122 }
123 if (remain > 0 || out === "") out += `${+remain.toFixed(2)}s`;
124 return out + ago;
125}