/** * Client-side mirror of storage.Slugify in the Go engine, used to preview the * filename a new title will produce before the request is sent. The backend * remains authoritative and re-normalises every slug it receives. */ const LATIN_FOLD = { á: 'a', à: 'a', â: 'a', ä: 'a', ã: 'a', å: 'a', ā: 'a', ă: 'a', ą: 'a', ç: 'c', ć: 'c', č: 'c', ĉ: 'c', ċ: 'c', ď: 'd', đ: 'd', é: 'e', è: 'e', ê: 'e', ë: 'e', ē: 'e', ė: 'e', ę: 'e', ě: 'e', ğ: 'g', ĝ: 'g', ġ: 'g', ģ: 'g', í: 'i', ì: 'i', î: 'i', ï: 'i', ī: 'i', į: 'i', ı: 'i', ĵ: 'j', ķ: 'k', ł: 'l', ľ: 'l', ĺ: 'l', ñ: 'n', ń: 'n', ň: 'n', ó: 'o', ò: 'o', ô: 'o', ö: 'o', õ: 'o', ø: 'o', ō: 'o', ő: 'o', ŕ: 'r', ř: 'r', ś: 's', š: 's', ş: 's', ș: 's', ß: 's', þ: 't', ť: 't', ú: 'u', ù: 'u', û: 'u', ü: 'u', ū: 'u', ů: 'u', ű: 'u', ŵ: 'w', ý: 'y', ÿ: 'y', ŷ: 'y', ź: 'z', ż: 'z', ž: 'z', }; const MAX_SLUG_LEN = 120; export function slugify(title) { let out = ''; let pendingSeparator = true; for (const raw of (title ?? '').toLowerCase()) { const char = LATIN_FOLD[raw] ?? raw; if (/[a-z0-9]/.test(char)) { out += char; pendingSeparator = false; } else if (!pendingSeparator) { out += '-'; pendingSeparator = true; } } let slug = out.replace(/^-+|-+$/g, ''); if ([...slug].length > MAX_SLUG_LEN) { slug = [...slug].slice(0, MAX_SLUG_LEN).join('').replace(/-+$/, ''); } return slug; } /** Render a slug back into a human title: core-service-api -> Core Service Api. */ export function titleize(slug) { return (slug ?? '') .split(/[-_]/) .filter(Boolean) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); }