String.prototype.toXPath = function() {
  var rule = this, index = 1, parts = ["//", "*"], lastRule, subRule, m, mm;
  var self = arguments.callee, originalRule = rule;
  if (!self.cache) self.cache = {};
  
  // see if we've done this conversion already
  if (self.cache[originalRule])
    return self.cache[originalRule];

  // Regular expressions for all constructs  
  var reg = {
    // tag name/id/class
    element:    /^([#.]?)([a-z0-9\\*_-]*)((\|)([a-z0-9\\*_-]*))?/i, 
    // attribute presence
    attr1:      /^\[([^\]]*)\]/,
    // attribute value match
    attr2:      /^\[\s*([^~=\s]+)\s*(~?=)\s*"([^"]+)"\s*\]/i,
    attrN:      /^:not\((.*?)\)/i,
    pseudo:     /^:([()a-z_-]+)/i,                                  
    // adjacency/direct descendance
    combinator: /^(\s*[>+\s])?/i,                                   
    // rule separator
    comma:      /^\s*,/i                                            
  };

  // Loop through each "unit" of the rule
  while (rule.length && rule != lastRule) {
    lastRule = rule;

    // Match elements
    m = reg.element.exec(rule);
    if (m) {
      switch(m[1]) {
        case "#": // ID
          parts.push("[@id='" + m[2] + "']"); 
          break;                
      case ".": // class
        parts.push("[contains(concat(' ', @class, ' '), ' " + m[2] + " ')]"); 
        break;
      default:
        parts[index] = (m[5] || m[2]);        
      }      
      rule = rule.substr(m[0].length);
    }

    // Match attribute selectors
    m = reg.attr2.exec(rule);
    if (m) {
      // negation (e.g. [input!="text"]) isn't implemented in CSS, but prototype includes it anyway:
      if (m[2] == "!=") 
        parts.push("[@" + m[1] + "!='" + m[3] + "]");
      if (m[2] == "~=") // substring attribute match
        parts.push("[contains(@" + m[1] + ", '" + m[3] + "')]");
      else              // exact match
        parts.push("[@" + m[1] + "='" + m[3] + "']");
      rule = rule.substr(m[0].length);
    } else {
      m = reg.attr1.exec(rule);
      if (m) { 
        parts.push("[@" + m[1] + "]");
        rule = rule.substr(m[0].length);
      }
    }

    // Match negation
    m = reg.attrN.exec(rule);
    if (m) {
      subRule = m[1];
      mm = reg.attr2.exec(subRule);
      if (mm) {
        if (mm[2] == "=")
          parts.push("[@" + mm[1] + "!='" + mm[3] + "]");
        if (mm[2] == "~=")
          parts.push(":not([contains(@" + mm[1] + ", '" + mm[3] + "')])");
      } else  {
        mm = reg.attr1.exec(subRule);
        if (mm) {
          parts.push(":not([@" + mm[1] + "])");
        }
      }
      rule = rule.substr(m[0].length);  
    }

    // Ignore pseudoclasses/pseudoelements
    m = reg.pseudo.exec(rule);
    while (m) {
      rule = rule.substr(m[0].length);
      m = reg.pseudo.exec(rule);
    }

    // Match combinators (> and +)
    m = reg.combinator.exec(rule);
    if (m) {
      if (m[0].length == 0) continue;
      switch (true) {
        case (m[0].indexOf(">") != -1): 
          parts.push("/"); 
          break;
        case (m[0].indexOf("+") != -1): 
          parts.push("/following-sibling::"); 
          break;
        default:
          parts.push("//");
      }      
      // new context
      index = parts.length;
      parts.push("*");
      rule = rule.substr(m[0].length);
    }

    // Match commas
    m = reg.comma.exec(rule);
    if (m) { 
      parts.push(" | ", "//", "*"); // ending one rule and beginning another
      index = parts.length - 1;
      rule = rule.substr(m[0].length);
    }
    m = null;
  }
  var xpath = parts.join('');
  self.cache[originalRule] = xpath;
  return xpath;
};


if (document.evaluate) {
  Element.addMethods({
    _getElementsByXPath: function(element, expression) {
      var results = [], node;
      if (window.opera) { expression = expression.replace(/\/\/(body|html)(.*?)\//, "/"); }
      var query = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
      for (var i = 0, len = query.snapshotLength; i < len; i++) {
        results.push(query.snapshotItem(i));
      }
      return results.uniq();
    },

    getElementsByClassName: function(element, className) {
      return (document.getElementsByClassName(className, element));
    },

    getElementsBySelector: function(element, expression) {
      return element.getElementsByXPath(expression.toXPath());
    }
  });

  document.getElementsByClassName = function(className, parentElement) {
    return (document._getElementsByXPath("//*[contains(concat(' ', @class, ' '), ' " + 
      className + " ')]", (parentElement || document)));
  };

  document._getElementsByXPath = function(expression) { 
    return document.body._getElementsByXPath(expression);
  };
  
  var $$old = $$;

  var $$ = function() {
    return $A(arguments).map(function(expression) {
      return document._getElementsByXPath(expression.toXPath());
    }).flatten();
  }; 
}

