diff --git a/src/text/template/exampleparent_test.go b/src/text/template/exampleparent_test.go new file mode 100644 index 00000000000000..89b2491183377f --- /dev/null +++ b/src/text/template/exampleparent_test.go @@ -0,0 +1,76 @@ +package template_test + +import ( + "log" + "os" + "text/template" +) + +func ExampleTemplate_parent() { + const base_html = ` + + + {{block "title" .}}My website{{end}} + + +
+ {{- block "content" .}}{{end -}} +
+ + + + +` + base := template.Must(template.New("base.html").Parse(base_html)) + + const index_html = `{{define "content"}}

Welcome!

{{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"}}

About us

{{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: + // + // + // + // My website + // + // + //

Welcome!

+ // + // + // + // + // + // + // + // My website - About + // + // + //

About us

+ // + // + // + +} diff --git a/src/text/template/exec.go b/src/text/template/exec.go index 5ad3b4ec582c41..40cb8e4669f1d2 100644 --- a/src/text/template/exec.go +++ b/src/text/template/exec.go @@ -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. @@ -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()) @@ -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 } @@ -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) } @@ -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 diff --git a/src/text/template/multi_test.go b/src/text/template/multi_test.go index b543ab5c47c6d3..f4d49cc0730589 100644 --- a/src/text/template/multi_test.go +++ b/src/text/template/multi_test.go @@ -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]) } @@ -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) } } diff --git a/src/text/template/parent_test.go b/src/text/template/parent_test.go new file mode 100644 index 00000000000000..cbfb6c2b6f2b9a --- /dev/null +++ b/src/text/template/parent_test.go @@ -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()) + } + } + +} diff --git a/src/text/template/parse/lex.go b/src/text/template/parse/lex.go index 6784071b1118d1..4a289ad1694b4e 100644 --- a/src/text/template/parse/lex.go +++ b/src/text/template/parse/lex.go @@ -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 @@ -82,6 +83,7 @@ var key = map[string]itemType{ "if": itemIf, "range": itemRange, "nil": itemNil, + "parent": itemParent, "template": itemTemplate, "with": itemWith, } diff --git a/src/text/template/parse/lex_test.go b/src/text/template/parse/lex_test.go index 6510eed674dd9f..ae0e517ebf7bd0 100644 --- a/src/text/template/parse/lex_test.go +++ b/src/text/template/parse/lex_test.go @@ -40,6 +40,7 @@ var itemName = map[itemType]string{ itemIf: "if", itemEnd: "end", itemNil: "nil", + itemParent: "parent", itemRange: "range", itemTemplate: "template", itemWith: "with", diff --git a/src/text/template/parse/node.go b/src/text/template/parse/node.go index 177482f9b26059..558a3a7ac2a9da 100644 --- a/src/text/template/parse/node.go +++ b/src/text/template/parse/node.go @@ -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 { @@ -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) @@ -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()) } diff --git a/src/text/template/parse/parse.go b/src/text/template/parse/parse.go index 1a63961c13ecce..19a1e09b61fd06 100644 --- a/src/text/template/parse/parse.go +++ b/src/text/template/parse/parse.go @@ -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: @@ -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) { diff --git a/src/text/template/parse/parse_test.go b/src/text/template/parse/parse_test.go index 9b1be272e573d9..2aaf9e4b58c317 100644 --- a/src/text/template/parse/parse_test.go +++ b/src/text/template/parse/parse_test.go @@ -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, diff --git a/src/text/template/template.go b/src/text/template/template.go index fd74d45e9b1c6d..59d49fb531c2bd 100644 --- a/src/text/template/template.go +++ b/src/text/template/template.go @@ -12,8 +12,11 @@ import ( // common holds the information shared by related templates. type common struct { - tmpl map[string]*Template // Map from name to defined templates. - muTmpl sync.RWMutex // protects tmpl + // Map from name to defined templates. There is a slice of templates, + // because there are generations of previously defined templates with the + // same name. + tmpl map[string][]*Template + muTmpl sync.RWMutex // protects tmpl option option // We use two maps, one for parsing and one for execution. // This separation makes the API cleaner since it doesn't @@ -70,7 +73,7 @@ func (t *Template) New(name string) *Template { func (t *Template) init() { if t.common == nil { c := new(common) - c.tmpl = make(map[string]*Template) + c.tmpl = make(map[string][]*Template) c.parseFuncs = make(FuncMap) c.execFuncs = make(map[string]reflect.Value) t.common = c @@ -91,14 +94,16 @@ func (t *Template) Clone() (*Template, error) { } t.muTmpl.RLock() defer t.muTmpl.RUnlock() - for k, v := range t.tmpl { + for k, generations := range t.tmpl { if k == t.name { - nt.tmpl[t.name] = nt + nt.tmpl[t.name] = append(nt.tmpl[t.name], nt) continue } // The associated templates share nt's common structure. - tmpl := v.copy(nt.common) - nt.tmpl[k] = tmpl + for i := range generations { + tmpl := generations[i].copy(nt.common) + nt.tmpl[k] = append(nt.tmpl[k], tmpl) + } } t.muFuncs.RLock() defer t.muFuncs.RUnlock() @@ -150,8 +155,8 @@ func (t *Template) Templates() []*Template { t.muTmpl.RLock() defer t.muTmpl.RUnlock() m := make([]*Template, 0, len(t.tmpl)) - for _, v := range t.tmpl { - m = append(m, v) + for _, generations := range t.tmpl { + m = append(m, generations[0]) } return m } @@ -191,7 +196,23 @@ func (t *Template) Lookup(name string) *Template { } t.muTmpl.RLock() defer t.muTmpl.RUnlock() - return t.tmpl[name] + return t.lookup(name, 0) +} + +func (t *Template) LookupByLevel(name string, level int) *Template { + if t.common == nil { + return nil + } + t.muTmpl.RLock() + defer t.muTmpl.RUnlock() + return t.lookup(name, level) +} + +func (t *Template) lookup(name string, level int) *Template { + if level < 0 || level >= len(t.tmpl[name]) { + return nil + } + return t.tmpl[name][level] } // Parse parses text as a template body for t. @@ -228,11 +249,22 @@ func (t *Template) associate(new *Template, tree *parse.Tree) bool { if new.common != t.common { panic("internal error: associate not common") } - if old := t.tmpl[new.name]; old != nil && parse.IsEmptyTree(tree.Root) && old.Tree != nil { + if old := t.lookup(new.name, 0); old != nil && parse.IsEmptyTree(tree.Root) && old.Tree != nil { // If a template by that name exists, // don't replace it with an empty template. return false } - t.tmpl[new.name] = new + + // associating a template with the name of an existing one retains a + // reference to the original, pushing the new one to the front of a slice + // of previously defined templates, so that previously defined templates by + // that name can be accessed by indexing into the slice, each successive + // level being an older generation. + generations := t.tmpl[new.name] + generations = append(generations, nil) + copy(generations[1:], generations[0:]) + generations[0] = new + t.tmpl[new.name] = generations + return true }