DOMDOMタイムス#12: classの大文字小文字の区別をねじれさせてみよう!

2023/09/14に公開

DOMDOMタイムスです🌞
今日はclassのちょっと不思議な現象についてです!

クイズ

さて、下記のようなHTMLのページがあったとします。

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <div id="container">
            <style id="style">
                .xFoo {
                color: blue;
                }
                .xfoo {
                color: red;
                }
            </style>
            <div id="target" class="xFoo">class:xFoo, so it should be blue</div>
        </div>
    </body>
</html>

さて、このとき「id=targetのdiv要素が赤色(.xfooの色)になる」ことはありえるでしょうか?
ただし、下記3条件が成立しているとします。

  • styleは当該styleタグ以外のどこからも指定されていないし、変更もされていない
  • div要素が赤色になっている時document.compatMode'CSS1Compat'である
  • ブラウザはchromeで、現時点での最新版であるバージョン116である

正解

あるんですねえ👶
下記のページのiframeの中で、まさに先ほどの条件を満たす現象が発生しています。

どういうことやねん

何をどうしたらこんなことになるのか順に見ていきましょう。
まず、このページはHTMLファイル1枚でできており、こんなソースコードになっています。
ちょっと長いですが載っけます。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <div id="container">
      <style id="style">
        .xFoo {
          color: blue;
        }
        .xfoo {
          color: red;
        }
      </style>
      <div id="target" class="xFoo">class:xFoo, so it should be blue</div>
    </div>
    <iframe src="about:blank"></iframe>
    <script>
      function main() {
        // quirksモード下でnodeをcloneする
        const iframeDocument = document.querySelector("iframe").contentDocument;
        const clone = iframeDocument.importNode(
          document.querySelector("#container"),
          true
        );

        // standardsモードへ切り替える
        iframeDocument.open("text/html", "replace");
        iframeDocument.write("<!DOCTYPE html>");
        iframeDocument.close();

        // さっきcloneしたnodeをぶちこむ
        iframeDocument.body.appendChild(clone);
      }
      main();
    </script>
  </body>
</html>

要するに処理としては、下記の通りです。

  • iframeをabout:blankでひらく
  • iframeのdocumentのimportNode()で例のstyleとdivを複製する
  • iframeにDOCTYPEを書き込む
  • 複製したstyleやdivをiframeに突っ込む

そして、今回の結論を一言でいうと「quirksモードでdivを読み込むことで、ブラウザに内部的にはclassをxfooと小文字で保持させ、後付けでstandardsモードに切り替えている」ワケです

では順を追って、詳しく見ていってみましょう。

quirksモードとstandardsモード

大前提としてquirksモードとstandardsモードについて、改めて確認しておきます。

window.documentは、HTML冒頭のDOCTYPE宣言の有無や内容によって3つのモードを切り替えます。
とりあえず宣言がなければquirksモード、よく見る<!DOCTYPE html>という宣言があればstandardsモードで動くのでした。
前者はいろいろ変わった挙動をし、後者は私たちが常日頃イメージする挙動をするのですが、今回はひとまず「quirksモードだとclass名で大文字小文字が区別されなくなる」ことがポイントです

なお、それ以外の詳しいことはDOMDOMタイムス10号をぜひ参考にしてくださいネ!👶
https://zenn.dev/canalun/articles/domdomtimes_quirks_mode_and_dom

about:blankはquirksモード

さきほどの処理では下記のようにしてiframeを初期化していました。

<iframe src="about:blank"></iframe>

about:blankで生成されたdocumentのモードはなんなのでしょうか!?
image
はい〜quirksモードです
よくjestでdocument付きのiframeを作りたいときにabout:blankでなんとかしていますが、なんか意外と怖い気がしてきました。

というわけで、先ほどの処理においてはquirksモードでiframeが立ち上げられて、そこでimportNode()されていたことになります。
そこで次のポイントになるのはquirksモードにおけるclass名の初期化処理です。

quirksモードにおけるclass名の内部的な取り扱い

さて先ほども述べたようにquirksモードにおいてブラウザはclass名の大文字小文字区別をやめます

では内部ではどうなっているのかというと、BlinkにおいてはClassCollectionの初期化においてquirksモードならば小文字への変更がなされています

ClassCollection::ClassCollection(ContainerNode & root_node,
    const AtomicString & class_names): HTMLCollection(root_node,
    kClassCollectionType,
    kDoesNotOverrideItemAfter),
  class_names_(GetDocument().InQuirksMode() ? class_names.LowerASCII() :
    class_names) {}

該当箇所へのリンク: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/dom/class_collection.cc;l=41;drc=0d2b73829fdd95a26f48689131150febe7c7792d

サクッと小文字にしていますネ。
このことから、今回のケースは「quirksモードでdivを読み込むことで、ブラウザの内部的にはclassをxfooと小文字で保持している」と思われます

実際それを裏付ける興味深い事実として、先ほどのiframeの中でgetElementsByClassName()をやってみるとxfooでは見つかるのにxFooでは見つからないという事象が起きます
しかもclassListを見るとxFooと出るし、要素のインスペクター上はxFooという表記にはなっているのにもかかわらずです👶

document.getElementsByClassName('xFoo')
// HTMLCollection []
document.getElementsByClassName('xfoo')
// HTMLCollection [div#target.xfoo, target: div#target.xfoo]
document.getElementsByClassName('xfoo')[0].classList
// DOMTokenList ['xFoo', value: 'xFoo']

👇一応、実際にやってみた様子
image

実はidでも再現する

で、ここからは未調査というか掘りきれていないというかなのですが、じつはidでも同じことが起こせます。

これは先ほどと全く同じことをidでやっているだけです。一応確かめたい方はぜひソースコードを見てみて下さい。
ただ、quirksモードにおいてcase-insensitiveが許容されるという仕様はclassだけしか見当たらずidは見つけられていないので個人的にナゾです
どなたかこれを仕様に着地させられる方いたら教えてください😭

おわりに

今回は久々に少し骨のある内容でしたが、やっぱりちょっとマニアックでしたねえ。
ではではまた来週じゃ。

GitHubで編集を提案

Discussion