SamePlace spinoffs #1: CSS Query

Before:


var xulScriptlet;
var blueprint = document.getElementById('blueprints').firstChild;
while(blueprint) {
    if(blueprint.getAttribute('class') == 'scriptlet')
        break;
    blueprint = blueprint.nextSibling;
}

var xulScriptlet = blueprint.cloneNode(true);
xulScriptlet.getElementsByAttribute('class', 'name')[0].value =
    scriptlet.info.name;
xulScriptlet.getElementsByAttribute('class', 'version')[0].value =
    scriptlet.info.version;
document.getElementById('scriptlets').appendChild(xulScriptlet);

After…



var xulScriptlet = $('#blueprints > .scriptlet)._.cloneNode(true);
$(xulScriptlet).$('.name')._.value = scriptlet.info.name;
$(xulScriptlet).$('.name')._.version = scriptlet.info.version;
$('#scriptlets')._.appendChild(xulScriptlet);

I got tired of writing DOM code, and envious of how the excellent jQuery uses CSS selectors. However, I needed a couple of things more than jQuery offered and a couple of hundreds less, I needed them in XUL rather than in HTML, and since Joel turned my XHTML templating system into an XML-to-Erlang compiler, I have been dying to write something with a “compile()” function in it. A few hours of healthy hacking later, the result was in the SamePlace repository.

(It was also an excercise in what I call API transparency, about which I’ll hopefully write soon.)

Here’s a quick rundown on how to use it. The ”$” function opens a “query environment”, where you can write using (a subset of) CSS selector syntax. Examples:


$('#foo')
$('.bar')
$('#foo > label')
$('#foo [hidden="true"])


You can chain query environments, so these two lines are equivalent:


$('#foo > label')
$('#foo').$('> label')

To escape the query environment and get to the DOM world, use ”_” for the first matched element and “_a” for an array of all matched elements:


$('#foo > label')._.value = 'hello, world!';
$('#foo > label')._a.forEach(function(label) { label.value = 'hello, world!'; });

If you already have a DOM element to start searching from, pass that to ”$()”; it will set the context for subsequent queries:


var xulFoo = $('#foo');
$(xulFoo).$('> .name')._.value = 'Ben';
$(xulFoo).$('> .surname')._.value = 'Bitdiddle';

As I said, I needed a couple of extra things: parent and ancestor selectors, respectively mapped to ”<” and ”^”. For example, to iterate over all boxes which are parent of hidden elements:


$('[hidden="true"] < box')._a.forEach(function(box) { doSomething(box); });

Limitations: search by class maps to getElementsByAttribute(), so $(”.bar”) will only find elements with class="bar" and not elements with class="bar baz". I didn’t want to compile down to XPath (which isn’t available in Thunderbird), and using JavaScript to walk an element’s subtree and test each descendant’s attribute for a match was a terrible performance hit. Also, I’m not happy at all with the “_a” property, can someone think of a less ugly name?

Anyway, enjoy, and let me know if it proves useful.

Trivia: it is said of obscure programmers that “they can’t understand they’re own code the day after they’ve written it”. I don’t usually consider myself “obscure”, but I believe I found a new low in the above criteria:


return function(context) {
    if(!('length' in context))
        context = [context];

    return fold(
        function(finder, candidates) {
            return fold(
                function(candidate, newCandidates) {
                    return newCandidates.concat(
                        Array.slice(finder(candidate)));
                }, [], candidates);
        },
        context, finders);
}


Which, I confess, I wasn’t understanding while I was writing it.