Austin Eng | cc2516a | 2023-10-17 20:57:54 +0000 | [diff] [blame] | 1 | // Copyright 2022 The Dawn & Tint Authors |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 2 | // |
Austin Eng | cc2516a | 2023-10-17 20:57:54 +0000 | [diff] [blame] | 3 | // Redistribution and use in source and binary forms, with or without |
| 4 | // modification, are permitted provided that the following conditions are met: |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 5 | // |
Austin Eng | cc2516a | 2023-10-17 20:57:54 +0000 | [diff] [blame] | 6 | // 1. Redistributions of source code must retain the above copyright notice, this |
| 7 | // list of conditions and the following disclaimer. |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 8 | // |
Austin Eng | cc2516a | 2023-10-17 20:57:54 +0000 | [diff] [blame] | 9 | // 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 Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 27 | |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 28 | // Package template wraps the golang "text/template" package to provide an |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 29 | // enhanced template generator. |
| 30 | package template |
| 31 | |
| 32 | import ( |
| 33 | "fmt" |
| 34 | "io" |
| 35 | "io/ioutil" |
Ben Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 36 | "os" |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 37 | "path/filepath" |
Ben Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 38 | "reflect" |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 39 | "strings" |
| 40 | "text/template" |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 41 | |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 42 | "dawn.googlesource.com/dawn/tools/src/container" |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 43 | "dawn.googlesource.com/dawn/tools/src/fileutils" |
Ben Clayton | e5decbe | 2024-02-09 17:29:28 +0000 | [diff] [blame] | 44 | "dawn.googlesource.com/dawn/tools/src/text" |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 45 | "dawn.googlesource.com/dawn/tools/src/transform" |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 46 | ) |
| 47 | |
| 48 | // The template function binding table |
Ben Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 49 | type Functions = template.FuncMap |
| 50 | |
| 51 | type 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 |
| 58 | func 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 |
| 67 | func FromString(name, content string) *Template { |
| 68 | return &Template{name: name, content: content} |
| 69 | } |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 70 | |
| 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 Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 75 | func (t *Template) Run(w io.Writer, data any, funcs Functions) error { |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 76 | g := generator{ |
Ben Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 77 | template: template.New(t.name), |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 78 | } |
| 79 | |
Ben Clayton | ad733018 | 2023-02-07 13:35:58 +0000 | [diff] [blame] | 80 | globals := newMap() |
| 81 | |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 82 | // Add a bunch of generic useful functions |
| 83 | g.funcs = Functions{ |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 84 | "Append": listAppend, |
| 85 | "Concat": listConcat, |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 86 | "Contains": strings.Contains, |
| 87 | "Eval": g.eval, |
Ben Clayton | ad733018 | 2023-02-07 13:35:58 +0000 | [diff] [blame] | 88 | "Globals": func() Map { return globals }, |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 89 | "HasPrefix": strings.HasPrefix, |
| 90 | "HasSuffix": strings.HasSuffix, |
| 91 | "Import": g.importTmpl, |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 92 | "Index": index, |
Ben Clayton | 3c54ba5 | 2023-11-28 21:36:08 +0000 | [diff] [blame] | 93 | "Is": is, |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 94 | "Iterate": iterate, |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 95 | "Join": strings.Join, |
Ben Clayton | 65a1831 | 2023-09-07 13:30:36 +0000 | [diff] [blame] | 96 | "List": list, |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 97 | "Map": newMap, |
Ben Clayton | e5decbe | 2024-02-09 17:29:28 +0000 | [diff] [blame] | 98 | "PascalCase": text.PascalCase, |
Ben Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 99 | "Repeat": strings.Repeat, |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 100 | "Replace": replace, |
| 101 | "SortUnique": listSortUnique, |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 102 | "Split": strings.Split, |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 103 | "Sum": sum, |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 104 | "Title": strings.Title, |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 105 | "ToLower": strings.ToLower, |
| 106 | "ToUpper": strings.ToUpper, |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 107 | "TrimLeft": strings.TrimLeft, |
| 108 | "TrimPrefix": strings.TrimPrefix, |
| 109 | "TrimRight": strings.TrimRight, |
| 110 | "TrimSuffix": strings.TrimSuffix, |
Ben Clayton | e5decbe | 2024-02-09 17:29:28 +0000 | [diff] [blame] | 111 | "Error": func(err string, args ...any) string { panic(fmt.Errorf(err, args...)) }, |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 112 | } |
| 113 | |
| 114 | // Append custom functions |
| 115 | for name, fn := range funcs { |
| 116 | g.funcs[name] = fn |
| 117 | } |
| 118 | |
Ben Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 119 | if err := g.bindAndParse(g.template, t.content); err != nil { |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 120 | return err |
| 121 | } |
| 122 | |
Ben Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 123 | return g.template.Execute(w, data) |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 124 | } |
| 125 | |
| 126 | type generator struct { |
| 127 | template *template.Template |
| 128 | funcs Functions |
| 129 | } |
| 130 | |
| 131 | func (g *generator) bindAndParse(t *template.Template, tmpl string) error { |
| 132 | _, err := t. |
Ben Clayton | 3c54ba5 | 2023-11-28 21:36:08 +0000 | [diff] [blame] | 133 | Funcs(map[string]any(g.funcs)). |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 134 | 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 Clayton | 3c54ba5 | 2023-11-28 21:36:08 +0000 | [diff] [blame] | 141 | func (g *generator) eval(template string, args ...any) (string, error) { |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 142 | 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 Clayton | f6c20f1 | 2024-02-20 14:45:27 +0000 | [diff] [blame] | 167 | return "", fmt.Errorf("while evaluating '%v' with args '%v'\n%v", template, args, err) |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 168 | } |
| 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. |
| 175 | func (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 Clayton | ad733018 | 2023-02-07 13:35:58 +0000 | [diff] [blame] | 184 | t := g.template.New("") |
| 185 | if err := g.bindAndParse(t, string(data)); err != nil { |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 186 | return "", fmt.Errorf("failed to parse '%v': %w", path, err) |
| 187 | } |
Ben Clayton | ad733018 | 2023-02-07 13:35:58 +0000 | [diff] [blame] | 188 | if err := t.Execute(ioutil.Discard, nil); err != nil { |
| 189 | return "", fmt.Errorf("failed to execute '%v': %w", path, err) |
| 190 | } |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 191 | return "", nil |
| 192 | } |
| 193 | |
| 194 | // Map is a simple generic key-value map, which can be used in the template |
Ben Clayton | 3c54ba5 | 2023-11-28 21:36:08 +0000 | [diff] [blame] | 195 | type Map map[any]any |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 196 | |
| 197 | func 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 Clayton | 3c54ba5 | 2023-11-28 21:36:08 +0000 | [diff] [blame] | 201 | func (m Map) Put(key, value any) string { |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 202 | 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 Clayton | 3c54ba5 | 2023-11-28 21:36:08 +0000 | [diff] [blame] | 208 | func (m Map) Get(key any) any { |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 209 | return m[key] |
| 210 | } |
| 211 | |
Ben Clayton | 3c54ba5 | 2023-11-28 21:36:08 +0000 | [diff] [blame] | 212 | // is returns true if the type of object is ty |
| 213 | func is(object any, ty string) bool { |
Ben Clayton | e5decbe | 2024-02-09 17:29:28 +0000 | [diff] [blame] | 214 | if object == nil { |
| 215 | return false |
| 216 | } |
Ben Clayton | 3c54ba5 | 2023-11-28 21:36:08 +0000 | [diff] [blame] | 217 | val := reflect.ValueOf(object) |
| 218 | for val.Kind() == reflect.Pointer { |
| 219 | val = val.Elem() |
| 220 | } |
| 221 | return ty == val.Type().Name() |
| 222 | } |
| 223 | |
dan sinclair | d98c29e | 2024-01-09 12:38:35 +0000 | [diff] [blame] | 224 | // sum returns the sum of provided values |
| 225 | func sum(numbers ...int) int { |
| 226 | n := 0 |
| 227 | for _, i := range numbers { |
| 228 | n += i |
| 229 | } |
| 230 | return n |
| 231 | } |
| 232 | |
Ben Clayton | d114055 | 2022-11-21 17:43:11 +0000 | [diff] [blame] | 233 | // 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}} |
| 235 | func 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 Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 243 | // listAppend returns the slice list with items appended |
| 244 | func 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 |
| 251 | func 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. |
| 261 | func 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 Clayton | 65a1831 | 2023-09-07 13:30:36 +0000 | [diff] [blame] | 269 | // list returns a new slice of elements from the argument list |
Ben Clayton | bc970d4 | 2024-04-16 16:35:43 +0000 | [diff] [blame] | 270 | // Useful for: {{- range List "a" "b" "c" -}}{{.}}{{end}} |
Ben Clayton | 65a1831 | 2023-09-07 13:30:36 +0000 | [diff] [blame] | 271 | func list(elements ...any) []any { return elements } |
| 272 | |
Ben Clayton | 70a2613 | 2023-08-12 09:24:21 +0000 | [diff] [blame] | 273 | func 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 Clayton | 55e6d71 | 2023-08-17 16:52:22 +0000 | [diff] [blame] | 296 | |
| 297 | func replace(s string, oldNew ...string) string { |
| 298 | return strings.NewReplacer(oldNew...).Replace(s) |
| 299 | } |