ユニットテスト
PrototypeライブラリをSubversionからcheckoutすると、
今回ライブラリ内に疑問点などがあったり、
assertEnumEqual([], $$('#p + #p'));
というような行を追加するだけで、ブラウザから簡単にテストが行えます。
では、
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行目からは、
concat()は、
mark(), unmark() は、
- 基準となるノード配列全体をunmark()する
- ルールに該当する要素を抽出する
- 抽出された配列をmark()する
- 必要に応じて2, 3を繰り返す
- 基準となるノード配列から_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.
ofTypeが偽の場合は、
ofTypeが真の場合はnth-of-type()系の擬似クラスであり、
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.
_countedを使う関数は使用後にクリアすることになっているので、
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()は、
CSSセレクタのパーサがdescendant combinatorにマッチするタイミングは、
そのための実装は、
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プロパティの配列をループで取得し、
IEではコメントノードのnodeTypeが1 (ELEMENT_
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"とする部分です。隣接するふたつのノードが対象ですので、
これで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()です。
原典がどこかわかりませんが、
実装は、
2359: nextElementSibling: function(node) {
2360: while (node = node.nextSibling)
2361: if (node.nodeType == 1) return node;
2362: return null;
2363: },
2364:
2359行目からはnextElementSibling()です。
これは主に他のハンドラ関数から呼ばれるヘルパ関数で、
2365: previousElementSibling: function(node) {
2366: while (node = node.previousSibling)
2367: if (node.nodeType == 1) return node;
2368: return null;
2369: },
2370:
previousElementSibling()は nextElementSibling()の逆で、
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行目からはタグ、
まずはtagName()関数です。Selector.
実装では、
そうでなければ、
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が空で、
nodesが空で、
nodesがすでにある場合
combinatorが'child'の場合、
combinatorが'descendant'の場合は、
combinatorが'adjacent'なら、
combinatorがそれ以外の場合
2408行目には (1) combinatorが指定されていないか、
(1)の場合は"div#id"のような指定なので、
(2)の場合は"div ~ #id"という形で、
(3)はcombinator 指定がある場合なので、
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.
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に集めます。
その後、
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]" という表記になります。
実装は、
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行目の正規表現が複雑ですが、
まず、
渡された演算子によって、
あとはすべてのノードに対して、
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が使われていた場合は、
あとは、
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行目からは、
2458行目からはfirst-childです。この擬似クラス自体はCSS2から存在しますが、
'first-child'は兄弟中の先頭の要素を示すので、
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と同じようにループして、
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配列をループさせ、
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.
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番目の子供である要素にマッチします
nth-last-child(n)は親から見て子供たちのうち後ろから数えてn番目の要素にマッチします。
nth-of-type(n)は親から見て、
nth-last-of-type(n)はその逆で、
first-of-type(n)はnth-of-type(1)と同じです。
last-of-type(n)はnth-last-of-type(1)と同じです。
only-of-typeは同名要素が兄弟で自分しかいないような要素にマッチします。
これらのハンドラはすべて後述するSelector.
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.
先のCSS3セレクタは、
この関数に渡されるのは an+b のa, bの部分、
aが0の場合はnがいくつでも答えは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.
nodes, rootは他の関数と同様、
まず2513行目で、
CSSセレクタの擬似クラスの引数には、
2517行目で、
h.
an+bが単なる整数なら話が単純です。すべてのnodes配列をチェックして、
formulaがan+b形式なら、
最後に、
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です。子供が空であるような要素
実装は、
- コメントノード
- firstChildプロパティが真を返し、
innerHTMLに空白以外を含んでいる場合
の場合はスキップし、
これで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プロパティをクリアして終了です。
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からです。
ノード全体をループし、
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配列をループして、
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と同様に、
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.
演算子がキーとなり、
関数内部はシンプルなものばかりで、
- CSS 2.
1 - 5. 8.1 Matching attributes and attribute values - CSS3 Selectors - 6.
3.2. Substring matching attribute 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.
まずSelector.
それにmark()で_countedにフラグを付け、
最後にunmark() _countedをクリアして、
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.
渡されたelementsのうち、
引数expressionは省略可能で、
あとはSelector.
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.
expressionsで渡された複数のCSSセレクタ式は、
その各々のセレクタ式に対して、
あとはSelector.