blob: e3adeff18c8abbaa8eebba06628201acaddd27ce [file] [log] [blame] [edit]
// 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"
"os"
"path/filepath"
"reflect"
"strings"
"text/template"
"unicode"
"dawn.googlesource.com/dawn/tools/src/fileutils"
)
// The template function binding table
type Functions = template.FuncMap
type Template struct {
name string
content string
}
// FromFile loads the template file at path and builds and returns a Template
// using the file content
func FromFile(path string) (*Template, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return FromString(path, string(content)), nil
}
// FromString returns a Template with the given name from content
func FromString(name, content string) *Template {
return &Template{name: name, content: content}
}
// 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 (t *Template) Run(w io.Writer, data any, funcs Functions) error {
g := generator{
template: template.New(t.name),
}
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,
"ToUpper": strings.ToUpper,
"ToLower": strings.ToLower,
"Repeat": strings.Repeat,
"Split": strings.Split,
"Title": strings.Title,
"TrimLeft": strings.TrimLeft,
"TrimPrefix": strings.TrimPrefix,
"TrimRight": strings.TrimRight,
"TrimSuffix": strings.TrimSuffix,
"Replace": replace,
"Index": index,
"Error": func(err any) string { panic(err) },
}
// Append custom functions
for name, fn := range funcs {
g.funcs[name] = fn
}
if err := g.bindAndParse(g.template, t.content); err != nil {
return err
}
return g.template.Execute(w, data)
}
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()
}
func index(obj any, indices ...any) (any, error) {
v := reflect.ValueOf(obj)
for _, idx := range indices {
for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {
v = v.Elem()
}
if !v.IsValid() || v.IsZero() || v.IsNil() {
return nil, nil
}
switch v.Kind() {
case reflect.Array, reflect.Slice:
v = v.Index(idx.(int))
case reflect.Map:
v = v.MapIndex(reflect.ValueOf(idx))
default:
return nil, fmt.Errorf("cannot index %T (%v)", obj, v.Kind())
}
}
if !v.IsValid() || v.IsZero() || v.IsNil() {
return nil, nil
}
return v.Interface(), nil
}
func replace(s string, oldNew ...string) string {
return strings.NewReplacer(oldNew...).Replace(s)
}