github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/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  	"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.
    32  func 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.
    41  type 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.
    47  func (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.
    64  func (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.
    73  func (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.
   103  func (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.
   116  func (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.
   132  func (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.
   147  func (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.
   162  func (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.
   177  func (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.
   194  func (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.
   225  func (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.
   269  func (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.
   290  func (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.
   365  func (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.
   375  func (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.
   386  func (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.
   397  func (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.
   408  func (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.
   424  func (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.
   440  func (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.
   456  func (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.
   472  func (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.
   487  func (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  }