Skip to content

Commit

Permalink
strip -> replace
Browse files Browse the repository at this point in the history
  • Loading branch information
lionel-rowe committed Dec 26, 2024
1 parent 8e48f0b commit ad6098a
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 237 deletions.
2 changes: 1 addition & 1 deletion text/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"./compare-similarity": "./compare_similarity.ts",
"./levenshtein-distance": "./levenshtein_distance.ts",
"./unstable-slugify": "./unstable_slugify.ts",
"./unstable-strip": "./unstable_strip.ts",
"./unstable-replace": "./unstable_replace.ts",
"./to-camel-case": "./to_camel_case.ts",
"./unstable-to-constant-case": "./unstable_to_constant_case.ts",
"./to-kebab-case": "./to_kebab_case.ts",
Expand Down
127 changes: 127 additions & 0 deletions text/unstable_replace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { escape } from "@std/regexp/escape";

/**
* A string or function that can be used as the second parameter of
* `String.prototype.replace()`.
*/
export type Replacer =
| string
| ((substring: string, ...args: unknown[]) => string);

/**
* Replaces the specified pattern at the start and end of the string.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param str The input string
* @param pattern The pattern to replace
* @param replacer String or function to be used as the replacement
*
* @example Strip non-word characters from start and end of a string
* ```ts
* import { replaceBoth } from "@std/text/unstable-replace";
* import { assertEquals } from "@std/assert";
*
* const result = replaceBoth("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]+/u, "");
* assertEquals(result, "Seguro que no");
* ```
*/
export function replaceBoth(
str: string,
pattern: string | RegExp,
replacer: Replacer,
): string {
return replaceStart(
replaceEnd(str, pattern, replacer),
pattern,
replacer,
);
}

/**
* Replaces the specified pattern at the start of the string.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param str The input string
* @param pattern The pattern to replace
* @param replacer String or function to be used as the replacement
*
* @example Strip byte-order mark
* ```ts
* import { replaceStart } from "@std/text/unstable-replace";
* import { assertEquals } from "@std/assert";
*
* const result = replaceStart("\ufeffhello world", "\ufeff", "");
* assertEquals(result, "hello world");
* ```
*
* @example Replace `http:` protocol with `https:`
* ```ts
* import { replaceStart } from "@std/text/unstable-replace";
* import { assertEquals } from "@std/assert";
*
* const result = replaceStart("http://example.com", "http:", "https:");
* assertEquals(result, "https://example.com");
* ```
*/
export function replaceStart(
str: string,
pattern: string | RegExp,
replacer: Replacer,
): string {
return str.replace(
cloneAsStatelessRegExp`^${pattern}`,
replacer as string,
);
}

/**
* Replaces the specified pattern at the start of the string.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param str The input string
* @param pattern The pattern to replace
* @param replacer String or function to be used as the replacement
*
* @example Remove a single trailing newline
* ```ts
* import { replaceEnd } from "@std/text/unstable-replace";
* import { assertEquals } from "@std/assert";
*
* const result = replaceEnd("file contents\n", "\n", "");
* assertEquals(result, "file contents");
* ```
*
* @example Ensure pathname ends with a single slash
* ```ts
* import { replaceEnd } from "@std/text/unstable-replace";
* import { assertEquals } from "@std/assert";
*
* const result = replaceEnd("/pathname", new RegExp("/*"), "/");
* assertEquals(result, "/pathname/");
* ```
*/
export function replaceEnd(
str: string,
pattern: string | RegExp,
replacement: Replacer,
): string {
return str.replace(
cloneAsStatelessRegExp`${pattern}$`,
replacement as string,
);
}

function cloneAsStatelessRegExp(
{ raw: [$0, $1] }: TemplateStringsArray,
pattern: string | RegExp,
) {
const { source, flags } = typeof pattern === "string"
? { source: escape(pattern), flags: "" }
: pattern;

return new RegExp(`${$0!}(?:${source})${$1!}`, flags.replace(/[gy]+/g, ""));
}
170 changes: 170 additions & 0 deletions text/unstable_replace_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals } from "@std/assert";
import { replaceBoth, replaceEnd, replaceStart } from "./unstable_replace.ts";

Deno.test("replaceStart()", async (t) => {
await t.step("strips a prefix", () => {
assertEquals(
replaceStart("https://example.com", "https://", ""),
"example.com",
);
});

await t.step("replaces a prefix", () => {
assertEquals(
replaceStart("http://example.com", "http://", "https://"),
"https://example.com",
);
});

await t.step("no replacement if pattern not found", () => {
assertEquals(
replaceStart("file:///a/b/c", "http://", "https://"),
"file:///a/b/c",
);
});

await t.step("strips prefixes by regex pattern", () => {
assertEquals(replaceStart("abc", /a|b/, ""), "bc");
assertEquals(replaceStart("xbc", /a|b/, ""), "xbc");

assertEquals(
replaceStart("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]+/u, ""),
"Seguro que no?!",
);
});

await t.step("complex replacers", () => {
assertEquals(replaceStart("abca", "a", "$'"), "bcabca");
assertEquals(replaceStart("xbca", "a", "$'"), "xbca");

assertEquals(replaceStart("abcxyz", /[a-c]+/, "<$&>"), "<abc>xyz");
assertEquals(replaceStart("abcxyz", /([a-c]+)/, "<$1>"), "<abc>xyz");
assertEquals(
replaceStart("abcxyz", /(?<match>[a-c]+)/, "<$<match>>"),
"<abc>xyz",
);

assertEquals(replaceStart("abcxyz", /[a-c]+/, (m) => `<${m}>`), "<abc>xyz");
assertEquals(
replaceStart("abcxyz", /([a-c]+)/, (_, p1) => `<${p1}>`),
"<abc>xyz",
);
assertEquals(
replaceStart("abcxyz", /(?<match>[a-c]+)/, (...args) =>
`<${
(args[
args.findIndex((x) => typeof x === "number") + 2
] as { match: string }).match
}>`),
"<abc>xyz",
);
});
});

Deno.test("replaceEnd()", async (t) => {
await t.step("strips a suffix", () => {
assertEquals(replaceEnd("/pathname/", "/", ""), "/pathname");
});

await t.step("replaces a suffix", () => {
assertEquals(replaceEnd("/pathname/", "/", "/?a=1"), "/pathname/?a=1");
});

await t.step("no replacement if pattern not found", () => {
assertEquals(replaceEnd("/pathname", "/", "/?a=1"), "/pathname");
});

await t.step("strips suffixes by regex pattern", () => {
assertEquals(replaceEnd("abc", /b|c/, ""), "ab");
assertEquals(replaceEnd("abx", /b|c/, ""), "abx");

assertEquals(
replaceEnd("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]+/u, ""),
"¡¿Seguro que no",
);
});

await t.step("complex replacers", () => {
assertEquals(replaceEnd("abca", "a", "$`"), "abcabc");
assertEquals(replaceEnd("abcx", "a", "$`"), "abcx");

assertEquals(replaceEnd("xyzabc", /[a-c]+/, "<$&>"), "xyz<abc>");
assertEquals(replaceEnd("xyzabc", /([a-c]+)/, "<$1>"), "xyz<abc>");
assertEquals(
replaceEnd("xyzabc", /(?<match>[a-c]+)/, "<$<match>>"),
"xyz<abc>",
);

assertEquals(replaceEnd("xyzabc", /[a-c]+/, (m) => `<${m}>`), "xyz<abc>");
assertEquals(
replaceEnd("xyzabc", /([a-c]+)/, (_, p1) => `<${p1}>`),
"xyz<abc>",
);
assertEquals(
replaceEnd("xyzabc", /(?<match>[a-c]+)/, (...args) =>
`<${
(args[
args.findIndex((x) => typeof x === "number") + 2
] as { match: string }).match
}>`),
"xyz<abc>",
);
});
});

Deno.test("replaceBoth()", async (t) => {
await t.step("strips both prefixes and suffixes", () => {
assertEquals(replaceBoth("/pathname/", "/", ""), "pathname");
});

await t.step("replaces both prefixes and suffixes", () => {
assertEquals(replaceBoth("/pathname/", "/", "!"), "!pathname!");
assertEquals(replaceBoth("//pathname", /\/+/, "/"), "/pathname");
assertEquals(replaceBoth("//pathname", /\/*/, "/"), "/pathname/");
});

await t.step("no replacement if pattern not found", () => {
assertEquals(replaceBoth("pathname", "/", "!"), "pathname");
});

await t.step("strips both prefixes and suffixes by regex pattern", () => {
assertEquals(replaceBoth("abc", /a|b|c/, ""), "b");
assertEquals(replaceBoth("xbx", /a|b|c/, ""), "xbx");

assertEquals(
replaceBoth("¡¿Seguro que no?!", /[^\p{L}\p{M}\p{N}]+/u, ""),
"Seguro que no",
);
});

await t.step("complex replacers", () => {
assertEquals(replaceBoth("abca", "a", "$$"), "$bc$");
assertEquals(replaceBoth("xbcx", "a", "$$"), "xbcx");

assertEquals(replaceBoth("abcxyzabc", /[a-c]+/, "<$&>"), "<abc>xyz<abc>");
assertEquals(replaceBoth("abcxyzabc", /([a-c]+)/, "<$1>"), "<abc>xyz<abc>");
assertEquals(
replaceBoth("abcxyzabc", /(?<match>[a-c]+)/, "<$<match>>"),
"<abc>xyz<abc>",
);

assertEquals(
replaceBoth("abcxyzabc", /[a-c]+/, (m) => `<${m}>`),
"<abc>xyz<abc>",
);
assertEquals(
replaceBoth("abcxyzabc", /([a-c]+)/, (_, p1) => `<${p1}>`),
"<abc>xyz<abc>",
);
assertEquals(
replaceBoth("abcxyzabc", /(?<match>[a-c]+)/, (...args) =>
`<${
(args[
args.findIndex((x) => typeof x === "number") + 2
] as { match: string }).match
}>`),
"<abc>xyz<abc>",
);
});
});
Loading

0 comments on commit ad6098a

Please sign in to comment.