OSS活動記 #5 - herb - HTML Spec にある Content Model に準拠しているかチェックする Linter ルールを作る

対象リポジトリ

github.com

背景課題

herb のツールセットの中に HTML + ERB ファイルのチェックをしてくれる Linter が含まれる。

Herb Linter | Herb

そのルールの1つに html-no-block-inside-inline というものがあるのだけど、

Linter Rule: No block elements inside inline elements | Herb

偽陽性になっている!というIssueが目についていた。

ということでデフォルトでは無効化され「後でなんとかする」というIssueが鎮座している。

github.com

HTML要素の配置を block or inline で決定するということへの違和感

block or inline というのは CSSdisplay だったり、フローレイアウトで出てくる概念。

CSS フローレイアウト - CSS | MDN

たしかに inline 要素の子要素に block 要素は持って来れないという認識はあるのだけど、 block or inline って特定要素に一意に決まるんだっけ?ということを思った。思ったので HTML Spec を確認する。

解釈としては 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.dev

あとこういう時に気軽に質問できる地域コミュニティもいいぞ〜

markuplint も参考にしつつ、実装方針を練る

さて、Content Model をベースにする場合、HTML Spec に記載されている全ての要素の Categories と Content Model を網羅しておく必要があるなと考えていた。markuplint を参考にしてもそこらへんの情報をデータとして保持している。ルックアップテーブル的な何かを作る必要がある。ただ HTML Spec に書いてある内容を手で書き写していくのもダルいのでまずはスクレイピングして雛形データを作ることにした。

書き捨てのスクリプトRuby で書くに限る。

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> タグといった特定タグでのみ利用可能な扱いとなっている。

4. 親要素・祖先要素によって Content Model が変わるパターン

<div><span> がそう。

ただ祖先が <option> or <optgroup> or <select> というやつは「カスタマイズ可能な select 要素」という文脈っぽく、

カスタマイズ可能な select 要素 - ウェブ開発の学習 | MDN

ポイントとしては <div> 要素が <dl> 要素内で利用可能で、その場合に限り <dt> <dd> あと script-supporting elements を受け入れるという話があるということ。

最終的に作成した PR

色々試行錯誤を重ねてこのような PR が仕上がった。

github.com

Content Model の全てを実装しようとすると実装が巨大で複雑になりそうだったので html-no-block-inside-inline の上位互換にはなる程度に留めて PR を出してみた。コメントにも書いたのだけど、この段階でもIssueを6個ほどまとめて解決できそうなのがとても良い。

作者から年明けにフィードバックが貰えると嬉しいな。

今回のまとめ

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