対象リポジトリ
背景課題
herb のツールセットの中に HTML + ERB ファイルのチェックをしてくれる Linter が含まれる。
そのルールの1つに html-no-block-inside-inline というものがあるのだけど、
Linter Rule: No block elements inside inline elements | Herb
偽陽性になっている!というIssueが目についていた。
- Unknown element <option> cannot be placed inside inline element <select>. · Issue #248 · marcoroth/herb · GitHub
- Linter: Diagnostic issue: `html-no-block-inside-inline` · Issue #255 · marcoroth/herb · GitHub
- Linter: Unknown element `<a-custom-element>` cannot be placed inside inline element · Issue #260 · marcoroth/herb · GitHub
- Linter: Diagnostic issue: `html-no-block-inside-inline` option cannot be placed inside element select · Issue #272 · marcoroth/herb · GitHub
- Linter: Possible diagnostic issue: `html-no-block-inside-inline` · Issue #291 · marcoroth/herb · GitHub
ということでデフォルトでは無効化され「後でなんとかする」というIssueが鎮座している。
HTML要素の配置を block or inline で決定するということへの違和感
block or inline というのは CSS の display だったり、フローレイアウトで出てくる概念。
たしかに inline 要素の子要素に block 要素は持って来れないという認識はあるのだけど、 block or inline って特定要素に一意に決まるんだっけ?ということを思った。思ったので HTML Spec を確認する。
- https://html.spec.whatwg.org/multipage/dom.html#content-models
- https://html.spec.whatwg.org/multipage/grouping-content.html#the-div-element
- https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-span-element
解釈としては Content Model で決定するということになる。例えば span 要素の下に div が置けないという規則もそれぞれに記載されている Content Model と Categories を照らし合わせれば確認可能。
既存 html-no-block-inside-inline ルールのそもそもの方針が間違っていそうなので、これは直したくなった。
Issue にコメントを残して開発に着手
いちおう作者はどう考えているかを確認したかったのでコメントを投げておいた。
https://github.com/marcoroth/herb/issues/267#issuecomment-3690764419

レスから「Content Model ベースで作ったらええがな」という気配を感じたので開発を始める。
地域コミュニティで HTML Linter あたりの現状について質問してみる
東葛.devという地域コミュニティに参加させてもらっているのだけど、フロントエンド方面に明るい方が何人かいらっしゃる気配を感じていたので「最近の HTML Linter 事情ってどないでっか?」という質問を投げてみた。Content Model 関連の Lint ルールがあれば参考にしてみようという魂胆でした。

markuplint はいいぞ〜という情報を得る。markuplintはいいぞ〜
あとこういう時に気軽に質問できる地域コミュニティもいいぞ〜
markuplint も参考にしつつ、実装方針を練る
さて、Content Model をベースにする場合、HTML Spec に記載されている全ての要素の Categories と Content Model を網羅しておく必要があるなと考えていた。markuplint を参考にしてもそこらへんの情報をデータとして保持している。ルックアップテーブル的な何かを作る必要がある。ただ HTML Spec に書いてある内容を手で書き写していくのもダルいのでまずはスクレイピングして雛形データを作ることにした。
https://gist.github.com/koji-nakamura-classi/aac4ea9ba6ed5acdd8819bd6fc5ba11f#file-parser-rb
以下のような JSON データの集合をゲットできた。
:
"p": {
"categories": [
"flow"
],
"contentModel": [
"phrasing"
],
"link": "https://html.spec.whatwg.org/multipage/grouping-content.html#the-p-element"
},
:
Content Model のパターンごとの実装
JSON データを単純にルックアップすれば終わりか、と思いきやそんなに話はシンプルではなかった。
1. 単純ルックアップパターン
子要素の Categories が親要素の Content Model に適合していれば OK になる。これは最もシンプルなパターン。
2. Transparentで祖先要素に移譲されるパターン
例えば <a> タグ。この Content Model は Transparent 。
https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element
これは何かというと <a> タグの親タグの Content Model がみるべきものになるやつ。
<div><a>...</a></div> の場合は <div> の Content Model が適用されるので Flow 、<span><a>...</a></span> の場合は <span> の Content Model が適用されるので Phrasing ということになる。
3. 特定タグが許可されるパターン
例えば <ul> タグと <li> タグ。 <li> タグは Categories が None. となっており、 <ul> タグといった特定タグでのみ利用可能な扱いとなっている。
- https://html.spec.whatwg.org/multipage/grouping-content.html#the-ul-element
- https://html.spec.whatwg.org/multipage/grouping-content.html#the-li-element
4. 親要素・祖先要素によって Content Model が変わるパターン
<div> と <span> がそう。
- https://html.spec.whatwg.org/multipage/grouping-content.html#the-div-element
- https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-span-element
ただ祖先が <option> or <optgroup> or <select> というやつは「カスタマイズ可能な select 要素」という文脈っぽく、
カスタマイズ可能な select 要素 - ウェブ開発の学習 | MDN
ポイントとしては <div> 要素が <dl> 要素内で利用可能で、その場合に限り <dt> <dd> あと script-supporting elements を受け入れるという話があるということ。
最終的に作成した PR
色々試行錯誤を重ねてこのような PR が仕上がった。
Content Model の全てを実装しようとすると実装が巨大で複雑になりそうだったので html-no-block-inside-inline の上位互換にはなる程度に留めて PR を出してみた。コメントにも書いたのだけど、この段階でもIssueを6個ほどまとめて解決できそうなのがとても良い。

作者から年明けにフィードバックが貰えると嬉しいな。
今回のまとめ
- オープンスタンダードな仕様(今回の話であれば HTML Spec)をちゃんと押さえておくの大事だなぁと改めて思った
- 久しぶりに TypeScript をちゃんと書いたのだけど、仕様の語彙をいい感じに型やコードへ落として込めた気がする
- 地域コミュニティはいいぞ〜