もう少し、複雑なドキュメントを変換してみよう。
このbox noteのJSON ASTを抜粋するとこんな感じである。
{ "doc": { "type": "doc" }, "content": [ { "type": "heading", "attrs": { "level": 1, "guid": "m3hfvv22" }, "content": [ { "type": "text", "text": "headline one" } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "first paragraph." } ] }, { "type": "heading", "attrs": { "level": 2, "guid": "m3hfwceu" }, "content": [ { "type": "text", "text": "headline two" } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "second paragraph. " }, { "type": "text", "marks": [ { "type": "strong" }, { "type": "author_id", "attrs": { "authorId": "304774041" } } ], "text": "Bold font message" }, { "type": "text", "text": "." } ] }, { "type": "paragraph" }, { "type": "bullet_list", "content": [ { "type": "list_item", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "list1" } ] } ] }, { "type": "list_item", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "list2" } ] } ] }, { "type": "list_item", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "list3" } ] } ] } ] } ] } }
要するに、ASTの1つ1つのノードにはtype
とcontent
という属性があり、子ノードはcontentに入っているという構造だ。ざっくり書くと、上のJSONはこういう構造になっている
- doc
- heading
- text
- paragraph
- text
- heading
- text
- paragraph
- text
- text(marks = strong)
- text
- bullet_list
- list_item
- paragraph
- text
- paragraph
- list_item
- paragraph
- text
- paragraph
- list_item
- paragraph
- text
- paragraph
- list_item
- heading
今のサンプルだと、リーフは全部textになっているが、これがbox noteのルールなのかどうかは現時点ではわからない。 これらのASTのノードは、基本的にpandocのASTノードにも対応するものがある。なので、Lua Readerの基本戦略は以下だ。
- 各ノードのtypeごとに、JSONをparseしたtableを受け取って、pandocのASTを返す関数を作る
- 自分に子ノードがいる場合には、子ノードのtypeをチェックして、子ノード部分はそのtypeの関数を呼び出して取り込む。 つまり、ノードを順にたどることが、関数呼び出しで続いていく
というわけで、典型的なノードの処理は、こんな関数になる。
local function box_heading(t) local l = t.attrs.level local inlines = {} for _, v in ipairs(t.content) do if v.type == 'text' then table.insert(inlines, box_text(v)) end end return pandoc.Header(l, inlines) end
- ノード固有の処理をする。headingだと見出しレベルを取得する
- contentの中のノードを1つずつ取り出してtypeをチェックし、typeごとの関数を呼び出してpandoc ASTのリストに変換する
- 最終的にノード固有の属性と、子ノードのリストを引数に、そのノードタイプのコンストラクタを呼び出して結果をそのまま返す。
これを全部のノードごとに作ってやればいいわけ。とりあえず、ベタに出てくるノードタイプの数だけ実装して実験してみた。
> cat sample_box_note.boxnote | pandoc -f boxnote1.lua -t native [ Header 1 ( "" , [] , [] ) [ Str "headline one" ] , Para [ Str "first paragraph." ] , Header 2 ( "" , [] , [] ) [ Str "headline two" ] , Para [ Str "second paragraph. " , Emph [ Str "Bold" , Space , Str "font" , Space , Str "message" ] , Str "." ] , BulletList [ [ Para [ Str "list1" ] ] , [ Para [ Str "list2" ] ] , [ Para [ Str "list3" ] ] ] ]
思い通りに変換出来ているように思う。太字のところは、boxnoteのASTでは「属性で太字と指定されたtext」という1ノードなんだけども、pandocでは「Strを子供に持つEmph」なので工夫が必要なんだけど、自分でStrを作らずにEmphに文字列をそのまま入れちゃったらどうなるか試したら、こうなった。勝手にスペースで切ってStrとSpaceのリストにされたけど、むしろありがたい。
出力をHTMLにしてみると、ちゃんと期待通りのHTMLになっていることがわかる。
> cat sample_box_note.boxnote | pandoc -f boxnote1.lua -t html5 <h1>headline one</h1> <p>first paragraph.</p> <h2>headline two</h2> <p>second paragraph. <em>Bold font message</em>.</p> <ul> <li><p>list1</p></li> <li><p>list2</p></li> <li><p>list3</p></li> </ul>
仕組みとしてはこれでOKなので、後はbox nodeのASTを全部調べて、1つずつ処理して関数を作ればゴールに近づいていくけど、遊んでみたいモチベーションは満たされたので、続きをやるかどうかは未定。
どちらかというと、ノードのtype別にすごく似てるけどちょっとずつ違う関数をたくさん書いてしまったので、これをまとめるリファクタリングをやりたいかな。