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