Insert javascript at top of including file in Jinja 2

Brian M. Hunt picture Brian M. Hunt · Nov 27, 2010 · Viewed 15.1k times · Source

In Jinja2, I would like the following to work as it looks like it should, by running:

from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('.'))
template = env.get_template('x.html')
print template.render()

Essentially the objective is to coalesce all the javascript into the <head> tags by using a a {% call js() %} /* some js */ {% endcall %} macro.


x.html

<html>
<head>
  <script type="text/javascript>
  {% block head_js %}{% endblock %}
  </script>
  </head>
<body>
  {% include "y.html" %}
</body>
</html>

y.html

{% macro js() -%}
    // extend head_js
    {%- block head_js -%}
    {{ super() }}
    try { {{ caller() }} } catch (e) {
       my.log.error(e.name + ": " + e.message);
    }
    {%- endblock -%}
{%- endmacro %}

Some ... <div id="abc">text</div> ...

{% call js() %}
    // jquery parlance:
    $(function () {
        $("#abc").css("color", "red");
    });
{% endcall %}

Expected result

When I run X.html through jinja2, I would expect the result to be:

<html>
<head>
  <script type="text/javascript>
  try { {{ $("#abc").css("color", "red"); }} } catch (e) {
       usf.log.error(e.name + ": " + e.message);
    }
  </script>
  </head>
<body>
      Some ... <div id="abc">text</div> ...
</body>
</html>

Actual result

The actual results are not encouraging. I get a couple types of potentially illuminating errors, e.g.:

TypeError: macro 'js' takes no keyword argument 'caller'

or, when I try adding another basis macro such as

{% macro js2() -%}
{%- block head_js -%}
//     ... something
{%- endblock -%}
{%- endmacro %}

I get the following exception

jinja2.exceptions.TemplateAssertionError: block 'head_js' defined twice

I feel as though I am running into a design issue regarding the precedence of the block tags over the macro tags (i.e. macros do not seem to encapsulate block tags in the way I expect).


I suppose my questions are quite simple:

  1. Can Jinja2 do what I am attempting? If so, how?

  2. If not, is there another Python based templating engine that does support this sort of pattern (e.g. mako, genshi, etc.), which would work without issue in Google App Engine

Thank you for reading - I appreciate your input.

Brian


Edit:

I'm trying to write an extension to resolve this problem. I'm halfway there -- using the following code:

from jinja2 import nodes, Environment, FileSystemLoader
from jinja2.ext import Extension

class JavascriptBuilderExtension(Extension):
    tags = set(['js', 'js_content'])

    def __init__(self, environment):
        super(JavascriptBuilderExtension, self).__init__(environment)
        environment.extend(
            javascript_builder_content = [],
        )

    def parse(self, parser):
        """Parse tokens """
        tag = parser.stream.next()
        return getattr(self, "_%s" % str(tag))(parser, tag)

    def _js_content(self, parser, tag):
        """ Return the output """
        content_list = self.environment.javascript_builder_content
        node = nodes.Output(lineno=tag.lineno)
        node.nodes = []

        for o in content_list:
            print "\nAppending node: %s" % str(o)
            node.nodes.extend(o[0].nodes)
        print "Returning node: %s \n" % node
        return node

    def _js(self, parser, tag):
        body = parser.parse_statements(['name:endjs'], drop_needle=True)
        print "Adding: %s" % str(body)
        self.environment.javascript_builder_content.append(body)
        return nodes.Const('<!-- Slurped Javascript -->')

env = Environment(
    loader      = FileSystemLoader('.'),
    extensions  = [JavascriptBuilderExtension],
    )

This makes it simple to add Javascript to the end of a template ... e.g.

<html>
<head></head>
<body>
    {% js %}
    some javascript {{ 3 + 5 }}
    {% endjs %}
    {% js %}
    more {{ 2 }}
    {% endjs %}

<script type="text/javascript">
{% js_content %}
</script>
</body>
</html>

Running env.get_template('x.html').render() will result in some illuminating comments and the expected output of:

<html>
<head>
  <script type="text/javascript>
  </script>
  </head>
<body>
    <!-- Slurped Javascript -->
    <!-- Slurped Javascript -->
<script type="text/javascript">
    some javascript 8
    more 2
</script>
</body>
</html>

Of course, this isn't the same as having the script in the head, as hoped, but at least it's conveniently coalesced into one place.

However, the solution is not complete because when you have a {% include "y.html" %} in there, where "y.html" includes a {% js %} statement, the {% js_content %} gets called before the include's {% js %} statement (i.e. x.html is fully parsed before y.html starts).

I also need to, but have not yet, inserted constant nodes that would have the static javascript try/catch, which I indicated I wanted to have in there. This is not an issue.

I'm pleased to be making progress, and I'm grateful for input.

I've opened the related question: Jinja2 compile extension after includes


Edit

Solution

class JavascriptBuilderExtension(Extension):
    tags = set(['js'])

    def __init__(self, environment):
        super(JavascriptBuilderExtension, self).__init__(environment)
        environment.extend(jbc = "",)

    def parse(self, parser):
        """Parse tokens """
        tag = parser.stream.next()
        body = parser.parse_statements(['name:endjs'], drop_needle=True)
        return nodes.CallBlock(
            self.call_method('_jbc', [], [], None, None),
            [], [], body
        ).set_lineno(tag.lineno)

    def _jbc(self, caller=None):
        self.environment.jbc += "\ntry { %s } catch (e) { ; };" % caller()
        return "<!-- Slurped -->"

After completed, the environment will contain a variable jbc that has all the Javascript. I can insert this via, for example, string.Template.


Answer

Wolph picture Wolph · Nov 30, 2010

From my comment:

If you would use extend instead of include you could do it. But because of the full separation between the parse and render step you won't be able to change the context of the parent scope till after it's too late. Also, the Jinja context is supposed to be immutable.

Example:

base.html

<html>
   <head>
      {% block head %}

      <title>{% block title %}This is the main template{% endblock %}</title>

      <script type="text/javascript">
      {% block head_js %}
      $(function () {
        $("#abc").css("color", "red");
      });
      {% endblock %}
      </script>

      {% endblock head_js %}
   </head>
   <body>
      {% block body %}
      <h1>{% block body_title %}This is the main template{% endblock body_title %}</h1>

      {% endblock body %}
   </body>
 </html>

some_page.html

{% block title %}This is some page{% endblock title %}

{% block head_js %}
{{ super() }}
try { {{ caller() }} } catch (e) {
   my.log.error(e.name + ": " + e.message);
}        // jquery parlance:
{% endblock head_js %}