Markdownで使えるタスクリストを実装する

Markdownを利用する場合の拡張機能として、GitHubやQiitaで利用されているようなタスクリスト機能を実装する方法について説明します。実装については github/task_listincrements/tasklist.js で公開されているので、これらの実装方法の説明ということになります。

Markdown

Markdownの変換を行う処理では、下例のような文字列が含まれたMarkdownをHTMLに変換するときに、うまくチェックボックスが表示されるように取り計らう必要があります。

- [x] Foo
- [ ] Bar
- [ ] Baz
  • [x] Foo
  • [ ] Bar
  • [ ] Baz

そこで、MarkdownをHTMLに変換したあとに実行する処理として「もしli要素の内容が [ ] [x] で始まっていたら、その部分を <input type="checkbox"> に置換する、という処理を加えます。どのように処理を追加しているかという点については、Markdownを拡張して独自記法をつくる - Qiita により詳しい説明が載っています。ソースコード上では、Qiita::Markdown::Filters::Checkbox がその処理を担当している部分になります。

JavaScript

JavaScript側では「チェックボックスが選択されたとき、Markdownの文字列中から該当するタスクリスト記法の行を判断し、[ ][x] を入れ替え、入れ替えたあとの文字列をサーバに送信して更新を依頼する」という処理を加える必要があります。選択されたチェックボックスと、Markdown内の対応する該当行。これを結び付けるには、コードブロックの中に含まれるタスクリストを無視しつつ、本文中のタスクリストを表現するそれぞれの行に番号を振り、何個目のチェックボックスがクリックされたかという情報と紐付けます。

github/task_list

github/task_listでは、正規表現を利用して本文からコードブロックを取り除いた文字列を別途用意したあと、本文を先頭から1行ずつ見ていき、その行がタスクリストっぽいパターンで且つコードブロックを取り除いた文字列に含まれている場合に、番号を振っています。

- [ ] Foo
- [ ] Bar
  • [ ] Foo
  • [ ] Bar

この方法には微妙に不具合があり、tasklist-test.md のように「コードブロックの内側にも外側にも同じ文字列で構成されたタスクリストの行がある」という条件下では、コードブロックの内部のタスクリストにも番号を割り当ててしまうため、意図しない文字列が更新されてしまいます (Forkすれば自分でチェックボックスを選択して試せます)。まあそんなケースはあまり無いので大丈夫でしょう。

increments/tasklist.js

increments/tasklist.js でも同じように番号を割り当てる方法で対応関係を調べています。先述した問題への対応として、コードブロック内の文字列を取り除くのをやめて、代わりに先頭に適当な文字列を付与しています。置換前と置換後で同じ行の文字列が同一であることを確認することで、コードブロック内のタスクリストが誤判定されてしまうことを防いでいます。

XXX-[ ] Foo
XXX-[ ] Bar

おしまい

以上、Markdownで使えるタスクリストを実装するための素朴な実装方法について説明しました。実際には、クライアント側ではなくサーバ側で処理するように最適化を加えたり、特に更新頻度が高いところなので、楽観的ロックを使って競合を判定するなどの処理を入れる余地があると思います ( Railsでコンフリクトをvalidate - Qiita も良かったらどうぞ )。最近はMarkdownを利用したサービスなども増えてきたので、実装時の参考になればと思います。