Amrita2の状況
Amrita2の開発は、昨年半ばから中断していましたが、年末年始ちょっとヒラメキがあって大幅に進展しました。
しかし、これまでにAmrita2という名前で何度かの試行錯誤を続けているので、次にリリースする時は、中途半端なものを公開したくありません。何らかの実績を作ってからリリースとしたいのです。
そこで、現状のものを使って、自分でRailsのアプリをいくつか開発した上で、いきなりV2.0の完成版としてリリースしようと決めています。
それまでの間、このブログに[Amrita2/2007]というカテゴリで、チュートリアルや機能の紹介(ドキュメントの下書き)を書いていきます。
最新ソースは、ここに置いておきます。
品質的にはある程度は使えるものになっていると思いますが、リリースまでにAPIを変更する可能性がかなり高いので、その点をご了承の上で、試してみてください。
ただし、何らかのプロジェクトに使ってみたい(人柱になってもいい)という方がいたら、密に連絡を取って相談しながら、開発したものが無駄にならないように開発を進めようとも考えています。その時はまずメール下さい。
AmXML
Amrita2は、AmritaV1.Xと同じくXMLドキュメントに対して、データを展開していくテンプレートエンジンです。
Amrita2では、idではなく、am:srcという属性をキーとして使用します。
# テンプレート t = Amrita2::Template.new <<-END <html> <head> <title am:src="page_title" /> </head> <body> <h1 am:src="header_title" /> <p class="text" am:src="text"> <span am:src="template" /> is a html template library for <span am:src="lang" /> </p> </body> </html> END # データ data = { :page_title=>'Amrita2', :header_title=>'Hello, Amrita2', :text=>{ :template => 'Amrita2', :lang => 'Ruby' } } # 結果 expected = <<-END <html> <head> <title>Amrita2</title> </head> <body> <h1>Hello, Amrita2</h1> <p class="text">Amrita2 is a html template library for Ruby</p> </body> </html> END # 結果の出力 puts t.render_with(data) # 結果の確認 t.render_with(data).should_be_samexml_as(expected)
XML整形式のテキストを与え、Amrita2::Templateオブジェクトを生成し、render_with というメソッドにデータを渡すと、そのデータを埋めこんだ結果を文字列として返却します。(API変更の可能性有り)
サンプルの形式について
should_be_samexml_asは、RSpecに追加したメソッドで、二つの文字列がXMLとして一致しているかテストするものです。
"<a class='aaa' href='bbb'> xxxxx </a>".should_be_samexml_as("<a href='bbb' class='aaa'>xxxx</a>") #->OK "<a class='aaa' href='bbb'> zzzzz </a>".should_be_samexml_as("<a href='bbb' class='aaa'>xxxx</a>") #->NG
このように属性の順番やタグの間のスペースを無視して、両者の一致を比較します。このチュートリアルでは、サンプルコードの結果は、このメソッドを使って実行可能な形で示します。アーカイブの specs/intro.rb には、このチュートリアルで示すサンプルとほぼ同じコードがありますので、そちらも参照してください。
AmXML
テンプレートをXMLで書くのは冗長なので、Amrita2では、AmXMLという専用の記述形式をサポートしています。上記のサンプルをAmXMLで書くと以下のようになります。
t2 = Amrita2::Template.new <<-END <<html< <<head< <<title:page_title>> >>> <<body< <<h1:header_title>> <<p class="text":text< <<:template>> is a html template library for <<:lang>> >>> >>> >>> END
AmXMLは、内部的にはプリプロセッサとして実装されており、XMLに変換してから、REXMLで読みこんでいます。ですから、AmXMLの記述には、全て、対応するXMLの記述があります。ですから、Amrita2では、テンプレートとしてXML形式のドキュメントを使用することもできます。
erbサポート
Amrita2は、eRubyをサポートしています。
include Amrita2::Runtime specify "simple erb" do t = Amrita2::Template.new <<-'END' <%= 1 + 2 %> END t.render_with(binding).should_be == " 3 " end
eRubyを使用する時は、render_withにBindingオブジェクトを渡してください。Bindingオブジェクトが渡された場合は、対応する変数をテンプレート内に展開していきます。
コンテキストデータ
eRubyの処理内から、$_という変数によって、Amrita2の展開しているデータを参照することができます。
specify "erb using model data" do t = Amrita2::Template.new <<-'END' <<ul< <<:list< <li><%= $_ * 10 %></li> >>> >>> END list = [1, 2, 3] t.render_with(binding).should_be_samexml_as <<-END <ul> <li>10</li> <li>20</li> <li>30</li> </ul> END end
この例では、<<:list< の時点で、ローカル変数の listの内容が取り出されます。listは配列なので、このタグが閉じるまでの内容は、配列要素の数だけ、繰り返し展開されます。そして、その中にある<%= ... %> に来た時点では、$_ には、配列要素が設定されています。
たとえば、最初のループの時には、$_には配列の最初の要素である「1」が設定されています。これを利用して
<li><%= $_ * 10 %></li>
を展開すると、結果は
<li>10</li>
となります。これを三回繰り返して、上記の結果が得られます。
この$_のことを、Amrita2では、「コンテキストデータ」と呼びます。
eRuby内でコンテキストデータを設定する
eRubyの処理の中で、$_ の内容を設定することができます。
specify "erb providing model data" do t = Amrita2::Template.new <<-'END' <<ul< <% list = (1..3).collect { |n| n*10 } %> <<li:list>> >>> END t.render_with(binding).should_be_samexml_as <<-END <ul> <li>10</li> <li>20</li> <li>30</li> </ul> END end
このようにすると、<
この機能を利用して、eRubyでデータを作り、Amrita2形式で指定したXMLテンプレートの中にそのデータを展開することができます。
eRubyでコンテキストデータを変更する
specify "erb filtering model data" do t = Amrita2::Template.new <<-'END' <<ul< <<li:list< <% $_[:no_times10] = $_[:no] * 10 %> <<:no>> * 10 = <<:no_times10>> >>> >>> END list = (1..3).collect do |n| { :no => n } end t.render_with(binding).should_be_samexml_as <<-END <ul> <li>1 * 10 = 10</li> <li>2 * 10 = 20</li> <li>3 * 10 = 30</li> </ul> END end
この場合は、コンテキストデータとして渡されるのは、ハッシュの配列です。外側では、:no に対応する要素だけを渡して、テンプレートの内部のeRubyで、それに対応した、:no_times10のデータを作成してハッシュの別の要素として設定しています。その結果の二つの要素を持つハッシュが、計3回
<<:no>> * 10 = <<:no_times10>>
という所に渡されます。その結果、上記の出力が得られます。
AmXMLでeRubyを記述する
AmXMLには、このようなeRubyの記述を簡単にする機能も含まれています。
specify "erb filtering model data with amxml" do t = Amrita2::Template.new <<-'END' <<ul< <<:list < <<li ?[($_[:no]%2) != 0 ] < <<{ :no => $_[:no], :no_times10 => $_[:no] * 10 } < <<:no>> * 10 = <<:no_times10>> >>> >>> >>> >>> END list = (1..3).collect do |n| { :no => n } end t.render_with(binding).should_be_samexml_as <<-END <ul> <li>1 * 10 = 10</li> <li>3 * 10 = 30</li> </ul> END end
「<< ?[.....] < .... >>>」 は、?[]内の式をRubyの式として評価して、その結果がnilかfalseであれば、そのブロック全体を出力しないという意味です。
「<<{...}< .... >>>」は、そのブロック内のコンテキストデータとして、新しく生成したハッシュを使用するという意味です。
フィルター
Amrita2は filterという機能によって、与えられたデータを変換して出力することができます。
specify "Format" do t = Amrita2::Template.new <<-END <<:a|Format['(%-4.2f)']>> END t.render_with(:a=>1234.56).should_be_samexml_as('(1234.56)') end
Formatが標準で用意されているフィルターの一つで、これによってデータの出力形式を指定します。
フィルターは、次のようにいくつでも連結することができます。
specify "Default and Format" do t = Amrita2::Template.new <<-END <<div< <<span class='number':a|Default[0.0]|Format['(%-4.2f)']>> >>> END t.render_with(:a=>[1234.56,nil,-7890]).should_be_samexml_as <<-END <div> <span class='number'>(1234.56)</span> <span class='number'>(0.00)</span> <span class='number'>(-7890.00)</span> </div> END end
属性の設定
Amrita2には、属性を設定する方法がいくつか用意されています。
以下のデータを使って、これを説明していきます。
setup do @data = { :mail=>{ :no=>142990, :title=>"[ANN] Amrita2 1.9.5" } } @expected = <<-EOF <a href='http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/142990'>[ruby-talk:142990][ANN] Amrita2 1.9.5</a> EOF end
@dataがテンプレート内に埋めこむべきデータで、@expectedが求める出力結果です。
NVar
NVar(Numbered Variables)というフィルターを使うと、そのブロック内部の$1,$2,$3..という文字列を、指定したデータに置き換えます。
specify "NVar" do t = Amrita2::Template.new <<-END <<a href="http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/$1" :mail|NVar[:no,:title] < [ruby-talk:$1]$2 >>> END t.render_with(@data).should_be_samexml_as(@expected) end
Attr
Attrというフィルターを使うと、ハッシュから指定した属性を展開します。
specify "Attr with erb" do t = Amrita2::Template.new <<-'END' <% mail = @data[:mail] mail[:href] = "http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/#{mail[:no]}" mail[:body] = "[ruby-talk:#{mail[:no]}]#{mail[:title]}" %> <<a :mail|Attr[:href]>> END t.render_with(binding).should_be_samexml_as(@expected) end
:bodyという要素は、そのXML要素のテキスト部分に展開されます。
フック
Amrita2では、テンプレートの展開方法は、与えたデータによって制御されます。
しかし、必要なロジックが複雑である場合、そのロジックをデータの形式によって表現することが難しい場合や不可能な場合があります。
その場合には、「フック」という機能を使用します。
specify "Hook" do t = Amrita2::Template.new <<-'END' <<:mail_hook|AcceptData[:hook] < <<a :m|Attr[:href]>> >>> END mail = @data[:mail] hook1 = Amrita2::Core::Hook.new do href = "http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/#{mail[:no]}" body = "[ruby-talk:#{mail[:no]}]#{mail[:title]}" render_child(:m, :href=>href, :body=>body) end #t.set_trace(STDOUT) t.render_with(:mail_hook=>hook1).should_be_samexml_as(@expected) end
このオブジェクトを渡すと、そのデータを展開する時に、フックメソッド(Hookオブジェクトの生成時に渡したブロック)が呼ばれます。
フックメソッドは、必要なデータを作成してから、render_childというメソッドを呼びます。
これによって、指定したXMLの子要素が、その位置に展開されます。
render_childを複数回呼んでもいいし、全く呼ばないでフックメソッドを終了してもいいので、これによって、展開の方法を細かく制御することができます。
また、フックメソッド内で、テンプレートを使わず直接結果の文字列を出力することや、該当するXML要素をREXMLのオブジェクトとしてアクセス参照することもできます。(API検討中)
テーブル出力の略記法
ここでは、AmXMLのテーブル関係の機能を説明しますが、その前に、ハッシュと配列以外のオブジェクトの扱いについて説明します。
ハッシュ以外のデータの扱い
以下のテーブル出力のサンプルでは、以下のデータを使用します。
context "table" do WebSite = Struct.new(:name, :url) class Lang include Amrita2::DictionaryData attr_reader :name, :author, :website def initialize(n, a, w) @name, @author, @website = n, a, w end end setup do @data = { :lang_list =>[ Lang.new("Ruby", "matz", WebSite.new('Ruby Home Page', 'http://www.ruby-lang.org/')), Lang.new("Perl", "Larry Wall", WebSite.new('The Source for Perl', 'http://perl.com/')), Lang.new("Python","Guido van Rossum", WebSite.new('Python Programing Language', 'http://www.python.org/')) ] } end
Amrita2では、Structもハッシュと同様にモデルデータとして使用できます。
また、目印としてAmrita2::DictionaryDataというモジュールを include することで、他の任意のオブジェクトをハッシュの代わりに使用することができます。
この場合は、データの取り出しは、対応する名前のメソッド呼び出しになります。
AmXMLのセルライン
AmXMLでは、<<...>> <<<...>>>に加えて、|で区切られた行から、td要素を生成する記述方式があります。
行の先頭が、「|」である行を「セルライン」と呼び、AmXMLプリプロセッサは、この行に対して特殊な処理を行ないます。
specify "simple" do t = Amrita2::Template.new <<-END <<table border='1'< <<tr:lang_list < ||| <<:name>> | <<:author>>| >>> >>> END t.render_with(@data).should_be_samexml_as <<-END <table border = "1"> <tr><td>Ruby</td><td>matz</td></tr> <tr><td>Perl</td><td>Larry Wall</td></tr> <tr><td>Python</td><td>Guido van Rossum</td></tr> </table> END end
一番単純な例では、上記のように、td要素を出力します。
th要素を出力したり、属性を設定することもできます。
specify "with title and attributes" do t = Amrita2::Template.new <<-END <<table border='1'< <<tr< #------------------------------------------- | || name || author ||cite || |class|| h_name || h_author ||h_cite || #------------------------------------------- >>> <<tr:lang_list |ToHash[:name=>:name, :author=>:author, :site=>:website] |Each[:class=>["odd", "even"]] |Attr[:class] < #------------------------------------------------------------------------------ | || <<:name>> | <<:author>>|<<a:site\\|Attr[:href=>:url, :body=>:name]>>| |class|| name | author |site | #------------------------------------------------------------------------------ >>> >>> END #t.set_trace(STDOUT) t.render_with(@data).should_be_samexml_as <<-END <table border='1'> <tr> <th class='h_name'>name</th> <th class='h_author'>author</th> <th class='h_cite'>cite</th> </tr> <tr class='odd'> <td class='name'>Ruby</td> <td class='author'>matz</td> <td class='site'> <a href='http://www.ruby-lang.org/'>Ruby Home Page</a> </td> </tr> <tr class='even'> <td class='name'>Perl</td> <td class='author'>Larry Wall</td> <td class='site'> <a href='http://perl.com/'>The Source for Perl</a> </td> </tr> <tr class='odd'> <td class='name'>Python</td> <td class='author'>Guido van Rossum</td> <td class='site'> <a href='http://www.python.org/'>Python Programing Language</a> </td> </tr> </table> END end
「|属性名||」という行があると、その行の値を属性として設定します。また、セルの右側が、「||」であるセルは、th要素として出力されます。
#------------------------------------------- | || name || author ||cite || |class|| h_name || h_author ||h_cite || #-------------------------------------------
という記述が、次のように展開されます。(#..はコメント行)
<th class='h_name'>name</th> <th class='h_author'>author</th> <th class='h_cite'>cite</th>
<<tr:lang_list |ToHash[:name=>:name, :author=>:author, :site=>:website] |Each[:class=>["odd", "even"]] |Attr[:class] <
Each[:class=>["odd", "even"]] によって、コンテキストデータの:class要素に"odd"と"even"がかわりばんこに設定され、それがAttrによって展開されます。
この機能により、clospanやrowspanを使う場合等も、区切りの位置を工夫することで、出力結果に近い形式でテンプレートを記述することができます。
specify 'rowspan colspan' do # from http://www.y-adagio.com/public/standards/tr_html4/struct/tables.html =begin A test table with merged cells /-----------------------------------------\ | | Average | Red | | |-------------------| eyes | | | height | weight | | |-----------------------------------------| | Males | 1.9 | 0.003 | 40% | |-----------------------------------------| | Females | 1.7 | 0.002 | 43% | \-----------------------------------------/ =end expected = <<-END <table border="1" summary="This table gives some statistics about fruit flies: average height and weight, and percentage with red eyes (for both males and females)."> <caption><em>A test table with merged cells</em></caption> <tr> <th rowspan="2" /> <th colspan="2">Average</th> <th rowspan="2">Red<br/>eyes</th> </tr> <tr> <th>height</th> <th>weight</th> </tr> <tr> <th>Males</th> <td>1.9</td> <td>0.003</td> <td>40%</td> </tr> <tr> <th>Females</th> <td>1.7</td> <td>0.002</td> <td>43%</td> </tr> </table> END t = Amrita2::Template.new <<-END <<table border='1':|Attr[:summary]< <<caption< <<em:caption>> >>> <<tr< #------------------------------------------------------ | rowspan || 2 || || 2 || | colspan || || 2 || || | || ||Average ||Red<br />eyes|| >>> <<tr< | || || || | ||height ||weight|| | || || || #------------------------------------------------------ >>> <<tr:data< | ||<<:sex>>||<<:height>> | | || || |<<:weight\\|Format['%1.3f']>>| | || || | |<<:percent\\|Format['%d%%']>>| >>> >>> END data = { :summary=>"This table gives some statistics about fruit flies: average height and weight, and percentage with red eyes (for both males and females).", :caption=>'A test table with merged cells', :data => [ { :sex=>'Males' , :height=>1.9, :weight=>0.003, :percent=>40 }, { :sex=>'Females' , :height=>1.7, :weight=>0.002, :percent=>43 } ] } t.render_with(data).should_be_samexml_as(expected)
┌────┬───────┬──┐ │ │ Average │Red │ │ ├───┬───┤eyes│ │ │height│weight│ │ ├────┼───┼───┼──┤ │ Males │1.9 │0.003 │40% │ ├────┼───┼───┼──┤ │Females │1.7 │0.002 │43% │ └────┴───┴───┴──┘