4
\$\begingroup\$

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.

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Try to beat jQuery: it is 85,000 bytes. \$\endgroup\$ Apr 24 '11 at 15:44
  • 4
    \$\begingroup\$ Sizzle, rather. \$\endgroup\$
    – Ry-
    Apr 24 '11 at 19:23
  • 3
    \$\begingroup\$ 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"? \$\endgroup\$ Apr 27 '11 at 15:52
  • \$\begingroup\$ @PeterTaylor No it wouldn't hurt. Feel free to propose an edit. \$\endgroup\$
    – William
    Aug 30 '15 at 3:26
  • \$\begingroup\$ @LiamWilliam You misunderstand: Peter wants you to make the necessary changes yourself \$\endgroup\$
    – Beta Decay
    Aug 30 '15 at 10:22
6
\$\begingroup\$

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.

\$\endgroup\$
2
  • 7
    \$\begingroup\$ 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. \$\endgroup\$ Apr 24 '11 at 15:20
  • 2
    \$\begingroup\$ 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. \$\endgroup\$
    – Joey
    Apr 24 '11 at 15:23
5
\$\begingroup\$

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;
    };
})();
\$\endgroup\$
1
  • 2
    \$\begingroup\$ 1.43 kb compressed and 743 kb gzipped? Might want to leave it just compressed :-) \$\endgroup\$
    – mellamokb
    Apr 29 '11 at 18:24

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