テーブル出力の略記法

ここでは、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>


のclass属性が、1行おきに変化しているのは、セルラインでなく、Eachというフィルターの機能です。

      <<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)

これをw3mレンダリングすると以下のようになります。

┌────┬───────┬──┐
│        │   Average    │Red │
│        ├───┬───┤eyes│
│        │height│weight│    │
├────┼───┼───┼──┤
│ Males  │1.9   │0.003 │40% │
├────┼───┼───┼──┤
│Females │1.7   │0.002 │43% │
└────┴───┴───┴──┘

フック

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検討中)

属性の設定

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は 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

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

このようにすると、<>は、あたかも最初に list = [10, 20, 30] というデータが与えられていたかのような動きをして、上記の結果になります。

この機能を利用して、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であれば、そのブロック全体を出力しないという意味です。

「<<{...}< .... >>>」は、そのブロック内のコンテキストデータとして、新しく生成したハッシュを使用するという意味です。

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形式のドキュメントを使用することもできます。

Amrita2の状況

Amrita2の開発は、昨年半ばから中断していましたが、年末年始ちょっとヒラメキがあって大幅に進展しました。

しかし、これまでにAmrita2という名前で何度かの試行錯誤を続けているので、次にリリースする時は、中途半端なものを公開したくありません。何らかの実績を作ってからリリースとしたいのです。

そこで、現状のものを使って、自分でRailsのアプリをいくつか開発した上で、いきなりV2.0の完成版としてリリースしようと決めています。

それまでの間、このブログに[Amrita2/2007]というカテゴリで、チュートリアルや機能の紹介(ドキュメントの下書き)を書いていきます。

最新ソースは、ここに置いておきます。

品質的にはある程度は使えるものになっていると思いますが、リリースまでにAPIを変更する可能性がかなり高いので、その点をご了承の上で、試してみてください。

ただし、何らかのプロジェクトに使ってみたい(人柱になってもいい)という方がいたら、密に連絡を取って相談しながら、開発したものが無駄にならないように開発を進めようとも考えています。その時はまずメール下さい。