1// Copyright (C) 2024 The Android Open Source Project
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//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package templatefns contains functions that are made available in genreqsrc templates.
16package templatefns
17
18import (
19	"fmt"
20	"strings"
21	"text/template"
22	"unicode"
23)
24
25// Funcs returns a mapping from names of template helper functions to the
26// functions themselves.
27func Funcs() template.FuncMap {
28	// These function are made available in templates by calling their key values, e.g. {{SnakeCase "HelloWorld"}}.
29	return template.FuncMap{
30		// go/keep-sorted start
31		"Dict":           dict,
32		"KebabCase":      kebabCase,
33		"LowerCamelCase": lowerCamelCase,
34		"LowerCase":      strings.ToLower,
35		"SafeReqID":      safeReqID,
36		"SnakeCase":      snakeCase,
37		"TitleCase":      titleCase,
38		"UpperCamelCase": upperCamelCase,
39		"UpperCase":      strings.ToUpper,
40		// go/keep-sorted end
41	}
42}
43
44// isDelimiter checks if a rune is some kind of whitespace, '_' or '-'.
45// helper function
46func isDelimiter(r rune) bool {
47	return r == '-' || r == '_' || unicode.IsSpace(r)
48}
49
50func shouldWriteDelimiter(r, prev, next rune) bool {
51	if isDelimiter(prev) {
52		// Don't delimit if we just delimited
53		return false
54	}
55	// Delimit before new uppercase letters and after acronyms
56	caseDelimit := unicode.IsUpper(r) && (unicode.IsLower(prev) || unicode.IsLower(next))
57	// Delimit after digits
58	digitDelimit := !unicode.IsDigit(r) && unicode.IsDigit(prev)
59	return isDelimiter(r) || caseDelimit || digitDelimit
60}
61
62// titleCase converts a string into Title Case.
63func titleCase(s string) string {
64	runes := []rune(s)
65	var out strings.Builder
66	for i, r := range runes {
67		prev, next := ' ', ' '
68		if i > 0 {
69			prev = runes[i-1]
70			if i+1 < len(runes) {
71				next = runes[i+1]
72			}
73		}
74
75		if shouldWriteDelimiter(r, prev, next) {
76			out.WriteRune(' ')
77		}
78
79		if !isDelimiter(r) {
80			// Output all non-delimiters unchanged
81			out.WriteRune(r)
82		}
83	}
84	return strings.Title(out.String())
85}
86
87// snakeCase converts a string into snake_case.
88func snakeCase(s string) string {
89	return strings.ReplaceAll(strings.ToLower(titleCase(s)), " ", "_")
90}
91
92// kebabCase converts a string into kebab-case.
93func kebabCase(s string) string {
94	return strings.ReplaceAll(strings.ToLower(titleCase(s)), " ", "-")
95}
96
97// upperCamelCase converts a string into UpperCamelCase.
98func upperCamelCase(s string) string {
99	return strings.ReplaceAll(titleCase(s), " ", "")
100}
101
102// lowerCamelCase converts a string into lowerCamelCase.
103func lowerCamelCase(s string) string {
104	if len(s) < 2 {
105		return strings.ToLower(s)
106	}
107	runes := []rune(upperCamelCase(s))
108	for i, r := range runes {
109		// Lowercase leading acronyms
110		if i > 1 && unicode.IsLower(r) {
111			return strings.ToLower(string(runes[:i-1])) + string(runes[i-1:])
112		}
113	}
114	return string(unicode.ToLower(runes[0])) + string(runes[1:])
115}
116
117// safeReqID converts a Media Performance Class (MPC) requirement id to a variable name safe string.
118func safeReqID(s string) string {
119	f := func(a, b, c string) string {
120		return strings.Replace(a, b, c, -1)
121	}
122	return "r" + strings.ToLower(f(f(f(s, "/", "__"), ".", "_"), "-", "_"))
123}
124
125// dict converts a list of key-value pairs into a map.
126// If there is an odd number of values, the last value is nil.
127// The last key is preserved so in the template it can be referenced like {{$myDict.key}}.
128func dict(v ...any) map[string]any {
129	dict := map[string]any{}
130	lenv := len(v)
131	for i := 0; i < lenv; i += 2 {
132		key := toString(v[i])
133		if i+1 >= lenv {
134			dict[key] = nil
135			continue
136		}
137		dict[key] = v[i+1]
138	}
139	return dict
140}
141
142// toString converts a value to a string.
143func toString(v any) string {
144	switch v := v.(type) {
145	case string:
146		return v
147	case []byte:
148		return string(v)
149	case error:
150		return v.Error()
151	case fmt.Stringer:
152		return v.String()
153	default:
154		return fmt.Sprintf("%v", v)
155	}
156}
157