prototype.jsを読み解く

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

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

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.xpathオブジェクトです。

これはSelector.prototype.compileXPathMatcher()から使われます。Selector.patternsでマッチしたものに対するアクションを定義しています。Selector.patternsから使われているプロパティはlaterSibling, child, adjacent, descendant, tagName, id, className, pseudo, attrPresence, attrなので,このxpathオブジェクトのプロパティのうちそれ以外のoperators, pesudosは直接は使われません。xpathオブジェクトの中の関数から利用されます。

descendant, child, adjacent, laterSiblingにはそれを示すXPathルールが文字列として入っています。

tagNameでは関数として定義されています。このmにはSelector.patternsのマッチ文字列を使ったマッチで返されるオブジェクトが渡されるため,タグ名を示す文字列が入っている場所はm[1]となります。そこに入っているタグ名から必要なXPathルールを作成します。

className, id, attrPresence には文字列が入っていますが,ここにはTemplateクラスで用いられる #{1} という表記の文字列が含まれています。ここがマッチ文字列のグルーピングの1番目で置き換えられます。

attrでも関数オブジェクトが指定されています。mとして渡されたマッチ結果を使って,CSSの属性セレクタの演算子部分によって適用するXPathルールを分けるために,Selector.xpath.operatorsのハッシュをテーブルとして使っています。それをTemplateクラスに渡して返ってきた文字列を返します。

pseudoでは擬似セレクタに対応するために,セレクタごとに処理を分ける目的でSelector.xpath.pseudosオブジェクトを参照しています。この参照テーブルのうち,first-child, last-child, only-child, empty, checked, disabled, enabledはほぼXPathルール文字列が入っているだけですが,'not'の部分に関しては,再帰的に処理するためにcompileXPathMatcher()と似たような処理を行っています。

例えばfirst-childには'[not(preceding-sibling::*)]'という文字列が入っています。入力されたCSSセレクタが'*:first-child'だった場合,最終的に生成されるXPath式は'.//*[not(preceding-sibling::*)]'というものになります。これは,コンテキストノードの内側の任意の要素のうち,先行する兄弟要素が存在しないものにマッチします。

他も同じようにXPath式を解釈すれば,該当するCSSセレクタと同等になります。

nth-child, nth-last-child, nth-of-type, nth-last-of-type, first-of-type, last-of-type, only-of-type は Selector.xpath.pseudo.nth() を呼んでいます。

これらの関数は,2272行目の正規表現にマッチした時に実行されます。長くてわかりにくいのですが,例えば:nth-child(5)の(5)の部分は正規表現において(\((.*?)\))?の部分にあたり,引数の値5はマッチ結果としてm[6]に入ります。

そこで,first-of-typeとlast-of-typeではm[6]に1を入れた上でそれぞれnth-of-typeとnth-last-of-typeを呼び出す,という形に変換されます。

nth-childでは,"count(./preceding-sibling::*) + 1"というXPath式をnth()関数に渡します。これは結果としてnth-child(99)の場合には'[ (count(./preceding-sibling::*) + 1) = 99]'というXPath式に変換されてそれを返します。これはコンテキストノードの内側の要素のうち,選考する兄弟要素の数+1が99になる要素,すなわち99番目の要素にマッチします。

nth()関数の中では,数値の引数だけでなく'even', 'odd'や'2n+1'という形式の式もm[6]として受け取れるようになっています。その部分をパースして最終的なXPath式にしているのが2232行目とその後のnew Template行になります。

著者プロフィール

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

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