Friday, January 25, 2008

Introducing CSSx

Sick of the lack of syntactic sugar for CSS? While many designers are fine with the simplicity of CSS, some developers managing large codebases (such as Zillow's still-terse 2500-line master.css) have been flailing for sanity. Enter CSSx, a syntax-enhancing compiler for CSS. It accepts all currently-valid CSS syntax, so you can use your current CSS chops unaltered, but adds variables and block nesting to the language.

/* sample.cssx */

$red = #f00 /* a comment here*/;
$block = {
    div {
        padding: 10px;
    }
    color: $red;
    border-width: 1px;
    margin: 20px 10px 15px 0;
}

.warning {
    color: $red;
}

.content {
    .warning {
        width: 200px;
    }

    cite { @rule($block); }

    p {
        span.tooltip {
            color: blue;
        }
        font-size: 1.1em;
    }

    .header {
        @rule($block);
        font-size: 0.8em;
    }

    font-family: Verdana, Arial, sans-serif;
}

It's still very beta (I'd peg it at 0.1), but please download, give it a spin, and give me feedback on what works/doesn't work for you, or what you'd like to see added. Download CSSx from Google Code.

Saturday, October 13, 2007

Type-Converting Operator Considered Ridiculous

I have come to question the wisdom of the type-converting == operator in JavaScript. The algorithm (defined in §11.9.3 of the ECMA-262 v3 spec) requires no fewer than 22 steps to execute, and produces such amazing intrasitivities as:

>>> '0' == 0
true

>>> 0 == ''
true

>>> '' == '0'
false

and random quirkiness such as:

>>> ' \t\r\n' == 0
true

but:

>>> null == 0
false

>>> null == false
false

>>> null == ''
false

>>> null == undefined
true

Do these behaviors strike anyone else as, well, a bit off? In the first example (discussed elsewhere) the intransitivity of the equality operator is a result of wanting an equality operator to play well within a weakly-typed language. If you want to be able to compare numbers to their string representations as though they were of the same type, while still preserving string comparison, this is the logical result. The second example is a result of the ToNumber algorithm, described in §9.3ff. The whitespace conversion to number value 0 is strange, but defined in the spec (in fact, it's a result of the same algorithm that dictates Number(' ') => 0). The third example seems bizarre as well; I'm not sure why null would be treated differently than 0 or false, but the same as undefined, in such expressions. Many articles from otherwise reputable sources often describe all of these values in terms of "truthiness" and "falsiness", but these articles sometimes gloss over these distinctions.

I don't condone transitioning JS into a strongly-typed system, but some of these behaviors are quite subtle. Given the fact that the vast majority of JavaScript programmers seem to be unaware of the strict-equality operator === (and its negative counterpart, !==), it's no wonder behaviors such as these can cause confusion. See also this article for even more pitfalls, this time simply with math operators combining string and numeric types.

So as Crockford says, use the strict comparison operator === unless you know what you're doing.

Thursday, October 4, 2007

All the Code Security You Need

Lots of beginning JS coders seem to have this idea that the web is all about having your cake and eating it too. I want to participate in the glorious Internet revolution, I just don't want to share my ideas or information with anybody else. I'm writing rich internet applications that are so great, just so unique, that I want to make sure nobody else can possibly look at my code and steal all those great juicy super-awesome proprietary ideas I've got. And maybe, just for bragging rights, I want my files to look HUGE as well. I've mocked up a handy little device that can take care of both tasks at once. Behold, the decompressJS module:

var decompressJS = (function () {
    var encoding = [' ', '\t', '\n', '\r'];
    var revEncoding = (function () {
        var ec = {};
        for (var i=0; i<encoding.length; i++) {
            ec[encoding[i]] = i;
        };
        return ec;
    })();

    return {
        encode: function (codeText) {
            var out = [], cc, i=0, len=codeText.length;

            while (i<len) {
                cc = codeText.charCodeAt(i++);

                // JS chars are actually 16-bit UTF-16 chars, 
                // so we need 4 chars encoded per source char
                // to fully encode
                for (var j=0; j<8; j+=2) {
                    out.push(encoding[(cc>>j)&3]);
                }

            }
            return out.join('');
        },

        decode: function (encodedText) {
            var out = [], cur, i=0, len=encodedText.length;
            while (i<len) {
                cur = 0;
                for (var j=0; j<8; j+=2) {
                    cur += revEncoding[encodedText.charAt(i++)]<<j;
                }
                out.push(String.fromCharCode(cur));
            }
            return out.join('');
        }
    };

})();

Like any mature programming paradigm, my decompressJS can even effectively encode and decode itself, the above 1225-character-long code example (including whitespace) decompressed neatly into an all-whitespace string with no fewer than 4900 characters! Just run your code through this baby and it'll come out the other end fully converted into only invisible whitespace characters! Transfer your JS code over the wire, making it look like nothing but an empty file with all whitespace! Magically increase your file size by a factor of 4! Impress your colleagues with your invisible code! Who's laughing now, code stealers?

Friday, July 27, 2007

Glorious links

BoingBoing's been on a good tear recently. Better than usual. For those of you whose eyes have glazed over, go back and re-read these posts:

Tuesday, July 17, 2007

All the Templating You Need

function replicate(items, template, defaults) {
    var indices = {};
    var i=0, item;

    // build direct map of column names -> row index
    while ((item = data.columns[i])) indices[item] = i++;

    return data.rows.map(function (row) {
            return template.replace(/__\$\{(.+?)\}__/g, function (str, keyword) {
                return (keyword in indices)? row[indices[keyword]] : '';
                });
            }).join('\n');
}

var data = {
    'columns': ['adj', 'noun'],
    'rows': [
        ['main', 'man'],
    ['leading', 'lady'],
    ['green', 'dreams']
        ]
};

var template = '<p>__${adj}__ __${noun}__</p>';
replicate(data, template);


Peter Michaux has some nice ideas about keeping the JSON format DRY, so that data returned resembles something more like a list of Python tuples. (Python is also probably the single language that helped me to understand efficient JavaScript patterns.)

Client-side transforms - converting an XML or JSON response into HTML on the client, to save server bandwidth and processing time - are a key part of modern web apps, but I'm not sure about a transform system that implements full-blown JavaScript logic. Branching or looping can be implemented easily in transforming functions; several templates can be used and plugged in to each other, leading to nested data structures in the response. (Hopefully, time permitting, I'll get to demonstrate how that works soon.)

innerHTML may not be a part of any standard, but there's no reason why it shouldn't be. Sometimes we need to interact with the DOM as a tree, sometimes it's more useful to unleash JavaScript's string parsing and regex power on it.

Monday, July 9, 2007

One-line CSS minifier

CSS minification in one line:

$ cat sourcefile.css | sed -e 's/^[ \t]*//g; s/[ \t]*$//g; s/\([:{;,]\) /\1/g; s/ {/{/g; s/\/\*.*\*\///g; /^$/d' | sed -e :a -e '$!N; s/\n\(.\)/\1/; ta' >target.css

With comments:

$ cat sourcefile.css | sed -e '
s/^[ \t]*//g;         # remove leading space
s/[ \t]*$//g;         # remove trailing space
s/\([:{;,]\) /\1/g;   # remove space after a colon, brace, semicolon, or comma
s/ {/{/g;             # remove space before a semicolon
s/\/\*.*\*\///g;      # remove comments
/^$/d                 # remove blank lines
' | sed -e :a -e '$!N; s/\n\(.\)/\1/; ta # remove all newlines
' > target.css

Using this script, I was able to chop about 29% (10K) off our master.css file. Assumes lines end in semicolons that should end in semicolons. May not play well with certain freakish outdated CSS hacks. Use at your own risk, and always test throughly before releasing into the wild.

Saturday, July 7, 2007

The Problem with SlickSpeed

For the past month or so, there's been a lot of noise about the SlickSpeed Selectors Test Suite. Since I'm in the market for a good selector engine for Zillow, and since it's a bit of a rite of passage (a front-end web dev's equivalent of compiler authoring?), I wrote my own, to see how well I could do and to see how it stacks up to the rest.

So of course, I modified the suite (under its MIT license) to test my little attempt as well. I was pleased with my initial results, but found the test document that comes packaged with the suite to be a little simplistic. Not enough variety or depth of nesting; the resulting DOM structures don't really resemble what I look at on a daily basis at work. I wanted to measure performance in the wild. So I replaced Shakespeare's As You Like It with the Home Details Page for Zillow.com, perhaps the most complex page on the site. Among other things, it includes a photo gallery, a Flash map, an Live Local-based Bird's Eye View map, a chart widget, several ads, tables, etc.

The results, you can see for yourself, here.

As it turns out, according to SlickSpeed, my engine outperforms all but 2 of the other engines on Firefox, and is the best performer on IE7.

So my misgivings on the nature of the document wandered over to the construction of the queries. The given queries perform a "breadth" pass, but they don't really provide a "depth" pass including all manner of combinations of possible selectors, so I wrote my own addition to the suite that picks random elements from the DOM and generates a matching CSS1 selector for it. You can see the dynamic test suite here.

Now, my Element.select engine's performance is fair to decent at best, but no longer the front-runner. Unless I can iron out the kinks, I might look into Ext's engine, especially since it fits nicely into the Yahoo! UI library we use at Zillow.

On the other hand, my Element.select engine is stand-alone and does not provide any other services or dependencies. It's a whopping 6KB (minified), but I wouldn't recommend the use of a CSS query engine for anything short of a full-scale web application anyway.

Some thoughts, though: For reasons that should be self-explanatory, it appears that all of the CSS engine makers are optimizing for Firefox. And once again, Opera's JavaScript engine (named Linear B) and DOM implementation beats out all the rest. Performance on IE looks to be all-around poorer. The Internet Explorer Team certainly has their work cut out for them, not only in improving their DOM and JScript performance and their developer tools (a decent profiler and a debugger that's not attached to Visual Studio would be nice), but also in winning over a hostile developer community. I guess that's what happens when the maker of the World's Number One Browser shuts down their development team for 5 years.

Prototype and MooTools appear to be compiling the CSS selectors into XPath statements for Firefox's (and Opera's) built-in xpath evaluator (too bad IE forgot to allow MSXML's XPath engine to apply to the HTML DOM). While the DOM performance for these XPath-based implementations is fantastic, they also help underline the end-user experience difference between browsers. Let's hope users take notice how much faster the leading non-IE browsers are in comparison; it's hard to win users over on the basis of standards compliance alone.

If nothing else, I hope my modified SlickSpeeds will help CSS query engine developers focus on what's important: CSS1 queries. The time scores at the bottom of the SlickSpeed test skew heavily toward obscure pseudoclass and attribute selectors which I for one won't use most of the time. It's the meat-and-potatoes tag, class, and ID that really count.