Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a python template library that can do "partial renderings"?

Lets say I have the following template:

<!DOCTYPE html>    
<html>
    {{ var1 }}
    {% if var1 and var2 %}
        <span>some text</span>
    {% endif %}
    {{ var2 }}
</html>

When I render it with just var1=3 as the context, it produces this output:

<!DOCTYPE html>    
<html>
    3
    {% if 3 and var2 %}
        <span>some text</span>
    {% endif %}
    {{ var2 }}
</html>

The when I render the output of the first rendering again with var2=5 as the context, the output is this:

<!DOCTYPE html>    
<html>
    3
        <span>some text</span>
    5
</html>

The problem is that most template engines will evaluate variables missing from the context as an empty string. The are all built under the assumption that there will only ever be one render.

I know jinja2 can kind of do this with this method: Multiple renders of jinja2 templates?

But it doesn't work across the if statement, it only works for single variables.

Does any of the popular template libraries have a rendering mode like how I describe? Mako? genshi? Something else? Maybe a non-python template engine that does this?

like image 362
priestc Avatar asked Dec 27 '12 19:12

priestc


2 Answers

TL;DR

This feature doesn't exist in any templating language I know of. Your best options are:

  1. Hack around using this SO question
  2. Write your own template tag (see below)

Writing your own template tag

I think you have a couple options here, some of which I got from here, though none of them involve built-in abilities of templating systems. Basically, the goal here would be to have the render output something that includes template tags to again be rendered.

As I see it, the easiest thing to do would be to use the templatetag template tag or to write your own that does something similar and takes care of escaping.

If you were reliably rendering var1 first and var2 second:

<!DOCTYPE html>    
<html>
    {{ var1 }}
    {% templatetag openblock %} if {{ var1 }} and var2 {% templatetag closeblock %}
        <span>some text</span>
    {% templatetag openblock %} endif {% templatetag closeblock %}
    {% templatetag openvariable %} var2 {% templatetag closevariable %}
</html>

Upon the first rendering, you'd get:

<!DOCTYPE html>    
<html>
    {{ 3 }}
    {% if 3 and var2 %}
        <span>some text</span>
    {% endif %}
    {{ var2 }}
</html>

Which, upon second rendering, would result in your desired output.

Obviously, constantly writing multiple layers of {% templatetag %} would be a huge pain, so what I'd suggest is writing your own recursive template tag that would take care of this for you, probably with an argument that specifies how many layers of nesting you'd need, and then obviously an argument for the input itself. The nice thing about this is if you only needed one layer of nesting, having the template tag simply output the input would work,

Basically, by having this custom template tag output itself recursively, you could achieve as many layers of nesting as you need pretty easily. Assuming the tag were implemented as {% t <layers of nesting> <input> %}:

    Initial template:  {% t 2 "{{ var2 }}" %}
    First rendering:   {% t 1 "{{ var2 }}" %}
    Second rendering:  {{ var2 }}
    Final rendering:   5

Now, this would be more difficult for some of the more complex tags, like {% if %}, for sure, especially if like in your example you have multiple layers of rendering required within a single if statement. You might be best off here splitting your if statements so you can have clearer rendering separation. The following example assumes an implementation of the {% t %} tag where it's a {% t %}/{% endt %} combination:

Initial HTML:

{% if var1 %}
   {% t 1 %}
       {% if var2 %}
          <span>some text</span>
       {% endif %}
   {% endt %}
{% endif %}

First rendering:

{# Note that the first if statement has been evaluated and is gone #}
{% if var2 %}
    <span>some text</span>
{% endif %}

Final rendering:

<span>some text</span>
like image 156
jdotjdot Avatar answered Oct 20 '22 07:10

jdotjdot


Resurrecting an old question here - I recently wanted to do this and found the original answer to be overly complicated. This is a more simple version of the same answer. TLDR; use an extension.

Making an extension to do this is fairly trivial. Here's a version of an extension I use to partially render a template. I use custom tags preserve and endpreserve to delimit a block I don't want to be rendered in the first-pass.

The rendered result of this template is just used to create another template which can have the non-rendered variables & such rendered during a second pass.

Usage.

text = ...
pass_1 = Template(text, extensions=[PreserveExtension])
result_1 = pass_1.render(...)
pass_2 = Template(pass_1)
final_render = pass_2.render(...)

Here is the extension.

class PreserveExtension(Extension):

  """
  Extensions ignores jinja templates between tags.
  NOTE: Preserved template spacing is slightly modified.

  Example Input:

    this is plain text
    {%- preserve %}
    this is plain text
    {{ a }}
    {%- if b %}{{ b }}{% endif -%}
    {% for i in c -%}
      {{ i }}
    {%- endfor %}
    {%- endpreserve %}

  Example Output:

    this is plain text
    this is plain text
    {{ a }}{%- if b %} {{ b }} {% endif -%}
    {% for i in c -%}
      {{ i }}{%- endfor %}
  """

  tags = {"preserve"}

  def parse(self, parser: Parser):
    lineno = parser.stream.current.lineno

    parser.parse_expression()
    parser.stream.skip()

    body = []
    raw = []

    def flush():
      nonlocal raw
      nonlocal body
      node = nodes.TemplateData(''.join(raw))
      body.append(node)
      raw = []

    while True:
      t: Token = next(parser.stream)
      if t.lineno != lineno:
        flush()
        lineno = t.lineno
      test = t.test('name:endpreserve')
      if test:
        raw.pop(-1)
        break
      if raw and not raw[-1].endswith('\n'):
        raw.append(' ')
      raw.append(t.value)
    if raw:
      flush()
    return body
like image 1
Aage Torleif Avatar answered Oct 20 '22 07:10

Aage Torleif