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

Make safe for use with concurrent goroutines (fix race hazards) #41

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
17 changes: 17 additions & 0 deletions doc.go
Expand Up @@ -38,6 +38,23 @@ Example:
}
textSub := slug.Make("water is hot")
fmt.Println(textSub) // Will print: "sand-is-hot"

// as above but goroutine safe, without race hazard

slugger := slug.New() // captures current global defaults

enText := slugger.MakeLang("This & that", "en")
fmt.Println(enText) // Will print: "this-and-that"

slugger.Lowercase = false // Keep uppercase characters
deUppercaseText := slugger.MakeLang("Diese & Dass", "de")
fmt.Println(deUppercaseText) // Will print: "Diese-und-Dass"

slugger.CustomSub = map[string]string{
"water": "sand",
}
textSub := slugger.Make("water is hot")
fmt.Println(textSub) // Will print: "sand-is-hot"
}

Requests or bugs?
Expand Down
4 changes: 2 additions & 2 deletions go.mod
@@ -1,5 +1,5 @@
module github.com/gosimple/slug

go 1.13

require github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be

go 1.13
71 changes: 58 additions & 13 deletions slug.go
Expand Up @@ -36,6 +36,26 @@ var (
regexpMultipleDashes = regexp.MustCompile("-+")
)

// Slugger configures slug generation
type Slugger struct {
timbunce marked this conversation as resolved.
Show resolved Hide resolved
// CustomSub stores custom substitution map
CustomSub map[string]string
// CustomRuneSub stores custom rune substitution map
CustomRuneSub map[rune]string

// MaxLength stores maximum slug length.
// It's smart so it will cat slug after full word.
// By default slugs aren't shortened.
// If MaxLength is smaller than length of the first word, then returned
// slug will contain only substring from the first word truncated
// after MaxLength.
MaxLength int

// Lowercase defines if the resulting slug is transformed to lowercase.
// Default is true.
Lowercase bool
}

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

// Make returns slug generated from provided string. Will use "en" as language
Expand All @@ -47,12 +67,28 @@ func Make(s string) (slug string) {
// MakeLang returns slug generated from provided string and will use provided
// language for chars substitution.
func MakeLang(s string, lang string) (slug string) {
return New().MakeLang(s, lang)
}

// New returns a Slugger initialized with the current global defaults
func New() Slugger {
return Slugger{
CustomSub: CustomSub,
CustomRuneSub: CustomRuneSub,
MaxLength: MaxLength,
Lowercase: Lowercase,
}
}

// MakeLang returns slug generated from provided string and will use provided
// language for chars substitution.
func (sl Slugger) MakeLang(s string, lang string) (slug string) {
timbunce marked this conversation as resolved.
Show resolved Hide resolved
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 +120,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,8 +129,8 @@ 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 = smartTruncate(slug, sl.MaxLength)
}

return slug
Expand Down Expand Up @@ -131,20 +167,20 @@ func SubstituteRune(s string, sub map[rune]string) string {
return buf.String()
}

func smartTruncate(text string) string {
if len(text) < MaxLength {
func smartTruncate(text string, maxLength int) 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]
// 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 {
if len(truncated)+len(word)-1 <= maxLength {
truncated = truncated + word
} else {
break
Expand All @@ -159,8 +195,17 @@ func smartTruncate(text string) string {
// It should be in range of the MaxLength var if specified.
// All output from slug.Make(text) should pass this test.
func IsSlug(text string) bool {
return Slugger{MaxLength: MaxLength}.IsSlug(text)
}

// 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.
// It should be in range of the MaxLength var if specified.
// All output from slug.Make(text) should pass this test.
func (sl Slugger) IsSlug(text string) bool {
timbunce marked this conversation as resolved.
Show resolved Hide resolved
if text == "" ||
(MaxLength > 0 && len(text) > MaxLength) ||
(sl.MaxLength > 0 && len(text) > sl.MaxLength) ||
text[0] == '-' || text[0] == '_' ||
text[len(text)-1] == '-' || text[len(text)-1] == '_' {
return false
Expand Down
137 changes: 84 additions & 53 deletions slug_test.go
Expand Up @@ -6,6 +6,7 @@
package slug

import (
"fmt"
"testing"
)

Expand Down Expand Up @@ -52,12 +53,16 @@ func TestSlugMake(t *testing.T) {
}

for index, st := range testCases {
got := Make(st.in)
if got != st.want {
t.Errorf(
"%d. Make(%#v) = %#v; want %#v",
index, st.in, got, st.want)
}
t.Run(fmt.Sprintf("%d", index), func(t *testing.T) {
//t.Parallel()
st := st
got := Make(st.in)
if got != st.want {
t.Errorf(
"%d. Make(%#v) = %#v; want %#v",
index, st.in, got, st.want)
}
})
}
}

Expand Down Expand Up @@ -116,13 +121,17 @@ func TestSlugMakeLang(t *testing.T) {
}

for index, smlt := range testCases {
Lowercase = smlt.lowercase
got := MakeLang(smlt.in, smlt.lang)
if got != smlt.want {
t.Errorf(
"%d. MakeLang(%#v, %#v) = %#v; want %#v",
index, smlt.in, smlt.lang, got, smlt.want)
}
t.Run(fmt.Sprintf("%d", index), func(t *testing.T) {
//t.Parallel()
smlt := smlt // closure
Lowercase = smlt.lowercase
got := MakeLang(smlt.in, smlt.lang)
if got != smlt.want {
t.Errorf(
"%d. MakeLang(%#v, %#v) = %#v; want %#v",
index, smlt.in, smlt.lang, got, smlt.want)
}
})
}
}

Expand All @@ -144,14 +153,18 @@ func TestSlugMakeUserSubstituteLang(t *testing.T) {
}

for index, smust := range testCases {
CustomSub = smust.cSub
got := MakeLang(smust.in, smust.lang)
if got != smust.want {
t.Errorf(
"%d. %#v; MakeLang(%#v, %#v) = %#v; want %#v",
index, smust.cSub, smust.in, smust.lang,
got, smust.want)
}
t.Run(fmt.Sprintf("%d", index), func(t *testing.T) {
//t.Parallel()
smust := smust
CustomSub = smust.cSub
got := MakeLang(smust.in, smust.lang)
if got != smust.want {
t.Errorf(
"%d. %#v; MakeLang(%#v, %#v) = %#v; want %#v",
index, smust.cSub, smust.in, smust.lang,
got, smust.want)
}
})
}
}

Expand All @@ -170,15 +183,19 @@ func TestSlugMakeSubstituteOrderLang(t *testing.T) {
}

for index, smsot := range testCases {
CustomRuneSub = smsot.rSub
CustomSub = smsot.sSub
got := Make(smsot.in)
if got != smsot.want {
t.Errorf(
"%d. %#v; %#v; Make(%#v) = %#v; want %#v",
index, smsot.rSub, smsot.sSub, smsot.in,
got, smsot.want)
}
t.Run(fmt.Sprintf("%d", index), func(t *testing.T) {
//t.Parallel()
smsot := smsot
CustomRuneSub = smsot.rSub
CustomSub = smsot.sSub
got := Make(smsot.in)
if got != smsot.want {
t.Errorf(
"%d. %#v; %#v; Make(%#v) = %#v; want %#v",
index, smsot.rSub, smsot.sSub, smsot.in,
got, smsot.want)
}
})
}
}

Expand All @@ -195,12 +212,16 @@ func TestSubstituteLang(t *testing.T) {
}

for index, sst := range testCases {
got := Substitute(sst.in, sst.cSub)
if got != sst.want {
t.Errorf(
"%d. Substitute(%#v, %#v) = %#v; want %#v",
index, sst.in, sst.cSub, got, sst.want)
}
t.Run(fmt.Sprintf("%d", index), func(t *testing.T) {
//t.Parallel()
sst := sst
got := Substitute(sst.in, sst.cSub)
if got != sst.want {
t.Errorf(
"%d. Substitute(%#v, %#v) = %#v; want %#v",
index, sst.in, sst.cSub, got, sst.want)
}
})
}
}

Expand All @@ -217,12 +238,16 @@ func TestSubstituteRuneLang(t *testing.T) {
}

for index, ssrt := range testCases {
got := SubstituteRune(ssrt.in, ssrt.cSub)
if got != ssrt.want {
t.Errorf(
"%d. SubstituteRune(%#v, %#v) = %#v; want %#v",
index, ssrt.in, ssrt.cSub, got, ssrt.want)
}
t.Run(fmt.Sprintf("%d", index), func(t *testing.T) {
//t.Parallel()
ssrt := ssrt
got := SubstituteRune(ssrt.in, ssrt.cSub)
if got != ssrt.want {
t.Errorf(
"%d. SubstituteRune(%#v, %#v) = %#v; want %#v",
index, ssrt.in, ssrt.cSub, got, ssrt.want)
}
})
}
}

Expand All @@ -241,13 +266,17 @@ func TestSlugMakeSmartTruncate(t *testing.T) {
}

for index, smstt := range testCases {
MaxLength = smstt.maxLength
got := Make(smstt.in)
if got != smstt.want {
t.Errorf(
"%d. MaxLength = %v; Make(%#v) = %#v; want %#v",
index, smstt.maxLength, smstt.in, got, smstt.want)
}
t.Run(fmt.Sprintf("%d", index), func(t *testing.T) {
//t.Parallel()
smstt := smstt
MaxLength = smstt.maxLength
got := Make(smstt.in)
if got != smstt.want {
t.Errorf(
"%d. MaxLength = %v; Make(%#v) = %#v; want %#v",
index, smstt.maxLength, smstt.in, got, smstt.want)
}
})
}
}

Expand Down Expand Up @@ -276,8 +305,10 @@ func TestIsSlug(t *testing.T) {
{"outside ASCII –", args{"2000–2013"}, false},
{"smile ☺", args{"smile ☺"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for index, tt := range tests {
t.Run(fmt.Sprintf("%d", index), func(t *testing.T) {
//t.Parallel()
tt := tt
if got := IsSlug(tt.args.text); got != tt.want {
t.Errorf("IsSlug() = %v, want %v", got, tt.want)
}
Expand Down Expand Up @@ -403,7 +434,7 @@ func BenchmarkSmartTruncateShort(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
smartTruncate(shortStr)
smartTruncate(shortStr, MaxLength)
}
}

Expand All @@ -428,7 +459,7 @@ func BenchmarkSmartTruncateLong(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
smartTruncate(longStr)
smartTruncate(longStr, MaxLength)
}
}

Expand Down