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

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

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

По сути, цель состоит в том, чтобы объединить весь javascript в теги <head> с помощью макроса {% call js() %} /* some js */ {% endcall %}.


х.html

<html>
<head>
  <script type="text/javascript>
  {% block head_js %}{% endblock %}
  </script>
  </head>
<body>
  {% include "y.html" %}
</body>
</html>

у.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' не принимает аргумент ключевого слова 'вызывающий'

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

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

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

jinja2.exceptions.TemplateAssertionError: блок 'head_js' определен дважды

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


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

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

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

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

Брайан


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

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

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 %} вызывается перед оператором include {% 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.



person Brian M. Hunt    schedule 27.11.2010    source источник
comment
Зачем тебе весь JS в голове?   -  person Cameron    schedule 27.11.2010
comment
@Cameron: согласованность, простота отладки и разделение кода и содержимого. Я бы предпочел, чтобы в моем HTML не было сотен тегов <script>.   -  person Brian M. Hunt    schedule 27.11.2010
comment
@Brian: Достаточно справедливо, но я боюсь, что нет простого способа сделать это ... Я также не уверен, что вы можете иметь блоки внутри макросов.   -  person Cameron    schedule 28.11.2010
comment
@Cameron: Я тоже так подозреваю, но мне это показалось не совсем нежелательным паттерном, который, возможно, уже решил кто-то другой.   -  person Brian M. Hunt    schedule 28.11.2010
comment
Если бы вы использовали extend вместо include, вы могли бы это сделать. Но из-за полного разделения между шагами parse и render вы не сможете изменить контекст родительской области, пока не станет слишком поздно. Кроме того, контекст Jinja должен быть неизменным.   -  person Wolph    schedule 30.11.2010
comment
Это очень полезно. Я собираюсь попробовать это. Если вы отправите это как ответ, он может быть вскоре отмечен как правильный. ;)   -  person Brian M. Hunt    schedule 30.11.2010
comment
Кстати, в Flask я использую g для хранения пропущенного кода.   -  person Brian M. Hunt    schedule 27.11.2011


Ответы (4)


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

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

Пример:

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 %}
person Wolph    schedule 30.11.2010
comment
@WoLpH: я отметил этот ответ, хотя есть одна нерешенная проблема: как объединить набор узлов, извлеченных из метода parse(). Конечно, хотя это намного проще проблемы! - person Brian M. Hunt; 01.12.2010
comment
@ Брайан М. Хант: вы можете вернуть список узлов на этапе синтаксического анализа. nodes.Template также принимает список узлов. Вы можете суммировать списки, чтобы получить один длинный список узлов. - person Wolph; 01.12.2010
comment
@WoLpH: я выяснил nodes.CallBack(self.call_method...), который будет анализировать все между тегами {% js %} и доступен в скомпилированной форме с аргументом/функцией caller() вызываемого метода. Какой глоток. :) Вы можете увидеть это в действии в методе _jbc моего последнего редактирования вопроса. - person Brian M. Hunt; 01.12.2010
comment
@ Брайан М. Хант: это правда, но метод _jbc не вызывается до шага render(). Это дает вам результат, но на более позднем этапе всего процесса. Для вас это, конечно, не проблема :) - person Wolph; 02.12.2010
comment
@WoLpH: Так уж получилось, что это не проблема. :) Я использую переменную environment.jbc после рендеринга и делаю string.replace() для замены какой-то волшебной строки на невнятный javascript. Сильно круто. - person Brian M. Hunt; 02.12.2010
comment
@WoLpH: Это действительно хакерство. ржу не могу. Но есть определенная четкость в результате, которым я наслаждаюсь (пока он не сломается! ха-ха). :) Спасибо за помощь в этом наборе вопросов! - person Brian M. Hunt; 02.12.2010

Вы можете обобщить это в универсальное расширение захвата, которое работает в макросах. Вот один из них, который я написал:

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 ""
person LS55321    schedule 22.05.2011

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

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 ""
person Sergei G    schedule 25.02.2013

Ответы выше почти ответили на мой вопрос (я хотел поместить разрозненные фрагменты 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 ''
person tgjones    schedule 26.02.2016