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.
<html>
<head>
<script type="text/javascript>
{% block head_js %}{% endblock %}
</script>
</head>
<body>
{% include "y.html" %}
</body>
</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 %}
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>
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:
Can Jinja2 do what I am attempting? If so, how?
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
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 have opened the related question: Jinja2 compile extension after includes
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 %}