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('>', '&gt;').replace('<', '&lt;');
 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: