「厳密分離指向テンプレートエンジン、その後」の、さらに続きの報告。今回から見出しに月と週を入れることにしました。(NNDAスタイルも楽じゃねー。)
既に何度か言っているように、Kuwataさんと僕がゴニョゴニョしているテンプレートエンジンは、テレンス・パーのStringTemplate (http://www.stringtemplate.org/)から、そのポリシー/機能性/実行モデルを拝借しています。ただし、構文解析部と展開エンジン(仮想機械)を分離しています。これにより、複数の構文を比較的容易にサポートできます。
まずはSmartyにほぼ互換のテンプレート構文を試してみたのですが、それはそれでいいとして、次はGenshiの構文(Kidを多少拡張した構文)をベースにしてみようかと思ってます。データ構造と実行モデルは固定されているので、構文だけを100%互換とはナカナカいきませんが、8割方は互換のサブセットを見繕ってみよう、と。
以下に現状の案を述べます。以下に出現する名前空間接頭辞cは、適当なURIに束縛されているとします。テンプレートがXML整形式であることは要求しません(レガシーHTMLを許す)が、名前空間の宣言だけは見て、接頭辞の束縛を解釈します -- ナンチャッテ名前空間です*1。
内容:
Genshi互換部分
意味と振る舞いは、Genshiの仕様の通りです。
変数(式)の参照
${expr}
内容テキストと属性値内に出現できます。Genshiでは中括弧(ブレイス)を省略できますが、明確性のために常に中括弧を必要だとします。また、中括弧内に書ける式は非常に限定されます(後述)。
ドル記号のエスケープ
$$
条件付き出力
<anElement c:if="booleanExpr">... </anElement>
booleanExpr が真ならanElement要素をそのまま出力して、そうでないならanElement要素は出力されません。else部分はありません。booleanExprとは何であるか? これは大問題ですが、まだハッキリしていません。
ケース分岐
<wrapperElement c:choose=""> <anElement c:when="expr1 is const1">...</anElement> <anElement c:when="expr2 is const2">...</anElement> <anElement c:when="expr3 is const32">...</anElement> <anElement c:otherwise="">...</anElement> </wrapperElement>
Genshiとの互換性から入れておきますが、後述する非互換構文 switch/case を推奨します。
繰り返し
<anElement c:for="itemVar in listExpr">.. ${itemVar} ..</anElement>
内容置換
<anElement c:content="expr">...</anElement>
要素置換
<anElement c:replace="expr">...</anElement>
コメント
<!-- this is a comment --> <!-- !this is a comment too, but one that will be stripped from the output --> <!--! it does not matter whether there's whitespace before or after the exclamation mark. -->
Genshi非互換部分
式言語
式とその評価に関しては、単一の計算モデル(StringTemplateモデル)と実装でサポートします。したがって、どんなテンプレート構文も同じ式言語(Expression Language)を使うことになります。単純な記号の違い程度の差はパーザーで吸収できますが、計算セマンティクスはひとつしかありません。そのセマンティクスで許される式は:
- 数値、文字列、真偽値を表現する定数リテラル: 2, 3.14, "hello", true など
- 単なる変数 : count, PI, greeting など
- ドット「.」を使ったプロパティ(メンバー、フィールド)参照:item.count, math.const.PI, user.name.given など
- ブラケットと番号「[0], [1], [2]など」を使った配列要素の参照:members[12], weeks[1][3], order.cancelled[0] など。
- ドットとブラケットの組み合わせ:company.people[3].hobbies[0] など。
StringTemplateでは、算術演算、論理演算、比較演算などを一切許しません。演算を許すと、それを使ってビジネスロジックを書いてしまうからです。ただ、定数との比較も許さないと、ケース分岐(多方向分岐)の構文が書けないので、定数と比較する演算子だけは入れますが、このあたりはまだ要検討です。最終的にやっぱり演算は禁止するかも。
ケース分岐
<wrapperElement c:switch="expr"> <anElement c:case="const1">...</anElement> <anElement c:case="const2">...</anElement> <anElement c:case="const3">...</anElement> <anElement c:default="">...</anElement> </wrapperElement>
<anElement c:switch="expr" c:case="const1">...</anElement> <anElement c:case="const2">...</anElement> <anElement c:case="const3">...</anElement> <anElement c:default="">...</anElement>
これも、定数との比較が入っていて表現力が強すぎるので、次が推奨です。
<wrapperElement c:switch=""> <anElement c:case="tag1">...</anElement> <anElement c:case="tag2">...</anElement> <anElement c:case="tag3">...</anElement> <anElement c:case="tag4">...</anElement> </wrapperElement>
<anElement c:switch="" c:case="tag1">...</anElement> <anElement c:case="tag2">...</anElement> <anElement c:case="tag3">...</anElement> <anElement c:case="tag4">...</anElement>
switchに何も書かないとユニオン型データの弁別子(タイプタグ)が参照されます。defaultを使わないほうが安全性が増します。
インクルード
Genshiは <xi:include> を使っていて正統派なんですが、レガシーHTMLを考慮して次の形式を使います。
<anElement c:include-href="url" >.. fallback ..</anElement>
インクルードが成功すると、anElement要素全体がサブテンプレート(または単なるファイル)と置き換えられます。GenshiはURLに変数を入れられますが、ここではリテラルURLだけにします。変数を含むURLはエラーです。
フラグメントブロック
いくつかのの要素とテキストの並びを1つのグループとして扱います。
<element1 c:block="start" >...<element1> <element2 >...<element2> ... text ... <element3 />... text ... <element4 c:block="end" >...<element4>
start要素の直前に「{」、end要素の直後に「}」を挿入すると思えばわかるでしょう。ifやforと一緒に使います*2。
<dl> <dt c:for="term in terms" c:block="start">${term.word}</dt> <dd c:block="end">${term.description}</dd> </dl>
Smarty風とGenshi風
HTMLテンプレートとしてはGenshi風のほうが使いやすいかもしれません。しかし、例えばテキストメール本文の生成ではSmarty風しか使えません。JavaScriptソースコードの生成とかだと、どっちも使いものになりません。デリミタの切り替えなどである程度は対応できますが、やっぱり目的によって構文を選べたほうが便利ですよね。
簡単な対応
以下に、Smarty風とGenshi風の対応を簡単にまとめておきます。
\ | Smarty風 | Genshi風 |
---|---|---|
変数参照 | {$var} | ${var} |
条件分岐 | {if $cond}...{/if} | |
繰り返し | {foreach item="x" from="$list"}...{/foreach} | |
インクルード | {include file="sub.html"} | |
ケース分岐とフラグメントブロック
ifにelseifをつなげる方式(Smarty風)と、choose/when(Genshi風), switch/case(拡張構文)の例です。
{if $dayType is "sun"} <p class="sun">わーい、日曜だ。</p> {elseif $dayType is "sat"} <p class="sat">土曜はお出かけ。</p> {elseif $dayType is "hol"} <p class="hol">寝てようかな。</p> {else} <p>お仕事です。</p> {/if}
<div class="wrapper" c:choose=""> <p class="sun" c:when='dayType is "sun"'>わーい、日曜だ。</p> <p class="sat" c:when='dayType is "sat"'>土曜はお出かけ。</p> <p class="hol" c:when='dayType is "hol"'>寝てようかな。</p> <p c:otherwise="">お仕事です。</p> </div>
<div class="wrapper" c:switch="dayType"> <p class="sun" c:case='"sun"'>わーい、日曜だ。</p> <p class="sat" c:case='"sat"'>土曜はお出かけ。</p> <p class="hol" c:case='"hol"'>寝てようかな。</p> <p c:default="">お仕事です。</p> </div>
属性ベースだと全体を囲むラッパー要素が必要になります。Genshiでも細工すればラッパーを削除できるかもしれません(よく分かってません)が、拡張構文では次のように書けます。ラッパー要素がないと若干分かりにくいですが、レイアウトへの影響をゼロにできます。
<p class="sun" c:switch="dayType" c:case='"sun"'>わーい、日曜だ。</p> <p class="sat" c:case='"sat"'>土曜はお出かけ。</p> <p class="hol" c:case='"hol"'>寝てようかな。</p> <p c:default="">お仕事です。</p>
フラグメントブロックの使い方の例を挙げると:
<p class="sun" c:switch="dayType" c:case='"sun"'>わーい、日曜だ。</p> <p class="sat" c:case='"sat"'>土曜はお出かけ。</p> <p class="hol" c:case='"hol"' c:block="start">寝てようかな。</p> <p class="hol">いや、せっかくの休日だしな。</p> <p class="hol" c:block="end">友達に電話しよう。</p> <p c:default="">お仕事です。</p>
同じことをSmarty風で書いてみると:
{if $dayType is "sun"} <p class="sun">わーい、日曜だ。</p> {elseif $dayType is "sat"} <p class="sat">土曜はお出かけ。</p> {elseif $dayType is "hol"} <p class="hol">寝てようかな。</p> <p class="hol">いや、せっかくの休日だしな。</p> <p class="hol">友達に電話しよう。</p> {else} <p>お仕事です。</p> {/if}
なぜGenshi風構文なのか
Smarty風構文は、テレンス・パーの言うレンダラーを切り替えれば、汎用的に使えます。しかし、HTMLテンプレートとして使い、ソースそのままプレビューするとレイアウトが崩れます。そこで、用途をHTMLテンプレートに限定し、プレビュー時にレイアウトが崩れない構文を探したら、Kid/Genshiに行き当たった、というわけです。
Smarty風が好きなら別にSmarty風を使ってもかまいません。コンパイル単位ごとに違う構文を使っても、特に問題がありません(1ファイル内で複数構文を混ぜるのはダメです)。Smarty風構文で書いたテンプレート/マクロ・ライブラリを、Genshi風テンプレートから使うなんてのもできそうです。
ただし、コンパイル方式にすると、マクロ定義/呼び出しやサブテンプレート・インクルードのセマンティクスが複雑化します。通常のプログラミング言語と同様に、「ヘッダーファイルかオブジェクトファイルか」、「静的リンクか動的リンクか」といった区別が必要になります。どういうメカニズムが心理的に自然なのか? 管理/メンテナンスが容易なのか? まだよく分かりません。