Skip to content

text/template: add the 'parent' keyword #48267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/text/template/exampleparent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package template_test

import (
"log"
"os"
"text/template"
)

func ExampleTemplate_parent() {
const base_html = `<!DOCTYPE html>
<html>
<head>
<title>{{block "title" .}}My website{{end}}</title>
</head>
<body>
<main>
{{- block "content" .}}{{end -}}
</main>
<footer>
{{- block "footer" .}}
<p>Thanks for visiting!</p>
{{- end}}
</footer>
</body>
</html>

`
base := template.Must(template.New("base.html").Parse(base_html))

const index_html = `{{define "content"}}<h1>Welcome!</h1>{{end}}`
index := template.Must(template.Must(base.Clone()).New("index.html").Parse(index_html))
{
err := index.ExecuteTemplate(os.Stdout, "base.html", nil)
if err != nil {
log.Println("executing template:", err)
}
}

const about_html = `{{define "title"}}{{template parent .}} - About{{end}}
{{define "content"}}<h1>About us</h1>{{end}}`
about := template.Must(template.Must(base.Clone()).New("about.html").Parse(about_html))
{
err := about.ExecuteTemplate(os.Stdout, "base.html", nil)
if err != nil {
log.Println("executing template:", err)
}
}

// Output:
// <!DOCTYPE html>
// <html>
// <head>
// <title>My website</title>
// </head>
// <body>
// <main><h1>Welcome!</h1></main>
// <footer>
// <p>Thanks for visiting!</p>
// </footer>
// </body>
// </html>
//
// <!DOCTYPE html>
// <html>
// <head>
// <title>My website - About</title>
// </head>
// <body>
// <main><h1>About us</h1></main>
// <footer>
// <p>Thanks for visiting!</p>
// </footer>
// </body>
// </html>

}
27 changes: 20 additions & 7 deletions src/text/template/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ func initMaxExecDepth() int {
type state struct {
tmpl *Template
wr io.Writer
node parse.Node // current node, for errors
vars []variable // push-down stack of variable values.
depth int // the height of the stack of executing templates.
node parse.Node // current node, for errors
vars []variable // push-down stack of variable values.
depth int // the height of the stack of executing templates.
level map[string]int // map from template name to current level.
}

// variable holds the dynamic value of a variable such as $, $x etc.
Expand Down Expand Up @@ -207,9 +208,10 @@ func (t *Template) execute(wr io.Writer, data interface{}) (err error) {
value = reflect.ValueOf(data)
}
state := &state{
tmpl: t,
wr: wr,
vars: []variable{{"$", value}},
tmpl: t,
wr: wr,
vars: []variable{{"$", value}},
level: make(map[string]int),
}
if t.Tree == nil || t.Root == nil {
state.errorf("%q is an incomplete or empty template", t.Name())
Expand All @@ -230,6 +232,7 @@ func (t *Template) DefinedTemplates() string {
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
for name, tmpl := range t.tmpl {
tmpl := tmpl[0] // TODO: invariant check - at least one
if tmpl.Tree == nil || tmpl.Root == nil {
continue
}
Expand Down Expand Up @@ -400,7 +403,12 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {

func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
s.at(t)
tmpl := s.tmpl.Lookup(t.Name)
level := s.level[t.Name]
if t.Parent {
level++
}
s.level[t.Name] = level
tmpl := s.tmpl.LookupByLevel(t.Name, level)
if tmpl == nil {
s.errorf("template %q not defined", t.Name)
}
Expand All @@ -415,6 +423,11 @@ func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
// No dynamic scoping: template invocations inherit no variables.
newState.vars = []variable{{"$", dot}}
newState.walk(dot, tmpl.Root)
level--
if level < 0 {
level = 0 // TODO: this could be masking a bug
}
newState.level[t.Name] = level
}

// Eval functions evaluate pipelines, commands, and their elements and extract
Expand Down
8 changes: 4 additions & 4 deletions src/text/template/multi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ func TestMultiParse(t *testing.T) {
continue
}
for i, name := range test.names {
tmpl, ok := template.tmpl[name]
tmpl, ok := template.tmpl[name] // TODO making this pass tests for now, turn into an accessor method
if !ok {
t.Errorf("%s: can't find template %q", test.name, name)
continue
}
result := tmpl.Root.String()
result := tmpl[0].Root.String() // TODO
if result != test.results[i] {
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.results[i])
}
Expand Down Expand Up @@ -234,10 +234,10 @@ func TestClone(t *testing.T) {
}
// Verify that the clone is self-consistent.
for k, v := range clone.tmpl {
if k == clone.name && v.tmpl[k] != clone {
if k == clone.name && v[0].tmpl[k][0] != clone { // TODO making this pass tests for now
t.Error("clone does not contain root")
}
if v != v.tmpl[v.name] {
if v[0] != v[0].tmpl[v[0].name][0] { // TODO making this pass tests for now
t.Errorf("clone does not contain self for %q", k)
}
}
Expand Down
84 changes: 84 additions & 0 deletions src/text/template/parent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package template_test

import (
"bytes"
"testing"
"text/template"
)

func TestParent(t *testing.T) {
parent, err := template.New("parent").Parse(`{{block "content" .}}parent{{end}}`)
if err != nil {
t.Fatalf("parsing parent template: %v", err)
}

var b bytes.Buffer
if err := parent.Execute(&b, nil); err != nil {
t.Fatalf("executing parent template: %v", err)
}
if b.String() != "parent" {
t.Errorf("want %q, got %q", "parent", b.String())
}

var child *template.Template
{
clone, err := parent.Clone()
if err != nil {
t.Fatalf("cloning parent: %v", err)
}

child, err = clone.Parse(`{{define "content"}}{{template parent}}child{{end}}`)
if err != nil {
t.Fatalf("parsing child template: %v", err)
}

b.Reset()
if err := child.Execute(&b, nil); err != nil {
t.Fatalf("executing child template: %v", err)
}
if b.String() != "parentchild" {
t.Errorf("want %q, got %q", "child", b.String())
}
}

{
clone, err := parent.Clone()
if err != nil {
t.Fatalf("cloning parent: %v", err)
}

child, err := clone.Parse(`{{define "content"}}{{template parent}}cloned child{{end}}`)
if err != nil {
t.Fatalf("parsing child template: %v", err)
}

b.Reset()
if err := child.Execute(&b, nil); err != nil {
t.Fatalf("executing child template: %v", err)
}
if b.String() != "parentcloned child" {
t.Errorf("want %q, got %q", "parentcloned child", b.String())
}
}

{
clone, err := child.Clone()
if err != nil {
t.Fatalf("cloning child: %v", err)
}

gc, err := clone.Parse(`{{define "content"}}{{template parent}}grandchild{{end}}`)
if err != nil {
t.Fatalf("parsing grandchild template: %v", err)
}

b.Reset()
if err := gc.Execute(&b, nil); err != nil {
t.Fatalf("executing grandchild template: %v", err)
}
if b.String() != "parentchildgrandchild" {
t.Errorf("want %q, got %q", "parentchildgrandchild", b.String())
}
}

}
2 changes: 2 additions & 0 deletions src/text/template/parse/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const (
itemEnd // end keyword
itemIf // if keyword
itemNil // the untyped nil constant, easiest to treat as a keyword
itemParent // parent keyword
itemRange // range keyword
itemTemplate // template keyword
itemWith // with keyword
Expand All @@ -82,6 +83,7 @@ var key = map[string]itemType{
"if": itemIf,
"range": itemRange,
"nil": itemNil,
"parent": itemParent,
"template": itemTemplate,
"with": itemWith,
}
Expand Down
1 change: 1 addition & 0 deletions src/text/template/parse/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var itemName = map[itemType]string{
itemIf: "if",
itemEnd: "end",
itemNil: "nil",
itemParent: "parent",
itemRange: "range",
itemTemplate: "template",
itemWith: "with",
Expand Down
21 changes: 13 additions & 8 deletions src/text/template/parse/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,14 +937,15 @@ func (w *WithNode) Copy() Node {
type TemplateNode struct {
NodeType
Pos
tr *Tree
Line int // The line number in the input. Deprecated: Kept for compatibility.
Name string // The name of the template (unquoted).
Pipe *PipeNode // The command to evaluate as dot for the template.
tr *Tree
Line int // The line number in the input. Deprecated: Kept for compatibility.
Name string // The name of the template (unquoted).
Parent bool // Whether to lookup template of this name in parent scope.
Pipe *PipeNode // The command to evaluate as dot for the template.
}

func (t *Tree) newTemplate(pos Pos, line int, name string, pipe *PipeNode) *TemplateNode {
return &TemplateNode{tr: t, NodeType: NodeTemplate, Pos: pos, Line: line, Name: name, Pipe: pipe}
func (t *Tree) newTemplate(pos Pos, line int, name string, parent bool, pipe *PipeNode) *TemplateNode {
return &TemplateNode{tr: t, NodeType: NodeTemplate, Pos: pos, Line: line, Name: name, Parent: parent, Pipe: pipe}
}

func (t *TemplateNode) String() string {
Expand All @@ -955,7 +956,11 @@ func (t *TemplateNode) String() string {

func (t *TemplateNode) writeTo(sb *strings.Builder) {
sb.WriteString("{{template ")
sb.WriteString(strconv.Quote(t.Name))
if t.Parent {
sb.WriteString("parent")
} else {
sb.WriteString(strconv.Quote(t.Name))
}
if t.Pipe != nil {
sb.WriteByte(' ')
t.Pipe.writeTo(sb)
Expand All @@ -968,5 +973,5 @@ func (t *TemplateNode) tree() *Tree {
}

func (t *TemplateNode) Copy() Node {
return t.tr.newTemplate(t.Pos, t.Line, t.Name, t.Pipe.CopyPipe())
return t.tr.newTemplate(t.Pos, t.Line, t.Name, t.Parent, t.Pipe.CopyPipe())
}
24 changes: 21 additions & 3 deletions src/text/template/parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ func (t *Tree) blockControl() Node {
block.add()
block.stopParse()

return t.newTemplate(token.pos, token.line, name, pipe)
return t.newTemplate(token.pos, token.line, name, false, pipe)
}

// Template:
Expand All @@ -590,14 +590,32 @@ func (t *Tree) blockControl() Node {
func (t *Tree) templateControl() Node {
const context = "template clause"
token := t.nextNonSpace()
name := t.parseTemplateName(token, context)
var (
name string
parent bool
)
if token.typ == itemParent {
// If we encounter the `parent` keyword, then we assume the template we're
// in the midst currently defining is overriding a previously defined
// template, and that the template author's intent is to execute that
// "parent" template as part of this "child" template.
//
// We know the name of the template currently being defined (i.e., the one
// overriding a previous definition), so we set a flag to indicate to
// template execution that it should lookup the "parent" template with the
// same name.
name = t.Name
parent = true
} else {
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, itemRightDelim)
}
return t.newTemplate(token.pos, token.line, name, pipe)
return t.newTemplate(token.pos, token.line, name, parent, pipe)
}

func (t *Tree) parseTemplateName(token item, context string) (name string) {
Expand Down
2 changes: 2 additions & 0 deletions src/text/template/parse/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ var parseTests = []parseTest{
`{{template "x"}}`},
{"template with arg", "{{template `x` .Y}}", noError,
`{{template "x" .Y}}`},
{"template with parent", "{{template parent .}}", noError,
`{{template parent .}}`},
{"with", "{{with .X}}hello{{end}}", noError,
`{{with .X}}"hello"{{end}}`},
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
Expand Down
Loading