说实话,关于自定义扩展的开发,Jinja2的官方文档写得真心的简单。到目前为止网上可参考的资料也非常少,你必须得好好读下源码,还好依然有乐于奉献的大牛们分享了些文章来帮助我理解怎么开发扩展。本文我就完全借鉴网上前人的例子,来给大家演示一个Jinja2的自定义扩展的开发方法。

系列文章

  1. Flask中Jinja2模板引擎详解(一)-控制语句和表达式
  2. Flask中Jinja2模板引擎详解(二)-上下文环境
  3. Flask中Jinja2模板引擎详解(三)-过滤器
  4. Flask中Jinja2模板引擎详解(四)-测试器
  5. Flask中Jinja2模板引擎详解(五)-全局函数
  6. Flask中Jinja2模板引擎详解(六)-块和宏
  7. Flask中Jinja2模板引擎详解(七)-本地化
  8. Flask中Jinja2模板引擎详解(八)-自定义扩展

Pygments

Pygments是Python提供语法高亮的工具,官网是pygments.org。我们在介绍Jinja2的自定义扩展时为什么要介绍Pygments呢?因为Jinja2的功能已经很强了,我一时半会想不出该开发哪个有用的扩展,写个没意义的扩展嘛,又怕误导了读者。恰巧网上找到了一位叫Larry的外国友人开发了一个基于Pygments的代码语法高亮扩展,感觉非常实用。他的代码使用了MIT License,那就我放心拿过来用了,不过还是要注明下这位Larry才是原创。

你需要先执行pip install pygments命令安装Pygments包。代码中用到Pygments的部分非常简单,主要就是调用pygments.highlight()方法来生成HTML文档。Pygments强的地方是它不把样式写在HTML当中,这样就给了我们很大的灵活性。开始写扩展前,让我们预先通过代码

from pygments.formatters import HtmlFormatter
HtmlFormatter(style='vim').get_style_defs('.highlight')

生成样式内容并将其保存在”static/css/style.css”文件中。这个css文件就是用来高亮语法的。

想深入了解Pygments的朋友们,可以先把官方文档看一下。

编写扩展

我们在Flask应用目录下,创建一个”pygments_ext.py”文件,内容如下:

#coding:utf8
from jinja2 import nodes
from jinja2.ext import Extension

from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import guess_lexer, get_lexer_by_name

# 创建一个自定义扩展类,继承jinja2.ext.Extension
class PygmentsExtension(Extension):
    # 定义该扩展的语句关键字,这里表示模板中的{% code %}语句会该扩展处理
    tags = set(['code'])

    def __init__(self, environment):
        # 初始化父类,必须这样写
        super(PygmentsExtension, self).__init__(environment)

        # 在Jinja2的环境变量中添加属性,
        # 这样在Flask中,就可以用app.jinja_env.pygments来访问
        environment.extend(
            pygments=self,
            pygments_support=True
        )

    # 重写jinja2.ext.Extension类的parse函数
    # 这是处理模板中{% code %}语句的主程序
    def parse(self, parser):
        # 进入此函数时,即表示{% code %}标签被找到了
        # 下面的代码会获取当前{% code %}语句在模板文件中的行号
        lineno = next(parser.stream).lineno

        # 获取{% code %}语句中的参数,比如我们调用{% code 'python' %},
        # 这里就会返回一个jinja2.nodes.Const类型的对象,值为'python'
        lang_type = parser.parse_expression()

        # 将参数封装为列表
        args = []
        if lang_type is not None:
            args.append(lang_type)

            # 下面的代码可以支持两个参数,参数之间用逗号分隔,不过本例中用不到
            # 这里先检查当前处理流的位置是不是个逗号,是的话就再获取一个参数
            # 不是的话,就在参数列表最后加个空值对象
            # if parser.stream.skip_if('comma'):
            #     args.append(parser.parse_expression())
            # else:
            #     args.append(nodes.Const(None))

        # 解析从{% code %}标志开始,到{% endcode %}为止中间的所有语句
        # 将解析完后的内容存在body里,并将当前流位置移到{% endcode %}之后
        body = parser.parse_statements(['name:endcode'],drop_needle=True)

        # 返回一个CallBlock类型的节点,并将其之前取得的行号设置在该节点中
        # 初始化CallBlock节点时,传入我们自定义的"_pygmentize"方法的调用,
        # 两个空列表,还有刚才解析后的语句内容body
        return nodes.CallBlock(self.call_method('_pygmentize', args),
                                [], [], body).set_lineno(lineno)

    # 这个自定义的内部函数,包含了本扩展的主要逻辑。
    # 其实上面parse()函数内容,大部分扩展都可以重用
    def _pygmentize(self, lang_type, caller):
        # 初始化HTML格式器
        formatter = HtmlFormatter(linenos='table')

        # 获取{% code %}语句中的内容
        # 这里caller()对应了上面调用CallBlock()时传入的body
        content = caller()

        # 将模板语句中解析到了lang_type设置为我们要高亮的语言类型
        # 如果这个变量不存在,则让Pygmentize猜测可能的语言类型
        lexer = None
        if lang_type is None:
            lexer = guess_lexer(content)
        else:
            lexer = get_lexer_by_name(lang_type)

        # 将{% code %}语句中的内容高亮,即添加各种<span>, class等标签属性
        return highlight(content, lexer, formatter)

这段程序解释起来太麻烦,我就把注释都写在代码里了。总的来说,扩展中核心部分就在parse()函数里,而最关键的就是这个parser对象,它是一个jinja2.parser.Parser的对象。建议大家可以参考下它的源码。我们使用的主要方法有:

  • parser.stream 获取当前的文档处理流,它可以基于文档中的行迭代,所以可以使用next()方法向下一行前进,并返回当前行
  • parser.parse_expression() 解析下一个表达式,并将结果返回
  • parser.parse_statements() 解析下一段语句,并将结果返回。可以连续解析多行。它有两个参数
    1. 第一个是结束位置end_tokens,上例中是{% endcode %}标签,它是个列表,可是设置多个结束标志,遇到其中任意一个即结束
    2. 第二个是布尔值drop_needle,默认为False,即解析完后流的当前位置指向结束语句{% endcode %}之前。设为True时,即将流的当前位置设在结束语句之后

parse()函数最后,我们创建了一个nodes.CallBlock的块节点对象,并将其返回。初始化时,我们先传入了_pygmentize()方法的调用;然后两个空列表分别对应了字段和属性,本例中用不到,所以设空;再传入解析后的语句块bodyCallBlock节点初始化完后,还要记得将当前行号设置进去。接下来,我们对于语句块的所有操作,都可以写在_pygmentize()方法里了。

_pygmentize()里的内容我就不多介绍了,只需要记得声明这个方法时,最后一定要接收一个参数caller,它是个回调函数,可以获取之前创建CallBlock节点时传入的语句块内容。

使用自定义扩展

扩展写完了,其实也没几行代码,就是注释多了点。现在我们在Flask应用代码中将其启用:

from flask import Flask,render_template
from pygments_ext import PygmentsExtension

app = Flask(__name__)
app.jinja_env.add_extension(PygmentsExtension)

然后让我们在模板中试一下:

<head>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<p>A sample of JS code</p>
{% autoescape false %}
{% code 'javascript' %}
    var name = 'World';
    function foo() {
        console.log('Hello ' + name);
    }
{% endcode %}
{% endautoescape %}
</body>

运行下,页面上这段代码是不是有VIM的效果呀?这里我们引入了刚才创建在”static/css”目录下”style.css”样式文件,另外千万别忘了要将自动转义关掉,不然你会看到一堆的HTML标签。

另外提醒下大家,网上有文章说,对于单条语句,也就是不需要结束标志的语句,parse()函数里无需调用nodes.CallBlock,只需返回return self.call_method(xxx)即可。别相信他,看看源码就知道,这个方法返回的是一个nodes.Expr表达式对象,而parse()必须返回一个nodes.Stmt语句对象。

本篇中的示例代码可以在这里下载