Erubi とは何か


Qiita には Ruby on Rails の記事が 2 万本以上あるのに，Rails で使われているライブラリー Erubi の記事はほぼゼロ。
Erubi とは何なのか。

# まず eRuby について

Rails を少しでもかじったことがあれば，eRuby というテンプレート言語を知っていると思う。いや「eRuby」という名称に馴染みがなくても，拡張子が `.erb` のアレと言えば分かるだろう。テキストに Ruby のコードを埋め込むテンプレート言語の名称だ。

読み方はおそらく「イー・ルビー」で，「e」は embedded（埋め込まれた）の頭文字。

データに基づいて HTML を生成するのによく使われるが，対象となるテキストは HTML に限らない。テキストであれば何でもいい。

実際，Rails では，例えば config/database.yml なんかは eRuby で記述されている。拡張子は単に `.yml` だが，

```erb
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
```

なんて行があるのを見たことがあると思う。環境変数 `RAILS_MAX_THREADS` の値を取得し（もし定義されてなければ `5` を採用し），それを `pool` の値にしているわけだ。
こんなふうに YAML テキストを生成するために eRuby テンプレートシステムが使われている。

ところで，この言語名について，「それ，『eRuby』じゃなくて『ERB』じゃないの？」と思った人も少なくないだろう。名称は本質ではないが，あとで触れよう。

# Erubi とは

さて，一般にテンプレートを処理するプログラム部品を「テンプレートエンジン」などと言うが，Erubi は eRuby 用テンプレートエンジンの一つだ。

eRuby テンプレートを処理するエンジンには，ERB，Erubis，Erubi などがある。歴史的にはこの順に登場した[^other]。これらは少し仕様が違うが，本記事ではそのことに触れない。

[^other]: ほかにもいくつかあるが，さしあたりこの三つの名前を知っていればいいと思う。

ERB は Ruby の標準添付ライブラリー [erb](https://docs.ruby-lang.org/ja/2.7.0/library/erb.html) で提供されているテンプレートエンジンだ。

テンプレート言語の名前として「eRuby」の代わりに「ERB」もしくは「erb」を使っているのをよく見かける。本来は**特定の**テンプレートエンジンのクラス名，ライブラリー名のはずだが，識者も使っているし，間違いとまでは言えないのかなと思っている。

それはさておき，標準添付ライブラリーに eRuby テンプレートエンジンが用意されているのに，新たに Erubis が作られたのは，高速化と高機能化が主な動機だろうと思う。
実際，Erubis は速いし，高機能だ。

Erubis は Rails に採用された。Rails 5.0 までは Erubis（を改造したもの）が Rails の eRuby テンプレートエンジンだった。

Erubi はこの Erubis のフォーク（派生）である。さらなる高速化を図るとともに，機能をそぎ落として簡素化した。動機の一つには，Erubis の開発が止まって久しい，ということもあるようだ[^ment]。

[^ment]: とはいえ，私の印象では Erubis の完成度は高い。最新版が古いからといって，それだけでダメとは言えないんじゃないか。

そして，Rails ではバージョン 5.1 から Erubi（の改造したもの）を採用するようになった。

# Erubi を知らなくて済む理由

ふつうの Rails プログラマーは Erubi について知る必要があまり無い。eRuby の仕様（正確に言えば Rails における eRuby の仕様）だけ分かっていればよい。

それは，テンプレートエンジンを動かすところを Rails がすべてやってくれるからだ[^fw]。

ではなぜこんな記事を書いているかというと，Rails のビュー以外にも eRuby の使い道があるからだ。とにかくどんなテキストでもデータに基づいて生成できるんだからね。

[^fw]: フレームワークというものの存在意義の一つはそういうところにある。

# Erubi を使ってみる

Rails などとは無関係に Erubi の機能を使ってみよう。
その際，Tilt という gem を援用すると楽ができるのでそうしよう。

まず最初に，テンプレートが外部ファイルになっている場合の Erubi の使い方を述べ，次にテンプレートが String オブジェクトとして用意されている場合の書き方に触れる。

テンプレートに対しては，ローカル変数 `lv`，インスタンス変数 `@iv`，メソッド `m` を渡してやることにする。

## Erubi の使い方

gem として erubi と tilt を使うので，Gemfile を以下のようにしておく。


```rb:Gemfile
source "https://rubygems.org"

gem "tilt"
gem "erubi"
```

テンプレートは外部ファイル sample.txt.erb

```erb:sample.txt.erb
iv: <%= @iv %>
lv: <%= lv %>
m: <%= m 3 %>
```

に置く。ファイル名は必ずしも拡張子を `.txt.erb` のように二重に付ける必要はなく，単に `sample.erb` で構わない[^extname]。

[^extname]: `.erb` 以外の拡張子でも構わない。ただ，そうするとテキストエディターでのコードハイライトが eRuby 用にならないし，（本記事では扱わないが）Tilt にテンプレートエンジンを自動的に選ばせる使い方もできない。

このテンプレートに，インスタンス変数，ローカル変数，メソッドを渡してレンダリングさせ，表示させてみよう。以下のコードになる。

```rb
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>
```

と表示される。

コードを読み解いていこう。
まずは

```rb
Tilt::ErubiTemplate.new("a.txt.erb")
```

の部分。
今の場合，Erubi のテンプレートを扱うので，Tilt::ErubiTemplate というクラスを使う。
`new` にファイルのパスを渡してやると，ファイルを読み込んでインスタンスを作ってくれる。

次に，いきなり最終行だが，

```rb
t.render(scope, lv: 2)
```

を見てみよう。

`render` は文字通りレンダリングのためのメソッド。レンダリング結果の文字列が返る。テンプレートに与えるデータを引数として渡してやる。
第一引数は置いておいて，第二引数 `lv: 2` に注目しよう。これはハッシュである（キーワード引数ではない）。
第二引数のハッシュは，ローカル変数の名前と値の組を指定するものだ。
今の場合，テンプレート中で，値 `2` を持つ `lv` というローカル変数が定義されることになる[^lv]。
ローカル変数は好きなだけ指定できる。

[^lv]: よく「テンプレートにローカル変数を渡す」という言い方をするが，正確に言えば渡しているのではなく，定義させているのだ。しかし，誤解がないかぎり「渡す」と表現してもよいと思う。

第一引数には任意のオブジェクトを渡すことができる。このオブジェクトはテンプレート中で `self` となる。
`render` の第一引数に渡すオブジェクトを「スコープ」と呼ぶらしい（なので変数名も `scope` としておいた）。

テンプレート中の Ruby コードは，このスコープオブジェクトのコンテキストで評価される。
だから，スコープオブジェクトのメソッドを呼び出すことができるし，スコープオブジェクトのインスタンス変数を読み書きすることができる。

上のコード例では，Object クラスのインスタンスを生成して `scope` に代入し，

```rb
scope.instance_variable_set "@iv", 1
```

のようにしてインスタンス変数を定義したが，もちろん他の手段でインスタンス変数を定義してもよい。

また，メソッドについては，上のコード例では

```rb
def scope.m(x)
  "<#{x}>"
end
```

のようにして特異メソッドを定義しておいたが，他の手段でメソッドを定義してもよい。
例えばメソッド群を定義したモジュールを用意しておき，スコープオブジェクトに `extend` するのでもよい。
つまり，

```rb
module Helper
  # 云々
end

scope.extend Helper
```


といった具合である。
HTML エスケープのメソッドなど，テンプレート中でよく使うヘルパーメソッドを定義しておくといい。

スコープが不要なら `render` メソッドの第一変数には `nil` でも渡しておけばよいだろう。

## ファイルでなく String の場合

eRuby テキストがファイルではなく既に String オブジェクトとして存在している場合はどう書くか。
たとえば

```rb
erb_text = <<EOT
iv: <%= @iv %>
lv: <%= lv %>
m: <%= m 3 %>
EOT
```

のように与えられている場合。
Tilt::ErubiTemplate.new に，引数ではなくブロックで与えればよい。つまり，

```rb
Tilt::ErubiTemplate.new{ erb_text }
```

のように。ただこれだけのこと。
「ファイルパスなら引数で与え，テキストならブロックで与える」というのは Nokogiri の使い方に似ている。

# Erubi::Engine を改造して埋め込み記号を変更

さいごに，改造に関する話題を。

こんなことがあった。
`rails new` が雛形ファイル群を生成するのと同じように，自作の Ruby 製コマンドでファイル群を生成したかった。
生成するファイルは，毎回完全に同じなわけではなく，いくつかのパラメーターによって内容が違っていた。
こういう場合に生成ファイルのテンプレートを eRuby 形式で用意するのは良いやり方だろう。

ただ，ちょっとややこしいのが，生成するファイル群の中に eRuby ファイルがあったこと。
これの何が問題かというと，「eRuby テンプレートを生成する eRuby テンプレート」になるので，Ruby コード埋め込みがややこしくなるのだ。

eRuby には，`<%% %>` が `<% %>` となり，`<%%= %>` が `<%= %>` となるルールがあるので，「テンプレートのテンプレート」も記述できる。
とは言え，`<% %>` と `<%% %>` がごちゃまぜになったテンプレートは見づらい。
よしっ，テンプレートのテンプレートでは `<% %>` や `<%= %>` の代わりに `[[ ]]` や `[[= ]]` を使うことにしよう！
（えっ？）

そんなことが，できるんである。

## regexp オプションを与える

Erubi のテンプレートエンジンである Erubi::Engine を生成する際，`regexp` というオプションを与えることができる。
これは，Erubi がテンプレートテキストから Ruby コード部分を抜き出す正規表現だ。
デフォルトでは

```rb
/<%(={1,2}|-|\#|%)?(.*?)([-=])?%>([ \t]*\r?\n)?/m
```

となっている。
これを

```rb
/\[\[(={1,2}|-|\#|%)?(.*?)([-=])?\]\]([ \t]*\r?\n)?/m
```

に変えてやればいい。

Tilt を使う場合，Erubis::Engine.new をしなくてよいぶん，一工夫が必要になる。
まず，`regexp` オプションの値を変えた Erubi::Engine のサブクラスを作り，`Tilt::ErubiTemplate.new` のオプションで，エンジンクラスとして指定するのだ。
以下のようなコードになる。

```rb
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 %>` になるのは興味深い。