Skip to content
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

proposal: text/templates: custom block preprocessor #71196

Open
panyam opened this issue Jan 9, 2025 · 5 comments
Open

proposal: text/templates: custom block preprocessor #71196

panyam opened this issue Jan 9, 2025 · 5 comments
Labels
Milestone

Comments

@panyam
Copy link

panyam commented Jan 9, 2025

Proposal Details

I wanted to share a feature request that I have been seeking for a long time. Would love to get feedback and see the appetite for this. The templating package is simple and yet powerful by design. Today the actions are hard coded in the parser (text/template/parse/parse.go). Templating languages like Jinja offer custom "nodes" - extensions for user defined action types instead of the fixed ones defined by the template parser. This would allow users to build custom functionality at parse time itself.

Example Usage:

// text/template/parse: 
// ActionParser take a parse tree and transforms/rewrites it.
type ActionParser interface {
    // TODO - Needs further if parent/root template is passed for sharing context etc is the right way
    HandleTree(t *template.Template, root *parse.Tree) *parse.Tree
}

type ActionMap map[string]ActionParser

// text.template to also have an ActionMap
func main() {
    actionMap := template.ActionMap{
       "myaction": func(root *parse.Tree) *parse.Tree {
           ... process/rewrite/transform body of custom action
       }
    }

    contents := `{{ myaction pipeline }} .... template body {{ end }}`
    t := template.Must(template.New("helloworld").Actions(actionMap).Parse(contents)
}
Parse(contents))

Goals:

  1. Must be optin
  2. Must not break existing templates
  3. Must not break existing syntax
  4. Functions to still be Prioritized over actions (??)
  5. Priority of Function vs Action
  6. Minimize exposure of parser interface - currently the parser is pretty closed and we may not want to open up top-down parser completely. eg, Should we add a new "custom node" type in the Tree?
  7. [Added] Secure: As called out in proposal: text/template: make "template" action accept dynamic name as argument #33449 html/template performs escaping of text/template before rendering. This must not be circumventable or incompatible.

In this proposal when a custom action is encountered the child is parsed and passed to the action instead of the action driving its own parser - this is in line with (2). Doing so will incur an inefficiency. However this is optin and parsed templates can be cached making this a one off.

To address (7). This proposal is only for a parser. By making the parser only act as a preprocessor and rewriting the parse tree, the standard exec phase can apply. For example a "switch" action could rewrite the body in terms of a "if/else-if" actions.

Use cases

"switch" action
{{ switch pipeline }}
    {{ case cond1 }}
    {{ end }}
    {{ case cond2 }}
    {{ end }}
    {{ case cond3 }}
    {{ end }}
    {{ default }} 
    {{ end }}
{{ end }}
"import" action

Instead of loading all templates, we could only provide a root template that loads its dependencies providing better isolation:


{{ import HomeHeader }}
{{ import HomeFooter }}

{{ define HomePage }}
   {{ template HomeHeader }} 
  .... Home Page template
   {{ template HomeFooter }} 
{{ end }}
@panyam panyam added the Proposal label Jan 9, 2025
@gopherbot gopherbot added this to the Proposal milestone Jan 9, 2025
@gabyhelp
Copy link

gabyhelp commented Jan 9, 2025

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

@seankhliao
Copy link
Member

cc @robpike

allowing the creation of a new DSLs within text/template feels out of scope? as in it becomes a different language entirely.

@panyam
Copy link
Author

panyam commented Jan 9, 2025

@seankhliao - +1 We would not want to create a new DSL and the current syntax/structure would be preserved. I was thinking of this in the same philosophy to the "block" action which is equivalent to a "define" followed by a "template". Is that fair?

@seankhliao
Copy link
Member

block and define/template allow for reuse and override of content, but it doesn't change the way you interact with the templating language.

this proposal (a preprocessor) can fundamentally change what the language looks like, switch/case and import both represent shifts in control flow, enough for it to be considered a DSL (just because everything is wrapped in {{}} doesn't mean it's the same syntax).

I think #54748 is loosely related, though it operates on already rendered template blocks.

@seankhliao seankhliao changed the title proposal: text/templates: Provide a way to pass custom actions to the template parser proposal: text/templates: custom block preprocessor Jan 9, 2025
@panyam
Copy link
Author

panyam commented Jan 9, 2025

@seankhliao - I appreciate the concerns about maintaining Go's simple template design. You raise valid points about DSLs - we definitely want to avoid fragmenting the ecosystem or creating a "template within a template" situation.

One thing I hadnt added here (due to this being a starting point) is to keep transformations predictable and inline with the Go templating philosophy. For example we could add validators to ensure that only standard nodes are produced (Though today this can still be hand-coded explicitly).

Couple of other advantages I wanted to share:

  • Issues with compatibility? This proposal is completely opt-in so the default behavior is not being changed and not even visible until activated. Only template (library) authors may want to provide custom extensions on top of the standard behavior.
  • Why not just use functions - The benefit of this over functions is that complex operations could be done with a cleaner syntax to suit customer domains (eg templates that favor constructs for config files vs templates that favor constructs for web pages and so on).
  • Can this lead to eco system fragementation? This has existed in several other engines (like Jinja) and community driven extensions have been very useful. The Go community having already established patterns for sharing can be a great place to vet/bless extensions. And again due to the opt-in nature users have choice to pick exactly what is allowed.

@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Jan 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Incoming
Development

No branches or pull requests

4 participants