Skip to content

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

Posted on:2009年6月8日 at 06:04

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

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

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

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

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

Table of Contents

Open Table of Contents

タグを定義する

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

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>

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

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

上記のコードだけでは、今一ピンとこず、上記の動作原理を理解するためには、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 を翻訳しているサイトを見つけた。。