mirror of
https://github.com/golang/go
synced 2024-11-05 14:56:10 -07:00
text/template, html/template: add block keyword and permit template redefinition
This change adds a new "block" keyword that permits the definition of templates inline inside existing templates, and loosens the restriction on template redefinition. Templates may now be redefined, but in the html/template package they may only be redefined before the template is executed (and therefore escaped). The intention is that such inline templates can be redefined by subsequent template definitions, permitting a kind of template "inheritance" or "overlay". (See the example for details.) Fixes #3812 Change-Id: I733cb5332c1c201c235f759cc64333462e70dc27 Reviewed-on: https://go-review.googlesource.com/14005 Reviewed-by: Rob Pike <r@golang.org>
This commit is contained in:
parent
09c6d13ac2
commit
12dfc3bee4
@ -78,9 +78,17 @@ func TestClone(t *testing.T) {
|
|||||||
Must(t0.Parse(`{{define "lhs"}} ( {{end}}`))
|
Must(t0.Parse(`{{define "lhs"}} ( {{end}}`))
|
||||||
Must(t0.Parse(`{{define "rhs"}} ) {{end}}`))
|
Must(t0.Parse(`{{define "rhs"}} ) {{end}}`))
|
||||||
|
|
||||||
// Clone t0 as t4. Redefining the "lhs" template should fail.
|
// Clone t0 as t4. Redefining the "lhs" template should not fail.
|
||||||
t4 := Must(t0.Clone())
|
t4 := Must(t0.Clone())
|
||||||
if _, err := t4.Parse(`{{define "lhs"}} FAIL {{end}}`); err == nil {
|
if _, err := t4.Parse(`{{define "lhs"}} OK {{end}}`); err != nil {
|
||||||
|
t.Error(`redefine "lhs": got err %v want non-nil`, err)
|
||||||
|
}
|
||||||
|
// Cloning t1 should fail as it has been executed.
|
||||||
|
if _, err := t1.Clone(); err == nil {
|
||||||
|
t.Error("cloning t1: got nil err want non-nil")
|
||||||
|
}
|
||||||
|
// Redefining the "lhs" template in t1 should fail as it has been executed.
|
||||||
|
if _, err := t1.Parse(`{{define "lhs"}} OK {{end}}`); err == nil {
|
||||||
t.Error(`redefine "lhs": got nil err want non-nil`)
|
t.Error(`redefine "lhs": got nil err want non-nil`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Example() {
|
func Example() {
|
||||||
@ -120,3 +121,38 @@ func Example_escape() {
|
|||||||
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
|
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The following example is duplicated in text/template; keep them in sync.
|
||||||
|
|
||||||
|
func ExampleBlock() {
|
||||||
|
const (
|
||||||
|
master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
|
||||||
|
overlay = `{{define "list"}} {{join . ", "}}{{end}} `
|
||||||
|
)
|
||||||
|
var (
|
||||||
|
funcs = template.FuncMap{"join": strings.Join}
|
||||||
|
guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
|
||||||
|
)
|
||||||
|
masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
// Names:
|
||||||
|
// - Gamora
|
||||||
|
// - Groot
|
||||||
|
// - Nebula
|
||||||
|
// - Rocket
|
||||||
|
// - Star-Lord
|
||||||
|
// Names: Gamora, Groot, Nebula, Rocket, Star-Lord
|
||||||
|
}
|
||||||
|
@ -18,7 +18,7 @@ import (
|
|||||||
// Template is a specialized Template from "text/template" that produces a safe
|
// Template is a specialized Template from "text/template" that produces a safe
|
||||||
// HTML document fragment.
|
// HTML document fragment.
|
||||||
type Template struct {
|
type Template struct {
|
||||||
// Sticky error if escaping fails.
|
// Sticky error if escaping fails, or escapeOK if succeeded.
|
||||||
escapeErr error
|
escapeErr error
|
||||||
// We could embed the text/template field, but it's safer not to because
|
// We could embed the text/template field, but it's safer not to because
|
||||||
// we need to keep our version of the name space and the underlying
|
// we need to keep our version of the name space and the underlying
|
||||||
@ -170,6 +170,8 @@ func (t *Template) Parse(src string) (*Template, error) {
|
|||||||
tmpl := t.set[name]
|
tmpl := t.set[name]
|
||||||
if tmpl == nil {
|
if tmpl == nil {
|
||||||
tmpl = t.new(name)
|
tmpl = t.new(name)
|
||||||
|
} else if tmpl.escapeErr != nil {
|
||||||
|
return nil, fmt.Errorf("html/template: cannot redefine %q after it has executed", name)
|
||||||
}
|
}
|
||||||
// Restore our record of this text/template to its unescaped original state.
|
// Restore our record of this text/template to its unescaped original state.
|
||||||
tmpl.escapeErr = nil
|
tmpl.escapeErr = nil
|
||||||
|
@ -115,6 +115,14 @@ data, defined in detail below.
|
|||||||
The template with the specified name is executed with dot set
|
The template with the specified name is executed with dot set
|
||||||
to the value of the pipeline.
|
to the value of the pipeline.
|
||||||
|
|
||||||
|
{{block "name" pipeline}} T1 {{end}}
|
||||||
|
A block is shorthand for defining a template
|
||||||
|
{{define "name"}} T1 {{end}}
|
||||||
|
and then executing it in place
|
||||||
|
{{template "name" .}}
|
||||||
|
The typical use is to define a set of root templates that are
|
||||||
|
then customized by redefining the block templates within.
|
||||||
|
|
||||||
{{with pipeline}} T1 {{end}}
|
{{with pipeline}} T1 {{end}}
|
||||||
If the value of the pipeline is empty, no output is generated;
|
If the value of the pipeline is empty, no output is generated;
|
||||||
otherwise, dot is set to the value of the pipeline and T1 is
|
otherwise, dot is set to the value of the pipeline and T1 is
|
||||||
|
@ -7,6 +7,7 @@ package template_test
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -72,3 +73,38 @@ Josie
|
|||||||
// Best wishes,
|
// Best wishes,
|
||||||
// Josie
|
// Josie
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The following example is duplicated in html/template; keep them in sync.
|
||||||
|
|
||||||
|
func ExampleBlock() {
|
||||||
|
const (
|
||||||
|
master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
|
||||||
|
overlay = `{{define "list"}} {{join . ", "}}{{end}} `
|
||||||
|
)
|
||||||
|
var (
|
||||||
|
funcs = template.FuncMap{"join": strings.Join}
|
||||||
|
guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
|
||||||
|
)
|
||||||
|
masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
// Names:
|
||||||
|
// - Gamora
|
||||||
|
// - Groot
|
||||||
|
// - Nebula
|
||||||
|
// - Rocket
|
||||||
|
// - Star-Lord
|
||||||
|
// Names: Gamora, Groot, Nebula, Rocket, Star-Lord
|
||||||
|
}
|
||||||
|
@ -1232,3 +1232,36 @@ func testBadFuncName(name string, t *testing.T) {
|
|||||||
// reports an error.
|
// reports an error.
|
||||||
t.Errorf("%q succeeded incorrectly as function name", name)
|
t.Errorf("%q succeeded incorrectly as function name", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBlock(t *testing.T) {
|
||||||
|
const (
|
||||||
|
input = `a({{block "inner" .}}bar({{.}})baz{{end}})b`
|
||||||
|
want = `a(bar(hello)baz)b`
|
||||||
|
overlay = `{{define "inner"}}foo({{.}})bar{{end}}`
|
||||||
|
want2 = `a(foo(goodbye)bar)b`
|
||||||
|
)
|
||||||
|
tmpl, err := New("outer").Parse(input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tmpl2, err := Must(tmpl.Clone()).Parse(overlay)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buf, "hello"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := buf.String(); got != want {
|
||||||
|
t.Errorf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
if err := tmpl2.Execute(&buf, "goodbye"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := buf.String(); got != want2 {
|
||||||
|
t.Errorf("got %q, want %q", got, want2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,7 +9,6 @@ package template
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"text/template/parse"
|
"text/template/parse"
|
||||||
)
|
)
|
||||||
@ -277,17 +276,11 @@ func TestRedefinition(t *testing.T) {
|
|||||||
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||||
t.Fatalf("parse 1: %v", err)
|
t.Fatalf("parse 1: %v", err)
|
||||||
}
|
}
|
||||||
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err == nil {
|
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err != nil {
|
||||||
t.Fatal("expected error")
|
t.Fatal("got error %v, expected nil", err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "redefinition") {
|
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err != nil {
|
||||||
t.Fatalf("expected redefinition error; got %v", err)
|
t.Fatal("got error %v, expected nil", err)
|
||||||
}
|
|
||||||
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "redefinition") {
|
|
||||||
t.Fatalf("expected redefinition error; got %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,7 +338,6 @@ func TestNew(t *testing.T) {
|
|||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
// In multiple calls to Parse with the same receiver template, only one call
|
// In multiple calls to Parse with the same receiver template, only one call
|
||||||
// can contain text other than space, comments, and template definitions
|
// can contain text other than space, comments, and template definitions
|
||||||
var err error
|
|
||||||
t1 := New("test")
|
t1 := New("test")
|
||||||
if _, err := t1.Parse(`{{define "test"}}{{end}}`); err != nil {
|
if _, err := t1.Parse(`{{define "test"}}{{end}}`); err != nil {
|
||||||
t.Fatalf("parsing test: %s", err)
|
t.Fatalf("parsing test: %s", err)
|
||||||
@ -356,10 +348,4 @@ func TestParse(t *testing.T) {
|
|||||||
if _, err := t1.Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
if _, err := t1.Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||||
t.Fatalf("parsing test: %s", err)
|
t.Fatalf("parsing test: %s", err)
|
||||||
}
|
}
|
||||||
if _, err = t1.Parse(`{{define "test"}}foo{{end}}`); err == nil {
|
|
||||||
t.Fatal("no error from redefining a template")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "redefinition") {
|
|
||||||
t.Fatalf("expected redefinition error; got %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ const (
|
|||||||
itemVariable // variable starting with '$', such as '$' or '$1' or '$hello'
|
itemVariable // variable starting with '$', such as '$' or '$1' or '$hello'
|
||||||
// Keywords appear after all the rest.
|
// Keywords appear after all the rest.
|
||||||
itemKeyword // used only to delimit the keywords
|
itemKeyword // used only to delimit the keywords
|
||||||
|
itemBlock // block keyword
|
||||||
itemDot // the cursor, spelled '.'
|
itemDot // the cursor, spelled '.'
|
||||||
itemDefine // define keyword
|
itemDefine // define keyword
|
||||||
itemElse // else keyword
|
itemElse // else keyword
|
||||||
@ -71,6 +72,7 @@ const (
|
|||||||
|
|
||||||
var key = map[string]itemType{
|
var key = map[string]itemType{
|
||||||
".": itemDot,
|
".": itemDot,
|
||||||
|
"block": itemBlock,
|
||||||
"define": itemDefine,
|
"define": itemDefine,
|
||||||
"else": itemElse,
|
"else": itemElse,
|
||||||
"end": itemEnd,
|
"end": itemEnd,
|
||||||
|
@ -33,6 +33,7 @@ var itemName = map[itemType]string{
|
|||||||
|
|
||||||
// keywords
|
// keywords
|
||||||
itemDot: ".",
|
itemDot: ".",
|
||||||
|
itemBlock: "block",
|
||||||
itemDefine: "define",
|
itemDefine: "define",
|
||||||
itemElse: "else",
|
itemElse: "else",
|
||||||
itemIf: "if",
|
itemIf: "if",
|
||||||
@ -58,6 +59,8 @@ type lexTest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
tDot = item{itemDot, 0, "."}
|
||||||
|
tBlock = item{itemBlock, 0, "block"}
|
||||||
tEOF = item{itemEOF, 0, ""}
|
tEOF = item{itemEOF, 0, ""}
|
||||||
tFor = item{itemIdentifier, 0, "for"}
|
tFor = item{itemIdentifier, 0, "for"}
|
||||||
tLeft = item{itemLeftDelim, 0, "{{"}
|
tLeft = item{itemLeftDelim, 0, "{{"}
|
||||||
@ -104,6 +107,9 @@ var lexTests = []lexTest{
|
|||||||
}},
|
}},
|
||||||
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
|
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
|
||||||
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
|
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
|
||||||
|
{"block", `{{block "foo" .}}`, []item{
|
||||||
|
tLeft, tBlock, tSpace, {itemString, 0, `"foo"`}, tSpace, tDot, tRight, tEOF,
|
||||||
|
}},
|
||||||
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
|
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
|
||||||
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
|
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
|
||||||
{"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
|
{"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
|
||||||
@ -155,7 +161,7 @@ var lexTests = []lexTest{
|
|||||||
}},
|
}},
|
||||||
{"dot", "{{.}}", []item{
|
{"dot", "{{.}}", []item{
|
||||||
tLeft,
|
tLeft,
|
||||||
{itemDot, 0, "."},
|
tDot,
|
||||||
tRight,
|
tRight,
|
||||||
tEOF,
|
tEOF,
|
||||||
}},
|
}},
|
||||||
@ -169,7 +175,7 @@ var lexTests = []lexTest{
|
|||||||
tLeft,
|
tLeft,
|
||||||
{itemField, 0, ".x"},
|
{itemField, 0, ".x"},
|
||||||
tSpace,
|
tSpace,
|
||||||
{itemDot, 0, "."},
|
tDot,
|
||||||
tSpace,
|
tSpace,
|
||||||
{itemNumber, 0, ".2"},
|
{itemNumber, 0, ".2"},
|
||||||
tSpace,
|
tSpace,
|
||||||
@ -501,9 +507,9 @@ func TestShutdown(t *testing.T) {
|
|||||||
func (t *Tree) parseLexer(lex *lexer, text string) (tree *Tree, err error) {
|
func (t *Tree) parseLexer(lex *lexer, text string) (tree *Tree, err error) {
|
||||||
defer t.recover(&err)
|
defer t.recover(&err)
|
||||||
t.ParseName = t.Name
|
t.ParseName = t.Name
|
||||||
t.startParse(nil, lex)
|
t.startParse(nil, lex, map[string]*Tree{})
|
||||||
t.parse(nil)
|
t.parse()
|
||||||
t.add(nil)
|
t.add()
|
||||||
t.stopParse()
|
t.stopParse()
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ type Tree struct {
|
|||||||
token [3]item // three-token lookahead for parser.
|
token [3]item // three-token lookahead for parser.
|
||||||
peekCount int
|
peekCount int
|
||||||
vars []string // variables defined at the moment.
|
vars []string // variables defined at the moment.
|
||||||
|
treeSet map[string]*Tree
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy returns a copy of the Tree. Any parsing state is discarded.
|
// Copy returns a copy of the Tree. Any parsing state is discarded.
|
||||||
@ -205,11 +206,12 @@ func (t *Tree) recover(errp *error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startParse initializes the parser, using the lexer.
|
// startParse initializes the parser, using the lexer.
|
||||||
func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer) {
|
func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer, treeSet map[string]*Tree) {
|
||||||
t.Root = nil
|
t.Root = nil
|
||||||
t.lex = lex
|
t.lex = lex
|
||||||
t.vars = []string{"$"}
|
t.vars = []string{"$"}
|
||||||
t.funcs = funcs
|
t.funcs = funcs
|
||||||
|
t.treeSet = treeSet
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopParse terminates parsing.
|
// stopParse terminates parsing.
|
||||||
@ -217,6 +219,7 @@ func (t *Tree) stopParse() {
|
|||||||
t.lex = nil
|
t.lex = nil
|
||||||
t.vars = nil
|
t.vars = nil
|
||||||
t.funcs = nil
|
t.funcs = nil
|
||||||
|
t.treeSet = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse parses the template definition string to construct a representation of
|
// Parse parses the template definition string to construct a representation of
|
||||||
@ -226,19 +229,19 @@ func (t *Tree) stopParse() {
|
|||||||
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
|
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
|
||||||
defer t.recover(&err)
|
defer t.recover(&err)
|
||||||
t.ParseName = t.Name
|
t.ParseName = t.Name
|
||||||
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim))
|
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim), treeSet)
|
||||||
t.text = text
|
t.text = text
|
||||||
t.parse(treeSet)
|
t.parse()
|
||||||
t.add(treeSet)
|
t.add()
|
||||||
t.stopParse()
|
t.stopParse()
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// add adds tree to the treeSet.
|
// add adds tree to t.treeSet.
|
||||||
func (t *Tree) add(treeSet map[string]*Tree) {
|
func (t *Tree) add() {
|
||||||
tree := treeSet[t.Name]
|
tree := t.treeSet[t.Name]
|
||||||
if tree == nil || IsEmptyTree(tree.Root) {
|
if tree == nil || IsEmptyTree(tree.Root) {
|
||||||
treeSet[t.Name] = t
|
t.treeSet[t.Name] = t
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !IsEmptyTree(t.Root) {
|
if !IsEmptyTree(t.Root) {
|
||||||
@ -274,7 +277,7 @@ func IsEmptyTree(n Node) bool {
|
|||||||
// 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.
|
||||||
func (t *Tree) parse(treeSet map[string]*Tree) (next Node) {
|
func (t *Tree) parse() (next Node) {
|
||||||
t.Root = t.newList(t.peek().pos)
|
t.Root = t.newList(t.peek().pos)
|
||||||
for t.peek().typ != itemEOF {
|
for t.peek().typ != itemEOF {
|
||||||
if t.peek().typ == itemLeftDelim {
|
if t.peek().typ == itemLeftDelim {
|
||||||
@ -283,8 +286,8 @@ func (t *Tree) parse(treeSet map[string]*Tree) (next Node) {
|
|||||||
newT := New("definition") // name will be updated once we know it.
|
newT := New("definition") // name will be updated once we know it.
|
||||||
newT.text = t.text
|
newT.text = t.text
|
||||||
newT.ParseName = t.ParseName
|
newT.ParseName = t.ParseName
|
||||||
newT.startParse(t.funcs, t.lex)
|
newT.startParse(t.funcs, t.lex, t.treeSet)
|
||||||
newT.parseDefinition(treeSet)
|
newT.parseDefinition()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
t.backup2(delim)
|
t.backup2(delim)
|
||||||
@ -300,9 +303,9 @@ func (t *Tree) parse(treeSet map[string]*Tree) (next Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseDefinition parses a {{define}} ... {{end}} template definition and
|
// parseDefinition parses a {{define}} ... {{end}} template definition and
|
||||||
// installs the definition in the treeSet map. The "define" keyword has already
|
// installs the definition in t.treeSet. The "define" keyword has already
|
||||||
// been scanned.
|
// been scanned.
|
||||||
func (t *Tree) parseDefinition(treeSet map[string]*Tree) {
|
func (t *Tree) parseDefinition() {
|
||||||
const context = "define clause"
|
const context = "define clause"
|
||||||
name := t.expectOneOf(itemString, itemRawString, context)
|
name := t.expectOneOf(itemString, itemRawString, context)
|
||||||
var err error
|
var err error
|
||||||
@ -316,7 +319,7 @@ func (t *Tree) parseDefinition(treeSet map[string]*Tree) {
|
|||||||
if end.Type() != nodeEnd {
|
if end.Type() != nodeEnd {
|
||||||
t.errorf("unexpected %s in %s", end, context)
|
t.errorf("unexpected %s in %s", end, context)
|
||||||
}
|
}
|
||||||
t.add(treeSet)
|
t.add()
|
||||||
t.stopParse()
|
t.stopParse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,6 +361,8 @@ func (t *Tree) textOrAction() Node {
|
|||||||
// First word could be a keyword such as range.
|
// First word could be a keyword such as range.
|
||||||
func (t *Tree) action() (n Node) {
|
func (t *Tree) action() (n Node) {
|
||||||
switch token := t.nextNonSpace(); token.typ {
|
switch token := t.nextNonSpace(); token.typ {
|
||||||
|
case itemBlock:
|
||||||
|
return t.blockControl()
|
||||||
case itemElse:
|
case itemElse:
|
||||||
return t.elseControl()
|
return t.elseControl()
|
||||||
case itemEnd:
|
case itemEnd:
|
||||||
@ -522,13 +527,51 @@ func (t *Tree) elseControl() Node {
|
|||||||
return t.newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber())
|
return t.newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block:
|
||||||
|
// {{block stringValue pipeline}}
|
||||||
|
// Block keyword is past.
|
||||||
|
// The name must be something that can evaluate to a string.
|
||||||
|
// The pipeline is mandatory.
|
||||||
|
func (t *Tree) blockControl() Node {
|
||||||
|
const context = "block clause"
|
||||||
|
|
||||||
|
token := t.nextNonSpace()
|
||||||
|
name := t.parseTemplateName(token, context)
|
||||||
|
pipe := t.pipeline(context)
|
||||||
|
|
||||||
|
block := New(name) // name will be updated once we know it.
|
||||||
|
block.text = t.text
|
||||||
|
block.ParseName = t.ParseName
|
||||||
|
block.startParse(t.funcs, t.lex, t.treeSet)
|
||||||
|
var end Node
|
||||||
|
block.Root, end = block.itemList()
|
||||||
|
if end.Type() != nodeEnd {
|
||||||
|
t.errorf("unexpected %s in %s", end, context)
|
||||||
|
}
|
||||||
|
block.add()
|
||||||
|
block.stopParse()
|
||||||
|
|
||||||
|
return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe)
|
||||||
|
}
|
||||||
|
|
||||||
// Template:
|
// Template:
|
||||||
// {{template stringValue pipeline}}
|
// {{template stringValue pipeline}}
|
||||||
// Template keyword is past. The name must be something that can evaluate
|
// Template keyword is past. The name must be something that can evaluate
|
||||||
// to a string.
|
// to a string.
|
||||||
func (t *Tree) templateControl() Node {
|
func (t *Tree) templateControl() Node {
|
||||||
var name string
|
const context = "template clause"
|
||||||
token := t.nextNonSpace()
|
token := t.nextNonSpace()
|
||||||
|
name := t.parseTemplateName(token, context)
|
||||||
|
var pipe *PipeNode
|
||||||
|
if t.nextNonSpace().typ != itemRightDelim {
|
||||||
|
t.backup()
|
||||||
|
// Do not pop variables; they persist until "end".
|
||||||
|
pipe = t.pipeline(context)
|
||||||
|
}
|
||||||
|
return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) parseTemplateName(token item, context string) (name string) {
|
||||||
switch token.typ {
|
switch token.typ {
|
||||||
case itemString, itemRawString:
|
case itemString, itemRawString:
|
||||||
s, err := strconv.Unquote(token.val)
|
s, err := strconv.Unquote(token.val)
|
||||||
@ -537,15 +580,9 @@ func (t *Tree) templateControl() Node {
|
|||||||
}
|
}
|
||||||
name = s
|
name = s
|
||||||
default:
|
default:
|
||||||
t.unexpected(token, "template invocation")
|
t.unexpected(token, context)
|
||||||
}
|
}
|
||||||
var pipe *PipeNode
|
return
|
||||||
if t.nextNonSpace().typ != itemRightDelim {
|
|
||||||
t.backup()
|
|
||||||
// Do not pop variables; they persist until "end".
|
|
||||||
pipe = t.pipeline("template")
|
|
||||||
}
|
|
||||||
return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// command:
|
// command:
|
||||||
|
@ -235,6 +235,8 @@ var parseTests = []parseTest{
|
|||||||
{"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
|
{"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
|
||||||
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
|
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
|
||||||
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
|
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
|
||||||
|
{"block definition", `{{block "foo" .}}hello{{end}}`, noError,
|
||||||
|
`{{template "foo" .}}`},
|
||||||
// Errors.
|
// Errors.
|
||||||
{"unclosed action", "hello{{range", hasError, ""},
|
{"unclosed action", "hello{{range", hasError, ""},
|
||||||
{"unmatched end", "{{end}}", hasError, ""},
|
{"unmatched end", "{{end}}", hasError, ""},
|
||||||
@ -284,6 +286,8 @@ var parseTests = []parseTest{
|
|||||||
{"wrong pipeline boolean", "{{.|true}}", hasError, ""},
|
{"wrong pipeline boolean", "{{.|true}}", hasError, ""},
|
||||||
{"wrong pipeline nil", "{{'c'|nil}}", hasError, ""},
|
{"wrong pipeline nil", "{{'c'|nil}}", hasError, ""},
|
||||||
{"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""},
|
{"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""},
|
||||||
|
// Missing pipeline in block
|
||||||
|
{"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
var builtins = map[string]interface{}{
|
var builtins = map[string]interface{}{
|
||||||
@ -457,3 +461,26 @@ func TestErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBlock(t *testing.T) {
|
||||||
|
const (
|
||||||
|
input = `a{{block "inner" .}}bar{{.}}baz{{end}}b`
|
||||||
|
outer = `a{{template "inner" .}}b`
|
||||||
|
inner = `bar{{.}}baz`
|
||||||
|
)
|
||||||
|
treeSet := make(map[string]*Tree)
|
||||||
|
tmpl, err := New("outer").Parse(input, "", "", treeSet, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if g, w := tmpl.Root.String(), outer; g != w {
|
||||||
|
t.Errorf("outer template = %q, want %q", g, w)
|
||||||
|
}
|
||||||
|
inTmpl := treeSet["inner"]
|
||||||
|
if inTmpl == nil {
|
||||||
|
t.Fatal("block did not define template")
|
||||||
|
}
|
||||||
|
if g, w := inTmpl.Root.String(), inner; g != w {
|
||||||
|
t.Errorf("inner template = %q, want %q", g, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
package template
|
package template
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"text/template/parse"
|
"text/template/parse"
|
||||||
@ -117,11 +116,10 @@ func (t *Template) copy(c *common) *Template {
|
|||||||
|
|
||||||
// AddParseTree adds parse tree for template with given name and associates it with t.
|
// AddParseTree adds parse tree for template with given name and associates it with t.
|
||||||
// If the template does not already exist, it will create a new one.
|
// If the template does not already exist, it will create a new one.
|
||||||
// It is an error to reuse a name except to overwrite an empty template.
|
// If the template does exist, it will be replaced.
|
||||||
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
|
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
|
||||||
t.init()
|
t.init()
|
||||||
// If the name is the name of this template, overwrite this template.
|
// If the name is the name of this template, overwrite this template.
|
||||||
// The associate method checks it's not a redefinition.
|
|
||||||
nt := t
|
nt := t
|
||||||
if name != t.name {
|
if name != t.name {
|
||||||
nt = t.New(name)
|
nt = t.New(name)
|
||||||
@ -185,11 +183,7 @@ func (t *Template) Lookup(name string) *Template {
|
|||||||
|
|
||||||
// Parse defines the template by parsing the text. Nested template definitions will be
|
// Parse defines the template by parsing the text. Nested template definitions will be
|
||||||
// associated with the top-level template t. Parse may be called multiple times
|
// 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
|
// to parse definitions of templates to associate with t.
|
||||||
// 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) {
|
func (t *Template) Parse(text string) (*Template, error) {
|
||||||
t.init()
|
t.init()
|
||||||
t.muFuncs.RLock()
|
t.muFuncs.RLock()
|
||||||
@ -208,25 +202,17 @@ func (t *Template) Parse(text string) (*Template, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// associate installs the new template into the group of templates associated
|
// 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
|
// with t. The two are already known to share the common structure.
|
||||||
// template. The two are already known to share the common structure.
|
// The boolean return value reports whether to store this tree as t.Tree.
|
||||||
// The boolean return value reports wither to store this tree as t.Tree.
|
|
||||||
func (t *Template) associate(new *Template, tree *parse.Tree) (bool, error) {
|
func (t *Template) associate(new *Template, tree *parse.Tree) (bool, error) {
|
||||||
if new.common != t.common {
|
if new.common != t.common {
|
||||||
panic("internal error: associate not common")
|
panic("internal error: associate not common")
|
||||||
}
|
}
|
||||||
name := new.name
|
if t.tmpl[new.name] != nil && parse.IsEmptyTree(tree.Root) {
|
||||||
if old := t.tmpl[name]; old != nil {
|
// If a template by that name exists,
|
||||||
oldIsEmpty := parse.IsEmptyTree(old.Root)
|
// don't replace it with an empty template.
|
||||||
newIsEmpty := parse.IsEmptyTree(tree.Root)
|
return false, nil
|
||||||
if newIsEmpty {
|
|
||||||
// Whether old is empty or not, new is empty; no reason to replace old.
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if !oldIsEmpty {
|
|
||||||
return false, fmt.Errorf("template: redefinition of template %q", name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
t.tmpl[name] = new
|
t.tmpl[new.name] = new
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user