Commit f56db6f5 authored by Rob Pike's avatar Rob Pike

text/template: new, simpler API

The Set type is gone. Instead, templates are automatically associated by
being parsed together; nested definitions implicitly create associations.
Only associated templates can invoke one another.

This approach dramatically reduces the breadth of the construction API.

For now, html/template is deleted from src/pkg/Makefile, so this can
be checked in. Nothing in the tree depends on it. It will be updated next.

R=dsymonds, adg, rsc, r, gri, mikesamuel, nigeltao
CC=golang-dev
https://golang.org/cl/5415060
parent af081cd4
...@@ -68,7 +68,7 @@ func saveHandler(w http.ResponseWriter, r *http.Request) { ...@@ -68,7 +68,7 @@ func saveHandler(w http.ResponseWriter, r *http.Request) {
} }
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, err := template.ParseFile(tmpl + ".html") t, err := template.ParseFiles(tmpl + ".html")
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
......
...@@ -33,14 +33,14 @@ func editHandler(w http.ResponseWriter, r *http.Request) { ...@@ -33,14 +33,14 @@ func editHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
p = &Page{Title: title} p = &Page{Title: title}
} }
t, _ := template.ParseFile("edit.html") t, _ := template.ParseFiles("edit.html")
t.Execute(w, p) t.Execute(w, p)
} }
func viewHandler(w http.ResponseWriter, r *http.Request) { func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:] title := r.URL.Path[lenPath:]
p, _ := loadPage(title) p, _ := loadPage(title)
t, _ := template.ParseFile("view.html") t, _ := template.ParseFiles("view.html")
t.Execute(w, p) t.Execute(w, p)
} }
......
...@@ -55,7 +55,7 @@ func saveHandler(w http.ResponseWriter, r *http.Request, title string) { ...@@ -55,7 +55,7 @@ func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
} }
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, err := template.ParseFile(tmpl+".html", nil) t, err := template.ParseFiles(tmpl+".html", nil)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
......
...@@ -51,7 +51,7 @@ func saveHandler(w http.ResponseWriter, r *http.Request) { ...@@ -51,7 +51,7 @@ func saveHandler(w http.ResponseWriter, r *http.Request) {
} }
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFile(tmpl+".html", nil) t, _ := template.ParseFiles(tmpl+".html", nil)
t.Execute(w, p) t.Execute(w, p)
} }
......
...@@ -58,7 +58,7 @@ var templates = make(map[string]*template.Template) ...@@ -58,7 +58,7 @@ var templates = make(map[string]*template.Template)
func init() { func init() {
for _, tmpl := range []string{"edit", "view"} { for _, tmpl := range []string{"edit", "view"} {
t := template.Must(template.ParseFile(tmpl + ".html")) t := template.Must(template.ParseFiles(tmpl + ".html"))
templates[tmpl] = t templates[tmpl] = t
} }
} }
......
...@@ -476,7 +476,7 @@ func editHandler(w http.ResponseWriter, r *http.Request) { ...@@ -476,7 +476,7 @@ func editHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
p = &Page{Title: title} p = &Page{Title: title}
} }
t, _ := template.ParseFile("edit.html") t, _ := template.ParseFiles("edit.html")
t.Execute(w, p) t.Execute(w, p)
} }
</pre> </pre>
...@@ -530,7 +530,7 @@ Modify <code>viewHandler</code> accordingly: ...@@ -530,7 +530,7 @@ Modify <code>viewHandler</code> accordingly:
func viewHandler(w http.ResponseWriter, r *http.Request) { func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:] title := r.URL.Path[lenPath:]
p, _ := loadPage(title) p, _ := loadPage(title)
t, _ := template.ParseFile(&#34;view.html&#34;) t, _ := template.ParseFiles(&#34;view.html&#34;)
t.Execute(w, p) t.Execute(w, p)
} }
</pre> </pre>
...@@ -558,7 +558,7 @@ func editHandler(w http.ResponseWriter, r *http.Request) { ...@@ -558,7 +558,7 @@ func editHandler(w http.ResponseWriter, r *http.Request) {
} }
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFile(tmpl+&#34;.html&#34;, nil) t, _ := template.ParseFiles(tmpl+&#34;.html&#34;, nil)
t.Execute(w, p) t.Execute(w, p)
} }
</pre> </pre>
...@@ -643,7 +643,7 @@ First, let's handle the errors in <code>renderTemplate</code>: ...@@ -643,7 +643,7 @@ First, let's handle the errors in <code>renderTemplate</code>:
<pre> <pre>
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, err := template.ParseFile(tmpl+&#34;.html&#34;, nil) t, err := template.ParseFiles(tmpl+&#34;.html&#34;, nil)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
...@@ -719,7 +719,7 @@ can't be loaded the only sensible thing to do is exit the program. ...@@ -719,7 +719,7 @@ can't be loaded the only sensible thing to do is exit the program.
<pre> <pre>
func init() { func init() {
for _, tmpl := range []string{&#34;edit&#34;, &#34;view&#34;} { for _, tmpl := range []string{&#34;edit&#34;, &#34;view&#34;} {
t := template.Must(template.ParseFile(tmpl + &#34;.html&#34;)) t := template.Must(template.ParseFiles(tmpl + &#34;.html&#34;))
templates[tmpl] = t templates[tmpl] = t
} }
} }
......
...@@ -45,7 +45,7 @@ func main() { ...@@ -45,7 +45,7 @@ func main() {
// Read and parse the input. // Read and parse the input.
name := flag.Args()[0] name := flag.Args()[0]
tmpl := template.New(name).Funcs(template.FuncMap{"code": code}) tmpl := template.New(name).Funcs(template.FuncMap{"code": code})
if _, err := tmpl.ParseFile(name); err != nil { if _, err := tmpl.ParseFiles(name); err != nil {
log.Fatal(err) log.Fatal(err)
} }
......
...@@ -102,7 +102,6 @@ DIRS=\ ...@@ -102,7 +102,6 @@ DIRS=\
hash/crc64\ hash/crc64\
hash/fnv\ hash/fnv\
html\ html\
html/template\
image\ image\
image/bmp\ image/bmp\
image/color\ image/color\
......
...@@ -10,7 +10,6 @@ GOFILES=\ ...@@ -10,7 +10,6 @@ GOFILES=\
exec.go\ exec.go\
funcs.go\ funcs.go\
helper.go\ helper.go\
parse.go\ template.go\
set.go\
include ../../../Make.pkg include ../../../Make.pkg
...@@ -18,8 +18,7 @@ The input text for a template is UTF-8-encoded text in any format. ...@@ -18,8 +18,7 @@ The input text for a template is UTF-8-encoded text in any format.
"{{" and "}}"; all text outside actions is copied to the output unchanged. "{{" and "}}"; all text outside actions is copied to the output unchanged.
Actions may not span newlines, although comments can. Actions may not span newlines, although comments can.
Once constructed, templates and template sets can be executed safely in Once constructed, a template may be executed safely in parallel.
parallel.
Actions Actions
...@@ -221,10 +220,9 @@ All produce the quoted word "output": ...@@ -221,10 +220,9 @@ All produce the quoted word "output":
Functions Functions
During execution functions are found in three function maps: first in the During execution functions are found in two function maps: first in the
template, then in the "template set" (described below), and finally in the template, then in the global function map. By default, no functions are defined
global function map. By default, no functions are defined in the template or in the template but the Funcs methods can be used to add them.
the set but the Funcs methods can be used to add them.
Predefined global functions are named as follows. Predefined global functions are named as follows.
...@@ -265,49 +263,63 @@ Predefined global functions are named as follows. ...@@ -265,49 +263,63 @@ Predefined global functions are named as follows.
The boolean functions take any zero value to be false and a non-zero value to The boolean functions take any zero value to be false and a non-zero value to
be true. be true.
Template sets Associated templates
Each template is named by a string specified when it is created. A template may Each template is named by a string specified when it is created. Also, each
use a template invocation to instantiate another template directly or by its template is associated with zero or more other templates that it may invoke by
name; see the explanation of the template action above. The name is looked up name; such associations are transitive and form a name space of templates.
in the template set associated with the template.
If no template invocation actions occur in the template, the issue of template A template may use a template invocation to instantiate another associated
sets can be ignored. If it does contain invocations, though, the template template; see the explanation of the "template" action above. The name must be
containing the invocations must be part of a template set in which to look up that of a template associated with the template that contains the invocation.
the names.
There are two ways to construct template sets. Nested template definitions
The first is to use a Set's Parse method to create a set of named templates from When parsing a template, another template may be defined and associated with the
a single input defining multiple templates. The syntax of the definitions is to template being parsed. Template definitions must appear at the top level of the
surround each template declaration with a define and end action. template, much like global variables in a Go program.
The syntax of such definitions is to surround each template declaration with a
"define" and "end" action.
The define action names the template being created by providing a string The define action names the template being created by providing a string
constant. Here is a simple example of input to Set.Parse: constant. Here is a simple example:
`{{define "T1"}} definition of template T1 {{end}} `{{define "T1"}}ONE{{end}}
{{define "T2"}} definition of template T2 {{end}} {{define "T2"}}TWO{{end}}
{{define "T3"}} {{template "T1"}} {{template "T2"}} {{end}}` {{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
{{template "T3"}}`
This defines two templates, T1 and T2, and a third T3 that invokes the other two This defines two templates, T1 and T2, and a third T3 that invokes the other two
when it is executed. when it is executed. Finally it invokes T3. If executed this template will
produce the text
ONE TWO
By construction, a template may reside in only one association. If it's
necessary to have a template addressable from multiple associations, the
template definition must be parsed multiple times to create distinct *Template
values.
Parse may be called multiple times to assemble the various associated templates;
see the ParseFiles and ParseGlob functions and methods for simple ways to parse
related templates stored in files.
The second way to build a template set is to use Set's Add method to add a A template may be executed directly or through ExecuteTemplate, which executes
parsed template to a set. A template may be bound to at most one set. If it's an associated template identified by name. To invoke our example above, we
necessary to have a template in multiple sets, the template definition must be might write,
parsed multiple times to create distinct *Template values.
Set.Parse may be called multiple times on different inputs to construct the set. err := tmpl.Execute(os.Stdout, "no data needed")
Two sets may therefore be constructed with a common base set of templates plus, if err != nil {
through a second Parse call each, specializations for some elements. log.Fatalf("execution failed: %s", err)
}
A template may be executed directly or through Set.Execute, which executes a or to invoke a particular template explicitly by name,
named template from the set. To invoke our example above, we might write,
err := set.Execute(os.Stdout, "T3", "no data needed") err := tmpl.ExecuteTemplate(os.Stdout, "T2", "no data needed")
if err != nil { if err != nil {
log.Fatalf("execution failed: %s", err) log.Fatalf("execution failed: %s", err)
} }
*/ */
package template package template
...@@ -85,8 +85,18 @@ func errRecover(errp *error) { ...@@ -85,8 +85,18 @@ func errRecover(errp *error) {
} }
} }
// ExecuteTemplate applies the template associated with t that has the given name
// to the specified data object and writes the output to wr.
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
tmpl := t.tmpl[name]
if tmpl == nil {
return fmt.Errorf("template: no template %q associated with template %q", name, t.name)
}
return tmpl.Execute(wr, data)
}
// Execute applies a parsed template to the specified data object, // Execute applies a parsed template to the specified data object,
// writing the output to wr. // and writes the output to wr.
func (t *Template) Execute(wr io.Writer, data interface{}) (err error) { func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
defer errRecover(&err) defer errRecover(&err)
value := reflect.ValueOf(data) value := reflect.ValueOf(data)
...@@ -251,13 +261,9 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { ...@@ -251,13 +261,9 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
} }
func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) { func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
set := s.tmpl.set tmpl := s.tmpl.tmpl[t.Name]
if set == nil {
s.errorf("no set defined in which to invoke template named %q", t.Name)
}
tmpl := set.tmpl[t.Name]
if tmpl == nil { if tmpl == nil {
s.errorf("template %q not in set", t.Name) s.errorf("template %q not defined", t.Name)
} }
// Variables declared by the pipeline persist. // Variables declared by the pipeline persist.
dot = s.evalPipeline(dot, t.Pipe) dot = s.evalPipeline(dot, t.Pipe)
...@@ -376,7 +382,7 @@ func (s *state) evalFieldChain(dot, receiver reflect.Value, ident []string, args ...@@ -376,7 +382,7 @@ func (s *state) evalFieldChain(dot, receiver reflect.Value, ident []string, args
} }
func (s *state) evalFunction(dot reflect.Value, name string, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalFunction(dot reflect.Value, name string, args []parse.Node, final reflect.Value) reflect.Value {
function, ok := findFunction(name, s.tmpl, s.tmpl.set) function, ok := findFunction(name, s.tmpl)
if !ok { if !ok {
s.errorf("%q is not a defined function", name) s.errorf("%q is not a defined function", name)
} }
...@@ -398,7 +404,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node ...@@ -398,7 +404,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node
if ptr.Kind() != reflect.Interface && ptr.CanAddr() { if ptr.Kind() != reflect.Interface && ptr.CanAddr() {
ptr = ptr.Addr() ptr = ptr.Addr()
} }
if method, ok := methodByName(ptr, fieldName); ok { if method := ptr.MethodByName(fieldName); method.IsValid() {
return s.evalCall(dot, method, fieldName, args, final) return s.evalCall(dot, method, fieldName, args, final)
} }
hasArgs := len(args) > 1 || final.IsValid() hasArgs := len(args) > 1 || final.IsValid()
...@@ -433,17 +439,6 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node ...@@ -433,17 +439,6 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node
panic("not reached") panic("not reached")
} }
// TODO: delete when reflect's own MethodByName is released.
func methodByName(receiver reflect.Value, name string) (reflect.Value, bool) {
typ := receiver.Type()
for i := 0; i < typ.NumMethod(); i++ {
if typ.Method(i).Name == name {
return receiver.Method(i), true // This value includes the receiver.
}
}
return zero, false
}
var ( var (
errorType = reflect.TypeOf((*error)(nil)).Elem() errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
......
...@@ -476,7 +476,7 @@ func vfunc(V, *V) string { ...@@ -476,7 +476,7 @@ func vfunc(V, *V) string {
return "vfunc" return "vfunc"
} }
func testExecute(execTests []execTest, set *Set, t *testing.T) { func testExecute(execTests []execTest, template *Template, t *testing.T) {
b := new(bytes.Buffer) b := new(bytes.Buffer)
funcs := FuncMap{ funcs := FuncMap{
"count": count, "count": count,
...@@ -486,12 +486,13 @@ func testExecute(execTests []execTest, set *Set, t *testing.T) { ...@@ -486,12 +486,13 @@ func testExecute(execTests []execTest, set *Set, t *testing.T) {
"zeroArgs": zeroArgs, "zeroArgs": zeroArgs,
} }
for _, test := range execTests { for _, test := range execTests {
tmpl := New(test.name).Funcs(funcs) var tmpl *Template
theSet := set var err error
if theSet == nil { if template == nil {
theSet = new(Set) tmpl, err = New(test.name).Funcs(funcs).Parse(test.input)
} else {
tmpl, err = template.New(test.name).Funcs(funcs).Parse(test.input)
} }
_, err := tmpl.ParseInSet(test.input, theSet)
if err != nil { if err != nil {
t.Errorf("%s: parse error: %s", test.name, err) t.Errorf("%s: parse error: %s", test.name, err)
continue continue
...@@ -663,24 +664,34 @@ func TestTree(t *testing.T) { ...@@ -663,24 +664,34 @@ func TestTree(t *testing.T) {
}, },
}, },
} }
set := new(Set) tmpl, err := New("root").Delims("(", ")").Parse(treeTemplate)
_, err := set.Delims("(", ")").Parse(treeTemplate)
if err != nil { if err != nil {
t.Fatal("parse error:", err) t.Fatal("parse error:", err)
} }
var b bytes.Buffer var b bytes.Buffer
err = set.Execute(&b, "tree", tree)
if err != nil {
t.Fatal("exec error:", err)
}
stripSpace := func(r rune) rune { stripSpace := func(r rune) rune {
if r == '\t' || r == '\n' { if r == '\t' || r == '\n' {
return -1 return -1
} }
return r return r
} }
result := strings.Map(stripSpace, b.String())
const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]" const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]"
// First by looking up the template.
err = tmpl.Template("tree").Execute(&b, tree)
if err != nil {
t.Fatal("exec error:", err)
}
result := strings.Map(stripSpace, b.String())
if result != expect {
t.Errorf("expected %q got %q", expect, result)
}
// Then direct to execution.
b.Reset()
err = tmpl.ExecuteTemplate(&b, "tree", tree)
if err != nil {
t.Fatal("exec error:", err)
}
result = strings.Map(stripSpace, b.String())
if result != expect { if result != expect {
t.Errorf("expected %q got %q", expect, result) t.Errorf("expected %q got %q", expect, result)
} }
......
...@@ -17,8 +17,9 @@ import ( ...@@ -17,8 +17,9 @@ import (
// FuncMap is the type of the map defining the mapping from names to functions. // FuncMap is the type of the map defining the mapping from names to functions.
// Each function must have either a single return value, or two return values of // Each function must have either a single return value, or two return values of
// which the second has type error. If the second argument evaluates to non-nil // which the second has type error. In that case, if the second (error)
// during execution, execution terminates and Execute returns an error. // argument evaluates to non-nil during execution, execution terminates and
// Execute returns that error.
type FuncMap map[string]interface{} type FuncMap map[string]interface{}
var builtins = FuncMap{ var builtins = FuncMap{
...@@ -78,18 +79,13 @@ func goodFunc(typ reflect.Type) bool { ...@@ -78,18 +79,13 @@ func goodFunc(typ reflect.Type) bool {
return false return false
} }
// findFunction looks for a function in the template, set, and global map. // findFunction looks for a function in the template, and global map.
func findFunction(name string, tmpl *Template, set *Set) (reflect.Value, bool) { func findFunction(name string, tmpl *Template) (reflect.Value, bool) {
if tmpl != nil { if tmpl != nil && tmpl.common != nil {
if fn := tmpl.execFuncs[name]; fn.IsValid() { if fn := tmpl.execFuncs[name]; fn.IsValid() {
return fn, true return fn, true
} }
} }
if set != nil {
if fn := set.execFuncs[name]; fn.IsValid() {
return fn, true
}
}
if fn := builtinFuncs[name]; fn.IsValid() { if fn := builtinFuncs[name]; fn.IsValid() {
return fn, true return fn, true
} }
...@@ -310,7 +306,6 @@ func JSEscape(w io.Writer, b []byte) { ...@@ -310,7 +306,6 @@ func JSEscape(w io.Writer, b []byte) {
if unicode.IsPrint(r) { if unicode.IsPrint(r) {
w.Write(b[i : i+size]) w.Write(b[i : i+size])
} else { } else {
// TODO(dsymonds): Do this without fmt?
fmt.Fprintf(w, "\\u%04X", r) fmt.Fprintf(w, "\\u%04X", r)
} }
i += size - 1 i += size - 1
......
This diff is collapsed.
...@@ -4,17 +4,49 @@ ...@@ -4,17 +4,49 @@
package template package template
// Tests for mulitple-template parsing and execution.
import ( import (
"bytes"
"fmt" "fmt"
"testing" "testing"
) )
type isEmptyTest struct {
name string
input string
empty bool
}
var isEmptyTests = []isEmptyTest{
{"empty", ``, true},
{"nonempty", `hello`, false},
{"spaces only", " \t\n \t\n", true},
{"definition", `{{define "x"}}something{{end}}`, true},
{"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},
{"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n}}", false},
{"definition and action", "{{define `x`}}something{{end}}{{if 3}}foo{{end}}", false},
}
func TestIsEmpty(t *testing.T) {
for _, test := range isEmptyTests {
template, err := New("root").Parse(test.input)
if err != nil {
t.Errorf("%q: unexpected error: %v", test.name, err)
continue
}
if empty := isEmpty(template.Root); empty != test.empty {
t.Errorf("%q: expected %t got %t", test.name, test.empty, empty)
}
}
}
const ( const (
noError = true noError = true
hasError = false hasError = false
) )
type setParseTest struct { type multiParseTest struct {
name string name string
input string input string
ok bool ok bool
...@@ -22,7 +54,7 @@ type setParseTest struct { ...@@ -22,7 +54,7 @@ type setParseTest struct {
results []string results []string
} }
var setParseTests = []setParseTest{ var multiParseTests = []multiParseTest{
{"empty", "", noError, {"empty", "", noError,
nil, nil,
nil}, nil},
...@@ -41,9 +73,9 @@ var setParseTests = []setParseTest{ ...@@ -41,9 +73,9 @@ var setParseTests = []setParseTest{
nil}, nil},
} }
func TestSetParse(t *testing.T) { func TestMultiParse(t *testing.T) {
for _, test := range setParseTests { for _, test := range multiParseTests {
set, err := new(Set).Parse(test.input) template, err := New("root").Parse(test.input)
switch { switch {
case err == nil && !test.ok: case err == nil && !test.ok:
t.Errorf("%q: expected error; got none", test.name) t.Errorf("%q: expected error; got none", test.name)
...@@ -58,15 +90,15 @@ func TestSetParse(t *testing.T) { ...@@ -58,15 +90,15 @@ func TestSetParse(t *testing.T) {
} }
continue continue
} }
if set == nil { if template == nil {
continue continue
} }
if len(set.tmpl) != len(test.names) { if len(template.tmpl) != len(test.names)+1 { // +1 for root
t.Errorf("%s: wrong number of templates; wanted %d got %d", test.name, len(test.names), len(set.tmpl)) t.Errorf("%s: wrong number of templates; wanted %d got %d", test.name, len(test.names), len(template.tmpl))
continue continue
} }
for i, name := range test.names { for i, name := range test.names {
tmpl, ok := set.tmpl[name] tmpl, ok := template.tmpl[name]
if !ok { if !ok {
t.Errorf("%s: can't find template %q", test.name, name) t.Errorf("%s: can't find template %q", test.name, name)
continue continue
...@@ -79,7 +111,7 @@ func TestSetParse(t *testing.T) { ...@@ -79,7 +111,7 @@ func TestSetParse(t *testing.T) {
} }
} }
var setExecTests = []execTest{ var multiExecTests = []execTest{
{"empty", "", "", nil, true}, {"empty", "", "", nil, true},
{"text", "some text", "some text", nil, true}, {"text", "some text", "some text", nil, true},
{"invoke x", `{{template "x" .SI}}`, "TEXT", tVal, true}, {"invoke x", `{{template "x" .SI}}`, "TEXT", tVal, true},
...@@ -96,144 +128,124 @@ var setExecTests = []execTest{ ...@@ -96,144 +128,124 @@ var setExecTests = []execTest{
} }
// These strings are also in testdata/*. // These strings are also in testdata/*.
const setText1 = ` const multiText1 = `
{{define "x"}}TEXT{{end}} {{define "x"}}TEXT{{end}}
{{define "dotV"}}{{.V}}{{end}} {{define "dotV"}}{{.V}}{{end}}
` `
const setText2 = ` const multiText2 = `
{{define "dot"}}{{.}}{{end}} {{define "dot"}}{{.}}{{end}}
{{define "nested"}}{{template "dot" .}}{{end}} {{define "nested"}}{{template "dot" .}}{{end}}
` `
func TestSetExecute(t *testing.T) { func TestMultiExecute(t *testing.T) {
// Declare a set with a couple of templates first. // Declare a couple of templates first.
set := new(Set) template, err := New("root").Parse(multiText1)
_, err := set.Parse(setText1)
if err != nil { if err != nil {
t.Fatalf("error parsing set: %s", err) t.Fatalf("parse error for 1: %s", err)
} }
_, err = set.Parse(setText2) _, err = template.Parse(multiText2)
if err != nil { if err != nil {
t.Fatalf("error parsing set: %s", err) t.Fatalf("parse error for 2: %s", err)
} }
testExecute(setExecTests, set, t) testExecute(multiExecTests, template, t)
} }
func TestSetParseFiles(t *testing.T) { func TestParseFiles(t *testing.T) {
set := new(Set) _, err := ParseFiles("DOES NOT EXIST")
_, err := set.ParseFiles("DOES NOT EXIST")
if err == nil { if err == nil {
t.Error("expected error for non-existent file; got none") t.Error("expected error for non-existent file; got none")
} }
_, err = set.ParseFiles("testdata/file1.tmpl", "testdata/file2.tmpl") template := New("root")
_, err = template.ParseFiles("testdata/file1.tmpl", "testdata/file2.tmpl")
if err != nil { if err != nil {
t.Fatalf("error parsing files: %v", err) t.Fatalf("error parsing files: %v", err)
} }
testExecute(setExecTests, set, t) testExecute(multiExecTests, template, t)
} }
func TestParseSetFiles(t *testing.T) { func TestParseGlob(t *testing.T) {
set := new(Set) _, err := ParseGlob("DOES NOT EXIST")
_, err := ParseSetFiles("DOES NOT EXIST")
if err == nil { if err == nil {
t.Error("expected error for non-existent file; got none") t.Error("expected error for non-existent file; got none")
} }
set, err = ParseSetFiles("testdata/file1.tmpl", "testdata/file2.tmpl") _, err = New("error").ParseGlob("[x")
if err != nil {
t.Fatalf("error parsing files: %v", err)
}
testExecute(setExecTests, set, t)
}
func TestSetParseGlob(t *testing.T) {
_, err := new(Set).ParseGlob("DOES NOT EXIST")
if err == nil {
t.Error("expected error for non-existent file; got none")
}
_, err = new(Set).ParseGlob("[x")
if err == nil { if err == nil {
t.Error("expected error for bad pattern; got none") t.Error("expected error for bad pattern; got none")
} }
set, err := new(Set).ParseGlob("testdata/file*.tmpl") template := New("root")
_, err = template.ParseGlob("testdata/file*.tmpl")
if err != nil { if err != nil {
t.Fatalf("error parsing files: %v", err) t.Fatalf("error parsing files: %v", err)
} }
testExecute(setExecTests, set, t) testExecute(multiExecTests, template, t)
} }
func TestParseSetGlob(t *testing.T) { // In these tests, actual content (not just template definitions) comes from the parsed files.
_, err := ParseSetGlob("DOES NOT EXIST")
if err == nil {
t.Error("expected error for non-existent file; got none")
}
_, err = ParseSetGlob("[x")
if err == nil {
t.Error("expected error for bad pattern; got none")
}
set, err := ParseSetGlob("testdata/file*.tmpl")
if err != nil {
t.Fatalf("error parsing files: %v", err)
}
testExecute(setExecTests, set, t)
}
var templateFileExecTests = []execTest{ var templateFileExecTests = []execTest{
{"test", `{{template "tmpl1.tmpl"}}{{template "tmpl2.tmpl"}}`, "template1\ntemplate2\n", 0, true}, {"test", `{{template "tmpl1.tmpl"}}{{template "tmpl2.tmpl"}}`, "template1\n\ny\ntemplate2\n\nx\n", 0, true},
} }
func TestSetParseTemplateFiles(t *testing.T) { func TestParseFilesWithData(t *testing.T) {
_, err := ParseTemplateFiles("DOES NOT EXIST") template, err := New("root").ParseFiles("testdata/tmpl1.tmpl", "testdata/tmpl2.tmpl")
if err == nil {
t.Error("expected error for non-existent file; got none")
}
set, err := new(Set).ParseTemplateFiles("testdata/tmpl1.tmpl", "testdata/tmpl2.tmpl")
if err != nil { if err != nil {
t.Fatalf("error parsing files: %v", err) t.Fatalf("error parsing files: %v", err)
} }
testExecute(templateFileExecTests, set, t) testExecute(templateFileExecTests, template, t)
} }
func TestParseTemplateFiles(t *testing.T) { func TestParseGlobWithData(t *testing.T) {
_, err := ParseTemplateFiles("DOES NOT EXIST") template, err := New("root").ParseGlob("testdata/tmpl*.tmpl")
if err == nil {
t.Error("expected error for non-existent file; got none")
}
set, err := new(Set).ParseTemplateFiles("testdata/tmpl1.tmpl", "testdata/tmpl2.tmpl")
if err != nil { if err != nil {
t.Fatalf("error parsing files: %v", err) t.Fatalf("error parsing files: %v", err)
} }
testExecute(templateFileExecTests, set, t) testExecute(templateFileExecTests, template, t)
} }
func TestSetParseTemplateGlob(t *testing.T) { const (
_, err := ParseTemplateGlob("DOES NOT EXIST") cloneText1 = `{{define "a"}}{{template "b"}}{{template "c"}}{{end}}`
if err == nil { cloneText2 = `{{define "b"}}b{{end}}`
t.Error("expected error for non-existent file; got none") cloneText3 = `{{define "c"}}root{{end}}`
cloneText4 = `{{define "c"}}clone{{end}}`
)
func TestClone(t *testing.T) {
// Create some templates and clone the root.
root, err := New("root").Parse(cloneText1)
if err != nil {
t.Fatal(err)
} }
_, err = new(Set).ParseTemplateGlob("[x") _, err = root.Parse(cloneText2)
if err == nil { if err != nil {
t.Error("expected error for bad pattern; got none") t.Fatal(err)
} }
set, err := new(Set).ParseTemplateGlob("testdata/tmpl*.tmpl") clone := root.Clone()
// Add variants to both.
_, err = root.Parse(cloneText3)
if err != nil { if err != nil {
t.Fatalf("error parsing files: %v", err) t.Fatal(err)
} }
testExecute(templateFileExecTests, set, t) _, err = clone.Parse(cloneText4)
} if err != nil {
t.Fatal(err)
func TestParseTemplateGlob(t *testing.T) {
_, err := ParseTemplateGlob("DOES NOT EXIST")
if err == nil {
t.Error("expected error for non-existent file; got none")
} }
_, err = ParseTemplateGlob("[x") // Execute root.
if err == nil { var b bytes.Buffer
t.Error("expected error for bad pattern; got none") err = root.ExecuteTemplate(&b, "a", 0)
if err != nil {
t.Fatal(err)
} }
set, err := ParseTemplateGlob("testdata/tmpl*.tmpl") if b.String() != "broot" {
t.Errorf("expected %q got %q", "broot", b.String())
}
// Execute copy.
b.Reset()
err = clone.ExecuteTemplate(&b, "a", 0)
if err != nil { if err != nil {
t.Fatalf("error parsing files: %v", err) t.Fatal(err)
}
if b.String() != "bclone" {
t.Errorf("expected %q got %q", "bclone", b.String())
} }
testExecute(templateFileExecTests, set, t)
} }
// Copyright 2011 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 template
import (
"reflect"
"text/template/parse"
)
// Template is the representation of a parsed template.
type Template struct {
name string
*parse.Tree
leftDelim string
rightDelim string
// We use two maps, one for parsing and one for execution.
// This separation makes the API cleaner since it doesn't
// expose reflection to the client.
parseFuncs FuncMap
execFuncs map[string]reflect.Value
set *Set // can be nil.
}
// Name returns the name of the template.
func (t *Template) Name() string {
return t.name
}
// Parsing.
// New allocates a new template with the given name.
func New(name string) *Template {
return &Template{
name: name,
parseFuncs: make(FuncMap),
execFuncs: make(map[string]reflect.Value),
}
}
// Delims sets the action delimiters, to be used in a subsequent
// parse, to the specified strings.
// An empty delimiter stands for the corresponding default: {{ or }}.
// The return value is the template, so calls can be chained.
func (t *Template) Delims(left, right string) *Template {
t.leftDelim = left
t.rightDelim = right
return t
}
// Funcs adds the elements of the argument map to the template's function
// map. It panics if a value in the map is not a function with appropriate
// return type.
// The return value is the template, so calls can be chained.
func (t *Template) Funcs(funcMap FuncMap) *Template {
addValueFuncs(t.execFuncs, funcMap)
addFuncs(t.parseFuncs, funcMap)
return t
}
// Parse parses the template definition string to construct an internal
// representation of the template for execution.
func (t *Template) Parse(s string) (tmpl *Template, err error) {
t.Tree, err = parse.New(t.name).Parse(s, t.leftDelim, t.rightDelim, nil, t.parseFuncs, builtins)
if err != nil {
return nil, err
}
return t, nil
}
// ParseInSet parses the template definition string to construct an internal
// representation of the template for execution. It also adds the template
// to the set, which must not be nil. It is an error if s is already defined in the set.
// Function bindings are checked against those in the set.
func (t *Template) ParseInSet(s string, set *Set) (tmpl *Template, err error) {
t.Tree, err = parse.New(t.name).Parse(s, t.leftDelim, t.rightDelim, set.trees, t.parseFuncs, set.parseFuncs, builtins)
if err != nil {
return nil, err
}
err = set.add(t)
return t, err
}
...@@ -97,6 +97,15 @@ func (t *Tree) expect(expected itemType, context string) item { ...@@ -97,6 +97,15 @@ func (t *Tree) expect(expected itemType, context string) item {
return token return token
} }
// expectEither consumes the next token and guarantees it has one of the required types.
func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item {
token := t.next()
if token.typ != expected1 && token.typ != expected2 {
t.errorf("expected %s or %s in %s; got %s", expected1, expected2, context, token)
}
return token
}
// unexpected complains about the token and terminates processing. // unexpected complains about the token and terminates processing.
func (t *Tree) unexpected(token item, context string) { func (t *Tree) unexpected(token item, context string) {
t.errorf("unexpected %s in %s", token, context) t.errorf("unexpected %s in %s", token, context)
...@@ -162,9 +171,18 @@ func (t *Tree) Parse(s, leftDelim, rightDelim string, treeSet map[string]*Tree, ...@@ -162,9 +171,18 @@ func (t *Tree) Parse(s, leftDelim, rightDelim string, treeSet map[string]*Tree,
t.startParse(funcs, lex(t.Name, s, leftDelim, rightDelim)) t.startParse(funcs, lex(t.Name, s, leftDelim, rightDelim))
t.parse(treeSet) t.parse(treeSet)
t.stopParse() t.stopParse()
t.add(treeSet)
return t, nil return t, nil
} }
// add adds tree to the treeSet.
func (t *Tree) add(treeSet map[string]*Tree) {
if _, present := treeSet[t.Name]; present {
t.errorf("template: multiple definition of template %q", t.Name)
}
treeSet[t.Name] = t
}
// parse is the top-level parser for a template, essentially the same // parse is the top-level parser for a template, essentially the same
// as itemList except it also parses {{define}} actions. // as itemList except it also parses {{define}} actions.
// It runs to EOF. // It runs to EOF.
...@@ -174,7 +192,7 @@ func (t *Tree) parse(treeSet map[string]*Tree) (next Node) { ...@@ -174,7 +192,7 @@ func (t *Tree) parse(treeSet map[string]*Tree) (next Node) {
if t.peek().typ == itemLeftDelim { if t.peek().typ == itemLeftDelim {
delim := t.next() delim := t.next()
if t.next().typ == itemDefine { if t.next().typ == itemDefine {
newT := New("new definition") // name will be updated once we know it. newT := New("definition") // name will be updated once we know it.
newT.startParse(t.funcs, t.lex) newT.startParse(t.funcs, t.lex)
newT.parseDefinition(treeSet) newT.parseDefinition(treeSet)
continue continue
...@@ -194,11 +212,8 @@ func (t *Tree) parse(treeSet map[string]*Tree) (next Node) { ...@@ -194,11 +212,8 @@ func (t *Tree) parse(treeSet map[string]*Tree) (next Node) {
// installs the definition in the treeSet map. The "define" keyword has already // installs the definition in the treeSet map. The "define" keyword has already
// been scanned. // been scanned.
func (t *Tree) parseDefinition(treeSet map[string]*Tree) { func (t *Tree) parseDefinition(treeSet map[string]*Tree) {
if treeSet == nil {
t.errorf("no set specified for template definition")
}
const context = "define clause" const context = "define clause"
name := t.expect(itemString, context) name := t.expectOneOf(itemString, itemRawString, context)
var err error var err error
t.Name, err = strconv.Unquote(name.val) t.Name, err = strconv.Unquote(name.val)
if err != nil { if err != nil {
...@@ -211,10 +226,7 @@ func (t *Tree) parseDefinition(treeSet map[string]*Tree) { ...@@ -211,10 +226,7 @@ func (t *Tree) parseDefinition(treeSet map[string]*Tree) {
t.errorf("unexpected %s in %s", end, context) t.errorf("unexpected %s in %s", end, context)
} }
t.stopParse() t.stopParse()
if _, present := treeSet[t.Name]; present { t.add(treeSet)
t.errorf("template: %q multiply defined", name)
}
treeSet[t.Name] = t
} }
// itemList: // itemList:
......
...@@ -236,7 +236,7 @@ var builtins = map[string]interface{}{ ...@@ -236,7 +236,7 @@ var builtins = map[string]interface{}{
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
for _, test := range parseTests { for _, test := range parseTests {
tmpl, err := New(test.name).Parse(test.input, "", "", nil, builtins) tmpl, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree), builtins)
switch { switch {
case err == nil && !test.ok: case err == nil && !test.ok:
t.Errorf("%q: expected error; got none", test.name) t.Errorf("%q: expected error; got none", test.name)
......
// Copyright 2011 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 template
import (
"fmt"
"io"
"reflect"
"text/template/parse"
)
// Set holds a set of related templates that can refer to one another by name.
// The zero value represents an empty set.
// A template may be a member of multiple sets.
type Set struct {
tmpl map[string]*Template
trees map[string]*parse.Tree // maintained by parse package
leftDelim string
rightDelim string
parseFuncs FuncMap
execFuncs map[string]reflect.Value
}
func (s *Set) init() {
if s.tmpl == nil {
s.tmpl = make(map[string]*Template)
s.parseFuncs = make(FuncMap)
s.execFuncs = make(map[string]reflect.Value)
}
}
// Delims sets the action delimiters, to be used in a subsequent
// parse, to the specified strings.
// An empty delimiter stands for the corresponding default: {{ or }}.
// The return value is the set, so calls can be chained.
func (s *Set) Delims(left, right string) *Set {
s.leftDelim = left
s.rightDelim = right
return s
}
// Funcs adds the elements of the argument map to the set's function map. It
// panics if a value in the map is not a function with appropriate return
// type.
// The return value is the set, so calls can be chained.
func (s *Set) Funcs(funcMap FuncMap) *Set {
s.init()
addValueFuncs(s.execFuncs, funcMap)
addFuncs(s.parseFuncs, funcMap)
return s
}
// Add adds the argument templates to the set. It panics if two templates
// with the same name are added or if a template is already a member of
// a set.
// The return value is the set, so calls can be chained.
func (s *Set) Add(templates ...*Template) *Set {
for _, t := range templates {
if err := s.add(t); err != nil {
panic(err)
}
}
return s
}
// add adds the argument template to the set.
func (s *Set) add(t *Template) error {
s.init()
if t.set != nil {
return fmt.Errorf("template: %q already in a set", t.name)
}
if _, ok := s.tmpl[t.name]; ok {
return fmt.Errorf("template: %q already defined in set", t.name)
}
s.tmpl[t.name] = t
t.set = s
return nil
}
// Template returns the template with the given name in the set,
// or nil if there is no such template.
func (s *Set) Template(name string) *Template {
return s.tmpl[name]
}
// FuncMap returns the set's function map.
func (s *Set) FuncMap() FuncMap {
return s.parseFuncs
}
// Execute applies the named template to the specified data object, writing
// the output to wr.
func (s *Set) Execute(wr io.Writer, name string, data interface{}) error {
tmpl := s.tmpl[name]
if tmpl == nil {
return fmt.Errorf("template: no template %q in set", name)
}
return tmpl.Execute(wr, data)
}
// Parse parses a string into a set of named templates. Parse may be called
// multiple times for a given set, adding the templates defined in the string
// to the set. It is an error if a template has a name already defined in the set.
func (s *Set) Parse(text string) (*Set, error) {
// TODO: "ROOT" is just a placeholder while we rejig the API.
trees, err := parse.Parse("ROOT", text, s.leftDelim, s.rightDelim, s.parseFuncs, builtins)
if err != nil {
return nil, err
}
s.init()
for name, tree := range trees {
tmpl := New(name)
tmpl.Tree = tree
err = s.add(tmpl)
if err != nil {
return s, err
}
}
return s, nil
}
// Copyright 2011 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 template
import (
"bytes"
"fmt"
"reflect"
"text/template/parse"
)
// common holds the information shared by related templates.
type common struct {
tmpl map[string]*Template
// We use two maps, one for parsing and one for execution.
// This separation makes the API cleaner since it doesn't
// expose reflection to the client.
parseFuncs FuncMap
execFuncs map[string]reflect.Value
}
// Template is the representation of a parsed template. The *parse.Tree
// field is exported only for use by html/template and should be treated
// as unexported by all other clients.
type Template struct {
name string
*parse.Tree
*common
leftDelim string
rightDelim string
}
// New allocates a new template with the given name.
func New(name string) *Template {
return &Template{
name: name,
}
}
// Name returns the name of the template.
func (t *Template) Name() string {
return t.name
}
// New allocates a new template associated with the given one and with the same
// delimiters. The association, which is transitive, allows one template to
// invoke another with a {{template}} action.
func (t *Template) New(name string) *Template {
t.init()
return &Template{
name: name,
common: t.common,
leftDelim: t.leftDelim,
rightDelim: t.rightDelim,
}
}
func (t *Template) init() {
if t.common == nil {
t.common = new(common)
t.tmpl = make(map[string]*Template)
t.parseFuncs = make(FuncMap)
t.execFuncs = make(map[string]reflect.Value)
}
}
// Clone returns a duplicate of the template, including all associated
// templates. The actual representation is not copied, but the name space of
// associated templates is, so further calls to Parse in the copy will add
// templates to the copy but not to the original. Clone can be used to prepare
// common templates and use them with variant definitions for other templates by
// adding the variants after the clone is made.
func (t *Template) Clone() *Template {
nt := t.copy()
nt.init()
for k, v := range t.tmpl {
// The associated templates share nt's common structure.
tmpl := v.copy()
tmpl.common = nt.common
nt.tmpl[k] = tmpl
}
for k, v := range t.parseFuncs {
nt.parseFuncs[k] = v
}
for k, v := range t.execFuncs {
nt.execFuncs[k] = v
}
return nt
}
// copy returns a shallow copy of t, with common set to nil.
func (t *Template) copy() *Template {
nt := New(t.name)
nt.Tree = t.Tree
nt.leftDelim = t.leftDelim
nt.rightDelim = t.rightDelim
return nt
}
// Templates returns a slice of the templates associated with t, including t
// itself.
func (t *Template) Templates() []*Template {
// Return a slice so we don't expose the map.
m := make([]*Template, 0, len(t.tmpl))
for _, v := range t.tmpl {
m = append(m, v)
}
return m
}
// Delims sets the action delimiters to the specified strings, to be used in
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
// definitions will inherit the settings. An empty delimiter stands for the
// corresponding default: {{ or }}.
// The return value is the template, so calls can be chained.
func (t *Template) Delims(left, right string) *Template {
t.leftDelim = left
t.rightDelim = right
return t
}
// Funcs adds the elements of the argument map to the template's function map.
// It panics if a value in the map is not a function with appropriate return
// type. However, it is legal to overwrite elements of the map. The return
// value is the template, so calls can be chained.
func (t *Template) Funcs(funcMap FuncMap) *Template {
t.init()
addValueFuncs(t.execFuncs, funcMap)
addFuncs(t.parseFuncs, funcMap)
return t
}
// Template returns the template with the given name that is associated with t,
// or nil if there is no such template.
func (t *Template) Template(name string) *Template {
return t.tmpl[name]
}
// Parse parses a string into a template. Nested template definitions will be
// associated with the top-level template t. Parse may be called multiple times
// to parse definitions of templates to associate with t. It is an error if a
// resulting template is non-empty (contains content other than template
// definitions) and would replace a non-empty template with the same name.
// (In multiple calls to Parse with the same receiver template, only one call
// can contain text other than space, comments, and template definitions.)
func (t *Template) Parse(text string) (*Template, error) {
t.init()
trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins)
if err != nil {
return nil, err
}
// Add the newly parsed trees, including the one for t, into our common structure.
for name, tree := range trees {
// If the name we parsed is the name of this template, overwrite this template.
// The associate method checks it's not a redefinition.
tmpl := t
if name != t.name {
tmpl = t.New(name)
}
// Even if t == tmpl, we need to install it in the common.tmpl map.
if err := t.associate(tmpl); err != nil {
return nil, err
}
tmpl.Tree = tree
tmpl.leftDelim = t.leftDelim
tmpl.rightDelim = t.rightDelim
}
return t, nil
}
// associate installs the new template into the group of templates associated
// with t. It is an error to reuse a name except to overwrite an empty
// template. The two are already known to share the common structure.
func (t *Template) associate(new *Template) error {
if new.common != t.common {
panic("internal error: associate not common")
}
name := new.name
if old := t.tmpl[name]; old != nil {
oldIsEmpty := isEmpty(old.Root)
newIsEmpty := isEmpty(new.Root)
if !oldIsEmpty && !newIsEmpty {
return fmt.Errorf("template: redefinition of template %q", name)
}
if newIsEmpty {
// Whether old is empty or not, new is empty; no reason to replace old.
return nil
}
}
t.tmpl[name] = new
return nil
}
// isEmpty reports whether this tree (node) is empty of everything but space.
func isEmpty(n parse.Node) bool {
switch n := n.(type) {
case *parse.ActionNode:
case *parse.IfNode:
case *parse.ListNode:
for _, node := range n.Nodes {
if !isEmpty(node) {
return false
}
}
return true
case *parse.RangeNode:
case *parse.TemplateNode:
case *parse.TextNode:
return len(bytes.TrimSpace(n.Text)) == 0
case *parse.WithNode:
default:
panic("unknown node: " + n.String())
}
return false
}
template1 template1
{{define "x"}}x{{end}}
{{template "y"}}
template2 template2
{{define "y"}}y{{end}}
{{template "x"}}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment