【问题标题】:Insert javascript at top of including file in Jinja 2在 Jinja 2 中包含文件的顶部插入 javascript
【发布时间】:2011-05-16 14:40:21
【问题描述】:

在 Jinja2 中,我希望通过运行以下命令来正常工作:

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

本质上,目标是使用 {% call js() %} /* some js */ {% endcall %} 宏将所有 javascript 合并到 <head> 标记中。


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

预期结果

当我通过 jinja2 运行 X.html 时,我希望结果是:

<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: block '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() 将产生一些有启发性的 cmets 和预期的输出:

<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 %} 语句之前被调用(即@ 987654339@ 在y.html 开始之前被完全解析)。

我还需要(但还没有)插入具有静态 javascript try/catch 的常量节点,我表示我希望在其中拥有它。这不是问题。

我很高兴能取得进展,我很感谢您的意见。

我已经打开了相关问题:Jinja2 compile extension after includes


编辑

解决方案

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 插入它。


【问题讨论】:

  • 为什么要把所有的JS都放在头上?
  • @Cameron:一致性、易于调试以及代码和内容的分离。我不想在我的 HTML 中乱扔数百个 &lt;script&gt; 标签。
  • @Brian:很公平,但我担心没有简单的方法可以做到这一点......我也不确定你是否可以在宏中包含块。
  • @Cameron:我也有同样的怀疑,但它确实让我觉得它不是完全不受欢迎的模式,其他人可能已经解决了。
  • 如果您使用extend 而不是include,您可以这样做。但是由于parserender 步骤之间的完全分离,您将无法更改父范围的上下文,直到为时已晚。此外,Jinja 上下文应该是不可变的。

标签: python templates mako jinja2 genshi


【解决方案1】:

上面的答案几乎回答了我的问题(我想将不同的 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 ''

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      您可以将其概括为在宏中工作的通用捕获扩展。这是我写的:

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

      【讨论】:

        【解决方案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 %}
        

        【讨论】:

        • @WoLpH:尽管有一个悬而未决的问题,但我已将其标记为答案:如何连接从parse() 方法中提取的一组节点。当然,虽然这是一个更容易的问题!
        • @Brian M. Hunt:您可以从解析步骤返回节点列表。 nodes.Template 也接受节点列表。您可以对列表求和以获得一长串节点。
        • @WoLpH:我找到了nodes.CallBack(self.call_method...),它将解析{% js %}标签之间的所有内容,并且可以使用被调用方法的caller()参数/函数以编译形式访问。多么拗口。 :) 您可以在我上次编辑问题的_jbc 方法中看到它的实际效果。
        • @Brian M. Hunt:确实如此,但直到 render() 步骤才调用 _jbc 方法。它确实为您提供了输出,但在整个过程的后续步骤中。当然,这对你来说可能不是问题:)
        • @WoLpH:碰巧这不是问题。 :) 我使用environment.jbc 变量后渲染并执行string.replace() 以将一些魔术字符串替换为slurped javascript。非常酷。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-29
        • 2012-11-19
        • 2016-05-05
        相关资源
        最近更新 更多