/** * Defines what a slug is: the readable name of a category, a dump or a * playlist in a page URL. The server makes and checks slugs with it, and the * edit forms prefill their slug field with {@link slugify}. * * @module */ /** The longest a dump's or a playlist's slug may be. */ export const SLUG_MAX = 60; /** * Matches a well-formed slug: lowercase letters and digits, in words joined by * single hyphens. Categories, dumps and playlists share it. */ export const SLUG_RE: RegExp = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; /** * Checks whether `slug` matches {@link SLUG_RE} and is at most `max` * characters long. */ export function isValidSlug(slug: string, max: number = SLUG_MAX): boolean { return slug.length <= max && SLUG_RE.test(slug); } /** * Turns `title` into lowercase ASCII words joined by hyphens, at most `max` * characters long, cut at a hyphen when it must be cut. Accents are dropped, * and a title with no ASCII letter or digit gives `untitled`. The result * always passes {@link isValidSlug}. */ export function slugify(title: string, max: number = SLUG_MAX): string { const slug = title .toLowerCase() .normalize("NFD") // NFD splits an accented letter into the letter and a combining mark. .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, "-") .substring(0, max + 1) .replace(/^-+/, ""); // Cut at the last hyphen within `max` rather than in the middle of a word, // unless the first word alone is longer. const cut = slug.length > max ? slug.substring( 0, slug.lastIndexOf("-", max) > 0 ? slug.lastIndexOf("-", max) : max, ) : slug; return cut.replace(/-+$/, "") || "untitled"; }