DOMDOMタイムス#12: classの大文字小文字の区別をねじれさせてみよう!
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号をぜひ参考にしてくださいネ!👶
about:blankはquirksモード
さきほどの処理では下記のようにしてiframeを初期化していました。
<iframe src="about:blank"></iframe>
about:blank
で生成されたdocumentのモードはなんなのでしょうか!?
はい〜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) {}
サクッと小文字にしていますネ。
このことから、今回のケースは「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']
👇一応、実際にやってみた様子
実はidでも再現する
で、ここからは未調査というか掘りきれていないというかなのですが、じつはidでも同じことが起こせます。
これは先ほどと全く同じことをidでやっているだけです。一応確かめたい方はぜひソースコードを見てみて下さい。
ただ、quirksモードにおいてcase-insensitiveが許容されるという仕様はclassだけしか見当たらずidは見つけられていないので個人的にナゾです。
どなたかこれを仕様に着地させられる方いたら教えてください😭
おわりに
今回は久々に少し骨のある内容でしたが、やっぱりちょっとマニアックでしたねえ。
ではではまた来週じゃ。
Discussion