Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Structure for re-usability and concurrency #51

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
111 changes: 81 additions & 30 deletions slug.go
Expand Up @@ -38,21 +38,40 @@ var (

//=============================================================================

// Slug a structure containing local settings. Using it allows for concurrent slugging
// with different settings.
type Slug struct {
CustomSub map[string]string
CustomRuneSub map[rune]string
MaxLength int
Lowercase bool
}

// New create a new Slug structure using the default settings.
func New() *Slug {
return &Slug{
CustomSub: cloneStringMap(CustomSub),
CustomRuneSub: cloneRuneMap(CustomRuneSub),
MaxLength: MaxLength,
Lowercase: Lowercase,
}
}

// Make returns slug generated from provided string. Will use "en" as language
// substitution.
func Make(s string) (slug string) {
return MakeLang(s, "en")
func (sl *Slug) Make(s string) (slug string) {
return sl.MakeLang(s, "en")
}

// MakeLang returns slug generated from provided string and will use provided
// language for chars substitution.
func MakeLang(s string, lang string) (slug string) {
func (sl *Slug) MakeLang(s string, lang string) (slug string) {
slug = strings.TrimSpace(s)

// Custom substitutions
// Always substitute runes first
slug = SubstituteRune(slug, CustomRuneSub)
slug = Substitute(slug, CustomSub)
slug = SubstituteRune(slug, sl.CustomRuneSub)
slug = Substitute(slug, sl.CustomSub)

// Process string with selected substitution language.
// Catch ISO 3166-1, ISO 639-1:2002 and ISO 639-3:2007.
Expand Down Expand Up @@ -84,7 +103,7 @@ func MakeLang(s string, lang string) (slug string) {
// Process all non ASCII symbols
slug = unidecode.Unidecode(slug)

if Lowercase {
if sl.Lowercase {
slug = strings.ToLower(slug)
}

Expand All @@ -93,13 +112,51 @@ func MakeLang(s string, lang string) (slug string) {
slug = regexpMultipleDashes.ReplaceAllString(slug, "-")
slug = strings.Trim(slug, "-_")

if MaxLength > 0 {
slug = smartTruncate(slug)
if sl.MaxLength > 0 {
slug = sl.smartTruncate(slug)
}

return slug
}

func (sl *Slug) smartTruncate(text string) string {
if len(text) < sl.MaxLength {
return text
}

var truncated string
words := strings.SplitAfter(text, "-")
// If MaxLength is smaller than length of the first word return word
// truncated after MaxLength.
if len(words[0]) > sl.MaxLength {
return words[0][:sl.MaxLength]
}
for _, word := range words {
if len(truncated)+len(word)-1 <= sl.MaxLength {
truncated = truncated + word
} else {
break
}
}
return strings.Trim(truncated, "-")
}

//=============================================================================

// Make returns slug generated from provided string. Will use "en" as language
// substitution.
// Global settings will be used.
func Make(s string) (slug string) {
return MakeLang(s, "en")
}

// MakeLang returns slug generated from provided string and will use provided
// language for chars substitution.
// Global settings will be used.
func MakeLang(s string, lang string) (slug string) {
return New().MakeLang(s, lang)
}

// Substitute returns string with superseded all substrings from
// provided substitution map. Substitution map will be applied in alphabetic
// order. Many passes, on one substitution another one could apply.
Expand Down Expand Up @@ -131,28 +188,6 @@ func SubstituteRune(s string, sub map[rune]string) string {
return buf.String()
}

func smartTruncate(text string) string {
if len(text) < MaxLength {
return text
}

var truncated string
words := strings.SplitAfter(text, "-")
// If MaxLength is smaller than length of the first word return word
// truncated after MaxLength.
if len(words[0]) > MaxLength {
return words[0][:MaxLength]
}
for _, word := range words {
if len(truncated)+len(word)-1 <= MaxLength {
truncated = truncated + word
} else {
break
}
}
return strings.Trim(truncated, "-")
}

// IsSlug returns True if provided text does not contain white characters,
// punctuation, all letters are lower case and only from ASCII range.
// It could contain `-` and `_` but not at the beginning or end of the text.
Expand All @@ -172,3 +207,19 @@ func IsSlug(text string) bool {
}
return true
}

func cloneStringMap(m map[string]string) map[string]string {
new := make(map[string]string, len(m))
for k, v := range m {
new[k] = v
}
return new
}

func cloneRuneMap(m map[rune]string) map[rune]string {
new := make(map[rune]string, len(m))
for k, v := range m {
new[k] = v
}
return new
}
35 changes: 27 additions & 8 deletions slug_test.go
Expand Up @@ -61,6 +61,17 @@ func TestSlugMake(t *testing.T) {
}
}

func TestSlugMakeWithStruct(t *testing.T) {
slug := New()
in := "DOBROSLAWZYBORT"
want := "dobroslawzybort"
got := slug.Make(in)

if got != want {
t.Errorf("slug.Make(%#v) = %#v; want %#v", in, got, want)
}
}

func TestSlugMakeLang(t *testing.T) {
testCases := []struct {
lang string
Expand Down Expand Up @@ -294,37 +305,42 @@ func TestIsSlug(t *testing.T) {
}

func BenchmarkMakeShortAscii(b *testing.B) {
slug := New()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
Make("Hello world")
slug.Make("Hello world")
}
}

func BenchmarkMakeShort(b *testing.B) {
slug := New()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
Make("хелло ворлд")
slug.Make("хелло ворлд")
}
}

func BenchmarkMakeShortSymbols(b *testing.B) {
slug := New()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
Make("·/,:;`˜'\" &€£¥")
slug.Make("·/,:;`˜'\" &€£¥")
}
}

func BenchmarkMakeMediumAscii(b *testing.B) {
slug := New()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
Make("ABCDE FGHIJ KLMNO PQRST UWXYZ ABCDE FGHIJ KLMNO PQRST UWXYZ ABCDE")
slug.Make("ABCDE FGHIJ KLMNO PQRST UWXYZ ABCDE FGHIJ KLMNO PQRST UWXYZ ABCDE")
}
}

func BenchmarkMakeMedium(b *testing.B) {
slug := New()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
Make("ヲァィゥェ ォャュョッ ーアイウエ オカキクケ コサシスセ ソタチツテ トナニヌネ ノハヒフヘ ホマミムメ モヤユヨラ リルレロワ")
slug.Make("ヲァィゥェ ォャュョッ ーアイウエ オカキクケ コサシスセ ソタチツテ トナニヌネ ノハヒフヘ ホマミムメ モヤユヨラ リルレロワ")
}
}

Expand All @@ -344,11 +360,12 @@ func BenchmarkMakeLongAscii(b *testing.B) {
"nisl. Etiam varius imperdiet placerat. Aliquam euismod lacus arcu, " +
"ultrices hendrerit est pellentesque vel. Aliquam sit amet laoreet leo. " +
"Integer eros libero, mollis sed posuere."
slug := New()

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
Make(longStr)
slug.Make(longStr)
}
}

Expand Down Expand Up @@ -399,11 +416,12 @@ func BenchmarkSubstituteRuneLong(b *testing.B) {
func BenchmarkSmartTruncateShort(b *testing.B) {
shortStr := "Hello-world"
MaxLength = 8
slug := New()

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
smartTruncate(shortStr)
slug.smartTruncate(shortStr)
}
}

Expand All @@ -424,11 +442,12 @@ func BenchmarkSmartTruncateLong(b *testing.B) {
"ultrices-hendrerit-est-pellentesque-vel.-Aliquam-sit-amet-laoreet-leo.-" +
"Integer-eros-libero,-mollis-sed-posuere."
MaxLength = 256
slug := New()

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
smartTruncate(longStr)
slug.smartTruncate(longStr)
}
}

Expand Down