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