Skip to content

Instantly share code, notes, and snippets.

@nicolasparada
Created September 14, 2024 20:06
Show Gist options
  • Save nicolasparada/f4324ac6e9cc5fab88805b067bbdac6f to your computer and use it in GitHub Desktop.
Save nicolasparada/f4324ac6e9cc5fab88805b067bbdac6f to your computer and use it in GitHub Desktop.
Slugify
package slug
import (
"crypto/rand"
"fmt"
"math/big"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
var (
reNonAllowed = regexp.MustCompile(`[^a-zA-Z0-9-_]`)
reConsecutiveDashes = regexp.MustCompile(`-+`)
reSpace = regexp.MustCompile(`\s+`)
)
var sub = map[string]string{
`"`: "",
`'`: "",
"’": "",
"`": "",
"‒": "-", // figure dash
"–": "-", // en dash
"—": "-", // em dash
"―": "-",
"&": "and",
"@": "at",
}
// dict to generate random suffixes.
// 0 (zero) and l (lowercase L) are removed to avoid confusion.
// Uppercases are also removed to ease typing.
const dict = "123456789abcdefghijkmnopqrstuvwxyz"
// New transforms a string into a slug.
// Example:
//
// New("Hello, World!") == "hello-world"
func New(s string) (string, error) {
s, err := toASCII(s)
if err != nil {
return "", err
}
s = strings.TrimSpace(s)
s = strings.ToLower(s)
s = reSpace.ReplaceAllString(s, "-")
for old, n := range sub {
s = strings.ReplaceAll(s, old, n)
}
s = reNonAllowed.ReplaceAllString(s, "")
s = reConsecutiveDashes.ReplaceAllString(s, "-")
s = strings.Trim(s, "-_")
return s, nil
}
// Random transforms a string into a slug and appends a random suffix.
// Example:
//
// Random("Hello, World!") == "hello-world-1a2b3c4d5e"
func Random(s string) (string, error) {
s, err := New(s)
if err != nil {
return "", err
}
if s == "" {
s, err := genStr(21)
if err != nil {
return "", fmt.Errorf("generate random slug: %w", err)
}
return s, nil
}
// fill-up the slug with a suffix to reach the minimum size
const minFinalSize = 20
var suffixSize int
if l := utf8.RuneCountInString(s); l < minFinalSize {
suffixSize = minFinalSize - l
}
// suffix is at least 10 characters long
if suffixSize < 10 {
suffixSize = 10
}
suffix, err := genStr(suffixSize)
if err != nil {
return "", fmt.Errorf("generate slug suffix: %w", err)
}
return fmt.Sprintf("%s-%s", s, suffix), nil
}
func toASCII(s string) (string, error) {
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
result, _, err := transform.String(t, s)
if err != nil {
return "", fmt.Errorf("to ascii: %w", err)
}
return result, nil
}
func genStr(n int) (string, error) {
ret := make([]byte, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(dict))))
if err != nil {
return "", err
}
ret[i] = dict[num.Int64()]
}
return string(ret), nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment