PureBuilder2 (2)
有用なユーティリティコード
- TOP
- Old Archives
- PureBuilder2 (2)
Kramdown拡張でPDocオブジェクト化
PureBuilder2はもともと思っていたよりもかなり大規模なものになっているが、MarkdownオブジェクトをPureDocと同様に扱えるようにする、というのが今回のテーマ。
例えばテンプレートで
DOC.body
のように書かれている場合がある。この場合は当然、HTMLへ変換したのであればHTML文字列が得られなくてはいけない。 また、
DOC.meta["title"]
のようにもアクセスできる。 それだけなら単にアクセッサを拡張してやればいい話なのだが、PureDocオブジェクトはTOCのためのループ機能が組み込まれている。 これにより章立てをループさせることができ、簡単に任意の形式でTOCを組める。 これはどうしてもパース時に情報を取らなくてはいけない。
もし、HTMLに出力するものである、というのであれば、単純に結果のHTMLをパースして取得する方法もある。 だが、KramdownライブラリはLaTeXとPDFをサポートする。PureDocもゆくゆくはLaTeX形式での出力をサポートする予定である。
であれば、やはりKramdownでのMarkdownパース時にTOCを作りたい。
基本的な方針としては、実際にPureDoc
オブジェクトを使用する。
これはパーサ/コンバータを含まないベースクラスで、本来は直接このクラスのインスタンスを生成することは想定していなかった。
だが、外側から使用するメソッドは一通り持っており、インターフェイスは揃っている。
DOC.body
で返すべき@body
はDOC.body=
を用いて入れ、DOC.meta
に関してはPureDoc
クラスが持っている機能によってドキュメントから取り込むといったことが可能。
そのため、DOC
はPureDoc
インスタンスであり、Kramdownの結果はDOC.body=
によって入れるだけだ。
だが、DOC.stock_ehader
を用いてヘッダを入力し、TOCを生成できるようにしなければいけない。
そこで、Kramdownに手を入れる必要があった。
ソースコードを追っていったが、結局Kramdown::Parser::Kramdown#new_block_el
をオーバーライドするのが良いと分かった。
ヘッダを取得するパートはあるが、new_block_el
メソッドはメソッド自体が短く、あくまでパース時に各エレメント対して呼ばれるものだ。何のために呼ばれているかを判定する必要もなく、引数を丸々渡すだけで良いため、overrideしやすかった。
require 'kramdown'
# Override Kramdown
class Kramdown::Parser::Kramdown
alias _new_block_el_orig new_block_el
def new_block_el(*arg)
if arg[-1].kind_of?(Hash)
case arg[0]
# Is Header?
when :header
p arg[-1][:level]
p arg[-1][:raw_text]
end
end
_new_block_el_orig(*arg)
end end
p Kramdown::Document.new(ARGF.read).to_html
というテストコードを書き、実際に動作することを確認、when :header
部分を
::DOC.stock_header(arg[-1][:level], arg[-1][:raw_text])
と書き換えた。
KramdownはPure Rubyで書かれているため、扱いやすいし、ソースコードを書くのも楽だ。 だが、できればサブクラス化するなど、もう少しスマートな方法でできればよかったな、と思う。クラスが細かく分割されて連携しているため、置き換えるのはかなり難しいと判断した。
Kramdownは非常に良いライブラリなのは間違いない。
forkの代わりに
RubyのKernel.fork
をはじめとするfork機能(例えば、IO.popen
で-
を渡すことを含む)はWindowsでは動作しない。
Perlerだった私としてはこれはかなり不満な点だ。Perlはコミュニティの努力により、forkがWindows上で動作する。これは、Windows版Perlではforkをエミュレートするためだ。
今回は、設定やドキュメントオブジェクトなどをセットアップした状態で、forkによって環境を独立させたいと考えていた。
これはグローバルなオブジェクトに変更を加えるためであり、また出力先の制御をSTDOUT.reopen
によって行うことができるかということについて考えていたためだ。
RubyのforkとWindowsについて検索すると、「forkは邪悪だ、threadを使え」という内容があふれる。 だが、今回は並列化のために使いたいわけではないため、Threadは用を成さない。
また、大量のドキュメントを変換する際のオーバーヘッド低減という目的もある。
Unicorn(Webアプリケーションサーバー)がこのforkによるCOWを活用した設計となっている。Unicornはどうしているのかと調べてみたら、UnicornもMongrelもWindowsでは動作しないらしい。
というわけで、forkの利用は諦めて、グローバルな名前に対する変更をいなす方向とした。
グローバルな名前のオブジェクトが変更されるのは、ほとんど
DOC.is {
...
}
という書式で記述するためだ。
これはPureDocドキュメントを分かりやすく記述するためであり、実際にテンプレートもDOC
オブジェクトを利用したデザインとなっている。
つまり、DOC
はthe PureDoc
objectであることを期待している。
この設計を維持するため、Delegateライブラリを使用することとした。
実体はDOC
ではなく、DOC
はただのDelegatorというデザインだ。これはDOCに実体はなく、ただの代名詞となるわけだ。
DOC = SimpleDelegator.new(nil)
とすることにより、まずDOC
という名前を用意しておく。
実際に新しいドキュメントを生成する場合は、
::DOC.__setobj__ @@config[:puredoc_class].new
のようにする。
これにより、DOC
が意味するドキュメントを入れ替えることができ、DOC
を変更しても、変更されるのはDOC
ではなく、移譲されているドキュメントであり、DOC
をまた新しいドキュメントにすることもできる。