Skip to content

text/template, html/template: add ExecuteFuncs #54450

@zeripath

Description

@zeripath

This proposal is about providing a way to pass a template.FuncMap at template execution overriding specific functions within the template.

When executing a template using one of new methods, then the provided template.FuncMap will be checked first.

Summary

Reasons:

  • Passing values to templates can only be achieved using the data any parameter and this is suboptimal for contextual rendering elements such as web nonces like csrf, or translation functions.
  • This is especially problematic when using sub-templates called from within template rendering. The template calling syntax only allows setting a single variable as its data, making passing this contextual rendering functions difficult.
  • The current suggestion is to use Clone(), Funcs() and then Execute() but this is slow and optimizing this appears very difficult. (text/template: optimize Clone+Funcs+Execute sequence #38114) It would require significant changes throughout the code and even if parent templates could be changed to allow inheritance, It would requires cloning every template that could be called in advance.

Advantages:

Detailed rationale

Sometimes data needs to be rendered differently based on the context it is rendered in.

The two most common examples anti-CSRF tokens and locales.

Although currently these can be added to the data struct provided when rendering - these are not strictly data and it significantly complicates rendering especially when considering subtemplating.

Proposed API

I propose that the following methods are added to text/template.Template and to html/template.Template

text/template:

func (t *Template) ExecuteFuncMap(wr io.Writer, data any, funcs FuncMap) error { ... }

func (t *Template) ExecuteTemplateFuncMap(wr io.Writer, name string, data any, funcs FuncMap) error { ... }

html/template:

func (t *Template) ExecuteFuncMap(wr io.Writer, data any, funcs FuncMap) error { ... }

func (t *Template) ExecuteTemplateFuncMap(wr io.Writer, name string, data interface{}, funcs FuncMap) error {

Calling these methods would execute the template as per Execute and ExecuteTemplate with the only difference being that when a template calls a function the provided funcs template.FuncMap would be checked first. In this way funcs can be considered a map of function overrides for template execution and acts similarly to the calling the Funcs(...) method on the template but would also override functions in templates called by the current executing template.

Examples

Current Status

Let us assume we have a function with the following signature:

func (l *Locale) Tr(key string, args ...any) string { ... }

In this example this function takes a key and provides the translated value for it.

Clearly, this cannot be compiled into shared templates as the locale will be different on a per request basis.

So assuming we don't want to have multiple compiled templates per locale we will need to pass this in as data. Let's say as .Tr.

This leads to:

...
{{.Tr "key" args}}
...

But... what about when ranging over a set of items? Now, . will refer to an item in the range of items so we need use $...

{{range .Items}}
   ...
   {{$.Tr "key" .}}
   ...
{{end}}

If we forget we will get a runtime error. So we won't know that it's a problem until it's run.

But... what if we want to reuse that range block as a subtemplate or even just extract it out because it is becoming unwieldy? Ideally we'd want to do:

{{range .Items}}
   {{template "render-item" .}}
{{end}}

That of course doesn't work because we lose access to $. So now we need to do something horrible like:

{{range .Items}}
   {{template "render-item" dict "Item" . "Tr" $.Tr}} {{/* or "root" $ */}}
{{end}}

and then in our subtemplate refer to .Item and .Tr (or .root.Tr). This means that we need to now know that we're in a subtemplate and every time we call the subtemplate we need to do a similar hack.

Again if we forget to add the correct prefix we'll get a runtime error but only at time of render.

Why isn't template.Clone() template.Funcs() template.Execute() sufficient?

The problem here is multifactorial, Clone() isn't very lightweight and requires locks, Funcs(...) also requires locks and every template would need to be cloned and its funcs updated in case when you execute the template it calls another template.

The referenced issue has been around for over 2 years and it is likely that improving the efficiency of this route significantly is not possible without significant refactors.

Instead, the proposed API makes for a clear and simple implementation with a minimal changeset.

Proposed

The proposed API allows for much simpler templating. The Tr function would be passed in within an override FuncMap e.g.:

err := tmpl.ExecuteFuncMap(wr, data, template.FuncMap(map[string]any{"Tr": locale.Tr})

Then the downstream templates look like:

{{Tr "key" args...}}
{{range .Items}}
    {{Tr "key" .}}
    {{template "item-template" .}}
{{end}}

And the Tr function is available everywhere without having to determine whether it needs a ., a $ or even a $.root. (Or even if it's not been passed in to this subtemplate.)

Compatibility

The proposed functionality is completely backwards compatible.

Implementation

The proposal is easy to implement and has been implemented as #54432.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Accepted

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions