在 上篇文章 中咱們的模板引擎實現了對 if
和 for
對支持,同時在文章的最後我給你們留了一個 問題:如何實現支持 include
和 extends
的標籤功能。html
在本篇文章中咱們將一塊兒來動手實現這兩個功能。python
include
標籤對語法是這樣的:假設有一個 item.html 模板文件,它的內容以下:git
<li>{{ item }}</li>
還有一個咱們要渲染的模板 list.html 內容以下:github
<ul> {% for item in items %} {% include "item.html" %} {% endfor %} </ul>
渲染 list.html 後的結果相似:web
<ul> <li>item1</li> <li>item2</li> <li>item3</li> </ul>
從上面能夠看出來 include
標籤的做用相似使用 include
所在位置的名字空間 渲染另外一個模板而後再使用渲染後的結果。因此咱們能夠將 include
的模板文件 看成普通的模板文件來處理,用解析那個模板生成後的代碼替換 include
所在的位置, 再將結果追加到 result_var
。 生成的代碼相似:安全
def func_name(): result = [] # 解析 include 的模板 def func_name_include(): result_include = [] return ''.join(result_include) # 調用生成的 func_name_include 函數獲取渲染結果 result.append(func_name_include()) return ''.join(result)
生成相似上面的代碼就是 include
的關鍵點,下面看一下實現 include
功能 都作了哪些改動 (能夠從 Github 上下載 template3a.py):app
class Template: def __init__(self, ..., template_dir='', encoding='utf-8'): # ... self.template_dir = template_dir self.encoding = encoding # ... def _handle_tag(self, token): """處理標籤""" # ... tag_name = tag.split()[0] if tag_name == 'include': self._handle_include(tag) else: self._handle_statement(tag) def _handle_include(self, tag): filename = tag.split()[1].strip('"\'') included_template = self._parse_another_template_file(filename) # 把解析 include 模板後獲得的代碼加入當前代碼中 # def __func_name(): # __result = [] # ... # def __func_name_hash(): # __result_hash = [] # return ''.join(__result_hash) self.code_builder.add(included_template.code_builder) # 把上面生成的代碼中函數的執行結果添加到原有的結果中 # __result.append(__func_name_hash()) self.code_builder.add_line( '{0}.append({1}())'.format( self.result_var, included_template.func_name ) ) def _parse_another_template_file(self, filename): template_path = os.path.realpath( os.path.join(self.template_dir, filename) ) name_suffix = str(hash(template_path)).replace('-', '_') func_name = '{}_{}'.format(self.func_name, name_suffix) result_var = '{}_{}'.format(self.result_var, name_suffix) with open(template_path, encoding=self.encoding) as fp: template = self.__class__( fp.read(), indent=self.code_builder.indent, default_context=self.default_context, func_name=func_name, result_var=result_var, template_dir=self.template_dir ) return template
首先是 __init__
增長了兩個參數 template_dir
和 encoding
:函數
template_dir
: 指定模板文件夾路徑,由於 include
的模板是相對路徑因此須要這個 選項來獲取模板的絕對路徑優化
encoding
: 指定模板文件的編碼,默認是 utf-8
ui
而後就是 _parse_another_template_file
了,這個方法是用來解析 include
中 指定的模板文件的,其中的 func_name
和 result_var
之因此加了個 hash 值 做爲後綴是不想跟其餘函數變量重名。
_handle_include
實現的是解析 include 的模板, 而後將生成的代碼和代碼中函數的執行結果添加到當前代碼中。
下面來看一下實現的效果。仍是用上面的模板文件:
item.html:
<li>{{ item }}</li>
list.html:
<ul> {% for item in items %} {% include "item.html" %} {% endfor %} </ul>
先來看一下生成的代碼:
>>> from template3a import Template >>> text = open('list.html').read() >>> t = Template(text) >>> t.code_builder def __func_name(): __result = [] __result.extend(['<ul>\n ']) for item in items: __result.extend(['\n ']) def __func_name_7654650009897399020(): __result_7654650009897399020 = [] __result_7654650009897399020.extend(['<li>',str(item),'</li>\n']) return "".join(__result_7654650009897399020) __result.append(__func_name_7654650009897399020()) __result.extend(['\n ']) __result.extend(['\n</ul>\n']) return "".join(__result)
而後是渲染效果:
>>> print(t.render({'items': ['item1', 'item2', 'item3']})) <ul> <li>item1</li> <li>item2</li> <li>item3</li> </ul>
include
已經實現了,下面讓咱們一塊兒來實現 extends
功能。
extends
標籤實現的是模板繼承的功能,而且只能在第一行出現,語法以下:
假設有一個 parent.html 文件它的內容是:
<div id="header">{% block header %} parent_header {% endblock header %}</div> <div id="footer">{% block footer %} parent_footer {% endblock footer %}</div>
還有一個 child.html 文件:
{% extends "parent.html" %} {% block header %} child_header {{ block.super }} {% endblock header %}
child.html 渲染後的結果:
<div id="header"> child_header parent_header </div> <div id="footer"> parent_footer </div>
能夠看到 extends
的效果相似用子模板裏的 block
替換父模板中定義的同名 block
, 同時又可使用 {{ block.super }}
引用父模板中定義的內容,有點相似 class
的繼承效果。
注意我剛纔說的是: 相似用子模板裏的 block
替換父模板中定義的同名 block
。
這個就是 extends
的關鍵點,咱們能夠先找出子模板裏定義的 block
, 而後用子模板裏的 block
替換父模板裏的同名 block
, 最後只處理替換後的父模板就能夠了。
暫時先無論 block.super
,支持 extends
的代碼改動以下(能夠從 Github 下載 template3b.py ):
class Template: def __init__(self, ...): # extends self.re_extends = re.compile(r'\{% extends (?P<name>.*?) %\}') # blocks self.re_blocks = re.compile( r'\{% block (?P<name>\w+) %\}' r'(?P<code>.*?)' r'\{% endblock \1 %\}', re.DOTALL) def _parse_text(self): # extends self._handle_extends() tokens = self.re_tokens.split(self.raw_text) # ... def _handle_extends(self): match_extends = self.re_extends.match(self.raw_text) if match_extends is None: return parent_template_name = match_extends.group('name').strip('"\' ') parent_template_path = os.path.join( self.template_dir, parent_template_name ) # 獲取當前模板裏的全部 blocks child_blocks = self._get_all_blocks(self.raw_text) # 用這些 blocks 替換掉父模板裏的同名 blocks with open(parent_template_path, encoding=self.encoding) as fp: parent_text = fp.read() new_parent_text = self._replace_parent_blocks( parent_text, child_blocks ) # 改成解析替換後的父模板內容 self.raw_text = new_parent_text def _replace_parent_blocks(self, parent_text, child_blocks): """用子模板的 blocks 替換掉父模板裏的同名 blocks""" def replace(match): name = match.group('name') parent_code = match.group('code') child_code = child_blocks.get(name) return child_code or parent_code return self.re_blocks.sub(replace, parent_text) def _get_all_blocks(self, text): """獲取模板內定義的 blocks""" return { name: code for name, code in self.re_blocks.findall(text) }
從上面的代碼能夠看出來咱們遵循的是使用子模板 block
替換父模板同名 block
而後改成解析替換後的父模板的思路. 即,雖然咱們要渲染的是:
{% extends "parent.html" %} {% block header %} child_header {% endblock header %}
實際上咱們最終渲染的是替換後的父模板:
<div id="header"> child_header </div> <div id="footer"> parent_footer </div>
依舊是來看一下實際效果:
parent1.html:
<div id="header">{% block header %} parent_header {% endblock header %}</div> <div id="footer">{% block footer %} parent_footer {% endblock footer %}</div>
child1.html:
{% extends "parent1.html" %} {% block header %} {{ header }} {% endblock header %}
看看最後要渲染的模板字符串:
>>> from template3b import Template >>> text = open('child1.html').read() >>> t = Template(text) >>> print(t.raw_text) <div id="header"> {{ header }} </div> <div id="footer"> parent_footer </div>
能夠看到確實是替換後的內容,再來看一下生成的代碼和渲染後的效果:
>>> t.code_builder def __func_name(): __result = [] __result.extend(['<div id="header"> ',str(header),' </div>\n<div id="footer"> parent_footer </div>\n']) return "".join(__result) >>> print(t.render({'header': 'child_header'})) <div id="header"> child_header </div> <div id="footer"> parent_footer </div>
extends
的基本功能就這樣實現了。下面再實現一下 {{ block.super }}
功能。
{{ block.super }}
相似 Python class
裏的 super
用來實現對父 block
的引用,讓子模板能夠重用父 block
中定義的內容。 只要改一下 _replace_parent_blocks
中的 replace
函數讓它支持 {{ block.super }}
就能夠了(能夠從 Github 下載 template3c.py):
class Template: def __init__(self, ....): # blocks self.re_blocks = ... # block.super self.re_block_super = re.compile(r'\{\{ block\.super \}\}') def _replace_parent_blocks(self, parent_text, child_blocks): def replace(match): ... parent_code = match.group('code') child_code = child_blocks.get(name, '') child_code = self.re_block_super.sub(parent_code, child_code) new_code = child_code or parent_code return new_code
效果:
parent2.html:
<div id="header">{% block header %} parent_header {% endblock header %}</div>
child2.html:
{% extends "parent2.html" %} {% block header %} child_header {{ block.super }} {% endblock header %}
>>> from template3c import Template >>> text = open('child2.html').read() >>> t = Template(text) >>> t.raw_text '<div id="header"> child_header parent_header </div>\n' >>> t.render() '<div id="header"> child_header parent_header </div>\n'
到目前爲主咱們已經實現了現代 python 模板引擎應有的大部分功能了:
變量
if
for
include
extends, block, block.super
後面須要作的工做就是完善咱們代碼了。
不知道你們有沒有注意到,我以前都是用生成 html 來試驗模板引擎的功能的, 這是由於模板引擎確實是在 web 開發中用的比較多,既然是生成 html 源碼那就須要考慮 針對 html 作一點優化,好比去掉多餘的空格,轉義之類的,還有就是一些 Web 安全方面的考慮。
至於怎麼實現這些優化項,我將在 第四篇文章 中向你詳細的講解。敬請期待。