| // Copyright 2022 The Tint Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // package template wraps the golang "text/template" package to provide an | |
| // enhanced template generator. | |
| package template | |
| import ( | |
| "fmt" | |
| "io" | |
| "io/ioutil" | |
| "path/filepath" | |
| "strings" | |
| "text/template" | |
| "unicode" | |
| "dawn.googlesource.com/dawn/tools/src/fileutils" | |
| ) | |
| // The template function binding table | |
| type Functions map[string]interface{} | |
| // Run executes the template tmpl, writing the output to w. | |
| // funcs are the functions provided to the template. | |
| // See https://golang.org/pkg/text/template/ for documentation on the template | |
| // syntax. | |
| func Run(tmpl string, w io.Writer, funcs Functions) error { | |
| g := generator{ | |
| template: template.New("<template>"), | |
| } | |
| globals := newMap() | |
| // Add a bunch of generic useful functions | |
| g.funcs = Functions{ | |
| "Contains": strings.Contains, | |
| "Eval": g.eval, | |
| "Globals": func() Map { return globals }, | |
| "HasPrefix": strings.HasPrefix, | |
| "HasSuffix": strings.HasSuffix, | |
| "Import": g.importTmpl, | |
| "Iterate": iterate, | |
| "Map": newMap, | |
| "PascalCase": pascalCase, | |
| "Split": strings.Split, | |
| "Title": strings.Title, | |
| "TrimLeft": strings.TrimLeft, | |
| "TrimPrefix": strings.TrimPrefix, | |
| "TrimRight": strings.TrimRight, | |
| "TrimSuffix": strings.TrimSuffix, | |
| } | |
| // Append custom functions | |
| for name, fn := range funcs { | |
| g.funcs[name] = fn | |
| } | |
| if err := g.bindAndParse(g.template, tmpl); err != nil { | |
| return err | |
| } | |
| return g.template.Execute(w, nil) | |
| } | |
| type generator struct { | |
| template *template.Template | |
| funcs Functions | |
| } | |
| func (g *generator) bindAndParse(t *template.Template, tmpl string) error { | |
| _, err := t. | |
| Funcs(map[string]interface{}(g.funcs)). | |
| Option("missingkey=error"). | |
| Parse(tmpl) | |
| return err | |
| } | |
| // eval executes the sub-template with the given name and argument, returning | |
| // the generated output | |
| func (g *generator) eval(template string, args ...interface{}) (string, error) { | |
| target := g.template.Lookup(template) | |
| if target == nil { | |
| return "", fmt.Errorf("template '%v' not found", template) | |
| } | |
| sb := strings.Builder{} | |
| var err error | |
| if len(args) == 1 { | |
| err = target.Execute(&sb, args[0]) | |
| } else { | |
| m := newMap() | |
| if len(args)%2 != 0 { | |
| return "", fmt.Errorf("Eval expects a single argument or list name-value pairs") | |
| } | |
| for i := 0; i < len(args); i += 2 { | |
| name, ok := args[i].(string) | |
| if !ok { | |
| return "", fmt.Errorf("Eval argument %v is not a string", i) | |
| } | |
| m.Put(name, args[i+1]) | |
| } | |
| err = target.Execute(&sb, m) | |
| } | |
| if err != nil { | |
| return "", fmt.Errorf("while evaluating '%v': %v", template, err) | |
| } | |
| return sb.String(), nil | |
| } | |
| // importTmpl parses the template at the given project-relative path, merging | |
| // the template definitions into the global namespace. | |
| // Note: The body of the template is not executed. | |
| func (g *generator) importTmpl(path string) (string, error) { | |
| if strings.Contains(path, "..") { | |
| return "", fmt.Errorf("import path must not contain '..'") | |
| } | |
| path = filepath.Join(fileutils.DawnRoot(), path) | |
| data, err := ioutil.ReadFile(path) | |
| if err != nil { | |
| return "", fmt.Errorf("failed to open '%v': %w", path, err) | |
| } | |
| t := g.template.New("") | |
| if err := g.bindAndParse(t, string(data)); err != nil { | |
| return "", fmt.Errorf("failed to parse '%v': %w", path, err) | |
| } | |
| if err := t.Execute(ioutil.Discard, nil); err != nil { | |
| return "", fmt.Errorf("failed to execute '%v': %w", path, err) | |
| } | |
| return "", nil | |
| } | |
| // Map is a simple generic key-value map, which can be used in the template | |
| type Map map[interface{}]interface{} | |
| func newMap() Map { return Map{} } | |
| // Put adds the key-value pair into the map. | |
| // Put always returns an empty string so nothing is printed in the template. | |
| func (m Map) Put(key, value interface{}) string { | |
| m[key] = value | |
| return "" | |
| } | |
| // Get looks up and returns the value with the given key. If the map does not | |
| // contain the given key, then nil is returned. | |
| func (m Map) Get(key interface{}) interface{} { | |
| return m[key] | |
| } | |
| // iterate returns a slice of length 'n', with each element equal to its index. | |
| // Useful for: {{- range Iterate $n -}}<this will be looped $n times>{{end}} | |
| func iterate(n int) []int { | |
| out := make([]int, n) | |
| for i := range out { | |
| out[i] = i | |
| } | |
| return out | |
| } | |
| // pascalCase returns the snake-case string s transformed into 'PascalCase', | |
| // Rules: | |
| // * The first letter of the string is capitalized | |
| // * Characters following an underscore or number are capitalized | |
| // * Underscores are removed from the returned string | |
| // See: https://en.wikipedia.org/wiki/Camel_case | |
| func pascalCase(s string) string { | |
| b := strings.Builder{} | |
| upper := true | |
| for _, r := range s { | |
| if r == '_' || r == ' ' { | |
| upper = true | |
| continue | |
| } | |
| if upper { | |
| b.WriteRune(unicode.ToUpper(r)) | |
| upper = false | |
| } else { | |
| b.WriteRune(r) | |
| } | |
| if unicode.IsNumber(r) { | |
| upper = true | |
| } | |
| } | |
| return b.String() | |
| } |