トップ «前の日記(2017年12月18日) 最新 次の日記(2017年12月21日)» 編集

KeN's GNU/Linux Diary


2017年12月19日

_ [reviewml] Re:VIEW Hacks (2) 〜コンパイラの仕組み

review-ext.rbでRe:VIEWの挙動を上書きするためには、当然ながらまずはRe:VIEWの仕掛けを理解しておく必要があります。実際のところ、私も青木さんの元コードを読んだりしながら習っているところがあり、完全には全容を理解しきれているとは言えません。

まずbin/にあってユーザーから呼び出されるreview-epubmaker, review-pdfmaker, review-compileあたりはオプション解析をする程度で、実体コードのほとんどはlib/reviewの中のライブラリ側に置いています。コマンド挙動を上書きするのはあまり意味がない(生成結果を加工処理するのはフック利用を期待している)ので、ここでは説明しません。

review-epubmaker, review-pdfmakerはReVIEW::Converter→ReVIEW::Compilerという呼び出し経路、review-compileはReVIEW::Compilerを直に呼び出して、与えられたRe:VIEW形式コンテンツを処理します。

ReVIEW::Compiler(lib/review/compiler.rb)は、以下の2つの重要な役割を担います。

  • 基本のインラインタグ、ブロックタグの宣言
  • ドキュメントパーサ

宣言は次のような形です。

 defblock :read, 0  (ブロックタグの宣言。1つめが名前、2つめが引数の数)
 defblock :lead, 0
 defblock :list, 2..3
 defblock :emlist, 0..2 (0〜2個の可変の引数をとる)
 ……
 defsingle :footnote, 2 (1行で使用する({〜//}を持たない)ブロックタグの宣言)
 defsingle :noindent, 0
 ……
 definline :chapref (インラインタグの宣言)
 definline :chap
 ……

あくまでもCompilerでは宣言するのみで、表現の実体は変換先、Re:VIEWで「ビルダ」と呼んでいる実装側に任されます。パーサで処理する際にどのようなタグが存在するのかわからないと困るので、このように設計されています。

パーサの中は、歴史的経緯もあって、現時点ではドキュメントオブジェクトモデルを構築するのではなく、正規表現で必死に処理しています(@takahashimさんがPEG版を開発していますが、正規表現パーサやビルダのあいまいさを消化するのに苦戦されているようです)。

 def do_compile
   f = LineInput.new(StringIO.new(@chapter.content))
   @strategy.bind self, @chapter, Location.new(@chapter.basename, f)
   tagged_section_init
   while f.next?
     case f.peek
     when /\A\#@/
       f.gets # Nothing to do
     when /\A=+[\[\s\{]/
       compile_headline f.gets
     when /\A\s+\*/
       compile_ulist f
     when /\A\s+\d+\./
       compile_olist f
     when /\A\s*:\s/
       compile_dlist f
     when %r{\A//\}}
       f.gets
       error 'block end seen but not opened'
     when %r{\A//[a-z]+}
       name, args, lines = read_command(f)
       syntax = syntax_descriptor(name)
       unless syntax
         error "unknown command: //#{name}"
         compile_unknown_command args, lines
         next
       end
       compile_command syntax, args, lines
     when %r{\A//}
       line = f.gets
       warn "`//' seen but is not valid command: #{line.strip.inspect}"
       if block_open?(line)
         warn 'skipping block...'
         read_block(f, false)
       end
     else
       if f.peek.strip.empty?
         f.gets
         next
       end
       compile_paragraph f
     end
   end
   close_all_tagged_section
 end

入れ子の処理などができないというのはこれに起因するものですが、実装としては把握しやすいですね。コアのパーサロジックがたったこれだけの行で済んでいるというのはちょっと驚きます。@strategyはビルダのオブジェクトです。

このパーサの時点ではインラインタグは処理せず、コメントの無視化、特例的なタグである見出し・箇条書き、ブロックタグ、そして残ったものを段落に、という区分けをしています。

それぞれの処理の中では、ビルダ(@strategy)に渡す情報を整理します。その際、テキストと見なされる箇所はtextメソッドでインラインタグを処理します。これもだいぶ必死な正規表現ですね。

 def text(str)
    return '' if str.empty?
    words = replace_fence(str).split(/(@<\w+>\{(?:[^\}\\]|\\.)*?\})/, -1)
    words.each { |w| error "`@<xxx>' seen but is not valid inline op: #{w}" if w.scan(/@<\w+>/).size > 1 && !/\A@<raw>/.match(w) }
    result = @strategy.nofunc_text(words.shift)
    until words.empty?
      result << compile_inline(words.shift.gsub(/\\\}/, '}').gsub(/\\\\/, '\\'))
      result << @strategy.nofunc_text(words.shift)
    end
    result.gsub("\x01", '@')
  rescue => err
    error err.message
  end
 ……
 def compile_inline(str)
   op, arg = /\A@<(\w+)>\{(.*?)\}\z/.match(str).captures
   raise CompileError, "no such inline op: #{op}" unless inline_defined?(op)
   raise "strategy does not support inline op: @<#{op}>" unless @strategy.respond_to?("inline_#{op}")
   @strategy.__send__("inline_#{op}", arg)
 rescue => err
   error err.message
   @strategy.nofunc_text(str)
 end

\x01という謎なものがありますが、これはフェンス記法のサポート(replace_fence)の副作用です。インラインタグでリテラルな}を使おうとすると\}とエスケープする必要があった(たとえば@<m>{\frac{1\}{2\}})のですが、これはTeX数式を書く際などにとても煩雑なため、@<m>$\frac{1}{2}$あるいは@<m>|\frac{1}{2}|といった代替の表記方法を提供します(あくまでも「代替」です)。このときに@が間に挟まっているといろいろと奇妙な動作を起こしてしまうため、いったん\x01に退避してから戻すという回避策をとりました(@znzさんのご提案)。インラインタグは以下の箇所のとおりビルダ側の「inline_タグ名」のメソッドで変換処理されます。

 @strategy.__send__("inline_#{op}", arg)

さて、こうしてできあがった文字列をビルダに渡します。たとえば見出しなら

  @strategy.headline level, label, caption

段落なら

  @strategy.paragraph buf

ブロックなら

  @strategy.__send__(syntax.name, (lines || default_block(syntax)), *args)

といった具合です。インラインタグが処理済みのものが渡されるという点に注意が必要です。

長くなりましたので今日はここまで。次はビルダの中に入っていきましょう。