CSSセレクタ4の字句解析器を書いた

CSS4 セレクタエンジンの事始めとして。字句解析の範囲では、CSS3 とそれほど変わらない。違いは Reference combinator で使う「/」と、セレクタの subject を指定する「!」くらい。

ユニットテストは後で書きます……。

以下はメモ。

トークン種別は少なめに

  • Identifier
  • String
  • Invalid
  • Number
  • WhiteSpace
  • Punctuator

の6種類。CSS セレクタには Descendant combinator があるので Whitespace を捨てられない。

charCodeAt オンリー

セレクタはたいてい短いので正規表現を使ってもコストは気にならないだろうけど、いずれは CSS パーサを作るつもりなので、正規表現を使わずに書くことにした。

1文字1文字を比較していくわけだけど、その1文字を文字列として扱ったり数値として扱ったりするのは煩わしいので、全て数値として扱うことにした。つまり、charCodeAt のみを使い、charAt は使わない。文字コードは配列に格納していき、最後に String.fromCharCode(null, 文字コード配列) で文字列に戻す。パフォーマンスも問題なさそう

間接的な戻り値

文字列を読み進める関数は、次のように成功か失敗かを表す bool 値を戻り値で返し、実際に読み込んだ文字コードは第一引数の配列に格納するようにした。

function readNonascii(codes) {
    var code = text.charCodeAt(offset);
    if (code > 127) {
        codes.push(code);
        offset++;
        return true;
    }
    return false;
}

JavaC# ではこの手のメソッドがよくある気がする。一見ダサい。普通は次のように、単に読み込んだ文字コードを返すだけだと思う。

// 他の関数で複数の文字コードを読む場合もあるので、戻り値を配列 or null で統一
function readNonascii_another() {
    var code = text.charCodeAt(offset);
    if (code > 127) {
        offset++;
        return [code];
    }
    return null;
}

これなら読み取りに成功したかどうかは戻り値が truthy であることを確認すれば分かる。でも、使うときは前者の関数の方がコードがすっきりする。

var codes = [];

// 前者の場合
if (readNonascii(codes)) {
    // ...
}

// 後者の場合
var result; // 前者とは違い変数が必要
if ((result = readNonascii_another())) {
    codes = codes.concat(result);  // 前者と比べて1行増える
    // ...
}

ということで、前者のタイプの関数も場合によっては悪くないと思う。

NaN を扱う

Java では charAt(index) の index が範囲外だと例外が投げられる(CharSequence (Java Platform SE 7 ))けど、JavaScript では charAt なら空文字列、charCodeAt なら NaN が返されるだけ。

なので、現在のインデックス値が文字列の長さ未満かどうかの確認(if (offset < limit))を少し削れる。例えば次のように。

// before
if (offset < limit) {
    code = text.charCodeAt(offset);
    if (code === 10/*\n*/) {
        offset++;
        // ...
            codes.push(code);
            return true;
        // ...
        offset--;
    }
}

// after
code = text.charCodeAt(offset); // offset < limit で、戻り値が NaN かもしれない
if (code === 10/*\n*/) {        // NaN なら false になるだけなので問題ない
    offset++;
    // ...
        codes.push(code);
        return true;
    // ...
    offset--;
}

ただし、!== を使う場合や、ある数値間の範囲であることを確かめる場合はうまくいかない。初めに code && を付けなくてはいけない(NaN は falsy)。

うーん、やっぱりこれはよくない感じがする。