Вставьте 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 %} макро.


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.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 %} вызывается до включения {% 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,


5 ответов

Решение

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

Если вы используете расширение вместо включения, вы можете сделать это. Но из-за полного разделения шага синтаксического анализа и рендеринга вы не сможете изменить контекст родительской области видимости, пока не станет слишком поздно. Кроме того, контекст 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 %}

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

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://stackru.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://stackru.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 ''
      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.


Этот ответ был опубликован как редактирование вопроса «Вставьте javascript вверху включения файла в Jinja 2» OP user19212 в соответствии с CC BY-SA 2.5.

Другие вопросы по тегам