SEAN_BLOG

プログラミング etc.

Ruby kramdown のソースコードリーディング

今回は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 メソッドは存在せず、BasicObjectmethod_missing メソッドをoverride することで、HTML への変換処理を実現していました。 method_missing は、メソッドが呼び出された時に継承チェーン内に該当するメソッドが定義されていなかった時に呼ばれるメソッドです。 つまり今回のケースでは、Kramdown::Documentインスタンスの継承チェーンでは、to_html メソッドが未定義、その結果method_missing が呼ばれるという流れになっています。 このような実装になっているのは、Kramdown がHTML 以外の出力形式にも対応していることに起因するのかと思います。 出力形式ごとにto_~ メソッドを定義するのではなく、一旦method_missing を呼び出しその後の処理を決定しています。 あまり詳しくありませんが、この辺りはメタプログラミングと呼ばれるような分野なのでしょうか??? 近く、メタプログラミングについても学んでみたいと思います。

具体的にMarkdown をHTML に変換している箇所は、lib/kramdown/converter/html.rbceonver_~ のメソッドでした。 それぞれ要素によって~ の部分を動的に変更し、convert~ を呼び出し、Markdown からHTML に変換しています。 恐らくこれもメタプログラミングってやつなんですかね。。。

気になった箇所・メソッド

まとめ

to_htmlto_~ をそれぞれ定義するのではなく、BasicObjectmethod_missing をoverride することで出力形式別に処理を出し分けていました。 恐らくこれがメタプログラミングというやつですね。これを機にメタプログラミング勉強してみたいと思います。

結果的にMarkdown からHTML への変換箇所の実装というより、出力結果による処理の出しわけ箇所を中心に読み進めてしまいました。。。 次回は脱線しない様に気をつけながらソースコードリーディングを行いたいと思います。

この前誰かが「Commit message に注目した、ソースコードリーディングも面白い。」と、言って気がするので、そのうち挑戦してみたいと思います。 確かにこの方法だと実装した人がどの様な目的で当該の機能を実装したのかがわかるので面白そうです。

それでは、本日はこの辺で!