1:/**
2: * JavaScript implementation of CSS3 selectors
3: * @see http://www.w3.org/TR/css3-selectors/
4: *
5: * @author Jakub Roztocil <jakub@roztocil.name>
6: * @licence Creative Commons Attribution 3.0 Unported <http://creativecommons.org/licenses/by/3.0/>
7: *
8: * Tested browsers:
9: * - Firefox
10: * - Opera
11: * - Konqueror
12: *
13: * Usage:
14: *
15: * var divs = Selectors.matchAll('table[summary^="Price List"] ~ div > div');
16: * var scriptsInBody = Selectors.matchAll('script', document.body);
17: *
18: * $Id: Selectors.js 506 2008-03-09 18:58:50Z jakub $
19: * :mode=javascript:encoding=utf-8::folding=explicit:collapseFolds=1:
20: */
21:
22://{{{ Selectors{}
23:
24:var Selectors = {
25:
26: WHITESPACE: /\s/,
27: dumpResult: false,
28:
29: //{{{ public matchAll()
30: /**
31: * @param String selectorsGroupString
32: * @return Array<Element>
33: */
34: matchAll: function(selectorsGroupString, root) {
35: //try {
36: var elements = [];
37: var selectors = Selectors.parse(selectorsGroupString);
38: for (var i = 0; i < selectors.length; i++) {
39: elements = elements.concat(selectors[i].findElements(root || document));
40: }
41: if (Selectors.dumpResult) {
42: Selectors.dump(selectorsGroupString, selectors, elements);
43: }
44: //if (selectors.length > 1) {
45: elements = Selectors.unique(elements);
46: //}
47: return elements;
48: //} catch (E) {
49: //alert(E);
50: return [];
51: //}
52: },
53: //}}}
54:
55: //{{{ parse()
56: /**
57: * @param String selectorsGroupString - one or more coma separated selectors
58: * @return Array<Selectors.Selector>
59: */
60: parse: function(selectorsGroupString) {
61:
62: selectorsGroupString = selectorsGroupString.replace(/^\s+/, '').replace(/\s+$/, '');
63:
64: var ch, // current character
65: pos = 0, // current position
66: eatedWhitespaces = 0, // position of next non-white-space character
67: len = selectorsGroupString.length,
68: selectors = [],
69: selector = new Selectors.Selector(), // Selector.SimpleSelector, Selectors.Combinator, ...
70: ss = new Selectors.SimpleSelector(),
71: openQuote = null,
72: negation = null,
73: escaped = false,
74: UNICODE = /^[0-9a-f]{1,6}/i;
75:
76: selectors.push(selector);
77:
78: var states = {
79:
80: type: true,
81: clazz: false,
82: id: false,
83: attr: false,
84: attrName: false,
85: attrValue: false,
86: pc: false, // pseudo-class
87: pcName: false, // pseudo-class name
88: pcArg: false, // pseudo-class argument
89: not: false,
90:
91: allFalse: function() {
92: for (var i in this) {
93: if (i != 'allFalse' && i != 'not') {
94: this[i] = false;
95: }
96: }
97: }
98:
99: }
100:
101: for (;;) {
102:
103: if (pos > len) {
104: break;
105: }
106:
107: escaped = false;
108: ch = selectorsGroupString.charAt(pos);
109: eatedWhitespaces = Selectors.eatWhitespaces(selectorsGroupString, pos);
110:
111: //{{{ backslash
112: if (ch == '\\') {
113: ++pos;
114: escaped = true;
115: var hexaCode = selectorsGroupString.substr(pos, 6).match(UNICODE);
116: if (hexaCode != null) {
117: hexaCode = hexaCode[0];
118: ch = String.fromCharCode(parseInt(hexaCode, 16));
119: pos += hexaCode.length - 1;
120: } else {
121: ch = selectorsGroupString.charAt(pos);
122: }
123: } //}}}
124:
125: //{{{ atribute selector
126: if (states.attr) {
127:
128: //{{{ attribute selector name
129: if (states.attrName) {
130: switch (ch) {
131: case '=':
132: ss.getLastAttributeSelelector().match += ch;
133: states.attrName = false;
134: states.attrValue = true;
135: pos = eatedWhitespaces;
136: break;
137: case '~':
138: case '|':
139: case '^':
140: case '$':
141: case '*':
142: if (selectorsGroupString.charAt(pos + 1) != '=') {
143: // TODO: namespaces are not supported - throw an specific error if the char is '|'
144: throw new Error('invalid match method in attribute selector: ' + pos);
145: }
146: ss.getLastAttributeSelelector().match += ch + '=';
147: states.attrName = false;
148: states.attrValue = true;
149: pos = Selectors.eatWhitespaces(selectorsGroupString, pos + 1);
150: break;
151: case ']':
152: if (ss.getLastAttributeSelelector().name == '') {
153: throw new Error('Expected attribute name or namespace but found "' + ch + '": ' + pos);
154: }
155: ss.getLastAttributeSelelector().match = Selectors.SimpleSelector.Attribute.PRESENCE;
156: states.allFalse();
157: states.type = true;
158: if (selectorsGroupString.charAt(eatedWhitespaces) == ',') {
159: pos = eatedWhitespaces;
160: } else {
161: ++pos;
162: }
163: break;
164: default:
165: if (Selectors.WHITESPACE.test(ch)) {
166: // TODO: check if the following character is allowed
167: pos = eatedWhitespaces;
168: break;
169: }
170: ss.getLastAttributeSelelector().name += ch;
171: ++pos;
172: }
173: continue;
174: }//}}}
175:
176: //{{{ attribute selector value
177: if (states.attrValue) {
178: switch (ch) {
179: case '"':
180: case "'":
181: if (openQuote == null) {
182: if (escaped) {
183: ss.getLastAttributeSelelector().value += ch;
184: ++pos;
185: } else {
186: openQuote = ch;
187: ++pos;
188: }
189: } else if (!escaped && ((ch == '"' && openQuote == '"') || (ch == "'" && openQuote == "'"))) {
190: if (selectorsGroupString.charAt(eatedWhitespaces) != ']') {
191: throw new Error('Missing "]" after attribute selector: ' + eatedWhitespaces);
192: }
193: // TODO: check white-space or "[" after "]"
194: openQuote = null;
195: pos = eatedWhitespaces + 1;
196: states.allFalse();
197: states.type = true;
198: if (selectorsGroupString.charAt(Selectors.eatWhitespaces(selectorsGroupString, pos)) == ',') {
199: pos = Selectors.eatWhitespaces(selectorsGroupString, pos);
200: }
201: } else {
202: ss.getLastAttributeSelelector().value += ch;
203: ++pos;
204: }
205: break;
206: case ']':
207: if (!openQuote && !escaped) {
208: states.allFalse();
209: states.type = true;
210: ++pos;
211: break;
212: } else {
213: // continue to "default:"
214: }
215: default:
216: if (!openQuote && !escaped && Selectors.WHITESPACE.test(ch)) {
217: pos = eatedWhitespaces;
218: ch = selectorsGroupString.charAt(pos);
219: if (ch != ']') {
220: throw new Error('Expected "]" to terminate attribute selector but found "' + ch + '": ' + pos);
221: }
222: states.allFalse();
223: states.type = true;
224: ++pos;
225: continue;
226: }
227: ss.getLastAttributeSelelector().value += ch;
228: ++pos;
229: }
230: continue;
231: } //}}}
232:
233: } //}}}
234:
235: //{{{ pseudo-class
236: if (states.pc) {
237: //{{{ psedo-class name
238: if (states.pcName) {
239:
240: if (Selectors.WHITESPACE.test(ch)) {
241: if (selectorsGroupString.charAt(eatedWhitespaces) == ',') {
242: pos = eatedWhitespaces;
243: }
244: states.allFalse();
245: states.type = true;
246: continue;
247: }
248:
249: var pc = ss.getLastPseudoClass();
250: switch (ch) {
251: case ')':
252: case ':':
253: case '[':
254: case ',':
255: case '#':
256: case '.':
257: case Selectors.Combinator.CHILD:
258: case Selectors.Combinator.ADJACENT_SIBLING:
259: case Selectors.Combinator.GENERAL_SIBLING:
260: states.allFalse();
261: states.type = true;
262: break;
263: case '(':
264: states.pcName = false;
265: if (pc.name == 'not') {
266: if (states.not) {
267: throw new Error("Negation pseudo-classes are not allowed inside the negation pseudo-class");
268: }
269: states.allFalse();
270: states.not = true;
271: states.type = true;
272: pos = eatedWhitespaces;
273: pc.ownerSelector = ss;
274: ss = new Selectors.SimpleSelector();
275: negation = pc;
276: } else {
277: states.pcName = false;
278: states.pcArg = true;
279: pc.arg = '';
280: pos = eatedWhitespaces;
281: }
282: continue;
283: break;
284: default:
285: // TODO: validate ch
286: pc.name += ch.toLowerCase();
287: ++pos;
288: continue;
289: break;
290:
291: }
292: } //}}}
293: //{{{ pseudo-class argument
294: else if (states.pcArg) {
295: if (ch == ')') {
296: states.allFalse();
297: states.type = true;
298: ++pos;
299: } else {
300: pc.arg += ch;
301: ++pos;
302: }
303: continue;
304: } //}}}
305: }
306: //}}}
307:
308: //{{{ whitespace
309: if (Selectors.WHITESPACE.test(ch)) {
310: pos = eatedWhitespaces;
311: ch = selectorsGroupString.charAt(pos);
312: states.allFalse();
313: states.type = true;
314: if (states.not) {
315: if (ch != ')') {
316: throw new Error('Syntax error, unexpected "' + ch + '": ' + pos);
317: }
318: } else if (ch != ',') {
319: selector.add(ss);
320: switch (ch) {
321: case Selectors.Combinator.CHILD:
322: case Selectors.Combinator.ADJACENT_SIBLING:
323: case Selectors.Combinator.GENERAL_SIBLING:
324: selector.add(new Selectors.Combinator(ch));
325: pos = Selectors.eatWhitespaces(selectorsGroupString, pos);
326: break;
327: default:
328: selector.add(new Selectors.Combinator(Selectors.Combinator.DESCENDANT));
329: }
330: ss = new Selectors.SimpleSelector();
331: states.allFalse();
332: states.type = true;
333: }
334: continue;
335:
336: } //}}}
337:
338: //{{{ type
339: switch (ch) {
340: case Selectors.Combinator.CHILD:
341: case Selectors.Combinator.ADJACENT_SIBLING:
342: case Selectors.Combinator.GENERAL_SIBLING:
343: selector.add(ss);
344: selector.add(new Selectors.Combinator(ch));
345: ss = new Selectors.SimpleSelector();
346: pos = eatedWhitespaces;
347: states.allFalse();
348: states.type = true;
349: continue;
350: break;
351: case '.':
352: if (ss.type == '') {
353: ss.type = '*';
354: }
355: var attr = new Selectors.SimpleSelector.Attribute();
356: attr.name = 'class';
357: attr.value = '';
358: attr.match = Selectors.SimpleSelector.Attribute.INCLUDES;
359: ss.attributes.push(attr);
360: states.allFalse();
361: states.clazz = true;
362: break;
363: case '#':
364: if (ss.type == '') {
365: ss.type = '*';
366: }
367: var attr = new Selectors.SimpleSelector.Attribute();
368: attr.name = 'id';
369: attr.value = '';
370: attr.match = Selectors.SimpleSelector.Attribute.EQUALS;
371: ss.attributes.push(attr);
372: states.allFalse();
373: states.id = true;
374: ss.hasId = true;
375: break;
376: case '[':
377: if (ss.type == '') {
378: ss.type = '*';
379: }
380: states.allFalse();
381: states.attr = true;
382: states.attrName = true;
383: attr = new Selectors.SimpleSelector.Attribute();
384: attr.name = '';
385: ss.attributes.push(attr);
386: pos = eatedWhitespaces;
387: continue;
388: break;
389: case ':':
390: if (selectorsGroupString.charAt(pos + 1) == ':') {
391: if (states.not) {
392: throw new Error("Pseudo-elements are not allowed in the negation pseudo-class");
393: } else {
394: throw new Error('Pseudo-elements not implemented: ' + pos);
395: }
396: }
397: if (ss.type == '') {
398: ss.type = '*';
399: }
400: states.allFalse();
401: states.pc = true;
402: states.pcName = true;
403: var ps = new Selectors.SimpleSelector.PseudoClass();
404: ps.name = '';
405: ss.pseudoClasses.push(ps);
406: break;
407: case ')':
408: if (!states.not) {
409: throw new Error('Parse error: unexpected ")" at position ' + pos);
410: }
411: states.allFalse();
412: states.not = false;
413: states.type = true;
414: negation.arg = ss;
415: ss = negation.ownerSelector;
416: negation = null;
417: break;
418: case ',':
419: selector.add(ss);
420: selector = new Selectors.Selector();
421: selectors.push(selector);
422: ss = new Selectors.SimpleSelector();
423: states.allFalse();
424: states.type = true;
425: pos = eatedWhitespaces;
426: continue;
427: case '*':
428: if (!states.type || (states.type && ss.type.length > 0)) {
429: throw new Error('Misplaced universal selector: ' + pos);
430: }
431: ss.type = ch;
432: break;
433: case '|':
434: throw new Error('Namespaces not implemented: ' + pos);
435: break;
436: default:
437: if (states.clazz || states.id) {
438: ss.getLastAttributeSelelector().value += ch;
439: } else if (states.type) {
440: ss.type += ch;
441: } else {
442: throw new Error('Internal parse error');
443: }
444: } //}}}
445:
446: ++pos;
447:
448: }
449:
450: if (states.attrValue) {
451: throw new Error('Unclosed atribut selector value');
452: }
453: if (states.attr) {
454: throw new Error('Unclosed atribut selector');
455: }
456: if ((states.clazz || states.id) && ss.getLastAttributeSelelector().value.length == 0) {
457: throw new Error('Empty class or id:' + pos);
458: }
459: if (ss.type.length == 0) {
460: throw new Error('Empty selector');
461: }
462:
463: selector.add(ss);
464: return selectors;
465: }, //}}}
466:
467: //{{{ eatWhitespaces()
468: /**
469: * @param String string
470: * @param Number pos
471: * @return Number - position of next non-white space character
472: */
473: eatWhitespaces: function(string, pos) {
474: ++pos;
475: while (pos <= string.length && this.WHITESPACE.test(string.charAt(pos))) {
476: ++pos;
477: }
478: return pos;
479: }, //}}}
480:
481: //{{{ nodeListToArray()
482: /**
483: * @param NodeList nodeList
484: * @return Array<Element>
485: */
486: nodeListToArray: function(nodeList) {
487: var i, array = [], len = nodeList.length;
488: for (i = 0; i < len; i++) {
489: array[i] = nodeList[i];
490: }
491: return array;
492: }, //}}}
493:
494: //{{{ escapeRe()
495: /**
496: * @see http://simonwillison.net/2006/Jan/20/escape/
497: */
498: escapeRe: function(text) {
499: if (!arguments.callee.sRE) {
500: var specials = ['.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'];
501: arguments.callee.sRE = new RegExp('(\\' + specials.join('|\\') + ')', 'g' );
502: }
503: return text.replace(arguments.callee.sRE, '\\$1');
504: }, //}}}
505:
506: //{{{ unique()
507: /**
508: * @param Array orig
509: * @return Array
510: */
511: unique: function(orig) {
512: var i, j, uniq = [], origLenght = orig.length;
513: loopOrig:for (i = 0; i < origLenght; i++) {
514: for (j = 0; j < uniq.length; j++) {
515: if (orig[i] == uniq[j]) {
516: continue loopOrig;
517: }
518: }
519: uniq.push(orig[i]);
520: }
521: return uniq;
522: }, //}}}
523:
524: //{{{ compareTagName()
525: compareTagName: function(tagNameA, tagNameB) {
526: // TODO: xml - case
527: return tagNameA.toLowerCase() == tagNameB.toLowerCase();
528: }, //}}}
529:
530: //{{{ dump()
531: dump: function (selectorString, selectors, resultElements) {
532: function sh(t) {
533: return t.toString().replace('>', '>').replace('<', '<');
534: }
535: var html = '<h1><code>' + sh(selectorString) + '</code></h1>';
536: html += '<h2>Query</h2>';
537: html += '<ul>';
538: for (var i = 0; i < selectors.length; i++) {
539: html += '<li><strong>Selector #' + (i+1) + ' (' + selectors[i].getSpecificity() + ')</strong><ol>';
540:
541: var components = selectors[i].components;
542:
543: for (var j = 0; j < components.length; j++) {
544:
545: var component = components[j];
546:
547: html += '<li><pre>';
548: html += '\n<strong>';
549: if (component instanceof Selectors.SimpleSelector) {
550: html += 'SimpleSelector <code>"' + sh(component.type) + '"</code>';
551: var sel = true;
552: } else {
553: html += 'Combinator <code>"' + sh(component.value) + '"</code>';
554: }
555: html += '</strong>';
556:
557: if (sel) {
558: if (component.attributes.length) {
559: html += '\n\n <em>Attribute selectors:</em>';
560: }
561: for (var k = 0; k < component.attributes.length; k++) {
562: html += '\n ' + sh(component.attributes[k].toString());
563: }
564: if (component.pseudoClasses.length) {
565: html += '\n \n <em>Pseudo-classes:</em>';
566: }
567: for (var k = 0; k < component.pseudoClasses.length; k++) {
568: html += '\n ' + sh(component.pseudoClasses[k].toString());
569: }
570: }
571: sel = false;
572: html += '</pre></li>';
573:
574: }
575: html += '</ol></li>';
576: }
577: html += '</ul>';
578: html += '<h2>Result</h2>';
579: html += '<ol>';
580: var xs = new XMLSerializer();
581: for (var i = 0; i < resultElements.length; i++) {
582: var html2 = xs.serializeToString(resultElements[i]);
583: html2 = html2.match(/[^>]+>/, html2)[0];
584: html += '<li><pre>' + sh(html2) + '</pre></li>';
585: }
586: html += '</ol>';
587: var w = window.open('', 'selectorsDump');
588: w.document.write(html);
589: w.document.close();
590: w.focus();
591: } //}}}
592:
593:}
594:
595://}}}
596:
597://{{{ Selectors.Selector ()
598:
599:Selectors.Selector = function() {
600: this.components = [];
601:}
602:
603:Selectors.Selector.prototype = {
604:
605: //{{{ add()
606: /**
607: * @param Selectors.SimpleSelector|Selectors.Combinator component
608: */
609: add: function(component) {
610: if (component instanceof Selectors.Combinator) {
611: if (this.components.length == 0) {
612: throw new Error('First component in selector must be a SimpleSelector');
613: }
614: if (this.components[this.components.length - 1] instanceof Selectors.Combinator) {
615: throw new Error('Combinator must be followed by SimpleSelector');
616: }
617: } else if (component instanceof Selectors.SimpleSelector) {
618: if (this.components.length > 0 && this.components[this.components.length - 1] instanceof Selectors.SimpleSelector) {
619: throw new Error('SimpleSelector must be followed by Combinator');
620: }
621: } else {
622: throw new Error('Unknown selector componnet: ' + component);
623: }
624: this.components.push(component);
625: }, //}}}
626:
627: //{{{ findElements()
628: /**
629: * @param Document|Element root
630: * @return Array<Element>
631: */
632: findElements: function(root) {
633: var i, j, elements, tmpElements, combinator, simpleSelector;
634: simpleSelector = this.components[0];
635: elements = simpleSelector.findElements(root || document, new Selectors.Combinator(Selectors.Combinator.DESCENDANT));
636: for (i = 1; i < this.components.length; i += 2) {
637: combinator = this.components[i];
638: simpleSelector = this.components[i + 1];
639: tmpElements = [];
640: for (j = 0; j < elements.length; j++) {
641: tmpElements = tmpElements.concat(simpleSelector.findElements(elements[j], combinator));
642: }
643: elements = tmpElements;
644: }
645: return elements;
646: }, //}}}
647:
648: //{{{ getSpecificity()
649: /**
650: * @see http://www.w3.org/TR/css3-selectors/#specificity
651: */
652: getSpecificity: function() {
653: var comp, specificity2, specificity = {a: 0, b: 0, c: 0};
654: for (var i = 0; i < this.components.length; i++) {
655: comp = this.components[i];
656: if (comp instanceof Selectors.SimpleSelector) {
657: specificity2 = comp.getSpecificity();
658: specificity.a += specificity2.a;
659: specificity.b += specificity2.b;
660: specificity.c += specificity2.c;
661: }
662: }
663: return Number(String(specificity.a) + String(specificity.b) + String(specificity.c));
664: } //}}}
665:
666:} //}}}
667:
668://{{{ Selectors.SimpleSelector()
669:
670:Selectors.SimpleSelector = function() {
671: this.type = '';
672: this.attributes = [];
673: this.pseudoClasses = [];
674: this.hasId = false; // for calculating a selector's specificity
675:}
676:
677:Selectors.SimpleSelector.prototype = {
678:
679: //{{{ toString()
680: toString: function() {
681: var string = '';
682: string += '[SimpleSelector type="' + this.type;
683: string += '", ' + this.attributes.join(', ');
684: string += ', ' + this.pseudoClasses.join(', ');
685: string += ']';
686: return string;
687: }, //}}}
688:
689: //{{{ findElements()
690: /**
691: * @param Element contextNode
692: * @param Selectors.Combinator combinator
693: * @return Array<Element>
694: */
695: findElements: function(contextNode, combinator) {
696: var i;
697: var nodes, node;
698: var foundNodes = [];
699: nodes = combinator.findElements(contextNode, this.type);
700: for (i = 0; i < nodes.length; i++) {
701: node = nodes[i];
702: if (this.test(node)) {
703: foundNodes.push(node);
704: }
705: }
706: return foundNodes;
707: }, //}}}
708:
709: //{{{ test()
710: /**
711: * Tests this selector's attribute selectors and pseudo-classes
712: * against given element.
713: *
714: * @param Element element
715: * @return Boolean
716: */
717: test: function(element, checkTagName) {
718: var i;
719:
720: if (checkTagName === true && this.type != '*'
721: && !Selectors.compareTagName(element.tagName, this.type)) {
722: return false;
723: }
724:
725: for (i = 0; i < this.attributes.length; i++) {
726: if (!this.attributes[i].test(element)) {
727: return false;
728: }
729: }
730: for (i = 0; i < this.pseudoClasses.length; i++) {
731: if (!this.pseudoClasses[i].test(element)) {
732: return false;
733: }
734: }
735: return true;
736: }, //}}}
737:
738: //{{{ getSpecificity()
739: getSpecificity: function() {
740: var specificity = {a: 0, b: 0, c: 0};
741: // A
742: if (this.hasId) {
743: specificity.a = 1;
744: }
745: // B
746: var idCounted = false;
747: for (var i = 0; i < this.attributes.length; i++) {
748: if (this.attributes[i].name == 'id' && this.attributes[i].match == Selectors.SimpleSelector.Attribute.EQUALS && this.hasId && !idCounted) {
749: idCounted = true;
750: continue;
751: }
752: ++specificity.b;
753: }
754: // C
755: for (var i = 0; i < this.pseudoClasses.length; i++) {
756: if (this.pseudoClasses[i].name == 'not') {
757: var specificity2 = this.pseudoClasses[i].arg.getSpecificity();
758: specificity.a += specificity2.a;
759: specificity.b += specificity2.b;
760: specificity.c += specificity2.c;
761: } else {
762: ++specificity.b;
763: }
764: }
765: if (this.type != '*') {
766: specificity.c = 1;
767: }
768: return specificity;
769: }, //}}}
770:
771: //{{{ getLastAttributeSelelector()
772: getLastAttributeSelelector: function() {
773: return this.attributes[this.attributes.length - 1];
774: }, //}}}
775:
776: //{{{ getLastPseudoClass()
777: getLastPseudoClass: function() {
778: return this.pseudoClasses[this.pseudoClasses.length - 1];
779: } //}}}
780:
781:}
782:
783://}}}
784:
785://{{{ Selectors.SimpleSelector.Attribute()
786:
787:/**
788: * @see http://www.w3.org/TR/css3-selectors/#attribute-selectors
789: */
790:
791:Selectors.SimpleSelector.Attribute = function() {
792: this.name = null;
793: this.match = '';
794: this.value = '';
795:}
796:
797:Selectors.SimpleSelector.Attribute.PRESENCE = '';
798:Selectors.SimpleSelector.Attribute.EQUALS = '=';
799:Selectors.SimpleSelector.Attribute.INCLUDES = '~=';
800:Selectors.SimpleSelector.Attribute.DASHMATCH = '|=';
801:Selectors.SimpleSelector.Attribute.PREFIXMATCH = '^=';
802:Selectors.SimpleSelector.Attribute.SUFFIXMATCH = '$=';
803:Selectors.SimpleSelector.Attribute.SUBSTRINGMATCH = '*=';
804:
805:Selectors.SimpleSelector.Attribute.prototype = {
806:
807: //{{{ toString()
808: toString: function() {
809: return '[Attribute name="' + this.name
810: + '", match="' + this.match
811: + '", value="' + this.value + '"]';
812: }, //}}}
813:
814: //{{{ test()
815: /**
816: * @param Element element
817: * @return Boolean
818: */
819: test: function(element) {
820: var attrValue = element.getAttribute(this.name);
821: if (attrValue == null) {
822: return false;
823: }
824: switch (this.match) {
825: case Selectors.SimpleSelector.Attribute.PRESENCE:
826: return element.hasAttribute(this.name);
827: case Selectors.SimpleSelector.Attribute.EQUALS:
828: return attrValue == this.value;
829: case Selectors.SimpleSelector.Attribute.INCLUDES:
830: var tokens = attrValue.replace(/^\s+|\s+$/g, '').split(/\s+/);
831: for (var i = 0; i < tokens.length; i++) {
832: if (tokens[i] == this.value) {
833: return true;
834: }
835: }
836: return false;
837: case Selectors.SimpleSelector.Attribute.DASHMATCH:
838: return attrValue == this.value || (new RegExp('^' + Selectors.escapeRe(this.value) + '-')).test(attrValue);
839: case Selectors.SimpleSelector.Attribute.PREFIXMATCH:
840: return attrValue.indexOf(this.value) == 0;
841: case Selectors.SimpleSelector.Attribute.SUFFIXMATCH:
842: return attrValue.lastIndexOf(this.value) == attrValue.length - this.value.length;
843: case Selectors.SimpleSelector.Attribute.SUBSTRINGMATCH:
844: return attrValue.indexOf(this.value) > -1;
845: default:
846: throw new Error(this + ': Invalid match method: "' + this.match + '"');
847: }
848: } //}}}
849:
850:}
851:
852://}}}
853:
854://{{{ Selectors.SimpleSelector.PseudoClass()
855:
856:Selectors.SimpleSelector.PseudoClass = function() {
857: this.name = null;
858: this.arg = null;
859: this.isValid = null;
860:}
861:
862:Selectors.SimpleSelector.PseudoClass.prototype = {
863:
864: toString: function() {
865: return '[PseudoClass name="' + this.name + '", arg="' + (this.arg ? this.arg.toString() : this.arg) + '"]';
866: },
867:
868: /**
869: * @return Boolean - is this valid pseudo-class?
870: */
871: validate: function() {
872: if (this.isValid == null) {
873: this.isValid = this.methods[this.name.toLowerCase()] instanceof Function;
874: }
875: return this.isValid;
876: },
877:
878: /**
879: * @param Element element
880: * @return Boolean
881: */
882: test: function(element) {
883: if (!this.validate()) {
884: throw new Error('Invalid psedo-class "' + this.name + '"');
885: }
886: return this.methods[this.name.toLowerCase()].call(this, element);
887: },
888:
889: //{{{ getOrder()
890: /**
891: * @param Element element
892: * @param Boolean fromEnd - count position from the end
893: * @param Boolean sameType - if true, than elements of other types will be
894: * be ignored
895: * @return Number - position of the element in its parent
896: */
897: getOrder: function(element, fromEnd, sameType) {
898: var curNode, order = 1;
899: var dir = fromEnd ? 'nextSibling': 'previousSibling';
900: for (curNode = element[dir]; curNode != null; curNode = curNode[dir]) {
901: if (curNode.nodeType == Node.ELEMENT_NODE
902: && (!sameType || Selectors.compareTagName(element.tagName, curNode.tagName))) {
903: ++order;
904: }
905: }
906: return order;
907: }, //}}}
908:
909: //{{{ nthTest()
910: /**
911: * @param Number order
912: * @return Boolean
913: */
914: nthTest: function(order) {
915: // TODO: parse this.arg only once
916: var arg = this.arg.replace(/^\s+|\s+$/g, '');
917: var parts, a, b;
918: switch (arg) {
919: case 'n':
920: return true;
921: case 'odd':
922: return order % 2 != 0;
923: case 'even':
924: return order % 2 == 0;
925: default:
926: if (/^\d+$/.test(arg)) {
927: // :nth-child(5)
928: return order == arg;
929: }
930: if (/^\d+n$/.test(arg)) {
931: // :nth-child(5n)
932: return order % parseInt(arg) == 0;
933: }
934: parts = arg.match(/^((\d+)|-)?n([-+])(\d+)$/);
935: if (parts != null) {
936: a = parts[1];
937: b = parts[4];
938: if (a == '-') {
939: // :nth-child(-n+2)
940: return order <= b;
941: }
942: if (a == '') {
943: // :nth-child(n+2)
944: a = 1;
945: }
946: if (parts[3] == '-') {
947: // :nth-child(an-2)
948: b = a - b;
949: }
950: return order >= b && ((order - b) % a == 0);
951: } else {
952: throw new Error('Invalid argument for ":' + this.name
953: + '" pseudo-class: "' + this.arg + '"');
954: }
955: }
956:
957: return false;
958: }, //}}}
959:
960: /**
961: * @param Element element
962: * @retrun Boolean
963: */
964: isRoot: function(element) {
965: return element == element.ownerDocument.documentElement;
966: },
967:
968: methods: {
969:
970: //{{{ 6.6.1. Dynamic pseudo-classes
971:
972: 'link': function(element) {
973: return false; // Error(':' + this.name + " not implemented yet")
974: },
975:
976: 'visited': function(element) {
977: return false; // Error(':' + this.name + " not implemented yet")
978: },
979:
980: 'hover': function(element) {
981: return false; // Error(':' + this.name + " not implemented yet")
982: },
983:
984: 'active': function(element) {
985: return false; // Error(':' + this.name + " not implemented yet")
986: },
987:
988: 'focus': function(element) {
989: return false; // Error(':' + this.name + " not implemented yet")
990: },
991:
992: //}}}
993:
994: //{{{ 6.6.2. The target pseudo-class :target
995:
996: 'target': function(element) {
997: var hash;
998: if (location.hash) {
999: hash = location.hash.substr(1);
1000: if (element.getAttribute('id') == hash || element.getAttribute('name') == hash) {
1001: return true;
1002: }
1003: }
1004: return false;
1005: },
1006:
1007: //}}}
1008:
1009: //{{{ 6.6.3. The language pseudo-class :lang
1010:
1011: 'lang': function(element) {
1012: // TODO: @xml:lang
1013: for (var parent = element; parent != null && parent != document; parent = parent.parentNode) {
1014: if (parent.hasAttribute('lang')) {
1015: if (parent.getAttribute('lang') == this.arg || (new RegExp('^' + Selectors.escapeRe(this.arg) + '-', 'i')).test(parent.getAttribute('lang'))) {
1016: return true;
1017: }
1018: return false;
1019: }
1020: }
1021: return false;
1022: },
1023:
1024: //}}}
1025:
1026: //{{{ 6.6.4. The UI element states pseudo-classes
1027:
1028: 'enabled': function(element) {
1029: return element.disabled === false;
1030: },
1031:
1032: 'disabled': function(element) {
1033: return element.disabled === true;
1034: },
1035:
1036: 'checked': function(element) {
1037: return element.checked === true;
1038: },
1039:
1040: 'indeterminate': function(element) {
1041: return false; // Error('Pseudo-class :' + this.name + " not implemented")
1042: },
1043:
1044: //}}}
1045:
1046: //{{{ 6.6.5. Structural pseudo-classes
1047:
1048: 'root': function(element) {
1049: return this.isRoot(element);
1050: },
1051:
1052: 'nth-child': function(element) {
1053: return !this.isRoot(element) && this.nthTest(this.getOrder(element, false, false));
1054: },
1055:
1056: 'nth-last-child': function(element) {
1057: return !this.isRoot(element) && this.nthTest(this.getOrder(element, true, false));
1058: },
1059:
1060: 'nth-of-type': function(element) {
1061: return !this.isRoot(element) && this.nthTest(this.getOrder(element, false, true));
1062: },
1063:
1064: 'nth-last-of-type': function(element) {
1065: return !this.isRoot(element) && this.nthTest(this.getOrder(element, true, true));
1066: },
1067:
1068: 'first-child': function(element) {
1069: if (this.isRoot(element)) {
1070: return false;
1071: }
1072: for (var curNode = element.previousSibling; curNode != null; curNode = curNode.previousSibling) {
1073: if (curNode.nodeType == Node.ELEMENT_NODE) {
1074: return false;
1075: }
1076: }
1077: return true;
1078: },
1079:
1080: 'last-child': function(element) {
1081: if (this.isRoot(element)) {
1082: return false;
1083: }
1084: for (var curNode = element.nextSibling; curNode != null; curNode = curNode.nextSibling) {
1085: if (curNode.nodeType == Node.ELEMENT_NODE) {
1086: return false;
1087: }
1088: }
1089: return true;
1090: },
1091:
1092: 'first-of-type': function(element) {
1093: if (this.isRoot(element)) {
1094: return false;
1095: }
1096: for (var curNode = element.previousSibling; curNode != null; curNode = curNode.previousSibling) {
1097: if (curNode.nodeType == Node.ELEMENT_NODE && Selectors.compareTagName(curNode.tagName, element.tagName)) {
1098: return false;
1099: }
1100: }
1101: return true;
1102: },
1103:
1104: 'last-of-type': function(element) {
1105: if (this.isRoot(element)) {
1106: return false;
1107: }
1108: for (var curNode = element.nextSibling; curNode != null; curNode = curNode.nextSibling) {
1109: if (curNode.nodeType == Node.ELEMENT_NODE && Selectors.compareTagName(curNode.tagName, element.tagName)) {
1110: return false;
1111: }
1112: }
1113: return true;
1114: },
1115:
1116: 'only-child': function(element) {
1117: if (this.isRoot(element)) {
1118: return false;
1119: }
1120: for (var curNode = element.parentNode.firstChild; curNode != null; curNode = curNode.nextSibling) {
1121: if (curNode.nodeType == Node.ELEMENT_NODE && curNode != element) {
1122: return false;
1123: }
1124: }
1125: return true;
1126: },
1127:
1128: 'only-of-type': function(element) {
1129: if (this.isRoot(element)) {
1130: return false;
1131: }
1132: for (var curNode = element.parentNode.firstChild; curNode != null; curNode = curNode.nextSibling) {
1133: if (curNode.nodeType == Node.ELEMENT_NODE && Selectors.compareTagName(curNode.tagName, element.tagName) && curNode != element) {
1134: return false;
1135: }
1136: }
1137: return true;
1138: },
1139:
1140: 'empty': function(element) {
1141: return !element.hasChildNodes();
1142: },
1143:
1144: //}}}
1145:
1146: //{{{ 6.6.7. The negation pseudo-class
1147:
1148: 'not': function(element) {
1149: if (this.arg instanceof Selectors.SimpleSelector) {
1150: return !this.arg.test(element, true);
1151: }
1152: return false; // Error("Missing argument for :not()")
1153: },
1154:
1155: //}}}
1156:
1157: //{{{ Compatibility for CSS 1 and CSS 2 one-colon pseudo-elements notation
1158: 'first-line': function(element) {return false; /* Error('Pseudo-elements not implemented') */},
1159: 'first-letter': function(element) {return false; /* Error('Pseudo-elements not implemented') */},
1160: 'before': function(element) {return false; /* Error('Pseudo-elements not implemented') */},
1161: 'after': function(element) {return false; /* Error('Pseudo-elements not implemented') */}
1162: //}}}
1163:
1164: }
1165:}
1166:
1167://}}}
1168:
1169://{{{ Selectors.Combinator()
1170:
1171:/**
1172: * @see http://www.w3.org/TR/css3-selectors/#combinators
1173: */
1174:
1175:Selectors.Combinator = function(value) {
1176: this.value = value;
1177:}
1178:
1179:Selectors.Combinator.ADJACENT_SIBLING = '+';
1180:Selectors.Combinator.CHILD = '>';
1181:Selectors.Combinator.DESCENDANT = ' ';
1182:Selectors.Combinator.GENERAL_SIBLING = '~';
1183:
1184:Selectors.Combinator.prototype = {
1185:
1186: //{{{ toString()
1187: toString: function() {
1188: return '[Combinator "' + this.value + '"]';
1189: }, //}}}
1190:
1191: //{{{ findElements()
1192: /**
1193: * @param Element contextElement
1194: * @param String tagName
1195: * @return Array<Element>
1196: */
1197: findElements: function(contextElement, tagName) {
1198: var elements = [];
1199:
1200: switch (this.value) {
1201:
1202: case Selectors.Combinator.DESCENDANT:
1203: elements = Selectors.nodeListToArray(contextElement.getElementsByTagName(tagName));
1204: break;
1205:
1206: case Selectors.Combinator.CHILD:
1207: for (var curNode = contextElement.firstChild; curNode != null; curNode = curNode.nextSibling) {
1208: if (curNode.nodeType == Node.ELEMENT_NODE && (tagName == '*' || Selectors.compareTagName(curNode.nodeName, tagName))) {
1209: elements.push(curNode);
1210: }
1211: }
1212: break;
1213:
1214: case Selectors.Combinator.ADJACENT_SIBLING:
1215: for (var curNode = contextElement.nextSibling; curNode != null; curNode = curNode.nextSibling) {
1216: if (curNode.nodeType == Node.ELEMENT_NODE) {
1217: if (tagName == '*' || Selectors.compareTagName(curNode.tagName, tagName)) {
1218: elements.push(curNode);
1219: }
1220: break;
1221: }
1222: }
1223: break;
1224:
1225: case Selectors.Combinator.GENERAL_SIBLING:
1226: for (var curNode = contextElement.nextSibling; curNode != null; curNode = curNode.nextSibling) {
1227: if (curNode.nodeType == Node.ELEMENT_NODE) {
1228: if (tagName == '*' || Selectors.compareTagName(curNode.tagName, tagName)) {
1229: elements.push(curNode);
1230: }
1231: }
1232: }
1233: break;
1234:
1235: }
1236: return elements;
1237: } //}}}
1238:}
1239:
1240://}}}
1241:
1242: