erubis徹底解説
DESCRIPTION
(RubyKaigi 2009 発表資料) Erubisとは、高速かつ高機能なeRubyライブラリです。本発表では、Erubisの機能と拡張性の高さについて、またeRubyにまつわる諸問題とErubisによる解決策について説明します。またこれらに関連して、テンプレートシステムの将来について私見を披露します。eRubyごときでこんなに考えなきゃいけないことがあるんだと知っていただければ幸いです。TRANSCRIPT
copyright(c) 2009 kuwata-lab.com all rights reserved.
Erubis徹底解説
makoto kuwata <[email protected]>http://www.kuwata-lab.com/
RubyKaigi2009
およびテンプレートシステムの将来について
1
copyright(c) 2009 kuwata-lab.com all rights reserved.
言いたいことは最初に言っとけ
‣事務局の皆様お疲れさまです&ありがとうございます
‣マイナー番組をご視聴いただくみなさんありがとうございます
2
copyright(c) 2009 kuwata-lab.com all rights reserved.
Agenda
‣Part 1. Erubis機能紹介
‣Part 2. eRubyの問題点とその解決策
‣Part 3. テンプレートシステムの将来
3
copyright(c) 2009 kuwata-lab.com all rights reserved.
Part 1. Erubis機能紹介
4
copyright(c) 2009 kuwata-lab.com all rights reserved.
Erubis概要
‣pure RubyなeRuby処理系
‣高性能• http://jp.rubyist.net/magazine/?0022-FasterThanC
‣高機能•デフォルトでHTMLエスケープ•埋め込みパターンの変更• PHP, Java, JS, C, Perl, Schemeに対応• ...他多数
5
copyright(c) 2009 kuwata-lab.com all rights reserved.
使い方のキホン
require 'rubygems'require 'erubis'str = File.read('template.eruby')eruby = Erubis::Eruby.new(str)print eruby.result(binding())
Ruby program:
$ erubis template.eruby$ erubis -x template.eruby $ erubis -z template.eruby
command-line:
# 文法チェック# Rubyコードへ変換# 実行
# 必要に応じて
6
copyright(c) 2009 kuwata-lab.com all rights reserved.
デフォルトでHTMLエスケープstr =<<END<%= var %><%== var %>ENDeruby = Erubis::Eruby.new(str, :escape=>true)puts eruby.result(:var=>"<B&B>")
<B&am;><B&B>
<%= %> でエスケープあり、<%== %> でエスケープなし
output:デフォルトでエスケープする・しないをユーザが選択可能
(choosability)
7
copyright(c) 2009 kuwata-lab.com all rights reserved.
埋め込みパターンの変更
‣例:<% %> のかわりに [% %]
[% for x in @list %] <li>[%= x %]</li>[% end %]
## RubyErubis::Eruby.new(str, :pattern=>'\[% %\]')## command-line$ erubis -p '\[% %\]' file.eruby
正規表現のメタ文字はエスケープすること!
8
copyright(c) 2009 kuwata-lab.com all rights reserved.
BindingのかわりにHashやObjectを
hash = { :title => "Example", :items => [1, 2, 3], }erubis = Erubis::Eruby.new(str)puts erubis.result(hash)
@title = "Example"@items = [1, 2, 3]erubis = Erubis::Eruby.new(str)puts erubis.evaluate(self)
Hashを使った例 Objectを使った例
<h1><%= title%></h1><% for x in items %><% end %>
<h1><%= @title%></h1><% for x in @items %><% end %>
9
copyright(c) 2009 kuwata-lab.com all rights reserved.
Enhancer
‣Erubisの機能を拡張するmoduleのこと## <%= %> でHTMLエスケープするmodule EscapeEnhancer def add_expr(src, code, indicator) if indicator == '=' src << " _buf<<escapeXml(#{code})" elsif indicator == '==' src << " _buf<<(#{code}).to_s;" end endend
Erubis内部は多数の細かいメソッドに分かれているため、上書きしやすい
10
copyright(c) 2009 kuwata-lab.com all rights reserved.
Enhancer (cont')
### 標準出力に出力させるEnhancer### (利点: print() が使える)module StdoutEnhancer def add_preamble(src) src << "_buf = $stdout;" end def add_postamble(src) src << "\n\"\"\n" endend
_buf="" のかわりに _buf=$stdout
_buf.to_s のかわりに "" (空文字列)
11
copyright(c) 2009 kuwata-lab.com all rights reserved.
Enhancerの使い方
### Rubyclass MyEruby < Erubis::Eruby include Erubis::EscapeEnhancer include Erubis::PercentLineEnhancerendputs MyEruby.new(str).result(:items=>[1,2,3])
### command-line$ erubis -E Escape,Percent file.eruby
include/extendするだけ
「,」で区切って指定
12
copyright(c) 2009 kuwata-lab.com all rights reserved.
標準Enhancer
‣ EscapeEnhancer : デフォルトでHTMLエスケープ
‣ PercentLineEnhancer : 行頭の '%' で埋め込み文
‣ InterporationEnhancer : _buf<<"#{式}" を使う
‣DeleteIndentEnhancer : HTMLインデントを削除
‣ StdoutEnhancer : _buf=""のかわりに_buf=$stdout
‣ ...他多数 (erubis -h で一覧が表示される)
13
copyright(c) 2009 kuwata-lab.com all rights reserved.
コンテキストデータ
‣テンプレートに渡すデータ(コンテキストデータ)をコマンドラインで指定可能
<% for x in @arr %><li><%= x %></li><% end %>
### command-line$ erubis -c '{arr: [A, B, C]}' template.eruby # YAML$ erubis -c '@arr=%w[A B C]' template.eruby # Ruby
<li>A</li><li>B</li><li>C</li>
14
copyright(c) 2009 kuwata-lab.com all rights reserved.
コンテキストデータファイル
‣「*.yaml」または「*.rb」を読み込む
$ erubis -f data.yaml template.eruby # YAML$ erubis -f data.rb template.eruby # Ruby
title: Exampleitems: - name: Foo - name: Bar
@title = "Example"@items = [ {"name"=>"Foo"}, {"name"=>"Bar"}, ]
data.yaml data.rb
15
copyright(c) 2009 kuwata-lab.com all rights reserved.
デバッグプリント
‣<%=== 式 %> でデバッグプリント
<%=== @var %>
### Ruby変換後$stderr.puts("*** debug: @var=#{@var.inspect}")
### 実行結果*** debug: @var=["A", "B", "C"]
同じ式を2箇所に書く必要がない
16
copyright(c) 2009 kuwata-lab.com all rights reserved.
複数プログラミング言語対応
‣PHP, Java, JS, C, Perl,Scheme に対応 (変換のみ)
<% for (i=0; i<n; i++) { %><li><%= "%d", i %><% } %>
#line 1 "file.ec" for (i=0; i<n; i++) { fputs("<li>", stdout); fprintf(stdout, "%d", i); fputs("\n", stdout); }
(erubis -xl c file.ec の出力)
(C言語の例)
printf()の書式をそのまま流用
17
copyright(c) 2009 kuwata-lab.com all rights reserved.
まとめ
‣Erubisは高機能かつ拡張性が高い•デフォルトでHTMLエスケープ
•埋め込みパターンの変更
• Enhancer
•コンテキストデータ/ファイル
•デバッグプリント
• PHP, Java, JS, C, Perl, Schemeに対応
18
copyright(c) 2009 kuwata-lab.com all rights reserved.
Part 2. eRubyの問題点とその解決策
19
copyright(c) 2009 kuwata-lab.com all rights reserved.
問題:ローカル変数が変更される
‣binding()を使うと、ローカル変数がローカルでなくなってしまう。•発見が非常に難しい
i = 0str = File.read('file.erb')ERB.new(str).result(binding)p i #=> 3
### file.erb<% for i in 1..3 %><li><%= i %></li><% end %>
書き変わってる!
20
copyright(c) 2009 kuwata-lab.com all rights reserved.
問題の原因
‣binding()ではすべてのローカル変数が渡されてしまう•渡したい変数だけを渡すことが困難
•どの変数を渡しているのか不明
b = Bingind.newb[:title] = "Example"b[:items] = [1, 2, 3]
こんなふうにできたらいいなぁ…
21
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:ERB
‣ (標準では)特になし
‣ Structを使う方法が紹介されている• http://d.hatena.ne.jp/m_seki/20080528/1211909590
Foo = Struct.new(:title, :items)class Foo def env; binding(); endendctx = Foo.new("Example", [1,2,3])ERB.new(str).result(ctx.env)
22
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:Erubis
‣BindingのかわりにHashを使う
def result(b=TOPLEVEL_BINDING) if b.is_a?(Hash) s = b.collect{|k,v| "#{k}=b[#{k.inspect}];"}.join b = binding() eval s, b end return eval(@src, b)end
新しいBindingを使ってローカル変数を設定
erubis.result(:items=>[1, 2, 3]) 何を渡しているか明確!
23
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:Erubis
‣BindingのかわりにObjectを使う
def evaluate(ctx) if ctx.is_a?(Hash) hash = ctx; ctx = Object.new hash.each {|k,v| ctx.instance_variable_set("@#{k}", v) } end return ctx.instance_eval(@src)end
@items = [1, 2, 3]; erubis.evaluate(self)
<% for x in @items %><% end %>
Hashなら値をインスタンス変数に詰め替える
24
copyright(c) 2009 kuwata-lab.com all rights reserved.
問題:変換コストと構文解析コスト
ERBErubis::Eruby
ERBErubis::Eruby
ERBErubis::Eruby
0 10 20 30 40
実行構文解析(by eval)変換(eRuby→Ruby)
1.8.6
1.8.7
1.9.1
(sec)
実行コストより構文解析や変換コストのほうが高い
25
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:ERB
‣変換コスト:特になし‣解析コスト:メソッド定義ヘルパー
•使い方が大きく異なるというデメリットclass Foo extend ERB::DefMethod def_erb_method('render', 'template.erb')endprint Foo.new.render
26
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:Erubis
‣変換コスト:ファイルにキャッシュ•初回:変換後のRubyコードを *.cache に保存
• 2回目以降:*.cache を読み込む
eruby = Erubis::Eruby.load_file("file.eruby")print eruby.result()
CGIでも大活躍!
27
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:Erubis
‣解析コスト:Procオブジェクトで保持•メソッド名が必要なく使いやすい
•メソッド定義とほぼ同速
def evaluate(ctx) @proc ||= eval(@src) ctx.instance_eval(@proc)end
instance_evalが引数にProcオブジェクトをとれることを利用
28
copyright(c) 2009 kuwata-lab.com all rights reserved.
問題:余分な改行
‣出力に余分な改行が含まれる•非HTMLでは大きな問題
<ul><% for x in @list %> <li><%= x %></li><% end %></ul>
<ul>
<li>AAA</li>
<li>BBB</li>
</ul>
余計な改行
余計な改行
29
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:ERB
‣さまざまな trim mode を用意• ">" : 行末が"%>"なら改行を削除• "<>" : 行頭が"<%"かつ行末が"%>"なら改行削除• "-" : "<%-" と "-%>" の前後の改行や空白を削除• "%" : "%" で始まる行を埋込み文と見なす• "%>", "%<>", "-" : "%"と、">"/"<>"/"-" の同時指定
ERB.new(str, nil, "%<>")
30
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:Erubis
‣「文」と「式」とで挙動を変える• <% 文 %> なら前後の空白と改行を取り除く
• <%= 式 %> なら何もしない
<ul> AAA BBB CCC</ul>
削除!<ul><% for x in @list %> <%= x %><% end %></ul> そのまま
31
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策の比較
ERB Erubis
eRuby仕様の準拠
×(仕様を拡張)
◎(仕様に準拠)
仕様の簡潔さ
×(種類が多すぎ)
◎(一種類だけ)
実装の容易さ
×(複雑怪奇)
◎(実装が簡単)
32
copyright(c) 2009 kuwata-lab.com all rights reserved.
‣「余分な改行」問題は昔から認識されていた• [ruby-list:18894] eRuby変換後の無駄(?)な改行
‣誰も「文と式とで挙動を変える」ことを思いついていない• <% %> と <%= %> とが同じに見えている
•同じに見えるけど実は違うものとして認識できることが重要!
考えるヒント
33
copyright(c) 2009 kuwata-lab.com all rights reserved.
問題:デフォルトでエスケープ
‣「<%= 式 %>」は デフォルトでHTMLエスケープされるべきだろ!
• eRubyはテキスト汎用の仕様であり、HTML専用ではない
•とはいっても、セキュリティは最重要課題
‣エスケープしたくないときはどうする?
34
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:ERB
‣ (公式には)特になし
‣非公式なら•「エスケープしない文字列」を表すクラスを
Stringとは別に用意
• http://www2a.biglobe.ne.jp/~seki/ruby/erbquote.html
35
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:Erubis
‣埋め込み書式とErubisクラスを拡張•実装が簡単、動作も高速
eruby = Erubis::Eruby.new(str, :escape=>true)# or eruby = Erubis::EscapedEruby.new(str)puts eruby.evaluate(ctx)
Hi <%= @name %>! # エスケープありHi <%== @name %>! # エスケープなし
36
copyright(c) 2009 kuwata-lab.com all rights reserved.
問題:文法エラーが見つけにくい<% unless @items.blank? %><table> <tbody> <% @items.each do |item| %> <tr class="item" id="item-<%=item.id%>"> <td class="item-id"><%= item.id %></td> <td class="item-name"> <% if item.url && !item.url.empty? %> <a href="<%= item.url %>"><%=item.name%></a> <% else %> <span><%=item.name%></span> <% end %> </td> </tr> <% end %> </tbody></table><% end %>
・HTMLとRubyとが混在・endの対応が確認しづらい (do と end が100行離れてたり)
37
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:ERB
‣特になし (せいぜい erb -x のみ)
$ erb -x foo.eruby_erbout = ''; unless @items.blank? ; _erbout.concat "\n"_erbout.concat "<table>\n"_erbout.concat " <tr class=\"record\">\n"・・・$ erb -x foo.eruby | ruby -wcSyntax OK
38
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:Erubis
‣様々なコマンドオプションを用意• -x : Ruby スクリプトを表示
• -X : HTMLを表示しない
• -N : 行番号つき(number)
• -U : 連続する空行を1行に(unique)
• -C : 連続する空行を削除(compact)
• -z : 文法チェック
39
copyright(c) 2009 kuwata-lab.com all rights reserved.
$ cat foo.eruby<% unless @items.blank? %><table> <% @items.each_with_index do|x, i| %> <tr class="record"> <td><%= i +1 %></td> <td><%=h x %></td> </tr> <% end %></table><% end %>
40
copyright(c) 2009 kuwata-lab.com all rights reserved.
$ erubis -x foo.eruby_buf = ''; unless @items.blank? _buf << '<table>'; @items.each_with_index do|x, i| _buf << ' <tr class="record"> <td>'; _buf << ( i +1 ).to_s; _buf << '</td> <td>'; _buf << (h x ).to_s; _buf << '</td> </tr>'; end _buf << '</table>'; end _buf.to_s
-xでRubyスクリプトを表示
41
copyright(c) 2009 kuwata-lab.com all rights reserved.
$ erubis -X foo.eruby_buf = ''; unless @items.blank?
@items.each_with_index do|x, i|
_buf << ( i +1 ).to_s; _buf << (h x ).to_s;
end
end _buf.to_s
-Xで埋込み文と式だけを表示
42
copyright(c) 2009 kuwata-lab.com all rights reserved.
$ erubis -XN foo.eruby 1: _buf = ''; unless @items.blank? 2: 3: @items.each_with_index do|x, i| 4: 5: _buf << ( i +1 ).to_s; 6: _buf << (h x ).to_s; 7: 8: end 9: 10: end 11: _buf.to_s
-Nで行番号つき(number)
43
copyright(c) 2009 kuwata-lab.com all rights reserved.
$ erubis -XNU foo.eruby 1: _buf = ''; unless @items.blank? 3: @items.each_with_index do|x, i| 5: _buf << ( i +1 ).to_s; 6: _buf << (h x ).to_s; 8: end 10: end 11: _buf.to_s
-Uで空行を1行に圧縮(uniq)
44
copyright(c) 2009 kuwata-lab.com all rights reserved.
$ erubis -XNC foo.eruby 1: _buf = ''; unless @items.blank? 3: @items.each_with_index do|x, i| 5: _buf << ( i +1 ).to_s; 6: _buf << (h x ).to_s; 8: end 10: end 11: _buf.to_s
-C で空行を削除(compact)
45
copyright(c) 2009 kuwata-lab.com all rights reserved.
問題:式が文を含むことが可能
<%= form_for :user do %> <div> <%= text_field :name %> </div><% end %>
ヘルパー関数の戻り値を<%= %> で埋め込みたい ブロックが文を
含んでいる
eRubyの仕様では想定外!
46
copyright(c) 2009 kuwata-lab.com all rights reserved.
問題の原因
_buf = "";_buf << ( 10.times do ).to_s;_buf << " Hello\n"; end
<%= 10.times do %> Hello<% end %>
<%= 式 %> は単体で完結していることが仕様上暗黙の前提
文法エラー!変換
47
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:ERB+Rails
‣呼出し元のローカル変数(_erbout) をメソッド内から参照・変更
<% form_for do %> Hello<% end %>
埋め込み式は諦め、かわりに…
_erbout = ""form_for do _erbout.concat("Hello")end
form_for()の内から_erboutに直接追加
まさに黒魔術!
48
copyright(c) 2009 kuwata-lab.com all rights reserved.
解決策:Erubis+Merb
‣Erubisのパーサを拡張
<%= form_for do %> Hello<% end =%>
@_buf << (form_for do;@_buf << "Hello\n" end);
ブロックの終端を示す記法を導入
ローカル変数からインスタンス変数に変更
ブロックを認識
49
copyright(c) 2009 kuwata-lab.com all rights reserved.
考察
‣ eRubyの仕様を拡張
‣黒魔術を使っていない (kool!)
‣実はヘルパー関数専用•ヘルパー関数内部から呼び出し元の @_buf をゴニョゴニョする必要がある
‣汎用の解決策は難しい
50
copyright(c) 2009 kuwata-lab.com all rights reserved.
まとめ
‣ eRubyにはこんなにも多くの問題点が!(たかがeRubyのくせに!)•余分な改行が出力されてしまう•ローカル変数が変更されてしまう•どれがコンテキスト引数なのかわかりにくい•変換コストと構文解析コストが大きい•デフォルトでHTMLエスケープしたい•文法エラーが見つけにくい•ブロックつきメソッド呼出しがうまく扱えない
51
copyright(c) 2009 kuwata-lab.com all rights reserved.
Part 3. テンプレートシステムの将来
52
copyright(c) 2009 kuwata-lab.com all rights reserved.
テンプレートとプログラミング
‣テンプレートもコードである<ul><% for x in @a %> <li><%=x%></li><% end %></ul>
print "<ul>\n"for x in @aprint "<li>#{x}</li>\n"endprint "</u>\n"
等価
プログラミングの各種概念がテンプレートにも適用できる?
53
copyright(c) 2009 kuwata-lab.com all rights reserved.
テンプレートとメソッド
<ul> <li><%=x%></li></ul>
テンプレートは一種のメソッド定義
s = File.read('foo.eruby')e = Erubis::Eruby.new(s)puts e.evaluate(:x=>1)
テンプレートの実行は一種のメソッド呼出し
コンテキストデータはメソッドの実引数
54
copyright(c) 2009 kuwata-lab.com all rights reserved.
テンプレートと仮引数
‣テンプレートにも仮引数宣言が必要?
<%#ARGS: items, name='guest' %>Hello <%= name %>!<% for x in items %> <li><%=x%></li><% end %>
・コンテキスト変数が一目瞭然・デフォルト値が指定可能
55
copyright(c) 2009 kuwata-lab.com all rights reserved.
テンプレートとモジュール化
‣HTMLも単一巨大から多数細小へ•メソッド定義と同じ原則
<html> <body> <h1><%=@title%></h1> <ul id="menulist"> <% for x in @items %> <li><%=x%></li> <% end %> </ul> </body></html>
<html> <body> </body></html>
<h1><%=@title%></h1> <ul id="menulist"> </ul>
<% for x in @items %> <li><%= x %></li> <% end %>
デザイナにも有益!(なはず)
分割
56
copyright(c) 2009 kuwata-lab.com all rights reserved.
テンプレートとオブジェクト指向
‣Djangoにおけるテンプレートの継承
....{% block pagetitle %}<h1>{{title}}</h1>{% endblock %}....
親テンプレート子テンプレートで・上書きする・前後に追加するなどが可能(method override)
57
copyright(c) 2009 kuwata-lab.com all rights reserved.
テンプレートとAOP
‣AOP : Aspect Oriented Programming•あるコードに別のコードを差し込む/重ねる
<table>
<tr>
<td>
</tr>
</table>
"for x in @a"
"end"
"print x"
•HTMLとプレゼンテーションロジックが分離可能
•同じロジックを複数箇所に挿入できる(重複を回避)
58
copyright(c) 2009 kuwata-lab.com all rights reserved.
テンプレートとデータ型
‣HTMLエスケープをView層で行なうのは限界•エスケープし忘れ、ヘルパー関数の引数、…
‣HTMLとStringは異なる型を持つべき?(http://www.oiwa.jp/~yutaka/tdiary/20051229.html)•エスケープする/しないを意識しなくてすむ•似てるけど違う型の例:Pythonのstrとunicode •「HTML+String」の演算結果は何型?•考慮すべきはHTMLだけではない(SQLなど)
59
copyright(c) 2009 kuwata-lab.com all rights reserved.
まとめ
‣テンプレートもコードである‣プログラミングの各種概念がテンプレートシステムにも適用可能•仮引数、継承、モジュール化、AOP、…
‣テンプレートシステムは未だ発展途上
60
copyright(c) 2009 kuwata-lab.com all rights reserved.
参考資料
‣テンプレートシステム入門(歴史編)• http://jp.rubyist.net/magazine/?0024-TemplateSystem
‣テンプレートシステム入門(基礎編)• http://jp.rubyist.net/magazine/?0024-TemplateSystem2
‣Erubis• http://www.kuwata-lab.com/erubis/
‣テンプレートエンジンベンチマーク• http://www.kuwata-lab.com/tenjin/
61
copyright(c) 2009 kuwata-lab.com all rights reserved.
one more thing
62
copyright(c) 2009 kuwata-lab.com all rights reserved.
Tenjin - template engine replacing eRuby
‣ERBもErubisも時代遅れ• eRubyなんて所詮テキストプロセッサ•テンプレートエンジンとしては機能が貧弱
‣Tenjin : 高速高機能なテンプレートエンジン•最初からテンプレートエンジンとして設計/実装•テンプレートエンジンに必要な機能を標準装備
- レイアウトテンプレート、部分テンプレート、...
• http://www.kuwata-lab.com/tenjin/
63
copyright(c) 2009 kuwata-lab.com all rights reserved.
thank you
64