diff --git a/Rakefile b/Rakefile index 987544c3..22fc19a1 100644 --- a/Rakefile +++ b/Rakefile @@ -20,11 +20,11 @@ def remove_exports(string) match ? match[1] : string end -minimal_deps = %w(parser compiler ast visitor runtime utils).map do |file| +minimal_deps = %w(parser compiler ast visitor runtime utils vm).map do |file| "lib/handlebars/#{file}.js" end -debug_deps = %w(parser compiler ast visitor printer runtime utils).map do |file| +debug_deps = %w(parser compiler ast visitor 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..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; }; @@ -64,8 +67,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/compiler.js b/lib/handlebars/compiler.js index f892a509..f6cc5118 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; @@ -47,16 +47,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) { + for(var i=0, j=context.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/lib/handlebars/vm.js b/lib/handlebars/vm.js new file mode 100644 index 00000000..ecb67a41 --- /dev/null +++ b/lib/handlebars/vm.js @@ -0,0 +1,587 @@ +var Handlebars = {}; +Handlebars.parse = require("handlebars/compiler").Handlebars.parse; + +// BEGIN(BROWSER) +Handlebars.Compiler = function() {}; +Handlebars.JavaScriptCompiler = function() {}; + +(function(Compiler, JavaScriptCompiler) { + Compiler.OPCODE_MAP = { + invokeContent: 1, + getContext: 2, + lookupWithFallback: 3, + lookup: 4, + append: 5, + invokeMustache: 6, + escape: 7, + pushString: 8, + truthyOrFallback: 9, + functionOrFallback: 10, + invokeProgram: 11, + invokePartial: 12, + push: 13, + invokeInverse: 14 + }; + + Compiler.MULTI_PARAM_OPCODES = { + invokeContent: 1, + getContext: 1, + lookupWithFallback: 1, + lookup: 1, + invokeMustache: 2, + pushString: 1, + truthyOrFallback: 1, + functionOrFallback: 1, + invokeProgram: 2, + invokePartial: 1, + push: 1, + invokeInverse: 1 + }; + + Compiler.DISASSEMBLE_MAP = {} + + for(prop in Compiler.OPCODE_MAP) { + var value = Compiler.OPCODE_MAP[prop]; + Compiler.DISASSEMBLE_MAP[value] = prop; + } + + Compiler.multiParamSize = function(code) { + return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]]; + }; + + Compiler.prototype = { + disassemble: function() { + var opcodes = this.opcodes, opcode, nextCode; + var out = [], str, name, value; + + for(var i=0, l=opcodes.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') + '"'; + } + } + + 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"}, '&\"\\<>', "expressions with 3 handlebars aren't escaped"); - shouldCompileTo("{{awesome}}", {awesome: "&\"\\<>"}, '&\"\\\\<>', + shouldCompileTo("{{awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', "by default expressions should be escaped"); shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', @@ -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"); }); @@ -452,7 +466,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 9ddde768..b784fe9a 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 @@ -75,6 +79,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