debiruはてなメモ

はてなブログの HTML が Invalid なの、わたし、気になります

Invalid な HTML のせいで counter-reset の仕様が捻じ曲げられた件

hn要素をナンバリングするテクニック

ここは h2 のセクションです。このブログではPC版のみですが、見出し文言の左上に ::before 疑似要素でCSSによる自動ナンバリングを行っています。

見出し1-1

ここは h3 のセクションです。

見出し1-1-1

ここは h4 のセクションです

見出し1-1-2

ここは h4 のセクションです

見出し1-2

ここは h3 のセクションです。

見出し1-2-1

ここは h4 のセクションです

いつの間にかこの採番がずれていた

body { counter-reset: n1 n2 n3 n4 n5 n6; }
h1 { counter-increment: n1; counter-reset: n2 n3 n4 n5 n6; }
h2 { counter-increment: n2; counter-reset: n3 n4 n5 n6; }
h3 { counter-increment: n3; counter-reset: n4 n5 n6; }
h4 { counter-increment: n4; counter-reset: n5 n6; }
h5 { counter-increment: n5; counter-reset: n6; }
h6 { counter-increment: n6; }

h1::before { content: counter(n1) "."; }
h2::before { content: counter(n1) "-" counter(n2) "."; }
h3::before { content: counter(n1) "-" counter(n2) "-" counter(n3) "."; }
h4::before { content: counter(n1) "-" counter(n2) "-" counter(n3) "-" counter(n4) "."; }
h5::before { content: counter(n1) "-" counter(n2) "-" counter(n3) "-" counter(n4) "-" counter(n5) "."; }
h6::before { content: counter(n1) "-" counter(n2) "-" counter(n3) "-" counter(n4) "-" counter(n5) "-" counter(n6) "."; }

こんな雰囲気のCSSを書いています。

「1章目」の例では、1, 1-1, 1-1-1, 1-1-2, 1-2, 1-2-1 となるべきです。

それがなんと Firefox の最新版で見たら 1, 1-1, 1-1-1, 1-1-2, 1-2, 1-2-3 となっていたのです。より分かりやすい図をお見せしましょう。

赤字に自動採番、黒字に本来のナンバリングを示した図

赤字と黒字が同じになるのが期待する状態ですが、Firefox ではバージョン82からこの挙動が変わって、自動採番の結果が異なるものになっていたのです。

Bugzillaにレポート報告した

この問題について、既存のIssueがないかを確認した上でレポートを書きました。(後から分かりましたが同様のレポートが既に存在していました。)

私のレポートでは、指摘は誤り(Firefox の実装が正しい)として Invalid でクローズされてしまいました。なぜ Firefox 最新版の挙動が「正しい」のかを調べてみたところ闇の力が働いていたことが分かりました。関連する過去のバグ修正が一体何だったのかについて説明しましょう。

Firefox 82 で取り込まれた修正の正体

Firefox 82 では、counter-reset の動作について互換性のない変更が加えられています。その変更が必要となった問題のバグレポートのタイトルをよく見てください。そのタイトルとは "list numbering recovery with wrong markup has changed behavior." です。

<ol>
  <li>item 1</li>
  <ul>
    <li>subitem 1.1</li>
    <li>subitem 1.2</li>
    <li>subitem 1.3</li>
  </ul>
  <li>item 2
    <ul>
      <li>subtitem 2.1</li>
    </ul>
  </li>
  <li>item 3</li>
</ol>

こんなマークアップ(ol/ul 直下に ol/ul がある)のようなとき、list-style-type: decimal で採番される番号が上の図(1, 2, 3, 4, 5)のように自然になるべきところで、Firefox 68 からは下の図(1, 4, 5, 6, 7)となってしまうという問題です。この問題はバージョン 67 以下では起きませんでした。

Firefox 68 と 66 で list-counter の挙動を確認している図

Firefox がバージョン 68 から動作が変わったのは、list-counter の採番アルゴリズムを内部的に CSS counter と同じ実装に変えたことがきっかけでした。これは counter-set プロパティなどが整備された新しい CSS counter の仕様に従って実装が改められた結果であり、CSS 仕様書でも明示されています。一部のブラウザは list-counter を CSS counter とは別物として特殊な実装をしているようですが、新しい仕様で決められている要求(例えば list-counter は CSS counter 名 list-item としてCSSで制御できる)は満たしているようです。

<list>
  <item>1</item>
  <item>2</item>
  <list>
    <item>2-1</item>
  </list>
  <item>3</item>
</list>

さて、例えば、上記のような HTML で、CSS counter を用いて採番を行うと、下図の右上のように、1, 2, 2-1, 3 となるべきところで 1, 2, 2-1, 2-2 となってしまっていました。

Firefox 82 と 81 でそれぞれ counter-reset, counterset の挙動を確認している図

このような不正な HTML をなぜUAが相手にしないといけないのかについては、問題となったMozillaBug 1548753 で次のように語られています。

当社のSiemens Polarion ALMのようなツールの場合、この問題は皆さんが思っている以上に深刻です。GWTとネイティブブラウザの機能をベースにした当社のリッチテキストエディタは、このような問題のあるマークアップをネイティブで生成しています。

当社のソフトウェアのユーザーは、自動車、飛行機、ピースメーカーなど、セーフティクリティカルなシステムの仕様を作成し、消費しています。仕様書ではごく当たり前のリストに間違ったリスト番号があると、解釈を誤ってしまい、命に関わる危険な状況に陥る可能性があります。

企業のソフトウェアがWebベースソリューションやクラウドに移行していく中で、このような問題の意味合いはますます深刻になってきていることをご認識ください。この問題は、一般的なウェブ利用者にとっては重要ではありませんが、自動車、コンピュータ、携帯電話、その他あらゆるスマート家電など、誰もが毎日使う製品を開発するために企業向けソフトウェアを活用している企業ユーザーにとっては非常に重要な問題なのです。

また、W3C側の GitHub Issue [css-lists] CSS counter scope/inheritance is incompatible with HTML ordinals - issue #5477 - w3c/csswg-drafts でも次のように語られています。

確かに上記の HTML は無効です―― <ol><ul> は <ol><ul> の直接の子要素として許可されませんが、この種の不正な HTML は残念ながらウェブ上ではしばしば存在します。これは document.execCommand('indent') が生成するDOMでもあり(ため息)、HTMLエディタに影響を及ぼします。ですから、作者にマークアップを直せというのは納得のいく解決策ではありません。

この問題は新しい CSS 仕様が示され Firefox 68 の振る舞いが変更された2019年ごろに顕在化したようですが、(list-counter ではなく)CSS counter の採番が Invalid な HTML の場合におかしいことは何年も前から度々指摘されていたようです(list-counter と CSS counter の採番に互換性がないという問題として)。

この問題に対してはMozillaが一つの解決策を示しました。それが counter-reset の動作を互換性のない形で変更してしまい、不正な HTML に対しても採番が「妥当」になるようにするというものでした。CSS利用者から見ると、複数回現れる要素に対する counter-reset が1回しか評価されないような振る舞いに変わりました。

このMozillaの仕様変更の方針については、W3C側でも同時期に考察されているようで同様の方針が良いだろうという結論になっているようです。[css-lists] CSS counter scope/inheritance is incompatible with HTML ordinals - issue #5477 - w3c/csswg-drafts

これらの議論は2021年6月を最後に止まっているようですが、ユーザから反論の声も出ています。当該Issueの最後のコメントにもありますが「conuter-reset の動作を変えるといった方針ではこれまで counter-reset を使っていたユーザに影響を与えてしまう。更にこれを許すと、今後も counter-reset, counter-set の動作が変わる可能性があり、何度もユーザが影響を受けることが懸念される。採るべき対応は既存プロパティの動作の変更ではなく、新しい counter-initialize のようなプロパティや reversed() と同様の新しい構文を取り入れるといった方針の方が良いのではないか」という意見があります。

闇の力で仕様変更された我々はどうすればいいのか

実は、Firefox における不正な HTML に対する counter-reset の仕様変更は大きな影響はありません。というのは、そもそも2019年以前は counter-set プロパティが存在せず状況が異なっていたためです。我々は2019年に counter-set が新設されると同時に CSS counter の仕様が大幅に改定された時点で counter-reset の使用方法を見直すべきだったのです。

counter-reset の互換性が失われた挙動については、影響を受けるのはフラットな HTML に対して counter-reset が何度も評価されるようなケースのみです。「フラットな HTML」が何を意味しているのかについては次の記事に詳しく書いておきました。

ところで CSS Working Group Editor Drafts (TRでなくdrafts)はタイミングによってコンテンツが吹き飛ぶのが恒例行事なのでしょうか。この記事を書いている現時点で drafts を閲覧することができなくなっています。まあいいや。

Invalid な HTML のせいで counter-reset の仕様が捻じ曲げられて、そんなことを知らない私が Bugzilla にバグレポートを書いて悉く Invalid Close されたお話は以上です。

そんなこんなで、MDN の説明ではこうした経緯が分からずブラウザ間での挙動の違いについての説明もないので、これらを説明したほうがよいのではないかという Issue を作成してみました。修正案の下書きを書きつつあります。

しかし原文である英語版の記事を大幅に書き換えて修正するのはなかなか大変だ。

Next debiru's HINT 「WebはWEBじゃない