Take the 2-minute tour ×
Programming Puzzles & Code Golf Stack Exchange is a question and answer site for programming puzzle enthusiasts and code golfers. It's 100% free, no registration required.

Create a small acceptably efficient JS CSS selector engine. Standard css 2.0 properties should be supported in Firefox 3.0+,Safari , Opera, IE7+, and extra brownie points for IE6 support.

Smallest entry wins.

Hacks are permitted!

Please post size of script, and function to call preferable $(selector,context) style.

share|improve this question
2  
Try to beat jQuery: it is 85,000 bytes. –  Peter Olson Apr 24 '11 at 15:44
4  
Sizzle, rather. –  minitech Apr 24 '11 at 19:23
    
Would it hurt to write a requirements spec, or at least link to a description of precisely what is meant by "JS CSS selector engine"? –  Peter Taylor Apr 27 '11 at 15:52
add comment

2 Answers

I'm on linux so I can't guarantee this is 100% foolproof in IE, but it has worked perfectly before.

The function is 1.43kb compressed and 743 gzipped. Call the function like $(selector,context).

It relies on querySelectorAll on all browsers that support it, and uses the browser's css rendering engine to check elements using the rarely used outline-color property and a special color that looks nearly black. That leaves 99.99999999% of colors to be still used in the web page. Basically the script supports as many selectors as the browser's css rendering engine. IE7 is nearly all CSS 2.0 selector compliant while IE6 is not so much :(. Yes this is a hack, but a working hack.

Recommendations would be great!

(function(){
    var
    rnotDigit = /\D+/g,
    attr = 'outline-color',
    attrOn = 'rgb(00,00,07)',
        rcamelCase = /-([a-z])/g,
        fcamelCase = function(a,letter) {
                return letter.toUpperCase();
        },
    curCSS,
        //From http://j.mp/FhELC
        getElementById = function(id){
            var elem = document.getElementById(id);
            if(elem){
              //verify it is a valid match!
              if(elem.attributes['id'] && elem.attributes['id'].value == id){
                //valid match!
                return elem;
              } else {
                //not a valid match!
                //the non-standard, document.all array has keys for all name'd, and id'd elements
                //start at one, because we know the first match, is wrong!
                for(var i=1;i<document.all[id].length;i++){
                  if(document.all[id][i].attributes['id'] && document.all[id][i].attributes['id'].value == id){
                    return document.all[id][i];
                  }
                }
              }
            }
            return null;
        };
    style = document.createElement('style'),
    script = document.getElementsByTagName('script')[0];
    script.parentNode.insertBefore(style, script);

        if (document.defaultView && document.defaultView.getComputedStyle) {
            curCSS = function( elem, name ) {
            return elem.ownerDocument.defaultView.getComputedStyle( elem, null ).getPropertyValue(name);    
        };

    } else if ( document.documentElement.currentStyle ) {
        curCSS = function( elem, name ) {
            return elem.currentStyle && elem.currentStyle[ name.replace(rcamelCase,fcamelCase) ];
        };
    }
    window['$']=function(selector,context,extend){
        context = context || document;
        extend = extend || [];

        var id, p = extend.length||0;

        try{style.innerHTML = selector+"{"+attr+":"+attrOn+"}";}
        //IE fix
        catch(id){style.styleSheet.cssText = selector +"{"+attr+":"+attrOn+"}";}

        if(document.defaultView && document.querySelectorAll){
            id = "";
            var _id = context.id,
                _context = context;
            if(context!=document){
                id = "__slim__";
                context.id = id;
                id = "#"+id+" ";
            }
            context=document.querySelectorAll(id + selector);
            if(_id)_context.id = _id;
            //Setting selector=1 skips checking elem
            selector=1;
        }
        else if(!context[0] && (id = /(.*)#([\w-]+)([^\s>~]*)[\s>~]*(.*)/.exec(selector)) && id[2]){
            //no selectors after id
            context =  getElementById(id[2]);
            //If there isn't a tagName after the id we know the el just needs to be checked
            if(!id[4]){
                context = [context];
                //Optimize for #id
                if(!id[1] && !id[3]){
                    selector=1;
                }
            }
        }
        //If context contains an array or nodeList of els check them otherwise retrieve new els by tagName using selector last tagName
        context = (selector == 1 || context[0] && context[0].nodeType==1) ? 
            context:                
            context.getElementsByTagName(selector.replace(/\[[^\]]+\]|\.[\w-]+|\s*$/g,'').replace(/^.*[^\w]/, '')||'*');

        for(var i=0,l=context.length; i < l; i++){
            //IE returns comments when using *
            if(context[i].nodeType==1 && (selector==1 || curCSS(context[i],attr).replace(rnotDigit,'')==7)){
            extend[p++]=context[i];
        }
    }
    extend.length = p;
    return extend;
    };
})();
share|improve this answer
1  
1.43 kb compressed and 743 kb gzipped? Might want to leave it just compressed :-) –  mellamokb Apr 29 '11 at 18:24
add comment

Funny, I just made one of these. It is quite efficient because it compiles selectors. I don't know if it's IE6-compatible but it should be, I think. It supports CSS3 too - bonus points? :)

if(![].indexOf)Array.prototype.indexOf=function(a,i){i=i||0;while(i<this.length)if(this[i++]===a)return i-1;return-1};
String.prototype.startsWith=function(s){return this.length>=s.length&&this.substring(0,s.length)===s};
String.prototype.endsWith=function(s){return this.length>=s.length&&this.substring(this.length-s.length)===s};

var Sprint = {
    Error: function(m) {
        this.message = m;
        this.toString = function() {
            return this.message;
        };
    },
    getAllElementsIn: function(element) {
        var r = [];
        var f = function recurse(l, arr) {
            from(l).each(function(k, v) {
                if(v.nodeType === 1) {
                    arr.push(v);
                    recurse(v, arr);
                }
            });
        };
        f(element, r);
        return r;
    },
    getAllElements: function() {
        return Sprint.getAllElementsIn(document.documentElement);
    },
    getSiblings: function(element) {
        return element.parentNode ? from(element.parentNode).where(function(k, v) { return v.nodeType === 1; }).toArray() : [element];
    },
    getInfo: function(selector) {
        var r = {ATTR: {}, CLASS: [], PSEUDO: []};

        var quotes = '\'"', nestable = '(){}', inAttribute = false, escaped = false, currentQuotes = null, currentNested = [], nonchar = /^[^a-zA-Z0-9_\-]$/, lastSplitter = null, i = -1, c, s = '';
        while(++i < selector.length) {
            c = selector.charAt(i);
            if(escaped) {
                escaped = false;
            } else if(c === '\\') {
                escaped = true;
                continue;
            } else if(currentQuotes) {
                if(currentQuotes === c) {
                    currentQuotes = null;
                }
            } else if(quotes.indexOf(c) > -1) {
                currentQuotes = c;
            } else if(currentNested.length) {
                if(nestable.indexOf(currentNested[currentNested.length - 1]) + 1 === nestable.indexOf(c)) {
                    currentNested.pop();
                }
            } else if(nestable.indexOf(c) > -1) {
                currentNested.push(c);
            } else if(nonchar.test(c)) {
                if(lastSplitter) {
                    switch(lastSplitter) {
                        case '#':
                            r.ID = s;
                            break;
                        case '.':
                            r.CLASS.push(s);
                            break;
                        case '[':
                        case ',':
                            if(inAttribute) {
                                var j;
                                if((j = s.indexOf('=')) > -1) {
                                    r.ATTR[s.substring(0, j)] = s.substring(j + 1);
                                } else {
                                    r.ATTR[s] = true;
                                }
                            }
                            break;
                        case ':':
                            r.PSEUDO.push(s);
                            break;
                    }
                } else {
                    r.TAG = s;
                }

                if(c === ']') {
                    inAttribute = false;
                } else if(c === '[') {
                    inAttribute = true;
                }

                lastSplitter = c;

                s = '';
                continue;
            }
            s += c;
        }

        if(lastSplitter) {
            switch(lastSplitter) {
                case '#':
                    r.ID = s;
                    break;
                case '.':
                    r.CLASS.push(s);
                    break;
                case '[':
                case ',':
                    if(inAttribute) {
                        var j;
                        if((j = s.indexOf('=')) > -1) {
                            r.ATTR[s.substring(0, j)] = s.substring(j + 1);
                        } else {
                            r.ATTR[s] = true;
                        }
                    }
                    break;
                case ':':
                    r.PSEUDO.push(s);
                    break;
            }
        } else {
            r.TAG = s;
        }

        return r;
    },
    compileSingle: function(info) {
        var f = 'var t=l.nodeName?l.nodeName.toLowerCase():"",i=l.id?l.id.toLowerCase():"",c=l.className?" "+l.className.toLowerCase()+" ":"",s=Sprint.getSiblings(l),ss=from(s).where(function(k,v){return v.nodeName?v.nodeName.toLowerCase()===t:false;}).toArray(),si=s.indexOf(l),ssi=ss.indexOf(l);return ';
        if(info.TAG && info.TAG !== '*') {
            f += 't===\'' + info.TAG.toLowerCase().replace(/[\\']/g, function(x) { return '\\' + x; }) + '\'&&';
        }
        if(info.ID) {
            f += 'i===\'' + info.ID.toLowerCase().replace(/[\\']/g, function(x) { return '\\' + x; }) + '\'&&';
        }
        if(info.CLASS) {
            for(var i = 0; i < info.CLASS.length; i++) {
                f += 'c.indexOf(\' ' + info.CLASS[i].toLowerCase() + ' \')>-1&&';
            }
        }
        if(info.PSEUDO) {
            for(var i = 0; i < info.PSEUDO.length; i++) {
                var p = info.PSEUDO[i], j, v = '';
                if((j = p.indexOf('(')) > -1) {
                    v = p.substring(j + 1, p.length - 1);
                    p = p.substring(0, j);
                }
                switch(p) {
                    case 'first-child':
                        f += '!si';
                        break;
                    case 'first-of-type':
                        f += '!ssi';
                        break;
                    case 'nth-child':
                        // TODO: Implement all nth-*** selectors.
                        break;
                    case 'nth-of-type':

                        break;
                    case 'last-child':
                        f += 'si===s.length-1';
                        break;
                    case 'last-of-type':
                        f += 'ssi===ss.length-1';
                        break;
                    case 'nth-last-child':

                        break;
                    case 'nth-last-of-type':

                        break;
                    case 'only-child':
                        f += 's.length===1';
                        break;
                    case 'only-of-type':
                        f += 'ss.length===1';
                        break;
                    case 'disabled':
                        f += 'l.disabled';
                        break;
                    case 'enabled':
                        f += '!l.disabled';
                        break;
                    case 'checked':
                        f += 'l.checked';
                        break;
                    case 'empty':
                        f += '!l.childNodes.length'; // TODO: Maybe restrict to elements (t 1) only?
                        break;
                    case 'not':
                        f += '!Sprint.isMatch(\'' + v.replace(/\\'/g, function(x) { return '\\' + x; }) + '\')(0,l)';
                        break;
                    default:
                        throw new Sprint.Error("Unrecognized pseudo-class \"" + p + "\".");
                }
                f += '&&';
            }
        }
        if(info.ATTR) {
            from(info.ATTR).each(function(k, v) {
                var c = k.charAt(k.length - 1);
                f += 'l.getAttribute(\'' + ('$^*~|'.indexOf(c) > -1 ? k.substring(0, k.length - 1) : k).replace(/\\'/g, function(x) { return '\\' + x; }) + '\')';
                if(typeof v === 'string') {
                    v = v.replace(/\\'/g, function(x) { return '\\' + x; });
                    switch(c) {
                        case '$':
                            f += '.endsWith(\'' + v + '\')';
                            break;
                        case '^':
                            f += '.startsWith(\'' + v + '\')';
                            break;
                        case '*':
                            f += '.indexOf(\'' + v + '\')+1';
                            break;
                        case '~':
                            f += '.split(\' \').indexOf(\'' + v + '\')+1';
                            break;
                        case '|':
                            f += '.split(\'-\').indexOf(\'' + v + '\')+1';
                            break;
                        default:
                            f += '===\'' + v + '\'';
                    }
                }
                f += '&&';
            });
        }

        if(f.substring(f.length - 2) !== '&&') {
            // It matches anything.
            f = 'return true  ';
        }

        return new Function(['l'], f.substring(0, f.length - 2) + ';');
    },
    compileTree: function(selector) {
        var t = Sprint.tokenizeSelector(selector), i = -1, j, splitters = ', >~+';
        while(++i < t.length) {
            if(splitters.indexOf(t[i]) > -1) {
                j = i;
                while(--j >= 0 && t[j] === ' ') t[j] = null;
                j = i;
                while(++j < t.length && t[j] === ' ') t[j] = null;
            }
        }
        return from(t).where().select(function(k, v) {
            return k % 2 ? v : Sprint.compileSingle(Sprint.getInfo(v));
        }).toArray();
    },
    matchSingleTree: function(element, tree) {
        if(!tree.length) {
            return true;
        }

        if(!tree[tree.length - 1](element)) {
            return false;
        }

        for(var i = tree.length - 3; i >= 0; i -= 2) {
            switch(tree[i + 1] || ' ') {
                case ' ':
                    while(true) {
                        if(!(element = element.parentNode)) {
                            return false;
                        }
                        if(tree[i](element)) {
                            break;
                        }
                    }
                    break;
                case '>':
                    if(!(element = element.parentNode) || !tree[i](element)) {
                        return false;
                    }
                    break;
                case '+':
                    while(true) {
                        if(!(element = element.previousSibling)) {
                            return false;
                        }
                        if(element.nodeType === 1) {
                            if(!tree[i](element)) {
                                return false;
                            } else {
                                break;
                            }
                        }
                    }
                    break;
                case '~':
                    while(true) {
                        if(!(element = element.previousSibling)) {
                            return false;
                        }
                        if(tree[i](element)) {
                            break;
                        }
                    }
                    break;
            }
        }

        return true;
    },
    matchTree: function(element, tree) {
        var c = [];
        for(var i = 0; i < tree.length; i++) {
            if(tree[i] === ',') {
                if(Sprint.matchSingleTree(element, c)) {
                    return true;
                }
                c = [];
            } else {
                c.push(tree[i]);
            }
        }
        return Sprint.matchSingleTree(element, c);
    },
    tokenizeSelector: function(selector) {
        var quotes = '\'"', nestable = '(){}[]', escaped = false, currentQuotes = null, currentNested = [], tokens = [], splitters = ', >~+', i = -1, c, s = '';
        while(++i < selector.length) {
            c = selector.charAt(i);
            if(escaped) {
                escaped = false;
            } else if(c === '\\') {
                escaped = true;
                continue;
            } else if(currentQuotes) {
                if(currentQuotes === c) {
                    currentQuotes = null;
                }
            } else if(quotes.indexOf(c) > -1) {
                currentQuotes = c;
            } else if(currentNested.length) {
                if(nestable.indexOf(currentNested[currentNested.length - 1]) + 1 === nestable.indexOf(c)) {
                    currentNested.pop();
                }
            } else if(nestable.indexOf(c) > -1) {
                currentNested.push(c);
            } else if(splitters.indexOf(c) > -1) {
                tokens.push(s);
                tokens.push(c);
                s = '';
                continue;
            }
            s += c;
        }
        tokens.push(s);

        return tokens;
    },
    isMatch: function(selector) {
        var f = Sprint.compileTree(selector);
        return function(key, element) {
            return Sprint.matchTree(element, f);
        };
    },
    select: function(selector) {
        return from(Sprint.getAllElements()).where(Sprint.isMatch(selector)).toArray();
    },
    selectIn: function(selector, element) {
        return from(Sprint.getAllElementsIn(element)).where(Sprint.isMatch(selector)).toArray();
    }
};

function from(x) {
    if(!x) {
        throw new Sprint.Error("null or undefined passed to Sprint:from().");
    }

    var r = {
        keys: [],
        values: []
    };

    if(x.length) {
        for(var i = 0; i < x.length; i++) {
            r.keys.push(i);
            r.values.push(x[i]);
        }
    } else if(x.childNodes) {
        return from(x.childNodes);
    } else {
        for(var y in x) {
            if(x.hasOwnProperty(y)) {
                r.keys.push(y);
                r.values.push(x[y]);
            }
        }
    }

    r.length = r.keys.length;

    r.where = function(f) {
        if(f) {
            if(typeof f === 'string') return this.where(new Function(['k', 'v'], 'return ' + f + ';'));

            var r = {};
            for(var i = 0; i < this.length; i++) {
                if(f(this.keys[i], this.values[i])) {
                    r[this.keys[i]] = this.values[i];
                }
            }
            return from(r);
        } else {
            return this.where(function(k, v) { return v; });
        }
    };

    r.select = function(f) {
        var r = {};
        for(var i = 0; i < this.length; i++) {
            r[this.keys[i]] = f(this.keys[i], this.values[i]);
        }
        return from(r);
    };

    r.each = function(f) {
        for(var i = 0; i < this.length; i++) {
            f(this.keys[i], this.values[i]);
        }
    };

    r.first = function() {
        return this.values[0];
    };

    r.last = function() {
        return this.values[this.length - 1];
    };

    r.splitBy = function(f) {
        if(typeof f === 'function') {
            var r = [], c = {};
            for(var i = 0; i < this.length; i++) {
                if(f(this.keys[i], this.values[i])) {
                    r.push(c);
                    c = {};
                } else {
                    c[this.keys[i]] = this.values[i];
                }
            }
            r.push(c);
            return from(r);
        } else {
            return this.splitBy(function(x) { return x === f; });
        }
    };

    r.limit = function(f) {
        var r = {}, i = -1;

        if(typeof f === 'function') {
            while(++i < this.length && f(this.keys[i], this.values[i])) {
                r[this.keys[i]] = this.values[i];
            }
        } else {
            while(++i < Math.min(this.length, f)) {
                r[this.keys[i]] = this.values[i];
            }
        }

        return from(r);
    };

    r.after = function(f) {
        var r = {}, i = -1;

        if(typeof f === 'function') {
            while(++i < this.length && !f(this.keys[i], this.values[i])) {
            }
            while(++i < this.length) {
                r[this.keys[i]] = this.values[i];
            }
        } else {
            if(f < this.length) {
                i = f - 1;
                while(++i < this.length) {
                    r[this.keys[i]] = this.values[i];
                }
            }
        }

        return from(r);
    };

    r.toArray = function() {
        return this.values;
    };

    return r;
}

You use it like so:

Sprint.selectIn("mySelector", element);

Or

Sprint.select("mySelector")

. The code is 6.89KB minified and 2.26KB gzipped.

share|improve this answer
6  
Please note that all user contributions here are cc-wiki licensed, meaning that you grant everyone the right to use and remix the content. If you don't want your code to be "stolen", then don't post it here. –  MtnViewMark Apr 24 '11 at 15:20
1  
Besides, the minification is trivial to overcome. IE9 even includes a JS formatter in the developer tools. However, it cannot be »stolen« legally; CC-By-SA requires attribution when re-using content. –  Јοеу Apr 24 '11 at 15:23
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.