I'm an IDIOT!!

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; }
blog comments powered by Disqus