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  }