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フォントのベースラインのずれの問題がだんだん煩わしい感じになってきているので修正することにしました。

r2095r2095

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の差が画面上でちらっと赤い部分が見えたりする原因になっていたようです。

r2096r2096

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で、ボックスの上下のマージン同士をつぶせる場合のテストです。

r2096r2096

r2096では、上下のマージンを親のボックスおよび次のボックスとつぶす処理が個別に実行されてしまって、上下のマージン同士はつぶせずに表示してしまっていました。

r2097r2097

r2097では、ひとまず上下のマージンをつぶし合える(collapse through)ボックスの処理を別個導入して、レイアウト上はマージンをつぶしたように見えるようにしています。ただし、この修正は一時的なものでr2107で大きく実装を変えています。
margin-collapse-101
このテスト自体は単純なマージンのつぶし処理のテストで左右のボックスが同じようになればOKです。

r2097r2097

r2098で、最後の子がcollapse throughする場合の処理を追加しました。

r2098r2098

margin-collapse-102
このテストも101と同様に子のボックスがcollapse throughするのですが、親のボックスが絶対配置されたボックスになっています。

r2098r2098

r2099で、絶対配置されたボックスでも、最後の子がcollapse throughだった場合の処理を行うように修正しました。

r2099r2099

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

という部分です。

r2100r2100

r2101で修正しています。

r2101r2101

margin-collapse-121
このテストはマージンが0の場合のテストで、マージンのつぶし処理は発生しないのですが、それはさておいて、フロートをクリアするときに、ボックスにボーダーが設定されているとクリアランスの計算を間違えるというバグが残っていました。

r2101r2101

r2102で修正しています。(一番下の行が赤いのはテーブルの実装のバグなのでここでは無視して進めます。)

r2102r2102

margin-collapse-130
このテストでは、赤くなっている部分にheightが0のボックスがあるのですが、中にテキストを含んだラインボックスがあってオーバーフローしているので、仕様にしたがうとこのボックスではcollapse throughの処理をしてはいけません。

r2103r2103

r2104で修正しています。ESウェブブラウザでは、フローティングボックスなどが高さ0のラインボックスに入るので、collapse throughするかどうかの判定が少し細かくなっています。

r2104r2104

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したボックスの次の弟のボックスの開始位置はこのズレの影響は受けません。)

r2107r2107

r2108で、親と上マージンをつぶしたときは子孫の開始位置を保存しないようにしました。(r2108では子孫の開始位置をclearaceに保存していたのですが、紛らわしいのでr2109で、BlockLevelBox::topBorderEdgeに保存するように修正しています。)

r2108r2108

margin-collapse-143
このテストもmargin-collapse-132と同様にcollapse throughしたマージンが親の上マージンともつぶし合う場合のテストです。ただその経路が複雑で、一度下マージンが親の下マージンにつぶされるのですが、その親のボックス自体もcollapse throughしていて親の下マージンが親の上マージンにつぶされる、という具合になっています。

r2109r2109

r2110で、collapsed-throughしたボックスの下マージンを上マージンに移動させたときにも、topBorderEdgeをクリアするようにしました。

r2110r2110

margin-collapse-142
このテストでは、マージンのテストをする以前に右側の参照パターンの色が崩れていました。

r2110r2110

クラスセレクタの大文字・小文字の区別の処理が崩れていたのをr2111で修正しました。

r2111r2111

ここからマージンのテストです。このテストも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するときは元のマージンがどうであろうと必ずクリアランスを導入する、と解釈すると整合がとれます。
このテストの場合だと、

としておけば意図通りの動作になります。r2114でその処理を実装しています。

r2114r2114

ちなみに、r2114の実装では黄色のボックスのマージンが80pxだった場合には、

という具合になって、黄色とシアン色のボックスは接したままになります。

r2114 ( <span class="Apple-style-span" style="font-size: small;"> 黄色のマージンが80pxの場合 </span> )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の仕様では、上記の動作の他に、

という、より直感的な次のようなレイアウトをとってもいいことになっています。

<span class="Apple-style-span" style="font-size: small;"> 黄色のマージンが80pxの場合のもうひとつの有効な解釈 </span> 黄色のマージンが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したボックスのマージンが親のボトムマージンとつぶしあって、さらにそのマージンが親のボトムからトップに移る、というパターンになっています。

r2114r2114

r2115で対応しています。

r2115r2115

margin-collapse-148
このテストは先頭からcollapsed-throughしている子のボックスが連なっていてそのつぶし合ったマージンが親の上マージンに移るパターンのテストです。

r2115r2115

r2116で、上マージンが親に移るときに、連なっている子のボックスtopBorderEdgeをまとめてクリアするように修正しました。

r2116r2116

margin-collapse-164
このテストはWebKit系, FireFox, IEどのブラウザも同じように失敗しているテストで、まだInvalidマークは付けられていないのですが、このテスト自体がInvalidだと思います。

r2116r2116

margin-collapse-142の修正の時に、CSS 2.1ではクリアランスの計算方法として2通りの方法が許されていて、r2114ではほかのブラウザと同じ動きの方を選択しましたよ、ということを書きました。
このテストは、許されているもう一方のクリアランスの計算方法を使うとパスするようになります。それで、どちらのクリアランスの計算方法を採用するか、という話なのですが、このテストにパスする方の計算方法を使うと、ESウェブブラウザは165,166のテストも同時にパスするようになることもあって、今は後者を採用することにします。それだとAcid 2にパスしないのでは、という話がもしかすると出てきたりするのかもしれませんが、それはまたそのときに、ということで。修正はr2117になります。

r2117r2117

補足: IE 9に関するマイクロソフト社の次のページにも詳しくこのテストの問題点が説明されています:3.1.50 CSS 2.1 Test: margin-collapse-164.htm
margin-collapse-128
このテストは2つ以上のマージンがつぶし合うときに、その中に負のマージンが混ざっていた場合のテストしています。いままではこういった負のマージンが混ざる複雑なケースには対応してこなかったのでr2119では以下の通り赤い部分が見えてしまっていました。

r2119r2119

CSS 2.1では、複数のマージンをまとめてつぶす場合は、正のマージンと負のマージンはそれぞれ別個に最大値、最小値を計算して、最後につぶすときにその差を残ったマージンの値とする、という規則があります。

r2120r2120

r2120で、FormattingContextに正のマージンと負のマージンの最大値、最小値を格納してマージンの値が確定したときに有効なフロートの高さを消費するような実装にしています。マージンの処理は有効なフローティング ボックスの高さを消費していく処理とも連動しないといけないので結構やっかいなのです。
margin-collapse-123
r2120ではやはりフローティング ボックスの高さを消費していく処理にバグが残っていました。

r2120r2120

r2121で修正しています。collapse throughするボックス内で新たに生成したフローティング ボックスと、それ以前のボックスから引き継いできたフローティング ボックスとでちょっと違う処理を行う必要があるのでした。

r2121r2121

margin-collapse-clear-006
マージンをつぶせるかどうかの判定方法は、ブロックレベルボックスの内側と外側ですこし違います。このテストではオレンジ色のボックスのoverflowに'hidden'が設定されています。この場合、オレンジ色のボックスのマージンは外側のボックスのマージンとはつぶし合うことができます。

r2121r2121

r2121までは単純にフロールートならマージンをつぶさない、という実装をしてしまっていたので、r2122で修正しています。

r2122r2122

margin-collapse-clear-009

このテストはmargin-collapse-clear-006の続きのようなテストで、overflowに'hidden'が設定されているボックスでもclearがちゃんと機能するかどうかテストしています。

r2122r2122

overflowが'visible'以外の値を取ると、そのボックスは新しいフォーマッティング コンテキストを作ります。新しいフォーマッティング コンテキストからはそれまでのコンテキストの中のフローティング ボックスは見えません。r2122では新しいフォーマッティング コンテキストを使い始めてからクリアの処理をしていたので、r2122のような結果になってしまっていました。

r2123r2123

r2123で修正しています。
margin-collapse-clear-012
このテストも赤い部分が見えたら失敗です。

r2123r2123

スクリーンショットからはわからないのですが、ブルーのボックスのあとに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つ以上ボックスが連なっている場合に対処できていなかったのでした。

r2124r2124

r2124でこのマージンの処理の問題を修正しています。マージンのテストとしてはこれでOKだと思いますが、r2123で赤い部分が半分しかないことからもわかるように、テスト本来の意図はライム色の部分は幅が50%になることを期待しています。
前回r2094では、パーセンテージで指定した幅の目安とする包含ブロックの幅が不明の時は幅を'auto'として扱う、という修正を入れました。r2123でライム色のボックスの幅が大きいままなのは、その判定がこのテストでもかかって幅が50%から'auto'扱いに変わってしまっているためです。
ということは、こういう場合、包含ブロックの幅は不明ではない、と考えるべき、ということですよね。r2125では包含ブロックの幅が'auto'というだけではなくて、包含ブロックの幅がshrink-to-fitし得る場合に限って包含ブロックの幅は不明と判断するように修正しました。ソースコードのコメントにも記載しましたが、'auto'扱いにするということ自体がCSS 2.1で決められているわけではないので、このあたりの処理は一番もっともらしいところに合わせていくしかない感じです。

r2125r2125

margin-collapse-clear-015 ,margin-collapse-157
r2125までは、clearanceはマージンとつぶし合わない、という理解で実装を進めてきていました(clearanceを挟んだ上下のボックスに関しては当然そうですよね)。ところが、この2つのテストに関してはボックスのクリアランスとそのボックスの先頭の子のボックスの上マージンはつぶし合えると考えないとうまく動かないと思われるテストになっています。

r2125 (margin-collapse-clear-015)r2125 (margin-collapse-clear-015)

r2125 (margin-collapse-157)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-clear-015)

r2126 (margin-collapse-157)r2126 (margin-collapse-157)

background-bg-pos-205
このテストではマージンの値が負のときにスクロールが正しい範囲で動くかどうかテストしています。単純に足し合わせて可動幅を決めてしまうと、本来の値より小さくなってしまいますよね。
正常なら右下までスクロールすると黄色や青のボックスが見えるのですがr2126では水平方向に付いてはスクロールバーさえ表示されない状態になっていました。

r2126r2126

r2127で修正しています。本当はさらにCSSのbackground属性によって黄色いボックスの上に小さなオレンジのボックスが表示されたりしないといけないのですが、今回は8章のマージンのテストということで進めているので、14章のテストを行う際に改めて調査したいと思います。

r2127r2127

c411-vt-mrgn-000
tableに未対応な部分があって若干表示が崩れている部分は残っていますですが、この段階で8.3.1のマージンの処理に関するテストについては(最初にskip扱いとした2つを除いて)すべてパスするようになりました。
このテストではマージンを使っている左側の列ではなくて、絶対配置で同じパターンを構築している右側の列の白いボックスの位置がおかしくなっていました。

r2128r2128

topとheightが'auto'で、bottomが'auto'以外のときに、heightを最終的に解決したらtopの値も調整しなおさないといけなかったのですが、その処理が落ちてしまっていました。10章のテストスイートで発見できないといけないバグだと思うのですが見落としがあったかもしれません。r2129で修正しています。

r2129r2129

最下行の色違いはtableの処理のバグによるものです。
www.esrille.com
ここでテストスイートではないのですが、esrilleのページの表示がr2108あたりから崩れるようになってしまっていたのですが、テストスイートの最後まで終わらせても崩れたままなのでした。

r2129r2129

本来、タブの上側にあるはずのマージンが、タブと用紙の間に入ってしまっています。

r2130r2130

クリアランスが入ったときに、そこでマージンのつぶしは途切れるわけですが、そのときに上側のマージンをcollapse-throughしたボックス間でまとめる処理が抜け落ちていました。テストスイートで動いているように見えるからといって、安心というわけではない、という変な例になっていまいました。r2130で修正しています。

まとめ

CSSの仕様書中の短さとは反対に修正にはけっこう手間取りましたが、マージンのつぶしもようやくテスト完了です。マージンのつぶし処理の修正中はどれかのテストをパスするように直すと、それまでパスしていた他のテストが失敗しだす、というもぐら叩き状態が続いたこともあって、少し前から使っているharnessというプログラムをr2119で修正して(ようやく)自動でテストも行えるようにしました。1回は目視で成功・失敗を判定しないといけないのは同じなのですが、レンダーツリーのテキスト形式のログを残しておくことで、2回目移行は成功したままか、失敗したままか、あるいは何か変化が起きたか、ということを自動でテストできるようになっています。
次回はもうひとつテストをまるごとスキップしてきた10.8 line-heightのテストに進む予定です。