CommonMark における HTML の取り扱いを理解したい
Markdown の表現では表現できないものを HTML を使って表現しているとき、Markdown が理解されないことがある。これがどのようにして起こるのかを主眼に理解していきたい。
本記事では Markdown ソースと HTML によるパースの結果を示しています。このパースの結果は commonmark.js によるものです。(ただし、代表的なタグとして <tag> のような表記を使用しています)
■ Markdown における HTML
Markdown における HTML の利用は邪道のようにも感じられるが、John Gruber による元祖 Markdown では次のように書かれている。
For any markup that is not covered by Markdown's syntax, you simply use HTML itself.
INLINE HTML - Daring Fireball: Markdown Syntax Documentation
すなわち、「Markdown で表現できないところは HTML で表現しましょう」としている。そのため、HTML を使うことは邪道ではなく、真っ当な使い方である。
■ CommonMark による HTML の制限
Qiita 等の各種 Markdown を利用したサービスでは HTML は制限されている場合がある。これはスタイルの変更などの任意の変更や意図しないプログラムの追加を防ぐためである。
しかし、CommonMark にはこれらの制限について記述はない。CommonMark では HTML の利用を制限するものではなく、Markdown 内の HTML の挙動の仕様を決定している。そのため、HTML タグが閉じているかどうかなど、HTML タグそのものの仕様は定義していない。
CommonMark では Markdown ソース内の Markdown と HTML の棲み分けのみを定義していると考えると良い。
■ HTML ブロックと raw HTML
Markdown 内で利用される HTML 要素は HTML ブロックと raw HTML に大別される。
-
HTML ブロック:
HTML 要素をブロックとして解釈し、このブロック内は HTML として解釈される。そのため、HTML ブロックは行頭(あるいは半角スペース 3 つ分までのインデント)から始まる。
HTML ブロックは 7 つのタイプに分類され、パラグラフの中断の有無によってタイプ 1~6 とタイプ 7 で大別される。
-
raw HTML:
パラグラフ内の HTML 要素を指し、この HTML 要素は生の HTML として解釈される。
これらの仕様から、パラグラフを中断するタイプ 1~6 の HTML タグはブロックレベルの HTML 要素のみで使用し、raw HTML として利用しない方が良い。これは # この節 で示したい。
■ 7 つの HTML ブロック種
HTML ブロックは 7 種のタイプがある。また、これらの内側は HTML として解釈される。そのため、それぞれの HTML ブロックには開始条件と終了条件がある。ただし、開始条件は常に行頭に置かれる。
| HTML ブロック種 | タイプ 1 | タイプ 2 | タイプ 3 | タイプ 4 | タイプ 5 |
|---|---|---|---|---|---|
| HTML 要素 |
pre, script, style, textarea
|
HTML コメント | 処理命令 | 宣言 | CDATA セクション |
| パラグラフの中断 (interrupt) |
可 | 可 | 可 | 可 | 可 |
| 開始条件 | <tag> |
<!-- |
<? |
<! |
<[CDATA[ |
| 終了条件 | </tag> |
--> |
?> |
> |
]]> |
| raw HTML | 不向き | 可能 | 不可 | 不可 | 不可 |
| 属性 | 可 | - | - | - | - |
タイプ 1~6 では、終了条件がある行も HTML ブロックに含まれる。
| HTML ブロック種 | タイプ 6 | タイプ 7 |
|---|---|---|
| HTML 要素 | 特定のHTML タグ (62 種) |
タイプ 1、タイプ 6 以外の任意名の HTML タグ |
| パラグラフの中断 (interrupt) |
可 | 不可 |
| 開始条件 |
<tag>、<tag/>、</tag>
|
<tag>、<tag/>、</tag>
|
| 終了条件 | 空白行 | 空白行 |
| raw HTML | 不向き | 可能 |
| 属性 | 可 | 可 |
タイプ 1・6・7 のタグは以下のようなものを採る。
| HTML ブロック種 | タグ |
|---|---|
| タイプ 1 |
pre, script, style, textarea
|
| タイプ 6 |
address, article, aside, base, basefont, blockquote, body, caption, center, col, colgroup, dd, details, dialog, dir, div, dl, dt, fieldset, figcaption, figure, footer, form, frame, frameset, h1, h2, h3, h4, h5, h6, head, header, hr, html, iframe, legend, li, link, main, menu, menuitem, nav, noframes, ol, optgroup, option, p, param, section, source, summary, table, tbody, td, tfoot, th, thead, title, tr, track, ul
|
| タイプ 7 | タイプ 1、タイプ 6 を除く任意のタグ ( a, br, code, del, em, img, ins, kbd, mark, q, samp, strikethrough, strong, sub, sup 等) |
本記事では、タイプ 3・4・5 については取り上げない。これは、利用される頻度が低いことや筆者にとってあまり興味がないためである。
▽ HTML タグの要件
HTML タグとして解釈されるための要件は、タイプ 1・6・7 で異なる。
タイプ 1 の HTML タグは pre、script、style、textarea のいずれかであり、それぞれの開きタグ(<pre>、<script>、<style>、<textarea>)、属性を含んだ開きタグ、閉じタグ(<pre>、<script>、<style>、 <textarea>)が要件となる。
タイプ 6 の HTML タグは、< または </ から開始され、特定の 62 のタグ名が続き、> または /> で閉じられる。
しかし、<tag の時点でタイプ 6 の HTML ブロックとして認識される。
<div
*in HTML tag*
ただし、これは有効な HTML タグではないため、HTML としては機能しない。(Garbage In, Garbage Out)
タイプ 7 の HTML タグは、開始タグと終了タグそれぞれの要件が決められている。
-
開始タグ (open tag):
<から開始され、タグ名、属性(オプション)、>または/>で閉じられる。 -
終了タグ (closing tag):
</から開始され、タグ名、>で閉じられる。
これらのタグが正常に書かれていない場合、タグとして解釈されず、リテラルに表示される。
■ よく使用する HTML タグ
タイプ 6 に含まれているタグの内、以下のようなものはよく利用することだろう。
- ブロッククォート (
blockquote) - リスト (
li,ul,ol) - 説明リスト (
dd,dl,dt) - 折りたたみ (
details) - 本文 (
p) - 表 (
table,tbody,tfoot,thead,td,th) - 見出し (
h1,h2,h3,h4,h5,h6)
タイプ 7 に含まれるタグはタイプ 1・6 以外の任意の名前のタグだが、デフォルトで提供されているタグであれば、strong や em などのインラインレベルの HTML 要素が多い。
タイプ 6・7 の開始条件と終了条件は共通しており、HTML ブロックの開始条件は <tag>、<tag/>、</tag>、終了条件は空白行となっている。そのため、タグの開始・終了を問わず、タグがあることそのものが HTML ブロックの開始条件となっている。
ただし、タイプ 6 はパラグラフを中断するのに対して、タイプ 7 はパラグラフを中断しない。そのため、HTML タグがパラグラフの次行にある場合、タイプによって取り扱いが異なる。
| 違い | タイプ 6 | タイプ 7 |
|---|---|---|
| パラグラフの中断 (interrupt) |
有 | 無 |
| パラグラフ次行の HTML タグの扱い |
HTML ブロック | raw HTML |
これについては、次節で具体例を用いながら理解したい。
▽ タイプ 6
タイプ 6 では、行頭に置かれたタグそのものが HTML ブロックを開始し、パラグラフを中断する。そのため、以下のように表現することが出来る。
ここは *Markdown*
<div>
Markdown ではない
ここは *Markdown*
</div> *Markdown* ではない
ここは **Markdown**
ここは Markdown
ここは Markdown
ここは Markdown
この場合、HTML ブロックの内側となっている部分では Markdown を理解しない。そのため、HTML ブロックの内側では半角スペース 4 つ以上のインデントをしても Indented code block と解釈されない。
行頭にあるタイプ 6 のタグが 2 行以上に渡る場合でも、それぞれは HTML ブロックとして解釈される。
<tag1>
<tag2>
タイプ 6 の HTML タグは、パラグラフである <p> を中断して別の要素が開始される。しかし、Markdown から HTML にパースされる際に、</p> が不用意に追加されてしまうため、不正な HTML を作成しかねない。raw HTML としてタイプ 6 のタグを利用するべきではない。
Paragraph text <tag>
</tag>
⇓(<tag> は raw HTML、</tag> は HTML ブロックとして解釈されている)
<p>Paragraph text <tag></p>
</tag>
このため、タイプ 6 の HTML タグは常に行頭に置き、HTML ブロックとして解釈されるべきである。
HTML にとって不正な Markdown の raw HTML(折りたたみ)
パラグラフ内に HTML によるリストを含む場合を考えたとき、HTML では不正とされる書き方を許してしまうことになる。
As follows: <ul><li>item 1</li><li>item 2</li></ul> is important.
⇓(commonmark.js によるもの)
<p>As follows: <ul><li>item 1</li><li>item 2</li></ul> is important.</p>
これは HTML Standard で不正だとされている。
NOTE
List elements (in particular,
olandulelements) cannot be children ofpelements.
しかしながら、パースされた HTML は Markdown ソースから考えられる結果となるわけではないらしい。
<p>
As follows:
</p>
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
is important.
これは CommonMark が仕様として規定している挙動とは異なるように思われるが、HTML の仕様の上に Markdown が成り立つと思えば、これは正しいパース結果であると思われる。ただし、is important. には <p> の外に出てしまっている。これはベストな結果とはなっていない。
▽ タイプ 7
タイプ 7 もタイプ 6 と同様に行頭に置かれたタグそのものが HTML ブロックを開始するが、パラグラフは中断しない。
Paragraph text <tag>
</tag>
⇓(<tag>・</tag> は raw HTML として解釈されている)
<p>Paragraph text <tag>
</tag></p>
タイプ 7 のタグは行頭で 1 つのみを使用する場合、HTML ブロックとして解釈される。例えば、独立した <img> を利用する場合、以下のようになる。
Paragraph text.
<img src = "link/to/image.png" width = "80%" alt = "Alternative text">
Paragraph text.
⇓
<p>Paragraph text.</p>
<img src = "link/to/image.png" width = "80%" alt = "Alternative text">
<p>Paragraph text.</p>
行頭にあるタグが前後いずれかのパラグラフに接する場合には、パラグラフ内の raw HTML として解釈される。
**ここ**はパラグラフ内の
<strong>
強調
</strong>
ここはパラグラフ内の
強調
Qiita では、Markdown ソース内の改行を <br> として解釈されるため、不自然な結果を得てしまう。しかし、HTML によるパースでは全体が <p> で囲われているため、<strong>・</strong> は raw HTML として解釈されている。
改行が含まれた HTML(折りたたみ)
<p>
<strong>ここ</strong>はパラグラフ内の
<br>
<strong>
<br>
強調
<br>
</strong>
</p>
また、前行が空行かつ HTML タグが行頭から始まっている場合でも、続けてテキストが続く場合には raw HTML として解釈される。
<tag> Paragraph *text*.
⇓
<p><tag> Paragraph <em>text</em>.</p>
行頭にあるタイプ 7 のタグが 2 行以上に渡る場合であっても、タグは raw HTML、全体がパラグラフとして解釈される。
<tag1>
<tag2>
⇓
<p><tag1>
<tag2></p>
これを踏まえれば、同じ行内に 2 つ以上のタグが並ぶ場合も raw HTML として解釈される。
<tag1><tag2>
⇓
<p><tag1><tag2></p>
混乱してしまいそうだが、以下のような場合はタイプ 7 の HTML ブロックとなる。そのため、<p> で囲われないものとなる。HTML タグ内の Markdown は解釈されないものの、HTML タグによる強調は機能する。
<strong>
`強調`
</strong>
⇓
<strong>
`強調`
</strong>
<strong> は本来 <p> 内にあるべきなので、この文と接するような形で表示されてしまっている。
HTML でパースされている様子(折りたたみ)
<strong>
`強調`
</strong>
<p><code><strong></code> は本来 <code><p></code> 内にあるべきなので、この文と接するような形で表示されてしまっている。</p>
これでは、<strong> `強調` </strong> が次行の文章の一部のように感じてしまう。
パラグラフのある行の HTML タグは、当然 HTML ブロックとして解釈されず、raw HTML として解釈される。
ここはパラグラフ内の <strong>強調</strong>。
ここはパラグラフ内の 強調。
■ タイプ 2(コメントアウト)
コメントアウトは <!--~--> で囲うことで表現されるが、いくつかの注意点がある。
HTML ブロックとしてコメントアウトを利用する場合、コメントアウトの終了条件 --> のある行(右側)も HTML ブロックとして解釈されるため、Markdown をリテラルに理解する。
⇩
<!--
コメントアウトの終了条件のある行も HTML ブロック
-->*Here is HTML blocks*
*Here is Markdown*
⇩
*Here is HTML blocks*Here is Markdown
また、raw HTML の際にはこれの内部に含むことのできる文字列には 3 つの制限がある。
-
コメントアウトの開始 (
<!--) 直後に>・->を置くことは出来ない(この書き方は HTML ブロックであっても不正なものになる)Markdown⇨ <!--> ← コメントアウト開始の直前に `>` を置く --> ⇦⇨ ← コメントアウト開始の直前に
>を置く --> ⇦Markdown⇨ <!---> ← コメントアウト開始の直前に `>` を置く --> ⇦⇨ ← コメントアウト開始の直前に
>を置く --> ⇦ -
コメントアウト終了の直前 (
-->) に-を置くことは出来ないMarkdown⇨ <!-- コメントアウト終了の直前に `-` を置く → ---> ⇦⇨ ⇦
-
コメントアウトの途中に連続する 2 つ以上の
-を置くことは出来ないMarkdown⇨ <!-- コメントアウト内に `--` を置く --> ⇦⇨ ⇦
2 や 3 について、Qiita (commonmarker) ではコメントアウトとして解釈されるようだ。![]()
これらを踏まえて、ベターな書き方は以下の要件を満たせば良い。
-
コメントアウト開始直後、コメントアウト終了直前に半角スペースを置く
Markdown⇨ <!-- > コメントアウトの開始直後に半角スペース、コメントアウト終了直前に半角スペース - --> ⇦⇨ ⇦
-
連続する 2 つ以上の
-が含まれる場合は、コメントアウト全体を独立させる(コメントアウトを開始する前に文字を置かない)Markdown⇩ *Markdown* <!-- コメントアウト内に --- --> ⇧ **Markdown**⇩ Markdown
⇧ Markdown
コメントアウト開始直後・終了直前の半角スペースは改行でも良い。そのため、上の 2 つの要件を合わせることで、以下のようにコメントアウトを表現できる。おそらく、この “開始条件と終了条件を独立させる” 方法がベストになるだろう。
⇩
<!--
> コメントアウトの開始直後に半角スペース
コメントアウト内に ---
コメントアウト終了直前に半角スペース -
-->
⇧
⇩
⇧
過剰な対策になるが、コメントアウトの前後を空白行で挟んで独立させておいても良い。(タイプ 2 のコメントアウトはパラグラフの中断 (interrupt) するため、空白行で挟む必要はない)
ちなみに、コメントアウトを raw HTML に解釈させるときには、Markdown をきちんと解釈する。
ここは *Markdown* <!-- コメントアウト --> ここも **Markdown**
ここは Markdown ここも Markdown
しかしながら、前後の Markdown がきちんと解釈されるかどうかを編集中に留意し続けることは面倒になる。そのため、コメントアウトは HTML ブロックとして Markdown に接さないようにしておくべきだろう。
HTML におけるコメントアウトの仕様について(折りたたみ)
上のような制約が加わっている理由は、CommonMark におけるコメントアウトの仕様が HTML のコメントアウトの仕様を踏襲しているためである。raw HTML 3 つ目の “-- を含めない” 制約の仕様を思うと、HTML 5.1 までの仕様を参照しているようだ。
§ 8.1.6. Comments
Comments must start with the four character sequence
U+003CLESS-THAN SIGN,U+0021EXCLAMATION MARK,U+002DHYPHEN-MINUS,U+002DHYPHEN-MINUS (<!--). Following this sequence, the comment may have text, with the additional restriction that the text must not start with a singleU+003EGREATER-THAN SIGN character (>), nor start with aU+002DHYPHEN-MINUS character (-) followed by aU+003EGREATER-THAN SIGN (>) character, nor contain two consecutiveU+002DHYPHEN-MINUS characters (--), nor end with aU+002DHYPHEN-MINUS character (-). Finally, the comment must be ended by the three character sequenceU+002DHYPHEN-MINUS,U+002DHYPHEN-MINUS,U+003EGREATER-THAN SIGN (-->).§ 8.1.6. Comments - HTML 5.1 2nd Edition: 8. The HTML syntax(2017/10/03, W3C 勧告)
要約すると、<!-- で開始し、--> で終了することに加えて、コメントアウトされるテキストには以下の制限が決められている。
-
>から開始しない -
-から開始しない - 2 つの連続する
-(--) を含めない -
-で終了しない
ただし、2 つめの “- から開始しない” は CommonMark には決められていない。そのため、CommonMark におけるコメントアウトでは <!--------- のような開始を認めている。
ちなみに、HTML 5.1 以降の HTML 5.2(2017/12/14 勧告)や現在の HTML Living Standard では、異なる仕様となっている。
§ 13.1.6 Comments
Comments must have the following format:
- The string
<!--.- Optionally, text, with the additional restriction that the text must not start with the string
>, nor start with the string->, nor contain the strings<!--,-->, or--!>, nor end with the string<!-.- The string
-->.
■ タイプ 1
タイプ 1 の HTML ブロックには pre、script、style、textarea が含まれる。これらには属性を含めることが出来る。
開始条件は <tag>、終了条件は </tag> のため、HTML として解釈される部分は以下のようになる。
ここは外側
<tag>
ここから~
~ここまで HTML ブロック
</tag> ここも HTML ブロック
ここは外側
上で示したように、</tag> のある行は HTML ブロックとして解釈されるため、これの後ろで Markdown を利用することが出来ない。(<p> タグにさえ囲われない)
また、タイプ 1 の HTML ブロックは raw HTML で解釈させるべきではない。これは、HTML ブロック内に意図しない </p> が含まれてしまうためである。
Paragraph text <textarea>
</textarea>
⇓
<p>Paragraph text <textarea></p>
</textarea>
■ 異なるタイプの組み合わせ
タイプ 1 とタイプ 6 の HTML ブロックがある場合の例が Example 148 にある。
<table><tr><td>
<pre>
**Hello**,
_world_.
</pre>
</td></tr></table>
タイプ 6 の <table><tr><td> の次行にタイプ 1 の <pre> を配置した場合、HTML ブロックは先にあるタイプ 6 を優先するため、**Hello** はリテラルに解釈される。一方、_world_. は Markdown として解釈される。すなわち、タイプ 1 の <pre> はタイプ 6 の一部のように振る舞う。
★ Markdown におけるパラグラフ
Markdown におけるパラグラフとは、パースした結果が <p> で囲われるブロックを指す。
Paragraph text
⇓
<p>Paragraph text</p>
このような HTML へのパースに伴って <p> で囲われる部分でのみ Markdown を理解するようになる。
Paragraph *emphasis* text
⇓
<p>Paragraph <em>emphasis</em> text</p>
これは Markdown ソースから HTML にパースした際に <p> に囲われるようになる。このような部分に関してのみ Markdown による記法が正しく解釈される。
文章が HTML ブロック内にある場合、文章は Markdown のパラグラフとして解釈されず、Markdown 記法によるインラインによる装飾が解釈されない。
ちなみに、Markdown ソース内で <p> によって囲われた文は HTML ブロックとして解釈される。
<p>*Markdown*??</p>
*Markdown*??
<p> のある行は HTML ブロックとして解釈されるため、*Markdown* は強調として認識されない。
参考
余談
さて、この記事で「HTML ブロックについてよく分かった」と言える人はいるだろうか。
HTML 交じりに Markdown を使っていると、HTML タグの内側では Markdown を利用できないように感じてしまう。しかし実際は、row HTML に解釈される場合には、HTML と Markdown をまぜこぜに利用しても意図した通りに解釈される。
HTML タグ内で Markdown が利用できなかった理由は、タイプ 6 の HTML ブロック内で Markdown を利用していたためだった。これを理解していれば、HTML タグ内で Markdown を利用してもきちんと解釈させられるだろう。
なんかこうもっと分かりやすい記事にした方が良かったのでは???(自戒)
ちなみに、Markdown のブロック内に HTML タグをネストした場合、Container blocks では HTML ブロックや raw HTML となるのに対して、Leaf bolcks では常に raw HTML となる。たとえば、表内で利用する HTML は raw HTML であり、HTML の内側で Markdown を利用することが出来る。おそらくこの情報が一番有益…。
GFM における HTML タグの制限
§6.11Disallowed Raw HTML (extension) - GitHub Flavored Markdown Spec
GFM では以下の 3 種 9 つのタグに制限が加わっている。
- タイプ 1
<script><style><textarea>
- タイプ 6
<iframe><noframes><title>
- タイプ 7
<noembed><plaintext><xmp>
ただし、実際にはこれよりも数多くの HTML タグが制限されている。(制限するなら GFM を改訂してほしい)
Qiita における HTML タグの制限
Qiita では制限の形ではなく、許可(ホワイトリスト)の形でタグが制限されている。
これは以下のファイルから確認できる。
qiita-markdown/lib/qiita/markdown/filters/user_input_sanitizer.rb
elements: %w[
a b blockquote br caption code dd del details div dl dt em font h1 h2 h3 h4 h5 h6
hr i img ins kbd li ol p pre q rp rt ruby s samp script iframe section strike strong sub
summary sup table tbody td tfoot th thead tr ul var
],
ただし、このホワイトリストに含まれるようなタグは、任意の属性を含められるわけではない。注意が必要になる。