/* document.getElementsBySelector(selector)
   - returns an array of element objects from the current document
     matching the CSS selector. Selectors can contain element names, 
     class names and ids and can be nested. For example:
     
       elements = document.getElementsBySelect('div#main p a.external')
     
     Will return an array of all 'a' elements with 'external' in their 
     class attribute that are contained inside 'p' elements that are 
     contained inside the 'div' element which has id="main"

   New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
   See http://www.w3.org/TR/css3-selectors/#attribute-selectors

   Version 0.4 - Simon Willison, March 25th 2003
   -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
   -- Opera 7 fails 
*/


function getFirstChildElement(element)
{
  return (element.firstChild.nodeType != Node.ELEMENT_NODE ? getNextElement(element.firstChild) : element.firstChild);
}

function getPreviousElement(element)
{
  var prev = element.previousSibling;
  while (prev && prev.nodeType != Node.ELEMENT_NODE && prev.nodeType != Node.DOCUMENT_NODE)
    prev = prev.previousSibling;
  return prev;
}

function getNextElement(element)
{
  var next = element.nextSibling;
  while (next && next.nodeType != Node.ELEMENT_NODE && next.nodeType != Node.DOCUMENT_NODE)
    next = next.nextSibling;
  return next;
}

function matchSelectorType(match, prevMatch, type)
{
  if (type == 'child')
    return (match.parentNode == prevMatch);
  else if (type == 'adjacent')
    return (getPreviousElement(match) == prevMatch);
  else if (type == '')
    return true;
  else
    return false;
}

function getElementsBy(match, context, value, type, checkFunc)
{
  var found = new Array();
  var found_count = 0;
  var e;
  var parent;
  for (var i = 0; i<context.length; i++)
  {
    if (type == 'adjacent')
      parent = context[i].parentNode;
    else 
      parent = context[i];
    if (match == 'id')
    {
      e =  parent.getElementById(value);
      if (e && checkFunc(e) && matchSelectorType(e, context[i], type))
        found[found_count++] = e;
    }
    else if (match == 'tagName')
    {
      e = ((value == '*' && parent.all) ? parent.all : parent.getElementsByTagName(value));
      for (var j = 0; j < e.length; j++)
      {
        if (checkFunc(e[j]) && matchSelectorType(e[j], context[i], type))
          found[found_count++] = e[j];
      }
    }
  }
  return found;
}

function getElementsBySelector(selector) 
{
  // Attempt to fail gracefully in lesser browsers
  if (!document.getElementsByTagName)
    return new Array();
    
  // a comma indicates multiple rules, split them and combine elements
  if (selector.indexOf(',') > -1)
  {
    var elements = new Array();
    var selectors = selector.split(',');
    for (var i = 0; i < selectors.length; i++)
    {
      elements = elements.concat(getElementsBySelector(selectors[i].trim()));
    }
    return elements;
  } 
    
  // Split selector into tokens
  var tokens = selector.replace(/(>|\+|~)/gi, ' $1 ').replace(/  /gi, ' ').split(' ');
  var selectortype = '';
  var currentContext = new Array(document);
  var found = new Array();
  
  for (var i = 0; i < tokens.length; i++)
  {
    token = tokens[i].trim();
    if (token == '+') 
    {
      selectortype = 'adjacent';
      //avoid clearing of selectortype
      continue;
    }
    else if (token == '>')
    {
      selectortype = 'child';
      //avoid clearing of selectortype
      continue;
    }
    else if (token.indexOf(':') > -1)
    {
      // Token is a pseudo class
      var bits = token.split(':');
      var tagName = bits[0];
      var pseudoclass = bits[1];
      var checkfunc;
      switch (pseudoclass) 
      {
        case 'last-child':
          checkfunc = function(e) { return (!getNextElement(e)); };
          break;
        case 'first-child':
          checkfunc = function(e) { return (!getPreviousElement(e)); };
          break;
        default:
          throw 'Unknown structural pseudo class ' + pseudoclass;
      }
      currentContext = getElementsBy('tagName', currentContext, tagName, selectortype, checkfunc);
    }
    else if (token.indexOf('#') > -1)
    {
      // Token is an ID selector
      var bits = token.split('#');
      var tagName = bits[0];
      var id = bits[1];
      currentContext = getElementsBy('id', currentContext, id, selectortype, function(e) { return (!tagName || e.nodeName.toLowerCase() == tagName); });
    }
    else if (token.indexOf('.') > -1) 
    {
      // Token contains a class selector
      var bits = token.split('.');
      var tagName = bits[0];
      var className = bits[1];
      if (!tagName)
        tagName = '*';
      currentContext = getElementsBy('tagName', currentContext, tagName, selectortype, function(e) { return (e.className && e.className.match(new RegExp('\\b'+className+'\\b'))); });
    }
    else if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/))
    {
      // Code to deal with attribute selectors
      var tagName = RegExp.$1;
      var attrName = RegExp.$2;
      var attrOperator = RegExp.$3;
      var attrValue = RegExp.$4;
      if (!tagName) 
        tagName = '*';
      var checkFunction; // This function will be used to filter the elements
      switch (attrOperator) 
      {
        case '=': // Equality
          checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
          break;
        case '~': // Match one of space seperated words 
          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
          break;
        case '|': // Match start with value followed by optional hyphen
          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
          break;
        case '^': // Match starts with value
          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
          break;
        case '$': // Match ends with value - fails with "Warning" in Opera 7
          checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
          break;
        case '*': // Match ends with value
          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
          break;
        default :
          // Just test for existence of attribute
          checkFunction = function(e) { return e.getAttribute(attrName); };
      }
      currentContext = getElementsBy('tagName', currentContext, tagName, selectortype, checkFunction);
    }
    else
    {
      // If we get here, token is JUST an element (not a class or ID selector)
      currentContext = getElementsBy('tagName', currentContext, token, selectortype,  function(e) { return true; });
    }
    selectortype = '';
    if (currentContext.length == 0)
      break;
  }
  return currentContext;
}

/* That revolting regular expression explained 
/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
  \---/  \---/\-------------/    \-------/
    |      |         |               |
    |      |         |           The value
    |      |    ~,|,^,$,* or =
    |   Attribute 
   Tag
*/

function applyToSelector(selector, func)
{
  var res = new Array();
  var elements = getElementsBySelector(selector);
  for (var i = 0; i < elements.length; i++)
  {
    res[i] = func(elements[i]);
  }
  return res;
}

/* Strip whitespace from the beginning and end of a string */
if (!String.prototype.trim) 
{
  String.prototype.trim = function() 
  {
    return this.replace(/^\s*|\s*$/g, "");
  };
}
