2018年01月02日
_ [reviewml] Re:VIEW Hacks (4) 〜Bookオブジェクトの深遠
あけましておめでとうございます。今年のRe:VIEWはTeXまわりの強化を考えていますが、後方互換性を派手に壊しそうなのでどうしたものかと……。
さて、前回に残していたBookやIndex、あとChapterについてです。……がかなり複雑なのでBookだけで終わりそうです。
ビルダとコンパイルについて説明してきましたが、「本」としての関係性定義はどこにあるのかというと、lib/review/book内の各クラスにあります。
前回少し言及した@bookは、lib/review/book/base.rbのReVIEW::Book::Baseから作られています。@bookはReVIEW::Book.load(<ディレクトリ>)で作られますが、self.loadが呼び出されたときに内部ではself.update_rubyenvが呼び出され、ここでreview-ext.rbが読み込まれてモンキーパッチによる自己書き換えが行われます(これは第1回で説明しました)。その後、ReVIEW::Bookオブジェクトが新たに作成される、という運びです。このあたりは本当に、青木さん巧みだなぁと思いますね。
module ReVIEW module Book class Base … def self.load(dir = '.') update_rubyenv dir new(dir) end … def self.update_rubyenv(dir) … Kernel.load File.expand_path("#{dir}/review-ext.rb") … end def initialize(basedir) @basedir = basedir @parts = nil @chapter_index = nil @config = ReVIEW::Configure.values @catalog = nil @read_part = nil @warn_old_files = {} # XXX for checking CHAPS, PREDEF, POSTDEF end
クラス内には、本を構成する各種のパラメータ取得代わりに使うインスタンスメソッドが定義されています。少々注意すべきは、initialize時(newでオブジェクトを作るとき)にはconfig.yml YAML設定の読み込み(@config)くらいしかされていないという点です。
「章一覧がほしい」「このIDの表のカウンタはいくつ?」といった要求を受けて初めて、該当のオブジェクトを探す→まだないな→必要な読み込みを行って該当のオブジェクトを作成→以降はそのオブジェクトを利用、というプロセスを踏みます。軽量で拡張しやすくはありますが、結果的に読み込み部分があちこちのメソッドに存在することになるため、わかりにくいかもしれません。
review-compileで見てみると、
book = ReVIEW::Book::Base.load(basedir) book.config = config # needs only at the first time ARGV.each do |item| chap_name = File.basename(item, '.*') chap = book.chapter(chap_name)
が呼び出し部分で、book.chapterにファイルIDを渡して章を表すChapterオブジェクトの取得を要求しています。
ここからlib/review/book/base.rbを追ってみると、
def chapter(id) chapter_index[id] end
とchapter_indexメソッドが返す連想配列を戻すようになっており、その実装は
def chapter_index return @chapter_index if @chapter_index contents = chapters parts.each { |prt| contents << prt if prt.id.present? } @chapter_index = ChapterIndex.new(contents) end
となっています。@chapter_indexをすでに構築済みならそれを返し、そうでないならChapterIndexオブジェクトを構築します。新たにchapters、partsの呼び出しが登場しました。
def chapters parts.map(&:chapters).flatten end def parts @parts ||= read_parts end
このように、chaptersを作るには結局partsを使っていました。parts.map(&:chapters).flattenは、partsで返る部配列の各部に対して.chaptersで章の配列を取り出し、flattenで平坦化して結果的に章の一覧の配列を得ます。
@partsはPartオブジェクトの配列です。構築済みならそれを返し、そうでないならread_partsメソッドで構築します。
def read_parts list = parse_chapters # NOTE: keep this = style to work this logic. if pre = prefaces list.unshift pre end if app = appendix list.push app end if post = postscripts list.push post end list end
少々長いですが、後半は前付・付録・後付時の対処で、ここではparse_chaptersメソッドの呼び出しをしているということに注目します。部を読んでいたはずなのに章読みになったのは、部の処理が後から付け足したことのせいだった気がします(すでに忘却の彼方)。
def parse_chapters part = 0 num = 0 if catalog return catalog.parts_with_chaps.map do |entry| if entry.is_a?(Hash) chaps = entry.values.first.map do |chap| chap = Chapter.new(self, num += 1, chap, "#{@basedir}/#{chap}") chap end Part.new(self, part += 1, chaps, read_part.split("\n")[part - 1]) else chap = Chapter.new(self, num += 1, entry, "#{@basedir}/#{entry}") if chap.number num = chap.number else num -= 1 end Part.new(self, nil, [chap]) end end end … end
全体でreturnするには詰め込みすぎている感じがありますが、
- 「catalog」は変数ではなく、章構成を定義しているcatalog.ymlから読み込みを行うcatalogメソッド。Catalogオブジェクトが入っている(lib/review/catalog.rb)。
- Catalogオブジェクト内で部と章を表すオブジェクト変数は階層構造になっているが、ひとまずCatalog#parts_with_chaps内でCHAPS情報の配列にする。
- 配列の各要素を見ていって、Hashなら部、そうでない(String)なら章と見なせる。部の場合は部内の各章のChapterオブジェクトを作成し、最後に部のPartオブジェクトを作る。章だけなら章のChapterオブジェクトを作成する。
- part=0、num=0の初期状態からオブジェクトを作るたびにインクリメントすることで、採番をしている。chap.numberを入れない設定にしている(見出し指定で[nonum]を付けているなど)なら、採番を1つ戻す
- 全体をmapにしているとおり、最初はただのHashとStringの配列だったものが、PartオブジェクトとChapterオブジェクトの配列に書き変わる。
やれやれ。自分でも書いていてよくわからなくなりますね。ともあれ、これでようやくreview-compile内のbook.chapterに対して返せるChapterオブジェクトができました。
read_partsで前付・付録・後付時の対処については飛ばしていましたが、prefaces, appendix, postscriptsではそれぞれ、
- mkpart_from_namelist→mkchap_ifexistでChapterオブジェクトを作成
- mkpartで空の部を作成してその中にChapterオブジェクトを格納
- 部を全体の配列に挿入
という流れになっています。いやはや。
実は、これらの流れからRe:VIEWのカタログ記法の制約がわかります。
- 前付・付録・後付は部構成にすることはできない
- 「部」「章」以外の細分化はできない。AsciiDocやSphinxのように節や項の単位で細分化した原稿にしておいて結合という形はできない
後者は編集者という立場で言えば原稿が散逸するのは手間なので実現に気乗りはしないのですが、こういう書き方をするほうが好みというユーザーもいると思うのでどうしたものかというところです。今の時点では分散原稿のマージは、#@mapfileとreview-preprocを使うのが推奨される手段です。
追跡で疲れたので今日はここまで。