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've opened the related question: Jinja2 compile extension after includes
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
.
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 %}
You can generalize this into a generic capture extension that works within macros. Here's one I wrote:
from jinja2 import nodes
from jinja2.ext import Extension
class CaptureExtension(Extension):
"""
Generic HTML capture, inspired by Rails' capture helper
In any template, you can capture an area of content and store it in a global
variable:
{% contentfor 'name_of_variable' %}
blah blah blah
{% endcontentfor %}
To display the result
{{ name_of_variable }}
Multiple contentfor blocks will append additional content to any previously
captured content.
The context is global, and works within macros as well, so it's useful for letting macros define
javascript or <head> tag content that needs to go at a particular position
on the base template.
Inspired by http://stackoverflow.com/questions/4292630/insert-javascript-at-top-of-including-file-in-jinja-2
and http://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html
"""
tags = set(['contentfor'])
def __init__(self, environment):
super(CaptureExtension, self).__init__(environment)
def parse(self, parser):
"""Parse tokens """
tag = parser.stream.next()
args = [parser.parse_expression()]
body = parser.parse_statements(['name:endcontentfor'], drop_needle=True)
return nodes.CallBlock(self.call_method('_capture', args),[], [], body).set_lineno(tag.lineno)
def _capture(self, name, caller):
if name not in self.environment.globals:
self.environment.globals[name] = ''
self.environment.globals[name] += caller()
return ""
The solution by Lee Semel did not work for me. I think globals are protected from this kind of modification at runtime now.
from jinja2 import nodes
import jinja2
from jinja2.ext import Extension
class CaptureExtension(Extension):
"""
Generic HTML capture, inspired by Rails' capture helper
In any template, you can capture an area of content and store it in a global
variable:
{% capture 'name_of_variable' %}
blah blah blah
{% endcapture %}
{% capture 'a' %}panorama{% endcapture %}
To display the result
{{ captured['name_of_variable'] }}
{{ captured['a'] }}
The context is global, and works within macros as well, so it's useful for letting macros define
javascript or <head> tag content that needs to go at a particular position
on the base template.
Inspired by http://stackoverflow.com/questions/4292630/insert-javascript-at-top-of-including-file-in-jinja-2
and http://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html
"""
tags = set(['capture'])
def __init__(self, environment):
super(CaptureExtension, self).__init__(environment)
assert isinstance(environment, jinja2.Environment)
self._myScope = {}
environment.globals['captured'] = self._myScope
def parse(self, parser):
"""Parse tokens """
assert isinstance(parser, jinja2.parser.Parser)
tag = parser.stream.next()
args = [parser.parse_expression()]
body = parser.parse_statements(['name:endcapture'], drop_needle=True)
return nodes.CallBlock(self.call_method('_capture', args),[], [], body).set_lineno(tag.lineno)
def _capture(self, name, caller):
self._myScope[name] = caller()
return ""
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With