using System; using System.Globalization; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.WebUtilities; namespace IdentityShroud.Core.Helpers; public static class SlugHelper { public static string GenerateSlug(string text, int maxLength = 40) { if (string.IsNullOrWhiteSpace(text)) return string.Empty; // Normalize to decomposed form (separates accents from letters) string normalized = text.Normalize(NormalizationForm.FormD); StringBuilder sb = new StringBuilder(normalized.Length); bool lastWasHyphen = false; foreach (char c in normalized) { // Skip diacritics (accents) if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark) continue; char lower = char.ToLowerInvariant(c); // Convert valid characters if ((lower >= 'a' && lower <= 'z') || (lower >= '0' && lower <= '9')) { sb.Append(lower); lastWasHyphen = false; } // Convert spaces, underscores, and other separators to hyphen else if (char.IsWhiteSpace(c) || c == '_' || c == '-') { if (!lastWasHyphen && sb.Length > 0) { sb.Append('-'); lastWasHyphen = true; } } // Skip all other characters } // Trim trailing hyphen if any if (sb.Length > 0 && sb[sb.Length - 1] == '-') sb.Length--; string slug = sb.ToString(); // Handle truncation with hash suffix for long strings if (slug.Length > maxLength) { // Generate hash of original text string hashSuffix = GenerateHashSuffix(text); int contentLength = maxLength - hashSuffix.Length; // Truncate at word boundary if possible int cutPoint = contentLength; int lastHyphen = slug.LastIndexOf('-', contentLength - 1); if (lastHyphen > contentLength / 2) cutPoint = lastHyphen; slug = slug.Substring(0, cutPoint).TrimEnd('-') + hashSuffix; } return slug; } private static string GenerateHashSuffix(string text) { using (var sha256 = SHA256.Create()) { byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(text)); // Take first 4 bytes (will become ~5-6 base64url chars) string base64Url = WebEncoders.Base64UrlEncode(hash, 0, 4); return "-" + base64Url; } } }