JavaScriptでCSSパーサーを書くための情報を収集中(5.5日目)

window.name + フレーム のくだりについてちょっと追記
uuCSSParser.js は uuAltCSS.js の古い呼び名です。

昨日の段階で、最新のCSS(CSS3)のセレクタを古いブラウザでも利用可能になりました。
このスクリプトを発展させれば、CSSハックや、ブラウザ毎にCSSファイルを用意する必要性を減らせることになります。

ただ、ちょっとした問題が見つかっていますので、今日はそれを解決します。

問題ズ

  1. Firefox3.5+, Safari3.1+, Opera9.5+, Google Chrome 2+ など CSS3 セレクタをブラウザがネイティブにサポートしている環境でも動いてしまう。
  2. ユニバーサルセレクタ(*)を見つけると、全ての要素に class="..." を追加してしまい、あまりよろしくない。
  3. IE6 で、アドレスバーからURLを直接入力したり、HTMLファイルをブラウザにドロップすると、cssText が改変済みの状態でロードされてしまい動かない。
これが
<style>
E > F {}
:digit {}
</style>

最初からこうなっている
<style>
UNKNOWN {}
:unknown {}
</style>

では、それぞれ解決していきましょう。
問題解決のために追加した処理は、ドーモ君(▼〜▲) で囲ってあります。

問題1. 最新のブラウザで動作する必要がないのに動いてしまう。

この表は「uuCSSParser.js を組み込むことにより、IE6, IE7, Firefox2, Firefox3, Safari3 で使用可能になるセレクタ」の一覧です。

Expr CSS NG Browser
E > F 2 IE6
E + F 2 IE6
E ~ F 3 IE6
[ATTR] 2 IE6
[ATTR=VALUE] 3 IE6
.class.class 2 IE6
:first-child 2 IE6
:focus (※) 2 IE6,IE7
:not 3 IE6,IE7
:root 3 IE6,IE7
:target 3 IE6,IE7
:enabled 3 IE6,IE7
:disabled 3 IE6,IE7
:checked 3 IE6,IE7
:empty (※) 3 IE6,IE7
:last-child 3 IE6,IE7,Sf3
:only-child 3 IE6,IE7,Sf3
:nth-child 3 IE6,IE7,Fx2,Fx3,Sf3
:nth-of-type 3 IE6,IE7,Fx2,Fx3,Sf3

IE で動かないかも

http://www.quirksmode.org/css/contents.html を参考にしました。

この表から導かれるコードを、初期化処理に追加します。

function autoexec() {
  _cssp.execute(_cssp.parse());
  _mm.event.unbind(_win, "load", autoexec);
}
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
if ((_ie && (_uaver < 8 || !_mm.iemode8)) ||
    (_mm.opera  && _uaver < 9.5) ||
    (_mm.gecko  && _uaver < 3.5) ||
    (_mm.safari && _uaver < 3.1))
  _mm.event.bind(_win, "load", autoexec);
}
// ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

問題2. ユニバーサルセレクタを見つけると全ての要素に class="..." を追加してしまい、あまりよろしくない。

 * { color: red }

としてしまうと、全ての要素に上記のスタイルを実現するコードが埋め込まれてしまうのと、ユニバーサルセレクタ(*)は古いブラウザでもほぼ完璧にサポートされているため、ケアする必要がありません。

uuCSSParser.execute() の途中に判定を追加します。

        for (j = 0, jz = data.length; j < jz; ++j) {
          expr = data[j].expr;
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
          if (expr === "*") { // skip universal selector
            continue;
          }
// ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

問題3. IE6 で、アドレスバーからURLを直接入力したり、HTMLファイルをブラウザにドロップすると、cssText が改変済みの状態でロードされてしまい動かない。

ページの更新ボタンをクリックした時には発生せず、URLの直接入力やHTMLファイルのドラッグ&ドロップで発生するようです。

uuCSSParser.parse() に判定を追加します。

var _win = window;

  parse:
      function(css) { // @param CSSString(= ""):
                      // @return Hash: { specs: [spec-num1, spec-num2, ...],
                      //                 data[spec-num1]: { rule, expr, decl },
                      //                 data[spec-num2]: ... }
    var specs = [], data = {}, reload = 0;

    _ie && _cssp._memento();
    if (!css) {
      css = _cssp._importStyleSheets();
      // ▼▼▼▼▼▼▼▼▼
      if (_ie && _uaver < 7) {
        // Auto reload for IE6
        //  case 1: Drop of HTML file
        //  case 2: Input of URL to address bar
        if (/UNKNOWN[^\{]+?\{|:unknown[^\{]+?\{/.test(css) &&
            "UNKNOWN" !== _win.name) {
          reload = 1;
        }
        _win.name = "";
        if (reload) {
          _win.name = "UNKNOWN";
          location.reload(false); // reload from cache
        }
      }
      // ▲▲▲▲▲▲▲▲▲
    }
    _cssp._collectStyleSheets(specs, data, css);
    specs.sort(function(a, b) { return b - a; }); // sort of number(10 -> 1)
    return { specs: specs, data: data };
  },

処理を読み解くためのヒントは、

  • IE の window.name はページをリロードしてもリロード前の内容を保持している
  • キャッシュからリロードする場合は、解釈できないCSSセレクタを含んでいても、UNKNOWN や :unknown に改変されない

です。
同様の処理をクロスブラウザで実現するには、クッキーやローカルストレージを使うことになるでしょう。
window.name を使用しているので、フレームを使用していて、各フレームを名前(window.name)で判別しているコードの邪魔をすることになりますが、それについては問題無いと判断しています。

  • HTML5 ではフレーム(frameset)が利用できない。
  • フレーム(frameset)は XHTML1.0 でも使えるが、XHTMLHTML5の登場で死んでゆく規格なので気にしない。 IE6 は、<!doctype>宣言を見て標準モードにするだけで、XHTML1.0 と HTML4.01 は同じものととして扱いますね。
  • Javadoc などのツールがフレーム(frameset)を必要とするHTMLを生成するが、frameset を利用したHTMLを人間が生成するケースは減少するはず。

という考えを持っています。ちょっと独善的かもしれませんね。
以下追記

  • window.name を利用した上記のコードは、IE6 だけで動作します。
  • クッキーやローカルストレージは OFF にできますが、window.name は ON/OFF できません。今回のケースでは、window.name を使う必然性があります。
  • frameset を求めているユーザが、同時に CSS3 セレクタを欲しがるケースはほとんど無いはずです。これらはかなり排他的な関係です。
  • iframe 要素にも name 属性がありますが、IE6 の仕様だと、iframe.name ではなく、iframe.contentWindow.name にアクセスすることになります。
    • 昨今の人なら、iframe.name は使わずに、ちゃんとDOM(iframe.id) を使うんじゃないかと。

つまり、<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"> な HTML で、CSS3 セレクタを使いたい場合に、Web1.0 な書き方をすると、IE6 で問題が発生するかもしれませんが、ニッチすぎるケースなんで、そのへんは「仕様」ということになりますね。
うーんと。まだ考えに抜けがあるかもしれませんね。引き続きご意見をお待ちしています。

反省会

  • 大きな問題をはらみつつ、それを回避する方法が隠されているのが IE の面白いところですね。
    • IE を深く知るにつれ「これが10年以上前に設計されたブラウザなのか!?」と大きな驚きがあったりします。
      • IE6 は10年も前から XHR, Canvas, CSS3 セレクタ, ローカルストレージ を実現できるだけの懐の深さを持っていたんだよ。