CSS 的可交換性、直書/橫書切換的邏輯屬性、Gecko 與 Webkit 的 CSS 實作差異

上次討論到要不要替 'transform' 生小孩的問題[1],pingooo 還特地搬出可交喚
性(commutative)這個嚇人的形容詞。後來突然回憶到這件事跟直書/橫書切換的
邏輯屬性(logical property)有關,進而發現一件 Gecko 的 CSS 實現相關的一
個很妙的事,花了一整天研究了一下,來跟大家分享。細節沒那麼有趣,先用†號
標起來之後再提。

[1]
http://lists.w3.org/Archives/Public/public-html-ig-zh/2011Apr/thread#msg14

邏輯屬性基本上就是把方向抽象化的 CSS 裡的很基本的屬性,例如:
* margin-before/margin-end/margin-after/margin-start
* padding-before/padding-end/padding-after/padding-start
* (... 數不完)

其中書寫模式(Writing Mode)為
* 橫書且文字左到右的情形下 before/end/after/start 是 上/右/下/左
* 直書且文字上到下的情形下 before/end/after/start 是 右/下/左/上
* 橫書且文字右到左(RTL)的情形下 before/end/after/start 是 上/左/下/右
* (... 請參考[2])

需要抽象化的方向的主因是我們不能把 上/右/下/左 照書寫模式改變方向的定
義,不然對於 RTL 的人來說左邊是右邊,右邊是左邊太混亂。邏輯屬性的優點是
同樣的樣式表可以用在不同的書寫模式上[3],對於重複使用樣式有幫助。Gecko
在多年以前就為了 RTL 文字引入了 *-start/end,而 WebKit 也在實作直書之後
也很完整的支援邏輯屬性[4]。

[2]
http://dev.w3.org/cvsweb/~checkout~/csswg/css3-writing-modes/Overview.html?rev=1.39;content-type=text%2Fhtml#logical-to-physical
(這是舊版本的《CSS3 書寫模式》。由於邏輯方向還是很混亂而且有爭議且會增加
太多屬性,這些屬性已經在新版本被拿掉了)
[3] http://nadita.com/murakami/epub-css/#p20 (第 20 頁)
[4] Gecko:
http://mxr.mozilla.org/mozilla-central/source/layout/style/html.css#145
     WebKit: http://www.ilovejs.net/lab/default-css/webkit-html-css.html


要支援這些屬性,可憐的瀏覽器商們需要考慮以下這些跟可交換性有關的問題:

== 問題一 ==

在一般的 CSS 的情況下

p { margin-left: 1em; margin-left: 2em; } 等同於 p { margin-left: 2em; }
(後面的蓋掉前面的)

但是在 left = start(橫書) 的情形下,

p { margin-left: 1em; margin-start: 2em; } 跟 p { margin-start: 2em;
margin-left: 1em; } (前後兩個宣告交換)

'margin-left' 分別該是什麼呢?

這種案例雖然會很稀少,但是考慮到移植到邏輯屬性的過渡時期很多人會為了向下
兼容寫 p { margin-left: 1em; margin-start: 1em; } 這種東西,不能說這種案
例絕對不會出現。

== 問題二 ==

p { writing-mode: vertical-rl;  /*直書*/
     margin-before: 1em;
     writing-mode: horizontal-tb; /* 橫書 */ }

該等同於 'margin-top: 1em; ' (p { margin-before: 1em; writing-mode:
horizontal-tb; })還是 'margin-right: 1em; ' (p { writing-mode:
vertical-rl; margin-before: 1em; })?

這雖然更罕見了,但是這是有前例的!根據 CSS2.1 規範†:

p { font-size: 12px; margin-left: 1em; font-size: 14px; }

等同於 'margin-left: 14px',所以前面的問題 'margin-before: 1em; ' 等同於
'margin-top: 1em; '


困難的問題在於問題一。現在 Gecko 跟 WebKit 的實作都是取後面的當作結果,
也就是 **當 left=start 的時候 'margin-left' 跟 'margin-start' 沒有可交換
性**(另一方面,當 left≠start 的時候 'margin-left' 跟 'margin-start' 是
完全可以交換的)。妙的地方來了, **Gecko 在解析 CSS 的一個宣告區塊
({...})的時候,用來存多個屬性宣告的資料結構基本上是近似於一個
Dictionary†**,也就是後面的非速記屬性的宣告會在解析時間就把前面的同一個
屬性的宣告蓋掉,這是因為 CSS 幾乎所有屬性都是可以交換的,這種優化一般來
說來說沒問題。請問,這看起來矛盾的兩件事('margin-left' 到底跟
'margin-start' 交換還是不交換),Gecko 到底是怎麼實作的?(另一方
面,Webkit 的資料結構就是把一個宣告區塊中間的屬性宣告存成一個 Vector†,
後來取後面的當結果就相對簡單)

提示:雖然 CSS 裡面有不少依存關係('em' 的大小由 'font-size' 決定、
'currentColor' 由 'color' 決定),但是一個宣告區塊裡面真正不交換的事實上
只有兩種東西:
1. 同樣的屬性宣告 — p { font-size: 12px; font-size: 14px; } 不等於 p {
font-size: 14px; font-size: 12px; }
2. 展開屬性有重疊 — p { border-top: solid; border-style: none; } 跟 p {
border-style: none; border-top: solid; } 不交換,因為
* 'border-top' 是 ['border-top-width', 'border-top-style',
'border-top-color'] 的展開屬性
* 'border-style' 是 ['border-top-style', 'border-right-style',
'border-bottom-style', 'border-left-style'] 的展開屬性
'border-top-style' 重疊而且值不一樣(一個是 'solid' 一個是 'none')
(這說不定可以當一個 Google 的面試考題,如果我沒看過答案我應該想不出來 :p)



Mozilla 的超級大強者 David Baron 在 2002 年想出了一個不會改變太多 Gecko
的結構,但是解決了維持邏輯屬性跟物理屬性的順序這個要求的非常妙的方法
[5],而這個方法也就成為今天 Gecko 的 *-start/end 的實作†。

[5] http://lists.w3.org/Archives/Public/www-style/2002Sep/0049

解:

先只考慮 'margin-start' 的及書寫模式只有兩種(代號為 hr 跟 vt,分別是橫
書跟縱書)的情形,其他都可以完全推廣。這個技巧引入一些系統用的展開屬性
(margin-*-source),把 'margin-start' 跟 'margin-left' 等等**當作這些系
統屬性的速記屬性**:

例子一:p { margin-left: 1em; margin-start: 2em; }
margin-left: 1em;   ==>
- margin-left-value: 1em;
- margin-hr-left-source: left;
- margin-vt-left-source: left;
margin-start: 2em; ==>
- margin-start-value: 2em;
- margin-hr-left-source: start;
- margin-vt-top-source: start; /* 在直書的時候 start = top */
----------------------------------------------  CSS 層疊結果
- margin-left-value: 1em;
- margin-start-value: 2em;
- margin-hr-left-source: start; /* 在橫書的情形下 start 蓋過 left */
- margin-vt-left-source: left;
- margin-vt-top-source: start;

之後假如你的書寫模式是直書 vt 要找 top 的 margin 就先找
margin-vt-top-source 的值指到哪裡然後再找那個 margin-*-value 就行了。注
意到這個技巧的重點就是用重複的的系統展開屬性 'margin-hr-left-source' 記
錄兩個屬性的順序。

例子二:p { margin-start: 2em; margin-left: 1em; margin-after: 3em; }
margin-start: 2em; ==>
- margin-start-value: 2em;
- margin-hr-left-source: start;
- margin-vt-top-source: start;
margin-left: 1em;   ==>
- margin-left-value: 1em;
- margin-hr-left-source: left;
- margin-vt-left-source: left;
margin-after: 3em; ==>
- margin-after-value: 3em;
- margin-hr-bottom-source: after;
- margin-vt-left-source: after;
----------------------------------------------  CSS 層疊結果
- margin-left-value: 1em; /* 跟上面的例子一樣的值 */
- margin-start-value: 2em; /* 跟上面的例子一樣的值 */
- margin-hr-left-source: left; /* 這次在橫書的情形下 left 蓋過 start
了!! */
- margin-vt-top-source: start;
- margin-after-value: 3em;
- margin-vt-left-source: after; /* 在直書的情形下 after 蓋過 left */
- margin-hr-bottom-source: after;


雖然是很複雜的東西,不過也蠻有意思,僅供參考。


p.s. 我之後會整理一些關於瀏覽器實作的連結到小組的 wiki 上


此致

呂 康豪(Kenny), 中文興趣小組W3C連絡人
推特: http://twitter.com/kanghaolu
噗浪: http://www.plurk.com/kennyluck
新浪微博: http://t.sina.com.cn/1950042164

Received on Tuesday, 19 April 2011 04:02:48 UTC