讓咱們一塊兒來構建一個模板引擎(二)

上篇文章中咱們的模板引擎實現了變量和註釋功能,同時在文章的最後我給你們留了一個 問題:如何實現支持 iffor 的標籤功能:html

{% if user.is_admin %}
    admin, {{ user.name }}
{% elif user.is_staff %}
    staff
{% else %}
    others
{% endif %}

{% for name in names %}
    {{ name }}
{% endfor %}

在本篇文章中咱們將一塊兒來實現這個功能。python

if ... elif ... else ... endif

首先咱們來實現對 if 語句的支持。 if 語句的語法以下:git

{% if True %}
...
{% elif True %}
...
{% else %}
...
{% endif %}

咱們首先要作的跟以前同樣,那就是肯定匹配標籤語法的正則表達式。這裏咱們用的是下面 的正則來匹配標籤語法:github

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 ):app

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 這一行以後將縮進往前移一步ui

  • 在遇到 elifelse 的時候, 須要將縮進先日後移一步,待 elif/ else 那一行完成後還須要把縮進再移回來code

  • 在遇到 endif 的時候, 咱們知道此時 if 語句已經結束了,須要把縮進日後移一步, 離開 if 語句的主體部分orm

咱們來看一下生成的代碼:

>>> 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 循環其實還支持 breakelse 。 下面咱們就一塊兒來讓咱們的模板引擎的 for 語法支持 breakelse (能夠從 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>

那麼,該如何實現 includeextends 功能呢? 我將在 第三篇文章 中向你詳細的講解。敬請期待。

相關文章
相關標籤/搜索