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