Tour_ja
Path: docs/Tour_ja
Modified: Wed Nov 06 15:02:26 JST 2002

Amrita ツアー

HTML要素の属性を変更する

href="..." のような属性の値を変更する方法を説明します。

コード:

  require "amrita/template"
  include Amrita

  tmpl = TemplateText.new <<END
  <table border="1">
    <tr><th>name</th><th>author</th><th>webpage</tr>
    <tr id=table1>
      <td id="name"></td>
      <td id="author"></td>
      <td><a id="webpage"></a></td>
    </tr>
  </table>
  END
  data = {
     :table1=>[
      {
        :name=>"Ruby",
        :author=>"matz" ,
        :webpage=> a(:href=>"http://www.ruby-lang.org/") { "Ruby Home Page" },
      },
      {
        :name=>"perl",
        :author=>"Larry Wall" ,
        :webpage=> a(:href=>"http://www.perl.com/") { "Perl.com" },
      },
      {
        :name=>"python",
        :author=>"Guido van Rossum" ,
        :webpage=> a(:href=>"http://www.python.org/") { "Python Language Website" },
      },
     ]
  }
  tmpl.prettyprint = true
  tmpl.use_compiler = true
  tmpl.expand(STDOUT, data)

出力:

   <table border="1">
     <tr>
     <th>name</th>
     <th>author</th>
     <th>webpage</th>
     </tr>
     <tr>
     <td>Ruby</td>
     <td>matz</td>
     <td><a href="http://www.ruby-lang.org/">Ruby Home Page</a></td>
     </tr>
     <tr>
     <td>perl</td>
     <td>Larry Wall</td>
     <td><a href="http://www.perl.com/">Perl.com</a></td>
     </tr>
     <tr>
     <td>python</td>
     <td>Guido van Rossum</td>
     <td><a href="http://www.python.org/">Python Language Website</a></td>
     </tr>
   </table>

説明

Amrita#a() というメソッドは Amrita::AttrArray という特別なオブジェクトを生成します。

    a(:href=>"http://www.ruby-lang.org/") { "Ruby Home Page" },

このオブジェクトをモデルデータとして使用すると、HTML要素の属性が変更されます。 例えば、次のようなテンプレートにこのデータを与えたとすると

    <td><a id="webpage"></a></td>

出力は次のようになります。

    <td><a href="http://www.ruby-lang.org/">Ruby Home Page</a></td>

docs/XML_ja で説明している filelist.rb というサンプルもAttrArrayを使用しています。

なお、属性の展開は別の方法もあります。詳しくは docs/Tour2の expand_attr を参照してください。


Procオブジェクト

モデルデータとid属性のマッチングというamrita独特の方法は、 シンプルで見通しのよいコードを可能にします。

しかし、そのような方法ではきれいに処理しきれないケースも稀には存在します。 Proc オブジェクトをモデルデータとして与えると、 手続き的にテンプレートを変更することができます。

コードと出力

コード:

  require "amrita/template"
  include Amrita

  tmpl = TemplateText.new <<END
  <ul>
    <li id=list><font id=data></font>
  </ul>
  END
  languages = %w(java Ruby perl python c++ c sml cobol fortran ada lisp)
  i = 0
  data = {
    :list => languages.collect do |l|
      {
        :data => proc do |elem|
          if l == "Ruby" # Ruby is special language to me!
            # use Amrita::Element's methods to edit
            elem[:color] = "red"
            elem[:size] = "big"
            elem.set_text("I love Ruby!")
            # e() is Amrita's method that generates Element
            e(:em) { elem }
          else
            i = i + 1 # i is shared by all procs
            elem[:color] = i%2 == 0 ? "blue" : "black"
            elem.set_text(l)
            elem
          end
        end
      }
    end
  }

  tmpl.prettyprint = true
  tmpl.expand(STDOUT, data)

出力:

   <ul>
     <li><font color="black">java</font> </li>
     <li><em><font color="red" size="big">I love Ruby!</font></em> </li>
     <li><font color="blue">perl</font> </li>
     ...
   </ul>

説明

モデルデータとして Proc オブジェクトが渡されると、amrita は、 テンプレート展開時に、そのProc を呼び出します。 その際、パラメータとして、対応する id のついたHTML要素を Amrita::Element オブジェクトとして 渡します。 そして、その Proc の結果とテンプレートを置き換えます。

この Proc の中で、次のようなメソッドを利用して自由にテンプレートを編集することができます。

属性値の設定

     elem[:color] = "red"

要素にテキストを設定する

     elem.set_text("I love Ruby!")

Amrita#e メソッドによって、新しいHTML要素を生成する

     e(:em) { elem }

既存のクラスをモデルデータとして使用する

HashやArrayだけでなく、 既存のクラス(Rubyの標準クラスやユーザ作成のクラス)のオブジェクトを そのまま、amrita のモデルデータとして使用する例です。

コードと出力

コード:

  require "amrita/template"
  include Amrita

  tmpl = TemplateText.new <<END
  <span id="time">
    <span id="year"></span>/<span id="month"></span>/<span id="day"></span>
  </span>
  END
  t = Time.now
  t.extend Amrita::ExpandByMember

  data = { :time=>t }
  tmpl.compact_space = true
  tmpl.expand(STDOUT, data)

出力:

  2002/7/17

説明

もし、モデルデータが、Amrita::ExpandByMember というモジュールをincludeしていたら、 amritaは id 属性の値をメソッド名と見なして、そのメソッドを呼び出します。

このサンプルでは、+:time+に対応するデータは、Rubyの標準のTimeオブジェクトですが、 ExpandByMember モジュールを extend しています。 それで id属性の値である year をメソッド名とみなし、amritaは t に対してそのメソッドの呼出しを行います。

その結果 <span id="year"></span> という部分は、 t.yearの結果 "2002" と展開され、他の部分も同様に処理されて 次のように展開されます。

  <span><span>2002</span>/<span>7</span>/<span>17</span></span>

amrita は 属性のない <span> 要素は削除しますので、最終的な出力は

  2002/7/17

となります。


プリコンパイル

amrita は HTML テンプレートを Ruby のコードにコンパイルすることができます。

コードと出力

コード(table.rbにコンパイラを利用するために追加した分) :

  tmpl = TemplateText.new(TEMPLATE)
  tmpl.use_compiler = true
  tmpl.set_hint_by_sample_data(data) # これを追加するとそのデータに最適化します
  tmpl.expand(STDOUT, data)  #
  puts "----code generated by Amrita -----------"
  puts tmpl.src
  puts "----code generated by Amrita end -------"

出力はtable.rbと同じですが、 コンパイラの出力したコードとベンチマークが追加されています。

私のCrusoe TM5600マシン(NEC Lavie MX)での出力は次のようになります。

  43.068354 seconds for 1000 times without compiling
  5.078764 seconds for 1000 times with pre-compiled code

説明

基本的には、次の処理を追加するだけでコンパイラを使用できます。

  tmpl.use_compiler = true

これ以降、expand はコンパイルされたRubyコードで実行されます。 prettyprintの機能はサポートされませんが、それ以外は同じ結果になります。

サンプルデータを利用して、最適化を行なうには次の処理を追加します。

And optionally give a sample data to amrita.

  tmpl.set_hint_by_sample_data(data)

amritaのコンパイラは、このデータを出力するRubyコードの最適化のために使用します。 従って、渡すモデルデータの構造が変化したら、再度、その新しいデータで set_hint_by_sample_data を呼ぶ必要があります。

amritaのコンパイラは、部分的にインタプリターモードを含めることができます。 部分的に構造が変化するデータに対して、コンパイラを利用する場合は、 サンプルデータの対応する部分(変化するデータの部分)に、nil を渡す必要があります。

コンパイラは、Element::expandを使用するようなコードを対応する個所に挿入します。

このようにして、スピードと柔軟性のトレードオフを自由に取ることができます。


サニタイジン -- XSS(クロスサイトスクリプティング)対策

amritaには、XSS対策として、Amrita::Sanitizer というモジュールが組込まれています。 Amrita::Formatter は自動的にこのモジュールを使用します。

I will provide interface to controle sanitizer through Amrita::Template in future release.

コードと出力

  require "amrita/template"
  include Amrita

  tmpl = TemplateText.new %q[<p id=body>xxx</p>]
  data = {
     :body=>"I want to insert new line.<br>But I can't"
  }

  tmpl.expand(STDOUT, data) # <p>I want to insert new line.&lt;br&gt;But I can't</p>
  puts
  data = {
      :body=>noescape { "I can insert new line <br>with escape { ... } <br>But it may be dangerous" }
  }

  tmpl.expand(STDOUT, data) # <p>I can insert new line <br>with escape { ... } <br>But it may be dangerous</p>
  puts
  data = {
      # The attacker expected amrita to print <p yyy=""></p>XSS attack<p>But amrita sanitize it!</p>
      :body=>a(:yyy=>%q["></p>XSS attack here<p]) { "But amrita sanitize it!" }
  }
  tmpl.expand(STDOUT, data) # <p yyy="&quot;&gt;&lt;/p&gt;XSS attack here&lt;p">But amrita sanitize it!</p>
  puts

  tmpl = TemplateText.new %q[<a id=body>href is treated in a special way</a>]
  data = {
      :body=>a(:href=>%q[javascript:alert('hello')])
  }
  tmpl.expand(STDOUT, data) # <a href="">href is treated in a special way</a>
  puts

説明

テキスト

xhtml/html 内のテキストとして危険な文字、(<>&) は自動的にエスケープされます。

    "<abc>" => "&lt;abc&gt;"

属性値

属性値として危険な文字(<>&"')は自動的にエスケープされます。

URL用属性の扱い

<a>要素のhref属性のように、URLを値として持つ属性値は、特別扱いされます。

どの属性値を特別扱いするかの詳細については tag.rb を参照してください。

これらの属性値は、次のようにさらに厳しくチェックされます。

  * これらの属性値はURLとして許されない文字を持つことはできません
  * これらの属性値は許されないスキーム(プロトコル指定)を持つことはできません

この条件に違反したら、属性値はnilで置きかえられて<a href="">....</a> のように表示されます。

次のようにsetup_taginfo メソッドを再定義することで、 どの属性をこのように扱うか(扱わないか)をカスタマイズすることができます。

    t = TemplateFile.new ...

    def t.setup_taginfo
      ret = TagInfo.new
      ret[:aaa].set_url_attr(:bbb)
      ret
    end

この場合は、 aaa要素のbbb属性は、URLとしてサニタイズされます。

サニタイズを無効にする

Amrita::SanitizedString オブジェクトをモデルデータに含めることで、 この機能を無効にすることができます。

    t = TemplateText.new '<p id="a">sample_text</p>'
    t.expand(STDOUT, { :a=>"<xxx>" })                  # => <p>&lt;xxx&gt;</p>
    t.expand(result, { :a=>SanitizedString["<xxx>"] }) # => <p><xxx></p>

この機能は、XSSについて理解した上で、充分注意して利用してください。

なお、もうひとつ同様の効果を得る方法として、escape {...} で モデルデータを囲むという方法もあります。