Skip to content

Commit

Permalink
text/template, html/template: add block keyword and permit template r…
Browse files Browse the repository at this point in the history
…edefinition

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 golang#3812

Change-Id: I733cb5332c1c201c235f759cc64333462e70dc27
Reviewed-on: https://go-review.googlesource.com/14005
Reviewed-by: Rob Pike <[email protected]>
  • Loading branch information
adg committed Sep 28, 2015
1 parent 09c6d13 commit 12dfc3b
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 72 deletions.
12 changes: 10 additions & 2 deletions src/html/template/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,17 @@ func TestClone(t *testing.T) {
Must(t0.Parse(`{{define "lhs"}} ( {{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())
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`)
}

Expand Down
36 changes: 36 additions & 0 deletions src/html/template/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"html/template"
"log"
"os"
"strings"
)

func Example() {
Expand Down Expand Up @@ -120,3 +121,38 @@ func Example_escape() {
// %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
}
4 changes: 3 additions & 1 deletion src/html/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// Template is a specialized Template from "text/template" that produces a safe
// HTML document fragment.
type Template struct {
// Sticky error if escaping fails.
// Sticky error if escaping fails, or escapeOK if succeeded.
escapeErr error
// 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
Expand Down Expand Up @@ -170,6 +170,8 @@ func (t *Template) Parse(src string) (*Template, error) {
tmpl := t.set[name]
if tmpl == nil {
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.
tmpl.escapeErr = nil
Expand Down
8 changes: 8 additions & 0 deletions src/text/template/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ data, defined in detail below.
The template with the specified name is executed with dot set
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}}
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
Expand Down
36 changes: 36 additions & 0 deletions src/text/template/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package template_test
import (
"log"
"os"
"strings"
"text/template"
)

Expand Down Expand Up @@ -72,3 +73,38 @@ Josie
// Best wishes,
// 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
}
33 changes: 33 additions & 0 deletions src/text/template/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1232,3 +1232,36 @@ func testBadFuncName(name string, t *testing.T) {
// reports an error.
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)
}
}
22 changes: 4 additions & 18 deletions src/text/template/multi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ package template
import (
"bytes"
"fmt"
"strings"
"testing"
"text/template/parse"
)
Expand Down Expand Up @@ -277,17 +276,11 @@ func TestRedefinition(t *testing.T) {
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
t.Fatalf("parse 1: %v", err)
}
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err == nil {
t.Fatal("expected error")
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err != nil {
t.Fatal("got error %v, expected nil", err)
}
if !strings.Contains(err.Error(), "redefinition") {
t.Fatalf("expected redefinition error; got %v", 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)
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err != nil {
t.Fatal("got error %v, expected nil", err)
}
}

Expand Down Expand Up @@ -345,7 +338,6 @@ func TestNew(t *testing.T) {
func TestParse(t *testing.T) {
// In multiple calls to Parse with the same receiver template, only one call
// can contain text other than space, comments, and template definitions
var err error
t1 := New("test")
if _, err := t1.Parse(`{{define "test"}}{{end}}`); err != nil {
t.Fatalf("parsing test: %s", err)
Expand All @@ -356,10 +348,4 @@ func TestParse(t *testing.T) {
if _, err := t1.Parse(`{{define "test"}}foo{{end}}`); err != nil {
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)
}
}
2 changes: 2 additions & 0 deletions src/text/template/parse/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const (
itemVariable // variable starting with '$', such as '$' or '$1' or '$hello'
// Keywords appear after all the rest.
itemKeyword // used only to delimit the keywords
itemBlock // block keyword
itemDot // the cursor, spelled '.'
itemDefine // define keyword
itemElse // else keyword
Expand All @@ -71,6 +72,7 @@ const (

var key = map[string]itemType{
".": itemDot,
"block": itemBlock,
"define": itemDefine,
"else": itemElse,
"end": itemEnd,
Expand Down
16 changes: 11 additions & 5 deletions src/text/template/parse/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var itemName = map[itemType]string{

// keywords
itemDot: ".",
itemBlock: "block",
itemDefine: "define",
itemElse: "else",
itemIf: "if",
Expand All @@ -58,6 +59,8 @@ type lexTest struct {
}

var (
tDot = item{itemDot, 0, "."}
tBlock = item{itemBlock, 0, "block"}
tEOF = item{itemEOF, 0, ""}
tFor = item{itemIdentifier, 0, "for"}
tLeft = item{itemLeftDelim, 0, "{{"}
Expand Down Expand Up @@ -104,6 +107,9 @@ var lexTests = []lexTest{
}},
{"empty action", `{{}}`, []item{tLeft, 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}},
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
{"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
Expand Down Expand Up @@ -155,7 +161,7 @@ var lexTests = []lexTest{
}},
{"dot", "{{.}}", []item{
tLeft,
{itemDot, 0, "."},
tDot,
tRight,
tEOF,
}},
Expand All @@ -169,7 +175,7 @@ var lexTests = []lexTest{
tLeft,
{itemField, 0, ".x"},
tSpace,
{itemDot, 0, "."},
tDot,
tSpace,
{itemNumber, 0, ".2"},
tSpace,
Expand Down Expand Up @@ -501,9 +507,9 @@ func TestShutdown(t *testing.T) {
func (t *Tree) parseLexer(lex *lexer, text string) (tree *Tree, err error) {
defer t.recover(&err)
t.ParseName = t.Name
t.startParse(nil, lex)
t.parse(nil)
t.add(nil)
t.startParse(nil, lex, map[string]*Tree{})
t.parse()
t.add()
t.stopParse()
return t, nil
}
Loading

0 comments on commit 12dfc3b

Please sign in to comment.