Insertar JavaScript en la parte superior de la inclusión de archivos en Jinja 2
Pregunta
En Jinja2, me gustaría que el siguiente para el trabajo, ya que parece que debería, ejecutando:
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('.'))
template = env.get_template('x.html')
print template.render()
En esencia, el objetivo es fusionar todo el Javascript en las etiquetas <head>
utilizando una macro de una {% call js() %} /* some js */ {% endcall %}
.
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 %}
Resultado esperado
Cuando corro x.html través Jinja2, yo esperaría que el resultado sea:
<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>
Resultado real
Los resultados reales no son alentadoras. Consigo un par de tipos de errores potencialmente iluminando, por ejemplo:.
TypeError: 'js' macro toma ningún argumento palabra clave 'llama'
o, cuando trate de añadir otra base macro como
{% macro js2() -%}
{%- block head_js -%}
// ... something
{%- endblock -%}
{%- endmacro %}
Me sale el siguiente excepción
jinja2.exceptions.TemplateAssertionError: bloque '' head_js definidos dos veces
Me siento como si estoy corriendo en un problema de diseño con respecto a la precedencia de las etiquetas block
más de las etiquetas macro
(es decir, las macros no parecen etiquetas de bloque encapsulado en el camino espero).
supongo que mis preguntas son bastante simples:
-
Can Jinja2 hacer lo que estoy tratando? Si es así, ¿cómo?
-
Si no es así, ¿hay otro motor de plantillas basado en Python que es compatible con esta clase de patrón (por ejemplo, mako, Genshi, etc.), que funcionaría sin la edición en Google App Engine
Gracias por leer - Aprecio su entrada
.Brian
Edit:
Estoy intentando escribir una extensión para resolver este problema. Estoy a mitad de camino - usando el siguiente código:
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],
)
Esto hace que sea sencillo para añadir JavaScript al final de una plantilla ... por ejemplo.
<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()
dará lugar a algunos comentarios que iluminan y el resultado esperado de:
<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>
Por supuesto, esto no es lo mismo que tener el guión en la cabeza, como se esperaba, pero al menos está convenientemente unió en un solo lugar.
Sin embargo, la solución no es completa porque cuando se tiene un {% include "y.html" %}
allí, donde "y.html" incluye una declaración {% js %}
, la {% js_content %}
se llama antes de la declaración de {% js %}
incluyen (es decir x.html
está totalmente procesadas antes que comience y.html
).
También es necesario, pero aún no se han insertado los nodos constantes que tendría el try/catch
Javascript estática, que he indicado que quería tener allí. Este no es un problema.
Me complace estar avanzando, y estoy agradecido por entrada.
He abierto la pregunta relacionada: Jinja2 compilación incluye extensión después
Editar
Solución
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 -->"
Después de completarse, el medio ambiente contendrá un jbc
variable que tiene todo el Javascript. Puedo insertar este a través de, por ejemplo, string.Template
.
Solución
Desde mi comentario:
Si desea utilizar en lugar de extender incluyes que podría hacerlo. Pero porque de la separación completa entre el analizar y dar paso no será capaz de cambiar el contexto de la ámbito padre hasta después de que sea demasiado tarde. Además, el contexto se supone que Jinja inmutable.
Ejemplo:
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 %}
Otros consejos
Se puede generalizar esto en una extensión de captura genérico que trabaja dentro de las macros. Aquí hay una que escribí:
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 ""
La solución por Lee Semel no funcionó para mí. Creo globales están protegidos de este tipo de modificación en tiempo de ejecución ahora.
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 ""
Las respuestas anteriores casi contestado a mi pregunta (yo quería poner trozos dispares de JavaScript en un solo lugar - la parte inferior), aceptar el uso de la variedad '+ =' que añade capturas entre sí causado problemas en Actualizar. La captura terminaría con múltiples copias de todo y ha causado todo tipo de problemas, dependiendo de cuántas veces fue golpeado actualización.
He trabajado en torno a esta utilizando el número de línea de la etiqueta en un diccionario para asegurar capturas se realizan sólo una vez. La desventaja de menor importancia de este enfoque es las necesidades globales para ser reconstruido cada vez que se encuentra un taje de captura.
funciona bien para mí, sin embargo.
from jinja2 import Markup, nodes
from jinja2.ext import Extension
class CaptureExtension(Extension):
tags = set(['capture'])
def __init__(self, environment):
super(CaptureExtension, self).__init__(environment)
environment.globals['captured'] = {}
self._captured = {}
def parse(self, parser):
lineno = next(parser.stream).lineno
args = [parser.parse_expression(), nodes.Const(lineno)]
body = parser.parse_statements(['name:endcapture'], drop_needle=True)
return nodes.CallBlock(self.call_method('_capture', args), [], [], body).set_lineno(lineno)
def _capture(self, name, lineno, caller):
if name not in self._captured:
self._captured[name] = {}
self._captured[name][lineno] = caller()
markup = Markup(''.join(s for s in self._captured[name].values()))
self.environment.globals['captured'][name] = markup
return ''