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:
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
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
.
The solution by Lee Semel did not work for me. I think globals are protected from this kind of modification at runtime now.
From my comment:
Example:
base.html
some_page.html
The answers above nearly answered my query (I wanted to put disparate bits of JavaScript all in one place - the bottom), accept using the '+=' variety which appends captures to each other caused problems on refresh. The capture would end up with multiple copies of everything and caused all sorts of issues depending on how many times refresh was hit.
I worked around this by using the line number of the tag in a dictionary to ensure captures are only done once. The one minor disadvantage of this approach is the global needs to be rebuilt every time a capture tage is encountered.
Works well for me though.
You can generalize this into a generic capture extension that works within macros. Here's one I wrote: