prototype.jsを読み解く

第7回 Prototypeライブラリ(2052~2276行目)

この記事を読むのに必要な時間:およそ 10 分

2241:   criteria: {
2242:     tagName:      'n = h.tagName(n, r, "#{1}", c);   c = false;',
2243:     className:    'n = h.className(n, r, "#{1}", c); c = false;',
2244:     id:           'n = h.id(n, r, "#{1}", c);        c = false;',
2245:     attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
2246:     attr: function(m) {
2247:       m[3] = (m[5] || m[6]);
2248:       return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
2249:     },
2250:     pseudo:       function(m) {
2251:       if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
2252:       return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
2253:     },
2254:     descendant:   'c = \"descendant\";',
2255:     child:        'c = \"child\";',
2256:     adjacent:     'c = \"adjacent\";',
2257:     laterSibling: 'c = \"laterSibling\";'
2258:   },
2259: 

2241行目からはSelector.criteriaです。compileMatcher()から参照されます。

compileMatcher()では,Selector.patternsから入力されたCSSフィルタ文字列を先頭から徐々にパースしていき,マッチした部分に対して,Selector.criteriaで定義されるハッシュ表に関連付けられているJavaScriptコードに変換します。

出力されるJavaScriptはほぼSelector.handlers以下の関数の呼び出しとなっています。Selector.patternsの正規表現では,タグ名やクラス名などがマッチ結果として残るように設計されているので,それらを引数として渡して呼び出しています。

JavaScriptコード文字列で使われている部分において,nが現時点で処理対象となりうるノードの配列,hがSelector.handlers,rがマッチの際のルートノード,cが直前のコンビネータ状態(例えばselector + selectorという形なら"adjacent"が入る。なければfalse)を示します。

tagNameは2269行目の正規表現で,2372行目のh.tagName()を呼び出します。やっていることは,ノード配列nから,そのタグ名にマッチするものに絞り込む,という処理です。結果は再度nに代入されます。

classNameは2271行目の正規表現で,2415行目のh.className()を呼び出します。こちらもやっていることは対象となるノード配列から,指定されたクラス名にマッチするものだけを返しています。

idは2270行目の正規表現にマッチした場合で,2391行目のh.id()を呼び出しています。実際にはコンビネータの処理が入りうるので,それほど単純ではありませんが,こちらも対象ノード配列n以下で,指定されたidにマッチするものを返しています。

attrPresenceは2273行目の正規表現で,指定された属性が存在するかどうかだけを見るためにh.attrPresence()を呼び出しています。

attrは2274行目の正規表現で,属性の値の等価,否定,マッチングを行います。後述する正規表現の解説で書いているように,$5, $6に属性の値の部分が入ります。それらのどちらかをm[3]に入れて,h.attr()を呼び出しています。Templateクラスを使ってそれらを代入してJavaScriptコード文字列にしています。

pseudoは2272行目の正規表現にマッチした結果です。正規表現の解説は後述しますが,$6の部分は関数形式の引数があった場合に$6が存在し,そこに引数の値が入ります。$1にはnth-of-childなどの擬似クラスの名前が入ります。こちらも結果をTemplateを使って変換して返しています。

残りのdescendant, child, adjacent, laterSiblingはコンビネータに対する正規表現マッチの結果で,コンビネータ状態を表すcに状態文字列を入れる,というJavaScriptコードを返しています。

2260:   patterns: {
2261:     // combinators must be listed first
2262:     // (and descendant needs to be last combinator)
2263:     laterSibling: /^\s*~\s*/,
2264:     child:        /^\s*>\s*/,
2265:     adjacent:     /^\s*\+\s*/,
2266:     descendant:   /^\s/,
2267: 
2268:     // selectors follow
2269:     tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
2270:     id:           /^#([\w\-\*]+)(\b|$)/,
2271:     className:    /^\.([\w\-\*]+)(\b|$)/,
2272:     pseudo:       /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|\s|(?=:))/,
2273:     attrPresence: /^\[([\w]+)\]/,
2274:     attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/
2275:   },
2276: 

2260行目からはパーサの核となる正規表現Selector.patternsです。

ハッシュ形式のオブジェクトとなっていて,値として正規表現オブジェクト,プロパティ名としてそのパターンの名称が入っています。この名称部分は他の処理の部分でもキーとして使われます。

このpatternsは,for (var foo in patterns)としてループで上から順にマッチするかどうかをチェックし,マッチしたら該当する処理関数(XPathか自前JavaScriptかで変わる)部分に渡します。コメント部分を読むと,正規表現の優先順位の都合上,上記for文が,定義した順でプロパティ名を列挙してくれることに依存しているようです。基本的に最長マッチにしたいのに,descendantの短い正規表現が先に来るとそれにマッチしてしまうから,ということだと思われますが,実際にその後に行われる処理を見てみると,特に順番がこの通りでなくてもうまくパースできるように見えます。

ECMAの仕様によると,"The mechanics of enumerating the properties is implementation dependent. The order of enumeration is defined by the object."となっており,for-inでのプロパティ列挙順はなんら保障はされていないようですが,簡単にFirefox, IE7で試してみたところ,for-inはここで定義された順にプロパティ名を返しているようです。

また,()によるグルーピングは,マッチ後のテンプレート文字列で#{1}などの形で参照されます。

以下,難しそうな正規表現だけ説明します。

2272行目はpseudo正規表現です。これは分解すると以下のようになります。

部分正規表現 グルーピング マッチ例
^:( $1
(first|last|nth|nth-last|only)(-child|-of-type) $2, $3 first-child, nth-of-type
|empty|checked|(en|dis)abled|not $4 checked, disabled
)
( $5
\((.*?)\) $6 (99)
)?
( $7
\b|$|\s|(?=:) $8
)

2274行目はattr正規表現です。これは分解すると以下のようになります。

部分正規表現 グルーピング マッチ例
^\[ [
((?:[\w-]*:)?[\w-]+) $1 href, src
\s*
(?:
([!^$*~|]?=) $2 =, ~=, |=, ^=, $=, *=
\s*
( $3
(['"])([^\]]*?)\4 $4, $5 "value", 'value'
|
([^'"][^\]]*?) $6 all, 99
)
)?
\] ]
  • ECMA-262 第三版 - 12.6.4 The for-in Statement

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。