Shiki’s Weblog
ESウェブブラウザ通信 - CSS 2.1 Test Suite #7
2011/11/13 #ESウェブブラウザ通信
前回までで W3C CSS 2.1テストスイート の 8章から10章まで一通り確認を行ってきて、マージンをつぶす前のボックスの配置については、だいたい正しくレイアウトできるようになってきたように思います。そこで今回は、これまでテストをスキップしてきた8.3.1 Collapsing marginsのテストの確認と修正を進めます。仕様書中ではたった1セクションなのですが、きちんと実装するのが難しい部分で、現状ではWebKit系の最新のウェブ ブラウザなどでもまだ全テストにパスするようにはなっていなかったりする部分でもあります。
Ahemフォントの処理
・containing-block-031 マージンのつぶしのテストの前に、Ahemフォントのベースラインのずれの問題がだんだん煩わしい感じになってきているので修正することにしました。
r2095
ESウェブブラウザで利用しているFree Type 2では、TrueTypeフォントからフォントのビットマップを生成する際に、フォントの線が綺麗にディスプレイのグリッドにフィットするように処理してくれます。ただESウェブブラウザでは、このようにして展開した32pxのフォントをOpenGLで拡大・縮小して表示しているので、実際に描画されるフォントはほとんどの場合グリッドにはフィットしていません。さてAhemフォントの場合、フォントのアセントとディセントの比は8:2なので、これを32pxに割り当てると、25.6px:6.4pxとなるのですが、Free Type 2ではグリッド フィット処理が入るので26px:6pxに丸められて展開されるようです。この26-25.6=0.4の差が画面上でちらっと赤い部分が見えたりする原因になっていたようです。
r2096
r2096では、この差を計算してOpenGLで描画する際はアセントを理論値で処理するように修正しました。Ahemフォントを利用しているcontaining-block-031では赤い部分は完全に見えなくなりました。ただOpenGLの処理と合わさって、テストによってはフォントの端の部分が背景を完全には隠せなかったり、という場合が残ってしまうことがあります。
マージンのつぶしのテスト
前準備はここまでで、8.3.1のテストに移ります。r2096の段階では、8.3.1のテスト結果は次のような具合でした。 # 8.3.1 html4/abspos-022.htm pass html4/background-bg-pos-205.htm fail html4/blocks-017.htm fail html4/c411-vt-mrgn-000.htm fail html4/margin-collapse-001.htm pass html4/margin-collapse-002.htm pass html4/margin-collapse-003.htm pass html4/margin-collapse-004.htm pass html4/margin-collapse-005.htm pass html4/margin-collapse-006.htm pass html4/margin-collapse-007.htm pass html4/margin-collapse-008.htm pass html4/margin-collapse-009.htm pass html4/margin-collapse-010.htm pass html4/margin-collapse-011.htm pass html4/margin-collapse-012.htm pass html4/margin-collapse-013.htm pass html4/margin-collapse-014.htm pass html4/margin-collapse-015.htm pass html4/margin-collapse-016.htm pass html4/margin-collapse-017.htm pass html4/margin-collapse-018.htm pass html4/margin-collapse-019.htm fail html4/margin-collapse-020.htm pass html4/margin-collapse-021.htm pass html4/margin-collapse-022.htm pass html4/margin-collapse-023.htm pass html4/margin-collapse-024.htm pass html4/margin-collapse-025.htm pass html4/margin-collapse-026.htm pass html4/margin-collapse-027.htm pass html4/margin-collapse-028.htm pass html4/margin-collapse-029.htm pass html4/margin-collapse-030.htm pass html4/margin-collapse-031.htm pass html4/margin-collapse-032.htm pass html4/margin-collapse-033.htm pass html4/margin-collapse-034.htm pass html4/margin-collapse-035.htm pass html4/margin-collapse-037.htm pass html4/margin-collapse-038.htm pass html4/margin-collapse-101.htm fail html4/margin-collapse-102.htm pass html4/margin-collapse-103.htm pass html4/margin-collapse-104.htm pass html4/margin-collapse-105.htm fail html4/margin-collapse-106.htm pass html4/margin-collapse-107.htm fail html4/margin-collapse-108.htm fail html4/margin-collapse-109.htm pass html4/margin-collapse-110.htm fail html4/margin-collapse-111.htm fail html4/margin-collapse-112.htm pass html4/margin-collapse-113.htm fail html4/margin-collapse-114.htm fail html4/margin-collapse-115.htm fail html4/margin-collapse-116.htm fail html4/margin-collapse-117.htm fail html4/margin-collapse-118.htm pass html4/margin-collapse-119.htm fail html4/margin-collapse-120.htm fail html4/margin-collapse-121.htm fail html4/margin-collapse-122.htm fail html4/margin-collapse-123.htm fail html4/margin-collapse-125.htm fail html4/margin-collapse-126.htm pass html4/margin-collapse-127.htm pass html4/margin-collapse-128.htm fail html4/margin-collapse-129.htm fail html4/margin-collapse-130.htm pass html4/margin-collapse-131.htm pass html4/margin-collapse-132.htm fail html4/margin-collapse-133.htm fail html4/margin-collapse-134.htm fail html4/margin-collapse-135.htm fail html4/margin-collapse-137.htm fail html4/margin-collapse-138.htm fail html4/margin-collapse-139.htm pass html4/margin-collapse-140.htm pass html4/margin-collapse-141.htm fail html4/margin-collapse-142.htm fail html4/margin-collapse-143.htm fail html4/margin-collapse-145.htm fail html4/margin-collapse-146.htm fail html4/margin-collapse-147.htm fail html4/margin-collapse-148.htm fail html4/margin-collapse-151.htm pass html4/margin-collapse-154.htm fail html4/margin-collapse-155.htm pass html4/margin-collapse-156.htm pass html4/margin-collapse-157.htm fail html4/margin-collapse-158.htm fail html4/margin-collapse-159.htm fail html4/margin-collapse-160.htm skip (page) html4/margin-collapse-162.htm fail html4/margin-collapse-163.htm fail html4/margin-collapse-164.htm pass html4/margin-collapse-165.htm pass html4/margin-collapse-166.htm pass html4/margin-collapse-clear-000.htm pass html4/margin-collapse-clear-001.htm pass html4/margin-collapse-clear-002.htm pass html4/margin-collapse-clear-003.htm pass html4/margin-collapse-clear-004.htm fail html4/margin-collapse-clear-005.htm fail html4/margin-collapse-clear-006.htm pass html4/margin-collapse-clear-007.htm pass html4/margin-collapse-clear-008.htm pass html4/margin-collapse-clear-009.htm fail html4/margin-collapse-clear-010.htm fail html4/margin-collapse-clear-011.htm pass html4/margin-collapse-clear-012.htm fail html4/margin-collapse-clear-013.htm fail html4/margin-collapse-clear-014.htm pass html4/margin-collapse-clear-015.htm pass html4/margin-collapse-clear-016.htm pass html4/margin-collapse-clear-017.htm pass html4/table-margin-004.htm skip (table) 比較的単純なmargin-collapse-0xx以外のテストについては、ほとんど失敗していました。今回は、これらのうちテーブルと印刷用メディアに関連した2つのテストをスキップする以外はすべてパスするように実装を修正するのが目標です。・margin-collapse-019 ボックスの高さが0で、ボックスの上下のマージン同士をつぶせる場合のテストです。
r2096
r2096では、上下のマージンを親のボックスおよび次のボックスとつぶす処理が個別に実行されてしまって、上下のマージン同士はつぶせずに表示してしまっていました。
r2097
r2097では、ひとまず上下のマージンをつぶし合える(collapse through)ボックスの処理を別個導入して、レイアウト上はマージンをつぶしたように見えるようにしています。ただし、この修正は一時的なものでr2107で大きく実装を変えています。・margin-collapse-101 このテスト自体は単純なマージンのつぶし処理のテストで左右のボックスが同じようになればOKです。
r2097
r2098で、最後の子がcollapse throughする場合の処理を追加しました。
r2098
・margin-collapse-102 このテストも101と同様に子のボックスがcollapse throughするのですが、親のボックスが絶対配置されたボックスになっています。
r2098
r2099で、絶対配置されたボックスでも、最後の子がcollapse throughだった場合の処理を行うように修正しました。
r2099
・margin-collapse-117 このテストでは親のボックスのheightが'auto'でないときに、最後の子の下マージンをつぶしてしまわないかどうかチェックしています。仕様書の、
bottom margin of a last in-flow child and bottom margin of its parent if the parent has 'auto' computed height
という部分です。
r2100
r2101で修正しています。
r2101
・margin-collapse-121 このテストはマージンが0の場合のテストで、マージンのつぶし処理は発生しないのですが、それはさておいて、フロートをクリアするときに、ボックスにボーダーが設定されているとクリアランスの計算を間違えるというバグが残っていました。
r2101
r2102で修正しています。(一番下の行が赤いのはテーブルの実装のバグなのでここでは無視して進めます。)
r2102
・margin-collapse-130 このテストでは、赤くなっている部分にheightが0のボックスがあるのですが、中にテキストを含んだラインボックスがあってオーバーフローしているので、仕様にしたがうとこのボックスではcollapse throughの処理をしてはいけません。
r2103
r2104で修正しています。ESウェブブラウザでは、フローティングボックスなどが高さ0のラインボックスに入るので、collapse throughするかどうかの判定が少し細かくなっています。
r2104
・margin-collapse-132 このあたりのテストからは、collapsed-throughしたボックスが連続しているときに、それらのマージンをまとめて親や兄のボックスに戻す、という処理のテストを行っています。r2106,r2107でそのための前準備として、クリアランスがあるときは仕様通りマージンをつぶし合わない用にしています(結構コードが変わっています)。このテストでは、 collapse throughするマージンの処理に関する、
If the element's margins are collapsed with its parent's top margin, the top border edge of the box is defined to be the same as the parent's.
というルールをテストしています。通常はcollapse throughするボックスの子の開始位置はそのボックスをレイアウトする時点の上マージンの値の分だけ下にずれるのですが、親と上マージンをつぶし合った場合には、この開始位置のズレを0にする、というわけです。(なお、collapse throughしたボックスの次の弟のボックスの開始位置はこのズレの影響は受けません。)
r2107
r2108で、親と上マージンをつぶしたときは子孫の開始位置を保存しないようにしました。(r2108では子孫の開始位置をclearaceに保存していたのですが、紛らわしいのでr2109で、BlockLevelBox::topBorderEdgeに保存するように修正しています。)
r2108
・margin-collapse-143 このテストもmargin-collapse-132と同様にcollapse throughしたマージンが親の上マージンともつぶし合う場合のテストです。ただその経路が複雑で、一度下マージンが親の下マージンにつぶされるのですが、その親のボックス自体もcollapse throughしていて親の下マージンが親の上マージンにつぶされる、という具合になっています。
r2109
r2110で、collapsed-throughしたボックスの下マージンを上マージンに移動させたときにも、topBorderEdgeをクリアするようにしました。
r2110
・margin-collapse-142 このテストでは、マージンのテストをする以前に右側の参照パターンの色が崩れていました。
r2110
クラスセレクタの大文字・小文字の区別の処理が崩れていたのをr2111で修正しました。
r2111
ここからマージンのテストです。このテストもHixieが作成したテストなのですが、もともと難しめのテストの多いHixieのテストの中で、さらにタイトルに"CSS Test: Margin Collapsing: clear (hard)"とわざわざhardと記載がされています。黄色いボックスにはclear: leftと一緒に上マージンとして64px設定されているのですが、 シアン色 のフローティング ボックスの高さもちょうど64pxなのです。なのでこれまでの処理ですと黄色いボックスにはclearanceは不要なので上マージンは親とつぶして大丈夫、という判定をしてしまって描画した結果がr2111です。何が難しいのかというと、clear: leftの意図からしてr2111がおかしいのは自明なのですが、仕様書の文言から上記の動作がいけない、ということをどう読みとるか、という点だったりします。先ほどの考え方では、まずクリアランスの計算をして、それに基づいてマージンのつぶしを行っていました。でもcollapse throughしているボックスをclearするときは、そのマージンをさら上にthroughさせてしまうとr2111のようになってしまいます。こういう場合、collapse throughするボックスをclearするときは元のマージンがどうであろうと必ずクリアランスを導入する、と解釈すると整合がとれます。このテストの場合だと、
- 黄色のボックスのclearance = 0px
- 黄色のボックスのmargin-top = 64px
としておけば意図通りの動作になります。r2114でその処理を実装しています。
r2114
ちなみに、r2114の実装では黄色のボックスのマージンが80pxだった場合には、
- 黄色のclearance = -16px
- 黄色のmargin-top = 80px
という具合になって、黄色とシアン色のボックスは接したままになります。
r2114 ( 黄色のマージンが80pxの場合 )
画面上ではマージンは指定した値より小さく見えるので不思議な感じもしますが、WebKit系もFirefoxもIEも現状みんなこう動きます。ちょっとややこしいですよね。今年の2月でもMozillaの開発者のひとりBoris Zbarskyさんは、
In the area of clear + margin-collapse interaction, pretty much no one understands the details. :(
とW3Cのメーリングリストに書かれているくらいです。
またCSS 2.1の仕様では、上記の動作の他に、
- 黄色のclearance = 0px
- 黄色のmargin-top = 80px
という、より直感的な次のようなレイアウトをとってもいいことになっています。
黄色のマージンが80pxの場合のもうひとつの有効な解釈
この仕様の曖昧さの理由はCSS Wg Blogに説明されていました:
The preferred behavior is the latter, since it doesn’t mysteriously eat margins and make clear to make things move up. But we need to evaluate web compat since Acid2 and therefore all browsers do the former
後者の直感的なレイアウトの方が望ましいけれど、Acid2が前者を想定していてどのブラウザも前者で実装しちゃっているから互換性の問題が……と。この曖昧さが実際にあとのテストケースでは問題になってくるのですが、そのはそのときに。・margin-collapse-146
このテストはmargin-collapse-143のより複雑なパターンで、連続したcollapse-throughしたボックスのマージンが親のボトムマージンとつぶしあって、さらにそのマージンが親のボトムからトップに移る、というパターンになっています。
r2114
r2115で対応しています。
r2115
・margin-collapse-148 このテストは先頭からcollapsed-throughしている子のボックスが連なっていてそのつぶし合ったマージンが親の上マージンに移るパターンのテストです。
r2115
r2116で、上マージンが親に移るときに、連なっている子のボックスtopBorderEdgeをまとめてクリアするように修正しました。
r2116
・ margin-collapse-164 このテストはWebKit系, FireFox, IEどのブラウザも同じように失敗しているテストで、まだInvalidマークは付けられていないのですが、このテスト自体がInvalidだと思います。
r2116
margin-collapse-142の修正の時に、CSS 2.1ではクリアランスの計算方法として2通りの方法が許されていて、r2114ではほかのブラウザと同じ動きの方を選択しましたよ、ということを書きました。このテストは、許されているもう一方のクリアランスの計算方法を使うとパスするようになります。それで、どちらのクリアランスの計算方法を採用するか、という話なのですが、このテストにパスする方の計算方法を使うと、ESウェブブラウザは165,166のテストも同時にパスするようになることもあって、今は後者を採用することにします。それだとAcid 2にパスしないのでは、という話がもしかすると出てきたりするのかもしれませんが、それはまたそのときに、ということで。修正はr2117になります。
r2117
補足: IE 9に関するマイクロソフト社の次のページにも詳しくこのテストの問題点が説明されています:3.1.50 CSS 2.1 Test: margin-collapse-164.htm ・margin-collapse-128 このテストは2つ以上のマージンがつぶし合うときに、その中に負のマージンが混ざっていた場合のテストしています。いままではこういった負のマージンが混ざる複雑なケースには対応してこなかったのでr2119では以下の通り赤い部分が見えてしまっていました。
r2119
CSS 2.1では、複数のマージンをまとめてつぶす場合は、正のマージンと負のマージンはそれぞれ別個に最大値、最小値を計算して、最後につぶすときにその差を残ったマージンの値とする、という規則があります。
r2120
r2120で、FormattingContextに正のマージンと負のマージンの最大値、最小値を格納してマージンの値が確定したときに有効なフロートの高さを消費するような実装にしています。マージンの処理は有効なフローティング ボックスの高さを消費していく処理とも連動しないといけないので結構やっかいなのです。・margin-collapse-123 r2120ではやはりフローティング ボックスの高さを消費していく処理にバグが残っていました。
r2120
r2121で修正しています。collapse throughするボックス内で新たに生成したフローティング ボックスと、それ以前のボックスから引き継いできたフローティング ボックスとでちょっと違う処理を行う必要があるのでした。
r2121
・margin-collapse-clear-006 マージンをつぶせるかどうかの判定方法は、ブロックレベルボックスの内側と外側ですこし違います。このテストではオレンジ色のボックスのoverflowに'hidden'が設定されています。この場合、オレンジ色のボックスのマージンは外側のボックスのマージンとはつぶし合うことができます。
r2121
r2121までは単純にフロールートならマージンをつぶさない、という実装をしてしまっていたので、r2122で修正しています。
r2122
このテストはmargin-collapse-clear-006の続きのようなテストで、overflowに'hidden'が設定されているボックスでもclearがちゃんと機能するかどうかテストしています。
r2122
overflowが'visible'以外の値を取ると、そのボックスは新しいフォーマッティング コンテキストを作ります。新しいフォーマッティング コンテキストからはそれまでのコンテキストの中のフローティング ボックスは見えません。r2122では新しいフォーマッティング コンテキストを使い始めてからクリアの処理をしていたので、r2122のような結果になってしまっていました。
r2123
r2123で修正しています。・margin-collapse-clear-012 このテストも赤い部分が見えたら失敗です。
r2123
スクリーンショットからはわからないのですが、ブルーのボックスのあとに2つ、collapse-throughする透明のボックスがあって、最初の方のボックスにはclearが設定されていてクリアランスが60pxになるように設定されています。さて、この2つのcollapse-throughする透明のボックスのマージンをつぶしていくと、140pxになるのですが、r2123ではこのマージンを ライム色 の親のボックスの下マージンに移してしまったので、ライム色の部分の高さが減って赤い部分が見えています。仕様書をよく読むと、クリアランスがあるcollapse-throughしているボックスに連なってつぶしたマージンは、親の下マージンに移してはいけない、と書いてあります:
If the top and bottom margins of an element withclearanceare adjoining, its margins collapse with the adjoining margins of following siblings but that resulting margin does not collapse with the bottom margin of the parent block.
r2123では最後の子のボックスがクリアランスがあるcollapse-throughしているボックスの場合のみ親の下マージンに移さないようなコードになっていて、このテストの用に2つ以上ボックスが連なっている場合に対処できていなかったのでした。
r2124
r2124でこのマージンの処理の問題を修正しています。マージンのテストとしてはこれでOKだと思いますが、r2123で赤い部分が半分しかないことからもわかるように、テスト本来の意図はライム色の部分は幅が50%になることを期待しています。前回、r2094では、パーセンテージで指定した幅の目安とする包含ブロックの幅が不明の時は幅を'auto'として扱う、という修正を入れました。r2123でライム色のボックスの幅が大きいままなのは、その判定がこのテストでもかかって幅が50%から'auto'扱いに変わってしまっているためです。ということは、こういう場合、包含ブロックの幅は不明ではない、と考えるべき、ということですよね。r2125では包含ブロックの幅が'auto'というだけではなくて、包含ブロックの幅がshrink-to-fitし得る場合に限って包含ブロックの幅は不明と判断するように修正しました。ソースコードのコメントにも記載しましたが、'auto'扱いにするということ自体がCSS 2.1で決められているわけではないので、このあたりの処理は一番もっともらしいところに合わせていくしかない感じです。
r2125
・ margin-collapse-clear-015 ,margin-collapse-157 r2125までは、clearanceはマージンとつぶし合わない、という理解で実装を進めてきていました(clearanceを挟んだ上下のボックスに関しては当然そうですよね)。ところが、この2つのテストに関してはボックスのクリアランスとそのボックスの先頭の子のボックスの上マージンはつぶし合えると考えないとうまく動かないと思われるテストになっています。
r2125 (margin-collapse-clear-015)
r2125 (margin-collapse-157)
margin-collapse-157では、左下の黒枠内が崩れています。黄色い下ボーダーのある高さ0のボックスにclear: leftが指定されていて、さらにその子として上下左右1emずつマージンのある高さ0のボックスが配置されています。この場合の解決値は、
・ アクア色 のフロートの高さ: 64px ・黄色のクリアランス: 64px ・黄色の上マージン: 0px のようになるので、クリアランスと子のボックスのマージン16pxがつぶし合えないとすると、r2125のような結果になります。margin-collapse-157はGeckoでも下の段のテストは3つともinvalidじゃない?という意見が出されていますが、margin-collapse-clear-015はパスしているところを見ると、先頭の子の上マージンはクリアランスでつぶせると考えないといけない気がします。r2126では、負のマージンが絡んだときの動作がどうなるかちょっとわからないのですが、その処理を推定して実装しています。
r2126 (margin-collapse-clear-015)
r2126 (margin-collapse-157)
・background-bg-pos-205 このテストではマージンの値が負のときにスクロールが正しい範囲で動くかどうかテストしています。単純に足し合わせて可動幅を決めてしまうと、本来の値より小さくなってしまいますよね。正常なら右下までスクロールすると黄色や青のボックスが見えるのですがr2126では水平方向に付いてはスクロールバーさえ表示されない状態になっていました。
r2126
r2127で修正しています。本当はさらにCSSのbackground属性によって黄色いボックスの上に小さなオレンジのボックスが表示されたりしないといけないのですが、今回は8章のマージンのテストということで進めているので、14章のテストを行う際に改めて調査したいと思います。
r2127
・c411-vt-mrgn-000 tableに未対応な部分があって若干表示が崩れている部分は残っていますですが、この段階で8.3.1のマージンの処理に関するテストについては(最初にskip扱いとした2つを除いて)すべてパスするようになりました。このテストではマージンを使っている左側の列ではなくて、絶対配置で同じパターンを構築している右側の列の白いボックスの位置がおかしくなっていました。
r2128
topとheightが'auto'で、bottomが'auto'以外のときに、heightを最終的に解決したらtopの値も調整しなおさないといけなかったのですが、その処理が落ちてしまっていました。10章のテストスイートで発見できないといけないバグだと思うのですが見落としがあったかもしれません。r2129で修正しています。
r2129
最下行の色違いはtableの処理のバグによるものです。・www.esrille.com ここでテストスイートではないのですが、esrilleのページの表示がr2108あたりから崩れるようになってしまっていたのですが、テストスイートの最後まで終わらせても崩れたままなのでした。
r2129
本来、タブの上側にあるはずのマージンが、タブと用紙の間に入ってしまっています。
r2130
クリアランスが入ったときに、そこでマージンのつぶしは途切れるわけですが、そのときに上側のマージンをcollapse-throughしたボックス間でまとめる処理が抜け落ちていました。テストスイートで動いているように見えるからといって、安心というわけではない、という変な例になっていまいました。r2130で修正しています。
まとめ
CSSの仕様書中の短さとは反対に修正にはけっこう手間取りましたが、マージンのつぶしもようやくテスト完了です。マージンのつぶし処理の修正中はどれかのテストをパスするように直すと、それまでパスしていた他のテストが失敗しだす、というもぐら叩き状態が続いたこともあって、少し前から使っているharnessというプログラムをr2119で修正して(ようやく)自動でテストも行えるようにしました。1回は目視で成功・失敗を判定しないといけないのは同じなのですが、レンダーツリーのテキスト形式のログを残しておくことで、2回目移行は成功したままか、失敗したままか、あるいは何か変化が起きたか、ということを自動でテストできるようになっています。次回はもうひとつテストをまるごとスキップしてきた10.8 line-heightのテストに進む予定です。