Skip to content
54 linesCodeBlameRaw
1/**
2 * Names in URLs: a space's slug, and a page's `<title-slug>-<id>`, which
3 * keeps working when the page is renamed (only the id is read). Pure.
4 */
5
6/** Words, lowercased, joined by hyphens, at most `max` characters. */
7export function slugify(text: string, max = 60): string {
8 const slug = text
9 .normalize("NFKD")
10 .replace(/[̀-ͯ]/g, "")
11 .toLowerCase()
12 .replace(/[^a-z0-9]+/g, "-")
13 .replace(/^-+|-+$/g, "")
14 .slice(0, max)
15 .replace(/-+$/g, "");
16 return slug;
17}
18
19/** A page's last URL segment. */
20export function pageSlug(title: string, id: string): string {
21 const words = slugify(title, 50);
22 return words ? `${words}-${id}` : id;
23}
24
25const PAGE_ID = /(pag_[0-9a-hjkmnp-tv-z]{26})$/;
26
27/** The page id at the end of a page's URL segment, or null. */
28export function pageIdFrom(segment: string): string | null {
29 return PAGE_ID.exec(String(segment ?? ""))?.[1] ?? null;
30}
31
32/** Every page id a Markdown text links to, for backlinks. */
33export function linkedPageIds(markdown: string): string[] {
34 return [...new Set(markdown.match(/pag_[0-9a-hjkmnp-tv-z]{26}/g) ?? [])];
35}
36
37/** Space slugs the site's routes use under `-/docs/`. */
38export const RESERVED_SPACE_SLUGS = new Set(["new", "search", "trash", "templates", "live", "api", "threads", "upload", "export", "settings", "recent", "favorites"]);
39
40/** A space slug that is free: `base`, else `base-2`, `base-3`, ... */
41export function freeSlug(base: string, taken: ReadonlySet<string>): string {
42 const root = slugify(base, 40) || "space";
43 const ok = (s: string) => !taken.has(s) && !RESERVED_SPACE_SLUGS.has(s);
44 if (ok(root)) return root;
45 for (let n = 2; ; n++) if (ok(`${root}-${n}`)) return `${root}-${n}`;
46}
47
48/** A space slug someone typed: valid as given, or null. */
49export function validSpaceSlug(slug: string): string | null {
50 const s = String(slug ?? "").trim().toLowerCase();
51 if (!/^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(s)) return null;
52 if (RESERVED_SPACE_SLUGS.has(s)) return null;
53 return s;
54}