今回はRuby library のkramdown のソースコードリーディングを行います。 kramdown はMarkdown をparse し、HTML 等任意の形式に変換する事ができるRuby のlibrary です。 普段からMarkdown を書く機会も多く、どの様に実装されているのか気になっておりました。 ということで、今回はMarkdown からどのようにHTML が生成されているのかといった箇所を中心に見ていきたいと思います。
目的
- Markdown をHTML に変換する箇所の実装
基本情報
Version
コード量
lib
配下のコードは5,830 ですね。前回のactive_hash が800 ちょっとでしたので、単純計算すると7倍ほどあります。。。
最初から最後まですべて読んでいくのは大変そうなので、要点を絞って見ていきたいと思います。
$ cloc ./kramdown/lib 64 text files. 64 unique files. 0 files ignored. github.com/AlDanial/cloc v 1.80 T=0.07 s (869.0 files/s, 121110.0 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Ruby 64 1171 1919 5830 ------------------------------------------------------------------------------- SUM: 64 1171 1919 5830 -------------------------------------------------------------------------------
階層構造
$ tree ./kramdown/lib ./kramdown/lib ├── kramdown │ ├── converter │ │ ├── base.rb │ │ ├── hash_ast.rb │ │ ├── html.rb │ │ ├── kramdown.rb │ │ ├── latex.rb │ │ ├── man.rb │ │ ├── math_engine │ │ │ ├── itex2mml.rb │ │ │ ├── katex.rb │ │ │ ├── mathjax.rb │ │ │ ├── mathjaxnode.rb │ │ │ ├── ritex.rb │ │ │ └── sskatex.rb │ │ ├── pdf.rb │ │ ├── remove_html_tags.rb │ │ ├── syntax_highlighter │ │ │ ├── coderay.rb │ │ │ ├── minted.rb │ │ │ └── rouge.rb │ │ ├── syntax_highlighter.rb │ │ └── toc.rb │ ├── converter.rb │ ├── document.rb │ ├── element.rb │ ├── error.rb │ ├── options.rb │ ├── parser │ │ ├── base.rb │ │ ├── gfm.rb │ │ ├── html.rb │ │ ├── kramdown │ │ │ ├── abbreviation.rb │ │ │ ├── autolink.rb │ │ │ ├── blank_line.rb │ │ │ ├── block_boundary.rb │ │ │ ├── blockquote.rb │ │ │ ├── codeblock.rb │ │ │ ├── codespan.rb │ │ │ ├── emphasis.rb │ │ │ ├── eob.rb │ │ │ ├── escaped_chars.rb │ │ │ ├── extensions.rb │ │ │ ├── footnote.rb │ │ │ ├── header.rb │ │ │ ├── horizontal_rule.rb │ │ │ ├── html.rb │ │ │ ├── html_entity.rb │ │ │ ├── line_break.rb │ │ │ ├── link.rb │ │ │ ├── list.rb │ │ │ ├── math.rb │ │ │ ├── paragraph.rb │ │ │ ├── smart_quotes.rb │ │ │ ├── table.rb │ │ │ └── typographic_symbol.rb │ │ ├── kramdown.rb │ │ └── markdown.rb │ ├── parser.rb │ ├── utils │ │ ├── configurable.rb │ │ ├── entities.rb │ │ ├── html.rb │ │ ├── lru_cache.rb │ │ ├── ordered_hash.rb │ │ ├── string_scanner.rb │ │ └── unidecoder.rb │ ├── utils.rb │ └── version.rb └── kramdown.rb
使い方
使い方は非常にシンプルです。 まずは、gem をインストール。
$ gem install kramdown
require 'kramdown' doc = Kramdown::Document.new('This *is* some kramdown text') puts doc.to_html
# Output #=> <p>This <em>is</em> some kramdown text</p>
Markdown をHTML に変換したい場合は、上記sample のようにKramdown::Document
のインスタンスを作成します。
この時、変換したいtext を引数に指定します。
あとは、生成したKramdown::Document
に対して、to_html
メソッドを呼んであげるだけです。
非常に簡単ですね。
実装内容
Markdown をHTML に変換する箇所の実装
Kramdown::Document
class に、to_html
メソッドが定義してあって、to_html
メソッドの中でMarkdown からHTML への変換処理が行われているものだと思っておりました。
#lib/kramdown/document.rb def method_missing(id, *attr, &block) if id.to_s =~ /^to_(\w+)$/ && (name = Utils.camelize($1)) && try_require('converter', name) && Converter.const_defined?(name) output, warnings = Converter.const_get(name).convert(@root, @options) @warnings.concat(warnings) output else super end end
しかし、実際にソースを覗いてみるとto_html
メソッドは存在せず、BasicObject
のmethod_missing
メソッドをoverride することで、HTML への変換処理を実現していました。
method_missing
は、メソッドが呼び出された時に継承チェーン内に該当するメソッドが定義されていなかった時に呼ばれるメソッドです。
つまり今回のケースでは、Kramdown::Document
のインスタンスの継承チェーンでは、to_html
メソッドが未定義、その結果method_missing
が呼ばれるという流れになっています。
このような実装になっているのは、Kramdown がHTML 以外の出力形式にも対応していることに起因するのかと思います。
出力形式ごとにto_~
メソッドを定義するのではなく、一旦method_missing
を呼び出しその後の処理を決定しています。
あまり詳しくありませんが、この辺りはメタプログラミングと呼ばれるような分野なのでしょうか???
近く、メタプログラミングについても学んでみたいと思います。
具体的にMarkdown をHTML に変換している箇所は、lib/kramdown/converter/html.rb
のceonver_~
のメソッドでした。
それぞれ要素によって~
の部分を動的に変更し、convert~
を呼び出し、Markdown からHTML に変換しています。
恐らくこれもメタプログラミングってやつなんですかね。。。
気になった箇所・メソッド
- Struct => 構造体クラス。クラスのサブクラスを新しく生成。
- const_get(module)
- autoload(module)
- send(object) => メソッドの可視性と関係なくメソッドを呼び出すことが可能。
まとめ
to_html
やto_~
をそれぞれ定義するのではなく、BasicObject
のmethod_missing
をoverride することで出力形式別に処理を出し分けていました。
恐らくこれがメタプログラミングというやつですね。これを機にメタプログラミング勉強してみたいと思います。
結果的にMarkdown からHTML への変換箇所の実装というより、出力結果による処理の出しわけ箇所を中心に読み進めてしまいました。。。 次回は脱線しない様に気をつけながらソースコードリーディングを行いたいと思います。
この前誰かが「Commit message に注目した、ソースコードリーディングも面白い。」と、言って気がするので、そのうち挑戦してみたいと思います。 確かにこの方法だと実装した人がどの様な目的で当該の機能を実装したのかがわかるので面白そうです。
それでは、本日はこの辺で!