Qiita には Ruby on Rails の記事が 2 万本以上あるのに,Rails で使われているライブラリー Erubi の記事はほぼゼロ。
Erubi とは何なのか。
まず eRuby について
Rails を少しでもかじったことがあれば,eRuby というテンプレート言語を知っていると思う。いや「eRuby」という名称に馴染みがなくても,拡張子が .erb
のアレと言えば分かるだろう。テキストに Ruby のコードを埋め込むテンプレート言語の名称だ。
読み方はおそらく「イー・ルビー」で,「e」は embedded(埋め込まれた)の頭文字。
データに基づいて HTML を生成するのによく使われるが,対象となるテキストは HTML に限らない。テキストであれば何でもいい。
実際,Rails では,例えば config/database.yml なんかは eRuby で記述されている。拡張子は単に .yml
だが,
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
なんて行があるのを見たことがあると思う。環境変数 RAILS_MAX_THREADS
の値を取得し(もし定義されてなければ 5
を採用し),それを pool
の値にしているわけだ。
こんなふうに YAML テキストを生成するために eRuby テンプレートシステムが使われている。
ところで,この言語名について,「それ,『eRuby』じゃなくて『ERB』じゃないの?」と思った人も少なくないだろう。名称は本質ではないが,あとで触れよう。
Erubi とは
さて,一般にテンプレートを処理するプログラム部品を「テンプレートエンジン」などと言うが,Erubi は eRuby 用テンプレートエンジンの一つだ。
eRuby テンプレートを処理するエンジンには,ERB,Erubis,Erubi などがある。歴史的にはこの順に登場した1。これらは少し仕様が違うが,本記事ではそのことに触れない。
ERB は Ruby の標準添付ライブラリー erb で提供されているテンプレートエンジンだ。
テンプレート言語の名前として「eRuby」の代わりに「ERB」もしくは「erb」を使っているのをよく見かける。本来は特定のテンプレートエンジンのクラス名,ライブラリー名のはずだが,識者も使っているし,間違いとまでは言えないのかなと思っている。
それはさておき,標準添付ライブラリーに eRuby テンプレートエンジンが用意されているのに,新たに Erubis が作られたのは,高速化と高機能化が主な動機だろうと思う。
実際,Erubis は速いし,高機能だ。
Erubis は Rails に採用された。Rails 5.0 までは Erubis(を改造したもの)が Rails の eRuby テンプレートエンジンだった。
Erubi はこの Erubis のフォーク(派生)である。さらなる高速化を図るとともに,機能をそぎ落として簡素化した。動機の一つには,Erubis の開発が止まって久しい,ということもあるようだ2。
そして,Rails ではバージョン 5.1 から Erubi(の改造したもの)を採用するようになった。
Erubi を知らなくて済む理由
ふつうの Rails プログラマーは Erubi について知る必要があまり無い。eRuby の仕様(正確に言えば Rails における eRuby の仕様)だけ分かっていればよい。
それは,テンプレートエンジンを動かすところを Rails がすべてやってくれるからだ3。
ではなぜこんな記事を書いているかというと,Rails のビュー以外にも eRuby の使い道があるからだ。とにかくどんなテキストでもデータに基づいて生成できるんだからね。
Erubi を使ってみる
Rails などとは無関係に Erubi の機能を使ってみよう。
その際,Tilt という gem を援用すると楽ができるのでそうしよう。
まず最初に,テンプレートが外部ファイルになっている場合の Erubi の使い方を述べ,次にテンプレートが String オブジェクトとして用意されている場合の書き方に触れる。
テンプレートに対しては,ローカル変数 lv
,インスタンス変数 @iv
,メソッド m
を渡してやることにする。
Erubi の使い方
gem として erubi と tilt を使うので,Gemfile を以下のようにしておく。
source "https://rubygems.org"
gem "tilt"
gem "erubi"
テンプレートは外部ファイル sample.txt.erb
iv: <%= @iv %>
lv: <%= lv %>
m: <%= m 3 %>
に置く。ファイル名は必ずしも拡張子を .txt.erb
のように二重に付ける必要はなく,単に sample.erb
で構わない4。
このテンプレートに,インスタンス変数,ローカル変数,メソッドを渡してレンダリングさせ,表示させてみよう。以下のコードになる。
require "bundler"
Bundler.require
t = Tilt::ErubiTemplate.new("sample.txt.erb")
scope = Object.new
scope.instance_variable_set "@iv", 1
def scope.m(x)
"<#{x}>"
end
puts t.render(scope, lv: 2)
これを実行すると
iv: 1
lv: 2
m: <3>
と表示される。
コードを読み解いていこう。
まずは
Tilt::ErubiTemplate.new("a.txt.erb")
の部分。
今の場合,Erubi のテンプレートを扱うので,Tilt::ErubiTemplate というクラスを使う。
new
にファイルのパスを渡してやると,ファイルを読み込んでインスタンスを作ってくれる。
次に,いきなり最終行だが,
t.render(scope, lv: 2)
を見てみよう。
render
は文字通りレンダリングのためのメソッド。レンダリング結果の文字列が返る。テンプレートに与えるデータを引数として渡してやる。
第一引数は置いておいて,第二引数 lv: 2
に注目しよう。これはハッシュである(キーワード引数ではない)。
第二引数のハッシュは,ローカル変数の名前と値の組を指定するものだ。
今の場合,テンプレート中で,値 2
を持つ lv
というローカル変数が定義されることになる5。
ローカル変数は好きなだけ指定できる。
第一引数には任意のオブジェクトを渡すことができる。このオブジェクトはテンプレート中で self
となる。
render
の第一引数に渡すオブジェクトを「スコープ」と呼ぶらしい(なので変数名も scope
としておいた)。
テンプレート中の Ruby コードは,このスコープオブジェクトのコンテキストで評価される。
だから,スコープオブジェクトのメソッドを呼び出すことができるし,スコープオブジェクトのインスタンス変数を読み書きすることができる。
上のコード例では,Object クラスのインスタンスを生成して scope
に代入し,
scope.instance_variable_set "@iv", 1
のようにしてインスタンス変数を定義したが,もちろん他の手段でインスタンス変数を定義してもよい。
また,メソッドについては,上のコード例では
def scope.m(x)
"<#{x}>"
end
のようにして特異メソッドを定義しておいたが,他の手段でメソッドを定義してもよい。
例えばメソッド群を定義したモジュールを用意しておき,スコープオブジェクトに extend
するのでもよい。
つまり,
module Helper
# 云々
end
scope.extend Helper
といった具合である。
HTML エスケープのメソッドなど,テンプレート中でよく使うヘルパーメソッドを定義しておくといい。
スコープが不要なら render
メソッドの第一変数には nil
でも渡しておけばよいだろう。
ファイルでなく String の場合
eRuby テキストがファイルではなく既に String オブジェクトとして存在している場合はどう書くか。
たとえば
erb_text = <<EOT
iv: <%= @iv %>
lv: <%= lv %>
m: <%= m 3 %>
EOT
のように与えられている場合。
Tilt::ErubiTemplate.new に,引数ではなくブロックで与えればよい。つまり,
Tilt::ErubiTemplate.new{ erb_text }
のように。ただこれだけのこと。
「ファイルパスなら引数で与え,テキストならブロックで与える」というのは Nokogiri の使い方に似ている。
Erubi::Engine を改造して埋め込み記号を変更
さいごに,改造に関する話題を。
こんなことがあった。
rails new
が雛形ファイル群を生成するのと同じように,自作の Ruby 製コマンドでファイル群を生成したかった。
生成するファイルは,毎回完全に同じなわけではなく,いくつかのパラメーターによって内容が違っていた。
こういう場合に生成ファイルのテンプレートを eRuby 形式で用意するのは良いやり方だろう。
ただ,ちょっとややこしいのが,生成するファイル群の中に eRuby ファイルがあったこと。
これの何が問題かというと,「eRuby テンプレートを生成する eRuby テンプレート」になるので,Ruby コード埋め込みがややこしくなるのだ。
eRuby には,<%% %>
が <% %>
となり,<%%= %>
が <%= %>
となるルールがあるので,「テンプレートのテンプレート」も記述できる。
とは言え,<% %>
と <%% %>
がごちゃまぜになったテンプレートは見づらい。
よしっ,テンプレートのテンプレートでは <% %>
や <%= %>
の代わりに [[ ]]
や [[= ]]
を使うことにしよう!
(えっ?)
そんなことが,できるんである。
regexp オプションを与える
Erubi のテンプレートエンジンである Erubi::Engine を生成する際,regexp
というオプションを与えることができる。
これは,Erubi がテンプレートテキストから Ruby コード部分を抜き出す正規表現だ。
デフォルトでは
/<%(={1,2}|-|\#|%)?(.*?)([-=])?%>([ \t]*\r?\n)?/m
となっている。
これを
/\[\[(={1,2}|-|\#|%)?(.*?)([-=])?\]\]([ \t]*\r?\n)?/m
に変えてやればいい。
Tilt を使う場合,Erubis::Engine.new をしなくてよいぶん,一工夫が必要になる。
まず,regexp
オプションの値を変えた Erubi::Engine のサブクラスを作り,Tilt::ErubiTemplate.new
のオプションで,エンジンクラスとして指定するのだ。
以下のようなコードになる。
require "bundler"
Bundler.require
class MyErubiEngine < Erubi::Engine
def initialize(input, properties={})
properties[:regexp] = /\[\[(={1,2}|-|\#|%)?(.*?)([-=])?\]\]([ \t]*\r?\n)?/m
super(input, properties)
end
end
t = Tilt::ErubiTemplate.new(engine_class: MyErubiEngine){ <<~EOT }
iv: [[= @iv ]]
lv: [[= lv ]]
m: [[= m 3]]
[[% 3/0 ]]
EOT
scope = Object.new
scope.instance_variable_set "@iv", 1
def scope.m(x)
"<#{x}>"
end
puts t.render(scope, lv: 2)
実行すると
iv: 1
lv: 2
m: <3>
<% 3/0 %>
のように表示される。
コメントの形式で書いた [[% 3/0 ]]
が <% 3/0 %>
になるのは興味深い。
-
ほかにもいくつかあるが,さしあたりこの三つの名前を知っていればいいと思う。 ↩
-
とはいえ,私の印象では Erubis の完成度は高い。最新版が古いからといって,それだけでダメとは言えないんじゃないか。 ↩
-
フレームワークというものの存在意義の一つはそういうところにある。 ↩
-
.erb
以外の拡張子でも構わない。ただ,そうするとテキストエディターでのコードハイライトが eRuby 用にならないし,(本記事では扱わないが)Tilt にテンプレートエンジンを自動的に選ばせる使い方もできない。 ↩ -
よく「テンプレートにローカル変数を渡す」という言い方をするが,正確に言えば渡しているのではなく,定義させているのだ。しかし,誤解がないかぎり「渡す」と表現してもよいと思う。 ↩