Scratchでテンプレートエンジンを構築する(Jinja2やDjangoテンプレートのように)
Daniel Hayes
Full-Stack Engineer · Leapcell

簡単なテンプレートエンジンの実装と原理分析
簡単なテンプレートエンジンを書き始め、その根本的な実装メカニズムを深く探求します。
言語設計
このテンプレート言語の設計は非常に基本的なもので、主に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>5より大きい</div> {% else %} <div>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:最高のサーバーレスWebホスティング
最後に、Pythonサービスのデプロイに最適なプラットフォームをお勧めします。Leapcell
🚀 お気に入りの言語で構築する
JavaScript、Python、Go、Rustで楽に開発できます。
🌍 無制限のプロジェクトを無料でデプロイする
使用した分だけ支払います—リクエストも請求もありません。
⚡ 従量課金制、隠れたコストなし
アイドル料金はなく、シームレスなスケーラビリティのみ。
🔹 Twitterでフォローしてください:@LeapcellHQ