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
を使うべきだろう)。
あんまり既存プラグインのコードとかも読んでいないので、これから勉強して詰めていきたいと思う。