slug.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. // Copyright 2013 by Dobrosław Żybort. All rights reserved.
  2. // This Source Code Form is subject to the terms of the Mozilla Public
  3. // License, v. 2.0. If a copy of the MPL was not distributed with this
  4. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
  5. package slug
  6. import (
  7. "regexp"
  8. "strings"
  9. "github.com/rainycape/unidecode"
  10. )
  11. var (
  12. // Custom substitution map
  13. CustomSub map[string]string
  14. // Custom rune substitution map
  15. CustomRuneSub map[rune]string
  16. // Maximum slug length. It's smart so it will cat slug after full word.
  17. // By default slugs aren't shortened.
  18. // If MaxLength is smaller than length of the first word, then returned
  19. // slug will contain only substring from the first word truncated
  20. // after MaxLength.
  21. MaxLength int
  22. )
  23. //=============================================================================
  24. // Make returns slug generated from provided string. Will use "en" as language
  25. // substitution.
  26. func Make(s string) (slug string) {
  27. return MakeLang(s, "en")
  28. }
  29. // MakeLang returns slug generated from provided string and will use provided
  30. // language for chars substitution.
  31. func MakeLang(s string, lang string) (slug string) {
  32. slug = strings.TrimSpace(s)
  33. // Custom substitutions
  34. // Always substitute runes first
  35. slug = SubstituteRune(slug, CustomRuneSub)
  36. slug = Substitute(slug, CustomSub)
  37. // Process string with selected substitution language
  38. switch lang {
  39. case "de":
  40. slug = SubstituteRune(slug, deSub)
  41. case "en":
  42. slug = SubstituteRune(slug, enSub)
  43. case "pl":
  44. slug = SubstituteRune(slug, plSub)
  45. case "es":
  46. slug = SubstituteRune(slug, esSub)
  47. default: // fallback to "en" if lang not found
  48. slug = SubstituteRune(slug, enSub)
  49. }
  50. slug = SubstituteRune(slug, defaultSub)
  51. // Process all non ASCII symbols
  52. slug = unidecode.Unidecode(slug)
  53. slug = strings.ToLower(slug)
  54. // Process all remaining symbols
  55. slug = regexp.MustCompile("[^a-z0-9-_]").ReplaceAllString(slug, "-")
  56. slug = regexp.MustCompile("-+").ReplaceAllString(slug, "-")
  57. slug = strings.Trim(slug, "-")
  58. if MaxLength > 0 {
  59. slug = smartTruncate(slug)
  60. }
  61. return slug
  62. }
  63. // Substitute returns string with superseded all substrings from
  64. // provided substitution map.
  65. func Substitute(s string, sub map[string]string) (buf string) {
  66. buf = s
  67. for key, val := range sub {
  68. buf = strings.Replace(s, key, val, -1)
  69. }
  70. return
  71. }
  72. // SubstituteRune substitutes string chars with provided rune
  73. // substitution map.
  74. func SubstituteRune(s string, sub map[rune]string) (buf string) {
  75. for _, c := range s {
  76. if d, ok := sub[c]; ok {
  77. buf += d
  78. } else {
  79. buf += string(c)
  80. }
  81. }
  82. return
  83. }
  84. func smartTruncate(text string) string {
  85. if len(text) < MaxLength {
  86. return text
  87. }
  88. var truncated string
  89. words := strings.SplitAfter(text, "-")
  90. // If MaxLength is smaller than length of the first word return word
  91. // truncated after MaxLength.
  92. if len(words[0]) > MaxLength {
  93. return words[0][:MaxLength]
  94. }
  95. for _, word := range words {
  96. if len(truncated)+len(word)-1 <= MaxLength {
  97. truncated = truncated + word
  98. } else {
  99. break
  100. }
  101. }
  102. return strings.Trim(truncated, "-")
  103. }