diff --git a/src/js/raSelectorJS.tw b/src/js/raSelectorJS.tw
deleted file mode 100644
index a53177cc7c7b8f71cd8bed01055b4c49b207eb40..0000000000000000000000000000000000000000
--- a/src/js/raSelectorJS.tw
+++ /dev/null
@@ -1,136 +0,0 @@
-:: RA Selector JS [script]
-
-window.growAdvSelector = function(slave, rule) {
-
-	var ret = "";
-	var setd = 0;
-
-	if ((rule.breastSize == "small" && slave.boobs < 350) ||
-		(rule.breastSize == "c-cup" && slave.boobs < 550) ||
-		(rule.breastSize == "f-cup" && slave.boobs < 1100) ||
-		(rule.breastSize == "k-cup" && slave.boobs < 2150) ||
-		(rule.breastSize == "p-cup" && slave.boobs < 3700) ||
-		(rule.breastSize == "unlimited" && slave.boobs < 24000))	
-	{ 
-		slave.drugs = "breast injections"; 
-		ret = slave.slaveName + "'s boobs could be bigger, so she's been put on breast injections.";
-		return ret;
-	}
-	
-	if ((rule.buttSize == "small" && slave.butt < 1) ||
-		(rule.buttSize == "plump" && slave.butt < 3) ||
-		(rule.buttSize == "huge" && slave.butt < 4) ||
-		(rule.buttSize == "enormous" && slave.butt < 6) ||
-		(rule.buttSize == "unlimited" && slave.butt < 19))	
-	{ 
-		slave.drugs = "butt injections"; 
-		ret = slave.slaveName + "'s butt could be bigger, so she's been put on butt injections.";
-		return ret;
-	}
-
-	if (slave.balls > 0 &&
-		((rule.ballSize == "small" && slave.balls < 2) ||
-		 (rule.ballSize == "big" && slave.balls < 4) ||
-		 (rule.ballSize == "huge" && slave.balls < 6) ||
-		 (rule.ballSize == "unlimited" && slave.balls < 9)))		
-	{ 
-		slave.drugs = "testicle enhancement"; 
-		ret = slave.slaveName + "'s balls aren't large enough, so she's been put on testicle enhancement.";
-		return ret;
-	}
-
-	if (slave.dick > 0 &&
-		((rule.penisSize == "small" && slave.dick < 2) ||
-		 (rule.penisSize == "big" && slave.dick < 4) ||
-		 (rule.penisSize == "huge" && slave.dick < 6) ||
-		 (rule.penisSize == "unlimited" && slave.dick < 12)))
-	{ 
-		slave.drugs = "penis enhancement"; 
-		ret = slave.slaveName + "'s dick isn't large enough, so she's been put on penis enhancement.";
-		return ret;
-	}
-
-	if ((rule.lipSize == "normal" && slave.lips < 15) ||
-		(rule.lipSize == "pretty" && slave.lips < 30) ||
-		(rule.lipSize == "plush" && slave.lips < 55) ||
-		(rule.lipSize == "huge" && slave.lips < 85) ||
-		(rule.lipSize == "facepussy" && slave.lips < 99))	
-	{ 
-		slave.drugs = "lip injections"; 
-		ret = slave.slaveName + "'s lips aren't large enough, so she's been put on lips enhancement.";
-		return ret;
-	}
-
-	if (slave.drugs != "no drugs")
-	{
-		slave.drugs = "no drugs";
-		ret = slave.slaveName + " has reached growth targets and has been taken off growth injections.";
-	}
-
-	return ret;
-
-}
-
-window.growAdvSelectorSlim = function(slave, rule) {
-
-	var ret = "";
-	var setd = 0;
-
-	if ((rule.breastSize == "small" && slave.boobs > 350) ||
-		(rule.breastSize == "c-cup" && slave.boobs > 550) ||
-		(rule.breastSize == "f-cup" && slave.boobs > 1100) ||
-		(rule.breastSize == "k-cup" && slave.boobs > 2150) ||
-		(rule.breastSize == "p-cup" && slave.boobs > 3700))	
-	{ 
-		slave.drugs = "breast redistributors"; 
-		ret = slave.slaveName + "'s boobs are too big, so she's been put on breast reduction drugs.";
-		return ret;
-	}
-	
-	if ((rule.buttSize == "small" && slave.butt > 1) ||
-		(rule.buttSize == "plump" && slave.butt > 3) ||
-		(rule.buttSize == "huge" && slave.butt > 4) ||
-		(rule.buttSize == "enormous" && slave.butt > 6))	
-	{ 
-		slave.drugs = "butt redistributors"; 
-		ret = slave.slaveName + "'s butt is too big, so she's been put on butt reduction drugs.";
-		return ret;
-	}
-
-	if ((rule.ballSize == "small" && slave.balls > 2) ||
-		(rule.ballSize == "big" && slave.balls > 4) ||
-		(rule.ballSize == "huge" && slave.balls > 6))	
-	{ 
-		slave.drugs = "testicle atrophiers"; 
-		ret = slave.slaveName + "'s balls are too big, so she's been put on testicle atrophiers.";
-		return ret;
-	}
-
-	if ((rule.penisSize == "small" && slave.dick > 2) ||
-		(rule.penisSize == "big" && slave.dick > 4) ||
-		(rule.penisSize == "huge" && slave.dick > 6))	
-	{ 
-		slave.drugs = "penis atrophiers"; 
-		ret = slave.slaveName + "'s dick is too big, so she's been put on penis atrophiers.";
-		return ret;
-	}
-
-	if ((rule.lipSize == "normal" && slave.lips > 15) ||
-		(rule.lipSize == "pretty" && slave.lips > 30) ||
-		(rule.lipSize == "plush" && slave.lips > 55) ||
-		(rule.lipSize == "huge" && slave.lips > 85))	
-	{ 
-		slave.drugs = "lip atrophiers"; 
-		ret = slave.slaveName + "'s lips are too big, so she's been put on lips atrophiers.";
-		return ret;
-	}
-
-	if (slave.drugs != "no drugs")
-	{
-		slave.drugs = "no drugs";
-		ret = slave.slaveName + " has reached growth targets and has been taken off growth injections.";
-	}
-
-	return ret;
-
-}
diff --git a/src/js/rulesAssistant.tw b/src/js/rulesAssistant.tw
index 85931119d7b75a3965a19c0cced8c0c524fc0051..861a44a9595d31af0d034306a3bc57b427b55d17 100644
--- a/src/js/rulesAssistant.tw
+++ b/src/js/rulesAssistant.tw
@@ -661,3 +661,483 @@ window.evalExpr = function(rule, slave) {
 	}
 	return flag
 }
+
+// raSelectorJS
+
+window.growAdvSelector = function(slave, rule) {
+
+	var ret = "";
+	var setd = 0;
+
+	if ((rule.breastSize == "small" && slave.boobs < 350) ||
+		(rule.breastSize == "c-cup" && slave.boobs < 550) ||
+		(rule.breastSize == "f-cup" && slave.boobs < 1100) ||
+		(rule.breastSize == "k-cup" && slave.boobs < 2150) ||
+		(rule.breastSize == "p-cup" && slave.boobs < 3700) ||
+		(rule.breastSize == "unlimited" && slave.boobs < 24000))	
+	{ 
+		slave.drugs = "breast injections"; 
+		ret = slave.slaveName + "'s boobs could be bigger, so she's been put on breast injections.";
+		return ret;
+	}
+	
+	if ((rule.buttSize == "small" && slave.butt < 1) ||
+		(rule.buttSize == "plump" && slave.butt < 3) ||
+		(rule.buttSize == "huge" && slave.butt < 4) ||
+		(rule.buttSize == "enormous" && slave.butt < 6) ||
+		(rule.buttSize == "unlimited" && slave.butt < 19))	
+	{ 
+		slave.drugs = "butt injections"; 
+		ret = slave.slaveName + "'s butt could be bigger, so she's been put on butt injections.";
+		return ret;
+	}
+
+	if (slave.balls > 0 &&
+		((rule.ballSize == "small" && slave.balls < 2) ||
+		 (rule.ballSize == "big" && slave.balls < 4) ||
+		 (rule.ballSize == "huge" && slave.balls < 6) ||
+		 (rule.ballSize == "unlimited" && slave.balls < 9)))		
+	{ 
+		slave.drugs = "testicle enhancement"; 
+		ret = slave.slaveName + "'s balls aren't large enough, so she's been put on testicle enhancement.";
+		return ret;
+	}
+
+	if (slave.dick > 0 &&
+		((rule.penisSize == "small" && slave.dick < 2) ||
+		 (rule.penisSize == "big" && slave.dick < 4) ||
+		 (rule.penisSize == "huge" && slave.dick < 6) ||
+		 (rule.penisSize == "unlimited" && slave.dick < 12)))
+	{ 
+		slave.drugs = "penis enhancement"; 
+		ret = slave.slaveName + "'s dick isn't large enough, so she's been put on penis enhancement.";
+		return ret;
+	}
+
+	if ((rule.lipSize == "normal" && slave.lips < 15) ||
+		(rule.lipSize == "pretty" && slave.lips < 30) ||
+		(rule.lipSize == "plush" && slave.lips < 55) ||
+		(rule.lipSize == "huge" && slave.lips < 85) ||
+		(rule.lipSize == "facepussy" && slave.lips < 99))	
+	{ 
+		slave.drugs = "lip injections"; 
+		ret = slave.slaveName + "'s lips aren't large enough, so she's been put on lips enhancement.";
+		return ret;
+	}
+
+	if (slave.drugs != "no drugs")
+	{
+		slave.drugs = "no drugs";
+		ret = slave.slaveName + " has reached growth targets and has been taken off growth injections.";
+	}
+
+	return ret;
+
+}
+
+window.growAdvSelectorSlim = function(slave, rule) {
+
+	var ret = "";
+	var setd = 0;
+
+	if ((rule.breastSize == "small" && slave.boobs > 350) ||
+		(rule.breastSize == "c-cup" && slave.boobs > 550) ||
+		(rule.breastSize == "f-cup" && slave.boobs > 1100) ||
+		(rule.breastSize == "k-cup" && slave.boobs > 2150) ||
+		(rule.breastSize == "p-cup" && slave.boobs > 3700))	
+	{ 
+		slave.drugs = "breast redistributors"; 
+		ret = slave.slaveName + "'s boobs are too big, so she's been put on breast reduction drugs.";
+		return ret;
+	}
+	
+	if ((rule.buttSize == "small" && slave.butt > 1) ||
+		(rule.buttSize == "plump" && slave.butt > 3) ||
+		(rule.buttSize == "huge" && slave.butt > 4) ||
+		(rule.buttSize == "enormous" && slave.butt > 6))	
+	{ 
+		slave.drugs = "butt redistributors"; 
+		ret = slave.slaveName + "'s butt is too big, so she's been put on butt reduction drugs.";
+		return ret;
+	}
+
+	if ((rule.ballSize == "small" && slave.balls > 2) ||
+		(rule.ballSize == "big" && slave.balls > 4) ||
+		(rule.ballSize == "huge" && slave.balls > 6))	
+	{ 
+		slave.drugs = "testicle atrophiers"; 
+		ret = slave.slaveName + "'s balls are too big, so she's been put on testicle atrophiers.";
+		return ret;
+	}
+
+	if ((rule.penisSize == "small" && slave.dick > 2) ||
+		(rule.penisSize == "big" && slave.dick > 4) ||
+		(rule.penisSize == "huge" && slave.dick > 6))	
+	{ 
+		slave.drugs = "penis atrophiers"; 
+		ret = slave.slaveName + "'s dick is too big, so she's been put on penis atrophiers.";
+		return ret;
+	}
+
+	if ((rule.lipSize == "normal" && slave.lips > 15) ||
+		(rule.lipSize == "pretty" && slave.lips > 30) ||
+		(rule.lipSize == "plush" && slave.lips > 55) ||
+		(rule.lipSize == "huge" && slave.lips > 85))	
+	{ 
+		slave.drugs = "lip atrophiers"; 
+		ret = slave.slaveName + "'s lips are too big, so she's been put on lips atrophiers.";
+		return ret;
+	}
+
+	if (slave.drugs != "no drugs")
+	{
+		slave.drugs = "no drugs";
+		ret = slave.slaveName + " has reached growth targets and has been taken off growth injections.";
+	}
+
+	return ret;
+}
+
+// rulesAssistantParser
+
+:: rulesAssistantParser [script]
+
+// Implements a Top Down Operator Precedence parser, also know as a Pratt
+// parser, after its "inventor", Vaughan Pratt.  The one implemented here
+// closely follows what's presented here,
+//     * http://javascript.crockford.com/tdop/tdop.html
+// by Douglas Crockford, that uses that technique in JSLint.  Other relevant
+// resources on the interweb
+//     * http://effbot.org/zone/simple-top-down-parsing.htm
+//     * http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing
+//     * http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
+//     * https://higherlogics.blogspot.gr/2009/11/extensible-statically-typed-pratt.html
+//     * https://github.com/fholm/Vaughan
+//     * https://github.com/DasIch/pratt
+// included here mostly as bookmarks for potential future reference.
+//
+// With regards to the lexer, I used the following with many changes
+//     * http://eli.thegreenplace.net/2013/06/25/regex-based-lexical-analysis-in-python-and-javascript/
+//
+// Other useful things that I may not use any more but wouldn't want to lose,
+//     * https://plainjs.com/javascript/utilities/merge-two-javascript-objects-19/
+//     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+
+
+function panic(index, msg) {
+    throw {index: index, message: msg};
+}
+
+var Lexer = function(skipWhitespace) {
+    this.rules = [];
+    this.index = 0;
+    this.buffer = "";
+    this.skipWhitespace = skipWhitespace ? /\S/ : null;
+}
+
+Lexer.prototype.addRule = function(id, defn) {
+    var pattern = (defn && defn.pattern) || RegExp.escape(id);
+    this.rules.push({
+        id: id,
+        pattern: new RegExp('^' + pattern)
+    });
+}
+
+Lexer.prototype.feed = function(buffer) {
+    this.buffer = buffer;
+    this.index = 0;
+}
+
+Lexer.prototype.nextInterestingChar = function() {
+    if (this.skipWhitespace) {
+        var match = this.skipWhitespace.exec(this.buffer.substr(this.index));
+        return match ? this.index + match.index
+                     : this.buffer.length;
+    }
+    return this.index;
+}
+
+Lexer.prototype.next = function() {
+    this.index = this.nextInterestingChar();
+
+    if (this.index >= this.buffer.length)
+        return { done: true };
+
+    for (var i = 0; i < this.rules.length; ++i) {
+        var rule = this.rules[i],
+            match = rule.pattern.exec(this.buffer.substr(this.index));
+        if (match) {
+            var token = {
+                id: rule.id,
+                value: match[0],
+                index: this.index,
+            };
+            this.index += token.value.length;
+            return { done: false, value: token };
+        }
+    }
+
+    panic(this.index, "illegal character");
+}
+
+
+
+var BaseSymbol = {
+    lbp: 0,
+    nud: function() { panic(this.index, "unexpected '" + this.id + "'"); },
+    led: function() { panic(this.index, "not an operator"); }
+};
+
+var Parser = function(eofToken) {
+    this.lexer = new Lexer(true);
+    this.currentSymbol = null;
+
+    this.eofToken = eofToken;
+    this.symbolTable = {
+        [this.eofToken]: Object.create(BaseSymbol, {id: {value: this.eofToken}})
+    };
+}
+
+Parser.prototype.addSymbol = function(id, defn) {
+    var s = this.symbolTable[id];
+    if (s) {
+        if (defn) {
+            if (defn.lbp !== undefined) s.lbp = defn.lbp;
+            if (defn.nud !== undefined) s.nud = defn.nud;
+            if (defn.led !== undefined) s.led = defn.led;
+        }
+    } else {
+        s = Object.create(BaseSymbol);
+        s.id = id;
+        if (defn && defn.lbp !== undefined) s.lbp = defn.lbp;
+        if (defn && defn.nud) s.nud = defn.nud;
+        if (defn && defn.led) s.led = defn.led;
+        this.symbolTable[id] = s;
+        this.lexer.addRule(id, defn);
+    }
+    return this;
+}
+
+Parser.prototype.addInfix = function(id, lbp, callback) {
+    this.addSymbol(id, {
+        lbp: lbp,
+        led: function(p, left) { return callback(this, left, p.parse(lbp)); }
+    });
+    return this;
+}
+
+Parser.prototype.addInfixR = function(id, lbp, callback) {
+    this.addSymbol(id, {
+        lbp: lbp,
+        led: function(p, left) { return callback(this, left, p.parse(lbp-1)); }
+    });
+    return this;
+}
+
+Parser.prototype.addPrefix = function(id, callback) {
+    this.addSymbol(id, {
+        // FIXME: this should not always be 70
+        nud: function (p) { return callback(this, p.parse(70)); }
+    });
+    return this;
+}
+
+Parser.prototype.addConstant = function(id, callback) {
+    this.addSymbol(id, {
+        nud: function () { return callback(this); }
+    });
+    return this;
+}
+
+Parser.prototype.advance = function(id) {
+    if (id !== undefined && this.currentSymbol.id !== id)
+        panic(this.currentSymbol.index, "expected '" + id + "', got '" + this.currentSymbol.id + "'");
+
+    var iter = this.lexer.next(),
+        token = iter.value;
+    if (iter.done)
+        token = {
+            id: this.eofToken,
+            index: this.lexer.buffer.length
+        };
+
+    var symbol = this.symbolTable[iter.done ? this.eofToken : token.id];
+    if (!symbol)
+        panic(token.index, "unknown token '" + token.id + "'");
+
+    var newSymbol = Object.create(symbol);
+    newSymbol.value = token.value;
+    newSymbol.index = token.index;
+    return this.currentSymbol = newSymbol;
+}
+
+Parser.prototype.parse = function(rbp) {
+    var symbol = this.currentSymbol;
+    this.advance();
+    var left = symbol.nud(this);
+
+    rbp = rbp || 0;
+    while (rbp < this.currentSymbol.lbp) {
+        symbol = this.currentSymbol;
+        this.advance();
+        left = symbol.led(this, left);
+    }
+    return left;
+}
+
+Parser.prototype.parseString = function(string) {
+    this.lexer.feed(string);
+    this.advance();  // "kickstart" the lexer
+    var result = this.parse();
+    this.advance(this.eofToken);
+    return result;
+}
+
+
+
+var ASTBuilder = function(eofToken) {
+    this.parser = new Parser(eofToken);
+}
+
+ASTBuilder.prototype.addSymbol = function(id, extra) {
+    this.parser.addSymbol(id, extra);
+    return this;
+}
+
+ASTBuilder.prototype.addInfix = function(id, lbp) {
+    this.parser.addInfix(id, lbp, function(symbol, left, right) {
+        return {
+            id: id,
+            first: left,
+            second: right,
+        };
+    });
+    return this;
+}
+
+ASTBuilder.prototype.addInfixR = function(id, lbp) {
+    this.parser.addInfixR(id, lbp, function(symbol, left, right) {
+        return {
+            id: id,
+            first: left,
+            second: right,
+        };
+    });
+    return this;
+}
+
+ASTBuilder.prototype.addPrefix = function(id) {
+    this.parser.addPrefix(id, function(symbol, left) {
+        return {
+            id: id,
+            first: left,  // it's not really the left is it?
+        };
+    });
+    return this;
+}
+
+ASTBuilder.prototype.addConstant = function(id, value) {
+    this.parser.addConstant(id, function(symbol) {
+        return {
+            id: id,
+            value: value,
+        };
+    });
+    return this;
+}
+
+
+var op = {
+    add: function(a, b) { return a + b; },
+    sub: function(a, b) { return a - b; },
+    mul: function(a, b) { return a * b; },
+    div: function(a, b) { return a / b; },
+    pow: function(a, b) { return Math.pow(a, b); },
+
+    neg: function(a) { return -a; },
+
+    lt: function(a, b) { return a < b; },
+    le: function(a, b) { return a <= b; },
+    gt: function(a, b) { return a > b; },
+    ge: function(a, b) { return a >= b; },
+    eq: function(a, b) { return a === b; },
+    neq: function(a, b) { return a !== b; },
+
+    not: function(a) { return !a; },
+    or: function(a, b) { return a || b; },
+    and: function(a, b) { return a && b; },
+};
+
+var parserBuilder = new ASTBuilder("(end)")
+    // XXX: need to be first to not be recognised as a (name)
+    .addConstant("true", true)
+    .addConstant("false", false)
+
+    .addSymbol("(number)", {
+        pattern: "\\d+",
+        nud: function() {
+            return {
+                id: "(number)",
+                value: parseInt(this.value),
+            };
+        }
+    })
+
+    .addSymbol("(string)", {
+        pattern: "\"(?:[^\\\\\"]|\\\\\"|\\\\(?!\"))*\"",
+        nud: function(p) {
+            return {
+                id: "(string)",
+                value: this.value.replace(/^\"|\"$/g, ""),
+            };
+        }
+    })
+
+    .addSymbol("(name)", {
+        pattern: "[a-zA-Z]\\w*",
+        nud: function(p) {
+            return {
+                id: "(name)",
+                name: this.value,
+            };
+        }
+    })
+
+    .addInfix("+", 50,  op.add)
+    .addInfix("-", 50,  op.sub)
+    .addInfix("*", 60,  op.mul)
+    .addInfix("/", 60,  op.div)
+    .addInfixR("^", 70, op.pow)
+
+    .addPrefix("-", op.neg)
+
+    .addInfix("<=", 40, op.le)
+    .addInfix("<",  40, op.lt)
+    .addInfix(">=", 40, op.ge)
+    .addInfix(">",  40, op.gt)
+    .addInfix("!=", 40, op.neq)
+    .addInfix("=",  40, op.eq)
+
+    .addPrefix("!", op.not)
+    .addInfix("||", 30, op.or)
+    .addInfix("&&", 30, op.and)
+
+    .addSymbol(")")
+    .addSymbol("(", {
+        nud: function(p) {
+            var expr = p.parse(0);
+            p.advance(")");
+            return expr;
+        }
+    });
+
+var parser = parserBuilder.parser;
+window.parser = parser;
+
+window.parseCondition = function(condition) {
+    try {
+        return {expr: parser.parseString(condition), error: null};
+    } catch (e) {
+        return {expr: null, error: e};
+    }
+}
diff --git a/src/js/rulesAssistantParser.tw b/src/js/rulesAssistantParser.tw
deleted file mode 100644
index d369bba1511fdc65fb6045d390583dc4c5772ea6..0000000000000000000000000000000000000000
--- a/src/js/rulesAssistantParser.tw
+++ /dev/null
@@ -1,358 +0,0 @@
-:: rulesAssistantParser [script]
-
-// Implements a Top Down Operator Precedence parser, also know as a Pratt
-// parser, after its "inventor", Vaughan Pratt.  The one implemented here
-// closely follows what's presented here,
-//     * http://javascript.crockford.com/tdop/tdop.html
-// by Douglas Crockford, that uses that technique in JSLint.  Other relevant
-// resources on the interweb
-//     * http://effbot.org/zone/simple-top-down-parsing.htm
-//     * http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing
-//     * http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
-//     * https://higherlogics.blogspot.gr/2009/11/extensible-statically-typed-pratt.html
-//     * https://github.com/fholm/Vaughan
-//     * https://github.com/DasIch/pratt
-// included here mostly as bookmarks for potential future reference.
-//
-// With regards to the lexer, I used the following with many changes
-//     * http://eli.thegreenplace.net/2013/06/25/regex-based-lexical-analysis-in-python-and-javascript/
-//
-// Other useful things that I may not use any more but wouldn't want to lose,
-//     * https://plainjs.com/javascript/utilities/merge-two-javascript-objects-19/
-//     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
-
-
-if (!RegExp.escape) {
-    RegExp.escape = function(s) {
-        return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-    };
-}
-
-if (!String.prototype.format) {
-    String.prototype.format = function() {
-        var args = arguments;
-        return this.replace(/{(\d+)}/g, function(match, number) {
-            return typeof args[number] !== "undefined" ? args[number] : match;
-        });
-    };
-}
-
-function panic(index, msg) {
-    throw {index: index, message: msg};
-}
-
-
-
-var Lexer = function(skipWhitespace) {
-    this.rules = [];
-    this.index = 0;
-    this.buffer = "";
-    this.skipWhitespace = skipWhitespace ? /\S/ : null;
-}
-
-Lexer.prototype.addRule = function(id, defn) {
-    var pattern = (defn && defn.pattern) || RegExp.escape(id);
-    this.rules.push({
-        id: id,
-        pattern: new RegExp('^' + pattern)
-    });
-}
-
-Lexer.prototype.feed = function(buffer) {
-    this.buffer = buffer;
-    this.index = 0;
-}
-
-Lexer.prototype.nextInterestingChar = function() {
-    if (this.skipWhitespace) {
-        var match = this.skipWhitespace.exec(this.buffer.substr(this.index));
-        return match ? this.index + match.index
-                     : this.buffer.length;
-    }
-    return this.index;
-}
-
-Lexer.prototype.next = function() {
-    this.index = this.nextInterestingChar();
-
-    if (this.index >= this.buffer.length)
-        return { done: true };
-
-    for (var i = 0; i < this.rules.length; ++i) {
-        var rule = this.rules[i],
-            match = rule.pattern.exec(this.buffer.substr(this.index));
-        if (match) {
-            var token = {
-                id: rule.id,
-                value: match[0],
-                index: this.index,
-            };
-            this.index += token.value.length;
-            return { done: false, value: token };
-        }
-    }
-
-    panic(this.index, "illegal character");
-}
-
-
-
-var BaseSymbol = {
-    lbp: 0,
-    nud: function() { panic(this.index, "unexpected '" + this.id + "'"); },
-    led: function() { panic(this.index, "not an operator"); }
-};
-
-var Parser = function(eofToken) {
-    this.lexer = new Lexer(true);
-    this.currentSymbol = null;
-
-    this.eofToken = eofToken;
-    this.symbolTable = {
-        [this.eofToken]: Object.create(BaseSymbol, {id: {value: this.eofToken}})
-    };
-}
-
-Parser.prototype.addSymbol = function(id, defn) {
-    var s = this.symbolTable[id];
-    if (s) {
-        if (defn) {
-            if (defn.lbp !== undefined) s.lbp = defn.lbp;
-            if (defn.nud !== undefined) s.nud = defn.nud;
-            if (defn.led !== undefined) s.led = defn.led;
-        }
-    } else {
-        s = Object.create(BaseSymbol);
-        s.id = id;
-        if (defn && defn.lbp !== undefined) s.lbp = defn.lbp;
-        if (defn && defn.nud) s.nud = defn.nud;
-        if (defn && defn.led) s.led = defn.led;
-        this.symbolTable[id] = s;
-        this.lexer.addRule(id, defn);
-    }
-    return this;
-}
-
-Parser.prototype.addInfix = function(id, lbp, callback) {
-    this.addSymbol(id, {
-        lbp: lbp,
-        led: function(p, left) { return callback(this, left, p.parse(lbp)); }
-    });
-    return this;
-}
-
-Parser.prototype.addInfixR = function(id, lbp, callback) {
-    this.addSymbol(id, {
-        lbp: lbp,
-        led: function(p, left) { return callback(this, left, p.parse(lbp-1)); }
-    });
-    return this;
-}
-
-Parser.prototype.addPrefix = function(id, callback) {
-    this.addSymbol(id, {
-        // FIXME: this should not always be 70
-        nud: function (p) { return callback(this, p.parse(70)); }
-    });
-    return this;
-}
-
-Parser.prototype.addConstant = function(id, callback) {
-    this.addSymbol(id, {
-        nud: function () { return callback(this); }
-    });
-    return this;
-}
-
-Parser.prototype.advance = function(id) {
-    if (id !== undefined && this.currentSymbol.id !== id)
-        panic(this.currentSymbol.index, "expected '" + id + "', got '" + this.currentSymbol.id + "'");
-
-    var iter = this.lexer.next(),
-        token = iter.value;
-    if (iter.done)
-        token = {
-            id: this.eofToken,
-            index: this.lexer.buffer.length
-        };
-
-    var symbol = this.symbolTable[iter.done ? this.eofToken : token.id];
-    if (!symbol)
-        panic(token.index, "unknown token '" + token.id + "'");
-
-    var newSymbol = Object.create(symbol);
-    newSymbol.value = token.value;
-    newSymbol.index = token.index;
-    return this.currentSymbol = newSymbol;
-}
-
-Parser.prototype.parse = function(rbp) {
-    var symbol = this.currentSymbol;
-    this.advance();
-    var left = symbol.nud(this);
-
-    rbp = rbp || 0;
-    while (rbp < this.currentSymbol.lbp) {
-        symbol = this.currentSymbol;
-        this.advance();
-        left = symbol.led(this, left);
-    }
-    return left;
-}
-
-Parser.prototype.parseString = function(string) {
-    this.lexer.feed(string);
-    this.advance();  // "kickstart" the lexer
-    var result = this.parse();
-    this.advance(this.eofToken);
-    return result;
-}
-
-
-
-var ASTBuilder = function(eofToken) {
-    this.parser = new Parser(eofToken);
-}
-
-ASTBuilder.prototype.addSymbol = function(id, extra) {
-    this.parser.addSymbol(id, extra);
-    return this;
-}
-
-ASTBuilder.prototype.addInfix = function(id, lbp) {
-    this.parser.addInfix(id, lbp, function(symbol, left, right) {
-        return {
-            id: id,
-            first: left,
-            second: right,
-        };
-    });
-    return this;
-}
-
-ASTBuilder.prototype.addInfixR = function(id, lbp) {
-    this.parser.addInfixR(id, lbp, function(symbol, left, right) {
-        return {
-            id: id,
-            first: left,
-            second: right,
-        };
-    });
-    return this;
-}
-
-ASTBuilder.prototype.addPrefix = function(id) {
-    this.parser.addPrefix(id, function(symbol, left) {
-        return {
-            id: id,
-            first: left,  // it's not really the left is it?
-        };
-    });
-    return this;
-}
-
-ASTBuilder.prototype.addConstant = function(id, value) {
-    this.parser.addConstant(id, function(symbol) {
-        return {
-            id: id,
-            value: value,
-        };
-    });
-    return this;
-}
-
-
-var op = {
-    add: function(a, b) { return a + b; },
-    sub: function(a, b) { return a - b; },
-    mul: function(a, b) { return a * b; },
-    div: function(a, b) { return a / b; },
-    pow: function(a, b) { return Math.pow(a, b); },
-
-    neg: function(a) { return -a; },
-
-    lt: function(a, b) { return a < b; },
-    le: function(a, b) { return a <= b; },
-    gt: function(a, b) { return a > b; },
-    ge: function(a, b) { return a >= b; },
-    eq: function(a, b) { return a === b; },
-    neq: function(a, b) { return a !== b; },
-
-    not: function(a) { return !a; },
-    or: function(a, b) { return a || b; },
-    and: function(a, b) { return a && b; },
-};
-
-var parserBuilder = new ASTBuilder("(end)")
-    // XXX: need to be first to not be recognised as a (name)
-    .addConstant("true", true)
-    .addConstant("false", false)
-
-    .addSymbol("(number)", {
-        pattern: "\\d+",
-        nud: function() {
-            return {
-                id: "(number)",
-                value: parseInt(this.value),
-            };
-        }
-    })
-
-    .addSymbol("(string)", {
-        pattern: "\"(?:[^\\\\\"]|\\\\\"|\\\\(?!\"))*\"",
-        nud: function(p) {
-            return {
-                id: "(string)",
-                value: this.value.replace(/^\"|\"$/g, ""),
-            };
-        }
-    })
-
-    .addSymbol("(name)", {
-        pattern: "[a-zA-Z]\\w*",
-        nud: function(p) {
-            return {
-                id: "(name)",
-                name: this.value,
-            };
-        }
-    })
-
-    .addInfix("+", 50,  op.add)
-    .addInfix("-", 50,  op.sub)
-    .addInfix("*", 60,  op.mul)
-    .addInfix("/", 60,  op.div)
-    .addInfixR("^", 70, op.pow)
-
-    .addPrefix("-", op.neg)
-
-    .addInfix("<=", 40, op.le)
-    .addInfix("<",  40, op.lt)
-    .addInfix(">=", 40, op.ge)
-    .addInfix(">",  40, op.gt)
-    .addInfix("!=", 40, op.neq)
-    .addInfix("=",  40, op.eq)
-
-    .addPrefix("!", op.not)
-    .addInfix("||", 30, op.or)
-    .addInfix("&&", 30, op.and)
-
-    .addSymbol(")")
-    .addSymbol("(", {
-        nud: function(p) {
-            var expr = p.parse(0);
-            p.advance(")");
-            return expr;
-        }
-    });
-
-var parser = parserBuilder.parser;
-window.parser = parser;
-
-window.parseCondition = function(condition) {
-    try {
-        return {expr: parser.parseString(condition), error: null};
-    } catch (e) {
-        return {expr: null, error: e};
-    }
-}