Skip to content

Commit ad6098a

Browse files
committed
strip -> replace
1 parent 8e48f0b commit ad6098a

5 files changed

+298
-237
lines changed

text/deno.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"./compare-similarity": "./compare_similarity.ts",
88
"./levenshtein-distance": "./levenshtein_distance.ts",
99
"./unstable-slugify": "./unstable_slugify.ts",
10-
"./unstable-strip": "./unstable_strip.ts",
10+
"./unstable-replace": "./unstable_replace.ts",
1111
"./to-camel-case": "./to_camel_case.ts",
1212
"./unstable-to-constant-case": "./unstable_to_constant_case.ts",
1313
"./to-kebab-case": "./to_kebab_case.ts",

text/unstable_replace.ts

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2+
import { escape } from "@std/regexp/escape";
3+
4+
/**
5+
* A string or function that can be used as the second parameter of
6+
* `String.prototype.replace()`.
7+
*/
8+
export type Replacer =
9+
| string
10+
| ((substring: string, ...args: unknown[]) => string);
11+
12+
/**
13+
* Replaces the specified pattern at the start and end of the string.
14+
*
15+
* @experimental **UNSTABLE**: New API, yet to be vetted.
16+
*
17+
* @param str The input string
18+
* @param pattern The pattern to replace
19+
* @param replacer String or function to be used as the replacement
20+
*
21+
* @example Strip non-word characters from start and end of a string
22+
* ```ts
23+
* import { replaceBoth } from "@std/text/unstable-replace";
24+
* import { assertEquals } from "@std/assert";
25+
*
26+
* const result = replaceBoth("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]+/u, "");
27+
* assertEquals(result, "Seguro que no");
28+
* ```
29+
*/
30+
export function replaceBoth(
31+
str: string,
32+
pattern: string | RegExp,
33+
replacer: Replacer,
34+
): string {
35+
return replaceStart(
36+
replaceEnd(str, pattern, replacer),
37+
pattern,
38+
replacer,
39+
);
40+
}
41+
42+
/**
43+
* Replaces the specified pattern at the start of the string.
44+
*
45+
* @experimental **UNSTABLE**: New API, yet to be vetted.
46+
*
47+
* @param str The input string
48+
* @param pattern The pattern to replace
49+
* @param replacer String or function to be used as the replacement
50+
*
51+
* @example Strip byte-order mark
52+
* ```ts
53+
* import { replaceStart } from "@std/text/unstable-replace";
54+
* import { assertEquals } from "@std/assert";
55+
*
56+
* const result = replaceStart("\ufeffhello world", "\ufeff", "");
57+
* assertEquals(result, "hello world");
58+
* ```
59+
*
60+
* @example Replace `http:` protocol with `https:`
61+
* ```ts
62+
* import { replaceStart } from "@std/text/unstable-replace";
63+
* import { assertEquals } from "@std/assert";
64+
*
65+
* const result = replaceStart("http://example.com", "http:", "https:");
66+
* assertEquals(result, "https://example.com");
67+
* ```
68+
*/
69+
export function replaceStart(
70+
str: string,
71+
pattern: string | RegExp,
72+
replacer: Replacer,
73+
): string {
74+
return str.replace(
75+
cloneAsStatelessRegExp`^${pattern}`,
76+
replacer as string,
77+
);
78+
}
79+
80+
/**
81+
* Replaces the specified pattern at the start of the string.
82+
*
83+
* @experimental **UNSTABLE**: New API, yet to be vetted.
84+
*
85+
* @param str The input string
86+
* @param pattern The pattern to replace
87+
* @param replacer String or function to be used as the replacement
88+
*
89+
* @example Remove a single trailing newline
90+
* ```ts
91+
* import { replaceEnd } from "@std/text/unstable-replace";
92+
* import { assertEquals } from "@std/assert";
93+
*
94+
* const result = replaceEnd("file contents\n", "\n", "");
95+
* assertEquals(result, "file contents");
96+
* ```
97+
*
98+
* @example Ensure pathname ends with a single slash
99+
* ```ts
100+
* import { replaceEnd } from "@std/text/unstable-replace";
101+
* import { assertEquals } from "@std/assert";
102+
*
103+
* const result = replaceEnd("/pathname", new RegExp("/*"), "/");
104+
* assertEquals(result, "/pathname/");
105+
* ```
106+
*/
107+
export function replaceEnd(
108+
str: string,
109+
pattern: string | RegExp,
110+
replacement: Replacer,
111+
): string {
112+
return str.replace(
113+
cloneAsStatelessRegExp`${pattern}$`,
114+
replacement as string,
115+
);
116+
}
117+
118+
function cloneAsStatelessRegExp(
119+
{ raw: [$0, $1] }: TemplateStringsArray,
120+
pattern: string | RegExp,
121+
) {
122+
const { source, flags } = typeof pattern === "string"
123+
? { source: escape(pattern), flags: "" }
124+
: pattern;
125+
126+
return new RegExp(`${$0!}(?:${source})${$1!}`, flags.replace(/[gy]+/g, ""));
127+
}

text/unstable_replace_test.ts

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2+
import { assertEquals } from "@std/assert";
3+
import { replaceBoth, replaceEnd, replaceStart } from "./unstable_replace.ts";
4+
5+
Deno.test("replaceStart()", async (t) => {
6+
await t.step("strips a prefix", () => {
7+
assertEquals(
8+
replaceStart("https://example.com", "https://", ""),
9+
"example.com",
10+
);
11+
});
12+
13+
await t.step("replaces a prefix", () => {
14+
assertEquals(
15+
replaceStart("http://example.com", "http://", "https://"),
16+
"https://example.com",
17+
);
18+
});
19+
20+
await t.step("no replacement if pattern not found", () => {
21+
assertEquals(
22+
replaceStart("file:///a/b/c", "http://", "https://"),
23+
"file:///a/b/c",
24+
);
25+
});
26+
27+
await t.step("strips prefixes by regex pattern", () => {
28+
assertEquals(replaceStart("abc", /a|b/, ""), "bc");
29+
assertEquals(replaceStart("xbc", /a|b/, ""), "xbc");
30+
31+
assertEquals(
32+
replaceStart("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]+/u, ""),
33+
"Seguro que no?!",
34+
);
35+
});
36+
37+
await t.step("complex replacers", () => {
38+
assertEquals(replaceStart("abca", "a", "$'"), "bcabca");
39+
assertEquals(replaceStart("xbca", "a", "$'"), "xbca");
40+
41+
assertEquals(replaceStart("abcxyz", /[a-c]+/, "<$&>"), "<abc>xyz");
42+
assertEquals(replaceStart("abcxyz", /([a-c]+)/, "<$1>"), "<abc>xyz");
43+
assertEquals(
44+
replaceStart("abcxyz", /(?<match>[a-c]+)/, "<$<match>>"),
45+
"<abc>xyz",
46+
);
47+
48+
assertEquals(replaceStart("abcxyz", /[a-c]+/, (m) => `<${m}>`), "<abc>xyz");
49+
assertEquals(
50+
replaceStart("abcxyz", /([a-c]+)/, (_, p1) => `<${p1}>`),
51+
"<abc>xyz",
52+
);
53+
assertEquals(
54+
replaceStart("abcxyz", /(?<match>[a-c]+)/, (...args) =>
55+
`<${
56+
(args[
57+
args.findIndex((x) => typeof x === "number") + 2
58+
] as { match: string }).match
59+
}>`),
60+
"<abc>xyz",
61+
);
62+
});
63+
});
64+
65+
Deno.test("replaceEnd()", async (t) => {
66+
await t.step("strips a suffix", () => {
67+
assertEquals(replaceEnd("/pathname/", "/", ""), "/pathname");
68+
});
69+
70+
await t.step("replaces a suffix", () => {
71+
assertEquals(replaceEnd("/pathname/", "/", "/?a=1"), "/pathname/?a=1");
72+
});
73+
74+
await t.step("no replacement if pattern not found", () => {
75+
assertEquals(replaceEnd("/pathname", "/", "/?a=1"), "/pathname");
76+
});
77+
78+
await t.step("strips suffixes by regex pattern", () => {
79+
assertEquals(replaceEnd("abc", /b|c/, ""), "ab");
80+
assertEquals(replaceEnd("abx", /b|c/, ""), "abx");
81+
82+
assertEquals(
83+
replaceEnd("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]+/u, ""),
84+
"¡¿Seguro que no",
85+
);
86+
});
87+
88+
await t.step("complex replacers", () => {
89+
assertEquals(replaceEnd("abca", "a", "$`"), "abcabc");
90+
assertEquals(replaceEnd("abcx", "a", "$`"), "abcx");
91+
92+
assertEquals(replaceEnd("xyzabc", /[a-c]+/, "<$&>"), "xyz<abc>");
93+
assertEquals(replaceEnd("xyzabc", /([a-c]+)/, "<$1>"), "xyz<abc>");
94+
assertEquals(
95+
replaceEnd("xyzabc", /(?<match>[a-c]+)/, "<$<match>>"),
96+
"xyz<abc>",
97+
);
98+
99+
assertEquals(replaceEnd("xyzabc", /[a-c]+/, (m) => `<${m}>`), "xyz<abc>");
100+
assertEquals(
101+
replaceEnd("xyzabc", /([a-c]+)/, (_, p1) => `<${p1}>`),
102+
"xyz<abc>",
103+
);
104+
assertEquals(
105+
replaceEnd("xyzabc", /(?<match>[a-c]+)/, (...args) =>
106+
`<${
107+
(args[
108+
args.findIndex((x) => typeof x === "number") + 2
109+
] as { match: string }).match
110+
}>`),
111+
"xyz<abc>",
112+
);
113+
});
114+
});
115+
116+
Deno.test("replaceBoth()", async (t) => {
117+
await t.step("strips both prefixes and suffixes", () => {
118+
assertEquals(replaceBoth("/pathname/", "/", ""), "pathname");
119+
});
120+
121+
await t.step("replaces both prefixes and suffixes", () => {
122+
assertEquals(replaceBoth("/pathname/", "/", "!"), "!pathname!");
123+
assertEquals(replaceBoth("//pathname", /\/+/, "/"), "/pathname");
124+
assertEquals(replaceBoth("//pathname", /\/*/, "/"), "/pathname/");
125+
});
126+
127+
await t.step("no replacement if pattern not found", () => {
128+
assertEquals(replaceBoth("pathname", "/", "!"), "pathname");
129+
});
130+
131+
await t.step("strips both prefixes and suffixes by regex pattern", () => {
132+
assertEquals(replaceBoth("abc", /a|b|c/, ""), "b");
133+
assertEquals(replaceBoth("xbx", /a|b|c/, ""), "xbx");
134+
135+
assertEquals(
136+
replaceBoth("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]+/u, ""),
137+
"Seguro que no",
138+
);
139+
});
140+
141+
await t.step("complex replacers", () => {
142+
assertEquals(replaceBoth("abca", "a", "$$"), "$bc$");
143+
assertEquals(replaceBoth("xbcx", "a", "$$"), "xbcx");
144+
145+
assertEquals(replaceBoth("abcxyzabc", /[a-c]+/, "<$&>"), "<abc>xyz<abc>");
146+
assertEquals(replaceBoth("abcxyzabc", /([a-c]+)/, "<$1>"), "<abc>xyz<abc>");
147+
assertEquals(
148+
replaceBoth("abcxyzabc", /(?<match>[a-c]+)/, "<$<match>>"),
149+
"<abc>xyz<abc>",
150+
);
151+
152+
assertEquals(
153+
replaceBoth("abcxyzabc", /[a-c]+/, (m) => `<${m}>`),
154+
"<abc>xyz<abc>",
155+
);
156+
assertEquals(
157+
replaceBoth("abcxyzabc", /([a-c]+)/, (_, p1) => `<${p1}>`),
158+
"<abc>xyz<abc>",
159+
);
160+
assertEquals(
161+
replaceBoth("abcxyzabc", /(?<match>[a-c]+)/, (...args) =>
162+
`<${
163+
(args[
164+
args.findIndex((x) => typeof x === "number") + 2
165+
] as { match: string }).match
166+
}>`),
167+
"<abc>xyz<abc>",
168+
);
169+
});
170+
});

0 commit comments

Comments
 (0)