blob: 313de309fb7330368e8a02ed7165579c6232f70a [file] [log] [blame]
Austin Engcc2516a2023-10-17 20:57:54 +00001// Copyright 2022 The Dawn & Tint Authors
Ben Claytond1140552022-11-21 17:43:11 +00002//
Austin Engcc2516a2023-10-17 20:57:54 +00003// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are met:
Ben Claytond1140552022-11-21 17:43:11 +00005//
Austin Engcc2516a2023-10-17 20:57:54 +00006// 1. Redistributions of source code must retain the above copyright notice, this
7// list of conditions and the following disclaimer.
Ben Claytond1140552022-11-21 17:43:11 +00008//
Austin Engcc2516a2023-10-17 20:57:54 +00009// 2. Redistributions in binary form must reproduce the above copyright notice,
10// this list of conditions and the following disclaimer in the documentation
11// and/or other materials provided with the distribution.
12//
13// 3. Neither the name of the copyright holder nor the names of its
14// contributors may be used to endorse or promote products derived from
15// this software without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Ben Claytond1140552022-11-21 17:43:11 +000027
Ben Claytonbc970d42024-04-16 16:35:43 +000028// Package template wraps the golang "text/template" package to provide an
Ben Claytond1140552022-11-21 17:43:11 +000029// enhanced template generator.
30package template
31
32import (
33 "fmt"
34 "io"
35 "io/ioutil"
Ben Clayton70a26132023-08-12 09:24:21 +000036 "os"
Ben Claytond1140552022-11-21 17:43:11 +000037 "path/filepath"
Ben Clayton70a26132023-08-12 09:24:21 +000038 "reflect"
Ben Claytond1140552022-11-21 17:43:11 +000039 "strings"
40 "text/template"
Ben Claytond1140552022-11-21 17:43:11 +000041
Ben Claytonbc970d42024-04-16 16:35:43 +000042 "dawn.googlesource.com/dawn/tools/src/container"
Ben Claytond1140552022-11-21 17:43:11 +000043 "dawn.googlesource.com/dawn/tools/src/fileutils"
Ben Claytone5decbe2024-02-09 17:29:28 +000044 "dawn.googlesource.com/dawn/tools/src/text"
Ben Claytonbc970d42024-04-16 16:35:43 +000045 "dawn.googlesource.com/dawn/tools/src/transform"
Ben Claytond1140552022-11-21 17:43:11 +000046)
47
48// The template function binding table
Ben Clayton70a26132023-08-12 09:24:21 +000049type Functions = template.FuncMap
50
51type Template struct {
52 name string
53 content string
54}
55
56// FromFile loads the template file at path and builds and returns a Template
57// using the file content
58func FromFile(path string) (*Template, error) {
59 content, err := os.ReadFile(path)
60 if err != nil {
61 return nil, err
62 }
63 return FromString(path, string(content)), nil
64}
65
66// FromString returns a Template with the given name from content
67func FromString(name, content string) *Template {
68 return &Template{name: name, content: content}
69}
Ben Claytond1140552022-11-21 17:43:11 +000070
71// Run executes the template tmpl, writing the output to w.
72// funcs are the functions provided to the template.
73// See https://golang.org/pkg/text/template/ for documentation on the template
74// syntax.
Ben Clayton70a26132023-08-12 09:24:21 +000075func (t *Template) Run(w io.Writer, data any, funcs Functions) error {
Ben Claytond1140552022-11-21 17:43:11 +000076 g := generator{
Ben Clayton70a26132023-08-12 09:24:21 +000077 template: template.New(t.name),
Ben Claytond1140552022-11-21 17:43:11 +000078 }
79
Ben Claytonad7330182023-02-07 13:35:58 +000080 globals := newMap()
81
Ben Claytond1140552022-11-21 17:43:11 +000082 // Add a bunch of generic useful functions
83 g.funcs = Functions{
Ben Claytonbc970d42024-04-16 16:35:43 +000084 "Append": listAppend,
85 "Concat": listConcat,
Ben Claytond1140552022-11-21 17:43:11 +000086 "Contains": strings.Contains,
87 "Eval": g.eval,
Ben Claytonad7330182023-02-07 13:35:58 +000088 "Globals": func() Map { return globals },
Ben Claytond1140552022-11-21 17:43:11 +000089 "HasPrefix": strings.HasPrefix,
90 "HasSuffix": strings.HasSuffix,
91 "Import": g.importTmpl,
Ben Claytonbc970d42024-04-16 16:35:43 +000092 "Index": index,
Ben Clayton3c54ba52023-11-28 21:36:08 +000093 "Is": is,
Ben Claytond1140552022-11-21 17:43:11 +000094 "Iterate": iterate,
Ben Claytonbc970d42024-04-16 16:35:43 +000095 "Join": strings.Join,
Ben Clayton65a18312023-09-07 13:30:36 +000096 "List": list,
Ben Claytond1140552022-11-21 17:43:11 +000097 "Map": newMap,
Ben Claytone5decbe2024-02-09 17:29:28 +000098 "PascalCase": text.PascalCase,
Ben Clayton70a26132023-08-12 09:24:21 +000099 "Repeat": strings.Repeat,
Ben Claytonbc970d42024-04-16 16:35:43 +0000100 "Replace": replace,
101 "SortUnique": listSortUnique,
Ben Claytond1140552022-11-21 17:43:11 +0000102 "Split": strings.Split,
Ben Claytonbc970d42024-04-16 16:35:43 +0000103 "Sum": sum,
Ben Claytond1140552022-11-21 17:43:11 +0000104 "Title": strings.Title,
Ben Claytonbc970d42024-04-16 16:35:43 +0000105 "ToLower": strings.ToLower,
106 "ToUpper": strings.ToUpper,
Ben Claytond1140552022-11-21 17:43:11 +0000107 "TrimLeft": strings.TrimLeft,
108 "TrimPrefix": strings.TrimPrefix,
109 "TrimRight": strings.TrimRight,
110 "TrimSuffix": strings.TrimSuffix,
Ben Claytone5decbe2024-02-09 17:29:28 +0000111 "Error": func(err string, args ...any) string { panic(fmt.Errorf(err, args...)) },
Ben Claytond1140552022-11-21 17:43:11 +0000112 }
113
114 // Append custom functions
115 for name, fn := range funcs {
116 g.funcs[name] = fn
117 }
118
Ben Clayton70a26132023-08-12 09:24:21 +0000119 if err := g.bindAndParse(g.template, t.content); err != nil {
Ben Claytond1140552022-11-21 17:43:11 +0000120 return err
121 }
122
Ben Clayton70a26132023-08-12 09:24:21 +0000123 return g.template.Execute(w, data)
Ben Claytond1140552022-11-21 17:43:11 +0000124}
125
126type generator struct {
127 template *template.Template
128 funcs Functions
129}
130
131func (g *generator) bindAndParse(t *template.Template, tmpl string) error {
132 _, err := t.
Ben Clayton3c54ba52023-11-28 21:36:08 +0000133 Funcs(map[string]any(g.funcs)).
Ben Claytond1140552022-11-21 17:43:11 +0000134 Option("missingkey=error").
135 Parse(tmpl)
136 return err
137}
138
139// eval executes the sub-template with the given name and argument, returning
140// the generated output
Ben Clayton3c54ba52023-11-28 21:36:08 +0000141func (g *generator) eval(template string, args ...any) (string, error) {
Ben Claytond1140552022-11-21 17:43:11 +0000142 target := g.template.Lookup(template)
143 if target == nil {
144 return "", fmt.Errorf("template '%v' not found", template)
145 }
146 sb := strings.Builder{}
147
148 var err error
149 if len(args) == 1 {
150 err = target.Execute(&sb, args[0])
151 } else {
152 m := newMap()
153 if len(args)%2 != 0 {
154 return "", fmt.Errorf("Eval expects a single argument or list name-value pairs")
155 }
156 for i := 0; i < len(args); i += 2 {
157 name, ok := args[i].(string)
158 if !ok {
159 return "", fmt.Errorf("Eval argument %v is not a string", i)
160 }
161 m.Put(name, args[i+1])
162 }
163 err = target.Execute(&sb, m)
164 }
165
166 if err != nil {
Ben Claytonf6c20f12024-02-20 14:45:27 +0000167 return "", fmt.Errorf("while evaluating '%v' with args '%v'\n%v", template, args, err)
Ben Claytond1140552022-11-21 17:43:11 +0000168 }
169 return sb.String(), nil
170}
171
172// importTmpl parses the template at the given project-relative path, merging
173// the template definitions into the global namespace.
174// Note: The body of the template is not executed.
175func (g *generator) importTmpl(path string) (string, error) {
176 if strings.Contains(path, "..") {
177 return "", fmt.Errorf("import path must not contain '..'")
178 }
179 path = filepath.Join(fileutils.DawnRoot(), path)
180 data, err := ioutil.ReadFile(path)
181 if err != nil {
182 return "", fmt.Errorf("failed to open '%v': %w", path, err)
183 }
Ben Claytonad7330182023-02-07 13:35:58 +0000184 t := g.template.New("")
185 if err := g.bindAndParse(t, string(data)); err != nil {
Ben Claytond1140552022-11-21 17:43:11 +0000186 return "", fmt.Errorf("failed to parse '%v': %w", path, err)
187 }
Ben Claytonad7330182023-02-07 13:35:58 +0000188 if err := t.Execute(ioutil.Discard, nil); err != nil {
189 return "", fmt.Errorf("failed to execute '%v': %w", path, err)
190 }
Ben Claytond1140552022-11-21 17:43:11 +0000191 return "", nil
192}
193
194// Map is a simple generic key-value map, which can be used in the template
Ben Clayton3c54ba52023-11-28 21:36:08 +0000195type Map map[any]any
Ben Claytond1140552022-11-21 17:43:11 +0000196
197func newMap() Map { return Map{} }
198
199// Put adds the key-value pair into the map.
200// Put always returns an empty string so nothing is printed in the template.
Ben Clayton3c54ba52023-11-28 21:36:08 +0000201func (m Map) Put(key, value any) string {
Ben Claytond1140552022-11-21 17:43:11 +0000202 m[key] = value
203 return ""
204}
205
206// Get looks up and returns the value with the given key. If the map does not
207// contain the given key, then nil is returned.
Ben Clayton3c54ba52023-11-28 21:36:08 +0000208func (m Map) Get(key any) any {
Ben Claytond1140552022-11-21 17:43:11 +0000209 return m[key]
210}
211
Ben Clayton3c54ba52023-11-28 21:36:08 +0000212// is returns true if the type of object is ty
213func is(object any, ty string) bool {
Ben Claytone5decbe2024-02-09 17:29:28 +0000214 if object == nil {
215 return false
216 }
Ben Clayton3c54ba52023-11-28 21:36:08 +0000217 val := reflect.ValueOf(object)
218 for val.Kind() == reflect.Pointer {
219 val = val.Elem()
220 }
221 return ty == val.Type().Name()
222}
223
dan sinclaird98c29e2024-01-09 12:38:35 +0000224// sum returns the sum of provided values
225func sum(numbers ...int) int {
226 n := 0
227 for _, i := range numbers {
228 n += i
229 }
230 return n
231}
232
Ben Claytond1140552022-11-21 17:43:11 +0000233// iterate returns a slice of length 'n', with each element equal to its index.
234// Useful for: {{- range Iterate $n -}}<this will be looped $n times>{{end}}
235func iterate(n int) []int {
236 out := make([]int, n)
237 for i := range out {
238 out[i] = i
239 }
240 return out
241}
242
Ben Claytonbc970d42024-04-16 16:35:43 +0000243// listAppend returns the slice list with items appended
244func listAppend(list any, items ...any) any {
245 itemValues := transform.SliceNoErr(items, reflect.ValueOf)
246 return reflect.Append(reflect.ValueOf(list), itemValues...).Interface()
247}
248
249// listConcat returns a slice formed from concatenating all the elements of all
250// the slice arguments
251func listConcat(firstList any, otherLists ...any) any {
252 out := reflect.ValueOf(firstList)
253 for _, list := range otherLists {
254 out = reflect.AppendSlice(out, reflect.ValueOf(list))
255 }
256 return out.Interface()
257}
258
259// listSortUnique returns items sorted by the string-formatted value of each element, with
260// items with the same strings deduplicated.
261func listSortUnique(items []any) []any {
262 m := make(container.Map[string, any], len(items))
263 for _, item := range items {
264 m.Add(fmt.Sprint(item), item)
265 }
266 return m.Values()
267}
268
Ben Clayton65a18312023-09-07 13:30:36 +0000269// list returns a new slice of elements from the argument list
Ben Claytonbc970d42024-04-16 16:35:43 +0000270// Useful for: {{- range List "a" "b" "c" -}}{{.}}{{end}}
Ben Clayton65a18312023-09-07 13:30:36 +0000271func list(elements ...any) []any { return elements }
272
Ben Clayton70a26132023-08-12 09:24:21 +0000273func index(obj any, indices ...any) (any, error) {
274 v := reflect.ValueOf(obj)
275 for _, idx := range indices {
276 for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {
277 v = v.Elem()
278 }
279 if !v.IsValid() || v.IsZero() || v.IsNil() {
280 return nil, nil
281 }
282 switch v.Kind() {
283 case reflect.Array, reflect.Slice:
284 v = v.Index(idx.(int))
285 case reflect.Map:
286 v = v.MapIndex(reflect.ValueOf(idx))
287 default:
288 return nil, fmt.Errorf("cannot index %T (%v)", obj, v.Kind())
289 }
290 }
291 if !v.IsValid() || v.IsZero() || v.IsNil() {
292 return nil, nil
293 }
294 return v.Interface(), nil
295}
Ben Clayton55e6d712023-08-17 16:52:22 +0000296
297func replace(s string, oldNew ...string) string {
298 return strings.NewReplacer(oldNew...).Replace(s)
299}