世界を動かす技術を、日本語で。

Markdownにおけるネストされたコードフェンス

概要

  • Markdownのコードフェンスインラインコード記法 の落とし穴と回避策を解説
  • CommonMarkおよび GitHub Flavored Markdown (GFM) に準拠した仕様説明
  • バッククォートやチルダ を使った安全な記述方法の紹介
  • 具体例と HTMLレンダリング結果 の比較
  • CommonMark仕様の 該当セクション抜粋 による根拠提示

コードフェンスとインラインコードの落とし穴と回避策

  • 主人公Corey Dummは Markdownのコードフェンス 内で生活するキャラクター
  • Markdownの実装は多様で、 レンダリングルールが微妙に異なる 現実
  • 本記事は CommonMark仕様 に基づく解説、 GFMにも同様に適用可能

基本的なコードフェンス

  • Coreyが子供の頃、 シンプルなコードブロック で問題なく描写
    • 例:
      (o_o)--.|[_]|
      
  • HTML変換では 期待通りのコードブロック として表示

フェンスの事故とその原因

  • Coreyが成長し、 フェンスの数が増えた ことで問題発生
    • 例:
      (o_o)--.|[_]|
  • 2つ目のトリプルバッククォートが コードブロックを即終了 させ、残りが外に出てしまう事故
  • バッククォートが Coreyの髪の毛 として消失する現象

回避策:ファンシーなコードフェンス

  • CommonMarkでは チルダ(~)もフェンスとして利用可能
    • 例:
      ```
      (o_o)--.|[_]|
      
  • バッククォートやチルダの個数も3つ以上なら自由
    • 例:
      ```
      (o_o)--.|[_]|
      
    • 例:
      ```
      (o_o)--.|[_]|
      
  • これらの方法でコードブロック内にバッククォートを安全に含められる

基本的なインラインコードスパン

  • インラインコードはバッククォート1つで囲むのが一般的
    • 例: (o_o)--.|[_]|
  • しかし、コード内にバッククォートを含む場合は問題発生
    • 例: (o_o) ← バッククォートが区切りとして機能し、意図通り表示されない

回避策:ファンシーなインラインコードスパン

  • インラインコードスパンのデリミタは複数のバッククォートでも可
    • 例: `(o_o)`
  • また、 デリミタ直後・直前のスペース は1つだけ自動で除去される仕様
  • このテクニックで バッククォートを含む文字列も安全に囲める

CommonMark仕様抜粋と根拠

  • コードフェンス
    • 3つ以上の連続したバッククォートまたはチルダで囲む
    • 開始と終了のフェンスは 同じ種類・同じ以上の個数 でなければならない
  • インラインコードスパン
    • バッククォート1個以上で囲み、 中身にバッククォートを含めたい場合はより長いデリミタを利用
    • 両端にスペースがある場合、 1つずつ除去 される

まとめ

  • Markdownのバッククォート事故 を防ぐには、 チルダや複数個のデリミタ を活用
  • CommonMark仕様 を理解することで、 意図通りのレンダリング が可能
  • Markdown記法の 落とし穴と回避策 を知ることで、より柔軟なドキュメント作成が実現

Hackerたちの意見

こういう複雑さは、結局のところ、フェンスに明確な開始と終了のマーカーがないことから来てるみたいだね。最初も最後も同じ記号("`"や"~")で、違う記号を使わないから余計にややこしくなる。バックティックやチルダの数が違うと、さらに混乱するし。なんでそんな曖昧さを加えるんだろう?正しく解析するのが難しくなるだけなのに。これで「4つのバックティックでブロックを始めて、5つで終わらせたらどうなるの?」って疑問が浮かぶよね。もっと考えられたデザインや記号の選択をしてれば、こういう複雑さは避けられたはず。例えば、ブラケットを使えばよかったのに:[[[言語コードここ]]]. もしネストしたいなら、自動的に動くべきだよね:[[[html htmlコード [[[css cssコード]] [[[js jsコード]] htmlコード]]]]. もし「[[[」をそのまま出力したいなら、通常のプログラミング言語みたいにバックスラッシュでエスケープすればいい。パーサーでは、もっと簡単に解析できると思う。S式を解析するのと似たようなもんだし。4つのバックティックや5つ、もっと多くは必要ないよ。ドキュメントの中でバックティックを数えるのは面倒だし、ネストされたコードブロックのどの部分に属するかを知るために数えたくない。ほんとに馬鹿げたデザインだよ。

確かに!この問題に直面したのは、自分用に作ったおもちゃアプリの時だったけど、その時はマークダウンパーサーを$LANG構文だけ読み込むようにして、は閉じタグとして扱うようにしたんだ。これで、オープニングタグとしては受け付けなくなったから、きれいな構文フォーマッターも楽に仕事できるようになったよ。

もし「[[[」をそのまま出力したいなら、通常のプログラミング言語のようにバックスラッシュでエスケープすればいい。時々、大きなコードの塊をコードブロックに貼り付けたい時があって、その内容をエスケープするのは、開始と終了のデリミタを修正するより難しいんだ。特にMarkdownでは、大きなコードやテキストを埋め込むのが一般的だから、他の言語なら別ファイルに入れるところだよね。だから、開くブラケットと閉じるブラケットの数を変更できる機能を提案するよ。そうすれば、開くブラケットで始まるコンテンツを区別するための暗黙の改行や他の方法も必要になるだろうね。

ここでの問題に対するあなたの解決策は、別の文字でエスケープすることだね。MDの解決策は、もっと特別な文字を追加すること。どちらも有効だし、他の言語にも存在するから、一方が他方よりも考えられているとは言えないよ。ただ、変更されたくないテキストについて話しているから、テキストに入って一つずつエスケープするより、ティックを追加する方が好みかな。複雑さは、明確な開始と終了の欠如から来ているわけじゃなくて、単一のブロックに複数の言語がある時に、各言語にきれいな色を付けたい時のことを解決しようとしているんだ。HTMLはpreタグの入れ子をサポートしていない(あるいは、次の中に埋め込まれたものをレンダリングしない)から、純粋なHTMLを生成せずに何かを作るのは難しいかもしれない。> パーサーでは、もっと簡単に解析できる。可変数の`を解析するのは、閉じる境界を見つけるのと同じくらい複雑じゃないよ。実際、エスケープ文字を導入すると、エスケープ文字のエスケープを処理する必要があって、少し複雑になるんだ。

あなたの解決策が基本的にタグを使うことだって気づいてる?それがMarkdownが開発された理由で、タグを使わないためなんだよ。Markdownでコードブロックを挿入するクラシックな方法は、コードをインデントすることだよ。

これ、特にLLMで使えるかもしれない。いつもコードフェンスで何かを頼むから。マークダウンをコードフェンスで頼むと、全部崩れちゃうんだ。でも、もし~~~コードフェンスでマークダウンを頼んだら、世界は正しくなる。だって、通常は```を使うから。

昨晩、文字列内で~~~を区切り文字として使うコードをデバッグしてたんだ。少なくとも、君が言うように、狂ったように~~~~~を使って回避することもできるね。

マークダウンのパーサーって、全く例外とコーナーケースだけで構成された不思議なアノマリーみたいだね。

笑、これは今まで表現できなかったことをうまく言い表してるね。時々考えるんだけど…Markdownの仕様の混沌が成功の理由なのかな?もしかしたら、使える範囲ギリギリの仕様で、誰でも自分なりの実装を作れるくらいの小ささだったのかも。失敗する資格がないって感じで。それで広まったんだね。でも、xkcdの問題は本当に残念だよ。少なくとも、"Standard"を指し示したい人のためにCommonMarkがあるけどね。

「Markdown」には仕様がなく、曖昧な部分が多い構文の説明しかないんだ。それに、22年前に書かれたPerlのリファレンス実装があって、それ以降は完全に放置されてる。CommonMarkは包括的な仕様で、リファレンス実装とテストスイートもあるよ。

ハッカーニュース大好き!ここで役立つことを学べるから。昔は、これを回避するためにHTML要素を使ってたよ。

マークダウンにとっては矛盾するかもしれないけど、すべてに長さプレフィックスを付けるのが、かなり低コストで色々楽にするって感じてる。デリミタや開始/終了タグに依存するものは、必然的に難しい引用ルールや他の面倒な仕組みに陥るからね(ここで見たように)。

これをmdview.ioのレンダリングベンチマークとして使うよ。

これはGitHubのコメント提案でコードブロックを追加する方法でもあるよ、参考までに。 suggestion この例はこうするべきだよ: ```basic 10 PRINT "LOL" 20 GOTO 10 ```

そうだね、これがJupyterBookでもやってる方法だよ(たぶんv1はMyst Markdownパーサーを使ってると思う)。これがすごくうまくいくことを見つけたよ!

もし````を表示したい場合はどうするの?`````タグを追加すべきかな?

これ、任意の深さで動くの?`````ts const Accordian = styled.details````css accent: rebeccapurple; font-family: ${ ```js props => props.isAprilFirst ? 'Comic Sans' : 'Time New Roman' ``` }; ````; ````

自分の記法をデザインしてた時にこの問題に直面したよ。テキスト内の最大連続バックティック数よりも多くのバックティックでコードを囲むことで解決した。これで任意のネストが可能になるんだ。Postgresは$something$ whatever $something$を使って解決してるよ。

コードをテキスト内の連続したバックティックの最大数よりも多くのバックティックで囲むことで解決したんだ。これで任意のネストが可能になる。つまり、TFAで説明されているように、Markdownがやってるのと同じことだよね?

ああ、YAMLとMarkdown、テクノロジーの美しい偶然だね。後から仕様を考えて、厳密なパーサーでいろんな問題を解決できなかったのが不思議で仕方ない。確かに、既存のものが壊れちゃうかもしれないけど、その痛みは多分価値があると思う。

CommonMarkはまさにそういう後付けの仕様だね。

これ知らなかった。なんかMIMEマルチパートメッセージ(メールの添付ファイルやMMSなどで使われる)を思い出すね。ヘッダーには「境界」タグが含まれていて、パーサーがそのタグを探してパートを終わらせるんだ。ちょっと変な感じがする。もしファイルが境界が何になるかを知っていたら、境界がずれて悪意のあるファイルに変わるリスクがあるように思える。