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  }