175 lines
4.7 KiB
Go
175 lines
4.7 KiB
Go
|
// Copyright 2013 by Dobrosław Żybort. All rights reserved.
|
||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
|
|
||
|
package slug
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"regexp"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/rainycape/unidecode"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// 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 = true
|
||
|
|
||
|
regexpNonAuthorizedChars = regexp.MustCompile("[^a-zA-Z0-9-_]")
|
||
|
regexpMultipleDashes = regexp.MustCompile("-+")
|
||
|
)
|
||
|
|
||
|
//=============================================================================
|
||
|
|
||
|
// Make returns slug generated from provided string. Will use "en" as language
|
||
|
// substitution.
|
||
|
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.
|
||
|
func 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)
|
||
|
|
||
|
// Process string with selected substitution language.
|
||
|
// Catch ISO 3166-1, ISO 639-1:2002 and ISO 639-3:2007.
|
||
|
switch strings.ToLower(lang) {
|
||
|
case "de", "deu":
|
||
|
slug = SubstituteRune(slug, deSub)
|
||
|
case "en", "eng":
|
||
|
slug = SubstituteRune(slug, enSub)
|
||
|
case "es", "spa":
|
||
|
slug = SubstituteRune(slug, esSub)
|
||
|
case "fi", "fin":
|
||
|
slug = SubstituteRune(slug, fiSub)
|
||
|
case "gr", "el", "ell":
|
||
|
slug = SubstituteRune(slug, grSub)
|
||
|
case "kz", "kk", "kaz":
|
||
|
slug = SubstituteRune(slug, kkSub)
|
||
|
case "nl", "nld":
|
||
|
slug = SubstituteRune(slug, nlSub)
|
||
|
case "pl", "pol":
|
||
|
slug = SubstituteRune(slug, plSub)
|
||
|
case "sv", "swe":
|
||
|
slug = SubstituteRune(slug, svSub)
|
||
|
case "tr", "tur":
|
||
|
slug = SubstituteRune(slug, trSub)
|
||
|
default: // fallback to "en" if lang not found
|
||
|
slug = SubstituteRune(slug, enSub)
|
||
|
}
|
||
|
|
||
|
// Process all non ASCII symbols
|
||
|
slug = unidecode.Unidecode(slug)
|
||
|
|
||
|
if Lowercase {
|
||
|
slug = strings.ToLower(slug)
|
||
|
}
|
||
|
|
||
|
// Process all remaining symbols
|
||
|
slug = regexpNonAuthorizedChars.ReplaceAllString(slug, "-")
|
||
|
slug = regexpMultipleDashes.ReplaceAllString(slug, "-")
|
||
|
slug = strings.Trim(slug, "-_")
|
||
|
|
||
|
if MaxLength > 0 {
|
||
|
slug = smartTruncate(slug)
|
||
|
}
|
||
|
|
||
|
return slug
|
||
|
}
|
||
|
|
||
|
// 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.
|
||
|
func Substitute(s string, sub map[string]string) (buf string) {
|
||
|
buf = s
|
||
|
var keys []string
|
||
|
for k := range sub {
|
||
|
keys = append(keys, k)
|
||
|
}
|
||
|
sort.Strings(keys)
|
||
|
|
||
|
for _, key := range keys {
|
||
|
buf = strings.Replace(buf, key, sub[key], -1)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// SubstituteRune substitutes string chars with provided rune
|
||
|
// substitution map. One pass.
|
||
|
func SubstituteRune(s string, sub map[rune]string) string {
|
||
|
var buf bytes.Buffer
|
||
|
for _, c := range s {
|
||
|
if d, ok := sub[c]; ok {
|
||
|
buf.WriteString(d)
|
||
|
} else {
|
||
|
buf.WriteRune(c)
|
||
|
}
|
||
|
}
|
||
|
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.
|
||
|
// 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 {
|
||
|
if text == "" ||
|
||
|
(MaxLength > 0 && len(text) > MaxLength) ||
|
||
|
text[0] == '-' || text[0] == '_' ||
|
||
|
text[len(text)-1] == '-' || text[len(text)-1] == '_' {
|
||
|
return false
|
||
|
}
|
||
|
for _, c := range text {
|
||
|
if (c < 'a' || c > 'z') && c != '-' && c != '_' && (c < '0' || c > '9') {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|