var regStr = "{{(.+)}}"; var localRegex = new RegExp(regStr); var globalRegex = new RegExp(regStr, "g"); function expectType(type, val) { if (typeof val !== type) throw new TypeError("Expected "+type+", got "+typeof val); } function expectInstance(obj, val) { if (!(val instanceof obj)) { var regex = /function (.+)\(/; var expectedName = obj.toString().match(regex)[1]; try { var realName = val.constructor.toString().match(regex)[1]; } catch (err) { var realName = "undefined"; } throw new TypeError("Expected "+expectedName+", got "+realName); } } function hasMultipleStatements(str) { return (str.indexOf(";") !== -1) } function Func(args) { return Function.apply(this, args); } Func.prototype = Function.prototype; function Templish(str, vals) { expectType("string", str); expectInstance(Array, vals); str = str.replace(/\s+/g, " ").replace(/}}/g, "}}\n"); this.string = str; this.vals = vals; this.subs = []; var matches = str.match(globalRegex); if (!matches) return; matches.forEach(function(m) { var str = m.match(localRegex)[1]; //Add user defined arguments var args = []; vals.forEach(function(v) { args.push(v); }); //We also need a callback function, for async things args.push("cb"); //We want to automatically return if there's only one statement if (hasMultipleStatements(str)) args.push(str); else args.push("return ("+str+")"); this.subs.push({ func: new Func(args), match: m+"\n" }); }.bind(this)); } Templish.prototype.exec = function(vals, cb) { expectInstance(Object, vals); expectType("function", cb); var str = this.string; //Prepare generic arguments var args = []; for (var i in vals) { var index = this.vals.indexOf(i); if (index === -1) throw new Error("Error: argument "+i+" doesn't exist"); args[index] = vals[i]; } var numArgs = args.length; //Keep track of how many callbacks we're waiting for var cbsRemaining = this.subs.length; //Replace this.subs.forEach(function(sub, i) { //Create a callback function for each substitute args[numArgs] = function(err, res) { if (err) throw err; str = str.replace(sub.match, res); //If this is the last callback to be executed, //call back with the resulting string if (cbsRemaining <= 0) cb(null, str); cbsRemaining -= 1; } var ret = sub.func.apply(null, args); //If the substitute's function returns anything, we use that directly, //but if it doesn't, we wait for a callback if (ret !== undefined) { str = str.replace(sub.match, ret); if (cbsRemaining <= 0) cb(str); cbsRemaining -= 1; } }); //If there's no asynchronous functions to wait for, //just call back immediately if (cbsRemaining === 0) return cb(null, str); } module.exports = Templish;