Ruby で書かれたタグベースのテンプレートエンジン Radius

Written by @dr_taka_n at 2009/06/08 06:04 [, ]

Radius は、Ruby で書かれたタグを使用したテンプレート言語。

Radiant に採用されており、独自の振る舞いをタグとして定義し、コンテンツの描画を HTML だけでなく、定義された Radius タグを使用して拡張することができる。

ここでは、その Radius の仕組みの一部をメモしておく。

Radius は以下の GitHub にてメンテされている。

その使用方法については、Quick Start を見ることで確認できる。

タグを定義する

Radius によるタグの生成からパーシング処理の流れは以下の通り。

  • Radius::Context のコンテキストオブジェクトを生成する
  • コンテキストオブジェクトの Radius::Context#define_tag メソッドにてタグを定義する
  • そのタグの中で保持しておきたい(ローカルな)情報は、ブロック引数の tag(Radius::TagBinding) インスタンスの locals インスタンス変数に保持する。 (入れ子になるタグで使用したい情報を保持する)
  • コンテキストオブジェクトをパーサオブジェクト Radius::Parser に預ける
  • パーサに描画したいテンプレート渡し、先に渡していたコンテキストにそってパースしてもらう
require "rubygems"
require "radius"

context = Radius::Context.new
context.define_tag "hello" do |tag|
  "Hello #{tag.attr['name'] || 'world'}!"
end

parser = Radius::Parser.new(context, :tag_prefix => 'r')
puts parser.parse('<p><r:hello /></p>')
puts parser.parse('<p><r:hello name="john" /></p>')

# >> <p>Hello world!</p>
# >> <p>Hello john!</p>

とてもお手軽だ。

コンテナタグ

コンテナタグとは、開始タグと終了タグの間に他のタグやコンテンツを含めたタグのこと。

Quick Start の例では、Textitle を使用したコンテナタグをとりあげている。

先程のソースに、以下のタグを定義する。

require "redcloth"

context.define_tag "textitle" do |tag|
  contents = tag.expand
  RedCloth.new(contents).to_html
end

出力は以下の通りとなる。

puts parser.parse('<r:textitle>h1. Hello **World**!</r:textitle>')

# >> <h1>Hello <b>World</b>!</h1>

tag.expand は開始タグと終了タグの間にある部分を返す。上記例では、”h1. Hello **World**!” が返されることになる。 次に取り上げるネストされたタグに触れることになるが、タグに含められるのは文字列だけではなく、Radius タグ自身も含めることができる。

ネストされたタグ

タグはネストできる。QUICKSTART jlong’s radius - GitHub にある次の例を見てみる。

require "rubygems"
require "radius"

context = Radius::Context.new

context.define_tag "stooge" do |tag|
  content = ''
  ["Larry", "Moe", "curly"].each do |name|
    tag.locals.name = name
    content << tag.expand
  end
  content
end

context.define_tag "stooge:name" do |tag|
  tag.locals.name
end

parse = Radius::Parser.new(context, :tag_prefix => 'r')

template = <<-TEMPLATE
<ul>
<r:stooge>
  <li><r:name /></li>
</r:stooge>
</ul>
TEMPLATE

puts parse.parse(template)

# >> <ul>
# >> 
# >>   <li>Larry</li>
# >> 
# >>   <li>Moe</li>
# >> 
# >>   <li>curly</li>
# >> 
# >> </ul>

ネストされたタグ固有の部分について、

  • テンプレートの表現を見ると、<r:stooge> タグに <r:name> タグを含む、Radius タグの中に Radius タグを含む構造となっている。
  • <r:stooge> タグの定義の中で、”Larry”、”Moe”、”curly”という文字列配列をイテレートし、それぞれの処理の中で、tag.locals.name への値の設定、tag.expand の呼び出し、そしてその結果の content への文字列追加を行っている。
  • ネストされた Radius タグの呼び出しは、開始タグと終了タグの間にある文字列を返す tag.expand(Radius::TagBinding#expand) をそのまま使うようだ。その文字列に Radius タグが含まれていた場合には、それを評価してくれるようだ。
  • Radius タグのネスト構造の表現は、Radius::Context#define_tag のタグ名の指定に stooge:name とあるように、: をセパレータとして記述するようだ。
  • そのネストされたタグを定義している stooge:name は、tag.locals.name の値を返すだけとなっている。

というところが見てとれる。

上記のコードだけでは、今一ピンとこず、上記の動作原理を理解するためには、Radius の実装を見てみる必要があった。

Radius::Context#define_tag は引数のタグ名称とコードブロックを Radius::Context インスタンス自身のインスタンス変数(@difinitions:Hash)にタグ名をキー、コードブロックを値として保持する。で、タグがレンダリングされる際に自身が保持している情報をコードブロックの tag パラメータを仲介役として実行してもらう。

この仲介役となる tag パラメータの実体は、Radius::TagBinding のインスタンスになる。

Radius::TagBinding は、そのタグ内に保有したいローカルなオブジェクトを保持するために、@locals インスタンス変数を持っており、上記のコードで出てくるtag.locals.name = という表記がそのインスタンス変数への値のセットとなっている。 また、入れ子となったタグの中では、tag.locals.name でセットされたオブジェクトに対してアクセスが可能となる。

この @locals のインスタンス変数の実体は、Radius::DelegatingOpenStruct のインスタンスで、これは Struct クラスのようなものだ。上記構文のように、そのタグ内にて保持しておきたい値がある場合には、tag.locals.name = name のような感じでオジェクトを保持できる。この例では、name を使っているが、Radius::DelegatingOpenStruct は、内部で method_missing を使用して、任意の名称のアクセサに対応できるようにしているので、name である必要はなく、適切な名称を任意に使用できる。

Radius::TagBinding#expand は、自身のタグ内の値を評価するメソッドであり、入れ子のタグを含まない場合は空文字を返す(開始タグと終了タグの間に Radius タグでない文字列が含まれている場合はその値をそのまま返す)。入れ子のタグを含む(コンテナタグ)であれば、その入れ子になったタグを評価する。これにより、ネストされたタグ、上記の例では、<r:name />stooge:name の名前で定義したコードブロックを評価する。

Proc オブジェクトを使用したストラテジパターンの適用例として、とても参考になる作りだ。

Radiant の中での Radius

Radius のコンテキストである Radius::Context は Radiant の PageContext model (AR は継承していない) に継承されている。 定義されたタグは、PageContext の initialize 処理にて登録される。

パーサ Radius::Parser は、Radiant の全てのページのベースとなる Page model (こちらは AR を継承) に抱えられている。

参考サイト

本家の Quick Start。ここでは下記の一部のみを記載した。

このメモを書いている途中に上記 Quick Start を翻訳しているサイトを見つけた。。

  • [Radiantで使用されているテンプレート言語Radius AIRS Labs](http://labs.airs.co.jp/2007/9/3/radius_quick_start)
blog comments powered by Disqus