| // Copyright 2021 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package tmplfunc provides an extension of Go templates |
| // in which templates can be invoked as if they were functions. |
| // |
| // For example, after parsing |
| // |
| // {{define "link url text"}}<a href="{{.url}}">{{.text}}</a>{{end}} |
| // |
| // this package installs a function named link allowing the template |
| // to be invoked as |
| // |
| // {{link "https://golang.org" "the Go language"}} |
| // |
| // instead of the longer-form (assuming an appropriate function named dict) |
| // |
| // {{template "link" (dict "url" "https://golang.org" "text" "the Go language")}} |
| // |
| // # Function Definitions |
| // |
| // The function installed for a given template depends on the name of the |
| // defined template, which can include not just a function name but also |
| // a list of parameter names. The function name and parameter names must |
| // consist only of letters, digits, and underscores, with a leading non-digit. |
| // |
| // If there is no parameter list, then the function is expected to take |
| // at most one argument, made available in the template body as “.” (dot). |
| // If such a function is called with no arguments, dot will be a nil interface value. |
| // |
| // If there is a parameter list, then the function requires an argument for |
| // each parameter, except for optional and variadic parameters, explained below. |
| // Inside the template, the top-level value “.” is a map[string]interface{} in which |
| // each parameter name is mapped to the corresponding argument value. |
| // A parameter x can therefore be accessed as {{(index . "x")}} or, more concisely, {{.x}}. |
| // |
| // The first special case in parameter handling is that |
| // a parameter can be made optional by adding a “?” suffix after its name. |
| // If the argument list ends before that parameter, the corresponding map entry |
| // will be present and set to a nil value. |
| // The second special case is that a parameter can be made variadic |
| // by adding a “...” suffix after its name. |
| // The corresponding map entry contains a []interface{} holding the |
| // zero or more arguments corresponding to that parameter. |
| // |
| // In the parameter list, required parameters must precede optional parameters, |
| // which must in turn precede any variadic parameter. |
| // |
| // For example, we can revise the link template given earlier to make the |
| // link text optional, substituting the URL when the text is omitted: |
| // |
| // {{define "link url text?"}}<a href="{{.url}}">{{or .text .url}}</a>{{end}} |
| // |
| // The Go home page is {{link "https://golang.org"}}. |
| // |
| // # Usage |
| // |
| // This package is meant to be used with templates from either the |
| // text/template or html/template packages. Given a *template.Template |
| // variable t, substitute: |
| // |
| // t.Parse(text) -> tmplfunc.Parse(t, text) |
| // t.ParseFiles(list) -> tmplfunc.ParseFiles(t, list) |
| // t.ParseGlob(pattern) -> tmplfunc.ParseGlob(t, pattern) |
| // |
| // Parse, ParseFiles, and ParseGlob parse the new templates but also add |
| // functions that invoke them, named according to the function signatures. |
| // Templates can only invoke functions for templates that have already been |
| // defined or that are being defined in the same Parse, ParseFiles, or ParseGlob call. |
| // For example, templates in two files x.tmpl and y.tmpl can call each other |
| // only if ParseFiles or ParseGlob is used to parse both files in a single call. |
| // Otherwise, the parsing of the first file will report that calls to templates in |
| // the second file are calling unknown functions. |
| // |
| // When used with the html/template package, all function-invoked template |
| // calls are treated as invoking templates producing HTML. In order to use a |
| // template that produces some other kind of text fragment, the template must |
| // be invoked directly using the {{template "name"}} form, not as a function call. |
| package tmplfunc |
| |
| import ( |
| "fmt" |
| "io/fs" |
| "io/ioutil" |
| "path" |
| "path/filepath" |
| |
| htmltemplate "html/template" |
| texttemplate "text/template" |
| ) |
| |
| // A Template is a *template.Template, where template refers to either |
| // the html/template or text/template package. |
| type Template interface { |
| // Method here only to make most types that are not a *template.Template |
| // not implement the interface. The requirement here is to be one of the two |
| // template types, not just to have this single method. |
| DefinedTemplates() string |
| Name() string |
| } |
| |
| // Parse is like t.Parse(text), adding functions for the templates defined in text. |
| func Parse(t Template, text string) error { |
| if err := funcs(t, []string{t.Name()}, []string{text}); err != nil { |
| return err |
| } |
| var err error |
| switch t := t.(type) { |
| case *texttemplate.Template: |
| _, err = t.Parse(text) |
| case *htmltemplate.Template: |
| _, err = t.Parse(text) |
| } |
| return err |
| } |
| |
| // ParseFiles is like t.ParseFiles(filenames...), adding functions for the parsed templates. |
| func ParseFiles(t Template, filenames ...string) error { |
| return parseFiles(t, readFileOS, filenames...) |
| } |
| |
| // ParseFS is like ParseFiles or ParseGlob but reads from the file system fs |
| // instead of the host operating system's file system. |
| // It accepts a list of glob patterns. |
| // (Note that most file names serve as glob patterns matching only themselves.) |
| // It of course adds functions for the parsed templates. |
| func ParseFS(t Template, fs fs.FS, patterns ...string) error { |
| return parseFS(t, fs, patterns) |
| } |
| |
| func parseFS(t Template, fsys fs.FS, patterns []string) error { |
| var filenames []string |
| for _, pattern := range patterns { |
| list, err := fs.Glob(fsys, pattern) |
| if err != nil { |
| return err |
| } |
| if len(list) == 0 { |
| return fmt.Errorf("template: pattern matches no files: %#q", pattern) |
| } |
| filenames = append(filenames, list...) |
| } |
| return parseFiles(t, readFileFS(fsys), filenames...) |
| } |
| |
| // parseFiles is the helper for the method and function. If the argument |
| // template is nil, it is created from the first file. |
| func parseFiles(t Template, readFile func(string) (string, []byte, error), filenames ...string) error { |
| if len(filenames) == 0 { |
| // Not really a problem, but be consistent. |
| return fmt.Errorf("tmplfunc: no files named in call to ParseFiles") |
| } |
| |
| var names []string |
| var texts []string |
| for _, filename := range filenames { |
| name, b, err := readFile(filename) |
| if err != nil { |
| return err |
| } |
| names = append(names, name) |
| texts = append(texts, string(b)) |
| } |
| |
| err := funcs(t, names, texts) |
| if err != nil { |
| return err |
| } |
| |
| switch t := t.(type) { |
| case *texttemplate.Template: |
| for i, name := range names { |
| var tmpl *texttemplate.Template |
| if name == t.Name() { |
| tmpl = t |
| } else { |
| tmpl = t.New(name) |
| } |
| if _, err := tmpl.Parse(texts[i]); err != nil { |
| return err |
| } |
| } |
| |
| case *htmltemplate.Template: |
| for i, name := range names { |
| var tmpl *htmltemplate.Template |
| if name == t.Name() { |
| tmpl = t |
| } else { |
| tmpl = t.New(name) |
| } |
| if _, err := tmpl.Parse(texts[i]); err != nil { |
| return err |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| // ParseGlob is like t.ParseGlob(pattern), adding functions for the parsed templates. |
| func ParseGlob(t Template, pattern string) error { |
| filenames, err := filepath.Glob(pattern) |
| if err != nil { |
| return err |
| } |
| if len(filenames) == 0 { |
| return fmt.Errorf("tmplfunc: pattern matches no files: %#q", pattern) |
| } |
| return parseFiles(t, readFileOS, filenames...) |
| } |
| |
| func must(err error) { |
| if err != nil { |
| panic(err) |
| } |
| } |
| |
| // MustParse is like Parse but panics on error. |
| func MustParse(t Template, text string) { |
| must(Parse(t, text)) |
| } |
| |
| // MustParseFiles is like ParseFiles but panics on error. |
| func MustParseFiles(t Template, filenames ...string) { |
| must(ParseFiles(t, filenames...)) |
| } |
| |
| // MustParseGlob is like ParseGlob but panics on error. |
| func MustParseGlob(t Template, pattern string) { |
| must(ParseGlob(t, pattern)) |
| } |
| |
| func readFileOS(file string) (name string, b []byte, err error) { |
| name = filepath.Base(file) |
| b, err = ioutil.ReadFile(file) |
| return |
| } |
| |
| func readFileFS(fsys fs.FS) func(string) (string, []byte, error) { |
| return func(file string) (name string, b []byte, err error) { |
| name = path.Base(file) |
| b, err = fs.ReadFile(fsys, file) |
| return |
| } |
| } |