1package stdlib
2
3import (
4	"fmt"
5	"regexp"
6	"sort"
7	"strings"
8
9	"github.com/apparentlymart/go-textseg/v13/textseg"
10
11	"github.com/zclconf/go-cty/cty"
12	"github.com/zclconf/go-cty/cty/function"
13	"github.com/zclconf/go-cty/cty/gocty"
14)
15
16var UpperFunc = function.New(&function.Spec{
17	Params: []function.Parameter{
18		{
19			Name:             "str",
20			Type:             cty.String,
21			AllowDynamicType: true,
22		},
23	},
24	Type: function.StaticReturnType(cty.String),
25	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
26		in := args[0].AsString()
27		out := strings.ToUpper(in)
28		return cty.StringVal(out), nil
29	},
30})
31
32var LowerFunc = function.New(&function.Spec{
33	Params: []function.Parameter{
34		{
35			Name:             "str",
36			Type:             cty.String,
37			AllowDynamicType: true,
38		},
39	},
40	Type: function.StaticReturnType(cty.String),
41	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
42		in := args[0].AsString()
43		out := strings.ToLower(in)
44		return cty.StringVal(out), nil
45	},
46})
47
48var ReverseFunc = function.New(&function.Spec{
49	Params: []function.Parameter{
50		{
51			Name:             "str",
52			Type:             cty.String,
53			AllowDynamicType: true,
54		},
55	},
56	Type: function.StaticReturnType(cty.String),
57	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
58		in := []byte(args[0].AsString())
59		out := make([]byte, len(in))
60		pos := len(out)
61
62		inB := []byte(in)
63		for i := 0; i < len(in); {
64			d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true)
65			cluster := in[i : i+d]
66			pos -= len(cluster)
67			copy(out[pos:], cluster)
68			i += d
69		}
70
71		return cty.StringVal(string(out)), nil
72	},
73})
74
75var StrlenFunc = function.New(&function.Spec{
76	Params: []function.Parameter{
77		{
78			Name:             "str",
79			Type:             cty.String,
80			AllowDynamicType: true,
81		},
82	},
83	Type: function.StaticReturnType(cty.Number),
84	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
85		in := args[0].AsString()
86		l := 0
87
88		inB := []byte(in)
89		for i := 0; i < len(in); {
90			d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true)
91			l++
92			i += d
93		}
94
95		return cty.NumberIntVal(int64(l)), nil
96	},
97})
98
99var SubstrFunc = function.New(&function.Spec{
100	Params: []function.Parameter{
101		{
102			Name:             "str",
103			Type:             cty.String,
104			AllowDynamicType: true,
105		},
106		{
107			Name:             "offset",
108			Type:             cty.Number,
109			AllowDynamicType: true,
110		},
111		{
112			Name:             "length",
113			Type:             cty.Number,
114			AllowDynamicType: true,
115		},
116	},
117	Type: function.StaticReturnType(cty.String),
118	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
119		in := []byte(args[0].AsString())
120		var offset, length int
121
122		var err error
123		err = gocty.FromCtyValue(args[1], &offset)
124		if err != nil {
125			return cty.NilVal, err
126		}
127		err = gocty.FromCtyValue(args[2], &length)
128		if err != nil {
129			return cty.NilVal, err
130		}
131
132		if offset < 0 {
133			totalLenNum, err := Strlen(args[0])
134			if err != nil {
135				// should never happen
136				panic("Stdlen returned an error")
137			}
138
139			var totalLen int
140			err = gocty.FromCtyValue(totalLenNum, &totalLen)
141			if err != nil {
142				// should never happen
143				panic("Stdlen returned a non-int number")
144			}
145
146			offset += totalLen
147		} else if length == 0 {
148			// Short circuit here, after error checks, because if a
149			// string of length 0 has been requested it will always
150			// be the empty string
151			return cty.StringVal(""), nil
152		}
153
154		sub := in
155		pos := 0
156		var i int
157
158		// First we'll seek forward to our offset
159		if offset > 0 {
160			for i = 0; i < len(sub); {
161				d, _, _ := textseg.ScanGraphemeClusters(sub[i:], true)
162				i += d
163				pos++
164				if pos == offset {
165					break
166				}
167				if i >= len(in) {
168					return cty.StringVal(""), nil
169				}
170			}
171
172			sub = sub[i:]
173		}
174
175		if length < 0 {
176			// Taking the remainder of the string is a fast path since
177			// we can just return the rest of the buffer verbatim.
178			return cty.StringVal(string(sub)), nil
179		}
180
181		// Otherwise we need to start seeking forward again until we
182		// reach the length we want.
183		pos = 0
184		for i = 0; i < len(sub); {
185			d, _, _ := textseg.ScanGraphemeClusters(sub[i:], true)
186			i += d
187			pos++
188			if pos == length {
189				break
190			}
191		}
192
193		sub = sub[:i]
194
195		return cty.StringVal(string(sub)), nil
196	},
197})
198
199var JoinFunc = function.New(&function.Spec{
200	Params: []function.Parameter{
201		{
202			Name: "separator",
203			Type: cty.String,
204		},
205	},
206	VarParam: &function.Parameter{
207		Name: "lists",
208		Type: cty.List(cty.String),
209	},
210	Type: function.StaticReturnType(cty.String),
211	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
212		sep := args[0].AsString()
213		listVals := args[1:]
214		if len(listVals) < 1 {
215			return cty.UnknownVal(cty.String), fmt.Errorf("at least one list is required")
216		}
217
218		l := 0
219		for _, list := range listVals {
220			if !list.IsWhollyKnown() {
221				return cty.UnknownVal(cty.String), nil
222			}
223			l += list.LengthInt()
224		}
225
226		items := make([]string, 0, l)
227		for ai, list := range listVals {
228			ei := 0
229			for it := list.ElementIterator(); it.Next(); {
230				_, val := it.Element()
231				if val.IsNull() {
232					if len(listVals) > 1 {
233						return cty.UnknownVal(cty.String), function.NewArgErrorf(ai+1, "element %d of list %d is null; cannot concatenate null values", ei, ai+1)
234					}
235					return cty.UnknownVal(cty.String), function.NewArgErrorf(ai+1, "element %d is null; cannot concatenate null values", ei)
236				}
237				items = append(items, val.AsString())
238				ei++
239			}
240		}
241
242		return cty.StringVal(strings.Join(items, sep)), nil
243	},
244})
245
246var SortFunc = function.New(&function.Spec{
247	Params: []function.Parameter{
248		{
249			Name: "list",
250			Type: cty.List(cty.String),
251		},
252	},
253	Type: function.StaticReturnType(cty.List(cty.String)),
254	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
255		listVal := args[0]
256
257		if !listVal.IsWhollyKnown() {
258			// If some of the element values aren't known yet then we
259			// can't yet predict the order of the result.
260			return cty.UnknownVal(retType), nil
261		}
262		if listVal.LengthInt() == 0 { // Easy path
263			return listVal, nil
264		}
265
266		list := make([]string, 0, listVal.LengthInt())
267		for it := listVal.ElementIterator(); it.Next(); {
268			iv, v := it.Element()
269			if v.IsNull() {
270				return cty.UnknownVal(retType), fmt.Errorf("given list element %s is null; a null string cannot be sorted", iv.AsBigFloat().String())
271			}
272			list = append(list, v.AsString())
273		}
274
275		sort.Strings(list)
276		retVals := make([]cty.Value, len(list))
277		for i, s := range list {
278			retVals[i] = cty.StringVal(s)
279		}
280		return cty.ListVal(retVals), nil
281	},
282})
283
284var SplitFunc = function.New(&function.Spec{
285	Params: []function.Parameter{
286		{
287			Name: "separator",
288			Type: cty.String,
289		},
290		{
291			Name: "str",
292			Type: cty.String,
293		},
294	},
295	Type: function.StaticReturnType(cty.List(cty.String)),
296	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
297		sep := args[0].AsString()
298		str := args[1].AsString()
299		elems := strings.Split(str, sep)
300		elemVals := make([]cty.Value, len(elems))
301		for i, s := range elems {
302			elemVals[i] = cty.StringVal(s)
303		}
304		if len(elemVals) == 0 {
305			return cty.ListValEmpty(cty.String), nil
306		}
307		return cty.ListVal(elemVals), nil
308	},
309})
310
311// ChompFunc is a function that removes newline characters at the end of a
312// string.
313var ChompFunc = function.New(&function.Spec{
314	Params: []function.Parameter{
315		{
316			Name: "str",
317			Type: cty.String,
318		},
319	},
320	Type: function.StaticReturnType(cty.String),
321	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
322		newlines := regexp.MustCompile(`(?:\r\n?|\n)*\z`)
323		return cty.StringVal(newlines.ReplaceAllString(args[0].AsString(), "")), nil
324	},
325})
326
327// IndentFunc is a function that adds a given number of spaces to the
328// beginnings of all but the first line in a given multi-line string.
329var IndentFunc = function.New(&function.Spec{
330	Params: []function.Parameter{
331		{
332			Name: "spaces",
333			Type: cty.Number,
334		},
335		{
336			Name: "str",
337			Type: cty.String,
338		},
339	},
340	Type: function.StaticReturnType(cty.String),
341	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
342		var spaces int
343		if err := gocty.FromCtyValue(args[0], &spaces); err != nil {
344			return cty.UnknownVal(cty.String), err
345		}
346		data := args[1].AsString()
347		pad := strings.Repeat(" ", spaces)
348		return cty.StringVal(strings.Replace(data, "\n", "\n"+pad, -1)), nil
349	},
350})
351
352// TitleFunc is a function that converts the first letter of each word in the
353// given string to uppercase.
354var TitleFunc = function.New(&function.Spec{
355	Params: []function.Parameter{
356		{
357			Name: "str",
358			Type: cty.String,
359		},
360	},
361	Type: function.StaticReturnType(cty.String),
362	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
363		return cty.StringVal(strings.Title(args[0].AsString())), nil
364	},
365})
366
367// TrimSpaceFunc is a function that removes any space characters from the start
368// and end of the given string.
369var TrimSpaceFunc = function.New(&function.Spec{
370	Params: []function.Parameter{
371		{
372			Name: "str",
373			Type: cty.String,
374		},
375	},
376	Type: function.StaticReturnType(cty.String),
377	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
378		return cty.StringVal(strings.TrimSpace(args[0].AsString())), nil
379	},
380})
381
382// TrimFunc is a function that removes the specified characters from the start
383// and end of the given string.
384var TrimFunc = function.New(&function.Spec{
385	Params: []function.Parameter{
386		{
387			Name: "str",
388			Type: cty.String,
389		},
390		{
391			Name: "cutset",
392			Type: cty.String,
393		},
394	},
395	Type: function.StaticReturnType(cty.String),
396	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
397		str := args[0].AsString()
398		cutset := args[1].AsString()
399		return cty.StringVal(strings.Trim(str, cutset)), nil
400	},
401})
402
403// TrimPrefixFunc is a function that removes the specified characters from the
404// start the given string.
405var TrimPrefixFunc = function.New(&function.Spec{
406	Params: []function.Parameter{
407		{
408			Name: "str",
409			Type: cty.String,
410		},
411		{
412			Name: "prefix",
413			Type: cty.String,
414		},
415	},
416	Type: function.StaticReturnType(cty.String),
417	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
418		str := args[0].AsString()
419		prefix := args[1].AsString()
420		return cty.StringVal(strings.TrimPrefix(str, prefix)), nil
421	},
422})
423
424// TrimSuffixFunc is a function that removes the specified characters from the
425// end of the given string.
426var TrimSuffixFunc = function.New(&function.Spec{
427	Params: []function.Parameter{
428		{
429			Name: "str",
430			Type: cty.String,
431		},
432		{
433			Name: "suffix",
434			Type: cty.String,
435		},
436	},
437	Type: function.StaticReturnType(cty.String),
438	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
439		str := args[0].AsString()
440		cutset := args[1].AsString()
441		return cty.StringVal(strings.TrimSuffix(str, cutset)), nil
442	},
443})
444
445// Upper is a Function that converts a given string to uppercase.
446func Upper(str cty.Value) (cty.Value, error) {
447	return UpperFunc.Call([]cty.Value{str})
448}
449
450// Lower is a Function that converts a given string to lowercase.
451func Lower(str cty.Value) (cty.Value, error) {
452	return LowerFunc.Call([]cty.Value{str})
453}
454
455// Reverse is a Function that reverses the order of the characters in the
456// given string.
457//
458// As usual, "character" for the sake of this function is a grapheme cluster,
459// so combining diacritics (for example) will be considered together as a
460// single character.
461func Reverse(str cty.Value) (cty.Value, error) {
462	return ReverseFunc.Call([]cty.Value{str})
463}
464
465// Strlen is a Function that returns the length of the given string in
466// characters.
467//
468// As usual, "character" for the sake of this function is a grapheme cluster,
469// so combining diacritics (for example) will be considered together as a
470// single character.
471func Strlen(str cty.Value) (cty.Value, error) {
472	return StrlenFunc.Call([]cty.Value{str})
473}
474
475// Substr is a Function that extracts a sequence of characters from another
476// string and creates a new string.
477//
478// As usual, "character" for the sake of this function is a grapheme cluster,
479// so combining diacritics (for example) will be considered together as a
480// single character.
481//
482// The "offset" index may be negative, in which case it is relative to the
483// end of the given string.
484//
485// The "length" may be -1, in which case the remainder of the string after
486// the given offset will be returned.
487func Substr(str cty.Value, offset cty.Value, length cty.Value) (cty.Value, error) {
488	return SubstrFunc.Call([]cty.Value{str, offset, length})
489}
490
491// Join concatenates together the string elements of one or more lists with a
492// given separator.
493func Join(sep cty.Value, lists ...cty.Value) (cty.Value, error) {
494	args := make([]cty.Value, len(lists)+1)
495	args[0] = sep
496	copy(args[1:], lists)
497	return JoinFunc.Call(args)
498}
499
500// Sort re-orders the elements of a given list of strings so that they are
501// in ascending lexicographical order.
502func Sort(list cty.Value) (cty.Value, error) {
503	return SortFunc.Call([]cty.Value{list})
504}
505
506// Split divides a given string by a given separator, returning a list of
507// strings containing the characters between the separator sequences.
508func Split(sep, str cty.Value) (cty.Value, error) {
509	return SplitFunc.Call([]cty.Value{sep, str})
510}
511
512// Chomp removes newline characters at the end of a string.
513func Chomp(str cty.Value) (cty.Value, error) {
514	return ChompFunc.Call([]cty.Value{str})
515}
516
517// Indent adds a given number of spaces to the beginnings of all but the first
518// line in a given multi-line string.
519func Indent(spaces, str cty.Value) (cty.Value, error) {
520	return IndentFunc.Call([]cty.Value{spaces, str})
521}
522
523// Title converts the first letter of each word in the given string to uppercase.
524func Title(str cty.Value) (cty.Value, error) {
525	return TitleFunc.Call([]cty.Value{str})
526}
527
528// TrimSpace removes any space characters from the start and end of the given string.
529func TrimSpace(str cty.Value) (cty.Value, error) {
530	return TrimSpaceFunc.Call([]cty.Value{str})
531}
532
533// Trim removes the specified characters from the start and end of the given string.
534func Trim(str, cutset cty.Value) (cty.Value, error) {
535	return TrimFunc.Call([]cty.Value{str, cutset})
536}
537
538// TrimPrefix removes the specified prefix from the start of the given string.
539func TrimPrefix(str, prefix cty.Value) (cty.Value, error) {
540	return TrimPrefixFunc.Call([]cty.Value{str, prefix})
541}
542
543// TrimSuffix removes the specified suffix from the end of the given string.
544func TrimSuffix(str, suffix cty.Value) (cty.Value, error) {
545	return TrimSuffixFunc.Call([]cty.Value{str, suffix})
546}
547