古いブラウザでもCSS3セレクタを使ってWebページをデザインできるようにしてみた

uuCSSBoost.js は uuAltCSS.js に名前が変更になりましたが、一部の説明が記事を書いた当時の古い名前のままになっています。最新版では、uuCSSBoost.revalidate() は廃止され uuAltCSS() を呼び出すように変更になっています。
コードの解説を追記しました。

  • 「最新の規格を実装したブラウザが登場しても、IE6 のせいで諦めるしかないのか」
  • 「CSS3セレクタを古いブラウザでも使いたい!」

といった現場の声にお応えして、ほぼ全てのブラウザで CSS3 セレクタを利用したページデザインが可能になるJavaScript ライブラリを作ってみました。

特徴

  • 軽いよ
  • いろんなブラウザで動くよ(Firefox2+, Opera9.2x+, Safari3+, IE6+, Google Chrome1+)
    • 95%〜98%ぐらいのシェアをカバーできるんじゃないかな?
  • 使えるセレクタはこれだよ CSS3セレクタ一覧
    • 全部使えるわけじゃないよ。擬似要素(:before や :first-line など)は残念ながら使えないよ。
  • @import が使えるよ
  • !important が使えるよ
  • 静的なCSS用だよ。つまりデザイナーさん向けだよ
    • JavaScriptで要素を動的に追加したり移動した場合は、uuAltCSS() で再評価すると大丈夫だよ

オマケの機能

  • IE6 で position: fixed が動くよ
  • IE6 で position: absolute したときにテキストを選択できなくなるバグを修正してくれるよ
  • IE6〜IE8 で opacity: 0.5 が動くよ
  • IE6 で透過png が使えるよ
  • IE6 で max-width, min-width, max-height, min-height が使えるよ
  • IE6, IE7 で display: table と display: table-cell が使えるよ
  • いろんなブラウザで border-radius: が使えるよ
  • いろんなブラウザで box-shadow: が使えるよ(擬似的な影を描画するブラウザもあるよ)
  • IE6で :hover が a要素以外でも使えるよ

これ以外にもシークレットなオマケがどんどん増えてたりするよ

制限事項

  • メタメタにハックされてるCSSはどうなるか試しきれてないよ
    • 普通のCSSは動くと思うよ
  • メディアタイプはサポートしていないよ
    • 印刷用のページやモバイル用のページで使わないほうがいいよ
      • JavaScript から「いまは印刷用のレイアウト」「いまはモバイル用のレイアウト」とか状況判断できないから実装できないんだよね
  • uupaa.js シリーズ(今回は uuAltCSS.js) を使う場合は、uu または UU で始まる識別子はシステムで予約済みのプリフィクスなので使わないで下さい。簡単に言うと私が自由に使いたいので使わないで下さい。
    • C/C++ の アンダーバー(_) と同じ感じでお願いします。

ソースコード


正味200ステップぐらいだし、チョット前の日記を読めば色々書いてあるから簡単にコードリードできると思うよ

って上では書いたけど、ちょっと仕組みを説明するよ

仕組みが謎なままだと採用されないと思うので、簡単ですがザックリと流れを説明します。

uuCSSBoost.validate() がやってることは、

A. 生のCSSをかき集める
B. CSSからコメントなどを除去しキレイにする
C. クォート("...") 内の特定の文字({};,)を数値参照化
D. "{" と "}" で split
E. 数値参照を元に戻す
F. !important とそれ以外にルールを分別
G. ルールの重み付けを計算
H. 新スタイルシートを追加
I. ルールを重み付けでソート
J. 新スタイルシートに ".uucss{数値} { color: ... }" を追加
K. ルールに書かれている セレクタ("div>ul") でDOM探索。
L. ヒットした要素の className に "uucss{数値}" を追加
M. !important は IE なら、新スタイルシートに ".uucss{数値} { color: ... }" を追加
N. IE 以外なら style.setProperty("color", 値, "important") で強制上書

です。uuCSSBoost.revalidate() は、

O. 新スタイルシートから全てのルールを削除
P. 各要素の className から、"uucss{数値}..." を除去
Q. style.setProperty("color", 値, "important") を除去し上書した値を元に戻す
R. A からやり直す(状況次第でJからでもいいような気がする)

をやっています。

http://code.google.com/p/uupaa-js-spinoff/source/browse/trunk/src/uuCSSBoost.js?spec=svn190&r=190 と対比しながら説明します。

  // uuCSSBoost.validate - CSS parse and apply
  validate: function(css) { // @param CSSString(= ""):
    if (!_markup && !_boostup) { return; }

/* [1] */
    _ss.memento();

/* [2] */
    // collect style sheets
    css = _ss.cleanup(css || _ss.imports());

/* [3] */
    // http://d.hatena.ne.jp/uupaa/20090619/1245348504
    if (_ie && _uaver < 7) {
      v = _win.name;
      _win.name = ""; // clear
      if (/UNKNOWN[^\{]+?\{|:unknown[^\{]+?\{/.test(css) &&
          "UNKNOWN" !== v) {
        _win.name = "UNKNOWN";
        location.reload(false);
        return;
      }
    }

[1]. uuStyleSheet.memnto() で style 要素の innerHTML から生のCSS文字列を取得して、style要素の uuSSMemento プロパティに bond します。bond ってのはオレオレプロパティを要素に紐付けすることです。

[2]. document.styleSheets を見て、link要素とstyle要素から生のCSSをかき集めます。このとき、@importがあれば、XHR(同期)も使ってかき集めます。ロードしたCSSはキャッシュしておいて、uuCSSBoost.revalidate() で再利用します。

[3]. IE6 で発生する CSS 書き換え問題 用の処理です。特定条件下では、window.name を一種のメモリとして使い、キャッシュからページをリロードすることで回避します。

/* [4] */
    _cssb._parse(css);

/* [5] */
    _specs = exsort(_specs);

/* [6] */
    _ss.create(_ssid);

[4]. パースして、内部変数 _specs と _data に結果を溜め込みます。uuCSSBoost.js はステートレスではなく状態を持ちます。_specs は 配列で、CSSセレクタの重み付けに基づいた、優先順位が格納されています。!important は +10000 された値になります。

[5]. _specs を特殊な順番にソートします。10000未満の値は降順に、10000以上の値は昇順にソートします。(例: [3, 2, 1, 0, 10000, 10001, ...]), Array.sort() 関数の中でがんばってもクロスブラウザにならなかったので、わざわざ関数を用意してやってます。

[6]. スタイルを上書するためにスタイルシートを追加しています。

    for (; i < iz; ++i) {
      spec = _specs[i];
      data = _data[spec];

      for (j = 0, jz = data.length; j < jz; ++j) {
        expr = data[j].expr;
/* [7] */
        if (expr === "*") {
          continue;
        }
        bz = "";
        if (_boostup) {
          for (z = [], l = 0, lz = data[j].pair.length; l < lz; ++l) {
/* [8] */
            w = data[j].pair[l].prop.toLowerCase();
            (w in BOOST_PROP) && z.push(BOOST_PROP[w]);

            w += data[j].pair[l].val.toLowerCase();
            (w in BOOST_DECL) && z.push(BOOST_DECL[w]);
          }
          bz = z.join(" ");
        }

[7]. ユニバーサルセレクタ(*) はケアする必要がないので除外しています。

[8]. BOOST_PROP = {} と"スタイルプロパティ"が一致するか、 BOOST_DECL = {} と"スタイルプロパティ" + "値"が一致する場合に、bz に文字が設定されます。これは、特定のスタイルをマークアップしておいて uuCSSBoostIE.js などで何か特別な処理をするためのものです。

/* [9] */
        node = uuQuery(expr);

        if (_ie || spec < 10000) {
/* [10] */
          id = _rule[expr] || (_rule[expr] = ++_uid);
          _markup &&
              _ss.insertRule(_ssid, ".uucss" + id,
                             (spec < 10000) ? data[j].decl.join(";")
                                            : data[j].decl.join(IMP) + IMP);
          for (k = 0, kz = node.length; k < kz; ++k) {
/* [11] */
            w = uuQuery.unique(node[k]);
            !(w in unique) &&
                (unique[w] = { node: node[k], hash: {}, dump: [] });
            if (!(id in unique[w].hash)) {
              unique[w].hash[id] = id;
              _markup  && unique[w].dump.push("uucss" + id);
            }
            if (bz && !(bz in unique[w].hash)) {
              unique[w].hash[bz] = bz;
              _boostup && unique[w].dump.push(bz);
            }
          }
        } else { // !important

[9]. uuQuery で要素を検索しています。expr には "div ul>li" などの文字列が入ります。

[10]. IE または !important 以外なら uuQuery で見つかった要素の className 属性に "uucss数値" を設定します(実際の設定はもっと下でやってます)。また、uuStyleSheet.insertRule で ".uucss数値 { color: red }" といったルールを新しいスタイルシートに登録しています。

[11]. 二重登録を回避するためのものです。本筋とは関係ありません。

          for (k = 0, kz = node.length; k < kz; ++k) {
            v = node[k];
            if (_markup) {
/* [12] */
              v.setAttribute("uuCSSI", 1);
/* [13] */
              !("uuCSSIHash" in v) && (v.uuCSSIHash = {}); // bond

              for (l = 0, lz = data[j].pair.length; l < lz; ++l) {
                w = data[j].pair[l];
/* [14] */
                v.uuCSSIHash[w.prop] = v.style.getPropertyValue(w.prop); // mem
                v.style.setProperty(w.prop, w.val, "important");
              }
            }
            _boostup && z && (v.className = z + " " + v.className);
          }
        }

[12]. !important の処理です。ここでは、uuQueryで見つかった要素をマークアップするために uuCSSI というオレオレ属性を bond してます。uuCSSBoost.revalidate() で uuCSSI を検索し除去するために必要です。

[13]. 同じく、uuCSSIHash = {} というオレオレハッシュをbondしています。

[14]. 現在のスタイルプロパティを uuCSSIHash に退避して、etProperty(w.prop, w.val, "important"); で !important 指定付きの値を強制的に上書しています。

/* [15] */
    for (i in unique) {
      v = unique[i].node;
      w = unique[i].dump.join(" ") + " " + v.className;
      v.className = w.replace(TRIM, "").replace(/\s+/g, " ");
/* [16] */
      v.setAttribute("uuCSSC", "1");
    }

[15]. [10] で貯めておいたデータを元に、className 属性に対し "uucss数値" を追加していきます。複数クラスを追加することがあるため、追加が終わったら、className をキレイに(正規化)しています

[16]. 追加したクラスを、uuCSSBoost.revalidate() で元に戻すため、uuCSSC というオレオレ属性を bond してマークアップしておきます。わざわざ要素にオレオレ属性を追加するのは、DOM操作で要素が複製された場合や、要素が削除/追加された場合に対応するためです。

/* [17] */
    function calcSpec(expr) {
      var a = 0, b = 0, c = 0;
      function B() { ++b; return ""; }

      expr.replace(SPEC_NOT, function(m, E) { return " " + E; }).
            replace(SPEC_ID, function() { ++a; return ""; }). // #id
            replace(SPEC_CLASS, B).     // .class
            replace(SPEC_CONTAINS, B).  // :contains("...")
            replace(SPEC_PELEMENT, ""). // ::pseudo-element
            replace(SPEC_PCLASS, B).    // :pseudo-class
            replace(SPEC_ATTR, B).      // [attr=value]
            replace(SPEC_E,  function() { ++c; return ""; }); // E
      return a * 100 + b * 10 + c;
    }

[17]. 重み付けを計算する処理です。#ID なら + 100, タグなら +1, それ以外なら +10 していきます。:afterなどの擬似要素は +0 です。

/* [18] */
    ary = css.replace(/(["'])((?:.*?)[\{\};,](?:.*?))\1/g, esc). // ungreedy
              replace(/\{\}/g, "{ }"). // ie bug
              split(/\s*\{|\}\s*/);
    !_ie && ary.pop(); // ie bug

/* [19] */
    if (ary.length % 2) {
      return; // parse error
    }

/* [20] */

[18]. uuCSSBoost.js は、CSSを正確に構文解析していません。"{" と "}" でぶった切っています(理由は後述)。CSS を "{" と "}" で切断する処理の前に数値参照化する処理が入ります。
これは、'E>F[title="{[hoge,:,:,huga]}"] { color: red }' のように クォート内にシンタックスに関係する特別な文字("{},;")が含まれている場合にパースできなくなるのを防ぐためです。
replace(/\{\}/g, "{ }") は IE の String.split() が 空要素を戻り値に含めないバグがあるためにわざわざこうしてます。

[19]. CSSを"{" と "}" で切断した結果の配列は必ず偶数になるはずなので、奇数個なら処理を中断します。

[20]. コードは省略しますが、やってることは、"color: red; font-size: 24pt !important" を2つのルールに分割し、_specs と _data を構築しています。

/* [21] */
if (_ie) {
  ++_markup;
  ++_boostup;
  (_uaver < 9) && _mm.mix(BOOST_PROP, { opacity: "uucssopacity" });
  (_uaver < 7) && _mm.mix(BOOST_DECL, { positionfixed: "uucssposfxd" });
}
/* [22] */
_mm.opera  && (_uaver < 9.5) && ++_markup;
_mm.gecko  && (_uaver < 3.5) && ++_markup;
_mm.safari && (_uaver < 3.1) && ++_markup;
_mm.chrome && (_uaver < 2)   && ++_markup;

[21]. IE9以下なら、opacity というスタイルプロパティをマークアップするように設定しています。また、IE6以下なら、position:fixed を見つけたときにマークアップするように設定しています。

[22]. Opera9.2x, Fx2, Fx3, Safari3, Google Chrome1 でCSS3セレクタが使えるように設定しています。

CSSを正確に構文解析していません」 の理由

色々やって、それがベストだったから。

# ちなみに、uuQuery.js は正確に構文を解析しています


反省会

  • ほかほかの作りたて
    • 判りやすいバグはとりあえず潰したつもりだけど、まだ色々あると思う
  • 動的な変更も監視できるけど、あえて静的なCSSのみ適用するのは軽くするため
    • 動的なスタイルの変更は JavaScript でどうとでもできるからね
  • IE専用なら IE7.js や IE8.js があるけど、CSS3 セレクタを使ったページデザインが可能になるクロスブラウザJavaScript は、もしかしたら初かもね

HTML5::Canvasも一緒に使いたい場合は、http://uupaa-js-spinoff.googlecode.com/svn/trunk/src/ にある uuCanvas.js をくっつけると使えるようになります。

  • あー、良く考えたらコンディショナルな仕組みは必要なのかも。
    • レンダリングエンジンでスタイルを適用したり/しなかったり、代替スタイルを用意するって仕組みはあってもいいのかもしれない
      • CSSハックは、ぐっちゃぐちゃなCSSになるのがアレだけど、バンドエイドとしては上手く機能しているもんね。

バグレポートや、ご要望/感想などお待ちしています。 @uupaa