From b418ef6eb23aa671b814a2c41163d043552b0ead Mon Sep 17 00:00:00 2001 From: wycats Date: Sat, 18 Dec 2010 23:27:34 -0800 Subject: [PATCH] Add optimized compiled version of handlebars, which should be significantly faster. Use Handlebars.VM.compile instead of Handlebars.compile to use the optimized version. Major TODOS: * clean up a bunch of code duplication in the compiler * reorganize the compiler * add support for debug symbols which would make it possible to provide information about what part of the source caused a runtime error. --- lib/handlebars/ast.js | 3 + lib/handlebars/compiler.js | 23 +- lib/handlebars/runtime.js | 2 +- lib/handlebars/vm.js | 571 +++++++++++++++++++++++++++++++++++++ spec/qunit_spec.js | 66 +++-- 5 files changed, 632 insertions(+), 33 deletions(-) create mode 100644 lib/handlebars/vm.js diff --git a/lib/handlebars/ast.js b/lib/handlebars/ast.js index f42e47c4..6c51b381 100644 --- a/lib/handlebars/ast.js +++ b/lib/handlebars/ast.js @@ -21,6 +21,9 @@ Handlebars.Exception = require("handlebars/utils").Exception; Handlebars.AST.PartialNode = function(id, context) { this.type = "partial"; + + // TODO: disallow complex IDs + this.id = id; this.context = context; }; diff --git a/lib/handlebars/compiler.js b/lib/handlebars/compiler.js index 33e1fdf2..96ae36ab 100644 --- a/lib/handlebars/compiler.js +++ b/lib/handlebars/compiler.js @@ -1,7 +1,7 @@ var handlebars = require("handlebars/parser").parser; // BEGIN(BROWSER) -Handlebars = {}; +var Handlebars = {}; Handlebars.Parser = handlebars; @@ -48,16 +48,27 @@ Handlebars.registerPartial = function(name, str) { this.partials[name] = str; }; -Handlebars.registerHelper('blockHelperMissing', function(context, fn) { +Handlebars.registerHelper('blockHelperMissing', function(context, fn, inverse) { + inverse = inverse || function() {}; + var ret = ""; + var type = Object.prototype.toString.call(context); + + if(type === "[object Function]") { + context = context(); + } if(context === true) { return fn(this); } else if(context === false || context == null) { - return ""; - } else if(Object.prototype.toString.call(context) === "[object Array]") { - for(var i=0, j=context.length; i 0) { + for(var i=0, j=context.length; i 0) { + this.source[this.locals] = "var " + this.stackVars.join(", ") + ";"; + } + + this.source.push("return buffer;") + + var params = ["context", "helpers", "partials"]; + + for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } + return "stack" + this.stackSlot; + }, + + popStack: function() { + return "stack" + this.stackSlot--; + }, + + topStack: function() { + return "stack" + this.stackSlot; + }, + + quotedString: function(str) { + return '"' + str + .replace(/\\/, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + '"'; + } + } + +})(Handlebars.Compiler, Handlebars.JavaScriptCompiler) + +Handlebars.VM = { + programWithDepth: function(fn, helpers, partials, depth) { + var args = [].slice.call(arguments, 1); + return function(context) { + return fn.apply(this, [context].concat(args)); + } + }, + program: function(fn, helpers, partials) { + return function(context) { + return fn(context, helpers, partials); + } + }, + noop: function() {}, + compile: function(string) { + var ast = Handlebars.parse(string); + var environment = new Handlebars.Compiler().compile(ast); + return new Handlebars.JavaScriptCompiler().compile(environment); + }, + invokePartial: function(partial, name, context, helpers, partials) { + if(partial instanceof Function) { + return partial(context, helpers, partials) + } else { + partials[name] = Handlebars.VM.compile(partial); + return partials[name](context, helpers, partials); + } + } +}; +// END(BROWSER) + +exports.Compiler = Handlebars.Compiler; +exports.JavaScriptCompiler = Handlebars.JavaScriptCompiler; +exports.VM = Handlebars.VM; diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js index 146747bc..c63edb01 100644 --- a/spec/qunit_spec.js +++ b/spec/qunit_spec.js @@ -43,7 +43,7 @@ test("compiling with a basic context", function() { }); test("comments", function() { - shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", + shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", "comments are ignored"); }); @@ -90,7 +90,7 @@ test("escaping expressions", function() { test("functions returning safestrings shouldn't be escaped", function() { var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }}; - shouldCompileTo("{{awesome}}", hash, '&\"\\<>', + shouldCompileTo("{{awesome}}", hash, '&\"\\<>', "functions returning safestrings aren't escaped"); }); @@ -100,7 +100,7 @@ test("functions", function() { }); test("functions with context argument", function() { - shouldCompileTo("{{awesome frank}}", + shouldCompileTo("{{awesome frank}}", {awesome: function(context) { return context; }, frank: "Frank"}, "Frank", "functions are called with context arguments"); @@ -121,7 +121,7 @@ test("--- TODO --- bad idea nested paths", function() { var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; shouldThrow(function() { Handlebars.compile("{{#goodbyes}}{{../name/../name}}{{/goodbyes}}")(hash); - }, Handlebars.Exception, + }, Handlebars.Exception, "Cannot jump (..) into previous context after moving into a context."); var string = "{{#goodbyes}}{{.././world}} {{/goodbyes}}"; @@ -140,8 +140,8 @@ test("complex but empty paths", function() { test("this keyword in paths", function() { var string = "{{#goodbyes}}{{this}}{{/goodbyes}}"; var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; - shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE", - "This keyword in paths evaluates to current context"); + shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE", + "This keyword in paths evaluates to current context"); string = "{{#hellos}}{{this/text}}{{/hellos}}" hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; @@ -161,7 +161,7 @@ test("inverted section with false value", function() { var hash = {goodbyes: false}; shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false."); }); - + test("inverted section with empty set", function() { var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; var hash = {goodbyes: []}; @@ -218,10 +218,10 @@ test("block with complex lookup", function() { }); test("helper with complex lookup", function() { - var string = "{{#goodbyes}}{{{link}}}{{/goodbyes}}" + var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}" var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; - var fallback = {link: function() { - return "" + this.text + "" + var fallback = {link: function(prefix) { + return "" + this.text + "" }}; shouldCompileTo(string, [hash, fallback], "Goodbye") }); @@ -229,11 +229,11 @@ test("helper with complex lookup", function() { test("helper block with complex lookup expression", function() { var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}" var hash = {name: "Alan"}; - var fallback = {goodbyes: function(context, fn) { + var fallback = {goodbyes: function(fn) { var out = ""; var byes = ["Goodbye", "goodbye", "GOODBYE"]; for (var i = 0,j = byes.length; i < j; i++) { - out += byes[i] + " " + fn(context) + "! "; + out += byes[i] + " " + fn(this) + "! "; } return out; }}; @@ -241,10 +241,10 @@ test("helper block with complex lookup expression", function() { }); test("helper with complex lookup and nested template", function() { - var string = "{{#goodbyes}}{{#link}}{{text}}{{/link}}{{/goodbyes}}"; + var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; - var fallback = {link: function (context, fn) { - return "" + fn(this) + ""; + var fallback = {link: function (prefix, fn) { + return "" + fn(this) + ""; }}; shouldCompileTo(string, [hash, fallback], "Goodbye") }); @@ -272,10 +272,10 @@ test("block helper staying in the same context", function() { equal(result, "

Yehuda

"); }); -test("block helper should have wrapped context in this", function() { +test("block helper should have context in this", function() { var source = "
    {{#people}}
  • {{#link}}{{name}}{{/link}}
  • {{/people}}
"; - var link = function(context, fn) { - return '' + fn(this) + ''; + var link = function(fn) { + return '' + fn(this) + ''; }; var data = { "people": [ { "name": "Alan", "id": 1 }, @@ -315,33 +315,47 @@ test("nested block helpers", function() { }); test("block inverted sections", function() { - shouldCompileTo("{{#people}}{{name}}{{^}}{{../none}}{{/people}}", {none: "No people"}, + shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, + "No people"); +}); + +test("block inverted sections with empty arrays", function() { + shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, "No people"); }); test("block helper inverted sections", function() { var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}" - var list = function(context, fn) { + var list = function(context, fn, inverse) { if (context.length > 0) { var out = "
    "; for(var i = 0,j=context.length; i < j; i++) { - out += "
  • "; + out += "
  • "; out += fn(context[i]); out += "
  • "; } out += "
"; return out; + } else { + return "

" + inverse(this) + "

"; } }; - list.not = function(context, fn) { - return "

" + fn(context, this) + "

"; - }; var hash = {list: list, people: [{name: "Alan"}, {name: "Yehuda"}]}; + var empty = {list: list, people: []}; + var rootMessage = { + list: function(context, fn, inverse) { if(context.length === 0) { return "

" + inverse(this) + "

"; } }, + people: [], + message: "Nobody's here" + } + + var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}"; // the meaning here may be kind of hard to catch, but list.not is always called, // so we should see the output of both - shouldCompileTo(string, hash, "
  • Alan
  • Yehuda

Nobody's here

", "Not is called when block inverted section is encountered."); + shouldCompileTo(string, hash, "
  • Alan
  • Yehuda
", "an inverse wrapper is passed in as a new context"); + shouldCompileTo(string, empty, "

Nobody's here

", "an inverse wrapper can be optionally called"); + shouldCompileTo(messageString, rootMessage, "

Nobody's here

", "the context of an inverse is the parent of the block"); }); module("fallback hash"); @@ -390,7 +404,7 @@ test("partial in a partial", function() { test("rendering undefined partial throws an exception", function() { shouldThrow(function() { - var template = Handlebars.compile("{{> whatever}}"); + var template = Handlebars.compile("{{> whatever}}"); template(); }, Handlebars.Exception, "Should throw exception"); });