Prototype 1.6.0 RC1
次期バージョン1.
前回のRC0から着実にバグを修正してきているようです。Event まわりなど、
今回は巨大な Selector クラスです。これも二分割でお送りします。
Selector クラス
500行を超える大物クラスです。
CSSのセレクタに基づく要素のマッチ機能を提供します。CSS3の仕様に含まれるものも実装されていますので、
2052: /* Portions of the Selector class are derived from Jack Slocum's DomQuery,
2053: * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
2054: * license. Please see http://www.yui-ext.com/ for more information. */
2055:
2056: var Selector = Class.create();
2057:
2058: Selector.prototype = {
2059: initialize: function(expression) {
2060: this.expression = expression.strip();
2061: this.compileMatcher();
2062: },
2063:
コンストラクタです。使い方としては、
コンストラクタでは、
2064: compileMatcher: function() {
2065: // Selectors with namespaced attributes can't use the XPath version
2066: if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression))
2067: return this.compileXPathMatcher();
2068:
2069: var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
2070: c = Selector.criteria, le, p, m;
2071:
2072: if (Selector._cache[e]) {
2073: this.matcher = Selector._cache[e]; return;
2074: }
2075: this.matcher = ["this.matcher = function(root) {",
2076: "var r = root, h = Selector.handlers, c = false, n;"];
2077:
2078: while (e && le != e && (/\S/).test(e)) {
2079: le = e;
2080: for (var i in ps) {
2081: p = ps[i];
2082: if (m = e.match(p)) {
2083: this.matcher.push(typeof c[i] == 'function' ? c[i](m) :
2084: new Template(c[i]).evaluate(m));
2085: e = e.replace(m[0], '');
2086: break;
2087: }
2088: }
2089: }
2090:
2091: this.matcher.push("return h.unique(n);\n}");
2092: eval(this.matcher.join('\n'));
2093: Selector._cache[this.expression] = this.matcher;
2094: },
2095:
コンストラクタから呼び出されるcompileMatcher()です。Selectorクラス内では、
まず、
XPathが使える場合はcompileXPathMatcher()を使って事前準備を行いそのまま帰ります。この関数の2069行目以降はJavaScript版の実装部分です。
変数名が長い物が多いので、
この関数の最後で、
2075行目から、
2078行目からのwhileループは、
ループ内では、
psであるSelector.
といことで、
そして、
ここで、
whileループが終わると、
最後に、
2096: compileXPathMatcher: function() {
2097: var e = this.expression, ps = Selector.patterns,
2098: x = Selector.xpath, le, m;
2099:
2100: if (Selector._cache[e]) {
2101: this.xpath = Selector._cache[e]; return;
2102: }
2103:
2104: this.matcher = ['.//*'];
2105: while (e && le != e && (/\S/).test(e)) {
2106: le = e;
2107: for (var i in ps) {
2108: if (m = e.match(ps[i])) {
2109: this.matcher.push(typeof x[i] == 'function' ? x[i](m) :
2110: new Template(x[i]).evaluate(m));
2111: e = e.replace(m[0], '');
2112: break;
2113: }
2114: }
2115: }
2116:
2117: this.xpath = this.matcher.join('');
2118: Selector._cache[this.expression] = this.xpath;
2119: },
2120:
2096行目からはXPath版のマッチ関数を作るcompileXPathMatcher()です。
こちらではSelector.
compileMatcher()と違う所は、
2121: findElements: function(root) {
2122: root = root || document;
2123: if (this.xpath) return document._getElementsByXPath(this.xpath, root);
2124: return this.matcher(root);
2125: },
2126:
ライブラリ内でも使われているfindElements()メソッドです。
コンストラクタでthis.
そうでなければthis.
2127: match: function(element) {
2128: return this.findElements(document).include(element);
2129: },
2130:
こちらもよく使われるmatch()メソッドです。先ほどのfindElements()を呼び出し、
2131: toString: function() {
2132: return this.expression;
2133: },
2134:
toString()メソッドでは、
2135: inspect: function() {
2136: return "#<Selector:" + this.expression.inspect() + ">";
2137: }
2138: };
2139:
inspect()メソッドでは、
2140: Object.extend(Selector, {
2141: _cache: {},
2142:
2140行目からは Selectorオブジェクトに対して関数を追加しています。これらはprototype以下ではなくSelector直下なので、
2141行目の_cacheは、
2143: xpath: {
2144: descendant: "//*",
2145: child: "/*",
2146: adjacent: "/following-sibling::*[1]",
2147: laterSibling: '/following-sibling::*',
2148: tagName: function(m) {
2149: if (m[1] == '*') return '';
2150: return "[local-name()='" + m[1].toLowerCase() +
2151: "' or local-name()='" + m[1].toUpperCase() + "']";
2152: },
2153: className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
2154: id: "[@id='#{1}']",
2155: attrPresence: "[@#{1}]",
2156: attr: function(m) {
2157: m[3] = m[5] || m[6];
2158: return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
2159: },
2160: pseudo: function(m) {
2161: var h = Selector.xpath.pseudos[m[1]];
2162: if (!h) return '';
2163: if (typeof h === 'function') return h(m);
2164: return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
2165: },
2166: operators: {
2167: '=': "[@#{1}='#{3}']",
2168: '!=': "[@#{1}!='#{3}']",
2169: '^=': "[starts-with(@#{1}, '#{3}')]",
2170: '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
2171: '*=': "[contains(@#{1}, '#{3}')]",
2172: '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
2173: '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
2174: },
2175: pseudos: {
2176: 'first-child': '[not(preceding-sibling::*)]',
2177: 'last-child': '[not(following-sibling::*)]',
2178: 'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
2179: 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
2180: 'checked': "[@checked]",
2181: 'disabled': "[@disabled]",
2182: 'enabled': "[not(@disabled)]",
2183: 'not': function(m) {
2184: var e = m[6], p = Selector.patterns,
2185: x = Selector.xpath, le, m, v;
2186:
2187: var exclusion = [];
2188: while (e && le != e && (/\S/).test(e)) {
2189: le = e;
2190: for (var i in p) {
2191: if (m = e.match(p[i])) {
2192: v = typeof x[i] == 'function' ? x[i](m) : new Template(x[i]).evaluate(m);
2193: exclusion.push("(" + v.substring(1, v.length - 1) + ")");
2194: e = e.replace(m[0], '');
2195: break;
2196: }
2197: }
2198: }
2199: return "[not(" + exclusion.join(" and ") + ")]";
2200: },
2201: 'nth-child': function(m) {
2202: return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
2203: },
2204: 'nth-last-child': function(m) {
2205: return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
2206: },
2207: 'nth-of-type': function(m) {
2208: return Selector.xpath.pseudos.nth("position() ", m);
2209: },
2210: 'nth-last-of-type': function(m) {
2211: return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
2212: },
2213: 'first-of-type': function(m) {
2214: m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
2215: },
2216: 'last-of-type': function(m) {
2217: m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
2218: },
2219: 'only-of-type': function(m) {
2220: var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
2221: },
2222: nth: function(fragment, m) {
2223: var mm, formula = m[6], predicate;
2224: if (formula == 'even') formula = '2n+0';
2225: if (formula == 'odd') formula = '2n+1';
2226: if (mm = formula.match(/^(\d+)$/)) // digit only
2227: return '[' + fragment + "= " + mm[1] + ']';
2228: if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
2229: if (mm[1] == "-") mm[1] = -1;
2230: var a = mm[1] ? Number(mm[1]) : 1;
2231: var b = mm[2] ? Number(mm[2]) : 0;
2232: predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
2233: "((#{fragment} - #{b}) div #{a} >= 0)]";
2234: return new Template(predicate).evaluate({
2235: fragment: fragment, a: a, b: b });
2236: }
2237: }
2238: }
2239: },
2240:
2143行目からはSelector.
これはSelector.
descendant, child, adjacent, laterSiblingにはそれを示すXPathルールが文字列として入っています。
tagNameでは関数として定義されています。このmにはSelector.
className, id, attrPresence には文字列が入っていますが、
attrでも関数オブジェクトが指定されています。mとして渡されたマッチ結果を使って、
pseudoでは擬似セレクタに対応するために、
例えばfirst-childには'[not(preceding-sibling::*)]'という文字列が入っています。入力されたCSSセレクタが'*:first-child'だった場合、
他も同じようにXPath式を解釈すれば、
nth-child, nth-last-child, nth-of-type, nth-last-of-type, first-of-type, last-of-type, only-of-type は Selector.
これらの関数は、
そこで、
nth-childでは、
nth()関数の中では、
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.
compileMatcher()では、
出力されるJavaScriptはほぼSelector.
JavaScriptコード文字列で使われている部分において、
tagNameは2269行目の正規表現で、
classNameは2271行目の正規表現で、
idは2270行目の正規表現にマッチした場合で、
attrPresenceは2273行目の正規表現で、
attrは2274行目の正規表現で、
pseudoは2272行目の正規表現にマッチした結果です。正規表現の解説は後述しますが、
残りのdescendant, child, adjacent, laterSiblingはコンビネータに対する正規表現マッチの結果で、
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は、
ECMAの仕様によると、
また、
以下、
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