Markdown のパーサーを書いている話

この記事は CAMPHOR- Advent Calendar 2018 12 日目の記事です。

こんにちは、 @km_conner です。

この記事では、自分が最近 C# で Markdown のパーサーを作っていることに関連して、 Markdown に関することなどをまとめました。

動機

Markdown を用いてレポートを書けたらいいなと思ったのがきっかけです。
しかしながら、 Markdown でレポートを書く上で、次のような機能が欲しくなります。

  • 数式を書く方法
  • 表紙を付ける機能

主に困るのは数式を書く方法がないことです。

そこで、 Pandoc によって拡張された Markdown を今まで使用してきました。
Pandoc Markdown ではこれらの機能が充実しています。

しかし、 Pandoc による Markdown から LaTeX への変換方法が自分にとってはあまり好みではないことや、いくつかの拡張構文を導入したいと思っていたこともあり、 Markdown -> LaTeX の変換ソフトを自作することになりました。

目標

目標は、 Markdown だけで快適にレポートが書ける環境を整えることです。

レポートは基本的に PDF 形式での提出が求められます。
LaTeX は、 PDF を出力する方法として長らく用いられてきました。
しかしながら、このフォーマットは機能が多い反面複雑すぎであると感じてしまう部分も数多くあります。
そこで、 LaTeX よりもシンプルに記述できる Markdown を使うことを考えました。

では、 Markdown に完全に移行して良いものかというと必ずしもそれが現実的ではない部分もあります。
というのも、完全に LaTeX を捨ててしまうと今まで作り上げてきた LaTeX によるコード資産 (デザインのテンプレートなど) が全て使えなくなることになるのです。

そこで、 Markdown を一旦 LaTeX のコードに変換してからそれを PDF にコンパイルすることにしました。
この方法を使えば既存の LaTeX のテンプレートなどを生かすことができるはずです。
(この方法自体は Pandoc の内部でも行われていることですが、 Pandoc よりも LaTeX への出力方法をよりフレキシブルに行えるように工夫したいと考えています。)

また、拡張構文としては GitHub Flavored Markdown (GFM) による拡張の他に以下のようなものを検討しています。

  • 数式をパースする機能
  • 複数のファイルを組み合わせる機能 (LaTeX の \input のような機能)
  • ドキュメント内でリンクを張ることのできる機能
  • 画像のサイズを指定する機能

Markdown とは

Markdown の始まりは https://daringfireball.net/projects/markdown にある仕様と同サイトからダウンロードできる Perl で実装された HTML への変換スクリプト (いわゆる Markdown.pl) です。

しかしながら、この仕様は Well-defined ではなく、他にもいくつかの問題がありました。

また、構文自体が比較的拡張しやすいものであったためオリジナルの仕様をより厳密にしたものや、独自に拡張した構文を追加したものなど、様々な仕様が誕生しました。

これらの仕様の間には機能が異なっているものがあるのに加え、同じ機能でもインデントの大きさなど微妙な違いがあるために互換性がないものもあります。そのため、単に Markdown とは言っても同じ仕様に基づいて書かれたドキュメントとは限らないので注意が必要です。

その中でも今回作成するパーサーの元としている仕様は CommonMark です。

CommonMark とはこれまでに書いたような Markdown.pl の仕様を Well-defined にした仕様です。また、 ``` によって囲まれたコードブロック (Fenced code block) の機能などが追加されています。

GitHub で使用されている GitHub Flavored Markdown (GFM) の元となっている仕様でもあります。

GFM によって追加された機能には

  • 取り消し線
  • タスクリスト項目

などがあります。
(詳細は長くなるので割愛します。)

パーサーの大まかな実装方法・動作

親切なことに、 CommonMark の仕様書の Appendix に構文解析の方法が簡単に書かれており、主にその方法に従って C# で実装しました。
C# を選んだ理由は自分が使い慣れていたこと、様々な OS、 デバイスで動かせることです。

パーサーの大まかな動作としては
  1. ドキュメント全体からブロックの構造を構成する。
  2. 参照リンクの定義を抽出する。
  3. 必要に応じて各ブロックのインライン構造を解析する。

といった方法です。
(これより詳細な部分はコードをそのまま説明するような感じになってしまうので省略します。)
とにかく、文法として冗長性が高い (等価な内容を複数の表し方ができる) のはパーサーを作るのは大変だなぁと感じました。。。

現状と、これから

現在の進捗状態としては CommonMark の機能が 85% 程度実装できたといった感じです。

大まかには完成しているものの、いくつかの細かなバグがあるといった状態です。
CommonMark  の仕様書には 600 通り以上のテストケースがあり、その一つ一つを実装していく中で、バグが見つかってはそれを修正するという 終わるのかどうかひたすらに怪しい 作業を行なっている途中です。
また、コードがあまりにも汚い & コメントも不十分なため、到底人様に見せられるようなものではないため、現在は Private Repository となっています。

完成後 (あるいは未完成でもコードの整理が出来次第) には GitHub でソースコードも含めて公開する予定ですので、もし興味を持った方はもうしばらくお待ちください。

おわりに

この記事で伝えたかったのは、

  • 単に Markdown といっても様々な仕様がある (それに加えてとてもよく似ていることが多いのではっきりとわからない) ので、正しいはずなのにフォーマットがおかしいと感じた時はそこを疑うと良い。
  • 冗長性の高い言語は人間には優しいが機械には厳しいこともある。
  • そうはいっても Markdown はいいぞ!!!

です!

パーサーを書くというのはかなり地味な作業です。しかし、どのプログラミング言語、マークアップ言語もパーサーが必ず必要です。この記事をきっかけにしてその存在に少しでも興味を持ってもらえると幸いです。

最後まで読んでいただきありがとうございました。

リンク

この記事を書くにあたって参考にしたサイトや、記事中で紹介されているドキュメントなどの仕様・リファレンスです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です