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 }