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/template: add ability to send executed content to a pipeline #54748

Open
bvisness opened this issue Aug 29, 2022 · 7 comments
Open
Labels
Milestone

Comments

@bvisness
Copy link

bvisness commented Aug 29, 2022

I propose adding an apply function to text/template which would execute a template and send its output to a pipeline:

{{ apply markdown | highlight "html" }}
    Hello, **Gopher**!
{{ end }}
<pre class="code">
    Hello, <span class="tag">&lt;strong&gt;</span>Gopher<span class="tag">&lt;/strong&gt;</span>!
</pre>

The motivation for this feature is essentially the same as the motivation for Hugo's shortcodes. When authoring content in Go templates, it is often desirable to run templated content through a template function for post-processing - for example, for syntax highlighting, processing markdown, creating "accordion" sections, etc.

Different flavors of this behavior have been proposed before (see below), and have been rejected with workarounds. Unfortunately, the best workaround available today is to add an eval "template name" . function and then use named templates, which is too cumbersome in practice:

{{ template "base.html" . }}

{{ define "content 1" }}
    Hello, **{{ .Name }}**!

    Watch this _sweet_ video to get started:
{{ end }}
{{ define "content 2" }}
    Don't forget to [like and subscribe](https://video.com/gopher)!
{{ end }}

{{ define "some block from base.html" }}
    {{ eval "content 1" . | markdown }}
    <iframe src="https://video.com/gopher/123456/embed"></iframe>
    {{ eval "content 2" . | markdown }}
{{ end }}

I propose for apply to simply be sugar for this workaround, albeit without defining a named template. With the proposed feature, this example would look like:

{{ template "base.html" . }}

{{ define "some block from base.html" }}
    {{ apply markdown }}
        Hello, **{{ .Name }}**!

        Watch this _sweet_ video to get started:
    {{ end }}
    <iframe src="https://video.com/gopher/123456/embed"></iframe>
    {{ apply markdown }}
        Don't forget to [like and subscribe](https://video.com/gopher)!
    {{ end }}
{{ end }}

I have created a prototype implementation of this feature.

Prior proposals

Open questions

Is this API sufficient? It may be wise to additionally expose APIs like the following, but I will leave that for further discussion.

  • eval "template name" .: evaluates a template by name and returns the output as a string. This behavior is implicitly present in apply, and is part of the commonly-suggested workaround, so it may be worth exposing explicitly. Example:

    {{ define "greet" }}Hello, {{ . }}!{{ end }}
    {{ $greeting := eval "greet" .Name }}
    {{ urlquery $greeting }}
    
    // Output (when executed with Name = "Gopher"):
    Hello%2C+Gopher!
    
@gopherbot gopherbot added this to the Proposal milestone Aug 29, 2022
@seankhliao
Copy link
Member

cc @robpike

@earthboundkid
Copy link
Contributor

I have a draft in my TODOs to propose a similar feature.

Compared to, say, classic Mustache-style (see also ERB, Django, Jinja, etc.) templates, the Go templating system is pretty similar, if superficially different in some ways. But compared to newer React, Vue, etc. style component template systems, Go is missing a key ability: the ability to send children out for further processing. A hypothetical React template might looks like

<div>
  <p>Normal template stuff. Some variable { val }.</p>
  <Button kind="also normal and doable" value={otherVal} /> 
  <ButtonGroup>
    <h2>Whoa, new capability</h2>
    <p>This button group has children!</p>
    <Button kind="etc" value={otherVal} /> 
    <Button kind="etc" value={otherVal} /> 
  </ButtonGroup>
</div>

In the function for ButtonGroup, the h2, p, and Buttons get passed in as the argument children, and the component can decide whether, where, and how to show the children. Vue goes further and lets you have multiple named child groups and pass values from the parent component into the child templates.

You can do the equivalent interpolation in Go with {{ .val }} and the equivalent of Button with {{ template "button" (dict "kind" "normal" "value" .otherVal) }}, but there's no good way to make the equivalent of ButtonGroup. This turns out to be a very powerful abstraction that I would love to see Go have.

@earthboundkid
Copy link
Contributor

Here is another fork of Go templates that add component-like functionality: https://github.com/philippta/go-template

In this case, the repo adds component and slot keywords that can be used in templates like:

{{ define "button-group" }}
  <div class="wrapper">
    {{ slot }}
  </div>
{{ end }}

{{/* elsewhere */}}

{{ component "button-group" . }}
   {{ template "button" .one }}
   {{ template "button" .two }}
   {{ template "button" .three }}
{{ end }}

@bvisness
Copy link
Author

My proposal really isn't about component-like functionality, although you can implement component-like functionality within my proposal.

The whole idea of "components" is too much of an HTML / React / web dev idea to fit within text/template anyway. I believe my proposal is still useful even for unstructured text, and a web-focused system like Hugo could easily implement "components" on top, e.g.

{{ define "button-group" }}
  <div class="wrapper">
    {{ .Children }}
  </div>
{{ end }}

{{/* elsewhere */}}

{{ apply component "button-group" }}
   {{ template "button" .one }}
   {{ template "button" .two }}
   {{ template "button" .three }}
{{ end }}

@earthboundkid
Copy link
Contributor

I think your proposal is a more general solution that gets at the same problem.

Just having the ability to do {{ $x := template "x" . }} would add a lot, because then you could pass $x to markdown etc. You can fake that, like Hugo does, with a render/partial function that feeds templates back into themselves, but it’s worth really thinking through the problem space to get the best solution here.

@bvisness
Copy link
Author

{{ $x := template "x" . }} could be a good way to expose the eval behavior I mentioned, but it doesn't address the core workflow improvement of my proposal, which is evaluating template data without defining it elsewhere. Adding eval is already possible today, and it doesn't work well for my use case.

@earthboundkid
Copy link
Contributor

earthboundkid commented Sep 15, 2022

I agree. I think there are two things that need to be exposed: the ability to treat a rendered template as a value so that it can be piped through e.g. markdown and passed to other templates and the ability to create a block of content in situ without needing an external definition.

I wonder if it would be possible to change the existing block keyword to work this way. Maybe:

{{ block "" . }}
    Hello, **Gopher**!
{{ end | markdown | highlight "html" }}
{{ template "base.html" . }}

{{ define "some block from base.html" }}
    {{ block "" . }}
        Hello, **{{ .Name }}**!

        Watch this _sweet_ video to get started:
    {{ end | markdown }}
    <iframe src="https://video.com/gopher/123456/embed"></iframe>
    {{ block "" . }}
        Don't forget to [like and subscribe](https://video.com/gopher)!
    {{ end | markdown }}
{{ end }}
{{ define "button-group" }}
  <div class="wrapper">
    {{ .Children }}
  </div>
{{ end }}

{{/* elsewhere */}}

{{ $child := block "" . }}
   {{ template "button" .one }}
   {{ template "button" .two }}
   {{ template "button" .three }}
{{ end }}
{{ template "button-group" (dict "Children" $child ) }}

rhansen added a commit to rhansen/docker-gen that referenced this issue Jan 14, 2023
This makes it possible to:
  * modularize a template by using an embedded template (defined with
    Go's built-in `define` action) as a function whose return value is
    the expansion of the template
  * post-process the expansion of a template (e.g., pipe it to
    Sprig's `indent` function)

For additional context, see
<golang/go#54748>.  Sprig is unlikely to add
an equivalent to this function any time soon: the function must be a
closure around the template, meaning either Sprig or Go would have to
change its API.
rhansen added a commit to rhansen/docker-gen that referenced this issue Jan 21, 2023
This makes it possible to:
  * modularize a template by using an embedded template (defined with
    Go's built-in `define` action) as a function whose return value is
    the expansion of the template
  * post-process the expansion of a template (e.g., pipe it to
    Sprig's `indent` function)

For additional context, see
<golang/go#54748>.  Sprig is unlikely to add
an equivalent to this function any time soon: the function must be a
closure around the template, meaning either Sprig or Go would have to
change its API.
rhansen added a commit to nginx-proxy/docker-gen that referenced this issue Jan 24, 2023
This makes it possible to:
  * modularize a template by using an embedded template (defined with
    Go's built-in `define` action) as a function whose return value is
    the expansion of the template
  * post-process the expansion of a template (e.g., pipe it to
    Sprig's `indent` function)

For additional context, see
<golang/go#54748>.  Sprig is unlikely to add
an equivalent to this function any time soon: the function must be a
closure around the template, meaning either Sprig or Go would have to
change its API.
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