prototype.jsを読み解く

第8回Prototypeライブラリ(2277~2620行目)

ユニットテスト

PrototypeライブラリをSubversionからcheckoutすると、test/ ディレクトリ以下にユニットテスト用のファイルが含まれています。

今回ライブラリ内に疑問点などがあったり、こういうCSSセレクタで正しく動くのだろうか、というのを確認するのに便利に使いました。

assertEnumEqual([], $$('#p + #p'));

というような行を追加するだけで、ブラウザから簡単にテストが行えます。

では、Selectorクラスの後半です。

2277:   handlers: {
2278:     // UTILITY FUNCTIONS
2279:     // joins two collections
2280:     concat: function(a, b) {
2281:       for (var i = 0, node; node = b[i]; i++)
2282:         a.push(node);
2283:       return a;
2284:     },
2285: 
2286:     // marks an array of nodes for counting
2287:     mark: function(nodes) {
2288:       for (var i = 0, node; node = nodes[i]; i++)
2289:         node._counted = true;
2290:       return nodes;
2291:     },
2292: 
2293:     unmark: function(nodes) {
2294:       for (var i = 0, node; node = nodes[i]; i++)
2295:         node._counted = undefined;
2296:       return nodes;
2297:     },
2298: 

2277行目からは、様々な処理を詰め込んだSelector.handlersです。まずは内部で使われるユーティリティ関数です。

concat()は、ノード配列aにbをマージするだけです。aの後ろにbが連結された配列を返します。

mark(), unmark() は、渡されたノード配列の中身各々について、_countedプロパティをtrueに設定したりundefinedにしたりします。両方とも元々の配列(ただし_countedプロパティが操作されたもの)を返します。これは、CSSルールによるフィルタを行う際に、

  1. 基準となるノード配列全体をunmark()する
  2. ルールに該当する要素を抽出する
  3. 抽出された配列をmark()する
  4. 必要に応じて2, 3を繰り返す
  5. 基準となるノード配列から_countedにマークされたものだけを抽出する

とするためなどに使われます。これで順番を保ったまま、重複なしで抽出することができます。

2299:     // mark each child node with its position (for nth calls)
2300:     // "ofType" flag indicates whether we're indexing for nth-of-type
2301:     // rather than nth-child
2302:     index: function(parentNode, reverse, ofType) {
2303:       parentNode._counted = true;
2304:       if (reverse) {
2305:         for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
2306:           node = nodes[i];
2307:           if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
2308:         }
2309:       } else {
2310:         for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
2311:           if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
2312:       }
2313:     },
2314: 

2302行目からはindex()関数です。pesudos.nth()関数からのみ呼ばれています。例えばE:nth-child(99)というセレクタが指定されている場合、pseudos.nth()のnodesには要素Eの配列が入っています。このnodesの中身それぞれの.parentNodeがこのindex()関数に渡されてきます。この時reverseが偽、ofTypeも偽だとすると、2310 行目からのループにおいて、nodes = parentNode.childNodes はnth()内のnodesの一要素である要素Eの兄弟要素全体、となります。この兄弟要素全体をループで回し、該当する要素のnodeIndexプロパティに順番となる数字を入れていきます。この時対象はnodeType == 1 (ELEMENT_NODE) で、かつofTypeが偽かnode._countedが真の場合のみになっています。

ofTypeが偽の場合は、nth-child()系の擬似クラスになるので、その場合は必ずnodeIndexに値を代入します。

ofTypeが真の場合はnth-of-type()系の擬似クラスであり、その場合はnth()のnodesは対象となる要素Eのみが含まれる配列となっているはずです。その時にnth()側でmark(nodes)を使って_countedプロパティがチェックされているはずです。これを利用して、自分自身と同じ要素名を持つ要素だけを順番に数える、という処理にしています。

2315:     // filters out duplicates and extends all nodes
2316:     unique: function(nodes) {
2317:       if (nodes.length == 0) return nodes;
2318:       var results = [], n;
2319:       for (var i = 0, l = nodes.length; i < l; i++)
2320:         if (!(n = nodes[i])._counted) {
2321:           n._counted = true;
2322:           results.push(Element.extend(n));
2323:         }
2324:       return Selector.handlers.unmark(results);
2325:     },
2326: 

2316行目からはunique()関数です。

フィルタ処理で蓄積されたノードの配列に対して、重複が無いようにした配列を返します。

これはSelector.findChildElements()か、Selector.compileMatcher()で生成されるmatcher関数の最後から呼ばれます。

_countedを使う関数は使用後にクリアすることになっているので、ここでは自分で_countedを使って初出のものだけをresults配列に積んでいき、最後にunmark()でクリアしています。

2327:     // COMBINATOR FUNCTIONS
2328:     descendant: function(nodes) {
2329:       var h = Selector.handlers;
2330:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2331:         h.concat(results, node.getElementsByTagName('*'));
2332:       return results;
2333:     },
2334: 

2327行目からは"COMBINATOR FUNCTIONS"すなわちCSSセレクタの結合子に対するハンドラ関数です。

descendant()は、"要素A 要素B"という形式の、ある要素より内側にある要素を示すものです。

CSSセレクタのパーサがdescendant combinatorにマッチするタイミングは、上記"要素A 要素B"の間の空白文字の位置となります。このハンドラに到達するタイミングでは、引数のnodesには"要素A"にマッチするものが集められています。ここでdescendant()ハンドラ関数の役割は、次の"要素B"セレクタでフィルタすることができるように(handlers.tagName()で行われます⁠⁠、nodesの内側の要素を列挙することです。

そのための実装は、渡されたノード配列に対して、各々の子供要素をgetElementsByTagName('*')ですべて列挙して、返り値用のresultsに蓄積して返す形となっています。

2335:     child: function(nodes) {
2336:       var h = Selector.handlers;
2337:       for (var i = 0, results = [], node; node = nodes[i]; i++) {
2338:         for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
2339:           if (child.nodeType == 1 && child.tagName != '!') results.push(child);
2340:       }
2341:       return results;
2342:     },
2343: 

2335行目からはchild()ハンドラです。

セレクタの表記では"要素A > 要素B"となり、直下の要素を示します。

渡された各ノードに対してchildNodesプロパティの配列をループで取得し、nodeTypeがELEMENT_NODE(1)のものだけを抽出して返しています。

IEではコメントノードのnodeTypeが1 (ELEMENT_NODE)となってしまう場合があるようで、この場合はtagNameが'!'となるのでこれもスキップします。

2338行目のchildrenは定義はされていますが使われていないようです。

2344:     adjacent: function(nodes) {
2345:       for (var i = 0, results = [], node; node = nodes[i]; i++) {
2346:         var next = this.nextElementSibling(node);
2347:         if (next) results.push(next);
2348:       }
2349:       return results;
2350:     },
2351: 

2344行目からはadjacent()です。

CSSセレクタでは"要素A + 要素B"とする部分です。隣接するふたつのノードが対象ですので、渡されたノード配列に対してfor文ですべてループし、各々に対してSelector.handlers.nextElementSibling()を呼び出して、要素が返ってくればpush()しています。

これでresultsで返すのは"要素B"にあたるものの集合、となります。

2352:     laterSibling: function(nodes) {
2353:       var h = Selector.handlers;
2354:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2355:         h.concat(results, Element.nextSiblings(node));
2356:       return results;
2357:     },
2358: 

2352行目からはlaterSibling()です。

原典がどこかわかりませんが、"要素A ~ 要素B"という書式で、"要素A"の兄弟要素のうち、順番が"要素A"より後ろとなる兄弟にマッチします。

実装は、渡された各ノードに対して、Element.nextSiblings()を呼び出して、候補となる要素を配列で返してresultsにマージする、という形になっています。

2359:     nextElementSibling: function(node) {
2360:       while (node = node.nextSibling)
2361:               if (node.nodeType == 1) return node;
2362:       return null;
2363:     },
2364: 

2359行目からはnextElementSibling()です。

これは主に他のハンドラ関数から呼ばれるヘルパ関数で、自分の後続兄弟のうち、ELEMENT_NODE(1)のものが見つかったらそれを返す、というものです。

2365:     previousElementSibling: function(node) {
2366:       while (node = node.previousSibling)
2367:         if (node.nodeType == 1) return node;
2368:       return null;
2369:     },
2370: 

previousElementSibling()は nextElementSibling()の逆で、自分の前にある兄弟の内で、最初に見つかった ELEMENT_NODE(1) であるものを返します。

2371:     // TOKEN FUNCTIONS
2372:     tagName: function(nodes, root, tagName, combinator) {
2373:       tagName = tagName.toUpperCase();
2374:       var results = [], h = Selector.handlers;
2375:       if (nodes) {
2376:         if (combinator) {
2377:           // fastlane for ordinary descendant combinators
2378:           if (combinator == "descendant") {
2379:             for (var i = 0, node; node = nodes[i]; i++)
2380:               h.concat(results, node.getElementsByTagName(tagName));
2381:             return results;
2382:           } else nodes = this[combinator](nodes);
2383:           if (tagName == "*") return nodes;
2384:         }
2385:         for (var i = 0, node; node = nodes[i]; i++)
2386:           if (node.tagName.toUpperCase() == tagName) results.push(node);
2387:         return results;
2388:       } else return root.getElementsByTagName(tagName);
2389:     },
2390: 

2371行目からはタグ、ID、クラス名などを処理するハンドラです。

まずはtagName()関数です。Selector.criteria.tagNameでmatcher関数を作る際に、CSSフィルタのパース中にtagNameを見つけると呼び出されます。これらの関数では、これまでに絞り込まれたノードの配列がnodesに、元々の検索対象のルートノードがrootに、tagName()の場合はCSSフィルタで見つかったタグ指定(もしくは"*")がtagNameに、直前に結合子(combinator)指定があればcombinatorに渡されて呼び出されます。返す値はCSSフィルタにマッチし得るノードの配列、となります。

実装では、nodesにまだ何も含まれていなければroot.getElementsByTagName()でroot以下のタグ名でマッチさせその配列を返します。

そうでなければ、combinatorが指定されていればそれに合わせてnodesを絞り込みます。"descendant"以外のcombinatorの場合はSelector.handlers以下の関数でフィルタ処理を行います。

tagNameが'*'なら、残っているnodesすべてをそのまま返し、そうでなければtagNameにマッチするものだけを集めて返します。

2391:     id: function(nodes, root, id, combinator) {
2392:       var targetNode = $(id), h = Selector.handlers;
2393:       if (!nodes && root == document) return targetNode ? [targetNode] : [];
2394:       if (nodes) {
2395:         if (combinator) {
2396:           if (combinator == 'child') {
2397:             for (var i = 0, node; node = nodes[i]; i++)
2398:               if (targetNode.parentNode == node) return [targetNode];
2399:           } else if (combinator == 'descendant') {
2400:             for (var i = 0, node; node = nodes[i]; i++)
2401:               if (Element.descendantOf(targetNode, node)) return [targetNode];
2402:           } else if (combinator == 'adjacent') {
2403:             for (var i = 0, node; node = nodes[i]; i++)
2404:               if (Selector.handlers.previousElementSibling(targetNode) == node)
2405:                 return [targetNode];
2406:           } else nodes = h[combinator](nodes);
2407:         }
2408:         for (var i = 0, node; node = nodes[i]; i++)
2409:           if (node == targetNode) return [targetNode];
2410:         return [];
2411:       }
2412:       return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
2413:     },
2414: 

2391行目からはid()ハンドラです。

まだnodesが空で、rootがDOMのトップであるdocumentなら、単純に$(id)したもの(もしくは空配列)を返します。

nodesが空で、rootがdocument以外の場合、$(id)が見つかって、それが検索開始要素のrootの内側にあるようならそれを配列で返します(2412行目の [targetNode] 部分⁠⁠。$(id)が見つからないか、見つかってもrootの外側だった場合には空配列を返します(同 [] 部分⁠⁠。

nodesがすでにある場合(2395行目以降⁠⁠、まずcombinatorが渡されているようならそれに従いnodesを絞り込みます。

combinatorが'child'の場合、nodes配列を調べて$(id).parentNodeと一致するものが見つかれば$(id)を配列で返します。

combinatorが'descendant'の場合は、nodes配列を調べて$(id)がその子孫ならば$(id)を配列で返します。

combinatorが'adjacent'なら、nodes配列の各々に対して、$(id)の直前の兄弟と一致するかどうかを調べ、そうなら$(id)を配列で返します。

combinatorがそれ以外の場合(といっても残りは'laterSibling'だけですが⁠⁠、Selector.handlers以下の関数(すなわち2352行目のSelector.handlers.laterSibling())を使っています。

2408行目には (1) combinatorが指定されていないか、(2) combinatorが'laterSibling'であるか、(3) それ以外のcombinatorで該当要素が無かった場合にのみ到達します。

(1)の場合は"div#id"のような指定なので、現在残っているnodesから該当IDを持つ要素を探して見つかれば返します。

(2)の場合は"div ~ #id"という形で、絞り込まれた結果が2406行目でnodesに入っているのでこの中から該当IDを探します。

(3)はcombinator 指定がある場合なので、直前の要素配列nodesから該当IDを探すのは間違っているように見えます。(3)の場合は空配列を返すべきでしょう。

CSSセレクタのID指定では、対象となる要素が見つかるか見つからないか、しか無いので、この関数の返り値は空配列か、該当要素が一つ入った配列か、のどちらかしかありません。

2415:     className: function(nodes, root, className, combinator) {
2416:       if (nodes && combinator) nodes = this[combinator](nodes);
2417:       return Selector.handlers.byClassName(nodes, root, className);
2418:     },
2419: 

2415行目からはCSSセレクタのクラス名指定です。

combinatorが指定されている場合はSelector.handlers.descendant()などを使って絞込み、その後byClassName()関数に渡してその結果を返します。

2420:     byClassName: function(nodes, root, className) {
2421:       if (!nodes) nodes = Selector.handlers.descendant([root]);
2422:       var needle = ' ' + className + ' ';
2423:       for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
2424:         nodeClassName = node.className;
2425:         if (nodeClassName.length == 0) continue;
2426:         if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
2427:           results.push(node);
2428:       }
2429:       return results;
2430:     },
2431: 

className()から呼び出されるbyClassName()です。

まずnodesがセットされていなければdescendant()を使って引数で渡されたroot要素以下の要素すべてをnodesに集めます。

その後、各ノードに対して、指定されたクラス名が含まれているかを確認しますが、その際の確認方法として、Element.ClassNamesは使わずに、文字列の包含関係だけで行っています。単純に含まれているかどうかをチェックすると、"A"というクラスを確認しようとすると"AB"もマッチしてしまうため、検索する際は" A "という風に前後にスペースをつけます。そうするとclassNameプロパティ文字列の先頭・末尾にスペースが無い場合があるので、こちらにも前後にスペースを付け" A AB C "のような形にします。この状態で " A AB C ".include(" A ") という式でクラス名含まれているかどうかを確認しています。

2432:     attrPresence: function(nodes, root, attr) {
2433:       var results = [];
2434:       for (var i = 0, node; node = nodes[i]; i++)
2435:         if (Element.hasAttribute(node, attr)) results.push(node);
2436:       return results;
2437:     },
2438: 

2432行目からはattrPresence()ハンドラです。CSSセレクタでは "[attr]" という表記になります。

実装は、各ノードに対して、渡された属性名が存在するかをElement.hasAttribute()で調べ、存在するノードだけを配列で返します。

2439:     attr: function(nodes, root, attr, value, operator) {
2440:       if (!nodes) nodes = root.getElementsByTagName("*");
2441:       var handler = Selector.operators[operator], results = [];
2442:       for (var i = 0, node; node = nodes[i]; i++) {
2443:         var nodeValue = Element.readAttribute(node, attr);
2444:         if (nodeValue === null) continue;
2445:         if (handler(nodeValue, value)) results.push(node);
2446:       }
2447:       return results;
2448:     },
2449: 

2439行目からはattr()ハンドラです。2274行目の正規表現が複雑ですが、[attr="value"] や [attr!="value"] などの文字列にマッチして、属性名、属性値、間の演算子がハンドラに渡されます。

まず、nodesが空の場合はrootから下の要素すべてをnodesに格納しておきます。

渡された演算子によって、処理を変えるために2581行目からのSelector.operatorsから関数オブジェクトを取得します。

あとはすべてのノードに対して、渡された属性が存在すれば値を関数オブジェクトに渡します。これらのhandler関数からはマッチするかどうかが真偽値で返されるので、それに応じて返り値用のresultsに溜めます。最後にresultsを返して終了です。

2450:     pseudo: function(nodes, name, value, root, combinator) {
2451:       if (nodes && combinator) nodes = this[combinator](nodes);
2452:       if (!nodes) nodes = root.getElementsByTagName("*");
2453:       return Selector.pseudos[name](nodes, value, root);
2454:     }
2455:   },
2456: 

2450行目からはpseudo()ハンドラです。CSS3で定義されたfirst-of-typeなどを含めた擬似クラスへの対応です。

事前にcombinatorが使われていた場合は、nodesをそれで絞り込みます。ここでのthisはSelector.handlersとなっています。nodesが空の場合はrootから下の要素をすべて取得しておきます。

あとは、Selector.pseudosオブジェクトにそれぞれの擬似クラスに対応したハンドラが定義されているので、それに渡して返り値をそのまま返します。

2457:   pseudos: {
2458:     'first-child': function(nodes, value, root) {
2459:       for (var i = 0, results = [], node; node = nodes[i]; i++) {
2460:         if (Selector.handlers.previousElementSibling(node)) continue;
2461:           results.push(node);
2462:       }
2463:       return results;
2464:     },

2457行目からは、先ほどのSelector.handlers.pseudo()から呼び出されるSelector.pseudosオブジェクトです。

2458行目からはfirst-childです。この擬似クラス自体はCSS2から存在しますが、このあたりの擬似クラスなどのセレクタはWin IEでの対応状況が悪いので、実際のCSSとしては使わないことが多いでしょう。一方Prototypeライブラリ内では自前で処理を行うので、互換性を気にすることなく使うことができます。

'first-child'は兄弟中の先頭の要素を示すので、渡されたnodes配列をループして、Selector.handlers.previousElementSibling()が偽を返すものをまとめて返しています。

2465:     'last-child': function(nodes, value, root) {
2466:       for (var i = 0, results = [], node; node = nodes[i]; i++) {
2467:         if (Selector.handlers.nextElementSibling(node)) continue;
2468:           results.push(node);
2469:       }
2470:       return results;
2471:     },

2465行目からはlast-childです。こちらはCSS3から登場です。

first-childと同じようにループして、Selector.handlers.previousElementSibling()が偽のものだけを集めて返しています。

2472:     'only-child': function(nodes, value, root) {
2473:       var h = Selector.handlers;
2474:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2475:         if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
2476:           results.push(node);
2477:       return results;
2478:     },

2472行目からはCSS3のonly-child 擬似クラスです。自分が他に兄弟がいない(親の子供が自分しかいない)要素にマッチします。

first-childなどと同様にnodes配列をループさせ、Selector.handlers.previousElementSibling()とSelector.handlers.nextElementSibling()の両方が偽になる(すなわち前にも後ろにも兄弟がいない)ノードを集めて返しています。

2479:     'nth-child':        function(nodes, formula, root) {
2480:       return Selector.pseudos.nth(nodes, formula, root);
2481:     },

2479行目はnth-child擬似クラスです。nth-child(1) や nth-child(2n+1) のように使います。

実際の処理はSelector.pseudos.nth()関数に任せています。

2482:     'nth-last-child':   function(nodes, formula, root) {
2483:       return Selector.pseudos.nth(nodes, formula, root, true);
2484:     },
2485:     'nth-of-type':      function(nodes, formula, root) {
2486:       return Selector.pseudos.nth(nodes, formula, root, false, true);
2487:     },
2488:     'nth-last-of-type': function(nodes, formula, root) {
2489:       return Selector.pseudos.nth(nodes, formula, root, true, true);
2490:     },
2491:     'first-of-type':    function(nodes, formula, root) {
2492:       return Selector.pseudos.nth(nodes, "1", root, false, true);
2493:     },
2494:     'last-of-type':     function(nodes, formula, root) {
2495:       return Selector.pseudos.nth(nodes, "1", root, true, true);
2496:     },
2497:     'only-of-type':     function(nodes, formula, root) {
2498:       var p = Selector.pseudos;
2499:       return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
2500:     },
2501: 

nth-child, nth-last-child, nth-of-type, nth-last-of-type, first-of-type, last-of-type, only-of-type はすべてCSS3で追加予定のものです。

nth-child(n)は指定された要素が親から見てn番目の子供である要素にマッチします(n==1が先頭,以下同様⁠⁠。

nth-last-child(n)は親から見て子供たちのうち後ろから数えてn番目の要素にマッチします。

nth-of-type(n)は親から見て、子供のうち同名要素のn番目の要素にマッチします。

nth-last-of-type(n)はその逆で、同名要素のうち後ろから数えてn番目の要素にマッチします。

first-of-type(n)はnth-of-type(1)と同じです。

last-of-type(n)はnth-last-of-type(1)と同じです。

only-of-typeは同名要素が兄弟で自分しかいないような要素にマッチします。

これらのハンドラはすべて後述するSelector.pseudos.nth()を呼び出すようになっています。

2502:     // handles the an+b logic
2503:     getIndices: function(a, b, total) {
2504:       if (a == 0) return b > 0 ? [b] : [];
2505:       return $R(1, total).inject([], function(memo, i) {
2506:         if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
2507:         return memo;
2508:       });
2509:     },
2510: 

次のnth()から使われるSelector.pseudos.getIndices()関数です。

先のCSS3セレクタは、an+b という式を受け付けるようになっています。例えばテーブル行に nth-child(2n+1) を適用すると奇数の行のみが対象になります。

この関数に渡されるのは an+b のa, bの部分、および対象となるノード配列の長さtotalです。返り値はnが変化した時のインデックス値の集合で、配列で返します。

aが0の場合はnがいくつでも答えはbなので、[b] のみを返し、bが0以下なら対象ノードが存在しないのでから配列を返します。

そうでなければ、$R(1, total) で対象ノード配列のインデックスすべてを含む配列をつくり、それを inject([]) に渡して返す配列を積み上げます。ループ処理時の2506行目の式は、an + b = i とすると n = (i - b) / a なので、剰余 (i - b) % a が0かつ商が0以上の時に返り値用のmemoにpush()する、となっています(したがってbを大きくしすぎて商が負になってしまうインデックスは答えに含まれません⁠⁠。

2511:     // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
2512:     nth: function(nodes, formula, root, reverse, ofType) {
2513:       if (nodes.length == 0) return [];
2514:       if (formula == 'even') formula = '2n+0';
2515:       if (formula == 'odd')  formula = '2n+1';
2516:       var h = Selector.handlers, results = [], indexed = [], m;
2517:       h.mark(nodes);
2518:       for (var i = 0, node; node = nodes[i]; i++) {
2519:         if (!node.parentNode._counted) {
2520:           h.index(node.parentNode, reverse, ofType);
2521:           indexed.push(node.parentNode);
2522:         }
2523:       }
2524:       if (formula.match(/^\d+$/)) { // just a number
2525:         formula = Number(formula);
2526:         for (var i = 0, node; node = nodes[i]; i++)
2527:           if (node.nodeIndex == formula) results.push(node);
2528:       } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
2529:         if (m[1] == "-") m[1] = -1;
2530:         var a = m[1] ? Number(m[1]) : 1;
2531:         var b = m[2] ? Number(m[2]) : 0;
2532:         var indices = Selector.pseudos.getIndices(a, b, nodes.length);
2533:         for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
2534:           for (var j = 0; j < l; j++)
2535:             if (node.nodeIndex == indices[j]) results.push(node);
2536:         }
2537:       }
2538:       h.unmark(nodes);
2539:       h.unmark(indexed);
2540:       return results;
2541:     },
2542: 

いくつかのCSSセレクタの具体的な処理を行うSelector.pseudos.nth()関数です。

nodes, rootは他の関数と同様、formulaにはセレクタの引数が文字列で入ります。reverse, ofTypeはbooleanで、nth()が呼び出される際に前から数えるかどうか、子供要素全体なのか同名要素のみを対称にするのか、が指定されます。

まず2513行目で、nodes配列が空なら返す要素も無いので空配列を返します。

CSSセレクタの擬似クラスの引数には、an+b形式だけでなく、偶数奇数を表すeven, oddを指定することもできます。それらをan+b形式に変換しているのが2514、2515 行目です。

2517行目で、nodes配列に入っているノードすべてに一旦_countedフラグをtrueにします。その上で、nodes配列内のすべての要素に対して、自分が兄弟中で何番目かをnodeIndexプロパティに記録するためにh.index()を呼び出します。h.index()呼び出しの外側にparentNode._countedのチェックが入っていますが、これだとちょっとうまくいかない場合があるので、この if 文は無いほうがよさそうです#10010⁠。

h.index()の中で、渡したparentNodeの_countedプロパティもtrueにしているので、それを忘れずに元に戻すためにindexed 配列にpush()しています。

an+bが単なる整数なら話が単純です。すべてのnodes配列をチェックして、インデックス番号がformulaで与えられた整数と一致しているかを確認します。一致しているものを返り値用のresultsにpush()します。

formulaがan+b形式なら、正規表現で構成要素を取り出し、先ほどのgetIndices()でnodes中のどのインデックス番号が対象となるのかを取得し、それに合った物をresultsにpush()します。

最後に、nodes, indexedに入ったノードの_countedプロパティをundefinedでクリアし、resultsを返しています。

2543:     'empty': function(nodes, value, root) {
2544:       for (var i = 0, results = [], node; node = nodes[i]; i++) {
2545:         // IE treats comments as element nodes
2546:         if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
2547:         results.push(node);
2548:       }
2549:       return results;
2550:     },
2551: 

2543行目からはemptyです。子供が空であるような要素(といいつつ仕様上は長さゼロのテキストノードも対象になるようですが)にマッチします。

実装は、すべてのnodes配列をループして、

  • コメントノード
  • firstChildプロパティが真を返し、innerHTMLに空白以外を含んでいる場合

の場合はスキップし、それ以外をresultsにpush()しています。

これでemptyに対応するノードだけがresultsに集まるので、最後にそれを返します。

2552:     'not': function(nodes, selector, root) {
2553:       var h = Selector.handlers, selectorType, m;
2554:       var exclusions = new Selector(selector).findElements(root);
2555:       h.mark(exclusions);
2556:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2557:         if (!node._counted) results.push(node);
2558:       h.unmark(exclusions);
2559:       return results;
2560:     },
2561: 

2552行目からはnotです。これもCSS3 Selectorsで定義されています。:not(:visited)など引数に他のセレクタを入れて否定します。

実装としては再帰的処理になります。まず new Selector(selector).findElements(root) で引数部分のセレクタで対象となるノードの配列を集めます。そしてそれらのノードにmark()で_countedフラグを付けておきます。

次に、今対象としているノード配列全体をループでたどり、_countedが偽のものだけをresultsに集めます。集まったものがnot()に対応するノード配列となります。

最後に_countedプロパティをクリアして終了です。

2562:     'enabled': function(nodes, value, root) {
2563:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2564:         if (!node.disabled) results.push(node);
2565:       return results;
2566:     },
2567: 

2562行目からはenabled擬似クラスです。これもCSS3 Selectorsからです。

ノード全体をループし、disabledプロパティが偽のもののみを集めて返しています。

2568:     'disabled': function(nodes, value, root) {
2569:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2570:         if (node.disabled) results.push(node);
2571:       return results;
2572:     },
2573: 

disabled はenabledの逆です。nodes配列をループして、disabledプロパティが真のノードを集めて返します。

2574:     'checked': function(nodes, value, root) {
2575:       for (var i = 0, results = [], node; node = nodes[i]; i++)
2576:         if (node.checked) results.push(node);
2577:       return results;
2578:     }
2579:   },
2580: 

checked擬似クラスもCSS3 Selectorsからです。

disabledと同様に、checkedプロパティが真のもののみを集めて返します。

2581:   operators: {
2582:     '=':  function(nv, v) { return nv == v; },
2583:     '!=': function(nv, v) { return nv != v; },
2584:     '^=': function(nv, v) { return nv.startsWith(v); },
2585:     '$=': function(nv, v) { return nv.endsWith(v); },
2586:     '*=': function(nv, v) { return nv.include(v); },
2587:     '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
2588:     '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
2589:   },
2590: 

Selector.handlers.attr()で使われるSelector.operatorsオブジェクトです。CSSセレクタでの [attr!='value'] というような文字列を分割したものの演算子部分に対応し、nvに"attr"部分が、vに"value"部分が渡されて、booleanで返します。

演算子がキーとなり、それに対応する処理(関数オブジェクト)が値となっています。'=', '~=', '|=' が CSS 2.1 で、'^=', '$=', '*=' がCSS3 Selectorsで定義されています。'!='は意味はわかりやすいのですが、出典が不明です。

関数内部はシンプルなものばかりで、条件に沿った結果を返しています。

2591:   matchElements: function(elements, expression) {
2592:     var matches = new Selector(expression).findElements(), h = Selector.handlers;
2593:     h.mark(matches);
2594:     for (var i = 0, results = [], element; element = elements[i]; i++)
2595:       if (element._counted) results.push(element);
2596:     h.unmark(matches);
2597:     return results;
2598:   },
2599: 

2591行目からはmatchElements()関数です。後述のSelector.findElement()から呼ばれます。

まずSelector.prototype.findElements()を使って、DOMツリーのルートノードから expression 式にマッチする要素をすべて列挙します。

それにmark()で_countedにフラグを付け、今度はこの関数に渡されたelementsをすべてループし、_countedが真のもの(すなわち全DOMツリーの中でexpression式にマッチしたもの)を見つけるとresultsにpush()します。

最後にunmark() _countedをクリアして、resultsに溜まったものを返します。

2600:   findElement: function(elements, expression, index) {
2601:     if (typeof expression == 'number') {
2602:       index = expression; expression = false;
2603:     }
2604:     return Selector.matchElements(elements, expression || '*')[index || 0];
2605:   },
2606: 

2600行目からはSelector.findElement()です。Element.Methodsのup(), down(), previous(), next()から呼ばれています。

渡されたelementsのうち、expressionにマッチしたもののインデックスがindexの要素を返します。

引数expressionは省略可能で、そのときの処理を2601~2603行目で行っています。

あとはSelector.matchElements()を呼び出して、返り値の配列から指定された要素を返しています。

2607:   findChildElements: function(element, expressions) {
2608:     var exprs = expressions.join(','), expressions = [];
2609:     exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
2610:       expressions.push(m[1].strip());
2611:     });
2612:     var results = [], h = Selector.handlers;
2613:     for (var i = 0, l = expressions.length, selector; i < l; i++) {
2614:       selector = new Selector(expressions[i].strip());
2615:       h.concat(results, selector.findElements(element));
2616:     }
2617:     return (l > 1) ? h.unique(results) : results;
2618:   }
2619: });
2620: 

2607行目からはSelector.findChildElements()です。Element.Methods.getElementsBySelector()と$$()から呼び出されています。

expressionsで渡された複数のCSSセレクタ式は、OR条件で評価されます。また、単一の文字列の中にカンマでセレクタ式を区切っても同じ結果が得られます。まずはそれらを "," で結合し、"," で分割することにより、一セレクタ式ごとに配列の一要素になる状態へと正規化します。

その各々のセレクタ式に対して、new Selector()してfindElements()した結果をresultsに集めます。

あとはSelector.handlers.unique()を使って重複を消して配列として返しています。

おすすめ記事

記事・ニュース一覧