diff --git a/lib/handlebars.js b/lib/handlebars.js index c91a26b0..ea9d6a06 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -2,9 +2,7 @@ Handlebars = { compile: function(string) { var compiler = new Handlebars.Compiler(string); compiler.compile(); - var result = compiler.fn; - console.log(result); - return new Function("context", "fallback", "fallback = fallback || {}; var stack = [];" + result); + return new Function("context", "fallback", "fallback = fallback || {}; var stack = [];" + compiler.fn); }, isFunction: function(fn) { @@ -175,7 +173,9 @@ Handlebars.helperMissing = function(object, fn) { ret = ret + fn(object[i]); } return ret; - } + } else { + return fn(object); + } }; Handlebars.Compiler.prototype = { @@ -243,11 +243,10 @@ Handlebars.Compiler.prototype = { // each function made internally needs a unique IDs. These are locals, so they // don't need to be globally unique, just per compiler var fnId = "fn" + this.pointer.toString(); - this.fn += "var proxy = Handlebars.buildContext(context, stack);"; this.fn += "var " + fnId + " = function(context) {" + result + "}; "; this.fn += "lookup = " + this.lookupFor(mustache) + "; "; - this.fn += "if(Handlebars.isFunction(lookup) && Handlebars.isEmpty(lookup())) out = out + " + fnId + "(proxy);"; - this.fn += "else if (Handlebars.isEmpty(lookup)) out = out + " + fnId + "(proxy);"; + this.fn += "if(Handlebars.isFunction(lookup) && Handlebars.isEmpty(lookup())) out = out + " + fnId + "(context);"; + this.fn += "else if (Handlebars.isEmpty(lookup)) out = out + " + fnId + "(context);"; this.openBlock = false; this.inverted = false; @@ -300,7 +299,7 @@ Handlebars.Compiler.prototype = { this.fn += "var " + fnId + " = function(context) {" + result + "}; "; this.fn += "lookup = " + this.lookupFor(mustache) + "; "; this.fn += "if(Handlebars.isFunction(lookup)) out = out + lookup.call(proxy, " + fnId + "); "; - this.fn += "else if(typeof lookup !== 'undefined') out = out + Handlebars.helperMissing.call(proxy, lookup, " + fnId + "); "; + this.fn += "else if(typeof lookup !== 'undefined') { out = out + Handlebars.helperMissing.call(proxy, lookup, " + fnId + "); }"; if (compiler.continueInverted) { var invertedCompiler = this.compileToEndOfBlock(mustache); @@ -317,8 +316,9 @@ Handlebars.Compiler.prototype = { addPartial: function(mustache, param) { // either used a cached copy of the partial or compile a new one this.fn += "if (typeof fallback['partials'] === 'undefined' || typeof fallback['partials']['" + mustache + "'] === 'undefined') throw new Handlebars.Exception('Attempted to render undefined partial: " + mustache + "');"; - this.fn += "if (!Handlebars.isFunction(fallback['partials']['" + mustache + "'])) fallback['partials']['" + mustache + "'] = Handlebars.compile(fallback['partials']['" + mustache + "']);"; - this.fn += "out = out + fallback['partials']['" + mustache + "'](" + param + ", fallback);"; + this.fn += "if (typeof fallback['compiledPartials'] === 'undefined') fallback['compiledPartials'] = [];"; + this.fn += "if (!Handlebars.isFunction(fallback['compiledPartials']['" + mustache + "'])) fallback['compiledPartials']['" + mustache + "'] = Handlebars.compile(fallback['partials']['" + mustache + "']);"; + this.fn += "out = out + fallback['compiledPartials']['" + mustache + "'](" + param + ", fallback);"; }, parseMustache: function() { diff --git a/test/mustache-0.3.0.js b/test/mustache-0.3.0.js new file mode 100755 index 00000000..0996227a --- /dev/null +++ b/test/mustache-0.3.0.js @@ -0,0 +1,324 @@ +/* + mustache.js — Logic-less templates in JavaScript + + See http://mustache.github.com/ for more info. +*/ + +var Mustache = function() { + var Renderer = function() {}; + + Renderer.prototype = { + otag: "{{", + ctag: "}}", + pragmas: {}, + buffer: [], + pragmas_implemented: { + "IMPLICIT-ITERATOR": true + }, + context: {}, + + render: function(template, context, partials, in_recursion) { + // reset buffer & set context + if(!in_recursion) { + this.context = context; + this.buffer = []; // TODO: make this non-lazy + } + + // fail fast + if(!this.includes("", template)) { + if(in_recursion) { + return template; + } else { + this.send(template); + return; + } + } + + template = this.render_pragmas(template); + var html = this.render_section(template, context, partials); + if(in_recursion) { + return this.render_tags(html, context, partials, in_recursion); + } + + this.render_tags(html, context, partials, in_recursion); + }, + + /* + Sends parsed lines + */ + send: function(line) { + if(line != "") { + this.buffer.push(line); + } + }, + + /* + Looks for %PRAGMAS + */ + render_pragmas: function(template) { + // no pragmas + if(!this.includes("%", template)) { + return template; + } + + var that = this; + var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + + this.ctag); + return template.replace(regex, function(match, pragma, options) { + if(!that.pragmas_implemented[pragma]) { + throw({message: + "This implementation of mustache doesn't understand the '" + + pragma + "' pragma"}); + } + that.pragmas[pragma] = {}; + if(options) { + var opts = options.split("="); + that.pragmas[pragma][opts[0]] = opts[1]; + } + return ""; + // ignore unknown pragmas silently + }); + }, + + /* + Tries to find a partial in the curent scope and render it + */ + render_partial: function(name, context, partials) { + name = this.trim(name); + if(!partials || partials[name] === undefined) { + throw({message: "unknown_partial '" + name + "'"}); + } + if(typeof(context[name]) != "object") { + return this.render(partials[name], context, partials, true); + } + return this.render(partials[name], context[name], partials, true); + }, + + /* + Renders inverted (^) and normal (#) sections + */ + render_section: function(template, context, partials) { + if(!this.includes("#", template) && !this.includes("^", template)) { + return template; + } + + var that = this; + // CSW - Added "+?" so it finds the tighest bound, not the widest + var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + + "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + + "\\s*", "mg"); + + // for each {{#foo}}{{/foo}} section do... + return template.replace(regex, function(match, type, name, content) { + var value = that.find(name, context); + if(type == "^") { // inverted section + if(!value || that.is_array(value) && value.length === 0) { + // false or empty list, render it + return that.render(content, context, partials, true); + } else { + return ""; + } + } else if(type == "#") { // normal section + if(that.is_array(value)) { // Enumerable, Let's loop! + return that.map(value, function(row) { + return that.render(content, that.create_context(row), + partials, true); + }).join(""); + } else if(that.is_object(value)) { // Object, Use it as subcontext! + return that.render(content, that.create_context(value), + partials, true); + } else if(typeof value === "function") { + // higher order section + return value.call(context, content, function(text) { + return that.render(text, context, partials, true); + }); + } else if(value) { // boolean section + return that.render(content, context, partials, true); + } else { + return ""; + } + } + }); + }, + + /* + Replace {{foo}} and friends with values from our view + */ + render_tags: function(template, context, partials, in_recursion) { + // tit for tat + var that = this; + + var new_regex = function() { + return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + + that.ctag + "+", "g"); + }; + + var regex = new_regex(); + var tag_replace_callback = function(match, operator, name) { + switch(operator) { + case "!": // ignore comments + return ""; + case "=": // set new delimiters, rebuild the replace regexp + that.set_delimiters(name); + regex = new_regex(); + return ""; + case ">": // render partial + return that.render_partial(name, context, partials); + case "{": // the triple mustache is unescaped + return that.find(name, context); + default: // escape the value + return that.escape(that.find(name, context)); + } + }; + var lines = template.split("\n"); + for(var i = 0; i < lines.length; i++) { + lines[i] = lines[i].replace(regex, tag_replace_callback, this); + if(!in_recursion) { + this.send(lines[i]); + } + } + + if(in_recursion) { + return lines.join("\n"); + } + }, + + set_delimiters: function(delimiters) { + var dels = delimiters.split(" "); + this.otag = this.escape_regex(dels[0]); + this.ctag = this.escape_regex(dels[1]); + }, + + escape_regex: function(text) { + // thank you Simon Willison + if(!arguments.callee.sRE) { + var specials = [ + '/', '.', '*', '+', '?', '|', + '(', ')', '[', ']', '{', '}', '\\' + ]; + arguments.callee.sRE = new RegExp( + '(\\' + specials.join('|\\') + ')', 'g' + ); + } + return text.replace(arguments.callee.sRE, '\\$1'); + }, + + /* + find `name` in current `context`. That is find me a value + from the view object + */ + find: function(name, context) { + name = this.trim(name); + + // Checks whether a value is thruthy or false or 0 + function is_kinda_truthy(bool) { + return bool === false || bool === 0 || bool; + } + + var value; + if(is_kinda_truthy(context[name])) { + value = context[name]; + } else if(is_kinda_truthy(this.context[name])) { + value = this.context[name]; + } + + if(typeof value === "function") { + return value.apply(context); + } + if(value !== undefined) { + return value; + } + // silently ignore unkown variables + return ""; + }, + + // Utility methods + + /* includes tag */ + includes: function(needle, haystack) { + return haystack.indexOf(this.otag + needle) != -1; + }, + + /* + Does away with nasty characters + */ + escape: function(s) { + s = String(s === null ? "" : s); + return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) { + switch(s) { + case "&": return "&"; + case "\\": return "\\\\"; + case '"': return '\"'; + case "<": return "<"; + case ">": return ">"; + default: return s; + } + }); + }, + + // by @langalex, support for arrays of strings + create_context: function(_context) { + if(this.is_object(_context)) { + return _context; + } else { + var iterator = "."; + if(this.pragmas["IMPLICIT-ITERATOR"]) { + iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; + } + var ctx = {}; + ctx[iterator] = _context; + return ctx; + } + }, + + is_object: function(a) { + return a && typeof a == "object"; + }, + + is_array: function(a) { + return Object.prototype.toString.call(a) === '[object Array]'; + }, + + /* + Gets rid of leading and trailing whitespace + */ + trim: function(s) { + return s.replace(/^\s*|\s*$/g, ""); + }, + + /* + Why, why, why? Because IE. Cry, cry cry. + */ + map: function(array, fn) { + if (typeof array.map == "function") { + return array.map(fn); + } else { + var r = []; + var l = array.length; + for(var i = 0; i < l; i++) { + r.push(fn(array[i])); + } + return r; + } + } + }; + + return({ + name: "mustache.js", + version: "0.3.0", + + /* + Turns a template and view into HTML + */ + to_html: function(template, view, partials, send_fun) { + var renderer = new Renderer(); + if(send_fun) { + renderer.send = send_fun; + } + renderer.render(template, view, partials); + if(!send_fun) { + return renderer.buffer.join("\n"); + } + } + }); +}(); diff --git a/test/perf.html b/test/perf.html new file mode 100644 index 00000000..e74fd19c --- /dev/null +++ b/test/perf.html @@ -0,0 +1,19 @@ + + + + + + + + + +

Performance Testing

+

+

+
    + + + + diff --git a/test/perf.js b/test/perf.js new file mode 100644 index 00000000..963c11f7 --- /dev/null +++ b/test/perf.js @@ -0,0 +1,46 @@ +function perfTest(test) { + var testStart = new Date().getTime(); + + // run the test 1000 times so we can get a happy average + for (var i = 0; i < 1000; i++) { + test(); + } + + var testEnd = new Date().getTime(); + return (testEnd-testStart)/1000; +} + +function perfCompare(test1, test2) { + var result1 = perfTest(test1); + var result2 = perfTest(test2); + + console.log("Result 1: " + result1 + ", Result 2: " + result2 + ", Difference: " + + (result1 - result2)); +} + + +// set up the templates +var tmpl = "This is the story of guys who work on a project\n" + + "called {{project}}. Their names were {{#people}}{{firstName}} and {{/people}}\n" + + "they both enjoyed working on {{project}}.\n\n" + + "{{#people}}\n" + + "{{>personPet}}\n" + + "{{/people}}"; +var partials = { + personPet: "{{firstName}} {{lastName}} {{#pet}} owned a {{species}}. Its name was {{name}}.{{/pet}}{{^pet}}didn't own a pet.{{/pet}}" +}; +var data = { + project: "Handlebars", + people: [ + { firstName: "Yehuda", lastName: "Katz" }, + { firstName: "Alan", lastName: "Johnson", pet: { species: "cat", name: "Luke" } } + ] +} + +var test1 = function() { + Handlebars.compile(tmpl)(data, {partials: partials}); +} +var test2 = function() { + Mustache.to_html(tmpl, data, partials); +} +perfCompare(test1, test2);