strings.go (12112B)
1 // Copyright 2017 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 // Package strings provides template functions for manipulating strings. 15 package strings 16 17 import ( 18 "errors" 19 "fmt" 20 "html/template" 21 "regexp" 22 "strings" 23 "unicode/utf8" 24 25 "github.com/gohugoio/hugo/common/text" 26 "github.com/gohugoio/hugo/deps" 27 "github.com/gohugoio/hugo/helpers" 28 "github.com/gohugoio/hugo/tpl" 29 30 "github.com/spf13/cast" 31 ) 32 33 // New returns a new instance of the strings-namespaced template functions. 34 func New(d *deps.Deps) *Namespace { 35 titleCaseStyle := d.Cfg.GetString("titleCaseStyle") 36 titleFunc := helpers.GetTitleFunc(titleCaseStyle) 37 return &Namespace{deps: d, titleFunc: titleFunc} 38 } 39 40 // Namespace provides template functions for the "strings" namespace. 41 // Most functions mimic the Go stdlib, but the order of the parameters may be 42 // different to ease their use in the Go template system. 43 type Namespace struct { 44 titleFunc func(s string) string 45 deps *deps.Deps 46 } 47 48 // CountRunes returns the number of runes in s, excluding whitespace. 49 func (ns *Namespace) CountRunes(s any) (int, error) { 50 ss, err := cast.ToStringE(s) 51 if err != nil { 52 return 0, fmt.Errorf("Failed to convert content to string: %w", err) 53 } 54 55 counter := 0 56 for _, r := range tpl.StripHTML(ss) { 57 if !helpers.IsWhitespace(r) { 58 counter++ 59 } 60 } 61 62 return counter, nil 63 } 64 65 // RuneCount returns the number of runes in s. 66 func (ns *Namespace) RuneCount(s any) (int, error) { 67 ss, err := cast.ToStringE(s) 68 if err != nil { 69 return 0, fmt.Errorf("Failed to convert content to string: %w", err) 70 } 71 return utf8.RuneCountInString(ss), nil 72 } 73 74 // CountWords returns the approximate word count in s. 75 func (ns *Namespace) CountWords(s any) (int, error) { 76 ss, err := cast.ToStringE(s) 77 if err != nil { 78 return 0, fmt.Errorf("Failed to convert content to string: %w", err) 79 } 80 81 isCJKLanguage, err := regexp.MatchString(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`, ss) 82 if err != nil { 83 return 0, fmt.Errorf("Failed to match regex pattern against string: %w", err) 84 } 85 86 if !isCJKLanguage { 87 return len(strings.Fields(tpl.StripHTML(ss))), nil 88 } 89 90 counter := 0 91 for _, word := range strings.Fields(tpl.StripHTML(ss)) { 92 runeCount := utf8.RuneCountInString(word) 93 if len(word) == runeCount { 94 counter++ 95 } else { 96 counter += runeCount 97 } 98 } 99 100 return counter, nil 101 } 102 103 // Count counts the number of non-overlapping instances of substr in s. 104 // If substr is an empty string, Count returns 1 + the number of Unicode code points in s. 105 func (ns *Namespace) Count(substr, s any) (int, error) { 106 substrs, err := cast.ToStringE(substr) 107 if err != nil { 108 return 0, fmt.Errorf("Failed to convert substr to string: %w", err) 109 } 110 ss, err := cast.ToStringE(s) 111 if err != nil { 112 return 0, fmt.Errorf("Failed to convert s to string: %w", err) 113 } 114 return strings.Count(ss, substrs), nil 115 } 116 117 // Chomp returns a copy of s with all trailing newline characters removed. 118 func (ns *Namespace) Chomp(s any) (any, error) { 119 ss, err := cast.ToStringE(s) 120 if err != nil { 121 return "", err 122 } 123 124 res := text.Chomp(ss) 125 switch s.(type) { 126 case template.HTML: 127 return template.HTML(res), nil 128 default: 129 return res, nil 130 } 131 } 132 133 // Contains reports whether substr is in s. 134 func (ns *Namespace) Contains(s, substr any) (bool, error) { 135 ss, err := cast.ToStringE(s) 136 if err != nil { 137 return false, err 138 } 139 140 su, err := cast.ToStringE(substr) 141 if err != nil { 142 return false, err 143 } 144 145 return strings.Contains(ss, su), nil 146 } 147 148 // ContainsAny reports whether any Unicode code points in chars are within s. 149 func (ns *Namespace) ContainsAny(s, chars any) (bool, error) { 150 ss, err := cast.ToStringE(s) 151 if err != nil { 152 return false, err 153 } 154 155 sc, err := cast.ToStringE(chars) 156 if err != nil { 157 return false, err 158 } 159 160 return strings.ContainsAny(ss, sc), nil 161 } 162 163 // HasPrefix tests whether the input s begins with prefix. 164 func (ns *Namespace) HasPrefix(s, prefix any) (bool, error) { 165 ss, err := cast.ToStringE(s) 166 if err != nil { 167 return false, err 168 } 169 170 sx, err := cast.ToStringE(prefix) 171 if err != nil { 172 return false, err 173 } 174 175 return strings.HasPrefix(ss, sx), nil 176 } 177 178 // HasSuffix tests whether the input s begins with suffix. 179 func (ns *Namespace) HasSuffix(s, suffix any) (bool, error) { 180 ss, err := cast.ToStringE(s) 181 if err != nil { 182 return false, err 183 } 184 185 sx, err := cast.ToStringE(suffix) 186 if err != nil { 187 return false, err 188 } 189 190 return strings.HasSuffix(ss, sx), nil 191 } 192 193 // Replace returns a copy of the string s with all occurrences of old replaced 194 // with new. The number of replacements can be limited with an optional fourth 195 // parameter. 196 func (ns *Namespace) Replace(s, old, new any, limit ...any) (string, error) { 197 ss, err := cast.ToStringE(s) 198 if err != nil { 199 return "", err 200 } 201 202 so, err := cast.ToStringE(old) 203 if err != nil { 204 return "", err 205 } 206 207 sn, err := cast.ToStringE(new) 208 if err != nil { 209 return "", err 210 } 211 212 if len(limit) == 0 { 213 return strings.ReplaceAll(ss, so, sn), nil 214 } 215 216 lim, err := cast.ToIntE(limit[0]) 217 if err != nil { 218 return "", err 219 } 220 221 return strings.Replace(ss, so, sn, lim), nil 222 } 223 224 // SliceString slices a string by specifying a half-open range with 225 // two indices, start and end. 1 and 4 creates a slice including elements 1 through 3. 226 // The end index can be omitted, it defaults to the string's length. 227 func (ns *Namespace) SliceString(a any, startEnd ...any) (string, error) { 228 aStr, err := cast.ToStringE(a) 229 if err != nil { 230 return "", err 231 } 232 233 var argStart, argEnd int 234 235 argNum := len(startEnd) 236 237 if argNum > 0 { 238 if argStart, err = cast.ToIntE(startEnd[0]); err != nil { 239 return "", errors.New("start argument must be integer") 240 } 241 } 242 if argNum > 1 { 243 if argEnd, err = cast.ToIntE(startEnd[1]); err != nil { 244 return "", errors.New("end argument must be integer") 245 } 246 } 247 248 if argNum > 2 { 249 return "", errors.New("too many arguments") 250 } 251 252 asRunes := []rune(aStr) 253 254 if argNum > 0 && (argStart < 0 || argStart >= len(asRunes)) { 255 return "", errors.New("slice bounds out of range") 256 } 257 258 if argNum == 2 { 259 if argEnd < 0 || argEnd > len(asRunes) { 260 return "", errors.New("slice bounds out of range") 261 } 262 return string(asRunes[argStart:argEnd]), nil 263 } else if argNum == 1 { 264 return string(asRunes[argStart:]), nil 265 } else { 266 return string(asRunes[:]), nil 267 } 268 } 269 270 // Split slices an input string into all substrings separated by delimiter. 271 func (ns *Namespace) Split(a any, delimiter string) ([]string, error) { 272 aStr, err := cast.ToStringE(a) 273 if err != nil { 274 return []string{}, err 275 } 276 277 return strings.Split(aStr, delimiter), nil 278 } 279 280 // Substr extracts parts of a string, beginning at the character at the specified 281 // position, and returns the specified number of characters. 282 // 283 // It normally takes two parameters: start and length. 284 // It can also take one parameter: start, i.e. length is omitted, in which case 285 // the substring starting from start until the end of the string will be returned. 286 // 287 // To extract characters from the end of the string, use a negative start number. 288 // 289 // In addition, borrowing from the extended behavior described at http://php.net/substr, 290 // if length is given and is negative, then that many characters will be omitted from 291 // the end of string. 292 func (ns *Namespace) Substr(a any, nums ...any) (string, error) { 293 s, err := cast.ToStringE(a) 294 if err != nil { 295 return "", err 296 } 297 298 asRunes := []rune(s) 299 rlen := len(asRunes) 300 301 var start, length int 302 303 switch len(nums) { 304 case 0: 305 return "", errors.New("too few arguments") 306 case 1: 307 if start, err = cast.ToIntE(nums[0]); err != nil { 308 return "", errors.New("start argument must be an integer") 309 } 310 length = rlen 311 case 2: 312 if start, err = cast.ToIntE(nums[0]); err != nil { 313 return "", errors.New("start argument must be an integer") 314 } 315 if length, err = cast.ToIntE(nums[1]); err != nil { 316 return "", errors.New("length argument must be an integer") 317 } 318 default: 319 return "", errors.New("too many arguments") 320 } 321 322 if rlen == 0 { 323 return "", nil 324 } 325 326 if start < 0 { 327 start += rlen 328 } 329 330 // start was originally negative beyond rlen 331 if start < 0 { 332 start = 0 333 } 334 335 if start > rlen-1 { 336 return "", nil 337 } 338 339 end := rlen 340 341 switch { 342 case length == 0: 343 return "", nil 344 case length < 0: 345 end += length 346 case length > 0: 347 end = start + length 348 } 349 350 if start >= end { 351 return "", nil 352 } 353 354 if end < 0 { 355 return "", nil 356 } 357 358 if end > rlen { 359 end = rlen 360 } 361 362 return string(asRunes[start:end]), nil 363 } 364 365 // Title returns a copy of the input s with all Unicode letters that begin words 366 // mapped to their title case. 367 func (ns *Namespace) Title(s any) (string, error) { 368 ss, err := cast.ToStringE(s) 369 if err != nil { 370 return "", err 371 } 372 373 return ns.titleFunc(ss), nil 374 } 375 376 // FirstUpper converts s making the first character upper case. 377 func (ns *Namespace) FirstUpper(s any) (string, error) { 378 ss, err := cast.ToStringE(s) 379 if err != nil { 380 return "", err 381 } 382 383 return helpers.FirstUpper(ss), nil 384 } 385 386 // ToLower returns a copy of the input s with all Unicode letters mapped to their 387 // lower case. 388 func (ns *Namespace) ToLower(s any) (string, error) { 389 ss, err := cast.ToStringE(s) 390 if err != nil { 391 return "", err 392 } 393 394 return strings.ToLower(ss), nil 395 } 396 397 // ToUpper returns a copy of the input s with all Unicode letters mapped to their 398 // upper case. 399 func (ns *Namespace) ToUpper(s any) (string, error) { 400 ss, err := cast.ToStringE(s) 401 if err != nil { 402 return "", err 403 } 404 405 return strings.ToUpper(ss), nil 406 } 407 408 // Trim returns converts the strings s removing all leading and trailing characters defined 409 // contained. 410 func (ns *Namespace) Trim(s, cutset any) (string, error) { 411 ss, err := cast.ToStringE(s) 412 if err != nil { 413 return "", err 414 } 415 416 sc, err := cast.ToStringE(cutset) 417 if err != nil { 418 return "", err 419 } 420 421 return strings.Trim(ss, sc), nil 422 } 423 424 // TrimLeft returns a slice of the string s with all leading characters 425 // contained in cutset removed. 426 func (ns *Namespace) TrimLeft(cutset, s any) (string, error) { 427 ss, err := cast.ToStringE(s) 428 if err != nil { 429 return "", err 430 } 431 432 sc, err := cast.ToStringE(cutset) 433 if err != nil { 434 return "", err 435 } 436 437 return strings.TrimLeft(ss, sc), nil 438 } 439 440 // TrimPrefix returns s without the provided leading prefix string. If s doesn't 441 // start with prefix, s is returned unchanged. 442 func (ns *Namespace) TrimPrefix(prefix, s any) (string, error) { 443 ss, err := cast.ToStringE(s) 444 if err != nil { 445 return "", err 446 } 447 448 sx, err := cast.ToStringE(prefix) 449 if err != nil { 450 return "", err 451 } 452 453 return strings.TrimPrefix(ss, sx), nil 454 } 455 456 // TrimRight returns a slice of the string s with all trailing characters 457 // contained in cutset removed. 458 func (ns *Namespace) TrimRight(cutset, s any) (string, error) { 459 ss, err := cast.ToStringE(s) 460 if err != nil { 461 return "", err 462 } 463 464 sc, err := cast.ToStringE(cutset) 465 if err != nil { 466 return "", err 467 } 468 469 return strings.TrimRight(ss, sc), nil 470 } 471 472 // TrimSuffix returns s without the provided trailing suffix string. If s 473 // doesn't end with suffix, s is returned unchanged. 474 func (ns *Namespace) TrimSuffix(suffix, s any) (string, error) { 475 ss, err := cast.ToStringE(s) 476 if err != nil { 477 return "", err 478 } 479 480 sx, err := cast.ToStringE(suffix) 481 if err != nil { 482 return "", err 483 } 484 485 return strings.TrimSuffix(ss, sx), nil 486 } 487 488 // Repeat returns a new string consisting of n copies of the string s. 489 func (ns *Namespace) Repeat(n, s any) (string, error) { 490 ss, err := cast.ToStringE(s) 491 if err != nil { 492 return "", err 493 } 494 495 sn, err := cast.ToIntE(n) 496 if err != nil { 497 return "", err 498 } 499 500 if sn < 0 { 501 return "", errors.New("strings: negative Repeat count") 502 } 503 504 return strings.Repeat(ss, sn), nil 505 }