prototype.jsを読み解く

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

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

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()を使って重複を消して配列として返しています。

著者プロフィール

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

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