Re:VIEWを読み解く - builder編

Re:VIEWで書いた原稿を特殊な形式で出力する必要に迫られ、独自でbuilderを書いたりしたのでメモ

# Re:VIEW出力の仕組み(まえおき)
review-compileを実行すると、「review\lib\review\[--targetに指定した名前] + "builder.rb"」というファイルが読み込まれます。

たとえば「--target html」と指定した場合は、Re:VIEWに収録されている「review\lib\review\htmlbuilder.rb」が読み込まれて、結果HTMLとして出力される という仕組みです。


review-compile自体はそのまま標準出力にコンパイル結果をはき出すので、適当なファイルに出力することで、ファイルとして保存する という仕組み。

つまるところ、「review\lib\review\」フォルダに別の「○○builder.rb」を作成すれば、任意のファイル形式での出力が可能となります。

# ○○builder.rbでできること

ざっくりできるのは、以下のようなもの。

 * ブロック出力時の動作をオーバーライドする
 * インライン強調句出力時の動作をオーバーライドする
 * 新しいブロックを定義する？(ソースコードを見たかぎりできそうですがやってない)

TOPBuilderクラスを継承して○○builder.rbを作っておくと、上位ビルダーのメソッドをオーバーライドするだけで、処理を実装できますし、実装方法がわからなくなったらtopbuilder.rbを見ればだいたいOKなので、オススメ。

## `@chapter`オブジェクト
見出しなどの情報が格納されたオブジェクトで、Builderクラスでは最初から使えます。とりあえずここだけは覚えておきたいプロパティ・メソッドは次のようなもの。

 * `@chapter.number`：章番号。前付け、後付け、付録、本編などそれぞれ別々の値が設定されている(たとえば前付けが超絶長い本を書いてても本編は一章からスタート)
 * `@chapter.on_CHAPS?`：現在の章が本編かどうか
 * `@chapter.on_POSTDEF?`：同じく後付け
 * `@chapter.on_PREDEF?`：同じく前付け
 * `@chapter.on_APPENDIX?`：同じく付録
 * `@chapter.image(number)`：画像に関する処理を行う。画像IDからファイルパスを引き出すなどの処理が可能。`@chapter.image(number).path`でファイルパス(catalog.ymlのフォルダ基準)を取得したり、`@chapter.image(number).id`で画像のIDを取得したり

## `@book`オブジェクト
catalog.ymlなどの情報が詰まったオブジェクトで、同じくBuilderクラスでは最初から使えます。とりあえずここだけは覚えておきたいプロパティ・メソッドは次のようなもの。

 * `@book.config[]`：連想配列形式。config.ymlの内容にアクセスできる
 * `@book.read_CHAPS`：catalog,ymlのCHAPSに入っている内容を配列で取得する。
 * `@book.read_PREDEF`：同じくPREDEF
 * `@book.read_APPENDIX`：同じくAPPENDIX
 * `@book.read_POSTDEF`：同じくPOSTDEF

## Builderでオーバーライドできる便利なメソッド

とりあえずいじりたいのは以下のメソッドか

 * `puts(s)`：文字を出力するとき必ず呼ばれるメソッド
 * `paragraph(lines)`：平文段落を出力するとき呼ばれるメソッド。たとえばHTMLBuilderでは&lt;p&gt;で段落挟んでいる
 * `builder_init_file`：初期化時に呼び出されるメソッド。review-compile開始時に独自の処理を行いたいときは、initializeではなくこれをオーバーライドする
 * `headline(level, label, caption)`：見出し
 * `read(lines)`：リード文
 * `image(lines, id, caption, metric=nil)`：画像
 * `indepimage(id, caption=nil, metric=nil)`：連番なし画像
 * `compile_inline(caption)`：インライン強調を適用してくれるメソッド。オーバーライド用というより、オーバーライドしたメソッド内で困ったら使う

また、ちょっと複雑で、複数のメソッドにまたがって処理を記述するものもあります。

 * テーブル：`table_header(id, caption)`/`table_begin(ncols)`/`tr(rows)`/`th(str)`/`td(str)`/`table_end`で構成されている
 * リスト(コード)：`list_header(id, caption)`/`list_body(id, lines)`で構成されている
 * コラム：`common_column_begin(type, caption)`/`common_column_end(type)`で構成されている

## 基本的なテクニック

### 見出し番号はどうやって取得するの？
章番号は`@chapter.number`で取得できるのですが、それ以外の大見出し番号や小見出し番号を取得するメソッドなどは存在しない模様。そのため、TOPBuilder.headlineでは、次のようにして見出し番号をクラス変数として保持している。

```rb:topbuilder.rb
      case level
      when 1
        # ・・・
        @section = 0
        @subsection = 0
        @subsubsection = 0
        @subsubsubsection = 0
      when 2
        @section += 1
        # ・・・
        @subsection = 0
        @subsubsection = 0
        @subsubsubsection = 0
      when 3
        @subsection += 1
        # ・・・
        @subsubsection = 0
        @subsubsubsection = 0
      when 4
        @subsubsection += 1
        # ・・・
        @subsubsubsection = 0
      when 5
        @subsubsubsection += 1
        # ・・・
      else
        raise "caption level too deep or unsupported: #{level}"
      end
```

これで、`@section`、`@subsection`、`@subsubsection`、`@subsubsubsection`の変数の中身を見れば、今の章番号がわかる仕組み。便利だね

### 今コンパイルしてる章のユニーク番号を取得したい

前述の通り、`@chapter.number`で取得できる章番号は「本編内での」章番号だったり、「前付け内での」章番号だったりします。よって、本編の最初の章と付録の最初の章は共に1になってしまい、画像ファイルの整理番号に使うときなどはちょっと面倒です。

そんなときは、chapnameなどのメソッドを別に用意してやって、Builder内部で章番号を取得する必要があるときにはそちらを見るようにすれば良い。

```rb:○○builder.rb
    def chapname
      if @chapter.on_CHAPS?
        return @chapter.number
      elsif @chapter.on_POSTDEF?
        return "P#{@chapter.number}"
      elsif @chapter.on_PREDEF?
        return "R#{@chapter.number}"
      elsif @chapter.on_APPENDIX?
        n = ""
        n = "#{@chapter.number}" if @book.read_APPENDIX.lines.size > 1
        return "A#{n}"
      end
    end
```

上記のメソッドを呼ぶと、「本編の」第一章の時は、1が、「付録の」第一章の時は、A1が返ってきます。これで章番号がユニークになるね。
なお、`@chapter.on_APPENDIX?`のあとの二行は、「もし付録が一章しか無かった場合は、A1でなくAと返す」という処理です。一章しかないのにA1というのはイヤだ という人はあわせて使ってみると良さそう。

# ところで

ところでこの手のOSS、母国語どころか英語ですらコメント書いてないの多いんだけどみんな「ソースは仕様書だ！バグも全て記載されている！」主義者なの？

わたしはネイティブでRuby話せる人じゃないのでマジ大変でした。