ERBのtrim_modeの違い

この記事ではRuby 2.4.1に同梱されているERBについて書く。読んだ過程をメモしているだけなので多分あまり要約されてないので、読みにくかったら[るびまの記事](http://magazine.rubyist.net/?0017-BundledLibraries)を参照して欲しい。

# ERBのオプション
`ERB#initialize`の引数は以下のように定義されている。

```rb
def initialize(str, safe_level=nil, trim_mode=nil, eoutvar='_erbout')
```

`eoutvar`は単に内部の変数名が変えられるだけなので普通は変える必要ないし、`safe_level`で調整できる`$SAFE`も普通は使わない機能だと思うが、`trim_mode`は出力結果に改行を入れるかどうかを細かくコントロールできるのでまあ使う機能なのではないだろうか。

というわけでこの記事では最も重要なERBのコンパイルオプションであろう`trim_mode`について書く。

## trim_mode オプション
`ERB#initialize`に渡した`trim_mode`なる引数は、[`ERB::Compiler#prepare_trim_mode`](https://github.com/ruby/ruby/blob/v2_4_1/lib/erb.rb#L686-L708)というメソッドで内部的なオプション`@percent`,`@trim_mode`に変換される。元の引数はそれ以降使われないので、`@percent`と`@trim_mode`が事実上のコンパイルオプションとなる。

`@trim_mode`に関しては同じ名前なのに中身が違うので少々紛らわしいが、この記事では`trim_mode`を`ERB#initialize`に与えられる引数、`@trim_mode`を`ERB::Compiler`で使われるインスタンス変数の値ということにする。

内部的には`trim_mode`は`1`,`2`,`0`,`String`およびそれ以外というように分岐しているが、最終的にセットされうる値から逆算してまとめると以下のようになる。

| `@percent` | `@trim_mode` | `trim_mode`の<br>代表的な値 | [ソースコード上のコメント](https://github.com/ruby/ruby/blob/v2_4_1/lib/erb.rb#L774-L777) |
|:-----------|:-------------|:----------------------|:-------|
| `false` | `nil` | `0`, `""`, `nil` |  |
| `false` | `">"` | `1`, `">"` |  `%>`で終わる行の改行を削除する|
| `false` | `"<>"` | `2`, `"<>"` |  `<%`で始まり`%>`で終わる行の改行を削除する |
| `false` | `"-"` | `"-"` |  `-%>`で終わる空行を削除する |
| `true` | `nil` | `"%"` |  `%`で始まる行でRubyのコード実行を有効にする |
| `true` | `">"` | `"%>"` |  `%`で始まる行でRubyのコード実行を有効にする,`%>`で終わる行の改行を削除する |
| `true` | `"<>"` | `"%<>"` | `%`で始まる行でRubyのコード実行を有効にする,`<%`で始まり`%>`で終わる行の改行を削除する |
| `true` | `"-"` | `"%-"` |  `%`で始まる行でRubyのコード実行を有効にする,`-%>`で終わる空行を削除する|

# ERBのScannerの決定
ではそれらのコンパイルオプションは何に使われるのか。実はこれらは一部`@percent`がマジックコメントの判定に使われているくらいで、コンパイラ本体では使われていない。
では何に使われているかというと、コンパイラが使用するScannerの決定とその内部で使われる。なので、ほとんどスキャナオプションといっても良い。

ERBでは4つのスキャナ(うち1つは使われる条件が重複しており使われなさそう)が定義されており、これは`@percent`と`@trim_mode`に応じて使われるものが以下のように決定される。

| `@percent` | `@trim_mode` | `trim_mode`の<br>代表的な値 | Scanner | [ソースコード上のコメント](https://github.com/ruby/ruby/blob/v2_4_1/lib/erb.rb#L774-L777) |
|:-----------|:-------------|:----------------------|:------|:-------|
| `false` | `nil` | `0`, `""`, `nil` | `SimpleScanner2` | |
| `false` | `">"` | `1`, `">"` | `TrimScanner` | `%>`で終わる行の改行を削除する|
| `false` | `"<>"` | `2`, `"<>"` | `TrimScanner` | `<%`で始まり`%>`で終わる行の改行を削除する |
| `false` | `"-"` | `"-"` | `ExplicitScanner` | `-%>`で終わる空行を削除する |
| `true` | `nil` | `"%"` | `TrimScanner` | `%`で始まる行でRubyのコード実行を有効にする |
| `true` | `">"` | `"%>"` | `TrimScanner` | `%`で始まる行でRubyのコード実行を有効にする,`%>`で終わる行の改行を削除する |
| `true` | `"<>"` | `"%<>"` | `TrimScanner` | `%`で始まる行でRubyのコード実行を有効にする,`<%`で始まり`%>`で終わる行の改行を削除する |
| `true` | `"-"` | `"%-"` | `TrimScanner` | `%`で始まる行でRubyのコード実行を有効にする,`-%>`で終わる空行を削除する|

# Scannerごとの挙動の違い
選択されうるこれらのScannerのそれぞれがどのような挙動をするか見ていく。

## SimpleScanner2
デフォルトの挙動。「`<%`, `<%=`, `<%#`といったトークンを読んだあとそれにマッチする`%>`を既に読んでいるか」でスキャンに使う正規表現を2パターン切り替える。TrimScannerは両方にマッチする正規表現を使うので、`SimpleScanner2`の方がスキャンが速そうに見える(試してない)。

開始タグとしてスキャンされるのは`<%%`,`<%=`,`<%#`,`<%`であり、終了タグとしてスキャンされるのは`%%>`, `%>`である。`<%%`や`%%>`はテキストとして`<%`や`%>`を打ちこむためのエスケープ用で、それらは通常のテキストとしてスキャンされる。

なおコンパイラはそれらの特別なタグがスキャンされる際は、そのタグの部分のみをスキャンされることを想定しており、改行とかがついた状態でスキャンされると壊れるようになっているため(`"%>"`じゃなくて`"%>\n"`だとダメ)、正規表現もそれに対応して作られている。

改行を無視するとかそういうことはやらないので、行末に`%>`とかがある場合でも、その後の改行が生成コードに付加される。

## ExplicitScanner
`%`開始行をRubyとして評価する機能を使わず、`-%>`で終わる行の空行を削除する場合に使われるスキャナ。
これも`SimpleScanner2`と同じく、スキャナがERBタグの途中かに応じて正規表現をスイッチするので多分`TrimScanner`より速い。機能自体は`TrimScanner`のサブセットに過ぎないと思う。

(全然関係ないけど、Itamaeとかだとこれが採用されているので`-%>`が使える)

## TrimScanner
ここまで書いてたら僕はもう理解できてしまったのでもう解説しなくていい気がしてきた。

唯一`%`開始行をRubyとして評価するのに対応しているスキャナであり、その分毎回スキャンが遅くなるはず。また`"<>"`や`">"`といったコンパイルオプションもこのスキャナでしか動作しない。

このスキャナを読む上で難しいところは`@trim_mode`に応じて行を読み込むためのメソッドが変わるところで、以下のように分岐している。

| `@trim_mode` | `@scan_line`用メソッド |
|:-------------|:---------------------|
| `">"` | `:trim_line1` |
| `"<>"` | `:trim_line2` |
| `"-"` | `:explicit_trim_line` |
| nil | `:scan_line` |

# まとめ

全ての情報をまとめると以下のようになる。

| `@percent` | `@trim_mode` | `trim_mode`の<br>代表的な値 | Scanner | 行スキャンメソッド | [ソースコード上のコメント](https://github.com/ruby/ruby/blob/v2_4_1/lib/erb.rb#L774-L777) |
|:-----------|:-------------|:----------------------|:-------|:------|:-------|
| `false` | `nil` | `0`, `""`, `nil` | `SimpleScanner2` | `#scan` | |
| `false` | `">"` | `1`, `">"` | `TrimScanner` | `#trim_line1` |`%>`で終わる行の改行を削除する|
| `false` | `"<>"` | `2`, `"<>"` | `TrimScanner` | `#trim_line2` | `<%`で始まり`%>`で終わる行の改行を削除する |
| `false` | `"-"` | `"-"` | `ExplicitScanner` | `#explicit_trim_line` | `-%>`で終わる空行を削除する |
| `true` | `nil` | `"%"` | `TrimScanner` | `#scan_line` | `%`で始まる行でRubyのコード実行を有効にする |
| `true` | `">"` | `"%>"` | `TrimScanner` | `#trim_line1` | `%`で始まる行でRubyのコード実行を有効にする,`%>`で終わる行の改行を削除する |
| `true` | `"<>"` | `"%<>"` | `TrimScanner` | `#trim_line2` | `%`で始まる行でRubyのコード実行を有効にする,`<%`で始まり`%>`で終わる行の改行を削除する |
| `true` | `"-"` | `"%-"` | `TrimScanner` | `#explicit_trim_line` | `%`で始まる行でRubyのコード実行を有効にする,`-%>`で終わる空行を削除する|