Radiantで使用されているテンプレート言語Radius

石上です。

業務でRadiusタグが使いたくなったので簡単に調べてみました。
Radius Quick Start を参考にまとめたものです。ただし、原文のコードのままでは動かない部分があったので少し修正しました。

タグの定義

テンプレートの中で使用するためのタグの定義は以下のように行います。

require 'rubygems'
require 'radius'

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

一度タグを作ってcontextを定義すると簡単にパーサーを作る事が出来ます。

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>

コンテナタグ(Container Tags)

Radiusはコンテナタグを定義することができます。コンテナタグとは開始タグと終了タグがあり、タグの間に他のタグやコンテンツを挟む事が出来ます。以下では、RedClothを利用してWiki記法であるTextileを出力してみたいと思います。

require 'redcloth'

context.define_tag "textile" do |tag|
  contents = tag.expand
  RedCloth.new(contents).to_html
end
# (tag.expandは、開始タグと終了タグの間の部分を返します。)

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

結果:

<h1>Hello <strong>World</strong>!</h1>

ネストされたタグ(Nested Tags)

さらに良い方法があります。コレクションをイテレートする際に、コンテナタグの間に囲まれた部分を扱う事が出来ます。

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

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

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

puts parser.parse(template)

結果:

<ul>

  <li>Larry</li>

  <li>Moe</li>

  <li>Curly</li>

</ul>

nameタグの頭に"stooge:"が付いています。これは、nameタグは必ずstoogeタグの内側で利用するということを意味しています。単にnameというタグを作成した場合、stoogeタグの外側で使用してください。

オブジェクトをテンプレートに展開する(Exposing Objects to Templates)

テンプレートの中に、特定のオブジェクトを展開する事が可能です。define_tagメソッドとforオプションを利用します。

context.define_tag "count", :for => 1

テンプレートにオブジェクト1をcountタグとして展開しました。以下のようにも書けます。

context.define_tag("count") { 1 }

タイプ数が多いね。また、オブジェクト上にメソッドを展開することもできます。

context.define_tag "user", :for => user, :expose => [ :name, :age, :email ]

4つのタグをcontextに追加する事ができました。userがその1つです。そこにexposeの3つのメソッドがそれぞれについています。ユーザの名前をテンプレートの中で取得するには以下のようにします。

<r:user><r:name /></r:user>

もし、user.nameに"John"があれば、"John"と描写します。

タグの速記(Tag Shorthand)

上の例では、user.nameを参照する際に、以下のようにタグを記述する必要がありました。

<r:user><r:name /></r:user>

以下のようにコロンを使用してnameを参照します。

<r:user:name />

Radiusでは、このような速記を全てのタグに許可しています。

タグプレフィクスを変更する(Changing the Tag Prefix)

デフォルトの状態では、Radiusタグは"radius"と頭に付けなければなりません。tag_prefix属性を変更することによって変えることが出来ます。

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

全てのタグの頭に"radius"の代わりに"r"を付けなければならなくなりました。

未定義タグの振る舞いを指定する(Custom Behavior for Undefined Tags)

Context#tag_missingは、Object#method_missingに似ており、Contextで定義されていないタグが使われた場合の動作を記述することが出来ます。

class LazyContext < Radius::Context
  def tag_missing(tag, attr, &block)
    "<strong>ERROR: Undefined tag `#{tag}' with attributes #{attr.inspect}</strong>"
  end
end

parser = Radius::Parser.new(LazyContext.new, :tag_prefix => 'lazy')
puts parser.parse('<lazy:weird value="true" />')

結果:

  <strong>ERROR: Undefined tag `weird' with attributes {"value"=>"true"}</strong>

定義されていないタグが含まれていた場合、通常はUndefinedTagErrorを出力します。

タグ・バインディング(Tag Bindings)

Radiusは、Context#define_tagメソッドのブロックに、TagBindingを渡します。このタグのバインディングは、いくつかの仕事に役立ちます。このタグのバインディングは、タグの内容を処理して、結果を返すメソッドを持つインスタンスを持ちます。例えば、属性のハッシュを返すattrメソッドを持ちます。また、TagBinding#single?とTagBinding#double?の2つメソッドを持ちます。コンテナタグかどうかをtrueまたはfalseで返します。詳しい事は、"Radius::TagBinding":http://radius.rubyforge.org/classes/Radius/TagBinding.html を参照してください。

Tag Binding Locals, Globals, and Context Sensitive Tags

TagBinding#globalsは、すべてのタグにアクセス可能な変数を保存することに役立ちます。

context.define_tag "inc" do |tag|
  tag.globals.count ||= 0
  tag.globals.count += 1
end

context.define_tag "count" do |tag|
  tag.globals.count || 0
end

TagBinding#localsはTagBinding#globalsにある変数を映すが、子のタグが変数を再定義するのを許可します。文脈依存タグを定める際に貴重です。

require 'radius'

class Person
  attr_accessor :name, :friend
  def initialize(name)
    @name = name
  end
end

jack = Person.new('Jack')
jill = Person.new('Jill')
jack.friend = jill
jill.friend = jack

context = Radius::Context.new do |c|
  c.define_tag "jack" do |tag|
    tag.locals.person = jack
    tag.expand
  end
  c.define_tag "jill" do |tag|
    tag.locals.person = jill
    tag.expand
  end
  c.define_tag "name" do |tag|
    tag.locals.person.name rescue tag.missing!
  end
  c.define_tag "friend" do |tag|
    tag.locals.person = tag.locals.person.friend rescue tag.missing!
    tag.expand
  end
end

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

parser.parse('<r:jack:name />') #=> "Jack"
parser.parse('<r:jill:name />') #=> "Jill"
parser.parse('<r:jill:friend:name />') #=> "Jack"
parser.parse('<r:jill:friend:friend:name />') #=> "Jack"
parser.parse('<r:jill><r:friend:name /> and <r:name /></r:jill>') #=> "Jack and Jill"
parser.parse('<r:name />') # raises an UndefinedTagError exception

TagBinding#localsには賢いネスティングを使う事が出来ます。"<r:jill:name />"は"Jill"と評価されます。"<r:jill:friend:name />"は"Jack"と評価されます。localeは、タグになるや否やスコープを失いますが、globalsの方はスコープを失いません。

例の最後の行では、TagMissing errorが出ています。これは、nameタグが以下のように定義されているからです。

tag.locals.person.name rescue tag.missing!

もし、personをlocalsに定義しないと、nilが返ってきます。さらにnameを参照するとNoMethodErrorエラーが発生します。‘rescue’のおかげで、TagBinding#missing!はContext#tag_missingによって呼び出されます。

標準では、Context#tag_missingは、UndefinedTagErrorを発生させます。‘rescue tag.missing!’は、エラーをチェックするのに便利です。

タグの特性(Tag Specificity)

2つの同じ名前で、ネスティングが異なるタグが出た場合、RadiusはCSSと同じアルゴリズムを使用して優先順位をつけます。以下のようなタグが定義されていた場合、

  • nesting
  • extra:nesting
  • parent:child:nesting

テンプレートで以下のようになっている場合、

<r:parent:extra:child:nesting />

Radiusは、以下のように計算します。

  • nesting => 1.0.0.0
  • extra:nesting => 1.0.1.0
  • parent:child:nesting => 1.1.0.1

parent:child:nestingが勝ちました。もし、テンプレートに以下のように書いてあったら、

<r:parent:child:extra:nesting />

Radiusは、以下のように計算します。

  • nesting => 1.0.0.0
  • extra:nesting => 1.1.0.0
  • parent:child:nesting => 1.0.1.1

extra:nestingは、勝ちました。

値は、右から左にポイントをタグの各々に割り当てることによって割り当てられます。テンプレートの中に4レベルのネストされたタグがあれば、タグは以下のように点数が付けられます。

  • 1.1.1.1

それぞれのレベルに1ポイントが与えられます。

Radiusが私たちの予想通りにタグを分解してくれるので必要ありませんが、万が一の場合はこの項目を思い出してください。

サンプル

<r:pages>
<h1><r:title /></h1>
<p><r:content /></p>
</r:pages>

サンプルコード を作成しました。併せてご利用ください。

Bookmark and Share