Rant Lite (a very bad idea)

For once, I finally have a project where I want to use Rant instead of just contributing to it. The problem is that this project is written in Javascript - specifically, browser-based Javascript - so I can't use Rant out of the box. So I thought I'd try out JSIL, a tool that converts compiled IL code (.NET assemblies) into Javascript. How bad could it be?

Pretty bad.

If you don't think nearly 25MB of scripts is ridiculous, you probably don't do much web dev. jQuery, clocking in at ~82kb minified, is considered hefty. This size was just unmanageable. So I thought "Well, maybe if I minify it?"

Erm, sort of.

Well, that kind of worked. It squeezed out about 5MB, which is alright. It's still not good enough. So I tried gzipping it.

Woop!

I'd prefer smaller, but 1.4MB is good enough. But then the question arose: how am I going to get it to clients? I don't want the server to have to gzip 19MB of content on every request, and it should be loaded during the asset loading stage anyways so that the page doesn't hang. It was then that I came up with a very bad idea: I was already using browserify, which has a ported version of zlib. I wonder...

function extractJsz(file)
{
  var data = zlib.gunzipSync(new Uint8Array(file));
  var script = Utf8ArrayToStr(data);
  eval(script);
}

// Utf8ArrayToStr implementation goes here

It was a bad idea, but it worked. It was slow, but it was faster than downloading 19MB of scripts. However, there was another problem: while the script was executing, it wasn't working. It was then that I went back and bothered to read the rest of the docs for JSIL.

Apparently, though there are no hidden secrets for reducing the size of your scripts by 24MB (at least none that I found), the docs did contain a "helpful" bit of information: JSIL comes with a synchronous loader that it forces you to use. That is, you can't load your scripts the way you want to load them - at least, not in a way that's documented - and more importantly, there's no way I could fit my hacked together gzipping contraption into there. I also just didn't like the idea of having to package so many loose dependencies loaded outside of my control. So I came up with a different, possibly worse idea.

Rant Lite

With JSIL off the table, I looked at my options for porting Rant to Javascript. I could either:

  • Port Rant to Javascript.
  • Port a small part of Rant to Javascript.

I went with the second option, because I'm lazy. I put together a PEG.js grammar, wrote a loader for rantpkg files, and a really lazy runtime. And yes, it's recursive. I'm sorry.

The result is a clone of Rant that only supports queries, blocks, and block weights. Because as far as I can tell, that's all I need. It only supports loading rantpkg files, since it's easier than trying to write a new dictionary format parser in Javascript. And it doesn't support multiple queries on the first item in a block, because I fucked up the grammar and I'll fix it later damn it.

The resulting PEG.js grammar looks like this:

rant
  = item*

item
  = block
  / query
  / t:text_acceptable+ s:space? { return { type: 'text', value: t.join('') + (s ? ' ' : '') } }
  / space { return ' '; }

block
  = "{" space* first_weight:block_weight? first_item:block_item space* rest:(space* "|" space* weight:block_weight? block_item*)* space* "}" {
    var items = [ first_item ];
    var weights = [first_weight ? first_weight : 1];
    items = items.concat(rest.map(function(r) { weights.push(r[3] || 1); return r[r.length - 1]; }));
    return {
      type: 'block',
      items: items,
      weights: weights
    }
  }

block_weight
  = ("(" space* w:number space* ")" space*) space* { return w.value; }

block_item
  = query
  / a:text_acceptable+ { return {type: 'text', value: a.join('') }; }

query
  = ("<" name:acceptable subtype:([\.]acceptable)? classes:([\-]name:acceptable)* ">") { 
      var cs = classes.map(function(c) { return c[1].join(''); });
      var s = (subtype ? subtype[1].join('') : '');
      return { 
        type: 'query', name: name.join(''), subtype: s, classes: cs
      }; 
    }

number
  = n:([0-9]+([\.][0-9]+)?) { 
      var number = n[0].join('') + (n[1] ? '.' + n[1][1].join('') : '');
      return { 
        type: 'number',
        value: parseFloat(number, 10)
      } 
    } 

acceptable
  = [A-Za-z0-9]+

text_acceptable
  = [A-Za-z0-9!:;,\-\.?'"$%&\*]

space = [ \n\t]

I might release it at some point, but I don't see anyone else really having much use for it. Plus I don't think it's Rant compatible. At some point I'll get around to writing a proper Javascript port of Rant. One of these days...