查看原文
其他

让我们一起来构建一个模板引擎(二)

(点击上方蓝字,快速关注我们)


来源:伯乐在线专栏作者 - mozillazg 

如有好文章投稿,请点击 → 这里了解详情

如需转载,发送「转载」二字查看说明


上篇文章 中我们的模板引擎实现了变量和注释功能,同时在文章的最后我给大家留了一个 问题:如何实现支持 if 和 for 的标签功能:


{% if user.is_admin %}

    admin, {{ user.name }}

{% elif user.is_staff %}

    staff

{% else %}

    others

{% endif %}

 

{% for name in names %}

    {{ name }}

{% endfor %}


在本篇文章中我们将一起来实现这个功能。


if … elif … else … endif


首先我们来实现对 if 语句的支持。 if 语句的语法如下:


{% if True %}

...

{% elif True %}

...

{% else %}

...

{% endif %}


我们首先要做的跟之前一样,那就是确定匹配标签语法的正则表达式。这里我们用的是下面 的正则来匹配标签语法:


re_tag = re.compile(r'\{% .*? %\}')

 

>>> re_tag.findall('{% if True %}...{% elif True %}...{% else %}...{% endif %}')

['{% if True %}', '{% elif True %}', '{% else %}', '{% endif %}']


然后就是生成代码了, if 语句跟之前的变量不一样那就是:需要进行缩进切换,这一点需要注意一下。


下面我们来看一下为了支持 if 标签增加了哪些代码吧(完整代码可以从 Github 上下载 template2a.py ):


class Template:

 

    def __init__(self, ...):

        # ...

        # 注释

        self.re_comment = re.compile(r'\{# .*? #\}')

        # 标签

        self.re_tag = re.compile(r'\{% .*? %\}')

        # 用于按变量,注释,标签分割模板字符串

        self.re_tokens = re.compile(r'''(

            (?:\{\{ .*? \}\})

            |(?:\{\# .*? \#\})

            |(?:\{% .*? %\})

        )''', re.X)

    

        # 生成 def __func_name():

        # ...

 

    def _parse_text(self):

        # ...

        for token in tokens:

            # ...

            if self.re_variable.match(token):

                # ...

            elif self.re_comment.match(token):

                continue

 

            # {% tag %}

            elif self.re_tag.match(token):

                # 将前面解析的字符串,变量写入到 code_builder 中

                # 因为标签生成的代码需要新起一行

                self.flush_buffer()

 

                tag = token.strip('{%} ')

                tag_name = tag.split()[0]

                if tag_name in ('if', 'elif', 'else'):

                    # elif 和 else 之前需要向后缩进一步

                    if tag_name in ('elif', 'else'):

                        self.code_builder.backward()

                    self.code_builder.add_line('{}:'.format(tag))

                    # if 语句条件部分结束,向前缩进一步,为下一行做准备

                    self.code_builder.forward()

                elif tag_name in ('endif',):

                    # if 语句结束,向后缩进一步

                    self.code_builder.backward()

 

            else:

                # ...


上面代码的关键点是生成代码时的缩进控制:


  • 在遇到 if 的时候, 需要在 if 这一行之后将缩进往前移一步

  • 在遇到 elif 和 else 的时候, 需要将缩进先往后移一步,待 elif/ else 那一行完成后还需要把缩进再移回来

  • 在遇到 endif 的时候, 我们知道此时 if 语句已经结束了,需要把缩进往后移一步, 离开 if 语句的主体部分


我们来看一下生成的代码:


>>> from template2a import Template

>>> t = Template('''

   ... {% if score >= 80 %}

   ... A

   ... {% elif score >= 60 %}

   ... B

   ... {% else %}

   ... C

   ... {% endif %}

   ... ''')

>>> t.code_builder

def __func_name():

    __result = []

    __result.extend(['\n'])

    if score >= 80:

        __result.extend(['\nA\n'])

    elif score >= 60:

        __result.extend(['\nB\n'])

    else:

        __result.extend(['\nC\n'])

    __result.extend(['\n'])

    return "".join(__result)


代码中的 if 语句和缩进没有问题。下面再看一下 render 的结果:


>>> t.render({'score': 90})

'\n\nA\n\n'

>>> t.render({'score': 70})

'\n\nB\n\n'

>>> t.render({'score': 50})

'\n\nC\n\n'


对 if 语句的支持就这样实现了。有了这次经验下面让我们一起来实现对 for 循环的支持吧。


for … endfor


模板中的 for 循环的语法如下:


{% for name in names %}

    ...

{% endfor %}


从语法上可以看出来跟 if 语句是很相似了,甚至比 if 语句还要简单。只需在原有 if 语句代码 的基础上稍作修改就可以(完整版可以从 Github 上下载 template2b.py ):


class Template:

 

    # ...

 

    def _parse_text(self):

        # ...

            elif self.re_tag.match(token):

                # ...

                if tag_name in ('if', 'elif', 'else', 'for'):

                    # ...

                elif tag_name in ('endif', 'endfor'):

                    # ...


可以看到其实就是修改了两行代码。按照惯例我们先来看一下生成的代码:


>>> from template2b import Template

>>> t = Template('''

   ... {% for number in numbers %}

   ... {{ number }}

   ... {% endfor %}

   ... ''')

>>> t.code_builder

def __func_name():

    __result = []

    __result.extend(['\n'])

    for number in numbers:

        __result.extend(['\n',str(number),'\n'])

    __result.extend(['\n'])

    return "".join(__result)


render 效果:


>>> t.render({'numbers': range(3)})

'\n\n0\n\n1\n\n2\n\n'


for ... endfor 语法就这样实现了。是不是很简单😄?但是还没完😁


相信大家都知道在 python 中 for 循环其实还支持 break 和 else 。 下面我们就一起来让我们的模板引擎的 for 语法支持 break 和 else (可以从 Github 上下载: template2c.py )


class Template:

 

    # ...

 

    def _parse_text(self):

        # ...

            elif self.re_tag.match(token):

                # ...

                if tag_name in ('if', 'elif', 'else', 'for'):

                    # ...

                elif tag_name in ('break',):

                    self.code_builder.add_line(tag)

                elif tag_name in ('endif', 'endfor'):

                    # ...


可以看到,其实也是只增加了两行代码。效果:


from template2c import Template

 

>>> t = Template('''

... {% for number in numbers %}

...    {% if number > 2 %}

...       {% break %}

...    {% else %}

...       {{ number }}

...    {% endif %}

... {% else %}

...    no break

... {% endfor %}

... ''')

>>> t.code_builder

def __func_name():

    __result = []

    __result.extend(['\n'])

    for number in numbers:

        __result.extend(['\n   '])

        if number > 2:

            __result.extend(['\n      '])

            break

            __result.extend(['\n   '])

        else:

            __result.extend(['\n      ',str(number),'\n   '])

        __result.extend(['\n'])

    else:

        __result.extend(['\n   no break\n'])

    __result.extend(['\n'])

    return "".join(__result)

 

>>> t.render({'numbers': range(3)}).replace('\n', '')

'         0            1            2      no break'

>>> t.render({'numbers': range(4)}).replace('\n', '')

'         0            1            2            '


就这样我们的模板引擎对 for 的支持算是比较完善了。 至于生成的代码里的换行和空格暂时先不管,留待之后优化代码的时候再处理。


重构


我们的 Template._parse_text 方法代码随着功能的增加已经变成下面这样了:


def _parse_text(self):

    """解析模板"""

    tokens = self.re_tokens.split(self.raw_text)

 

    for token in tokens:

        if self.re_variable.match(token):

            variable = token.strip('{} ')

            self.buffered.append('str({})'.format(variable))

        elif self.re_comment.match(token):

            continue

        elif self.re_tag.match(token):

            self.flush_buffer()

 

            tag = token.strip('{%} ')

            tag_name = tag.split()[0]

            if tag_name in ('if', 'elif', 'else', 'for'):

                if tag_name in ('elif', 'else'):

                    self.code_builder.backward()

                self.code_builder.add_line('{}:'.format(tag))

                self.code_builder.forward()

            elif tag_name in ('break',):

                self.code_builder.add_line(tag)

            elif tag_name in ('endif', 'endfor'):

                self.code_builder.backward()

        else:

            self.buffered.append('{}'.format(repr(token)))


有什么问题呢?问题就是 for 循环里的代码太长了,我们需要分割 for 循环里的 代码。比如把对变量,if/for 的处理封装到单独的方法里。


下面展示了一种方法(可以从 Github 下载 template2d.py ):


def _parse_text(self):

    """解析模板"""

    tokens = self.re_tokens.split(self.raw_text)

    handlers = (

        (self.re_variable.match, self._handle_variable),   # {{ variable }}

        (self.re_tag.match, self._handle_tag),             # {% tag %}

        (self.re_comment.match, self._handle_comment),     # {# comment #}

    )

    default_handler = self._handle_string                  # 普通字符串

 

    for token in tokens:

        for match, handler in handlers:

            if match(token):

                handler(token)

                break

        else:

            default_handler(token)

 

def _handle_variable(self, token):

    """处理变量"""

    variable = token.strip('{} ')

    self.buffered.append('str({})'.format(variable))

 

def _handle_comment(self, token):

    """处理注释"""

    pass

 

def _handle_string(self, token):

    """处理字符串"""

    self.buffered.append('{}'.format(repr(token)))

 

def _handle_tag(self, token):

    """处理标签"""

    # 将前面解析的字符串,变量写入到 code_builder 中

    # 因为标签生成的代码需要新起一行

    self.flush_buffer()

    tag = token.strip('{%} ')

    tag_name = tag.split()[0]

    self._handle_statement(tag, tag_name)

 

def _handle_statement(self, tag, tag_name):

    """处理 if/for"""

    if tag_name in ('if', 'elif', 'else', 'for'):

        # elif 和 else 之前需要向后缩进一步

        if tag_name in ('elif', 'else'):

            self.code_builder.backward()

        # if True:, elif True:, else:, for xx in yy:

        self.code_builder.add_line('{}:'.format(tag))

        # if/for 表达式部分结束,向前缩进一步,为下一行做准备

        self.code_builder.forward()

    elif tag_name in ('break',):

        self.code_builder.add_line(tag)

    elif tag_name in ('endif', 'endfor'):

        # if/for 结束,向后缩进一步

        self.code_builder.backward()


这样处理后是不是比之前那个都放在 _parse_text 方法里要好很多?


至此,我们的模板引擎已经支持了如下语法:


  • 变量: {{ variable }}

  • 注释: {# comment #}

  • if 语句: {% if ... %} ... {% elif ... %} ... {% else %} ... {% endif %}

  • for 循环: {% for ... in ... %} ... {% break %} ... {% else %} ... {% endfor %}


之后的文章还将实现其他实用的模板语法,比如 include, extends 模板继承等。


include 的语法(item.html 是个独立的模板文件, list.html 中 include item.html):


{# item.html #}

<li>{{ item }}</li>

 

{# list.html #}

<ul>

    {% for name in names %}

        {% include "item.html" %}

    {% endfor %}

</ul>


list.html 渲染后将生成类似下面这样的字符串:


<ul>

    <li>Tom</li>

    <li>Jim<li>

</ul>


extends 的语法(base.html 是基础模板, child.html 继承 base.html 然后重新定义 base.html 中定义过的 block):


{# base.html #}

<div id="content">

{% block content %}

    parent_content

{% endblock content %}

</div>

<footer id="footer">

{% block footer %}

    (c) 2016 example.com

{% endblock footer %}

</footer>


child.html:


{% extends "base.html" %}

 

{% block content %}

    child_content

    {{ block.super }}

{% endblock content %}


child.html 渲染后将生成类似下面这样的字符串:


<div id="content">

    child_content

    parent_content

</div>

<footer id="footer">

    (c) 2016 example.com

</footer>


那么,该如何实现 include 和 extends 功能呢? 我将在 第三篇文章 中向你详细的讲解。敬请期待。


觉得本文对你有帮助?请分享给更多人

关注「Python开发者」

看更多技术干货

专栏作者简介点击 → 加入专栏作者 )


mozillazg:好好学习,天天向上。

打赏支持作者写出更多好文章,谢谢

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存