Вставьте javascript поверх включения файла в Jinja 2

в Jinja2 я хотел бы, чтобы следующее работало так, как это выглядит, запустив:

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

по сути, цель состоит в том, чтобы объединить весь javascript в <head> теги с помощью a {% 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 %}

ожидаемый результат

когда я бегу X.html через jinja2 я ожидал бы, что результат быть:

<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>

фактический результат

фактические результаты не радуют. Я получаю несколько типов потенциально освещающих ошибок, например:

TypeError: макрос ' js 'не принимает аргумент ключевого слова 'caller'

или, когда я пытаюсь добавить другой базовый макрос, такой как

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

я получаю следующее исключение

jinja2.исключения.TemplateAssertionError: блок 'head_js' определено дважды

я чувствую, как будто я сталкиваюсь с проблемой дизайна относительно приоритета block теги за macro теги (т. е. макросы, похоже, не инкапсулируют теги блоков так, как я ожидаю).


полагаю, мои вопросы довольно просты:

  1. может Jinja2 сделать то, что я пытаюсь? Если да, то как?

  2. если нет, есть ли другой механизм шаблонов на основе Python, который делает поддержите этот вид картины (например mako, genshi, etc.), который будет работать без проблем в Google App Engine

Спасибо за чтение - я ценю ваш вклад.

Брайан


Edit:

я пытаюсь написать расширение для решения этой проблемы. Я на полпути - используя следующий код:

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],
    )

это упрощает добавление Javascript в конец шаблона ... например,

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

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

под управлением env.get_template('x.html').render() приведет к некоторым освещающим комментариям и ожидаемому результату:

<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>

однако решение не завершено, потому что когда у вас есть {% include "y.html" %} там, где "y.html-включая а {% js %} заявление {% js_content %} вызывается перед включением {% js %} заявление (т. е. x.html полностью разбирается перед y.html начинается).

мне также нужно, но еще не вставили постоянные узлы, которые имели бы статический javascript try/catch, который я указал, что хотел бы иметь там. Это не проблема.

я рад, что делаю успехи, и я благодарен за вклад.

я открыл соответствующий вопрос: Jinja2 компилировать расширение после включает


редактировать

решение

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 -->"

после завершения среда будет содержать переменную jbc у этого есть весь Javascript. Я могу вставить это через, например, string.Template.


4 ответов


из моего комментария:

Если бы вы использовали extend вместо в том числе вы могли бы сделать это. Но ведь полного разделения между разобрать и сделать шаг вы не будете возможность изменения контекста Родительский прицел, пока не станет слишком поздно. Кроме того, контекст Jinja должен будьте неизменны.

пример:

базы.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 %}

вы можете обобщить это в общее расширение захвата, которое работает в макросах. Вот что я написал:--2-->

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 ""

решение ли Семела не сработало для меня. Я думаю, что глобалы теперь защищены от такого рода изменений во время выполнения.

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 ""

ответы выше почти ответил на мой запрос (я хотел поместить разрозненные биты JavaScript все в одном месте - внизу), принять использование разнообразия"+=", которое добавляет захваты друг к другу, вызвало проблемы при обновлении. Захват закончится несколькими копиями всего и вызовет всевозможные проблемы в зависимости от того, сколько раз обновление было поражено.

Я работал вокруг этого, используя номер строки тега в словаре, чтобы убедиться, захватывает только один раз. Одним из незначительных недостатков этого подхода является то, что глобальные потребности должны перестраиваться каждый раз при обнаружении захвата.

хорошо работает для меня.

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 ''