Skip to content

Commit 75f7e56

Browse files
committed
feat(text/unstable): add trim functions
1 parent 3650bce commit 75f7e56

File tree

3 files changed

+179
-0
lines changed

3 files changed

+179
-0
lines changed

text/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"./unstable-longest-common-prefix": "./unstable_longest_common_prefix.ts",
1111
"./unstable-reverse": "./unstable_reverse.ts",
1212
"./unstable-slugify": "./unstable_slugify.ts",
13+
"./unstable-trim": "./unstable_trim.ts",
1314
"./to-camel-case": "./to_camel_case.ts",
1415
"./unstable-to-constant-case": "./unstable_to_constant_case.ts",
1516
"./to-kebab-case": "./to_kebab_case.ts",

text/unstable_trim.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
import { escape } from "@std/regexp/escape";
4+
5+
/**
6+
* A pattern that can be used to trim characters from an input string.
7+
* - If `string`, trim all characters in the pattern string.
8+
* - If `Iterable<string>`, trim all substrings equal to any item.
9+
* - If `RegExp`, trim all substrings that match the regex.
10+
*/
11+
export type TrimPattern =
12+
| string
13+
| Iterable<string>
14+
| RegExp;
15+
16+
/**
17+
* Trims all instances of the specified pattern at the start and end of the string.
18+
*
19+
* @experimental **UNSTABLE**: New API, yet to be vetted.
20+
*
21+
* @param str The input string
22+
* @param pattern The pattern to trim
23+
* @returns The trimmed input string
24+
*
25+
* @example Strip non-word characters from start and end of a string
26+
* ```ts
27+
* import { trim } from "@std/text/unstable-trim";
28+
* import { assertEquals } from "@std/assert";
29+
*
30+
* const result = trim("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]/u);
31+
* assertEquals(result, "Seguro que no");
32+
* ```
33+
*/
34+
export function trim(
35+
str: string,
36+
pattern: TrimPattern,
37+
): string {
38+
return trimStart(trimEnd(str, pattern), pattern);
39+
}
40+
41+
/**
42+
* Trims all instances of the specified pattern at the start of the string.
43+
*
44+
* @experimental **UNSTABLE**: New API, yet to be vetted.
45+
*
46+
* @param str The input string
47+
* @param pattern The pattern to trim
48+
* @returns The trimmed input string
49+
*
50+
* @example Remove leading byte-order marks
51+
* ```ts
52+
* import { trimStart } from "@std/text/unstable-trim";
53+
* import { assertEquals } from "@std/assert";
54+
*
55+
* const result = trimStart("\ufeffhello world", "\ufeff");
56+
* assertEquals(result, "hello world");
57+
* ```
58+
*/
59+
export function trimStart(
60+
str: string,
61+
pattern: TrimPattern,
62+
): string {
63+
return str.replace(regExpFromTrimPattern`^${pattern}`, "");
64+
}
65+
66+
/**
67+
* Trims all instances of the specified pattern at the end of the string.
68+
*
69+
* @experimental **UNSTABLE**: New API, yet to be vetted.
70+
*
71+
* @param str The input string
72+
* @param pattern The pattern to trim
73+
* @returns The trimmed input string
74+
*
75+
* @example Remove trailing line endings
76+
* ```ts
77+
* import { trimEnd } from "@std/text/unstable-trim";
78+
* import { assertEquals } from "@std/assert";
79+
*
80+
* const result = trimEnd("file contents\r\n\n", ["\n", "\r\n"]);
81+
* assertEquals(result, "file contents");
82+
* ```
83+
*/
84+
export function trimEnd(
85+
str: string,
86+
pattern: TrimPattern,
87+
): string {
88+
return str.replace(regExpFromTrimPattern`${pattern}$`, "");
89+
}
90+
91+
function regExpFromTrimPattern(t: TemplateStringsArray, pattern: TrimPattern) {
92+
let { source, flags } = pattern instanceof RegExp ? pattern : {
93+
source: `${
94+
[...new Set(pattern)]
95+
.sort((a, b) => b.length - a.length)
96+
.map(escape)
97+
.join("|")
98+
}`,
99+
flags: "",
100+
};
101+
102+
source = `${t[0]!}(?:${source})+${t[1]!}`;
103+
flags = flags.replace(/[gy]+/g, "");
104+
105+
return new RegExp(source, flags);
106+
}

text/unstable_trim_test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert";
3+
import { trim, trimEnd, trimStart } from "./unstable_trim.ts";
4+
5+
Deno.test("trim()", async (t) => {
6+
await t.step("empty patterns - no-op", () => {
7+
assertEquals(trimStart("abc", ""), "abc");
8+
assertEquals(trimStart("abc", [""]), "abc");
9+
assertEquals(trimStart("abc", /(?:)/), "abc");
10+
});
11+
12+
await t.step("trims both prefixes and suffixes", () => {
13+
assertEquals(trim("/pathname/", "/"), "pathname");
14+
});
15+
16+
await t.step("trims both prefixes and suffixes by regex pattern", () => {
17+
assertEquals(trim("abc", /[abc]/), "");
18+
assertEquals(trim("xbx", /[abc]/), "xbx");
19+
20+
assertEquals(
21+
trim("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]/u),
22+
"Seguro que no",
23+
);
24+
});
25+
});
26+
27+
Deno.test("trimStart()", async (t) => {
28+
await t.step("trims a prefix", () => {
29+
assertEquals(trimStart("/pathname/", "/"), "pathname/");
30+
});
31+
32+
await t.step("prefix is based on iterated members of input", async (t) => {
33+
await t.step("chars of string", () => {
34+
assertEquals(
35+
trimStart("https://sth.example.com", "https://"),
36+
".example.com",
37+
);
38+
});
39+
await t.step("strings of string[]", () => {
40+
assertEquals(
41+
trimStart("https://sth.example.com", ["https://"]),
42+
"sth.example.com",
43+
);
44+
});
45+
});
46+
47+
await t.step("trims prefixes by regex pattern", () => {
48+
assertEquals(trimStart("abc", /[ab]/), "c");
49+
assertEquals(trimStart("xbc", /[ab]/), "xbc");
50+
51+
assertEquals(
52+
trimStart("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]/u),
53+
"Seguro que no?!",
54+
);
55+
});
56+
});
57+
58+
Deno.test("trimEnd()", async (t) => {
59+
await t.step("trims a suffix", () => {
60+
assertEquals(trimEnd("/pathname/", "/"), "/pathname");
61+
});
62+
63+
await t.step("trims suffixes by regex pattern", () => {
64+
assertEquals(trimEnd("abc", /[bc]/), "a");
65+
assertEquals(trimEnd("abx", /[bc]/), "abx");
66+
67+
assertEquals(
68+
trimEnd("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]/u),
69+
"¡¿Seguro que no",
70+
);
71+
});
72+
});

0 commit comments

Comments
 (0)