/**
 * 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:
 * <p>    
 * <code>elements = document.getElementsBySelect('div#main p a.external')</code>
 * <p>    
 * Will return an array of all '<code>a</code>' elements with
 * '<code>external</code>' in their <code>class</code> attribute
 * that are contained inside '<code>p</code>' elements that are 
 * contained inside the '<code>div</code>' element which has
 * <code>id="main"</code>.
 * <p>
 * Supports CSS2 and CSS3 attribute selectors.
 * <p>
 * Based on: 
 * <a href="http://simon.incutio.com/archive/2003/03/25/getElementsBySelector">getElementsBySelector()</a>
 * <p>
 * Fails in Safari: '.external', because the special case "*" is not
 * supported for the getElementsByTagName() function. Use element
 * selectors (like 'a.external') whenever possible instead.
 */
function getAllChildren(e) {
    // Returns all children of element. Workaround required for IE5/Windows. Ugh.
    return e.all ? e.all : e.getElementsByTagName('*');
}

document.getElementsBySelector = function(selector) {
    // Required variables
    var bits, tagName, elements, found, foundCount, currentContextIndex;
    // Attempt to fail gracefully in lesser browsers
    if (!document.getElementsByTagName) {
        return new Array();
    }
    // Split selector in to tokens
    var tokens = selector.split(' ');
    var currentContext = new Array(document);
    for (var i = 0; i < tokens.length; i++) {
        var token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');
        if (token.indexOf('#') > -1) {
            // Token is an Id selector
            bits = token.split('#');
            tagName = bits[0];
            var id = bits[1];
            var element = document.getElementById(id);
            if (!element) {
                // Id not found, return false
                return new Array();
            }
            if (tagName && element.nodeName.toLowerCase() != tagName) {
                // Tag with that Id not found, return false
                return new Array();
            }
            // Set currentContext to contain just this element
            currentContext = new Array(element);
            continue; // Skip to next token
        }
        if (token.indexOf('.') > -1) {
            // Token contains a class selector
            bits = token.split('.');
            tagName = bits[0];
            var className = bits[1];
            if (!tagName) {
                tagName = '*';
            }
            // Get elements matching tag, filter them for class selector
            found = new Array;
            foundCount = 0;
            for (var j = 0; j < currentContext.length; j++) {
                if (tagName == '*') {
                    elements = getAllChildren(currentContext[j]);
                } else {
                    elements = currentContext[j].getElementsByTagName(tagName);
                }
                for (var k = 0; k < elements.length; k++) {
                    found[foundCount++] = elements[k];
                }
            }
            currentContext = new Array;
            currentContextIndex = 0;
            for (var m = 0; m < found.length; m++) {
                var classAttr = found[m].className ? found[m].className : found[m].getAttribute('class');
                if (classAttr) {
                    var classNames = classAttr.split(' ');
                    for (var n = 0; n < classNames.length; n++) {
                        if (className == classNames[n]) {
                            currentContext[currentContextIndex++] = found[m];
                            break;
                        }
                    }
                }
                /* Opera 6 does not support \b
                if (classAttr && classAttr.match(new RegExp('\\b'+className+'\\b'))) {
                  currentContext[currentContextIndex++] = found[k];
                } */
            }
            continue; // Skip to next token
        }
        // Code to deal with attribute selectors
        if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
            tagName = RegExp.$1;
            var attrName = RegExp.$2;
            var attrOperator = RegExp.$3;
            var attrValue = RegExp.$4;
            if (!tagName) {
                tagName = '*';
            }
            // Grab all of the tagName elements within current context
            found = new Array;
            foundCount = 0;
            for (var x = 0; x < currentContext.length; x++) {
                if (tagName == '*') {
                    elements = getAllChildren(currentContext[x]);
                } else {
                    elements = currentContext[x].getElementsByTagName(tagName);
                }
                for (var y = 0; y < elements.length; y++) {
                    found[foundCount++] = elements[y];
                }
            }
            currentContext = new Array;
            currentContextIndex = 0;
            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 = new Array;
            currentContextIndex = 0;
            for (var z = 0; z < found.length; z++) {
                if (checkFunction(found[z])) {
                    currentContext[currentContextIndex++] = found[z];
                }
            }
            // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
            continue; // Skip to next token
        }
        // If we get here, token is just an element (not a class or Id selector)
        tagName = token;
        found = new Array;
        foundCount = 0;
        for (var a = 0; a < currentContext.length; a++) {
            elements = currentContext[a].getElementsByTagName(tagName);
            for (var b = 0; b < elements.length; b++) {
                found[foundCount++] = elements[b];
            }
        }
        currentContext = found;
    }
    return currentContext;
}

/* That revolting regular expression explained 
/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
  \---/  \---/\-------------/    \-------/
    |      |         |               |
    |      |         |           The value
    |      |    ~,|,^,$,* or =
    |   Attribute 
   Tag
*/

