Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

71 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([
39 "new",
40 "search",
41 "trash",
42 "templates",
43 "live",
44 "api",
45 "threads",
46 "upload",
47 "export",
48 "settings",
49 "recent",
50 "favorites",
51 "stale",
52 "repo",
53]);
54
55/** A space slug that is free: `base`, else `base-2`, `base-3`, ... */
56export function freeSlug(base: string, taken: ReadonlySet<string>): string {
57 const root = slugify(base, 40) || "space";
58 const ok = (s: string) => !taken.has(s) && !RESERVED_SPACE_SLUGS.has(s);
59 if (ok(root)) return root;
60 for (let n = 2; ; n++) if (ok(`${root}-${n}`)) return `${root}-${n}`;
61}
62
63/** A space slug someone typed: valid as given, or null. */
64export function validSpaceSlug(slug: string): string | null {
65 const s = String(slug ?? "")
66 .trim()
67 .toLowerCase();
68 if (!/^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(s)) return null;
69 if (RESERVED_SPACE_SLUGS.has(s)) return null;
70 return s;
71}