37 lines
1 KiB
TypeScript
37 lines
1 KiB
TypeScript
const MAX_SLUG_LENGTH = 96;
|
|
|
|
/**
|
|
* Turns arbitrary text into a URL-safe slug: lowercase ASCII words joined
|
|
* by single hyphens. Returns "" when nothing usable remains, so callers
|
|
* can decide on a fallback.
|
|
*/
|
|
export function slugify(input: string): string {
|
|
return input
|
|
.normalize("NFKD")
|
|
.replace(/[̀-ͯ]/g, "") // strip combining diacritics
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, MAX_SLUG_LENGTH)
|
|
.replace(/-+$/g, "");
|
|
}
|
|
|
|
/**
|
|
* Finds a free slug by appending -2, -3, ... to the base while `isTaken`
|
|
* reports a conflict. The base falls back to "untitled" when empty.
|
|
*/
|
|
export async function ensureUniqueSlug(
|
|
base: string,
|
|
isTaken: (slug: string) => Promise<boolean>,
|
|
): Promise<string> {
|
|
const root = base || "untitled";
|
|
let candidate = root;
|
|
for (let suffix = 2; await isTaken(candidate); suffix++) {
|
|
if (suffix > 500) {
|
|
throw new Error(`Could not find a free slug for "${root}"`);
|
|
}
|
|
candidate = `${root}-${suffix}`;
|
|
}
|
|
return candidate;
|
|
}
|