このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

厳密分離指向テンプレートエンジン:6月3週

「厳密分離指向テンプレートエンジン、その後」の、さらに続きの報告。今回から見出しに月と週を入れることにしました。(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)を使うことになります。単純な記号の違い程度の差はパーザーで吸収できますが、計算セマンティクスはひとつしかありません。そのセマンティクスで許される式は:

  1. 数値、文字列、真偽値を表現する定数リテラル: 2, 3.14, "hello", true など
  2. 単なる変数 : count, PI, greeting など
  3. ドット「.」を使ったプロパティ(メンバー、フィールド)参照:item.count, math.const.PI, user.name.given など
  4. ブラケットと番号「[0], [1], [2]など」を使った配列要素の参照:members[12], weeks[1][3], order.cancelled[0] など。
  5. ドットとブラケットの組み合わせ: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風テンプレートから使うなんてのもできそうです。

ただし、コンパイル方式にすると、マクロ定義/呼び出しやサブテンプレート・インクルードのセマンティクスが複雑化します。通常のプログラミング言語と同様に、「ヘッダーファイルかオブジェクトファイルか」、「静的リンクか動的リンクか」といった区別が必要になります。どういうメカニズムが心理的に自然なのか? 管理/メンテナンスが容易なのか? まだよく分かりません。

*1:かつての僕は、ナンチャッテなんて許さなかったけど、最近は軟弱になったのよー。

*2:if, for, when, caseなどの処理はグループ化した後になります。