markdown-itは、サーバー/クライアント問わず使える、MarkdownをHTMLに変換してくれるライブラリである。プラグインを追加すれば、独自の記法も手軽にサポートできる。
ちょっと見た感じ、だいたいのことは既存のプラグインを工夫すればなんとかなりそうだ。おそらく、いま私がやりたいこともなんとかなる。
なので見ないふりをして、自分で手を動かしてみたいと思う。
まずアーキテクチャを知る
markdown-itでは、パーサーによってマークダウンをトークナイズし、トークンの配列を作る(このトークンは入れ子になっている)。そしてその配列をレンダラーに渡し、それぞれのトークンをHTMLへ変換する。
このトークナイズとレンダリングは、それぞれルールと呼ばれる関数(というか概念)で対応づけられている。
ルールは、大きくcore、block、inlineの3種類のチェーン(≒グループ)に分けられる。coreには、パースの下準備をしたり、blockトークンとinlineトークンを作ったりするチェーンが含まれる。blockとinlineには、それぞれブロック要素、インライン要素の各トークンを作るチェーンが含まれる。
これら3つのチェーンは、ネストされて使われる。各チェーンを適用してトークンの配列を作ったら、今度はそのトークンタイプに対応するレンダラーのルールを適用して、各トークンをHTMLに変換していく。
パース段階とレンダリング段階で、2種類のルールが適用される点が微妙にややこしい。以降はパースで使うルールは無印の「ルール」とし、レンダラーで使うルールは「レンダールール」と呼んで区別する。
チェーンとルール
チェーンは、ルール関数のグループにつける名前である。例えばblockチェーン(というと余計ややこしいか)には、paragrahやblockquoteというチェーンが連なっている。
ルール関数は、Markdown記法がサポートする要素と1対1で対応する。開始タグ、コンテンツ、終了タグのようにトークナイズされるので、トークンと1対1の関係ではない。
また、HTMLは要素が入れ子になる。あるルール関数の中で別のルール関数を使うケースも当然出てくる。例えばblockquoteというトークンの中にはparagraphがあるかも知れず、別のblockquoteが入れ子になっているかも知れない。
そのためmarkdown-itでは、出力したいトークンに関連するルール関数を1つのチェーン名のもとにまとめ、ルール関数の中で適宜取得・適用できるような仕組みになっている。
例として、paragraphのルール関数を参照する。
なんだか再帰っぽく見えるかも知れないが、state.md.block.ruler.getRules('paragraph')で取得しているのはparagraphチェーンのルール関数群である。ここで定義しているparagraph()関数もそのチェーンには含まれているが、自身を取得しているわけではない。
デフォルトのチェーンに連なるルール関数は、それぞれのパーサーのソース(core、block、 inline)でそれぞれ確認できる。
tokenオブジェクト
markdown-itのパース処理は、入力されたテキストをtokenオブジェクトの配列(≒トークンストリーム)に置き換える。
tokenオブジェクトには、そのトークンの種類や変換するタグ、属性に関連するメソッドやプロパティが用意されている。
ルール関数は、与えられたテキストをこのトークンに分ける処理を担う。
ルールをあれこれするには
ルールをあれこれするには、core,block,inlineという3つのパーサーが持つRulerインスタンスのメソッドを使う。
Rulerインスタンスは、以下の8つのメソッドを持っている。なお、xxxは、core, block,inlineのいずれかである。簡便のためにオプションの引数は省略している。
ドキュメントにはルールとあるが、上記のメソッドで指定するルール(existed_ruleとかmy_ruleとか)はチェーン名と考えた方がわかりやすいかと思う。同名のチェーン名に対して、2つ以上のルール関数を渡すことができるからだ。
例えば、以下のようにすると、my_token1()関数とmy_token2()関数が両方実行される。
なお、markdown-itのプラグインは、markdown-itのインスタンスに、md.use(plugin, params)のような形で渡される。これはplugin(md, params)を呼び出すためのシンタクスシュガーである。
つまりプラグインはmd(=markdown-itのインスタンス)とparams(任意の引数)を1つ受け取る関数である。
だんだん込み入ってきた。
ルール関数の型
ルール関数は、大元となるcore、block、inlineのいずれかによって、異なる引数を受け取る。具体的には、チェーンごとに以下のように型づけされている。
それぞれの第一引数は、stateという名前は同じだが、実は違うインスタンスであることに留意したい。
例えばblockチェーンのルール関数は、第一引数にStateBlock、第二引数、第三引数に開始行のインデックスと最終行のインデックス、第四引数にトークンを生成しないか(バリデーションだけを行うか)を判断する真偽値を渡す。
ルール関数の中では、渡された情報をもとにトークンを探し、行数(state.line)を進めてトークンを生成、プッシュする。順番はどうでもよさそうだが、とにかくそんなことをする。
ルール関数は、トークンを生成したらtrue、マッチするパターンがなければfalseを返す。この値に応じて、パーサーは次の行へ処理を進めるか、次のルールを適用する。
inlineチェーンの場合も、行単位が文字単位になるだけで、似たり寄ったりである。
ルール関数を作る際は、このstateオブジェクトをあれこれして、任意のトークンをトークンストリームにプッシュすることになる。それぞれのstateオブジェクトの構造を知ることが不可欠だ。
StateCoreオブジェクト
StateCoreオブジェクトは以下のように定義されている。
coreチェーンのルールは、トークンを作るのではなく、入力テキストの下準備をしたり、block、inlineのそれぞれのパーサーを呼び出したりするために使われる。
coreのデフォルトのルールはここで参照できる。
StateBlockオブジェクト
StateBlockオブジェクトは以下のように定義されている。blockチェーンに連なるルールはここで参照できる。
StateInlineオブジェクト
StateInlineオブジェクトは以下のように定義されている。inlineチェーンに連なるルールはここで参照できる。
レンダールール
パーサーによってテキストからトークンストリームが生成されると、それはレンダラーに渡される。レンダラーはすべてのトークンをイテレートして、トークンタイプと同名のレンダールールを適用してHTMLに変換する。
レンダールールをあれこれするには
レンダールールをあれこれするには、レンダールール関数の中でtokensやRendererインスタンスのプロパティあるいはメソッドを使ってアレコレする。
レンダールール関数の型
まずレンダールール関数は以下のように型づけされている。
レンダールールを追加したり、変更したり際は、以下のようにmd.renderer.rulesにトークンタイプを直接指定し、関数を代入する。
レンダールールはトークンと1対1で対応づけられているため、パーサーのルールと違ってチェーンにできない。
そのためデフォルトのレンダールールの前段で何らかの処理をしたいときは、以下のように変数に保持した上でルール関数を定義し、その関数内でデフォルトのルールを呼び出す形を取る。
デフォルトのレンダールールは1つのファイルにまとまっているので、自作する際はこれを参考にすると良い。
自作プラグインの例
このブログのために、!demo-startと!demo-endの間にあるコードブロックの中身)を、タブ形式で表示できるような簡単なコンポーネントを出力できるようにしたい。
以下はそのために書いたお試しコードである。
これがmarkdown-itの設計意図に適うものかは謎だ。例えばtoken.hiddenの使い方とか、使用目的にかなっているのか謎だ(たぶんmetaを使うべきだろう)。
あんまり既存プラグインのコードとかも読んでいないので、これから勉強して詰めていきたいと思う。