I'm an IDIOT!! June 11, 2013
I spent a ton of time trying to write a syntax parser for a meta-language which would be parsed by Javascript. I just replaced a lot of confusing logic with about 40 lines of code.
First, Javascript is dynamic and has stuff built in that can evaluate arbitrary code passed in via a string.
I'm not talking about eval().
Function!
However, my syntax looks like this: Hello {{page.user == null ? 'Anonymous' : page.user.firstName }}
So, we need to know what "page" is in the context of the function. I still have to parse out top level variables. In the code above, it will get "page" as a top level variable.
Then, I build up the function:
var f = new Function(vars[0], body);
"body" is actually modified, I set it to "return " + body; So that I can do {{ page.user == null ? 'Anonymous' : page.user.firstName }} and it will return the display name instead of undefined, the default behavior of a void function.
I have to count up the number of variables used, and build the function accordingly. Currently, this is a switch statement.
switch (vars.length){
case 0: f = new Function(body); break;
case 1: f = new Function(vars[0], body); break;
case 2: f = new Function(vars[0], vars[1], body); break;
}
Luckily in my code, there aren't more than 3-4 "top level" variables, including globals like "Array" and "String".
Here's the variable parsing part:
var shared = require("./shared");
require("strings");
var constants = { "true": true, "false": true, "null": true };
var lookup = {};
this.getVariables = function(body){
if (body in lookup)
return lookup[body];
var vars = [];
var instr = false;
var instrch = null;
var buf = "";
var toplevel = false;
var result = null;
for (var i = 0; i < body.length; i++){
var ch = body.charAt(i);
switch (ch){
case "'": case "\"":
instr = ch != instrch;
instrch = instrch == null ? ch : (instr ? instrch : null);
break;
}
if ((!instr && shared.tokenSeparator.test(ch)) || i == body.length-1){
if (i == body.length-1) buf+= ch;
if (!toplevel && (result = shared.variable.exec(buf)) != null && !(result[1] in constants)){
if (!vars.some(function(d){ return d == result[1]})){
vars.push(result[1]);
toplevel = true;
}
}
buf = "";
}
else if (instr) buf = "";
else buf += ch;
if (toplevel && (instr || (shared.tokenSeparator.test(ch) && ch != "." && ch != "]"))) toplevel = false;
}
lookup[body] = vars;
return vars;
}
And here's the evaluation part:
var syntax = require("./syntax");
var shared = require("./shared");
var lookup = {};
var Evaluator = function(globals){
this.globals = globals;
}
Evaluator.prototype.evaluate = function(body, context){
body = shared.replaceComps(body);
var vars = syntax.getVariables(body);
var args = [];
body = "return " + body;
for (var i = 0; i < vars.length; i++){
if (context && vars[i] in context)
args.push(context[vars[i]]);
else if (this.globals && vars[i] in this.globals)
args.push(this.globals[vars[i]]);
}
if (body in lookup){
return lookup[body].apply(null, args);
}
var f = null;
switch (vars.length){
case 0: f = new Function(body); break;
case 1: f = new Function(vars[0], body); break;
case 2: f = new Function(vars[0], vars[1], body); break;
case 3: f = new Function(vars[0], vars[1], vars[2], body); break;
case 4: f = new Function(vars[0], vars[1], vars[2], vars[3], body); break;
}
var result = null;
if (f != null){
result = f.apply(null, args);
lookup[body] = f;
}
return result;
}
this.Evaluator = Evaluator;
shared.js has a regular expression, a map (for old syntax considerations), and a function to replace some old ways of doing things with the new, pure Javascript way of doing it.
this.replCompRegex = / (eq|ne|gt|lt|gte|lte) /;
this.replCompMap = { eq: "==", ne: "!=", gt: ">", lt: "<", gte: ">=", lte: "<=" };
this.replaceComps = function(body){
var res = null;
while ((res = this.replCompRegex.exec(body)) != null){
body = body.replace(res[1], this.replCompMap[res[1]]);
}
return body;
}