テンプレートエンジンTempuraメモ

2003.11.29新規作成;
2004.11.06更新 リンク切れを修正。

Tempuraは、Ruby用のテンプレートエンジンの一つ。テンプレートエンジンにはすでにAmritaなどがあるが、新たに登場したこのTempuraに、何か違いがあるかどうか試してみる。

インストール

アーカイブファイルは、次のところから入手できる。

インストールは、アーカイブを解凍し、rootになって install.rb を走らせるだけでいい。

# ruby install.rb 

Tempuraに添付されている文字コード変換器を使うときは、別途、Uconvモジュールが必要。Rubyに添付されているiconvを使って自分で変換器を書いてもよい。

Uconvモジュールは、次のところから入手できる。

小さなテンプレートとスクリプト

さっそく小さなサンプルを書いてみる。

TempuraはテンプレートデータをREXMLでパースする。そのため、テンプレートデータはXMLで書かれていなければならない。終了タグを省略したりすると、パース時点でエラーが発生する。

まず、テンプレートファイルを用意する。Tempuraでは特別な要素は使わず、属性として指示を埋め込んでいく。属性値としてRubyスクリプトの断片を埋め込む。

<p _child_="fetch(:obj1)">(ここが置き換えられる)

これを素にして変換結果を出力するRubyスクリプトを書く。テンプレートにRubyスクリプトを書くため、テンプレートデータが汚染されていると(外部からの入力だと)セキュリティエラーが発生する。テンプレートデータの汚染マークを明示的に取り除く必要がある。実際には、盲目的にuntaint()するとセキュリティ上の脆弱性となるので、テンプレートファイルがworld-writableでないかなど、十分な確認が必要となる。

  2| require "tempura/template"
  3| $SAFE = 1
  4| 
  5| model = {:obj1 => "はろーはろー"}
  6| tmpl = Tempura::Template.new_with_string(File.read("1.html").untaint)
  7| puts tmpl.expand(model)

Amritaではテンプレートに埋め込まれたIDとRubyデータとのマッピングをモデルデータして作成し、これをテンプレートエンジンに与えて展開する。

一方、Tempuraでは、このマッピングを作成せず、テンプレートデータにRubyのデータへアクセスするスクリプトを直接埋め込む。上記の場合、model.fetch(:obj1) によって "はろーはろー" という文字列が得られ、これで要素の内容が置換される。

Tempuraのやり方では、例えば要素の内容を文字列で置換するだけだったのを繰り返しにするときなど、モデルデータを変更したときにテンプレートの修正が必要になる可能性が高い。相互依存度が高く、このデザインは非常に不味い。

要素を繰り返す場合は、次のようにする。_block_属性値には、"オブジェクト//メソッド//ブロック引数" を書く。

<ul>
  <li _block_="self//each//i" _child_="i">(replaced)</li>
</ul>

このテンプレートのためのモデルデータは、次のとおり。これ以外の部分は、上述のスクリプトと同じ。

  model = ["最初の", "二番目の"]

実行結果;

<ul>
  <li>最初の</li><li>二番目の</li>
</ul>

これを組み合わせると、よくある表を作れる。

<table>
  <tr><th>row1</th><th>row2</th></tr>
  <tr _block_="self//each//i">
    <td _child_="i.fetch(:o1)">(cell1)</td>
    <td _child_="i.fetch(:o2)">(cell2)</td>
  </tr>
</table>

モデルデータは次のようになる。

  model = [{:o1 => "左上", :o2 => "右上"}, {:o1 => "左下", :o2 => "右下"}]

実行結果;

<table>
  <tr><th>row1</th><th>row2</th></tr>
  <tr>
    <td>左上</td>
    <td>右上</td>
  </tr><tr>
    <td>左下</td>
    <td>右下</td>
  </tr>
</table>

属性値の置き換えも似たような感じで書く。_attr_* 属性で指示する。

<p _attr_style="fetch(:a)">スタイルを変更する</p>

次のスクリプトでは、Tempura::Template.new_with_string()の第2引数として、文字コード変換器を渡している。

  4| require "tempura/template"
  5| require "euc.rb"
  6| $SAFE = 1
  7| 
  8| model = {:a => "font-size:10pt"}
  9| tmpl = Tempura::Template.new_with_string(File.read("4.html").untaint, EucJp)
 10| puts tmpl.expand(model)

文字コード変換器は、次のように書けばいい。これは日本語EUC用。

  2| require "iconv"
  3| 
  4| module EucJp
  5|   def self.to_u8(euc)
  6|     Iconv.iconv("UTF-8", "EUC-JP", euc).to_s
  7|   end
  8| 
  9|   def self.from_u8(utf)
 10|     Iconv.iconv("EUC-JP", "UTF-8", utf).to_s
 11|   end
 12| end

さて、出力するデータをテンプレートの外から与える場合、< や " を適切にエスケープしなければならない。

<a _child_="fetch(:o1)" _attr_href="fetch(:o2)">(replaced)</a>

次のモデルデータを与えてみる。

  model = {:o1 => "内容として< > \" & ' を含む",
           :o2 => "attr < > \" & ' "}

実行結果は次のとおり。適切にエスケープされる。

<a href='attr &lt; &gt; &quot; &amp; &apos; '>内容として&lt; &gt; &quot; &amp; &apos; を含む</a>

イベント

Webアプリケーションを製作する場合、aタグのhref属性値などを適切に設定する必要がある。これが意外と面倒。Tempuraは、formとaを特別扱いし、この手間を減らしている。

_event_属性は、formまたはaに書く。_event_を書いた場合、formではaction属性を省略し、aではhref属性を省略する。

<div>
<form method="post" _event_="act11">
  <input type="text" name="s" />
  <input type="submit" value="送信" />
</form>
<a _event_="act2">別アクション</a>
</div>

スクリプトでは、default_action メソッドでイベントを処理するスクリプトを指定する。

  4| require "tempura/template"
  5| $SAFE = 1
  6| 
  7| tmpl = Tempura::Template.new_with_string(File.read("11.html").untaint)
  8| tmpl.default_action = "do.cgi"
  9| puts tmpl.expand({})

実行結果

<div>
<form action='do.cgi' method='POST'><input name='event' type='hidden' value='act11'/>
  <input name='s' type='text'/>
  <input type='submit' value='送信'/>
</form>
<a href='do.cgi?event=act2'>別アクション</a>
</div>

do.cgiでは、eventコントロール値を見るだけで、処理を分岐できるようになる。

ふーむ、この部分は少し便利かも。でも、大したことはない。

結論

セキュリティへの考慮、モデルデータとテンプレートデータの分離という点で難点がある。Amritaでいい。amrita-altered