diff options
-rw-r--r-- | .jshintrc | 5 | ||||
-rw-r--r-- | Rakefile | 8 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | spec/acceptance_spec.rb | 101 | ||||
-rw-r--r-- | spec/artifacts/example_1.handlebars (renamed from spec/example_1.handlebars) | 0 | ||||
-rw-r--r-- | spec/artifacts/example_2.hbs (renamed from spec/example_2.hbs) | 0 | ||||
-rw-r--r-- | spec/basic.js | 162 | ||||
-rw-r--r-- | spec/blocks.js | 86 | ||||
-rw-r--r-- | spec/builtins.js | 125 | ||||
-rw-r--r-- | spec/data.js | 240 | ||||
-rw-r--r-- | spec/env/browser.js | 13 | ||||
-rw-r--r-- | spec/env/common.js | 28 | ||||
-rw-r--r-- | spec/env/node.js | 13 | ||||
-rw-r--r-- | spec/env/runner.js | 40 | ||||
-rw-r--r-- | spec/env/runtime.js | 15 | ||||
-rw-r--r-- | spec/helpers.js | 532 | ||||
-rw-r--r-- | spec/parser.js | 8 | ||||
-rw-r--r-- | spec/partials.js | 119 | ||||
-rw-r--r-- | spec/qunit_spec.js | 1657 | ||||
-rw-r--r-- | spec/regressions.js | 119 | ||||
-rw-r--r-- | spec/require.js | 23 | ||||
-rw-r--r-- | spec/source-map.js | 18 | ||||
-rw-r--r-- | spec/spec_helper.rb | 132 | ||||
-rw-r--r-- | spec/string-params.js | 145 | ||||
-rw-r--r-- | spec/tokenizer.js | 7 | ||||
-rw-r--r-- | spec/utils.js | 56 |
26 files changed, 1748 insertions, 1906 deletions
@@ -24,7 +24,8 @@ "module", "describe", - "it" + "it", + "afterEach" ], "node" : true, @@ -35,7 +36,7 @@ "curly": false, "debug": false, "devel": false, - "eqeqeq": true, + "eqeqeq": false, "evil": true, "forin": false, "immed": false, @@ -28,17 +28,11 @@ task :compile => "lib/handlebars/compiler/parser.js" desc "run the spec suite" task :spec => [:release] do - rc = system "rspec -cfs spec" - fail "rspec spec failed with exit code #{$?.exitstatus}" if (rc.nil? || ! rc || $?.exitstatus != 0) -end - -desc "run the npm test suite" -task :npm_test => [:release] do rc = system "npm test" fail "npm test failed with exit code #{$?.exitstatus}" if (rc.nil? || ! rc || $?.exitstatus != 0) end -task :default => [:compile, :spec, :npm_test] +task :default => [:compile, :spec] def remove_exports(string) match = string.match(%r{^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)}m) diff --git a/package.json b/package.json index e647791..3f517c1 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "handlebars": "bin/handlebars" }, "scripts": { - "test": "node_modules/.bin/mocha spec/tokenizer.js spec/parser.js && node_modules/.bin/mocha -u qunit spec/qunit_spec.js" + "test": "node ./spec/env/runner" }, "optionalDependencies": {} } diff --git a/spec/acceptance_spec.rb b/spec/acceptance_spec.rb deleted file mode 100644 index 03b23c5..0000000 --- a/spec/acceptance_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -require "spec_helper" - -class TestContext - class TestModule - attr_reader :name, :tests - - def initialize(name) - @name = name - @tests = [] - end - end - - attr_reader :modules - - def initialize - @modules = [] - end - - def module(name) - @modules << TestModule.new(name) - end - - def test(name, function) - @modules.last.tests << [name, function] - end -end - -test_context = TestContext.new -js_context = Handlebars::Spec::CONTEXT - -Module.new do - extend Test::Unit::Assertions - - def self.js_backtrace(context) - begin - context.eval("throw") - rescue V8::JSError => e - return e.backtrace(:javascript) - end - end - - js_context["p"] = proc do |this, str| - p str - end - - js_context["ok"] = proc do |this, ok, message| - js_context["$$RSPEC1$$"] = ok - - result = js_context.eval("!!$$RSPEC1$$") - - message ||= "#{ok} was not truthy" - - unless result - backtrace = js_backtrace(js_context) - message << "\n#{backtrace.join("\n")}" - end - - assert result, message - end - - js_context["equals"] = proc do |this, first, second, message| - js_context["$$RSPEC1$$"] = first - js_context["$$RSPEC2$$"] = second - - result = js_context.eval("$$RSPEC1$$ == $$RSPEC2$$") - - additional_message = "#{first.inspect} did not == #{second.inspect}" - message = message ? "#{message} (#{additional_message})" : additional_message - - unless result - backtrace = js_backtrace(js_context) - message << "\n#{backtrace.join("\n")}" - end - - assert result, message - end - - js_context["equal"] = js_context["equals"] - - js_context["suite"] = proc do |this, name| - test_context.module(name) - end - - js_context["test"] = proc do |this, name, function| - test_context.test(name, function) - end - - local = Regexp.escape(File.expand_path(Dir.pwd)) - qunit_spec = File.expand_path("../qunit_spec.js", __FILE__) - js_context.load(qunit_spec.sub(/^#{local}\//, '')) -end - -test_context.modules.each do |mod| - describe mod.name do - mod.tests.each do |name, function| - it name do - function.call - end - end - end -end diff --git a/spec/example_1.handlebars b/spec/artifacts/example_1.handlebars index 054e96c..054e96c 100644 --- a/spec/example_1.handlebars +++ b/spec/artifacts/example_1.handlebars diff --git a/spec/example_2.hbs b/spec/artifacts/example_2.hbs index 963eab9..963eab9 100644 --- a/spec/example_2.hbs +++ b/spec/artifacts/example_2.hbs diff --git a/spec/basic.js b/spec/basic.js new file mode 100644 index 0000000..a46636c --- /dev/null +++ b/spec/basic.js @@ -0,0 +1,162 @@ +describe("basic context", function() { + it("most basic", function() { + shouldCompileTo("{{foo}}", { foo: "foo" }, "foo"); + }); + + it("escaping", function() { + shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}"); + shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food"); + shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food"); + }); + + it("compiling with a basic context", function() { + shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", + "It works if all the required keys are provided"); + }); + + it("comments", function() { + shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", + {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", + "comments are ignored"); + }); + + it("boolean", function() { + var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!"; + shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", + "booleans show the contents when true"); + + shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", + "booleans do not show the contents when false"); + }); + + it("zeros", function() { + shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0}, + "num1: 42, num2: 0"); + shouldCompileTo("num: {{.}}", 0, "num: 0"); + shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0"); + }); + + it("newlines", function() { + shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest"); + shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest"); + }); + + it("escaping text", function() { + shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes"); + shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored"); + shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes"); + shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes"); + shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript"); + }); + + it("escaping expressions", function() { + shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>', + "expressions with 3 handlebars aren't escaped"); + + shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', + "expressions with {{& handlebars aren't escaped"); + + shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>', + "by default expressions should be escaped"); + + shouldCompileTo("{{awesome}}", {awesome: "Escaped, <b> looks like: <b>"}, 'Escaped, <b> looks like: &lt;b&gt;', + "escaping should properly handle amperstands"); + }); + + it("functions returning safestrings shouldn't be escaped", function() { + var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }}; + shouldCompileTo("{{awesome}}", hash, '&\"\\<>', + "functions returning safestrings aren't escaped"); + }); + + it("functions", function() { + shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome", + "functions are called and render their output"); + shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more: "More awesome"}, "More awesome", + "functions are bound to the context"); + }); + + it("functions with context argument", function() { + shouldCompileTo("{{awesome frank}}", + {awesome: function(context) { return context; }, + frank: "Frank"}, + "Frank", "functions are called with context arguments"); + }); + + + it("paths with hyphens", function() { + shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)"); + shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)"); + shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)"); + }); + + it("nested paths", function() { + shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}}, + "Goodbye beautiful world!", "Nested paths access nested objects"); + }); + + it("nested paths with empty string value", function() { + shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}}, + "Goodbye world!", "Nested paths access nested objects with empty string"); + }); + + it("literal paths", function() { + shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}}, + "Goodbye beautiful world!", "Literal paths can be used"); + shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}}, + "Goodbye beautiful world!", "Literal paths can be used"); + }); + + it('literal references', function() { + shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"}, + "Goodbye beautiful world!", "Literal paths can be used"); + }); + + it("that current context path ({{.}}) doesn't hit helpers", function() { + shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: "); + }); + + it("complex but empty paths", function() { + shouldCompileTo("{{person/name}}", {person: {name: null}}, ""); + shouldCompileTo("{{person/name}}", {person: {}}, ""); + }); + + it("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"); + + string = "{{#hellos}}{{this/text}}{{/hellos}}"; + hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; + shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths"); + }); + + it("this keyword nested inside path", function() { + var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}"; + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("this keyword in helpers", function() { + var helpers = {foo: function(value) { + return 'bar ' + value; + }}; + var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}"; + var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; + shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE", + "This keyword in paths evaluates to current context"); + + string = "{{#hellos}}{{foo this/text}}{{/hellos}}"; + hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; + shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths"); + }); + + it("this keyword nested inside helpers param", function() { + var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}"; + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); +}); diff --git a/spec/blocks.js b/spec/blocks.js new file mode 100644 index 0000000..1880eb5 --- /dev/null +++ b/spec/blocks.js @@ -0,0 +1,86 @@ +/*global CompilerContext, shouldCompileTo */ +describe('blocks', function() { + it("array", function() { + var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", + "Arrays iterate over the contents when not empty"); + + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "Arrays ignore the contents when empty"); + + }); + + it("array with @index", function() { + var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); + }); + + it("empty block", function() { + var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + shouldCompileTo(string, hash, "cruel world!", + "Arrays iterate over the contents when not empty"); + + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "Arrays ignore the contents when empty"); + }); + + it("block with complex lookup", function() { + var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"; + var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]}; + + shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ", + "Templates can access variables in contexts up the stack with relative path syntax"); + }); + + it("block with complex lookup using nested context", function() { + var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}"; + + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("block with deep nested complex lookup", function() { + var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}"; + var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] }; + + shouldCompileTo(string, hash, "Goodbye cruel OMG!"); + }); + + describe('inverted sections', function() { + it("inverted sections with unset value", function() { + var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; + var hash = {}; + shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set."); + }); + + it("inverted section with false value", function() { + var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; + var hash = {goodbyes: false}; + shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false."); + }); + + it("inverted section with empty set", function() { + var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; + var hash = {goodbyes: []}; + shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set."); + }); + + it("block inverted sections", function() { + shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, + "No people"); + }); + + it("block inverted sections with empty arrays", function() { + shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, + "No people"); + }); + }); +}); diff --git a/spec/builtins.js b/spec/builtins.js new file mode 100644 index 0000000..4379725 --- /dev/null +++ b/spec/builtins.js @@ -0,0 +1,125 @@ +/*global CompilerContext, shouldCompileTo, compileWithPartials */ +describe('builtin helpers', function() { + var originalLog = Handlebars.log; + afterEach(function() { + Handlebars.log = originalLog; + }); + + describe('#if', function() { + it("if", function() { + var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; + shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", + "if with boolean argument shows the contents when true"); + shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!", + "if with string argument shows the contents"); + shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", + "if with boolean argument does not show the contents when false"); + shouldCompileTo(string, {world: "world"}, "cruel world!", + "if with undefined does not show the contents"); + shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!", + "if with non-empty array shows the contents"); + shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!", + "if with empty array does not show the contents"); + }); + + it("if with function argument", function() { + var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; + shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!", + "if with function shows the contents when function returns true"); + shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!", + "if with function shows the contents when function returns string"); + shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!", + "if with function does not show the contents when returns false"); + shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!", + "if with function does not show the contents when returns undefined"); + }); + }); + + describe('#with', function() { + it("with", function() { + var string = "{{#with person}}{{first}} {{last}}{{/with}}"; + shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson"); + }); + it("with with function argument", function() { + var string = "{{#with person}}{{first}} {{last}}{{/with}}"; + shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson"); + }); + }); + + describe('#each', function() { + it("each", function() { + var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", + "each with array argument iterates over the contents when not empty"); + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "each with array argument ignores the contents when empty"); + }); + + it("each with an object and @key", function() { + var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: {"<b>#1</b>": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"}; + + // Object property iteration order is undefined according to ECMA spec, + // so we need to check both possible orders + // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop + var actual = compileWithPartials(string, hash); + var expected1 = "<b>#1</b>. goodbye! 2. GOODBYE! cruel world!"; + var expected2 = "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"; + + (actual === expected1 || actual === expected2).should.equal(true, "each with object argument iterates over the contents when not empty"); + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "each with object argument ignores the contents when empty"); + }); + + it("each with @index", function() { + var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); + }); + + it("each with function argument", function() { + var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"}; + shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", + "each with array function argument iterates over the contents when not empty"); + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "each with array function argument ignores the contents when empty"); + }); + + it("data passed to helpers", function() { + var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}"; + var hash = {letters: ['a', 'b', 'c']}; + + var template = CompilerContext.compile(string); + var result = template(hash, { + data: { + exclaim: '!' + } + }); + equal(result, 'a!b!c!', 'should output data'); + }); + + Handlebars.registerHelper('detectDataInsideEach', function(options) { + return options.data && options.data.exclaim; + }); + }); + + it("#log", function() { + + var string = "{{log blah}}"; + var hash = { blah: "whee" }; + + var levelArg, logArg; + Handlebars.log = function(level, arg){ levelArg = level, logArg = arg; }; + + shouldCompileTo(string, hash, "", "log should not display"); + equals(1, levelArg, "should call log with 1"); + equals("whee", logArg, "should call log with 'whee'"); + }); + +}); diff --git a/spec/data.js b/spec/data.js new file mode 100644 index 0000000..5af35b7 --- /dev/null +++ b/spec/data.js @@ -0,0 +1,240 @@ +/*global CompilerContext */ +describe('data', function() { + it("passing in data to a compiled function that expects data - works with helpers", function() { + var template = CompilerContext.compile("{{hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.data.adjective + " " + this.noun; + } + }; + + var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}}); + equals("happy cat", result, "Data output by helper"); + }); + + it("data can be looked up via @foo", function() { + var template = CompilerContext.compile("{{@hello}}"); + var result = template({}, { data: { hello: "hello" } }); + equals("hello", result, "@foo retrieves template data"); + }); + + var objectCreate = Handlebars.createFrame; + + it("deep @foo triggers automatic top-level data", function() { + var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}'); + + var helpers = objectCreate(Handlebars.helpers); + + helpers.let = function(options) { + var frame = Handlebars.createFrame(options.data); + + for (var prop in options.hash) { + frame[prop] = options.hash[prop]; + } + return options.fn(this, { data: frame }); + }; + + var result = template({ foo: true }, { helpers: helpers }); + equals("Hello world", result, "Automatic data was triggered"); + }); + + it("parameter data can be looked up via @foo", function() { + var template = CompilerContext.compile("{{hello @world}}"); + var helpers = { + hello: function(noun) { + return "Hello " + noun; + } + }; + + var result = template({}, { helpers: helpers, data: { world: "world" } }); + equals("Hello world", result, "@foo as a parameter retrieves template data"); + }); + + it("hash values can be looked up via @foo", function() { + var template = CompilerContext.compile("{{hello noun=@world}}"); + var helpers = { + hello: function(options) { + return "Hello " + options.hash.noun; + } + }; + + var result = template({}, { helpers: helpers, data: { world: "world" } }); + equals("Hello world", result, "@foo as a parameter retrieves template data"); + }); + + it("nested parameter data can be looked up via @foo.bar", function() { + var template = CompilerContext.compile("{{hello @world.bar}}"); + var helpers = { + hello: function(noun) { + return "Hello " + noun; + } + }; + + var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } }); + equals("Hello world", result, "@foo as a parameter retrieves template data"); + }); + + it("nested parameter data does not fail with @world.bar", function() { + var template = CompilerContext.compile("{{hello @world.bar}}"); + var helpers = { + hello: function(noun) { + return "Hello " + noun; + } + }; + + var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } }); + equals("Hello undefined", result, "@foo as a parameter retrieves template data"); + }); + + it("parameter data throws when using this scope references", function() { + var string = "{{#goodbyes}}{{text}} cruel {{@./name}}! {{/goodbyes}}"; + + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("parameter data throws when using parent scope references", function() { + var string = "{{#goodbyes}}{{text}} cruel {{@../name}}! {{/goodbyes}}"; + + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("parameter data throws when using complex scope references", function() { + var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}"; + + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("data is inherited downstream", function() { + var template = CompilerContext.compile("{{#let foo=bar.baz}}{{@foo}}{{/let}}", { data: true }); + var helpers = { + let: function(options) { + for (var prop in options.hash) { + options.data[prop] = options.hash[prop]; + } + return options.fn(this); + } + }; + + var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} }); + equals("hello world", result, "data variables are inherited downstream"); + }); + + it("passing in data to a compiled function that expects data - works with helpers in partials", function() { + var template = CompilerContext.compile("{{>my_partial}}", {data: true}); + + var partials = { + my_partial: CompilerContext.compile("{{hello}}", {data: true}) + }; + + var helpers = { + hello: function(options) { + return options.data.adjective + " " + this.noun; + } + }; + + var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}}); + equals("happy cat", result, "Data output by helper inside partial"); + }); + + it("passing in data to a compiled function that expects data - works with helpers and parameters", function() { + var template = CompilerContext.compile("{{hello world}}", {data: true}); + + var helpers = { + hello: function(noun, options) { + return options.data.adjective + " " + noun + (this.exclaim ? "!" : ""); + } + }; + + var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}}); + equals("happy world!", result, "Data output by helper"); + }); + + it("passing in data to a compiled function that expects data - works with block helpers", function() { + var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.fn(this); + }, + world: function(options) { + return options.data.adjective + " world" + (this.exclaim ? "!" : ""); + } + }; + + var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}}); + equals("happy world!", result, "Data output by helper"); + }); + + it("passing in data to a compiled function that expects data - works with block helpers that use ..", function() { + var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.fn({exclaim: "?"}); + }, + world: function(thing, options) { + return options.data.adjective + " " + thing + (this.exclaim || ""); + } + }; + + var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}}); + equals("happy world?", result, "Data output by helper"); + }); + + it("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() { + var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.data.accessData + " " + options.fn({exclaim: "?"}); + }, + world: function(thing, options) { + return options.data.adjective + " " + thing + (this.exclaim || ""); + } + }; + + var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}}); + equals("#win happy world?", result, "Data output by helper"); + }); + + it("you can override inherited data when invoking a helper", function() { + var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} }); + }, + world: function(thing, options) { + return options.data.adjective + " " + thing + (this.exclaim || ""); + } + }; + + var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}}); + equals("sad world?", result, "Overriden data output by helper"); + }); + + + it("you can override inherited data when invoking a helper with depth", function() { + var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.fn({exclaim: "?"}, { data: {adjective: "sad"} }); + }, + world: function(thing, options) { + return options.data.adjective + " " + thing + (this.exclaim || ""); + } + }; + + var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}}); + equals("sad world?", result, "Overriden data output by helper"); + }); + +}); diff --git a/spec/env/browser.js b/spec/env/browser.js new file mode 100644 index 0000000..a17aa66 --- /dev/null +++ b/spec/env/browser.js @@ -0,0 +1,13 @@ +require('./common'); + +global.Handlebars = require('../../dist/handlebars'); + +global.CompilerContext = { + compile: function(template, options) { + var templateSpec = Handlebars.precompile(template, options); + return Handlebars.template(eval('(' + templateSpec + ')')); + }, + compileWithPartial: function(template, options) { + return Handlebars.compile(template, options); + } +}; diff --git a/spec/env/common.js b/spec/env/common.js new file mode 100644 index 0000000..53ddd61 --- /dev/null +++ b/spec/env/common.js @@ -0,0 +1,28 @@ +global.should = require('should'); + +global.shouldCompileTo = function(string, hashOrArray, expected, message) { + shouldCompileToWithPartials(string, hashOrArray, false, expected, message); +}; + +global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) { + var result = compileWithPartials(string, hashOrArray, partials); + result.should.equal(expected, "'" + expected + "' should === '" + result + "': " + message); +}; + +global.compileWithPartials = function(string, hashOrArray, partials) { + var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary; + if(Object.prototype.toString.call(hashOrArray) === "[object Array]") { + ary = []; + ary.push(hashOrArray[0]); + ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); + } else { + ary = [hashOrArray]; + } + + return template.apply(this, ary); +}; + + +global.equals = global.equal = function(a, b, msg) { + a.should.equal(b, msg || ''); +}; diff --git a/spec/env/node.js b/spec/env/node.js new file mode 100644 index 0000000..fe34f94 --- /dev/null +++ b/spec/env/node.js @@ -0,0 +1,13 @@ +require('./common'); + +global.Handlebars = require('../../lib/handlebars'); + +global.CompilerContext = { + compile: function(template, options) { + var templateSpec = Handlebars.precompile(template, options); + return Handlebars.template(eval('(' + templateSpec + ')')); + }, + compileWithPartial: function(template, options) { + return Handlebars.compile(template, options); + } +}; diff --git a/spec/env/runner.js b/spec/env/runner.js new file mode 100644 index 0000000..919f0ae --- /dev/null +++ b/spec/env/runner.js @@ -0,0 +1,40 @@ +var fs = require('fs'), + Mocha = require('mocha'), + path = require('path'); + +var errors = 0, + testDir = path.dirname(__dirname), + grep = process.argv[2]; + +var files = fs.readdirSync(testDir) + .filter(function(name) { return (/.*\.js$/).test(name); }) + .map(function(name) { return testDir + '/' + name; }); + +run('./node', function() { + run('./browser', function() { + run('./runtime', function() { + process.exit(errors); + }); + }); +}); + + +function run(env, callback) { + var mocha = new Mocha(); + mocha.ui('bdd'); + mocha.files = files.slice(); + if (grep) { + mocha.grep(grep); + } + + files.forEach(function(name) { + delete require.cache[name]; + }); + + console.log('Running env: ' + env); + require(env); + mocha.run(function(errorCount) { + errors += errorCount; + callback(); + }); +} diff --git a/spec/env/runtime.js b/spec/env/runtime.js new file mode 100644 index 0000000..fb4b342 --- /dev/null +++ b/spec/env/runtime.js @@ -0,0 +1,15 @@ +require('./common'); + +global.Handlebars = require('../../dist/handlebars.runtime'); + +var compiler = require('../../lib/handlebars'); + +global.CompilerContext = { + compile: function(template, options) { + var templateSpec = compiler.precompile(template, options); + return Handlebars.template(eval('(' + templateSpec + ')')); + }, + compileWithPartial: function(template, options) { + return compiler.compile(template, options); + } +}; diff --git a/spec/helpers.js b/spec/helpers.js new file mode 100644 index 0000000..9fb32e6 --- /dev/null +++ b/spec/helpers.js @@ -0,0 +1,532 @@ +/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */ +describe('helpers', function() { + it("helper with complex lookup$", function() { + var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"; + var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; + var helpers = {link: function(prefix) { + return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>"; + }}; + shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>"); + }); + + it("helper block with complex lookup expression", function() { + var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"; + var hash = {name: "Alan"}; + var helpers = {goodbyes: function(options) { + var out = ""; + var byes = ["Goodbye", "goodbye", "GOODBYE"]; + for (var i = 0,j = byes.length; i < j; i++) { + out += byes[i] + " " + options.fn(this) + "! "; + } + return out; + }}; + shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! "); + }); + + it("helper with complex lookup and nested template", function() { + var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; + var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; + var helpers = {link: function (prefix, options) { + return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>"; + }}; + shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>"); + }); + + it("helper with complex lookup and nested template in VM+Compiler", function() { + var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; + var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; + var helpers = {link: function (prefix, options) { + return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>"; + }}; + shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>"); + }); + + it("block helper", function() { + var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; + var template = CompilerContext.compile(string); + + var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}}); + equal(result, "GOODBYE! cruel world!", "Block helper executed"); + }); + + it("block helper staying in the same context", function() { + var string = "{{#form}}<p>{{name}}</p>{{/form}}"; + var template = CompilerContext.compile(string); + + var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }}); + equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context"); + }); + + it("block helper should have context in this", function() { + var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>"; + var link = function(options) { + return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>'; + }; + var data = { "people": [ + { "name": "Alan", "id": 1 }, + { "name": "Yehuda", "id": 2 } + ]}; + + shouldCompileTo(source, [data, {link: link}], "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>"); + }); + + it("block helper for undefined value", function() { + shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, ""); + }); + + it("block helper passing a new context", function() { + var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}"; + var template = CompilerContext.compile(string); + + var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}}); + equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved"); + }); + + it("block helper passing a complex path context", function() { + var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}"; + var template = CompilerContext.compile(string); + + var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}}); + equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved"); + }); + + it("nested block helpers", function() { + var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}"; + var template = CompilerContext.compile(string); + + var result = template({ + yehuda: {name: "Yehuda" } + }, { + helpers: { + link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; }, + form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; } + } + }); + equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed"); + }); + + it("block helper inverted sections", function() { + var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"; + var list = function(context, options) { + if (context.length > 0) { + var out = "<ul>"; + for(var i = 0,j=context.length; i < j; i++) { + out += "<li>"; + out += options.fn(context[i]); + out += "</li>"; + } + out += "</ul>"; + return out; + } else { + return "<p>" + options.inverse(this) + "</p>"; + } + }; + + var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]}; + var empty = {people: []}; + var rootMessage = { + 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, { list: list }], "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context"); + shouldCompileTo(string, [empty, { list: list }], "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called"); + shouldCompileTo(messageString, [rootMessage, { list: list }], "<p>Nobody's here</p>", "the context of an inverse is the parent of the block"); + }); + + describe("helpers hash", function() { + it("providing a helpers hash", function() { + shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!", + "helpers hash is available"); + + shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}], + "Goodbye cruel world!", "helpers hash is available inside other blocks"); + }); + + it("in cases of conflict, helpers win", function() { + shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", + "helpers hash has precedence escaped expansion"); + shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", + "helpers hash has precedence simple expansion"); + }); + + it("the helpers hash is available is nested contexts", function() { + shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}", + [{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper", + "helpers hash is available in nested contexts."); + }); + + it("the helper hash should augment the global hash", function() { + Handlebars.registerHelper('test_helper', function() { return 'found it!'; }); + + shouldCompileTo( + "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [ + {cruel: "cruel"}, + {world: function() { return "world!"; }} + ], + "found it! Goodbye cruel world!!"); + }); + }); + + it("Multiple global helper registration", function() { + var helpers = Handlebars.helpers; + try { + Handlebars.helpers = {}; + Handlebars.registerHelper({ + 'if': helpers['if'], + world: function() { return "world!"; }, + test_helper: function() { return 'found it!'; } + }); + + shouldCompileTo( + "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", + [{cruel: "cruel"}], + "found it! Goodbye cruel world!!"); + } finally { + if (helpers) { + Handlebars.helpers = helpers; + } + } + }); + it("negative number literals work", function() { + var string = 'Message: {{hello -12}}'; + var hash = {}; + var helpers = {hello: function(times) { + if(typeof times !== 'number') { times = "NaN"; } + return "Hello " + times + " times"; + }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal"); + }); + + describe("String literal parameters", function() { + it("simple literals work", function() { + var string = 'Message: {{hello "world" 12 true false}}'; + var hash = {}; + var helpers = {hello: function(param, times, bool1, bool2) { + if(typeof times !== 'number') { times = "NaN"; } + if(typeof bool1 !== 'boolean') { bool1 = "NaB"; } + if(typeof bool2 !== 'boolean') { bool2 = "NaB"; } + return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2; + }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal"); + }); + + it("using a quote in the middle of a parameter raises an error", function() { + (function() { + var string = 'Message: {{hello wo"rld"}}'; + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("escaping a String is possible", function(){ + var string = 'Message: {{{hello "\\"world\\""}}}'; + var hash = {}; + var helpers = {hello: function(param) { return "Hello " + param; }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal"); + }); + + it("it works with ' marks", function() { + var string = 'Message: {{{hello "Alan\'s world"}}}'; + var hash = {}; + var helpers = {hello: function(param) { return "Hello " + param; }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark"); + }); + }); + + it("negative number literals work", function() { + var string = 'Message: {{hello -12}}'; + var hash = {}; + var helpers = {hello: function(times) { + if(typeof times !== 'number') { times = "NaN"; } + return "Hello " + times + " times"; + }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal"); + }); + + describe("multiple parameters", function() { + it("simple multi-params work", function() { + var string = 'Message: {{goodbye cruel world}}'; + var hash = {cruel: "cruel", world: "world"}; + var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}; + shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params"); + }); + + it("block multi-params work", function() { + var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}'; + var hash = {cruel: "cruel", world: "world"}; + var helpers = {goodbye: function(cruel, world, options) { + return options.fn({greeting: "Goodbye", adj: cruel, noun: world}); + }}; + shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params"); + }); + }); + describe('hash', function() { + it("helpers can take an optional hash", function() { + var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}'); + + var helpers = { + goodbye: function(options) { + return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES"; + } + }; + + var context = {}; + + var result = template(context, {helpers: helpers}); + equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash"); + }); + + it("helpers can take an optional hash with booleans", function() { + var helpers = { + goodbye: function(options) { + if (options.hash.print === true) { + return "GOODBYE " + options.hash.cruel + " " + options.hash.world; + } else if (options.hash.print === false) { + return "NOT PRINTING"; + } else { + return "THIS SHOULD NOT HAPPEN"; + } + } + }; + + var context = {}; + + var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}'); + var result = template(context, {helpers: helpers}); + equals(result, "GOODBYE CRUEL WORLD", "Helper output hash"); + + template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}'); + result = template(context, {helpers: helpers}); + equals(result, "NOT PRINTING", "Boolean helper parameter honored"); + }); + + it("block helpers can take an optional hash", function() { + var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}'); + + var helpers = { + goodbye: function(options) { + return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES"; + } + }; + + var result = template({}, {helpers: helpers}); + equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output"); + }); + + it("block helpers can take an optional hash with single quoted stings", function() { + var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}"); + + var helpers = { + goodbye: function(options) { + return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES"; + } + }; + + var result = template({}, {helpers: helpers}); + equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output"); + }); + + it("block helpers can take an optional hash with booleans", function() { + var helpers = { + goodbye: function(options) { + if (options.hash.print === true) { + return "GOODBYE " + options.hash.cruel + " " + options.fn(this); + } else if (options.hash.print === false) { + return "NOT PRINTING"; + } else { + return "THIS SHOULD NOT HAPPEN"; + } + } + }; + + var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}'); + var result = template({}, {helpers: helpers}); + equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored"); + + template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}'); + result = template({}, {helpers: helpers}); + equals(result, "NOT PRINTING", "Boolean hash parameter honored"); + }); + }); + + describe("helperMissing", function() { + it("if a context is not found, helperMissing is used", function() { + (function() { + var template = CompilerContext.compile("{{hello}} {{link_to world}}"); + template({}); + }).should.throw(/Missing helper: 'link_to'/); + }); + + it("if a context is not found, custom helperMissing is used", function() { + var string = "{{hello}} {{link_to world}}"; + var context = { hello: "Hello", world: "world" }; + + var helpers = { + helperMissing: function(helper, context) { + if(helper === "link_to") { + return new Handlebars.SafeString("<a>" + context + "</a>"); + } + } + }; + + shouldCompileTo(string, [context, helpers], "Hello <a>world</a>"); + }); + }); + + describe("knownHelpers", function() { + it("Known helper should render helper", function() { + var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}}); + + var result = template({}, {helpers: {hello: function() { return "foo"; }}}); + equal(result, "foo", "'foo' should === '" + result); + }); + + it("Unknown helper in knownHelpers only mode should be passed as undefined", function() { + var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true}); + + var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}}); + equal(result, "undefined", "'undefined' should === '" + result); + }); + it("Builtin helpers available in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true}); + + var result = template({}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Field lookup works in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); + + var result = template({foo: 'bar'}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Conditional blocks work in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true}); + + var result = template({foo: 'baz'}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Invert blocks work in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true}); + + var result = template({foo: false}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Functions are bound to the context in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); + var result = template({foo: function() { return this.bar; }, bar: 'bar'}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Unknown helper call in knownHelpers only mode should throw", function() { + (function() { + CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true}); + }).should.throw(Error); + }); + }); + + describe("blockHelperMissing", function() { + it("lambdas are resolved by blockHelperMissing, not handlebars proper", function() { + var string = "{{#truthy}}yep{{/truthy}}"; + var data = { truthy: function() { return true; } }; + shouldCompileTo(string, data, "yep"); + }); + it("lambdas resolved by blockHelperMissing are bound to the context", function() { + var string = "{{#truthy}}yep{{/truthy}}"; + var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } }; + shouldCompileTo(string, boundData, ""); + }); + }); + + describe('name conflicts', function() { + it("helpers take precedence over same-named context properties", function() { + var template = CompilerContext.compile("{{goodbye}} {{cruel world}}"); + + var helpers = { + goodbye: function() { + return this.goodbye.toUpperCase(); + }, + + cruel: function(world) { + return "cruel " + world.toUpperCase(); + } + }; + + var context = { + goodbye: "goodbye", + world: "world" + }; + + var result = template(context, {helpers: helpers}); + equals(result, "GOODBYE cruel WORLD", "Helper executed"); + }); + + it("helpers take precedence over same-named context properties$", function() { + var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}"); + + var helpers = { + goodbye: function(options) { + return this.goodbye.toUpperCase() + options.fn(this); + }, + + cruel: function(world) { + return "cruel " + world.toUpperCase(); + } + }; + + var context = { + goodbye: "goodbye", + world: "world" + }; + + var result = template(context, {helpers: helpers}); + equals(result, "GOODBYE cruel WORLD", "Helper executed"); + }); + + it("Scoped names take precedence over helpers", function() { + var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}"); + + var helpers = { + goodbye: function() { + return this.goodbye.toUpperCase(); + }, + + cruel: function(world) { + return "cruel " + world.toUpperCase(); + }, + }; + + var context = { + goodbye: "goodbye", + world: "world" + }; + + var result = template(context, {helpers: helpers}); + equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed"); + }); + + it("Scoped names take precedence over block helpers", function() { + var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}"); + + var helpers = { + goodbye: function(options) { + return this.goodbye.toUpperCase() + options.fn(this); + }, + + cruel: function(world) { + return "cruel " + world.toUpperCase(); + }, + }; + + var context = { + goodbye: "goodbye", + world: "world" + }; + + var result = template(context, {helpers: helpers}); + equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed"); + }); + }); +}); diff --git a/spec/parser.js b/spec/parser.js index 6fb8d9a..3f2a012 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -1,8 +1,8 @@ -var Handlebars = require('../lib/handlebars'); - -require('should'); - describe('parser', function() { + if (!Handlebars.print) { + return; + } + function ast_for(template) { var ast = Handlebars.parse(template); return Handlebars.print(ast); diff --git a/spec/partials.js b/spec/partials.js new file mode 100644 index 0000000..d919eed --- /dev/null +++ b/spec/partials.js @@ -0,0 +1,119 @@ +/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */ +describe('partials', function() { + it("basic partials", function() { + var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; + var partial = "{{name}} ({{url}}) "; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", + "Basic partials output based on current context."); + }); + + it("partials with context", function() { + var string = "Dudes: {{>dude dudes}}"; + var partial = "{{#this}}{{name}} ({{url}}) {{/this}}"; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", + "Partials can be passed a context"); + }); + + it("partial in a partial", function() { + var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}"; + var dude = "{{name}} {{> url}} "; + var url = "<a href='{{url}}'>{{url}}</a>"; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials"); + }); + + it("rendering undefined partial throws an exception", function() { + (function() { + var template = CompilerContext.compile("{{> whatever}}"); + template(); + }).should.throw(Handlebars.Exception, 'The partial whatever could not be found'); + }); + + it("rendering template partial in vm mode throws an exception", function() { + (function() { + var template = CompilerContext.compile("{{> whatever}}"); + template(); + }).should.throw(Handlebars.Exception, 'The partial whatever could not be found'); + }); + + it("rendering function partial in vm mode", function() { + var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; + var partial = function(context) { + return context.name + ' (' + context.url + ') '; + }; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", + "Function partials output based in VM."); + }); + + it("GH-14: a partial preceding a selector", function() { + var string = "Dudes: {{>dude}} {{another_dude}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); + }); + + it("Partials with slash paths", function() { + var string = "Dudes: {{> shared/dude}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); + + it("Partials with slash and point paths", function() { + var string = "Dudes: {{> shared/dude.thing}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths"); + }); + + it("Global Partials", function() { + Handlebars.registerPartial('global_test', '{{another_dude}}'); + + var string = "Dudes: {{> shared/dude}} {{> global_test}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); + }); + + it("Multiple partial registration", function() { + Handlebars.registerPartial({ + 'shared/dude': '{{name}}', + global_test: '{{another_dude}}' + }); + + var string = "Dudes: {{> shared/dude}} {{> global_test}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); + }); + + it("Partials with integer path", function() { + var string = "Dudes: {{> 404}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); + + it("Partials with complex path", function() { + var string = "Dudes: {{> 404/asdf?.bar}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); + + it("Partials with escaped", function() { + var string = "Dudes: {{> [+404/asdf?.bar]}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); + + it("Partials with string", function() { + var string = "Dudes: {{> \"+404/asdf?.bar\"}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); +}); diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js deleted file mode 100644 index 5d52e44..0000000 --- a/spec/qunit_spec.js +++ /dev/null @@ -1,1657 +0,0 @@ -var Handlebars; -if (!Handlebars) { - // Setup for Node package testing - Handlebars = require('../lib/handlebars'); - - var assert = require("assert"), - - equal = assert.equal, - equals = assert.equal, - ok = assert.ok; - - // Note that this doesn't have the same context separation as the rspec test. - // Both should be run for full acceptance of the two libary modes. - var CompilerContext = { - compile: function(template, options) { - var templateSpec = Handlebars.precompile(template, options); - return Handlebars.template(eval('(' + templateSpec + ')')); - }, - compileWithPartial: function(template, options) { - return Handlebars.compile(template, options); - } - }; -} else { - var _equal = equal; - equals = equal = function(a, b, msg) { - // Allow exec with missing message params - _equal(a, b, msg || ''); - }; -} - -suite("basic context"); - -function shouldCompileTo(string, hashOrArray, expected, message) { - shouldCompileToWithPartials(string, hashOrArray, false, expected, message); -} - -function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) { - var result = compileWithPartials(string, hashOrArray, partials); - equal(result, expected, "'" + expected + "' should === '" + result + "': " + message); -} - -function compileWithPartials(string, hashOrArray, partials) { - var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary; - if(Object.prototype.toString.call(hashOrArray) === "[object Array]") { - ary = []; - ary.push(hashOrArray[0]); - ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); - } else { - ary = [hashOrArray]; - } - - return template.apply(this, ary); -} - -function shouldThrow(fn, exception, message) { - var caught = false, - exType, exMessage; - - if (exception instanceof Array) { - exType = exception[0]; - exMessage = exception[1]; - } else if (typeof exception === 'string') { - exType = Error; - exMessage = exception; - } else { - exType = exception; - } - - try { - fn(); - } - catch (e) { - if (e instanceof exType) { - if (!exMessage || e.message === exMessage) { - caught = true; - } - } - } - - ok(caught, message || null); -} - -test("most basic", function() { - shouldCompileTo("{{foo}}", { foo: "foo" }, "foo"); -}); - -test("escaping", function() { - shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}"); - shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food"); - shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food"); -}); - -test("compiling with a basic context", function() { - shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", - "It works if all the required keys are provided"); -}); - -test("comments", function() { - shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", - {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", - "comments are ignored"); -}); - -test("boolean", function() { - var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!"; - shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", - "booleans show the contents when true"); - - shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", - "booleans do not show the contents when false"); -}); - -test("zeros", function() { - shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0}, - "num1: 42, num2: 0"); - shouldCompileTo("num: {{.}}", 0, "num: 0"); - shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0"); -}); - -test("newlines", function() { - shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest"); - shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest"); -}); - -test("escaping text", function() { - shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes"); - shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored"); - shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes"); - shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes"); - shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript"); -}); - -test("escaping expressions", function() { - shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>', - "expressions with 3 handlebars aren't escaped"); - - shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', - "expressions with {{& handlebars aren't escaped"); - - shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>', - "by default expressions should be escaped"); - - shouldCompileTo("{{awesome}}", {awesome: "Escaped, <b> looks like: <b>"}, 'Escaped, <b> looks like: &lt;b&gt;', - "escaping should properly handle amperstands"); -}); - -test("functions returning safestrings shouldn't be escaped", function() { - var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }}; - shouldCompileTo("{{awesome}}", hash, '&\"\\<>', - "functions returning safestrings aren't escaped"); -}); - -test("functions", function() { - shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome", - "functions are called and render their output"); - shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more: "More awesome"}, "More awesome", - "functions are bound to the context"); -}); - -test("functions with context argument", function() { - shouldCompileTo("{{awesome frank}}", - {awesome: function(context) { return context; }, - frank: "Frank"}, - "Frank", "functions are called with context arguments"); -}); - - -test("paths with hyphens", function() { - shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)"); - shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)"); - shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)"); -}); - -test("nested paths", function() { - shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}}, - "Goodbye beautiful world!", "Nested paths access nested objects"); -}); - -test("nested paths with empty string value", function() { - shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}}, - "Goodbye world!", "Nested paths access nested objects with empty string"); -}); - -test("literal paths", function() { - shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}}, - "Goodbye beautiful world!", "Literal paths can be used"); - shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}}, - "Goodbye beautiful world!", "Literal paths can be used"); -}); - -test('literal references', function() { - shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"}, - "Goodbye beautiful world!", "Literal paths can be used"); -}); - -test("that current context path ({{.}}) doesn't hit helpers", function() { - shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: "); -}); - -test("complex but empty paths", function() { - shouldCompileTo("{{person/name}}", {person: {name: null}}, ""); - shouldCompileTo("{{person/name}}", {person: {}}, ""); -}); - -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"); - - string = "{{#hellos}}{{this/text}}{{/hellos}}"; - hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; - shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths"); -}); - -test("this keyword nested inside path", function() { - var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}"; - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("this keyword in helpers", function() { - var helpers = {foo: function(value) { - return 'bar ' + value; - }}; - var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}"; - var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; - shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE", - "This keyword in paths evaluates to current context"); - - string = "{{#hellos}}{{foo this/text}}{{/hellos}}"; - hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; - shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths"); -}); - -test("this keyword nested inside helpers param", function() { - var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}"; - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -suite("inverted sections"); - -test("inverted sections with unset value", function() { - var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; - var hash = {}; - shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set."); -}); - -test("inverted section with false value", function() { - var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; - 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: []}; - shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set."); -}); - -suite("blocks"); - -test("array", function() { - var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", - "Arrays iterate over the contents when not empty"); - - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "Arrays ignore the contents when empty"); - -}); - -test("array with @index", function() { - var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - - var template = CompilerContext.compile(string); - var result = template(hash); - - equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); -}); - -test("empty block", function() { - var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - shouldCompileTo(string, hash, "cruel world!", - "Arrays iterate over the contents when not empty"); - - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "Arrays ignore the contents when empty"); -}); - -test("nested iteration", function() { - -}); - -test("block with complex lookup", function() { - var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"; - var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]}; - - shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ", - "Templates can access variables in contexts up the stack with relative path syntax"); -}); - -test("block with complex lookup using nested context", function() { - var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}"; - - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("helper with complex lookup$", function() { - var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"; - var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; - var helpers = {link: function(prefix) { - return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>"; - }}; - shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>"); -}); - -test("helper block with complex lookup expression", function() { - var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"; - var hash = {name: "Alan"}; - var helpers = {goodbyes: function(options) { - var out = ""; - var byes = ["Goodbye", "goodbye", "GOODBYE"]; - for (var i = 0,j = byes.length; i < j; i++) { - out += byes[i] + " " + options.fn(this) + "! "; - } - return out; - }}; - shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! "); -}); - -test("helper with complex lookup and nested template", function() { - var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; - var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; - var helpers = {link: function (prefix, options) { - return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>"; - }}; - shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>"); -}); - -test("helper with complex lookup and nested template in VM+Compiler", function() { - var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; - var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; - var helpers = {link: function (prefix, options) { - return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>"; - }}; - shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>"); -}); - -test("block with deep nested complex lookup", function() { - var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}"; - var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] }; - - shouldCompileTo(string, hash, "Goodbye cruel OMG!"); -}); - -test("block helper", function() { - var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; - var template = CompilerContext.compile(string); - - var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}}); - equal(result, "GOODBYE! cruel world!", "Block helper executed"); -}); - -test("block helper staying in the same context", function() { - var string = "{{#form}}<p>{{name}}</p>{{/form}}"; - var template = CompilerContext.compile(string); - - var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }}); - equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context"); -}); - -test("block helper should have context in this", function() { - var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>"; - var link = function(options) { - return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>'; - }; - var data = { "people": [ - { "name": "Alan", "id": 1 }, - { "name": "Yehuda", "id": 2 } - ]}; - - shouldCompileTo(source, [data, {link: link}], "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>"); -}); - -test("block helper for undefined value", function() { - shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, ""); -}); - -test("block helper passing a new context", function() { - var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}"; - var template = CompilerContext.compile(string); - - var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}}); - equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved"); -}); - -test("block helper passing a complex path context", function() { - var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}"; - var template = CompilerContext.compile(string); - - var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}}); - equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved"); -}); - -test("nested block helpers", function() { - var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}"; - var template = CompilerContext.compile(string); - - var result = template({ - yehuda: {name: "Yehuda" } - }, { - helpers: { - link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; }, - form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; } - } - }); - equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed"); -}); - -test("block inverted sections", function() { - 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}}{{^}}<em>Nobody's here</em>{{/list}}"; - var list = function(context, options) { - if (context.length > 0) { - var out = "<ul>"; - for(var i = 0,j=context.length; i < j; i++) { - out += "<li>"; - out += options.fn(context[i]); - out += "</li>"; - } - out += "</ul>"; - return out; - } else { - return "<p>" + options.inverse(this) + "</p>"; - } - }; - - var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]}; - var empty = {people: []}; - var rootMessage = { - 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, { list: list }], "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context"); - shouldCompileTo(string, [empty, { list: list }], "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called"); - shouldCompileTo(messageString, [rootMessage, { list: list }], "<p>Nobody's here</p>", "the context of an inverse is the parent of the block"); -}); - -suite("helpers hash"); - -test("providing a helpers hash", function() { - shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!", - "helpers hash is available"); - - shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}], - "Goodbye cruel world!", "helpers hash is available inside other blocks"); -}); - -test("in cases of conflict, helpers win", function() { - shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", - "helpers hash has precedence escaped expansion"); - shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", - "helpers hash has precedence simple expansion"); -}); - -test("the helpers hash is available is nested contexts", function() { - shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}", - [{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper", - "helpers hash is available in nested contexts."); -}); - -test("the helper hash should augment the global hash", function() { - Handlebars.registerHelper('test_helper', function() { return 'found it!'; }); - - shouldCompileTo( - "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [ - {cruel: "cruel"}, - {world: function() { return "world!"; }} - ], - "found it! Goodbye cruel world!!"); -}); - -test("Multiple global helper registration", function() { - var helpers = Handlebars.helpers; - try { - Handlebars.helpers = {}; - Handlebars.registerHelper({ - 'if': helpers['if'], - world: function() { return "world!"; }, - test_helper: function() { return 'found it!'; } - }); - - shouldCompileTo( - "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", - [{cruel: "cruel"}], - "found it! Goodbye cruel world!!"); - } finally { - if (helpers) { - Handlebars.helpers = helpers; - } - } -}); - -suite("partials"); - -test("basic partials", function() { - var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; - var partial = "{{name}} ({{url}}) "; - var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; - shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", - "Basic partials output based on current context."); -}); - -test("partials with context", function() { - var string = "Dudes: {{>dude dudes}}"; - var partial = "{{#this}}{{name}} ({{url}}) {{/this}}"; - var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; - shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", - "Partials can be passed a context"); -}); - -test("partial in a partial", function() { - var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}"; - var dude = "{{name}} {{> url}} "; - var url = "<a href='{{url}}'>{{url}}</a>"; - var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; - shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials"); -}); - -test("rendering undefined partial throws an exception", function() { - shouldThrow(function() { - var template = CompilerContext.compile("{{> whatever}}"); - template(); - }, [Handlebars.Exception, 'The partial whatever could not be found'], "Should throw exception"); -}); - -test("rendering template partial in vm mode throws an exception", function() { - shouldThrow(function() { - var template = CompilerContext.compile("{{> whatever}}"); - template(); - }, [Handlebars.Exception, 'The partial whatever could not be found'], "Should throw exception"); -}); - -test("rendering function partial in vm mode", function() { - var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; - var partial = function(context) { - return context.name + ' (' + context.url + ') '; - }; - var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; - shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", - "Function partials output based in VM."); -}); - -test("GH-14: a partial preceding a selector", function() { - var string = "Dudes: {{>dude}} {{another_dude}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); -}); - -test("Partials with slash paths", function() { - var string = "Dudes: {{> shared/dude}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -test("Partials with slash and point paths", function() { - var string = "Dudes: {{> shared/dude.thing}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths"); -}); - -test("Global Partials", function() { - Handlebars.registerPartial('global_test', '{{another_dude}}'); - - var string = "Dudes: {{> shared/dude}} {{> global_test}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); -}); - -test("Multiple partial registration", function() { - Handlebars.registerPartial({ - 'shared/dude': '{{name}}', - global_test: '{{another_dude}}' - }); - - var string = "Dudes: {{> shared/dude}} {{> global_test}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); -}); - -test("Partials with integer path", function() { - var string = "Dudes: {{> 404}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -test("Partials with complex path", function() { - var string = "Dudes: {{> 404/asdf?.bar}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -test("Partials with escaped", function() { - var string = "Dudes: {{> [+404/asdf?.bar]}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -test("Partials with string", function() { - var string = "Dudes: {{> \"+404/asdf?.bar\"}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -suite("String literal parameters"); - -test("simple literals work", function() { - var string = 'Message: {{hello "world" 12 true false}}'; - var hash = {}; - var helpers = {hello: function(param, times, bool1, bool2) { - if(typeof times !== 'number') { times = "NaN"; } - if(typeof bool1 !== 'boolean') { bool1 = "NaB"; } - if(typeof bool2 !== 'boolean') { bool2 = "NaB"; } - return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2; - }}; - shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal"); -}); -test("negative number literals work", function() { - var string = 'Message: {{hello -12}}'; - var hash = {}; - var helpers = {hello: function(times) { - if(typeof times !== 'number') { times = "NaN"; } - return "Hello " + times + " times"; - }}; - shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal"); -}); - - -test("using a quote in the middle of a parameter raises an error", function() { - shouldThrow(function() { - var string = 'Message: {{hello wo"rld"}}'; - CompilerContext.compile(string); - }, Error, "should throw exception"); -}); - -test("escaping a String is possible", function(){ - var string = 'Message: {{{hello "\\"world\\""}}}'; - var hash = {}; - var helpers = {hello: function(param) { return "Hello " + param; }}; - shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal"); -}); - -test("it works with ' marks", function() { - var string = 'Message: {{{hello "Alan\'s world"}}}'; - var hash = {}; - var helpers = {hello: function(param) { return "Hello " + param; }}; - shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark"); -}); - -suite("multiple parameters"); - -test("simple multi-params work", function() { - var string = 'Message: {{goodbye cruel world}}'; - var hash = {cruel: "cruel", world: "world"}; - var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}; - shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params"); -}); - -test("block multi-params work", function() { - var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}'; - var hash = {cruel: "cruel", world: "world"}; - var helpers = {goodbye: function(cruel, world, options) { - return options.fn({greeting: "Goodbye", adj: cruel, noun: world}); - }}; - shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params"); -}); - -suite("safestring"); - -test("constructing a safestring from a string and checking its type", function() { - var safe = new Handlebars.SafeString("testing 1, 2, 3"); - ok(safe instanceof Handlebars.SafeString, "SafeString is an instance of Handlebars.SafeString"); - equal(safe, "testing 1, 2, 3", "SafeString is equivalent to its underlying string"); -}); - -test("it should not escape SafeString properties", function() { - var name = new Handlebars.SafeString("<em>Sean O'Malley</em>"); - - shouldCompileTo('{{name}}', [{ name: name }], "<em>Sean O'Malley</em>"); -}); - -suite("helperMissing"); - -test("if a context is not found, helperMissing is used", function() { - shouldThrow(function() { - var template = CompilerContext.compile("{{hello}} {{link_to world}}"); - template({}); - }, [Error, "Missing helper: 'link_to'"], "Should throw exception"); -}); - -test("if a context is not found, custom helperMissing is used", function() { - var string = "{{hello}} {{link_to world}}"; - var context = { hello: "Hello", world: "world" }; - - var helpers = { - helperMissing: function(helper, context) { - if(helper === "link_to") { - return new Handlebars.SafeString("<a>" + context + "</a>"); - } - } - }; - - shouldCompileTo(string, [context, helpers], "Hello <a>world</a>"); -}); - -suite("knownHelpers"); - -test("Known helper should render helper", function() { - var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}}); - - var result = template({}, {helpers: {hello: function() { return "foo"; }}}); - equal(result, "foo", "'foo' should === '" + result); -}); - -test("Unknown helper in knownHelpers only mode should be passed as undefined", function() { - var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true}); - - var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}}); - equal(result, "undefined", "'undefined' should === '" + result); -}); -test("Builtin helpers available in knownHelpers only mode", function() { - var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true}); - - var result = template({}); - equal(result, "bar", "'bar' should === '" + result); -}); -test("Field lookup works in knownHelpers only mode", function() { - var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); - - var result = template({foo: 'bar'}); - equal(result, "bar", "'bar' should === '" + result); -}); -test("Conditional blocks work in knownHelpers only mode", function() { - var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true}); - - var result = template({foo: 'baz'}); - equal(result, "bar", "'bar' should === '" + result); -}); -test("Invert blocks work in knownHelpers only mode", function() { - var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true}); - - var result = template({foo: false}); - equal(result, "bar", "'bar' should === '" + result); -}); -test("Functions are bound to the context in knownHelpers only mode", function() { - var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); - var result = template({foo: function() { return this.bar; }, bar: 'bar'}); - equal(result, "bar", "'bar' should === '" + result); -}); -test("Unknown helper call in knownHelpers only mode should throw", function() { - shouldThrow(function() { - CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true}); - }, Error, 'specified knownHelpersOnly'); -}); - -suite("blockHelperMissing"); - -test("lambdas are resolved by blockHelperMissing, not handlebars proper", function() { - var string = "{{#truthy}}yep{{/truthy}}"; - var data = { truthy: function() { return true; } }; - shouldCompileTo(string, data, "yep"); -}); -test("lambdas resolved by blockHelperMissing are bound to the context", function() { - var string = "{{#truthy}}yep{{/truthy}}"; - var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } }; - shouldCompileTo(string, boundData, ""); -}); - -var teardown; -suite("built-in helpers", { - setup: function(){ teardown = null; }, - teardown: function(){ if (teardown) { teardown(); } } -}); - -test("with", function() { - var string = "{{#with person}}{{first}} {{last}}{{/with}}"; - shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson"); -}); -test("with with function argument", function() { - var string = "{{#with person}}{{first}} {{last}}{{/with}}"; - shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson"); -}); - -test("if", function() { - var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; - shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", - "if with boolean argument shows the contents when true"); - shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!", - "if with string argument shows the contents"); - shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", - "if with boolean argument does not show the contents when false"); - shouldCompileTo(string, {world: "world"}, "cruel world!", - "if with undefined does not show the contents"); - shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!", - "if with non-empty array shows the contents"); - shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!", - "if with empty array does not show the contents"); -}); - -test("if with function argument", function() { - var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; - shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!", - "if with function shows the contents when function returns true"); - shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!", - "if with function shows the contents when function returns string"); - shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!", - "if with function does not show the contents when returns false"); - shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!", - "if with function does not show the contents when returns undefined"); -}); - -test("each", function() { - var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", - "each with array argument iterates over the contents when not empty"); - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "each with array argument ignores the contents when empty"); -}); - -test("each with an object and @key", function() { - var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: {"<b>#1</b>": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"}; - - // Object property iteration order is undefined according to ECMA spec, - // so we need to check both possible orders - // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop - var actual = compileWithPartials(string, hash); - var expected1 = "<b>#1</b>. goodbye! 2. GOODBYE! cruel world!"; - var expected2 = "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"; - - ok(actual === expected1 || actual === expected2, "each with object argument iterates over the contents when not empty"); - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "each with object argument ignores the contents when empty"); -}); - -test("each with @index", function() { - var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - - var template = CompilerContext.compile(string); - var result = template(hash); - - equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); -}); - -test("each with function argument", function() { - var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"}; - shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", - "each with array function argument iterates over the contents when not empty"); - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "each with array function argument ignores the contents when empty"); -}); - -test("data passed to helpers", function() { - var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}"; - var hash = {letters: ['a', 'b', 'c']}; - - var template = CompilerContext.compile(string); - var result = template(hash, { - data: { - exclaim: '!' - } - }); - equal(result, 'a!b!c!', 'should output data'); -}); - -Handlebars.registerHelper('detectDataInsideEach', function(options) { - return options.data && options.data.exclaim; -}); - -test("log", function() { - var string = "{{log blah}}"; - var hash = { blah: "whee" }; - - var levelArg, logArg; - var originalLog = Handlebars.log; - Handlebars.log = function(level, arg){ levelArg = level, logArg = arg; }; - teardown = function(){ Handlebars.log = originalLog; }; - - shouldCompileTo(string, hash, "", "log should not display"); - equals(1, levelArg, "should call log with 1"); - equals("whee", logArg, "should call log with 'whee'"); -}); - -test("overriding property lookup", function() { - -}); - - -test("passing in data to a compiled function that expects data - works with helpers", function() { - var template = CompilerContext.compile("{{hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.data.adjective + " " + this.noun; - } - }; - - var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}}); - equals("happy cat", result, "Data output by helper"); -}); - -test("data can be looked up via @foo", function() { - var template = CompilerContext.compile("{{@hello}}"); - var result = template({}, { data: { hello: "hello" } }); - equals("hello", result, "@foo retrieves template data"); -}); - -var objectCreate = Handlebars.createFrame; - -test("deep @foo triggers automatic top-level data", function() { - var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}'); - - var helpers = objectCreate(Handlebars.helpers); - - helpers.let = function(options) { - var frame = Handlebars.createFrame(options.data); - - for (var prop in options.hash) { - frame[prop] = options.hash[prop]; - } - return options.fn(this, { data: frame }); - }; - - var result = template({ foo: true }, { helpers: helpers }); - equals("Hello world", result, "Automatic data was triggered"); -}); - -test("parameter data can be looked up via @foo", function() { - var template = CompilerContext.compile("{{hello @world}}"); - var helpers = { - hello: function(noun) { - return "Hello " + noun; - } - }; - - var result = template({}, { helpers: helpers, data: { world: "world" } }); - equals("Hello world", result, "@foo as a parameter retrieves template data"); -}); - -test("hash values can be looked up via @foo", function() { - var template = CompilerContext.compile("{{hello noun=@world}}"); - var helpers = { - hello: function(options) { - return "Hello " + options.hash.noun; - } - }; - - var result = template({}, { helpers: helpers, data: { world: "world" } }); - equals("Hello world", result, "@foo as a parameter retrieves template data"); -}); - -test("nested parameter data can be looked up via @foo.bar", function() { - var template = CompilerContext.compile("{{hello @world.bar}}"); - var helpers = { - hello: function(noun) { - return "Hello " + noun; - } - }; - - var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } }); - equals("Hello world", result, "@foo as a parameter retrieves template data"); -}); - -test("nested parameter data does not fail with @world.bar", function() { - var template = CompilerContext.compile("{{hello @world.bar}}"); - var helpers = { - hello: function(noun) { - return "Hello " + noun; - } - }; - - var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } }); - equals("Hello undefined", result, "@foo as a parameter retrieves template data"); -}); - -test("parameter data throws when using this scope references", function() { - var string = "{{#goodbyes}}{{text}} cruel {{@./name}}! {{/goodbyes}}"; - - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("parameter data throws when using parent scope references", function() { - var string = "{{#goodbyes}}{{text}} cruel {{@../name}}! {{/goodbyes}}"; - - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("parameter data throws when using complex scope references", function() { - var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}"; - - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("data is inherited downstream", function() { - var template = CompilerContext.compile("{{#let foo=bar.baz}}{{@foo}}{{/let}}", { data: true }); - var helpers = { - let: function(options) { - for (var prop in options.hash) { - options.data[prop] = options.hash[prop]; - } - return options.fn(this); - } - }; - - var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} }); - equals("hello world", result, "data variables are inherited downstream"); -}); - -test("passing in data to a compiled function that expects data - works with helpers in partials", function() { - var template = CompilerContext.compile("{{>my_partial}}", {data: true}); - - var partials = { - my_partial: CompilerContext.compile("{{hello}}", {data: true}) - }; - - var helpers = { - hello: function(options) { - return options.data.adjective + " " + this.noun; - } - }; - - var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}}); - equals("happy cat", result, "Data output by helper inside partial"); -}); - -test("passing in data to a compiled function that expects data - works with helpers and parameters", function() { - var template = CompilerContext.compile("{{hello world}}", {data: true}); - - var helpers = { - hello: function(noun, options) { - return options.data.adjective + " " + noun + (this.exclaim ? "!" : ""); - } - }; - - var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}}); - equals("happy world!", result, "Data output by helper"); -}); - -test("passing in data to a compiled function that expects data - works with block helpers", function() { - var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.fn(this); - }, - world: function(options) { - return options.data.adjective + " world" + (this.exclaim ? "!" : ""); - } - }; - - var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}}); - equals("happy world!", result, "Data output by helper"); -}); - -test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() { - var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.fn({exclaim: "?"}); - }, - world: function(thing, options) { - return options.data.adjective + " " + thing + (this.exclaim || ""); - } - }; - - var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}}); - equals("happy world?", result, "Data output by helper"); -}); - -test("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() { - var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.data.accessData + " " + options.fn({exclaim: "?"}); - }, - world: function(thing, options) { - return options.data.adjective + " " + thing + (this.exclaim || ""); - } - }; - - var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}}); - equals("#win happy world?", result, "Data output by helper"); -}); - -test("you can override inherited data when invoking a helper", function() { - var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} }); - }, - world: function(thing, options) { - return options.data.adjective + " " + thing + (this.exclaim || ""); - } - }; - - var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}}); - equals("sad world?", result, "Overriden data output by helper"); -}); - - -test("you can override inherited data when invoking a helper with depth", function() { - var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.fn({exclaim: "?"}, { data: {adjective: "sad"} }); - }, - world: function(thing, options) { - return options.data.adjective + " " + thing + (this.exclaim || ""); - } - }; - - var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}}); - equals("sad world?", result, "Overriden data output by helper"); -}); - -test("helpers take precedence over same-named context properties", function() { - var template = CompilerContext.compile("{{goodbye}} {{cruel world}}"); - - var helpers = { - goodbye: function() { - return this.goodbye.toUpperCase(); - }, - - cruel: function(world) { - return "cruel " + world.toUpperCase(); - } - }; - - var context = { - goodbye: "goodbye", - world: "world" - }; - - var result = template(context, {helpers: helpers}); - equals(result, "GOODBYE cruel WORLD", "Helper executed"); -}); - -test("helpers take precedence over same-named context properties$", function() { - var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}"); - - var helpers = { - goodbye: function(options) { - return this.goodbye.toUpperCase() + options.fn(this); - }, - - cruel: function(world) { - return "cruel " + world.toUpperCase(); - } - }; - - var context = { - goodbye: "goodbye", - world: "world" - }; - - var result = template(context, {helpers: helpers}); - equals(result, "GOODBYE cruel WORLD", "Helper executed"); -}); - -test("Scoped names take precedence over helpers", function() { - var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}"); - - var helpers = { - goodbye: function() { - return this.goodbye.toUpperCase(); - }, - - cruel: function(world) { - return "cruel " + world.toUpperCase(); - }, - }; - - var context = { - goodbye: "goodbye", - world: "world" - }; - - var result = template(context, {helpers: helpers}); - equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed"); -}); - -test("Scoped names take precedence over block helpers", function() { - var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}"); - - var helpers = { - goodbye: function(options) { - return this.goodbye.toUpperCase() + options.fn(this); - }, - - cruel: function(world) { - return "cruel " + world.toUpperCase(); - }, - }; - - var context = { - goodbye: "goodbye", - world: "world" - }; - - var result = template(context, {helpers: helpers}); - equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed"); -}); - -test("helpers can take an optional hash", function() { - var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}'); - - var helpers = { - goodbye: function(options) { - return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES"; - } - }; - - var context = {}; - - var result = template(context, {helpers: helpers}); - equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash"); -}); - -test("helpers can take an optional hash with booleans", function() { - var helpers = { - goodbye: function(options) { - if (options.hash.print === true) { - return "GOODBYE " + options.hash.cruel + " " + options.hash.world; - } else if (options.hash.print === false) { - return "NOT PRINTING"; - } else { - return "THIS SHOULD NOT HAPPEN"; - } - } - }; - - var context = {}; - - var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}'); - var result = template(context, {helpers: helpers}); - equals(result, "GOODBYE CRUEL WORLD", "Helper output hash"); - - template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}'); - result = template(context, {helpers: helpers}); - equals(result, "NOT PRINTING", "Boolean helper parameter honored"); -}); - -test("block helpers can take an optional hash", function() { - var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}'); - - var helpers = { - goodbye: function(options) { - return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES"; - } - }; - - var result = template({}, {helpers: helpers}); - equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output"); -}); - -test("block helpers can take an optional hash with single quoted stings", function() { - var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}"); - - var helpers = { - goodbye: function(options) { - return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES"; - } - }; - - var result = template({}, {helpers: helpers}); - equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output"); -}); - -test("block helpers can take an optional hash with booleans", function() { - var helpers = { - goodbye: function(options) { - if (options.hash.print === true) { - return "GOODBYE " + options.hash.cruel + " " + options.fn(this); - } else if (options.hash.print === false) { - return "NOT PRINTING"; - } else { - return "THIS SHOULD NOT HAPPEN"; - } - } - }; - - var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}'); - var result = template({}, {helpers: helpers}); - equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored"); - - template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}'); - result = template({}, {helpers: helpers}); - equals(result, "NOT PRINTING", "Boolean hash parameter honored"); -}); - - -test("arguments to helpers can be retrieved from options hash in string form", function() { - var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true}); - - var helpers = { - wycats: function(passiveVoice, noun) { - return "HELP ME MY BOSS " + passiveVoice + ' ' + noun; - } - }; - - var result = template({}, {helpers: helpers}); - - equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output"); -}); - -test("when using block form, arguments to helpers can be retrieved from options hash in string form", function() { - var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true}); - - var helpers = { - wycats: function(passiveVoice, noun, options) { - return "HELP ME MY BOSS " + passiveVoice + ' ' + - noun + ': ' + options.fn(this); - } - }; - - var result = template({}, {helpers: helpers}); - - equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output"); -}); - -test("when inside a block in String mode, .. passes the appropriate context in the options hash", function() { - var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true}); - - var helpers = { - tomdale: function(desire, noun, options) { - return "STOP ME FROM READING HACKER NEWS I " + - options.contexts[0][desire] + " " + noun; - }, - - "with": function(context, options) { - return options.fn(options.contexts[0][context]); - } - }; - - var result = template({ - dale: {}, - - need: 'need-a' - }, {helpers: helpers}); - - equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output"); -}); - -test("in string mode, information about the types is passed along", function() { - var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true }); - - var helpers = { - tomdale: function(desire, noun, trueBool, falseBool, options) { - equal(options.types[0], 'STRING', "the string type is passed"); - equal(options.types[1], 'ID', "the expression type is passed"); - equal(options.types[2], 'BOOLEAN', "the expression type is passed"); - equal(desire, "need", "the string form is passed for strings"); - equal(noun, "dad.joke", "the string form is passed for expressions"); - equal(trueBool, true, "raw booleans are passed through"); - equal(falseBool, false, "raw booleans are passed through"); - return "Helper called"; - } - }; - - var result = template({}, { helpers: helpers }); - equal(result, "Helper called"); -}); - -test("in string mode, hash parameters get type information", function() { - var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true }); - - var helpers = { - tomdale: function(exclamation, options) { - equal(exclamation, "he.says"); - equal(options.types[0], "ID"); - - equal(options.hashTypes.desire, "STRING"); - equal(options.hashTypes.noun, "ID"); - equal(options.hashTypes.bool, "BOOLEAN"); - equal(options.hash.desire, "need"); - equal(options.hash.noun, "dad.joke"); - equal(options.hash.bool, true); - return "Helper called"; - } - }; - - var result = template({}, { helpers: helpers }); - equal(result, "Helper called"); -}); - -test("in string mode, hash parameters get context information", function() { - var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true }); - - var context = {dale: {}}; - - var helpers = { - tomdale: function(exclamation, options) { - equal(exclamation, "he.says"); - equal(options.types[0], "ID"); - - equal(options.contexts.length, 1); - equal(options.hashContexts.noun, context); - equal(options.hash.desire, "need"); - equal(options.hash.noun, "dad.joke"); - equal(options.hash.bool, true); - return "Helper called"; - }, - "with": function(context, options) { - return options.fn(options.contexts[0][context]); - } - }; - - var result = template(context, { helpers: helpers }); - equal(result, "Helper called"); -}); - -test("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() { - var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true}); - - var helpers = { - tomdale: function(desire, noun, options) { - return "STOP ME FROM READING HACKER NEWS I " + - options.contexts[0][desire] + " " + noun + " " + - options.fn(this); - }, - - "with": function(context, options) { - return options.fn(options.contexts[0][context]); - } - }; - - var result = template({ - dale: {}, - - need: 'need-a' - }, {helpers: helpers}); - - equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output"); -}); - -suite("Regressions"); - -test("GH-94: Cannot read property of undefined", function() { - var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]}; - var string = "{{#books}}{{title}}{{author.name}}{{/books}}"; - shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes", - "Renders without an undefined property error"); -}); - -test("GH-150: Inverted sections print when they shouldn't", function() { - var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}"; - - shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context"); - shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined"); - shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false"); - shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true"); -}); - -test("Mustache man page", function() { - var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}"; - var data = { - "name": "Chris", - "value": 10000, - "taxed_value": 10000 - (10000 * 0.4), - "in_ca": true - }; - - shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works"); -}); - -test("GH-158: Using array index twice, breaks the template", function() { - var string = "{{arr.[0]}}, {{arr.[1]}}"; - var data = { "arr": [1,2] }; - - shouldCompileTo(string, data, "1, 2", "it works as expected"); -}); - -test("bug reported by @fat where lambdas weren't being properly resolved", function() { - var string = "<strong>This is a slightly more complicated {{thing}}.</strong>.\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n<ul>\n{{#things}}\n<li class={{className}}>{{word}}</li>\n{{/things}}</ul>.\n{{/hasThings}}\n{{^hasThings}}\n\n<small>Nothing to check out...</small>\n{{/hasThings}}"; - var data = { - thing: function() { - return "blah"; - }, - things: [ - {className: "one", word: "@fat"}, - {className: "two", word: "@dhg"}, - {className: "three", word:"@sayrer"} - ], - hasThings: function() { - return true; - } - }; - - var output = "<strong>This is a slightly more complicated blah.</strong>.\n\nCheck this out:\n\n<ul>\n\n<li class=one>@fat</li>\n\n<li class=two>@dhg</li>\n\n<li class=three>@sayrer</li>\n</ul>.\n\n"; - shouldCompileTo(string, data, output); -}); - -test("Passing falsy values to Handlebars.compile throws an error", function() { - shouldThrow(function() { - CompilerContext.compile(null); - }, "You must pass a string or Handlebars AST to Handlebars.precompile. You passed null"); -}); - -test("can pass through an already-compiled AST via compile/precompile", function() { - equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello') -}); - -test('GH-408: Multiple loops fail', function() { - var context = [ - { name: "John Doe", location: { city: "Chicago" } }, - { name: "Jane Doe", location: { city: "New York"} } - ]; - - var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}'); - - var result = template(context); - equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times'); -}); - -test('GS-428: Nested if else rendering', function() { - var succeedingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; - var failingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; - - var helpers = { - blk: function(block) { return block.fn(''); }, - inverse: function(block) { return block.inverse(''); } - }; - - shouldCompileTo(succeedingTemplate, [{}, helpers], ' Expected '); - shouldCompileTo(failingTemplate, [{}, helpers], ' Expected '); -}); - -test('GH-458: Scoped this identifier', function() { - shouldCompileTo('{{./foo}}', {foo: 'bar'}, 'bar'); -}); - -test('GH-375: Unicode line terminators', function() { - shouldCompileTo('\u2028', {}, '\u2028'); -}); - -test('GH-534: Object prototype aliases', function() { - Object.prototype[0xD834] = true; - - shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar'); - - delete Object.prototype[0xD834]; -}); - -test('GH-437: Matching escaping', function() { - shouldThrow(function() { - CompilerContext.compile('{{{a}}'); - }, Error); - shouldThrow(function() { - CompilerContext.compile('{{a}}}'); - }, Error); -}); - -suite('Utils'); - -test('escapeExpression', function() { - equal(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo<&"'>'); - equal(Handlebars.Utils.escapeExpression(new Handlebars.SafeString('foo<&"\'>')), 'foo<&"\'>'); - equal(Handlebars.Utils.escapeExpression(''), ''); - equal(Handlebars.Utils.escapeExpression(undefined), ''); - equal(Handlebars.Utils.escapeExpression(null), ''); - equal(Handlebars.Utils.escapeExpression(false), ''); - - equal(Handlebars.Utils.escapeExpression(0), '0'); - equal(Handlebars.Utils.escapeExpression({}), {}.toString()); - equal(Handlebars.Utils.escapeExpression([]), [].toString()); -}); - -test('isEmpty', function() { - equal(Handlebars.Utils.isEmpty(undefined), true); - equal(Handlebars.Utils.isEmpty(null), true); - equal(Handlebars.Utils.isEmpty(false), true); - equal(Handlebars.Utils.isEmpty(''), true); - equal(Handlebars.Utils.isEmpty([]), true); - - equal(Handlebars.Utils.isEmpty(0), false); - equal(Handlebars.Utils.isEmpty([1]), false); - equal(Handlebars.Utils.isEmpty('foo'), false); - equal(Handlebars.Utils.isEmpty({bar: 1}), false); -}); - -if (typeof(require) !== 'undefined') { - suite('Require'); - - test('Load .handlebars files with require()', function() { - var template = require("./example_1"); - assert.deepEqual(template, require("./example_1.handlebars")); - - var expected = 'foo\n'; - var result = template({foo: "foo"}); - - equal(result, expected); - }); - - test('Load .hbs files with require()', function() { - var template = require("./example_2"); - assert.deepEqual(template, require("./example_2.hbs")); - - var expected = 'Hello, World!\n'; - var result = template({name: "World"}); - - equal(result, expected); - }); -} diff --git a/spec/regressions.js b/spec/regressions.js new file mode 100644 index 0000000..44bde81 --- /dev/null +++ b/spec/regressions.js @@ -0,0 +1,119 @@ +/*global CompilerContext, shouldCompileTo */ +describe('Regressions', function() { + it("GH-94: Cannot read property of undefined", function() { + var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]}; + var string = "{{#books}}{{title}}{{author.name}}{{/books}}"; + shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes", + "Renders without an undefined property error"); + }); + + it("GH-150: Inverted sections print when they shouldn't", function() { + var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}"; + + shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context"); + shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined"); + shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false"); + shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true"); + }); + + it("GH-158: Using array index twice, breaks the template", function() { + var string = "{{arr.[0]}}, {{arr.[1]}}"; + var data = { "arr": [1,2] }; + + shouldCompileTo(string, data, "1, 2", "it works as expected"); + }); + + it("bug reported by @fat where lambdas weren't being properly resolved", function() { + var string = "<strong>This is a slightly more complicated {{thing}}.</strong>.\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n<ul>\n{{#things}}\n<li class={{className}}>{{word}}</li>\n{{/things}}</ul>.\n{{/hasThings}}\n{{^hasThings}}\n\n<small>Nothing to check out...</small>\n{{/hasThings}}"; + var data = { + thing: function() { + return "blah"; + }, + things: [ + {className: "one", word: "@fat"}, + {className: "two", word: "@dhg"}, + {className: "three", word:"@sayrer"} + ], + hasThings: function() { + return true; + } + }; + + var output = "<strong>This is a slightly more complicated blah.</strong>.\n\nCheck this out:\n\n<ul>\n\n<li class=one>@fat</li>\n\n<li class=two>@dhg</li>\n\n<li class=three>@sayrer</li>\n</ul>.\n\n"; + shouldCompileTo(string, data, output); + }); + + it('GH-408: Multiple loops fail', function() { + var context = [ + { name: "John Doe", location: { city: "Chicago" } }, + { name: "Jane Doe", location: { city: "New York"} } + ]; + + var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}'); + + var result = template(context); + equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times'); + }); + + it('GS-428: Nested if else rendering', function() { + var succeedingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; + var failingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; + + var helpers = { + blk: function(block) { return block.fn(''); }, + inverse: function(block) { return block.inverse(''); } + }; + + shouldCompileTo(succeedingTemplate, [{}, helpers], ' Expected '); + shouldCompileTo(failingTemplate, [{}, helpers], ' Expected '); + }); + + it('GH-458: Scoped this identifier', function() { + shouldCompileTo('{{./foo}}', {foo: 'bar'}, 'bar'); + }); + + it('GH-375: Unicode line terminators', function() { + shouldCompileTo('\u2028', {}, '\u2028'); + }); + + it('GH-534: Object prototype aliases', function() { + Object.prototype[0xD834] = true; + + shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar'); + + delete Object.prototype[0xD834]; + }); + + it('GH-437: Matching escaping', function() { + (function() { + CompilerContext.compile('{{{a}}'); + }).should.throw(Error); + (function() { + CompilerContext.compile('{{a}}}'); + }).should.throw(Error); + }); + + it("Mustache man page", function() { + var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}"; + var data = { + "name": "Chris", + "value": 10000, + "taxed_value": 10000 - (10000 * 0.4), + "in_ca": true + }; + + shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works"); + }); + + it("Passing falsy values to Handlebars.compile throws an error", function() { + (function() { + CompilerContext.compile(null); + }).should.throw("You must pass a string or Handlebars AST to Handlebars.precompile. You passed null"); + }); + + if (Handlebars.AST) { + it("can pass through an already-compiled AST via compile/precompile", function() { + equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello'); + }); + } +}); diff --git a/spec/require.js b/spec/require.js new file mode 100644 index 0000000..d750d7d --- /dev/null +++ b/spec/require.js @@ -0,0 +1,23 @@ +if (typeof(require) !== 'undefined' && require.extensions[".handlebars"]) { + describe('Require', function() { + it('Load .handlebars files with require()', function() { + var template = require("./artifacts/example_1"); + template.should.eql(require("./artifacts/example_1.handlebars")); + + var expected = 'foo\n'; + var result = template({foo: "foo"}); + + result.should.equal(expected); + }); + + it('Load .hbs files with require()', function() { + var template = require("./artifacts/example_2"); + template.should.eql(require("./artifacts/example_2.hbs")); + + var expected = 'Hello, World!\n'; + var result = template({name: "World"}); + + result.should.equal(expected); + }); + }); +} diff --git a/spec/source-map.js b/spec/source-map.js new file mode 100644 index 0000000..9b9fa51 --- /dev/null +++ b/spec/source-map.js @@ -0,0 +1,18 @@ +var sourceMap = require('source-map'), + SourceMapConsumer = sourceMap.SourceMapConsumer; + +describe('source-map', function() { + if (!Handlebars.Parser) { + return; + } + + it('returns source map', function() { + var template = Handlebars.precompile('{{foo}}', {filename: 'foo.handlebars', sourcemap: true}); + (typeof template.template).should.equal('string'); + (typeof template.sourcemap).should.equal('string'); + return; + var consumer = new SourceMapConsumer(template.sourcemap); + consumer.eachMapping(function(mapping) { + }, this, SourceMapConsumer.ORIGINAL_ORDER); + }); +}); diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index eb2f26a..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,132 +0,0 @@ -require "v8" - -# Monkey patches due to bugs in RubyRacer -class V8::JSError - def initialize(try, to) - @to = to - begin - super(initialize_unsafe(try)) - rescue Exception => e - # Original code does not make an Array here - @boundaries = [Boundary.new(:rbframes => e.backtrace)] - @value = e - super("BUG! please report. JSError#initialize failed!: #{e.message}") - end - end - - def parse_js_frames(try) - raw = @to.rb(try.StackTrace()) - if raw && !raw.empty? - raw.split("\n")[1..-1].tap do |frames| - # Original code uses strip!, and the frames are not guaranteed to be strippable - frames.each {|frame| frame.strip.chomp!(",")} - end - else - [] - end - end -end - -module Handlebars - module Spec - def self.js_backtrace(context) - begin - context.eval("throw") - rescue V8::JSError => e - return e.backtrace(:javascript) - end - end - - def self.remove_exports(string) - match = string.match(%r{\A(.*?)^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)(.*?)\Z}m) - prelines = match ? match[1].count("\n") + 1 : 0 - ret = match ? match[2] : string - ("\n" * prelines) + ret - end - - def self.load_helpers(context) - context["exports"] = nil - - context["p"] = proc do |this, val| - p val if ENV["DEBUG_JS"] - end - - context["puts"] = proc do |this, val| - puts val if ENV["DEBUG_JS"] - end - - context["puts_node"] = proc do |this, val| - puts context["Handlebars"]["PrintVisitor"].new.accept(val) - puts - end - - context["puts_caller"] = proc do - puts "BACKTRACE:" - puts Handlebars::Spec.js_backtrace(context) - puts - end - end - - def self.js_load(context, file) - str = File.read(file) - context.eval(remove_exports(str), file) - end - - CONTEXT = V8::Context.new - CONTEXT.instance_eval do |context| - Handlebars::Spec.load_helpers(context); - - Handlebars::Spec.js_load(context, 'dist/handlebars.js'); - - context["CompilerContext"] = {} - CompilerContext = context["CompilerContext"] - CompilerContext["compile"] = proc do |this, *args| - template, options = args[0], args[1] || nil - templateSpec = COMPILE_CONTEXT["Handlebars"]["precompile"].call(template, options); - context["Handlebars"]["template"].call(context.eval("(#{templateSpec})")); - end - CompilerContext["compileWithPartial"] = proc do |this, *args| - template, options = args[0], args[1] || nil - context["Handlebars"]["compile"].call(template, options); - end - end - - PARSER_CONTEXT = V8::Context.new - PARSER_CONTEXT.instance_eval do |context| - Handlebars::Spec.load_helpers(context); - - Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/parser.js'); - end - - COMPILE_CONTEXT = V8::Context.new - COMPILE_CONTEXT.instance_eval do |context| - Handlebars::Spec.load_helpers(context); - - Handlebars::Spec.js_load(context, 'dist/handlebars.js'); - Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/visitor.js'); - Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/printer.js'); - - context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4 - - context["Handlebars"]["logger"]["log"] = proc do |this, level, str| - logger_level = context["Handlebars"]["logger"]["level"].to_i - - if logger_level <= level - puts str - end - end - end - end -end - - -require "test/unit/assertions" - -RSpec.configure do |config| - config.include Test::Unit::Assertions - - # Each is required to allow classes to mark themselves as compiler tests - config.before(:each) do - @context = @compiles ? Handlebars::Spec::COMPILE_CONTEXT : Handlebars::Spec::CONTEXT - end -end diff --git a/spec/string-params.js b/spec/string-params.js new file mode 100644 index 0000000..1ebb583 --- /dev/null +++ b/spec/string-params.js @@ -0,0 +1,145 @@ +describe('string params mode', function() { + it("arguments to helpers can be retrieved from options hash in string form", function() { + var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true}); + + var helpers = { + wycats: function(passiveVoice, noun) { + return "HELP ME MY BOSS " + passiveVoice + ' ' + noun; + } + }; + + var result = template({}, {helpers: helpers}); + + equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output"); + }); + + it("when using block form, arguments to helpers can be retrieved from options hash in string form", function() { + var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true}); + + var helpers = { + wycats: function(passiveVoice, noun, options) { + return "HELP ME MY BOSS " + passiveVoice + ' ' + + noun + ': ' + options.fn(this); + } + }; + + var result = template({}, {helpers: helpers}); + + equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output"); + }); + + it("when inside a block in String mode, .. passes the appropriate context in the options hash", function() { + var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true}); + + var helpers = { + tomdale: function(desire, noun, options) { + return "STOP ME FROM READING HACKER NEWS I " + + options.contexts[0][desire] + " " + noun; + }, + + "with": function(context, options) { + return options.fn(options.contexts[0][context]); + } + }; + + var result = template({ + dale: {}, + + need: 'need-a' + }, {helpers: helpers}); + + equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output"); + }); + + it("information about the types is passed along", function() { + var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true }); + + var helpers = { + tomdale: function(desire, noun, trueBool, falseBool, options) { + equal(options.types[0], 'STRING', "the string type is passed"); + equal(options.types[1], 'ID', "the expression type is passed"); + equal(options.types[2], 'BOOLEAN', "the expression type is passed"); + equal(desire, "need", "the string form is passed for strings"); + equal(noun, "dad.joke", "the string form is passed for expressions"); + equal(trueBool, true, "raw booleans are passed through"); + equal(falseBool, false, "raw booleans are passed through"); + return "Helper called"; + } + }; + + var result = template({}, { helpers: helpers }); + equal(result, "Helper called"); + }); + + it("hash parameters get type information", function() { + var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true }); + + var helpers = { + tomdale: function(exclamation, options) { + equal(exclamation, "he.says"); + equal(options.types[0], "ID"); + + equal(options.hashTypes.desire, "STRING"); + equal(options.hashTypes.noun, "ID"); + equal(options.hashTypes.bool, "BOOLEAN"); + equal(options.hash.desire, "need"); + equal(options.hash.noun, "dad.joke"); + equal(options.hash.bool, true); + return "Helper called"; + } + }; + + var result = template({}, { helpers: helpers }); + equal(result, "Helper called"); + }); + + it("hash parameters get context information", function() { + var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true }); + + var context = {dale: {}}; + + var helpers = { + tomdale: function(exclamation, options) { + equal(exclamation, "he.says"); + equal(options.types[0], "ID"); + + equal(options.contexts.length, 1); + equal(options.hashContexts.noun, context); + equal(options.hash.desire, "need"); + equal(options.hash.noun, "dad.joke"); + equal(options.hash.bool, true); + return "Helper called"; + }, + "with": function(context, options) { + return options.fn(options.contexts[0][context]); + } + }; + + var result = template(context, { helpers: helpers }); + equal(result, "Helper called"); + }); + + it("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() { + var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true}); + + var helpers = { + tomdale: function(desire, noun, options) { + return "STOP ME FROM READING HACKER NEWS I " + + options.contexts[0][desire] + " " + noun + " " + + options.fn(this); + }, + + "with": function(context, options) { + return options.fn(options.contexts[0][context]); + } + }; + + var result = template({ + dale: {}, + + need: 'need-a' + }, {helpers: helpers}); + + equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output"); + }); +}); diff --git a/spec/tokenizer.js b/spec/tokenizer.js index c69f406..f86774a 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -1,5 +1,4 @@ -var Handlebars = require('../lib/handlebars'), - should = require('should'); +var should = require('should'); should.Assertion.prototype.match_tokens = function(tokens) { this.obj.forEach(function(value, index) { @@ -11,6 +10,10 @@ should.Assertion.prototype.be_token = function(name, text) { }; describe('Tokenizer', function() { + if (!Handlebars.Parser) { + return; + } + function tokenize(template) { var parser = Handlebars.Parser, lexer = parser.lexer; diff --git a/spec/utils.js b/spec/utils.js new file mode 100644 index 0000000..135beb4 --- /dev/null +++ b/spec/utils.js @@ -0,0 +1,56 @@ +/*global shouldCompileTo */ +describe('utils', function() { + describe('#SafeString', function() { + it("constructing a safestring from a string and checking its type", function() { + var safe = new Handlebars.SafeString("testing 1, 2, 3"); + safe.should.be.instanceof(Handlebars.SafeString); + (safe == "testing 1, 2, 3").should.equal(true, "SafeString is equivalent to its underlying string"); + }); + + it("it should not escape SafeString properties", function() { + var name = new Handlebars.SafeString("<em>Sean O'Malley</em>"); + + shouldCompileTo('{{name}}', [{ name: name }], "<em>Sean O'Malley</em>"); + }); + }); + + describe('#escapeExpression', function() { + it('shouhld escape html', function() { + Handlebars.Utils.escapeExpression('foo<&"\'>').should.equal('foo<&"'>'); + }); + it('should not escape SafeString', function() { + var string = new Handlebars.SafeString('foo<&"\'>'); + Handlebars.Utils.escapeExpression(string).should.equal('foo<&"\'>'); + + }); + it('should handle falsy', function() { + Handlebars.Utils.escapeExpression('').should.equal(''); + Handlebars.Utils.escapeExpression(undefined).should.equal(''); + Handlebars.Utils.escapeExpression(null).should.equal(''); + Handlebars.Utils.escapeExpression(false).should.equal(''); + + Handlebars.Utils.escapeExpression(0).should.equal('0'); + }); + it('should handle empty objects', function() { + Handlebars.Utils.escapeExpression({}).should.equal({}.toString()); + Handlebars.Utils.escapeExpression([]).should.equal([].toString()); + }); + }); + + describe('#isEmpty', function() { + it('should not be empty', function() { + Handlebars.Utils.isEmpty(undefined).should.equal(true); + Handlebars.Utils.isEmpty(null).should.equal(true); + Handlebars.Utils.isEmpty(false).should.equal(true); + Handlebars.Utils.isEmpty('').should.equal(true); + Handlebars.Utils.isEmpty([]).should.equal(true); + }); + + it('should be empty', function() { + Handlebars.Utils.isEmpty(0).should.equal(false); + Handlebars.Utils.isEmpty([1]).should.equal(false); + Handlebars.Utils.isEmpty('foo').should.equal(false); + Handlebars.Utils.isEmpty({bar: 1}).should.equal(false); + }); + }); +}); |