hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }