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.
15package strings
16
17import (
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.
32func 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.
41type 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.
47func (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.
64func (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.
73func (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.
103func (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.
116func (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.
132func (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.
147func (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.
162func (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.
177func (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.
194func (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.
225func (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.
269func (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.
290func (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.
365func (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.
375func (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.
386func (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.
397func (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.
408func (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.
424func (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.
440func (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.
456func (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.
472func (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.
487func (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}
504