diff --git a/slugify.go b/slugify.go new file mode 100644 index 0000000..c94f23d --- /dev/null +++ b/slugify.go @@ -0,0 +1,36 @@ +package util + +import "strings" + +// Slugify returns input as lowercase ASCII letters, digits and hyphens, which +// is safe to put in a URL path, a filename or a fragment identifier. Every run +// of other characters becomes a single hyphen, and the result never starts or +// ends with one. +// +// Characters outside ASCII are treated as separators rather than being folded +// to their nearest ASCII relative, because doing that properly needs a Unicode +// table that this package does not carry. Text with no ASCII letters or digits +// in it therefore slugifies to an empty string. +func Slugify(input string) string { + var out strings.Builder + pendingHyphen := false + + for _, character := range strings.ToLower(input) { + if (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') { + if pendingHyphen { + out.WriteByte('-') + pendingHyphen = false + } + out.WriteRune(character) + continue + } + + // Remember that a separator was seen, but only write a hyphen once + // another usable character turns up, so nothing trails off the end. + if out.Len() > 0 { + pendingHyphen = true + } + } + + return out.String() +} diff --git a/slugify_test.go b/slugify_test.go new file mode 100644 index 0000000..0672bf1 --- /dev/null +++ b/slugify_test.go @@ -0,0 +1,34 @@ +package util + +import "testing" + +func TestSlugify(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"already a slug", "hello-world", "hello-world"}, + {"mixed case", "Hello World", "hello-world"}, + {"punctuation", "What's new?", "what-s-new"}, + {"run of separators", "one --- two", "one-two"}, + {"leading separators", "///one", "one"}, + {"trailing separators", "one///", "one"}, + {"separators at both ends", " --one two-- ", "one-two"}, + {"digits are kept", "Go 1.14 release", "go-1-14-release"}, + {"underscores are separators", "some_name_here", "some-name-here"}, + {"nothing usable", "!!! ???", ""}, + {"empty string", "", ""}, + {"outside ascii", "Grüße, Welt", "gr-e-welt"}, + {"only characters outside ascii", "日本語", ""}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := Slugify(test.input) + if got != test.expected { + t.Errorf("expected %q got %q", test.expected, got) + } + }) + } +}