From e15d675a64c7bf2c28fc52fb655705462ed60cbb Mon Sep 17 00:00:00 2001 From: wycats Date: Wed, 15 Dec 2010 23:42:53 -0800 Subject: [PATCH 1/4] Everything is working now on the new VM except for partials and inverse sections --- Rakefile | 4 +-- lib/handlebars.js | 19 +++++----- lib/handlebars/ast.js | 5 +-- lib/handlebars/runtime.js | 2 +- lib/handlebars/utils.js | 76 +++++++++++++++++++-------------------- spec/qunit_spec.js | 6 ++-- spec/spec_helper.rb | 7 +++- 7 files changed, 64 insertions(+), 55 deletions(-) diff --git a/Rakefile b/Rakefile index f15f6190..0df623bc 100644 --- a/Rakefile +++ b/Rakefile @@ -20,11 +20,11 @@ def remove_exports(string) match ? match[1] : string end -minimal_deps = %w(parser compiler ast jison_ext handlebars_lexer runtime utils).map do |file| +minimal_deps = %w(parser compiler ast jison_ext handlebars_lexer runtime utils vm).map do |file| "lib/handlebars/#{file}.js" end -debug_deps = %w(parser compiler ast jison_ext handlebars_lexer printer runtime utils).map do |file| +debug_deps = %w(parser compiler ast jison_ext handlebars_lexer printer runtime utils vm).map do |file| "lib/handlebars/#{file}.js" end diff --git a/lib/handlebars.js b/lib/handlebars.js index b6cbbc2d..0b867409 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -1,13 +1,16 @@ var Handlebars = require("handlebars/compiler").Handlebars; -Handlebars.AST = require("handlebars/ast").AST; -Handlebars.HandlebarsLexer = require("handlebars/handlebars_lexer").Lexer; -Handlebars.PrintVisitor = require("handlebars/printer").PrintVisitor; -Handlebars.Runtime = require("handlebars/runtime").Runtime; -Handlebars.Context = require("Handlebars/runtime").Context; -Handlebars.Utils = require("handlebars/utils").Utils; -Handlebars.SafeString = require("handlebars/utils").SafeString; -Handlebars.Exception = require("handlebars/utils").Exception; +Handlebars.AST = require("handlebars/ast").AST; +Handlebars.HandlebarsLexer = require("handlebars/handlebars_lexer").Lexer; +Handlebars.PrintVisitor = require("handlebars/printer").PrintVisitor; +Handlebars.Runtime = require("handlebars/runtime").Runtime; +Handlebars.Context = require("Handlebars/runtime").Context; +Handlebars.Utils = require("handlebars/utils").Utils; +Handlebars.SafeString = require("handlebars/utils").SafeString; +Handlebars.Exception = require("handlebars/utils").Exception; +Handlebars.Compiler = require("handlebars/vm").Compiler; +Handlebars.JavaScriptCompiler = require("handlebars/vm").JavaScriptCompiler; +Handlebars.VM = require("handlebars/vm").VM; // BEGIN(BROWSER) diff --git a/lib/handlebars/ast.js b/lib/handlebars/ast.js index 932e52ca..f42e47c4 100644 --- a/lib/handlebars/ast.js +++ b/lib/handlebars/ast.js @@ -64,8 +64,9 @@ Handlebars.Exception = require("handlebars/utils").Exception; else { dig.push(part); } } - this.parts = dig; - this.depth = depth; + this.parts = dig; + this.depth = depth; + this.isSimple = (dig.length === 1) && (depth === 0) }; Handlebars.AST.StringNode = function(string) { diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index d78f0d81..07e9485b 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -104,7 +104,7 @@ Handlebars.Runtime.prototype = { mustache: function(mustache) { var idObj = this.ID(mustache.id); - var params = mustache.params; + var params = mustache.params.slice(0); var buf; for(var i=0, l=params.length; i": ">", + }; - return string.toString().replace(/&(?!\w+;)|["\\<>]/g, function(str) { - switch(str) { - case "&": - return "&"; - case '"': - return "\""; - case "\\": - return "\\\\"; - case "<": - return "<"; - case ">": - return ">"; - default: - return str; + var badChars = /&(?!\w+;)|[<>]/g; + var possible = /[&<>]/ + + var escapeChar = function(chr) { + return escape[chr] || "&" + }; + + Handlebars.Utils = { + escapeExpression: function(string) { + // don't escape SafeStrings, since they're already safe + if (string instanceof Handlebars.SafeString) { + return string.toString(); + } else if (string === null) { + string = ""; + } + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); + }, + + isEmpty: function(value) { + if (typeof value === "undefined") { + return true; + } else if (value === null) { + return true; + } else if (value === false) { + return true; + } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { + return true; + } else { + return false; } - }); - }, - isEmpty: function(value) { - if (typeof value === "undefined") { - return true; - } else if (value === null) { - return true; - } else if (value === false) { - return true; - } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { - return true; - } else { - return false; } - } -}; + }; +})(); // END(BROWSER) exports.Utils = Handlebars.Utils; diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js index 5ccc4578..146747bc 100644 --- a/spec/qunit_spec.js +++ b/spec/qunit_spec.js @@ -7,7 +7,7 @@ Handlebars.registerHelper('helperMissing', function(helper, context) { }); var shouldCompileTo = function(string, hash, expected, message) { - var template = Handlebars.compile(string); + var template = Handlebars.VM.compile(string); if(Object.prototype.toString.call(hash) === "[object Array]") { if(hash[1]) { for(var prop in Handlebars.helpers) { @@ -80,7 +80,7 @@ test("escaping expressions", function() { shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>', "expressions with 3 handlebars aren't escaped"); - shouldCompileTo("{{awesome}}", {awesome: "&\"\\<>"}, '&\"\\\\<>', + shouldCompileTo("{{awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', "by default expressions should be escaped"); shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', @@ -452,7 +452,7 @@ test("block multi-params work", function() { var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}'; var hash = {cruel: "cruel", world: "world"} var fallback = {goodbye: function(cruel, world, fn) { - return fn({greeting: "Goodbye", adj: "cruel", noun: "world"}); + return fn({greeting: "Goodbye", adj: cruel, noun: world}); }} shouldCompileTo(string, [hash, fallback], "Message: Goodbye cruel world", "block helpers with multiple params"); }) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5394cb00..289912c5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -54,10 +54,14 @@ module Handlebars context["exports"] = nil context["p"] = proc do |val| - p val + p val if ENV["DEBUG_JS"] end context["puts"] = proc do |val| + puts val if ENV["DEBUG_JS"] + end + + context["puts_node"] = proc do |val| puts context["Handlebars"]["PrintVisitor"].new.accept(val) puts end @@ -76,6 +80,7 @@ module Handlebars Handlebars::Spec.js_load('lib/handlebars/printer.js') Handlebars::Spec.js_load('lib/handlebars/runtime.js') Handlebars::Spec.js_load('lib/handlebars/utils.js') + Handlebars::Spec.js_load('lib/Handlebars/vm.js') Handlebars::Spec.js_load('lib/handlebars.js') end end From b418ef6eb23aa671b814a2c41163d043552b0ead Mon Sep 17 00:00:00 2001 From: wycats Date: Sat, 18 Dec 2010 23:27:34 -0800 Subject: [PATCH 2/4] 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"); }); From f4ae0c27fecddba3b627a3a7d4a63b386f756120 Mon Sep 17 00:00:00 2001 From: wycats Date: Sun, 19 Dec 2010 00:06:30 -0800 Subject: [PATCH 3/4] Allow reserved words to be used without slowing down most operations in browsers with faster foo.bar vs. foo['bar']. --- lib/handlebars/vm.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/handlebars/vm.js b/lib/handlebars/vm.js index fe3e502e..ecb67a41 100644 --- a/lib/handlebars/vm.js +++ b/lib/handlebars/vm.js @@ -1,8 +1,6 @@ var Handlebars = {}; Handlebars.parse = require("handlebars/compiler").Handlebars.parse; -if(typeof puts === "undefined") { var puts = function() {}; }; - // BEGIN(BROWSER) Handlebars.Compiler = function() {}; Handlebars.JavaScriptCompiler = function() {}; @@ -252,8 +250,8 @@ Handlebars.JavaScriptCompiler = function() {}; this.compileChildren(environment); - puts(environment.disassemble()); - puts("") + //puts(environment.disassemble()); + //puts("") var opcodes = environment.opcodes; var opcode, name; @@ -317,8 +315,8 @@ Handlebars.JavaScriptCompiler = function() {}; var fn = Function.apply(this, params); fn.displayName = "Handlebars.js" - puts(fn.toString()) - puts("") + //puts(fn.toString()) + //puts("") container.render = fn; @@ -355,11 +353,19 @@ Handlebars.JavaScriptCompiler = function() {}; } }, + nameLookup: function(parent, name) { + if(JavaScriptCompiler.RESERVED_WORDS[name]) { + return parent + "['" + name + "']"; + } else { + return parent + "." + name; + } + }, + lookupWithFallback: function(name) { if(name) { - this.pushStack("currentContext." + name); + this.pushStack(this.nameLookup('currentContext', name)); var topStack = this.topStack(); - this.source.push("if(" + topStack + " == null) { " + topStack + " = helpers." + name + "; }"); + this.source.push("if(" + topStack + " == null) { " + topStack + " = " + this.nameLookup('helpers', name) + "; }"); } else { this.pushStack("currentContext"); } @@ -367,7 +373,7 @@ Handlebars.JavaScriptCompiler = function() {}; lookup: function(name) { var topStack = this.topStack(); - this.source.push(topStack + " = " + topStack + "." + name + ";"); + this.source.push(topStack + " = " + this.nameLookup(topStack, name) + ";"); }, pushString: function(string) { @@ -482,7 +488,7 @@ Handlebars.JavaScriptCompiler = function() {}; }, invokePartial: function(context) { - this.pushStack("Handlebars.VM.invokePartial(partials." + context + ", '" + context + "', " + this.popStack() + ", helpers, partials);"); + this.pushStack("Handlebars.VM.invokePartial(" + this.nameLookup('partials', context) + ", '" + context + "', " + this.popStack() + ", helpers, partials);"); }, escape: function() { @@ -535,6 +541,16 @@ Handlebars.JavaScriptCompiler = function() {}; } } + var reservedWords = ("break case catch continue default delete do else finally " + + "for function if in instanceof new return switch this throw " + + "try typeof var void while with null true false").split(" "); + + compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; + + for(var i=0, l=reservedWords.length; i Date: Sun, 19 Dec 2010 00:06:55 -0800 Subject: [PATCH 4/4] Add #each and #if built-in helpers for more readable iteration and conditionals --- lib/handlebars/compiler.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/handlebars/compiler.js b/lib/handlebars/compiler.js index 96ae36ab..2ffd8296 100644 --- a/lib/handlebars/compiler.js +++ b/lib/handlebars/compiler.js @@ -77,6 +77,28 @@ Handlebars.registerHelper('blockHelperMissing', function(context, fn, inverse) { }, function(context, fn) { return fn(context) }); + +Handlebars.registerHelper('each', function(context, fn, inverse) { + var ret = ""; + + if(context.length > 0) { + for(var i=0, j=context.length; i