51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
/**
|
|
* 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";
|
|
}
|