Skip to content

Commit 12dfc3b

Browse files
committed
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 <[email protected]>
1 parent 09c6d13 commit 12dfc3b

File tree

12 files changed

+239
-72
lines changed

12 files changed

+239
-72
lines changed

src/html/template/clone_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,17 @@ func TestClone(t *testing.T) {
7878
Must(t0.Parse(`{{define "lhs"}} ( {{end}}`))
7979
Must(t0.Parse(`{{define "rhs"}} ) {{end}}`))
8080

81-
// Clone t0 as t4. Redefining the "lhs" template should fail.
81+
// Clone t0 as t4. Redefining the "lhs" template should not fail.
8282
t4 := Must(t0.Clone())
83-
if _, err := t4.Parse(`{{define "lhs"}} FAIL {{end}}`); err == nil {
83+
if _, err := t4.Parse(`{{define "lhs"}} OK {{end}}`); err != nil {
84+
t.Error(`redefine "lhs": got err %v want non-nil`, err)
85+
}
86+
// Cloning t1 should fail as it has been executed.
87+
if _, err := t1.Clone(); err == nil {
88+
t.Error("cloning t1: got nil err want non-nil")
89+
}
90+
// Redefining the "lhs" template in t1 should fail as it has been executed.
91+
if _, err := t1.Parse(`{{define "lhs"}} OK {{end}}`); err == nil {
8492
t.Error(`redefine "lhs": got nil err want non-nil`)
8593
}
8694

src/html/template/example_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"html/template"
1010
"log"
1111
"os"
12+
"strings"
1213
)
1314

1415
func Example() {
@@ -120,3 +121,38 @@ func Example_escape() {
120121
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
121122

122123
}
124+
125+
// The following example is duplicated in text/template; keep them in sync.
126+
127+
func ExampleBlock() {
128+
const (
129+
master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
130+
overlay = `{{define "list"}} {{join . ", "}}{{end}} `
131+
)
132+
var (
133+
funcs = template.FuncMap{"join": strings.Join}
134+
guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
135+
)
136+
masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
137+
if err != nil {
138+
log.Fatal(err)
139+
}
140+
overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
141+
if err != nil {
142+
log.Fatal(err)
143+
}
144+
if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
145+
log.Fatal(err)
146+
}
147+
if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
148+
log.Fatal(err)
149+
}
150+
// Output:
151+
// Names:
152+
// - Gamora
153+
// - Groot
154+
// - Nebula
155+
// - Rocket
156+
// - Star-Lord
157+
// Names: Gamora, Groot, Nebula, Rocket, Star-Lord
158+
}

src/html/template/template.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
// Template is a specialized Template from "text/template" that produces a safe
1919
// HTML document fragment.
2020
type Template struct {
21-
// Sticky error if escaping fails.
21+
// Sticky error if escaping fails, or escapeOK if succeeded.
2222
escapeErr error
2323
// We could embed the text/template field, but it's safer not to because
2424
// 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) {
170170
tmpl := t.set[name]
171171
if tmpl == nil {
172172
tmpl = t.new(name)
173+
} else if tmpl.escapeErr != nil {
174+
return nil, fmt.Errorf("html/template: cannot redefine %q after it has executed", name)
173175
}
174176
// Restore our record of this text/template to its unescaped original state.
175177
tmpl.escapeErr = nil

src/text/template/doc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ data, defined in detail below.
115115
The template with the specified name is executed with dot set
116116
to the value of the pipeline.
117117
118+
{{block "name" pipeline}} T1 {{end}}
119+
A block is shorthand for defining a template
120+
{{define "name"}} T1 {{end}}
121+
and then executing it in place
122+
{{template "name" .}}
123+
The typical use is to define a set of root templates that are
124+
then customized by redefining the block templates within.
125+
118126
{{with pipeline}} T1 {{end}}
119127
If the value of the pipeline is empty, no output is generated;
120128
otherwise, dot is set to the value of the pipeline and T1 is

src/text/template/example_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package template_test
77
import (
88
"log"
99
"os"
10+
"strings"
1011
"text/template"
1112
)
1213

@@ -72,3 +73,38 @@ Josie
7273
// Best wishes,
7374
// Josie
7475
}
76+
77+
// The following example is duplicated in html/template; keep them in sync.
78+
79+
func ExampleBlock() {
80+
const (
81+
master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
82+
overlay = `{{define "list"}} {{join . ", "}}{{end}} `
83+
)
84+
var (
85+
funcs = template.FuncMap{"join": strings.Join}
86+
guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
87+
)
88+
masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
89+
if err != nil {
90+
log.Fatal(err)
91+
}
92+
overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
93+
if err != nil {
94+
log.Fatal(err)
95+
}
96+
if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
97+
log.Fatal(err)
98+
}
99+
if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
100+
log.Fatal(err)
101+
}
102+
// Output:
103+
// Names:
104+
// - Gamora
105+
// - Groot
106+
// - Nebula
107+
// - Rocket
108+
// - Star-Lord
109+
// Names: Gamora, Groot, Nebula, Rocket, Star-Lord
110+
}

src/text/template/exec_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,3 +1232,36 @@ func testBadFuncName(name string, t *testing.T) {
12321232
// reports an error.
12331233
t.Errorf("%q succeeded incorrectly as function name", name)
12341234
}
1235+
1236+
func TestBlock(t *testing.T) {
1237+
const (
1238+
input = `a({{block "inner" .}}bar({{.}})baz{{end}})b`
1239+
want = `a(bar(hello)baz)b`
1240+
overlay = `{{define "inner"}}foo({{.}})bar{{end}}`
1241+
want2 = `a(foo(goodbye)bar)b`
1242+
)
1243+
tmpl, err := New("outer").Parse(input)
1244+
if err != nil {
1245+
t.Fatal(err)
1246+
}
1247+
tmpl2, err := Must(tmpl.Clone()).Parse(overlay)
1248+
if err != nil {
1249+
t.Fatal(err)
1250+
}
1251+
1252+
var buf bytes.Buffer
1253+
if err := tmpl.Execute(&buf, "hello"); err != nil {
1254+
t.Fatal(err)
1255+
}
1256+
if got := buf.String(); got != want {
1257+
t.Errorf("got %q, want %q", got, want)
1258+
}
1259+
1260+
buf.Reset()
1261+
if err := tmpl2.Execute(&buf, "goodbye"); err != nil {
1262+
t.Fatal(err)
1263+
}
1264+
if got := buf.String(); got != want2 {
1265+
t.Errorf("got %q, want %q", got, want2)
1266+
}
1267+
}

src/text/template/multi_test.go

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package template
99
import (
1010
"bytes"
1111
"fmt"
12-
"strings"
1312
"testing"
1413
"text/template/parse"
1514
)
@@ -277,17 +276,11 @@ func TestRedefinition(t *testing.T) {
277276
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
278277
t.Fatalf("parse 1: %v", err)
279278
}
280-
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err == nil {
281-
t.Fatal("expected error")
279+
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err != nil {
280+
t.Fatal("got error %v, expected nil", err)
282281
}
283-
if !strings.Contains(err.Error(), "redefinition") {
284-
t.Fatalf("expected redefinition error; got %v", err)
285-
}
286-
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err == nil {
287-
t.Fatal("expected error")
288-
}
289-
if !strings.Contains(err.Error(), "redefinition") {
290-
t.Fatalf("expected redefinition error; got %v", err)
282+
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err != nil {
283+
t.Fatal("got error %v, expected nil", err)
291284
}
292285
}
293286

@@ -345,7 +338,6 @@ func TestNew(t *testing.T) {
345338
func TestParse(t *testing.T) {
346339
// In multiple calls to Parse with the same receiver template, only one call
347340
// can contain text other than space, comments, and template definitions
348-
var err error
349341
t1 := New("test")
350342
if _, err := t1.Parse(`{{define "test"}}{{end}}`); err != nil {
351343
t.Fatalf("parsing test: %s", err)
@@ -356,10 +348,4 @@ func TestParse(t *testing.T) {
356348
if _, err := t1.Parse(`{{define "test"}}foo{{end}}`); err != nil {
357349
t.Fatalf("parsing test: %s", err)
358350
}
359-
if _, err = t1.Parse(`{{define "test"}}foo{{end}}`); err == nil {
360-
t.Fatal("no error from redefining a template")
361-
}
362-
if !strings.Contains(err.Error(), "redefinition") {
363-
t.Fatalf("expected redefinition error; got %v", err)
364-
}
365351
}

src/text/template/parse/lex.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const (
5858
itemVariable // variable starting with '$', such as '$' or '$1' or '$hello'
5959
// Keywords appear after all the rest.
6060
itemKeyword // used only to delimit the keywords
61+
itemBlock // block keyword
6162
itemDot // the cursor, spelled '.'
6263
itemDefine // define keyword
6364
itemElse // else keyword
@@ -71,6 +72,7 @@ const (
7172

7273
var key = map[string]itemType{
7374
".": itemDot,
75+
"block": itemBlock,
7476
"define": itemDefine,
7577
"else": itemElse,
7678
"end": itemEnd,

src/text/template/parse/lex_test.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var itemName = map[itemType]string{
3333

3434
// keywords
3535
itemDot: ".",
36+
itemBlock: "block",
3637
itemDefine: "define",
3738
itemElse: "else",
3839
itemIf: "if",
@@ -58,6 +59,8 @@ type lexTest struct {
5859
}
5960

6061
var (
62+
tDot = item{itemDot, 0, "."}
63+
tBlock = item{itemBlock, 0, "block"}
6164
tEOF = item{itemEOF, 0, ""}
6265
tFor = item{itemIdentifier, 0, "for"}
6366
tLeft = item{itemLeftDelim, 0, "{{"}
@@ -104,6 +107,9 @@ var lexTests = []lexTest{
104107
}},
105108
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
106109
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
110+
{"block", `{{block "foo" .}}`, []item{
111+
tLeft, tBlock, tSpace, {itemString, 0, `"foo"`}, tSpace, tDot, tRight, tEOF,
112+
}},
107113
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
108114
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
109115
{"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
@@ -155,7 +161,7 @@ var lexTests = []lexTest{
155161
}},
156162
{"dot", "{{.}}", []item{
157163
tLeft,
158-
{itemDot, 0, "."},
164+
tDot,
159165
tRight,
160166
tEOF,
161167
}},
@@ -169,7 +175,7 @@ var lexTests = []lexTest{
169175
tLeft,
170176
{itemField, 0, ".x"},
171177
tSpace,
172-
{itemDot, 0, "."},
178+
tDot,
173179
tSpace,
174180
{itemNumber, 0, ".2"},
175181
tSpace,
@@ -501,9 +507,9 @@ func TestShutdown(t *testing.T) {
501507
func (t *Tree) parseLexer(lex *lexer, text string) (tree *Tree, err error) {
502508
defer t.recover(&err)
503509
t.ParseName = t.Name
504-
t.startParse(nil, lex)
505-
t.parse(nil)
506-
t.add(nil)
510+
t.startParse(nil, lex, map[string]*Tree{})
511+
t.parse()
512+
t.add()
507513
t.stopParse()
508514
return t, nil
509515
}

0 commit comments

Comments
 (0)