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