Amrita2のマクロテンプレート

Amrita2はだいぶ進んでいるけど、ドキュメントを書くヒマがありません。

そこで、面白そうな部分のRSpecのコードとごく簡単な説明をメモ程度にここに書いていきます。なお、以下のRSpecは全て動いてます。

まず、マクロテンプレートについて。

context "Macro can contain dynamic element" do
  class Hello2 < Amrita2::Macro::Base
    include Amrita2

    ElementName = "hello"
    TemplateText = <<-END.gsub(/\s+/m, ' ')
      <span macro:src="hello">
        <span macro:src="greeting" />, macro world!
        <span target_src="msg" />
      </span>
    END
  end

  specify " " do
    text = <<-END
    <span><hello greeting='Hello'/> </span>
    END
    t = Amrita2::TemplateText.new(text)
    t.add_macro(Hello2.new)
    t.test_with(:msg => "Enjoy it!") do |result|
      result.should_be_samexml_as "Hello, macro world! Enjoy it!  \n"
    end
  end
end

マクロテンプレートとは、テンプレートの一部を部品化する機能。Amrita2のマクロの特色は、

  1. マクロの定義も参照もwell-formed XMLで行なえる
  2. マクロの展開もテンプレートの展開も同じ、Amrita2のロジックで行なう

上記の例では、class Hello2がマクロの定義、テンプレート内の ...がマクロの参照。

macro:src属性のある要素は、テンプレート展開前に展開される。その後、展開されたテンプレートの中で、target_src属性のあるものが、データによって展開される。

もう少し実用的な例。

context "Amazon Link Macro" do
  class Amazon < Amrita2::Macro::Base
    include Amrita2
    ElementName = "book"
    TemplateText = <<-END
      <span>
        <a macro:src = "book|NVar[:isbn, :content]"
           target_src = "book|NVar[:$1, :$2]"
           href="http://www.amazon.co.jp/exec/obidos/ASIN/$$1/">
          $$2
        </a>
      </span>
    END
  end      

  specify " " do
    text = <<-END
    <div>
       <book isbn="isbn" content="title" />
    </div>
    END
#    
#   after macro expansion
#   <div><a href='http://www.amazon.co.jp/exec/obidos/ASIN/$1/'
#           am:src='book|NVar[:isbn_no, :title]'>$2</a></div>
#   
    t = Amrita2::TemplateText.new(text) do |e, name, filters|
      filters << Amrita2::Filters::MacroFilter[Amazon]
    end
    
    data = {
      :book=>{
        :isbn=>'4798013951',
        :title=>'Ruby on Rails for dummies'
      }
    }
    #t.set_trace(STDOUT)
    t.test_with(data) do |result|
      result.should_be_samexml_as <<-END
        <div>
          <a href="http://www.amazon.co.jp/exec/obidos/ASIN/4798013951/">Ruby on Rails for dummies</a>
        </div>
      END
    end
  end
end

NVar[:isbn_no, :title]というのは、フィルターの一つで、対応するデータを$1,$2 ...と置きかえるもの。

macro:src = "book|NVar[:isbn, :content]" によって、$1,$2が、テンプレート内の要素の属性値で置き換わる。

これによって、「target_src = "book|NVar[:$1, :$2]」という属性が、「am:src='book|NVar[:isbn_no, :title]」という属性に置き換わる。つまり

<book isbn="isbn" content="title" />

というマクロ参照が、マクロ展開によって

<a am:src='book|NVar[:isbn_no, :title]' href='http://www.amazon.co.jp/exec/obidos/ASIN/$1/'>$2</a>

に置き換わり、それにデータを与えると、テンプレート展開によって

<a href="http://www.amazon.co.jp/exec/obidos/ASIN/4798013951/">Ruby on Rails for dummies</a>

という形になる。

この、「マクロ展開」「テンプレート展開」という二つの段階が、共に同じAmrita2のロジックで行なわれる。

これをテーブル展開のマクロと組合せると次のようなことができる。

context "Table and Amazon" do
  specify " " do
    text = <<-END
      <m:table>
        <column header='No' column_id='no' />
        <column header='Title'>
          <contents><book isbn="isbn" content="title" /></contents>
        </column>
      </m:table>
    END
    t = Amrita2::TemplateText.new(text)
    t.add_macro(Table.new)
    t.add_macro(Amazon.new)

    #<table>
    #  <tr><th>No</th><th>Title</th></tr>
    #  <tr am:src='detail'>
    #    <td am:src='no'/>
    #    <td>
    #      <a href='http://www.amazon.co.jp/exec/obidos/ASIN/$1/' am:src='book|NVar[:isbn, :title]'>$2</a>
    #    </td>
    #  </tr>
    #</table>

    data = {
      :detail => [
        { :no=>'1', :book=>{ :isbn=>'isbn1', :title=>'b1'} },
        { :no=>'2', :book=>{ :isbn=>'isbn2', :title=>'b2'} },
        { :no=>'3', :book=>{ :isbn=>'isbn3', :title=>'b3'} },
      ]
    }
    t.test_with(data) do |result|
      #IO.popen("w3m -T text/html", "w") { |f| f.puts result}
      result.should_be_samexml_as <<-END
      <table>
        <tr><th>No</th><th>Title</th></tr>
        <tr>
          <td>1</td>
          <td>
            <a href='http://www.amazon.co.jp/exec/obidos/ASIN/isbn1/'>b1</a>
          </td>
        </tr>
        <tr>
          <td>2</td>
          <td>
            <a href='http://www.amazon.co.jp/exec/obidos/ASIN/isbn2/'>b2</a>
          </td>
        </tr>
        <tr>
          <td>3</td>
          <td>
            <a href='http://www.amazon.co.jp/exec/obidos/ASIN/isbn3/'>b3</a>
          </td>
        </tr>
      </table>
      END
    end
  end
end

テーブルのマクロ定義は、以下の通り。

  class Table < Amrita2::Macro::Base
    include Amrita2
    ElementName = "m:table"
    TemplateText = <<-END
      <table macro:src="table">
        <tr><th macro:src='header'/></tr>
        <tr target_src='detail'>
          <td macro:src="column|Attr[:target_src=>'column_id']">
            <span macro:src="contents" />
          </td>
        </tr>
      </table>
    END

    def preprocess_element(mt, element)
      data = element.as_amrita_dictionary(:use_child=>'column', :use_child_contents=>'//contents')

      data[:table][:header] = data[:table][:column].collect do |c|
        c[:header]
      end

      #p data
      #mt.set_trace(STDOUT)
      mt.render_with(data)
    end
  end

preprocess_elementというメソッドは、マクロ呼び出しのXMLを、Amrita2で処理できるデータに変換してから、マクロ展開を行なっている。ここはちょっと難しいかもしれないが、TemplateText内のHTMLを修正すれば(borderやclassの追加等)、それが、そのまま最終出力結果(このTableというマクロを参照している全ての処理)に反映される。

つまり、プログラマーでなくても、ある程度、マクロの(見た目の)調整が行なえる。