シンプルなテンプレートエンジンの実装と原理分析
私たちは、シンプルなテンプレートエンジンを書き始め、その根底にある実装メカニズムを深く掘り下げます。
言語設計
このテンプレート言語の設計は非常に基本的で、主に2種類のタグ:変数タグとブロックタグを使用しています。
変数タグ
変数タグは {{
と }}
を識別子として使用します。以下はサンプルコードです:
// 変数は `{{` と `}}` を識別子として使用
<div>{{template_variable}}</div>
ブロックタグ
ブロックタグは {%
と %}
を識別子として使用します。ほとんどのブロックは、終了タグ {% end %}
で終了する必要があります。以下は例です:
// ブロックは `{%` と `%}` を識別子として使用
{% each item_list %}
<div>{{current_item}}</div>
{% end %}
このテンプレートエンジンは、基本的なループと条件文を処理でき、またブロック内で呼び出し可能なオブジェクトを呼び出すこともサポートしています。テンプレート内で任意のPython関数を呼び出すことが非常に便利です。
ループ構造
ループ構造は、コレクションや反復可能なオブジェクトを反復処理するために使用できます。サンプルコードは以下の通りです:
// peopleコレクションを反復処理
{% each person_list %}
<div>{{current_person.name}}</div>
{% end %}
// [1, 2, 3]リストを反復処理
{% each [1, 2, 3] %}
<div>{{current_num}}</div>
{% end %}
// recordsコレクションを反復処理
{% each record_list %}
<div>{{..outer_name}}</div>
{% end %}
上記の例で、person_list
などはコレクションで、current_person
などは現在反復処理されている要素を指します。ドットで区切られたパスは辞書属性として解釈され、..
を使用して外側のコンテキスト内のオブジェクトにアクセスすることができます。
条件文
条件文のロジックは比較的直感的です。この言語は if
と else
構造、および ==
, <=
, >=
, !=
, is
, <
, >
などの演算子をサポートしています。例は以下の通りです:
// numの値に応じて異なるコンテンツを出力
{% if num > 5 %}
<div>more than 5</div>
{% else %}
<div>less than or equal to 5</div>
{% end %}
ブロックの呼び出し
呼び出し可能なオブジェクトは、テンプレートコンテキストを通じて渡され、通常の位置引数または名前付き引数を使用して呼び出すことができます。ブロックを呼び出す際には、end
を使用して閉じる必要はありません。例は以下の通りです:
// 通常の引数を使用
<div class='date'>{% call format_date date_created %}</div>
// 名前付き引数を使用
<div>{% call log_message 'here' verbosity='debug' %}</div>
コンパイル原理とプロセス
ステップ1: テンプレートのトークン化 (tokenize)
原理
テンプレートのトークン化はコンパイルの最初のステップで、その核心となる目的は、テンプレートのコンテンツを独立した断片に分割することです。これらの断片は、通常のHTMLテキストでもよいし、テンプレートで定義された変数タグやブロックタグでもよいです。数学的には、これは複雑な文字列を分割することに似ており、特定の規則に従って文字列を複数の部分文字列に分割します。
実装
正規表現と split()
関数を使用してテキストの分割を完了します。以下は具体的なコード例です:
import re
# 変数タグの開始と終了識別子を定義
VAR_TOKEN_START = '{{'
VAR_TOKEN_END = '}}'
# ブロックタグの開始と終了識別子を定義
BLOCK_TOKEN_START = '{%'
BLOCK_TOKEN_END = '%}'
# 変数タグまたはブロックタグをマッチする正規表現をコンパイル
TOK_REGEX = re.compile(r"(%s.*?%s|%s.*?%s)" % (
VAR_TOKEN_START,
VAR_TOKEN_END,
BLOCK_TOKEN_START,
BLOCK_TOKEN_END
))
TOK_REGEX
正規表現の意味は、変数タグまたはブロックタグをマッチさせて、テキストの分割を実現することです。式の最外側の括弧は、マッチしたテキストをキャプチャするために使用され、?
は非貪欲なマッチを表し、正規表現が最初のマッチで止まるようにします。例は以下の通りです:
# 実際に正規表現の分割効果を示す
>>> TOK_REGEX.split('{% each vars %}<i>{{it}}</i>{% endeach %}')
['{% each vars %}', '<i>', '{{it}}', '</i>', '{% endeach %}']
その後、各断片は Fragment
オブジェクトにカプセル化され、これは断片のタイプを含み、コンパイル関数のパラメータとして使用することができます。断片のタイプは合計4種類あります:
# 断片タイプ定数を定義
VAR_FRAGMENT = 0
OPEN_BLOCK_FRAGMENT = 1
CLOSE_BLOCK_FRAGMENT = 2
TEXT_FRAGMENT = 3
ステップ2: 抽象構文木 (AST) の構築
原理
抽象構文木 (AST) は、ソースコードを構造化された方法で表すデータ構造であり、コードの構文構造を木の形で表します。テンプレートのコンパイルにおいて、ASTを構築する目的は、トークン化で得られた断片を階層構造に整理することで、後続の処理とレンダリングを容易にすることです。数学的には、これは木の図を構築することに似ており、各ノードは構文単位を表し、ノード間の関係はコードの論理構造を反映しています。
実装
トークン化を完了した後、各断片を反復処理して構文木を構築します。Node
クラスを木ノードの基底クラスとして使用し、各ノードタイプのサブクラスを作成します。各サブクラスは process_fragment
と render
メソッドを提供する必要があります。process_fragment
は断片のコンテンツをさらに解析し、必要な属性を Node
オブジェクトに格納するために使用されます;render
メソッドは、提供されたコンテキストを使用して、対応するノードのコンテンツをHTMLに変換する役割を担います。
以下は Node
基底クラスの定義です:
class TemplateNode(object):
def __init__(self, fragment=None):
# 子ノードを格納
self.children = []
# 新しいスコープを作成するかどうかをマーク
self.creates_scope = False
# 断片を処理
self.process_fragment(fragment)
def process_fragment(self, fragment):
pass
def enter_scope(self):
pass
def render(self, context):
pass
def exit_scope(self):
pass
def render_children(self, context, children=None):
if children is None:
children = self.children
def render_child(child):
child_html = child.render(context)
return '' if not child_html else str(child_html)
return ''.join(map(render_child, children))
以下は変数ノードの定義です:
class TemplateVariable(_Node):
def process_fragment(self, fragment):
# 変数名を格納
self.name = fragment
def render(self, context):
# コンテキスト内の変数値を解決
return resolve_in_context(self.name, context)
Node
のタイプを判定し、正しいクラスを初期化するには、断片のタイプとテキストをチェックする必要があります。テキストと変数断片は直接テキストノードと変数ノードに変換できますが、ブロック断片は追加の処理が必要で、そのタイプはブロックコマンドによって決定されます。例えば、{% each items %}
は each
タイプのブロックノードです。
ノードはまた、スコープを作成することもできます。コンパイル中、私たちは現在のスコープを記録し、新しいノードを現在のスコープの子ノードにします。正しい終了タグに遭遇すると、現在のスコープが閉じられ、スコープスタックからスコープがポップされ、スタックの先頭が新しいスコープになります。サンプルコードは以下の通りです:
def template_compile(self):
# ルートノードを作成
root = _Root()
# スコープスタックを初期化
scope_stack = [root]
for fragment in self.each_fragment():
if not scope_stack:
raise TemplateError('nesting issues')
# 現在のスコープを取得
parent_scope = scope_stack[-1]
if fragment.type == CLOSE_BLOCK_FRAGMENT:
# 現在のスコープを終了
parent_scope.exit_scope()
# 現在のスコープをポップ
scope_stack.pop()
continue
# 新しいノードを作成
new_node = self.create_node(fragment)
if new_node:
# 新しいノードを現在のスコープの子ノードリストに追加
parent_scope.children.append(new_node)
if new_node.creates_scope:
# 新しいノードをスコープスタックに追加
scope_stack.append(new_node)
# 新しいスコープに入る
new_node.enter_scope()
return root
ステップ3: レンダリング
原理
レンダリングは、構築されたASTを最終的なHTML出力に変換するプロセスです。このプロセスでは、ASTノードのタイプとコンテキスト情報に基づいて、テンプレート内の変数とロジックを実際の値とコンテンツに置き換える必要があります。数学的には、これは木構造を巡回して評価することに似ており、各ノードの情報を規則に従って変換して結合します。
実装
最後のステップは、ASTをHTMLにレンダリングすることです。このステップでは、AST内のすべてのノードを訪問し、テンプレートに渡された context
パラメータを使用して render
メソッドを呼び出します。レンダリングプロセス中、render
はコンテキスト変数の値を継続的に解決します。ast.literal_eval
関数を使用して、Pythonコードを含む文字列を安全に実行することができます。サンプルコードは以下の通りです:
import ast
def eval_expression(expr):
try:
return 'literal', ast.literal_eval(expr)
except (ValueError, SyntaxError):
return 'name', expr
コンテキスト変数がリテラルの代わりに使用される場合、その値はコンテキスト内で検索する必要があります。ここでは、ドットを含む変数名と、2つのドットを使用して外側のコンテキストにアクセスする変数を処理する必要があります。以下は resolve
関数の実装です:
def resolve(name, context):
if name.startswith('..'):
# 外側のコンテキストを取得
context = context.get('..', {})
name = name[2:]
try:
for tok in name.split('.'):
# コンテキスト内の変数を検索
context = context[tok]
return context
except KeyError:
raise TemplateContextError(name)
結論
このシンプルな学術的な例を通じて、テンプレートエンジンの動作原理についての初步的な理解を得ることができることを願っています。このコードはまだ本番レベルには程遠いものですが、より完全なツールを開発するための基礎となることができます。
参考: https://github.com/alexmic/microtemplates
Leapcell: サーバレスウェブホスティングのベスト
最後に、Pythonサービスをデプロイするのに最適なプラットフォームをおすすめします:Leapcell
🚀 好きな言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。
🌍 無料で無制限のプロジェクトをデプロイ
使用した分だけ支払います — リクエストがなければ、請求はありません。
⚡ 使った分だけ支払い、隠された費用はありません
アイドル料はなく、シームレスにスケーリングできます。
🔹 Twitterでフォローしてください: @LeapcellHQ