diff --git a/doc.go b/doc.go index f6f764e..9807fea 100644 --- a/doc.go +++ b/doc.go @@ -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? diff --git a/go.mod b/go.mod index f528a7a..dfbd3a3 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/gosimple/slug -go 1.13 - require github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be diff --git a/slug.go b/slug.go index 272cc3f..017fc29 100644 --- a/slug.go +++ b/slug.go @@ -36,6 +36,25 @@ var ( regexpMultipleDashes = regexp.MustCompile("-+") ) +type Slugger struct { + // 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 @@ -47,12 +66,26 @@ 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, + } +} + +func (sl Slugger) 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. @@ -84,7 +117,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) } @@ -93,8 +126,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 @@ -131,20 +164,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 @@ -159,8 +192,12 @@ 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) +} + +func (sl Slugger) IsSlug(text string) bool { 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 diff --git a/slug_test.go b/slug_test.go index e43eeed..f4a4bda 100644 --- a/slug_test.go +++ b/slug_test.go @@ -403,7 +403,7 @@ func BenchmarkSmartTruncateShort(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - smartTruncate(shortStr) + smartTruncate(shortStr, MaxLength) } } @@ -428,7 +428,7 @@ func BenchmarkSmartTruncateLong(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - smartTruncate(longStr) + smartTruncate(longStr, MaxLength) } }