tDiaryでMarkdownスタイルが使いたかっただけなのに…

結論

CommonMarkの中の人は難しく考えすぎなんじゃないか?

個人的には、必要に応じてバックスラッシュなりバッククォートを使うので、愚直に変換して欲しい…

何があったか

最近Markdown形式で書く機会が増えたので、tDiaryでもMarkdown形式で記述できるようにしたいと思った。

  • 既存のスタイルを探して最初に見つかった
  • テーブル表記くらい使いたいので、オリジナルMarkdownでは機能が足りない
  • Githubで使われる拡張形式である

という理由でGFM(Github Flavored Markdown)スタイルを導入してみたのだが、記述していてすぐ、あまり直感的ではない動きをする事に気が付いた。

導入したのはTDiary::Style::Gfm

例えば「*test!*だよ!」等と記述した際、test!を<em></em>でマークアップして欲しいのに、マークアップされずにそのまま表示される。他の強調指定している部分では意図した通りにマークアップされている。

で、色々調べることにした。

TDiary::Style::Gfmのバグでは?

まず導入したスタイル自体のバグを疑った。

ソースコードを見ると一部の処理を除いてパースとHTML化はCommonMarkerというコンポーネントに丸投げしている事が判った。

調べると"CommonMarker"とはlibcmarkのRuby向けのラッパーで、libcmarkは"CommonMark"と言うMarkdownの派生仕様をCで実装したものなのだそうだ。

CommonMarkとは?

Why is CommonMark needed?によると、「(オリジナルのMarkdownは)仕様に曖昧な点が多く、perlで書かれたオリジナルの実装もバギー。それが放置された10年の間に数多く独自の実装が作られた結果、同じ記述でもレンダリング結果が異なる状況が生まれたので、標準的な仕様としてまとめた」ものらしい。仕様策定にはgithubやredditの人も関わっているようだ。

CJavaScriptで書かれたリファレンス実装が公開されている。

Markdownと同じ機能の範囲で、様々な記述についてそれらがどう描画されるべきか定義されている。

例えば、Markdownではアンダースコアが強調指定の記号として使われるので、本文中にスネークケースで記述した変数などがあると意図しない形でマークアップされてしまう。

m_snake_casem<em>snake</em>case という風になる。

これは意図した記述じゃないので、文字と連結されているアンダースコアはマークアップ符号とみなしてほしくない、といったCommonMarkを作った人たちが気に入らない動作と理想の動作について仕様書に延々と書かれている。

基本的には賛同できる仕様なのだが…🤔

CommonMarkを基準に、一部機能の削除やテーブルや自動リンクなどの機能を追加したものがGFM、という位置づけになるようだ。

※Markdown Extraなどの別の派生仕様についてはここでは割愛する。

CommonMarkの不可解な動作とその記録

本当は実装のソースコードを追い、問題があればPRでも送るべきなのだろうが、そこまでの気力は無いので今回遭遇した問題を検証するための最小限の記述をまとめておく。

検証データは以下の通り。

無効化 外側に文字なし 外側前後に文字あり 外側前方のみ 外側後方のみ
\*!い\* *!い* あ*!い*あ あ*!い* *!い*あ
\*い!\* *い!* あ*い!*あ あ*い!* *い!*あ
\*!い!\* *!い!* あ*!い!*あ あ*!い!* *!い!*あ
外側を記号に変更 ?*!い!*? ?*!い!* *!い!*?
\*い!い\* *い!い* あ*い!い*あ あ*い!い* *い!い*あ
外側を記号に変更 ?*い!い*? ?*い!い* *い!い*?
無効化 外側に文字なし 外側前後に文字あり 外側前方のみ 外側後方のみ
\*!b\* *!b* a*!b*a a*!b* *!b*a
\*b!\* *b!* a*b!*a a*b!* *b!*a
\*!b!\* *!b!* a*!b!*a a*!b!* *!b!*a
外側を記号に変更 ?*!b!*? ?*!b!* *!b!*?
\*b!b\* *b!b* a*b!b*a a*b!b* *b!b*a
外側を記号に変更 ?*b!b*? ?*b!b* *b!b*?

まずは本家のMarkdown.plを用いて、検証データの各項目を変換した結果を示す。

無効化 外側に文字なし 外側前後に文字あり 外側前方のみ 外側後方のみ
*!い* <em>!い</em> あ<em>!い</em>あ あ<em>!い</em> <em>!い</em>あ
*い!* <em>い!</em> あ<em>い!</em>あ あ<em>い!</em> <em>い!</em>あ
*!い!* <em>!い!</em> あ<em>!い!</em>あ あ<em>!い!</em> <em>!い!</em>あ
外側を記号に変更 ?<em>!い!</em>? ?<em>!い!</em> <em>!い!</em>?
*い!い* <em>い!い</em> あ<em>い!い</em>あ あ<em>い!い</em> <em>い!い</em>あ
外側を記号に変更 ?<em>い!い</em>? ?<em>い!い</em> <em>い!い</em>?
無効化 外側に文字なし 外側前後に文字あり 外側前方のみ 外側後方のみ
*!b* <em>!b</em> a<em>!b</em>a a<em>!b</em> <em>!b</em>a
*b!* <em>b!</em> a<em>b!</em>a a<em>b!</em> <em>b!</em>a
*!b!* <em>!b!</em> a<em>!b!</em>a a<em>!b!</em> <em>!b!</em>a
外側を記号に変更 ?<em>!b!</em>? ?<em>!b!</em> <em>!b!</em>?
*b!b* <em>b!b</em> a<em>b!b</em>a a<em>b!b</em> <em>b!b</em>a
外側を記号に変更 ?<em>b!b</em>? ?<em>b!b</em> <em>b!b</em>?

バックスラッシュで無効化した場合を除いて、全て'*'で囲った通りにマークアップされる。

次にcommonmark.jsを用いて、検証データの各項目を変換した結果を示す。

無効化 外側に文字なし 外側前後に文字あり 外側前方のみ 外側後方のみ
*!い* <em>!い</em> あ*!い*あ あ*!い* <em>!い</em>あ
*い!* <em>い!</em> あ*い!*あ あ<em>い!</em> *い!*あ
*!い!* <em>!い!</em> あ*!い!*あ あ*!い!* *!い!*あ
外側を記号に変更 ?<em>!い!</em>? ?<em>!い!</em> <em>!い!</em>?
*い!い* <em>い!い</em> あ<em>い!い</em>あ あ<em>い!い</em> <em>い!い</em>あ
外側を記号に変更 ?<em>い!い</em>? ?<em>い!い</em> <em>い!い</em>?
無効化 外側に文字なし 外側前後に文字あり 外側前方のみ 外側後方のみ
*!b* <em>!b</em> a*!b*a a*!b* <em>!b</em>a
*b!* <em>b!</em> a*b!*a a<em>b!</em> *b!*a
*!b!* <em>!b!</em> a*!b!*a a*!b!* *!b!*a
外側を記号に変更 ?<em>!b!</em>? ?<em>!b!</em> <em>!b!</em>?
*b!b* <em>b!b</em> a<em>b!b</em>a a<em>b!b</em> <em>b!b</em>a
外側を記号に変更 ?<em>b!b</em>? ?<em>b!b</em> <em>b!b</em>?

アスタリスクで囲った内側でアスタリスクに記号が隣接していて、かつアスタリスクの外側で文字が隣接している場合に'*'が無視されマークアップされない。文字や記号は全角半角には関係なく発生する。

強調指定に関するルールは、6.4Emphasis and strong emphasisのあたりに書かれているが、いずれのルールもここで問題にする症状には該当しない様に思う。