Rubyのパターンマッチを使ってMarkdownからコードブロックを抜き出す

Rubyのパターンマッチを使って、(GitHub Flavored) Markdownのテキストからコードブロックを抜き出す。次のようなテキストを考える。

# Ruby
## hello world
Rubyでは次のようにhello worldします。
```ruby
puts 'hello world'
```
```
これはRubyではありません。
```
## error
Rubyでは次のようにエラーを起こします。
```ruby
raise 'error'
```

Markdownのパース

まず、このMarkdownをパースするために、kramdown (2.4.0)とkramdown-parser-gfm (1.1.0)を使う。Markdownをパースして、ASTのハッシュを変換する。

require 'kramdown'
require 'kramdown-parser-gfm'

doc = <<~EOS
    # Ruby
    ## hello world
    Rubyでは次のようにhello worldします。
    ```ruby
    puts 'hello world'
    ```
    ```
    これはRubyではありません。
    ```
    ## error
    Rubyでは次のようにエラーを起こします。
    ```ruby
    raise 'error'
    ```
EOS

ast = Kramdown::Document.new(doc, input: 'GFM').to_hash_ast

これで次のようなASTが取得できる。childrenにMarkdownテキスト上の各要素を持ち、それぞれ同じレベルに並んでいる。

{:type=>:root,
 :options=>
  {:encoding=>#<Encoding:UTF-8>,
   :location=>1,
   :options=>{},
   :abbrev_defs=>{},
   :abbrev_attr=>{},
   :footnote_count=>0},
 :children=>
  [{:type=>:header,
    :attr=>{"id"=>"ruby"},
    :options=>
     {:level=>1, :raw_text=>"Ruby", :location=>1},
    :children=>
     [{:type=>:text,
       :value=>"Ruby",
       :options=>{:location=>1}}]},
   {:type=>:header,
    :attr=>{"id"=>"hello-world"},
    :options=>
     {:level=>2,
      :raw_text=>"hello world",
      :location=>2},
    :children=>
     [{:type=>:text,
       :value=>"hello world",
       :options=>{:location=>2}}]},
   {:type=>:p,
    :options=>{:location=>3},
    :children=>
     [{:type=>:text,
       :value=>"Rubyでは次のようにhello worldします。",
       :options=>{:location=>3}}]},
   {:type=>:codeblock,
    :attr=>{"class"=>"language-ruby"},
    :value=>"puts 'hello world'\n",
    :options=>
     {:location=>4, :fenced=>true, :lang=>"ruby"}},
   {:type=>:codeblock,
    :value=>"これはRubyではありません。\n",
    :options=>{:location=>7, :fenced=>true}},
   {:type=>:header,
    :attr=>{"id"=>"error"},
    :options=>
     {:level=>2, :raw_text=>"error", :location=>10},
    :children=>
     [{:type=>:text,
       :value=>"error",
       :options=>{:location=>10}}]},
   {:type=>:p,
    :options=>{:location=>11},
    :children=>
     [{:type=>:text,
       :value=>"Rubyでは次のようにエラーを起こします。",
       :options=>{:location=>11}}]},
   {:type=>:codeblock,
    :attr=>{"class"=>"language-ruby"},
    :value=>"raise 'error'\n",
    :options=>
     {:location=>12, :fenced=>true, :lang=>"ruby"}}]}

配列とハッシュの構造に対するパターンマッチでコードブロックを抽出

このASTに対して、パターンマッチで特定の構造にマッチさせて値を取得できる。rubyinfo stringが付与されたコードブロックを抜き出すときは、次のようにchildrenの各ノードに対してパターンマッチを適用する。

# rubyのinfo stringが付与されたコードブロックを抜き出す
def extract_ruby_codeblocks(nodes, code_blocks)
  return if nodes.empty?

  # Markdownテキストの各要素のハッシュの配列に対してパターンマッチを実行する
  case nodes

  # この構造のハッシュにマッチするとき、キー:valueの値を変数valueに入れる。
  # マッチするハッシュの前後についてもパターンの適用が必要。
  # 最初にマッチするハッシュが出てくるまでの分は*でマッチさせて捨てる。
  # マッチしたハッシュより後ろの配列は*restという記法で変数restに入れる
  in *, { type: :codeblock, options: { lang: 'ruby' }, value: value }, *rest 
    code_blocks << value

    # restに対して繰り返しパターンマッチを実行する
    extract_ruby_codeblocks(rest, code_blocks)
  else
    # pass
  end
end

code_blocks = []
extract_ruby_codeblocks(ast[:children], code_blocks)

結果のcode_blocksの中身は次のとおり。rubyというinfo stringが付与されたコードブロックだけ抜き出せている。

["puts 'hello world'\n", "raise 'error'\n"]