From aaa590d846048cf1e5a1d13e0211c48ed3d5e417 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 1 Sep 2014 21:42:45 -0500 Subject: [PATCH 01/99] Fix non-prerelease handling for gemspec --- components/handlebars-source.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/handlebars-source.gemspec b/components/handlebars-source.gemspec index f9e1a4bc4..7b5d59566 100644 --- a/components/handlebars-source.gemspec +++ b/components/handlebars-source.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |gem| gem.description = %q{Handlebars.js source code wrapper for (pre)compilation gems.} gem.summary = %q{Handlebars.js source code wrapper} gem.homepage = "https://github.com/wycats/handlebars.js/" - gem.version = package["version"].sub! "-", "." + gem.version = package["version"].sub "-", "." gem.license = "MIT" gem.files = [ From 8aa2a34a8d2453a45a3bc9d45e1fbed17e4d8bbc Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 2 Sep 2014 00:14:10 -0500 Subject: [PATCH 02/99] Update jsfiddle link --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6db72a75..4c5256b41 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,4 +75,4 @@ After this point the handlebars site needs to be updated to point to the new ver [generator-release]: https://github.com/walmartlabs/generator-release [pull-request]: https://github.com/wycats/handlebars.js/pull/new/master [issue]: https://github.com/wycats/handlebars.js/issues/new -[jsfiddle]: http://jsfiddle.net/9D88g/25/ +[jsfiddle]: http://jsfiddle.net/9D88g/26/ From 62ed555b2c8d9e0fd37a1384dadfaeb3a7a94e61 Mon Sep 17 00:00:00 2001 From: tomByrer Date: Sat, 6 Sep 2014 08:08:23 -0600 Subject: [PATCH 03/99] + jsDelivr CDN info Thanks to libgrabber, every time a [new release is posted](https://github.com/components/handlebars.js/releases), jsDelivr will find it & automatically upload it. If you change your file structure, please updated the [include section](https://github.com/jsdelivr/jsdelivr/blob/master/files/handlebarsjs/update.json) or ask us to do it. I was going to link CDNJS, but they [haven't uploaded to v2.0.0](http://cdnjs.com/libraries/handlebars.js). Nothing against those cats; we [use their network](http://www.jsdelivr.com/network.php). --- README.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.markdown b/README.markdown index 77552860d..04d609701 100644 --- a/README.markdown +++ b/README.markdown @@ -16,6 +16,8 @@ Installing ---------- Installing Handlebars is easy. Simply download the package [from the official site](http://handlebarsjs.com/) or the [bower repository][bower-repo] and add it to your web pages (you should usually use the most recent version). +For web browsers, a free CDN is available at [jsDelivr](http://www.jsdelivr.com/#!handlebarsjs). Advanced usage, such as [version aliasing & concocting](https://github.com/jsdelivr/jsdelivr#usage), is available. + Alternatively, if you prefer having the latest version of handlebars from the 'master' branch, passing builds of the 'master' branch are automatically published to S3. You may download the latest passing master build by grabbing From 5fe7ef63d862d3aabbd24509e3fc08aba9f45d06 Mon Sep 17 00:00:00 2001 From: Jon Sadka Date: Thu, 18 Sep 2014 22:08:51 -0700 Subject: [PATCH 04/99] Update FAQ.md Fixed misspelling of sufficent to sufficient on Line 60 --- FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index 0bd0997d6..108e839af 100644 --- a/FAQ.md +++ b/FAQ.md @@ -57,4 +57,4 @@ The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility. - If not using ES6 transpilers or accessing submodules in the build the former option should be sufficent for most use cases. + If not using ES6 transpilers or accessing submodules in the build the former option should be sufficient for most use cases. From a0e2e200d91c719f6475f91f20fdaea2b7886340 Mon Sep 17 00:00:00 2001 From: Maxb Date: Fri, 19 Sep 2014 11:35:16 -0700 Subject: [PATCH 05/99] Registering undefined partial throws exception. --- lib/handlebars/base.js | 3 +++ spec/partials.js | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 76f53e276..e0fda81d5 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -47,6 +47,9 @@ HandlebarsEnvironment.prototype = { if (toString.call(name) === objectType) { Utils.extend(this.partials, name); } else { + if (typeof partial === 'undefined') { + throw new Exception('Attempting to register a partial as undefined'); + } this.partials[name] = partial; } }, diff --git a/spec/partials.js b/spec/partials.js index 20187f81d..e239b7ffb 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -46,6 +46,13 @@ describe('partials', function() { }, Handlebars.Exception, 'The partial whatever could not be found'); }); + it("registering undefined partial throws an exception", function() { + shouldThrow(function() { + var undef; + handlebarsEnv.registerPartial('undefined_test', undef); + }, Handlebars.Exception, 'Attempting to register a partial as undefined'); + }); + it("rendering template partial in vm mode throws an exception", function() { shouldThrow(function() { var template = CompilerContext.compile("{{> whatever}}"); From b87e601063c0d943038d22fd74ba2435d760adc1 Mon Sep 17 00:00:00 2001 From: Maxb Date: Fri, 19 Sep 2014 11:46:02 -0700 Subject: [PATCH 06/99] Fixing indentation in spec/partials.js --- spec/partials.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/partials.js b/spec/partials.js index e239b7ffb..0863487e8 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -41,8 +41,8 @@ describe('partials', function() { it("rendering undefined partial throws an exception", function() { shouldThrow(function() { - var template = CompilerContext.compile("{{> whatever}}"); - template(); + var template = CompilerContext.compile("{{> whatever}}"); + template(); }, Handlebars.Exception, 'The partial whatever could not be found'); }); @@ -71,10 +71,10 @@ describe('partials', function() { }); 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"); + 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() { From 4aab967796affd593324b3ed88101c4b93f18d5c Mon Sep 17 00:00:00 2001 From: Alex Jeng Date: Tue, 23 Sep 2014 11:59:30 -0700 Subject: [PATCH 07/99] Capitalization change in comments for consistency --- lib/handlebars/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 76f53e276..19139b672 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -209,7 +209,7 @@ export var logger = { ERROR: 3, level: 3, - // can be overridden in the host environment + // Can be overridden in the host environment log: function(level, message) { if (logger.level <= level) { var method = logger.methodMap[level]; From 4282668d47b90da0d00cf4c4a86977f18fc8cde4 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 27 Oct 2014 00:23:32 -0500 Subject: [PATCH 08/99] Implement parser for else chaining of helpers Allows users to chain multiple helpers together using their inverse callbacks. I.e. ``` {{#if foo}} {{else if bar}} {{else}} {{/if}} ``` The control flow here effectively causes the helpers to be nested. The above is actually syntactic sugar for this: ``` {{#if foo}} {{else}} {{#if bar}} {{else}} {{/if}} {{/if}} ``` Any helper may be used in this manner, the only requirement is they support normal calls and inverse calls. Introduces a breaking change in that `{{else foo}}` may no longer be used as a root level operator. Instead `{{^foo}}` must be used. Fixes #72. --- lib/handlebars/compiler/helpers.js | 33 +++++++++++++++++++++++------- spec/blocks.js | 20 ++++++++++++++++++ spec/parser.js | 10 +++++++-- src/handlebars.l | 2 +- src/handlebars.yy | 18 +++++++++++++++- 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 758c740d5..b375479a6 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -10,11 +10,29 @@ export function stripFlags(open, close) { export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { /*jshint -W040 */ - if (mustache.sexpr.id.original !== close.path.original) { + // When we are chaining inverse calls, we will not have a close path + if (close && close.path && (mustache.sexpr.id.original !== close.path.original)) { throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache); } - var inverse = inverseAndProgram && inverseAndProgram.program; + // Safely handle a chained inverse that does not have a non-conditional inverse + // (i.e. both inverseAndProgram AND close are undefined) + if (!close) { + close = {strip: {}}; + } + + // Find the inverse program that is involed with whitespace stripping. + var inverse = inverseAndProgram && inverseAndProgram.program, + firstInverse = inverse, + lastInverse = inverse; + if (inverse && inverse.inverse) { + firstInverse = inverse.statements[0].program; + + // Walk the inverse chain to find the last inverse that is actually in the chain. + while (lastInverse.inverse) { + lastInverse = lastInverse.statements[lastInverse.statements.length-1].program; + } + } var strip = { left: mustache.strip.left, @@ -23,7 +41,7 @@ export function prepareBlock(mustache, program, inverseAndProgram, close, invert // Determine the standalone candiacy. Basically flag our content as being possibly standalone // so our parent can determine if we actually are standalone openStandalone: isNextWhitespace(program.statements), - closeStandalone: isPrevWhitespace((inverse || program).statements) + closeStandalone: isPrevWhitespace((firstInverse || program).statements) }; if (mustache.strip.right) { @@ -36,19 +54,20 @@ export function prepareBlock(mustache, program, inverseAndProgram, close, invert if (inverseStrip.left) { omitLeft(program.statements, null, true); } + if (inverseStrip.right) { - omitRight(inverse.statements, null, true); + omitRight(firstInverse.statements, null, true); } if (close.strip.left) { - omitLeft(inverse.statements, null, true); + omitLeft(lastInverse.statements, null, true); } // Find standalone else statments if (isPrevWhitespace(program.statements) - && isNextWhitespace(inverse.statements)) { + && isNextWhitespace(firstInverse.statements)) { omitLeft(program.statements); - omitRight(inverse.statements); + omitRight(firstInverse.statements); } } else { if (close.strip.left) { diff --git a/spec/blocks.js b/spec/blocks.js index a172970d8..21fb71842 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -89,6 +89,20 @@ describe('blocks', function() { shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, "No people"); }); + it("chained inverted sections", function() { + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/people}}", {none: "No people"}, + "No people"); + shouldCompileTo("{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}", {none: "No people"}, + "No people"); + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}", {none: "No people"}, + "No people"); + }); + it("chained inverted sections with mismatch", function() { + shouldThrow(function() { + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/if}}", {none: "No people"}, + "No people"); + }, Error); + }); it("block inverted sections with empty arrays", function() { shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, @@ -105,6 +119,12 @@ describe('blocks', function() { shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, 'No people\n'); }); + it('block standalone chained else sections', function() { + shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, + 'No people\n'); + shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n', {none: 'No people'}, + 'No people\n'); + }); it('should handle nesting', function() { shouldCompileTo('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.', {data: [1, 3, 5]}, '1\n3\n5\nOK.'); }); diff --git a/spec/parser.js b/spec/parser.js index 131160a74..3686363c7 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -116,6 +116,10 @@ describe('parser', function() { equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); + it('parses multiple inverse sections', function() { + equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n {{ ID:if [ID:bar] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"); + }); + it('parses empty blocks', function() { equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n"); }); @@ -147,8 +151,10 @@ describe('parser', function() { it('parses a standalone inverse section', function() { equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); }); - it('parses a standalone inverse section', function() { - equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + it('throws on old inverse section', function() { + shouldThrow(function() { + equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + }, Error); }); it("raises if there's a Parse error", function() { diff --git a/src/handlebars.l b/src/handlebars.l index 0f420e7ee..f268fd152 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -73,7 +73,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} "{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; "{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; "{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE'; -"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE'; +"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE_CHAIN'; "{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED'; "{{"{LEFT_STRIP}?"&" return 'OPEN'; "{{!--" this.popState(); this.begin('com'); diff --git a/src/handlebars.yy b/src/handlebars.yy index a8d288f68..61489b5fc 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -30,7 +30,7 @@ openRawBlock ; block - : openBlock program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) + : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) ; @@ -42,10 +42,26 @@ openInverse : OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) ; +openInverseChain + : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + ; + inverseAndProgram : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 } ; +inverseChain + : openInverseChain program inverseChain? { + var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), + program = new yy.ProgramNode(yy.prepareProgram([inverse]), {}, @$); + + program.inverse = inverse; + + $$ = { strip: $1.strip, program: program, chain: true }; + } + | inverseAndProgram -> $1 + ; + closeBlock : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} ; From 9fbf7aa753f5d48951c3f373c40f787739489962 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 2 Nov 2014 11:06:52 -0600 Subject: [PATCH 09/99] Fix help message for CLI Fixes #895 --- bin/handlebars | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bin/handlebars b/bin/handlebars index 4ea329661..4007cc725 100755 --- a/bin/handlebars +++ b/bin/handlebars @@ -1,7 +1,7 @@ #!/usr/bin/env node var optimist = require('optimist') - .usage('Precompile handlebar templates.\nUsage: $0 template...', { + .usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...', { 'f': { 'type': 'string', 'description': 'Output File', @@ -80,6 +80,11 @@ var optimist = require('optimist') 'type': 'boolean', 'description': 'Prints the current compiler version', 'alias': 'version' + }, + + 'help': { + 'type': 'boolean', + 'description': 'Outputs this message' } }) @@ -89,7 +94,14 @@ var optimist = require('optimist') } }); + var argv = optimist.argv; argv.templates = argv._; delete argv._; + +if (argv.help || !argv.templates.length) { + optimist.showHelp(); + return; +} + return require('../lib/precompiler').cli(argv); From f30b3ea3293ad28bad8167e40b8372c48cb2419e Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 2 Nov 2014 11:55:52 -0600 Subject: [PATCH 10/99] Allow whitespace control on comments This changes the call signature for the CommentNode constructor, which is a potentially breaking change for AST users. Fixes #866 --- lib/handlebars/compiler/ast.js | 7 +++---- lib/handlebars/compiler/helpers.js | 5 +++++ spec/ast.js | 2 +- spec/basic.js | 3 +++ spec/tokenizer.js | 6 +++--- src/handlebars.l | 16 +++++++++++++--- src/handlebars.yy | 2 +- 7 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 49bdc3337..e05ceec63 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -196,14 +196,13 @@ var AST = { this.stringModeValue = bool === "true"; }, - CommentNode: function(comment, locInfo) { + CommentNode: function(comment, strip, locInfo) { LocationInfo.call(this, locInfo); this.type = "comment"; this.comment = comment; - this.strip = { - inlineStandalone: true - }; + this.strip = strip; + strip.inlineStandalone = true; } }; diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 758c740d5..20f13d656 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -7,6 +7,11 @@ export function stripFlags(open, close) { }; } +export function stripComment(comment) { + return comment.replace(/^\{\{~?\!-?-?/, '') + .replace(/-?-?~?\}\}$/, ''); +} + export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { /*jshint -W040 */ diff --git a/spec/ast.js b/spec/ast.js index c28e876e8..7118c72dd 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -137,7 +137,7 @@ describe('ast', function() { describe("CommentNode", function(){ it('stores location info', function(){ - var comment = new handlebarsEnv.AST.CommentNode("HI", LOCATION_INFO); + var comment = new handlebarsEnv.AST.CommentNode("HI", {}, LOCATION_INFO); testLocationInfoStorage(comment); }); }); diff --git a/spec/basic.js b/spec/basic.js index 8a9c116cf..139d6d08a 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -33,6 +33,9 @@ describe("basic context", function() { shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", "comments are ignored"); + + shouldCompileTo(' {{~! comment ~}} blah', {}, 'blah'); + shouldCompileTo(' {{~!-- long-comment --~}} blah', {}, 'blah'); }); it("boolean", function() { diff --git a/spec/tokenizer.js b/spec/tokenizer.js index 0f0dc0b88..6f5cc380d 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -217,19 +217,19 @@ describe('Tokenizer', function() { it('tokenizes a comment as "COMMENT"', function() { var result = tokenize("foo {{! this is a comment }} bar {{ baz }}"); shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); - shouldBeToken(result[1], "COMMENT", " this is a comment "); + shouldBeToken(result[1], "COMMENT", "{{! this is a comment }}"); }); it('tokenizes a block comment as "COMMENT"', function() { var result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}"); shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); - shouldBeToken(result[1], "COMMENT", " this is a {{comment}} "); + shouldBeToken(result[1], "COMMENT", "{{!-- this is a {{comment}} --}}"); }); it('tokenizes a block comment with whitespace as "COMMENT"', function() { var result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}"); shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); - shouldBeToken(result[1], "COMMENT", " this is a\n{{comment}}\n"); + shouldBeToken(result[1], "COMMENT", "{{!-- this is a\n{{comment}}\n--}}"); }); it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function() { diff --git a/src/handlebars.l b/src/handlebars.l index 0f420e7ee..fbeec34b5 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -56,7 +56,10 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} } [^\x00]*?/("{{{{/") { return 'CONTENT'; } -[\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT'; +[\s\S]*?"--"{RIGHT_STRIP}?"}}" { + this.popState(); + return 'COMMENT'; +} "(" return 'OPEN_SEXPR'; ")" return 'CLOSE_SEXPR'; @@ -76,8 +79,15 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} "{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE'; "{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED'; "{{"{LEFT_STRIP}?"&" return 'OPEN'; -"{{!--" this.popState(); this.begin('com'); -"{{!"[\s\S]*?"}}" strip(3,5); this.popState(); return 'COMMENT'; +"{{"{LEFT_STRIP}?"!--" { + this.unput(yytext); + this.popState(); + this.begin('com'); +} +"{{"{LEFT_STRIP}?"!"[\s\S]*?"}}" { + this.popState(); + return 'COMMENT'; +} "{{"{LEFT_STRIP}? return 'OPEN'; "=" return 'EQUALS'; diff --git a/src/handlebars.yy b/src/handlebars.yy index a8d288f68..ccc0d2248 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -18,7 +18,7 @@ statement | rawBlock -> $1 | partial -> $1 | CONTENT -> new yy.ContentNode($1, @$) - | COMMENT -> new yy.CommentNode($1, @$) + | COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), @$) ; rawBlock From 7c220b9af5a904f99a4d58db11afe24a8beda9d0 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 2 Nov 2014 12:09:37 -0600 Subject: [PATCH 11/99] Add test for whitespace control w/ partial indent Verifies the behaviors discussed in #858 --- spec/whitespace-control.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/whitespace-control.js b/spec/whitespace-control.js index 86364292e..cce9405dd 100644 --- a/spec/whitespace-control.js +++ b/spec/whitespace-control.js @@ -66,6 +66,9 @@ describe('whitespace control', function() { shouldCompileToWithPartials('foo {{~> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foobar'); shouldCompileToWithPartials('foo {{> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar'); shouldCompileToWithPartials('foo {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar '); + + shouldCompileToWithPartials('foo\n {{~> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foobar'); + shouldCompileToWithPartials('foo\n {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo\n bar'); }); it('should only strip whitespace once', function() { From 632fadcea39b6328983c459ff7e8c4010a976837 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 2 Nov 2014 12:19:56 -0600 Subject: [PATCH 12/99] Add preventIndent option for partials This disables the standalone partial indent behavior required by the Mustache spec and allows for users to utilize partials in the same manner as under 1.x. Fixes #858 --- lib/handlebars/compiler/compiler.js | 7 ++++++- spec/env/common.js | 2 +- spec/partials.js | 8 ++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 1aba34b4e..1303d8fc4 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -179,7 +179,12 @@ Compiler.prototype = { this.opcode('pushContext'); } - this.opcode('invokePartial', partialName.name, partial.indent || ''); + var indent = partial.indent || ''; + if (this.options.preventIndent && indent) { + this.opcode('appendContent', indent); + indent = ''; + } + this.opcode('invokePartial', partialName.name, indent); this.opcode('append'); }, diff --git a/spec/env/common.js b/spec/env/common.js index 92cc61123..62edf1d8b 100644 --- a/spec/env/common.js +++ b/spec/env/common.js @@ -18,7 +18,7 @@ global.compileWithPartials = function(string, hashOrArray, partials) { ary = []; ary.push(hashOrArray[0]); ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); - options = {compat: hashOrArray[3]}; + options = typeof hashOrArray[3] === 'object' ? hashOrArray[3] : {compat: hashOrArray[3]}; if (hashOrArray[4] != null) { options.data = !!hashOrArray[4]; ary[1].data = hashOrArray[4]; diff --git a/spec/partials.js b/spec/partials.js index 20187f81d..9c0be4002 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -167,6 +167,14 @@ describe('partials', function() { shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n"); }); + it("prevent nested indented partials", function() { + var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; + var dude = "{{name}}\n {{> url}}"; + var url = "{{url}}!\n"; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}, {preventIndent: true}], true, + "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n"); + }); }); describe('compat mode', function() { From 12e3d210e8b3b22d19823e59bb789dc8768eb34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Mu=C3=B1oz?= Date: Sun, 2 Nov 2014 14:54:54 -0500 Subject: [PATCH 13/99] Simplify BlockNode by removing intermediate MustacheNode --- lib/handlebars/compiler/ast.js | 14 ++++--------- lib/handlebars/compiler/compiler.js | 3 +-- lib/handlebars/compiler/helpers.js | 32 ++++++++++++++++++++++------- lib/handlebars/compiler/printer.js | 2 +- spec/ast.js | 2 +- spec/parser.js | 22 ++++++++++---------- src/handlebars.yy | 14 ++++++++----- 7 files changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index e05ceec63..05de762b6 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -79,11 +79,11 @@ var AST = { this.strip.inlineStandalone = true; }, - BlockNode: function(mustache, program, inverse, strip, locInfo) { + BlockNode: function(sexpr, program, inverse, strip, locInfo) { LocationInfo.call(this, locInfo); this.type = 'block'; - this.mustache = mustache; + this.sexpr = sexpr; this.program = program; this.inverse = inverse; this.strip = strip; @@ -93,17 +93,11 @@ var AST = { } }, - RawBlockNode: function(mustache, content, close, locInfo) { + RawBlockNode: function(sexpr, content, locInfo) { LocationInfo.call(this, locInfo); - if (mustache.sexpr.id.original !== close) { - throw new Exception(mustache.sexpr.id.original + " doesn't match " + close, this); - } - - content = new AST.ContentNode(content, locInfo); - this.type = 'block'; - this.mustache = mustache; + this.sexpr = sexpr; this.program = new AST.ProgramNode([content], {}, locInfo); }, diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 1303d8fc4..51e66203a 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -108,7 +108,7 @@ Compiler.prototype = { }, block: function(block) { - var mustache = block.mustache, + var sexpr = block.sexpr, program = block.program, inverse = block.inverse; @@ -120,7 +120,6 @@ Compiler.prototype = { inverse = this.compileProgram(inverse); } - var sexpr = mustache.sexpr; var type = this.classifySexpr(sexpr); if (type === "helper") { diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 20f13d656..c9a35c24d 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -12,17 +12,35 @@ export function stripComment(comment) { .replace(/-?-?~?\}\}$/, ''); } +export function prepareRawBlock(openRawBlock, content, close, locInfo) { + /*jshint -W040 */ + if (openRawBlock.sexpr.id.original !== close) { + var errorNode = { + firstLine: openRawBlock.sexpr.firstLine, + firstColumn: openRawBlock.sexpr.firstColumn + }; + + throw new Exception(openRawBlock.sexpr.id.original + " doesn't match " + close, errorNode); + } -export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { + return new this.RawBlockNode(openRawBlock.sexpr, content, locInfo); +} + +export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { /*jshint -W040 */ - if (mustache.sexpr.id.original !== close.path.original) { - throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache); + if (openBlock.sexpr.id.original !== close.path.original) { + var errorNode = { + firstLine: openBlock.sexpr.firstLine, + firstColumn: openBlock.sexpr.firstColumn + }; + + throw new Exception(openBlock.sexpr.id.original + ' doesn\'t match ' + close.path.original, errorNode); } var inverse = inverseAndProgram && inverseAndProgram.program; var strip = { - left: mustache.strip.left, + left: openBlock.strip.left, right: close.strip.right, // Determine the standalone candiacy. Basically flag our content as being possibly standalone @@ -31,7 +49,7 @@ export function prepareBlock(mustache, program, inverseAndProgram, close, invert closeStandalone: isPrevWhitespace((inverse || program).statements) }; - if (mustache.strip.right) { + if (openBlock.strip.right) { omitRight(program.statements, null, true); } @@ -62,9 +80,9 @@ export function prepareBlock(mustache, program, inverseAndProgram, close, invert } if (inverted) { - return new this.BlockNode(mustache, inverse, program, strip, locInfo); + return new this.BlockNode(openBlock.sexpr, inverse, program, strip, locInfo); } else { - return new this.BlockNode(mustache, program, inverse, strip, locInfo); + return new this.BlockNode(openBlock.sexpr, program, inverse, strip, locInfo); } } diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 7654245c5..c329373ea 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -40,7 +40,7 @@ PrintVisitor.prototype.block = function(block) { out = out + this.pad("BLOCK:"); this.padding++; - out = out + this.accept(block.mustache); + out = out + this.pad(this.accept(block.sexpr)); if (block.program) { out = out + this.pad("PROGRAM:"); this.padding++; diff --git a/spec/ast.js b/spec/ast.js index 7118c72dd..9b3edb03b 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -71,7 +71,7 @@ describe('ast', function() { it('should throw on mustache mismatch', function() { shouldThrow(function() { handlebarsEnv.parse("\n {{#foo}}{{/bar}}"); - }, Handlebars.Exception, "foo doesn't match bar - 2:2"); + }, Handlebars.Exception, "foo doesn't match bar - 2:5"); }); it('stores location info', function(){ diff --git a/spec/parser.js b/spec/parser.js index 131160a74..3d5fe0e09 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -109,46 +109,46 @@ describe('parser', function() { }); it('parses an inverse section', function() { - equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); + equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); it('parses an inverse (else-style) section', function() { - equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); + equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); it('parses empty blocks', function() { - equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n"); + equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n"); }); it('parses empty blocks with empty inverse section', function() { - equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n"); + equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n"); }); it('parses empty blocks with empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n"); + equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n"); }); it('parses non-empty blocks with empty inverse section', function() { - equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); + equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); }); it('parses non-empty blocks with empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); + equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); }); it('parses empty blocks with non-empty inverse section', function() { - equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); + equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); }); it('parses empty blocks with non-empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); + equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); }); it('parses a standalone inverse section', function() { - equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n ID:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"); }); it('parses a standalone inverse section', function() { - equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n ID:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"); }); it("raises if there's a Parse error", function() { diff --git a/src/handlebars.yy b/src/handlebars.yy index ccc0d2248..233088ba3 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -17,16 +17,20 @@ statement | block -> $1 | rawBlock -> $1 | partial -> $1 - | CONTENT -> new yy.ContentNode($1, @$) + | content -> $1 | COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), @$) ; +content + : CONTENT -> new yy.ContentNode($1, @$) + ; + rawBlock - : openRawBlock CONTENT END_RAW_BLOCK -> new yy.RawBlockNode($1, $2, $3, @$) + : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) ; openRawBlock - : OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> new yy.MustacheNode($2, null, '', '', @$) + : OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> { sexpr: $2 } ; block @@ -35,11 +39,11 @@ block ; openBlock - : OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + : OPEN_BLOCK sexpr CLOSE -> { sexpr: $2, strip: yy.stripFlags($1, $3) } ; openInverse - : OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + : OPEN_INVERSE sexpr CLOSE -> { sexpr: $2, strip: yy.stripFlags($1, $3) } ; inverseAndProgram From a5d0491d57f53425ef155a3a803a881371bf07b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Mu=C3=B1oz?= Date: Wed, 5 Nov 2014 11:00:39 -0500 Subject: [PATCH 14/99] Remove RawBlockNode in favor of prepareRawBlock helper --- lib/handlebars/compiler/ast.js | 8 -------- lib/handlebars/compiler/helpers.js | 4 +++- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 05de762b6..35a60dbed 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -93,14 +93,6 @@ var AST = { } }, - RawBlockNode: function(sexpr, content, locInfo) { - LocationInfo.call(this, locInfo); - - this.type = 'block'; - this.sexpr = sexpr; - this.program = new AST.ProgramNode([content], {}, locInfo); - }, - ContentNode: function(string, locInfo) { LocationInfo.call(this, locInfo); this.type = "content"; diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index c9a35c24d..02b307b05 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -23,7 +23,9 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { throw new Exception(openRawBlock.sexpr.id.original + " doesn't match " + close, errorNode); } - return new this.RawBlockNode(openRawBlock.sexpr, content, locInfo); + var program = new this.ProgramNode([content], {}, locInfo); + + return new this.BlockNode(openRawBlock.sexpr, program, undefined, undefined, locInfo); } export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { From a7000f81292816065b122389ea3454ad85c1798e Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 5 Nov 2014 21:57:09 -0600 Subject: [PATCH 15/99] Provide stubbed visitor implementation Part of #889 --- lib/handlebars/compiler/visitor.js | 60 +++++++++++++++++++++++++++++- spec/visitor.js | 48 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 spec/visitor.js diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 6a0373e19..a4eb2b44b 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -4,8 +4,64 @@ Visitor.prototype = { constructor: Visitor, accept: function(object) { - return this[object.type](object); - } + return object && this[object.type] && this[object.type](object); + }, + + program: function(program) { + var statements = program.statements, + i, l; + + for(i=0, l=statements.length; i bar }} {{/foo}}')); + }); + + it('should traverse to stubs', function() { + var visitor = new Handlebars.Visitor(); + + visitor.PARTIAL_NAME = function(partialName) { + equal(partialName.name, 'bar'); + }; + + visitor.STRING = function(string) { + equal(string.string, '2'); + }; + visitor.NUMBER = function(number) { + equal(number.stringModeValue, 1); + }; + visitor.BOOLEAN = function(bool) { + equal(bool.stringModeValue, true); + }; + visitor.ID = function(id) { + equal(id.original, 'foo.bar'); + }; + visitor.content = function(content) { + equal(content.string, ' '); + }; + visitor.comment = function(comment) { + equal(comment.comment, 'comment'); + }; + + visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}')); + }); +}); From 9665379cdc20d6ef9f8e21157f623b69d6f21db0 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 5 Nov 2014 22:04:37 -0600 Subject: [PATCH 16/99] Use terinary operator for inline appends Allows for append operations to avoid breaking inline chain. --- .../compiler/javascript-compiler.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index d41cacd73..61e99cb58 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -289,13 +289,18 @@ JavaScriptCompiler.prototype = { // If `value` is truthy, or 0, it is coerced into a string and appended // Otherwise, the empty string is appended append: function() { - // Force anything that is inlined onto the stack so we don't have duplication - // when we examine local - this.flushInline(); - var local = this.popStack(); - this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }'); - if (this.environment.isSimple) { - this.pushSource("else { " + this.appendToBuffer("''") + " }"); + if (this.isInline()) { + this.replaceStack(function(current) { + return ' != null ? ' + current + ' : ""'; + }); + + this.pushSource(this.appendToBuffer(this.popStack())); + } else { + var local = this.popStack(); + this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }'); + if (this.environment.isSimple) { + this.pushSource("else { " + this.appendToBuffer("''") + " }"); + } } }, From d3bd1a523d13453e0b7028ac8a3015100bd63eda Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 5 Nov 2014 23:23:43 -0600 Subject: [PATCH 17/99] Simplify flushInline implementation --- .../compiler/javascript-compiler.js | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 61e99cb58..661d1e211 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -697,7 +697,7 @@ JavaScriptCompiler.prototype = { }, pushStackLiteral: function(item) { - return this.push(new Literal(item)); + this.push(new Literal(item)); }, pushSource: function(source) { @@ -711,15 +711,6 @@ JavaScriptCompiler.prototype = { } }, - pushStack: function(item) { - this.flushInline(); - - var stack = this.incrStack(); - this.pushSource(stack + " = " + item + ";"); - this.compileStack.push(stack); - return stack; - }, - replaceStack: function(callback) { var prefix = '', inline = this.isInline(), @@ -769,15 +760,15 @@ JavaScriptCompiler.prototype = { }, flushInline: function() { var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } + this.inlineStack = []; + for (var i = 0, len = inlineStack.length; i < len; i++) { + var entry = inlineStack[i]; + if (entry instanceof Literal) { + this.compileStack.push(entry); + } else { + var stack = this.incrStack(); + this.pushSource(stack + " = " + entry + ";"); + this.compileStack.push(stack); } } }, From 30034c75e78c13d1311de65669760d13024a0993 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 6 Nov 2014 09:43:43 -0600 Subject: [PATCH 18/99] Merge setupParams and setupOptions --- lib/handlebars/compiler/javascript-compiler.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 661d1e211..2c23632c3 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -848,7 +848,7 @@ JavaScriptCompiler.prototype = { }; }, - setupOptions: function(helper, paramSize, params) { + setupParams: function(helper, paramSize, params, useRegister) { var options = {}, contexts = [], types = [], ids = [], param, inverse, program; options.name = this.quotedString(helper); @@ -908,14 +908,7 @@ JavaScriptCompiler.prototype = { options.data = "data"; } - return options; - }, - - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(helperName, paramSize, params, useRegister) { - var options = this.objectLiteral(this.setupOptions(helperName, paramSize, params)); - + options = this.objectLiteral(options); if (useRegister) { this.useRegister('options'); params.push('options'); From 83bcbee222077350ee2b7e3322f0cdad18f34b83 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 7 Nov 2014 18:38:56 -0600 Subject: [PATCH 19/99] Ignore unused code paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t want to remove these as the generic code may need it in the future, but these code paths are not triggered through the existing language constructs. --- lib/handlebars/compiler/javascript-compiler.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 2c23632c3..4b7ee6f57 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -763,6 +763,7 @@ JavaScriptCompiler.prototype = { this.inlineStack = []; for (var i = 0, len = inlineStack.length; i < len; i++) { var entry = inlineStack[i]; + /* istanbul ignore if */ if (entry instanceof Literal) { this.compileStack.push(entry); } else { @@ -798,6 +799,7 @@ JavaScriptCompiler.prototype = { var stack = (this.isInline() ? this.inlineStack : this.compileStack), item = stack[stack.length - 1]; + /* istanbul ignore if */ if (item instanceof Literal) { return item.value; } else { From d47e4dd1f742ae90d71b4fd5141a0e7577098cc5 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 7 Nov 2014 21:23:51 -0600 Subject: [PATCH 20/99] Bump test coverage --- lib/handlebars/compiler/helpers.js | 3 +- .../compiler/javascript-compiler.js | 4 +-- spec/artifacts/bom.handlebars | 1 + spec/precompiler.js | 31 ++++++++++++++++++- 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 spec/artifacts/bom.handlebars diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 20f13d656..5aa0f2c7f 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -98,7 +98,8 @@ export function prepareProgram(statements, isRoot) { if (omitLeft(statements, i)) { // If we are on a standalone node, save the indent info for partials if (current.type === 'partial') { - current.indent = (/([ \t]+$)/).exec(statements[i-1].original) ? RegExp.$1 : ''; + // Pull out the whitespace from the final line + current.indent = (/([ \t]+$)/).exec(statements[i-1].original)[1]; } } } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 4b7ee6f57..376bc6bfb 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -732,8 +732,8 @@ JavaScriptCompiler.prototype = { usedLiteral = true; } else { // Get or create the current stack name for use by the inline - createdStack = !this.stackSlot; - var name = !createdStack ? this.topStackName() : this.incrStack(); + createdStack = true; + var name = this.incrStack(); prefix = '(' + this.push(name) + ' = ' + top + ')'; stack = this.topStack(); diff --git a/spec/artifacts/bom.handlebars b/spec/artifacts/bom.handlebars new file mode 100644 index 000000000..548d71419 --- /dev/null +++ b/spec/artifacts/bom.handlebars @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/spec/precompiler.js b/spec/precompiler.js index bd19fbf85..9d883b6c0 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -9,27 +9,38 @@ describe('precompiler', function() { var Handlebars = require('../lib'), Precompiler = require('../lib/precompiler'), + fs = require('fs'), uglify = require('uglify-js'); var log, logFunction, precompile, - minify; + minify, + + file, + content, + writeFileSync; beforeEach(function() { precompile = Handlebars.precompile; minify = uglify.minify; + writeFileSync = fs.writeFileSync; logFunction = console.log; log = ''; console.log = function() { log += Array.prototype.join.call(arguments, ''); }; + fs.writeFileSync = function(_file, _content) { + file = _file; + content = _content; + }; }); afterEach(function() { Handlebars.precompile = precompile; uglify.minify = minify; + fs.writeFileSync = writeFileSync; console.log = logFunction; }); @@ -121,6 +132,24 @@ describe('precompiler', function() { equal(log, 'simple\n'); }); + it('should handle different root', function() { + Handlebars.precompile = function() { return 'simple'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', root: 'foo/'}); + equal(log, 'simple\n'); + }); + it('should output to file system', function() { + Handlebars.precompile = function() { return 'simple'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', output: 'file!'}); + equal(file, 'file!'); + equal(content, 'simple\n'); + equal(log, ''); + }); + it('should handle BOM', function() { + Handlebars.precompile = function(template) { return template === 'a' ? 'simple' : 'fail'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/bom.handlebars'], simple: true, extension: 'handlebars', bom: true}); + equal(log, 'simple\n'); + }); + it('should output minimized templates', function() { Handlebars.precompile = function() { return 'amd'; }; uglify.minify = function() { return {code: 'min'}; }; From d595ebc0e2232b35a2ba0628ee73b54cd8f9069c Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 7 Nov 2014 01:05:17 -0600 Subject: [PATCH 21/99] Simplify program generation logic --- lib/handlebars/compiler/javascript-compiler.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 376bc6bfb..4e414e547 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -870,16 +870,8 @@ JavaScriptCompiler.prototype = { // Avoid setting fn and inverse if neither are set. This allows // helpers to do a check for `if (options.fn)` if (program || inverse) { - if (!program) { - program = 'this.noop'; - } - - if (!inverse) { - inverse = 'this.noop'; - } - - options.fn = program; - options.inverse = inverse; + options.fn = program || 'this.noop'; + options.inverse = inverse || 'this.noop'; } // The parameters go on to the stack in order (making sure that they are evaluated in order) From 01a22e61df48eb512ba5dc0316855c1a8dd25956 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 26 Oct 2014 15:06:32 -0500 Subject: [PATCH 22/99] Use toHTML vs. instanceof checks for SafeString Allows for us to play nicely in environments such as Node that could have multiple versions of the library loaded. Also allows for implementors to provide their own behavior, provided they know what they are doing. Fixes #886 --- lib/handlebars/safe-string.js | 2 +- lib/handlebars/utils.js | 4 ++-- spec/utils.js | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/handlebars/safe-string.js b/lib/handlebars/safe-string.js index 2ae49aa89..a6b8ecf20 100644 --- a/lib/handlebars/safe-string.js +++ b/lib/handlebars/safe-string.js @@ -3,7 +3,7 @@ function SafeString(string) { this.string = string; } -SafeString.prototype.toString = function() { +SafeString.prototype.toString = SafeString.prototype.toHTML = function() { return "" + this.string; }; diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index f38b7afb2..ce850776b 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -53,8 +53,8 @@ export var isArray = Array.isArray || function(value) { export function escapeExpression(string) { // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { - return string.toString(); + if (string && string.toHTML) { + return string.toHTML(); } else if (string == null) { return ""; } else if (!string) { diff --git a/spec/utils.js b/spec/utils.js index 0216c8de8..4582e2496 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -25,6 +25,12 @@ describe('utils', function() { var string = new Handlebars.SafeString('foo<&"\'>'); equals(Handlebars.Utils.escapeExpression(string), 'foo<&"\'>'); + var obj = { + toHTML: function() { + return 'foo<&"\'>'; + } + }; + equals(Handlebars.Utils.escapeExpression(obj), 'foo<&"\'>'); }); it('should handle falsy', function() { equals(Handlebars.Utils.escapeExpression(''), ''); From 249f559104cb7f85736e7e83e38ccc67b9b84bf6 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 6 Nov 2014 09:44:38 -0600 Subject: [PATCH 23/99] Include location information in all opcodes --- lib/handlebars/compiler/compiler.js | 94 ++++++++++--------- .../compiler/javascript-compiler.js | 1 + 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 1303d8fc4..a75fc4ab7 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -130,36 +130,36 @@ Compiler.prototype = { // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - this.opcode('emptyHash'); - this.opcode('blockValue', sexpr.id.original); + this.opcode('pushProgram', block, program); + this.opcode('pushProgram', block, inverse); + this.opcode('emptyHash', block); + this.opcode('blockValue', block, sexpr.id.original); } else { this.ambiguousSexpr(sexpr, program, inverse); // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - this.opcode('emptyHash'); - this.opcode('ambiguousBlockValue'); + this.opcode('pushProgram', block, program); + this.opcode('pushProgram', block, inverse); + this.opcode('emptyHash', block); + this.opcode('ambiguousBlockValue', block); } - this.opcode('append'); + this.opcode('append', block); }, hash: function(hash) { var pairs = hash.pairs, i, l; - this.opcode('pushHash'); + this.opcode('pushHash', hash); for(i=0, l=pairs.length; i Date: Thu, 6 Nov 2014 09:56:00 -0600 Subject: [PATCH 24/99] Generate source maps Allow the precompiler to generate source maps when the srcFile parameter is passed. This refactors large chunks of the code generation pipeline, allowing metadata to be associated with code chunks as well as breaking out much of the code generation logic into a separate helper. --- lib/handlebars/compiler/code-gen.js | 151 ++++++++++++ .../compiler/javascript-compiler.js | 224 ++++++++++-------- package.json | 3 +- spec/env/browser.js | 2 + spec/expected/empty.amd.js | 2 +- spec/helpers.js | 4 + spec/javascript-compiler.js | 2 +- spec/source-map.js | 45 ++++ 8 files changed, 330 insertions(+), 103 deletions(-) create mode 100644 lib/handlebars/compiler/code-gen.js create mode 100644 spec/source-map.js diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js new file mode 100644 index 000000000..7d1b4ca2e --- /dev/null +++ b/lib/handlebars/compiler/code-gen.js @@ -0,0 +1,151 @@ +import {isArray} from "../utils"; + +try { + var SourceMap = require('source-map'), + SourceNode = SourceMap.SourceNode; +} catch (err) { + /* istanbul ignore next: tested but not covered in istanbul due to dist build */ + SourceNode = function(line, column, srcFile, chunks) { + this.src = ''; + if (chunks) { + this.add(chunks); + } + }; + /* istanbul ignore next */ + SourceNode.prototype = { + add: function(chunks) { + if (isArray(chunks)) { + chunks = chunks.join(''); + } + this.src += chunks; + }, + prepend: function(chunks) { + if (isArray(chunks)) { + chunks = chunks.join(''); + } + this.src = chunks + this.src; + }, + toStringWithSourceMap: function() { + return {code: this.toString()}; + }, + toString: function() { + return this.src; + } + }; +} + + +function castChunk(chunk, codeGen, loc) { + if (isArray(chunk)) { + var ret = []; + + for (var i = 0, len = chunk.length; i < len; i++) { + ret.push(codeGen.wrap(chunk[i], loc)); + } + return ret; + } else if (typeof chunk === 'boolean' || typeof chunk === 'number') { + // Handle primitives that the SourceNode will throw up on + return chunk+''; + } + return chunk; +} + + +function CodeGen(srcFile) { + this.srcFile = srcFile; + this.source = []; +} + +CodeGen.prototype = { + prepend: function(source, loc) { + this.source.unshift(this.wrap(source, loc)); + }, + push: function(source, loc) { + this.source.push(this.wrap(source, loc)); + }, + + merge: function() { + var source = this.empty(); + this.each(function(line) { + source.add([' ', line, '\n']); + }); + return source; + }, + + each: function(iter) { + for (var i = 0, len = this.source.length; i < len; i++) { + iter(this.source[i]); + } + }, + + empty: function(loc) { + loc = loc || this.currentLocation || {}; + return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile); + }, + wrap: function(chunk, loc) { + if (chunk instanceof SourceNode) { + return chunk; + } + + loc = loc || this.currentLocation || {}; + chunk = castChunk(chunk, this, loc); + + return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile, chunk); + }, + + functionCall: function(fn, type, params) { + params = this.generateList(params); + return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']); + }, + + quotedString: function(str) { + return '"' + str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 + .replace(/\u2029/g, '\\u2029') + '"'; + }, + + objectLiteral: function(obj) { + var pairs = []; + + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + pairs.push([this.quotedString(key), ':', castChunk(obj[key], this)]); + } + } + + var ret = this.generateList(pairs); + ret.prepend('{'); + ret.add('}'); + return ret; + }, + + + generateList: function(entries, loc) { + var ret = this.empty(loc); + + for (var i = 0, len = entries.length; i < len; i++) { + if (i) { + ret.add(','); + } + + ret.add(castChunk(entries[i], this, loc)); + } + + return ret; + }, + + generateArray: function(entries, loc) { + var ret = this.generateList(entries, loc); + ret.prepend('['); + ret.add(']'); + + return ret; + } +}; + +export default CodeGen; + diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 4256b1c20..cb9b1cab4 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -1,5 +1,7 @@ import { COMPILER_REVISION, REVISION_CHANGES } from "../base"; import Exception from "../exception"; +import {isArray} from "../utils"; +import CodeGen from "./code-gen"; function Literal(value) { this.value = value; @@ -12,15 +14,15 @@ JavaScriptCompiler.prototype = { // alternative compiled forms for name lookup and buffering semantics nameLookup: function(parent, name /* , type*/) { if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { - return parent + "." + name; + return [parent, ".", name]; } else { - return parent + "['" + name + "']"; + return [parent, "['", name, "']"]; } }, depthedLookup: function(name) { this.aliases.lookup = 'this.lookup'; - return 'lookup(depths, "' + name + '")'; + return ['lookup(depths, "', name, '")']; }, compilerInfo: function() { @@ -29,15 +31,23 @@ JavaScriptCompiler.prototype = { return [revision, versions]; }, - appendToBuffer: function(string) { + appendToBuffer: function(string, location, explicit) { + // Force a string as this simplifies the merge logic. + if (!isArray(string)) { + string = [string]; + } + string = this.source.wrap(string, location); + if (this.environment.isSimple) { - return "return " + string + ";"; + return ['return ', string, ';']; + } else if (explicit) { + // This is a case where the buffer operation occurs as a child of another + // construct, generally braces. We have to explicitly output these buffer + // operations to ensure that the emitted code goes in the correct location. + return ['buffer += ', string, ';']; } else { - return { - appendToBuffer: true, - content: string, - toString: function() { return "buffer += " + string + ";"; } - }; + string.appendToBuffer = true; + return string; } }, @@ -84,11 +94,12 @@ JavaScriptCompiler.prototype = { for (i = 0, l = opcodes.length; i < l; i++) { opcode = opcodes[i]; - this.currentLoc = opcode.loc; + this.source.currentLocation = opcode.loc; this[opcode.opcode].apply(this, opcode.args); } // Flush any trailing content that might be pending. + this.source.currentLocation = undefined; this.pushSource(''); /* istanbul ignore next */ @@ -125,6 +136,13 @@ JavaScriptCompiler.prototype = { if (!asObject) { ret.compiler = JSON.stringify(ret.compiler); ret = this.objectLiteral(ret); + + if (options.srcName) { + ret = ret.toStringWithSourceMap({file: options.destName}); + ret.map = ret.map && ret.map.toString(); + } else { + ret = ret.toString(); + } } return ret; @@ -137,7 +155,7 @@ JavaScriptCompiler.prototype = { // track the last context pushed into place to allow skipping the // getContext opcode when it would be a noop this.lastContext = 0; - this.source = []; + this.source = new CodeGen(this.options.srcName); }, createFunctionContext: function(asObject) { @@ -169,59 +187,67 @@ JavaScriptCompiler.prototype = { return Function.apply(this, params); } else { - return 'function(' + params.join(',') + ') {\n ' + source + '}'; + return this.source.wrap(['function(', params.join(','), ') {\n ', source, '}']); } }, mergeSource: function(varDeclarations) { - var source = '', - buffer, + var isSimple = this.environment.isSimple, appendOnly = !this.forceBuffer, - appendFirst; + appendFirst, - for (var i = 0, len = this.source.length; i < len; i++) { - var line = this.source[i]; + sourceSeen, + bufferStart, + bufferEnd; + this.source.each(function(line) { if (line.appendToBuffer) { - if (buffer) { - buffer = buffer + '\n + ' + line.content; + if (bufferStart) { + line.prepend(' + '); } else { - buffer = line.content; + bufferStart = line; } + bufferEnd = line; } else { - if (buffer) { - if (!source) { + if (bufferStart) { + if (!sourceSeen) { appendFirst = true; - source = buffer + ';\n '; } else { - source += 'buffer += ' + buffer + ';\n '; + bufferStart.prepend('buffer += '); } - buffer = undefined; + bufferEnd.add(';'); + bufferStart = bufferEnd = undefined; } - source += line + '\n '; - if (!this.environment.isSimple) { + sourceSeen = true; + if (!isSimple) { appendOnly = false; } } - } + }); + if (appendOnly) { - if (buffer || !source) { - source += 'return ' + (buffer || '""') + ';\n'; + if (bufferStart) { + bufferStart.prepend('return '); + bufferEnd.add(';'); + } else { + this.source.push('return "";'); } } else { varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer()); - if (buffer) { - source += 'return buffer + ' + buffer + ';\n'; + + if (bufferStart) { + bufferStart.prepend('return buffer + '); + bufferEnd.add(';'); } else { - source += 'return buffer;\n'; + this.source.push('return buffer;'); } } if (varDeclarations) { - source = 'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ') + source; + this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ')); } - return source; + return this.source.merge(); }, // [blockValue] @@ -242,7 +268,7 @@ JavaScriptCompiler.prototype = { var blockName = this.popStack(); params.splice(1, 0, blockName); - this.push('blockHelperMissing.call(' + params.join(', ') + ')'); + this.push(this.source.functionCall('blockHelperMissing', 'call', params)); }, // [ambiguousBlockValue] @@ -263,7 +289,10 @@ JavaScriptCompiler.prototype = { var current = this.topStack(); params.splice(1, 0, current); - this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); + this.pushSource([ + 'if (!', this.lastHelper, ') { ', + current, ' = ', this.source.functionCall('blockHelperMissing', 'call', params), + '}']); }, // [appendContent] @@ -275,6 +304,8 @@ JavaScriptCompiler.prototype = { appendContent: function(content) { if (this.pendingContent) { content = this.pendingContent + content; + } else { + this.pendingLocation = this.source.currentLocation; } this.pendingContent = content; @@ -292,15 +323,15 @@ JavaScriptCompiler.prototype = { append: function() { if (this.isInline()) { this.replaceStack(function(current) { - return ' != null ? ' + current + ' : ""'; + return [' != null ? ', current, ' : ""']; }); this.pushSource(this.appendToBuffer(this.popStack())); } else { var local = this.popStack(); - this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }'); + this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']); if (this.environment.isSimple) { - this.pushSource("else { " + this.appendToBuffer("''") + " }"); + this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']); } } }, @@ -314,7 +345,7 @@ JavaScriptCompiler.prototype = { appendEscaped: function() { this.aliases.escapeExpression = 'this.escapeExpression'; - this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")")); + this.pushSource(this.appendToBuffer(['escapeExpression(', this.popStack(), ')'])); }, // [getContext] @@ -364,10 +395,10 @@ JavaScriptCompiler.prototype = { // We want to ensure that zero and false are handled properly if the context (falsy flag) // needs to have the special handling for these values. if (!falsy) { - return ' != null ? ' + lookup + ' : ' + current; + return [' != null ? ', lookup, ' : ', current]; } else { // Otherwise we can use generic falsy handling - return ' && ' + lookup; + return [' && ', lookup]; } }); } @@ -390,7 +421,7 @@ JavaScriptCompiler.prototype = { var len = parts.length; for (var i = 0; i < len; i++) { this.replaceStack(function(current) { - return ' && ' + this.nameLookup(current, parts[i], 'data'); + return [' && ', this.nameLookup(current, parts[i], 'data')]; }); } }, @@ -405,7 +436,7 @@ JavaScriptCompiler.prototype = { resolvePossibleLambda: function() { this.aliases.lambda = 'this.lambda'; - this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')'); + this.push(['lambda(', this.popStack(), ', ', this.contextName(0), ')']); }, // [pushStringParam] @@ -453,14 +484,14 @@ JavaScriptCompiler.prototype = { this.hash = this.hashes.pop(); if (this.trackIds) { - this.push('{' + hash.ids.join(',') + '}'); + this.push(this.objectLiteral(hash.ids)); } if (this.stringParams) { - this.push('{' + hash.contexts.join(',') + '}'); - this.push('{' + hash.types.join(',') + '}'); + this.push(this.objectLiteral(hash.contexts)); + this.push(this.objectLiteral(hash.types)); } - this.push('{\n ' + hash.values.join(',\n ') + '\n }'); + this.push(this.objectLiteral(hash.values)); }, // [pushString] @@ -473,17 +504,6 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral(this.quotedString(string)); }, - // [push] - // - // On stack, before: ... - // On stack, after: expr, ... - // - // Push an expression onto the stack - push: function(expr) { - this.inlineStack.push(expr); - return expr; - }, - // [pushLiteral] // // On stack, before: ... @@ -526,9 +546,13 @@ JavaScriptCompiler.prototype = { var nonHelper = this.popStack(); var helper = this.setupHelper(paramSize, name); + var simple = isSimple ? [helper.name, ' || '] : ''; - var lookup = (isSimple ? helper.name + ' || ' : '') + nonHelper + ' || helperMissing'; - this.push('((' + lookup + ').call(' + helper.callParams + '))'); + this.push( + this.source.functionCall( + ['('].concat(simple, nonHelper, ' || ', 'helperMissing)'), + 'call', + helper.callParams)); }, // [invokeKnownHelper] @@ -540,7 +564,7 @@ JavaScriptCompiler.prototype = { // so a `helperMissing` fallback is not required. invokeKnownHelper: function(paramSize, name) { var helper = this.setupHelper(paramSize, name); - this.push(helper.name + ".call(" + helper.callParams + ")"); + this.push(this.source.functionCall(helper.name, 'call', helper.callParams)); }, // [invokeAmbiguous] @@ -567,10 +591,12 @@ JavaScriptCompiler.prototype = { var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); - this.push( - '((helper = (helper = ' + helperName + ' || ' + nonHelper + ') != null ? helper : helperMissing' - + (helper.paramsInit ? '),(' + helper.paramsInit : '') + '),' - + '(typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper))'); + this.push([ + '((helper = (helper = ', helperName, ' || ', nonHelper, ') != null ? helper : helperMissing', + (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),', + '(typeof helper === functionType ? ', + this.source.functionCall('helper','call', helper.callParams), ' : helper))' + ]); }, // [invokePartial] @@ -592,7 +618,7 @@ JavaScriptCompiler.prototype = { params.push('depths'); } - this.push("this.invokePartial(" + params.join(", ") + ")"); + this.push(this.source.functionCall('this.invokePartial', '', params)); }, // [assignToHash] @@ -617,15 +643,15 @@ JavaScriptCompiler.prototype = { var hash = this.hash; if (context) { - hash.contexts.push("'" + key + "': " + context); + hash.contexts[key] = context; } if (type) { - hash.types.push("'" + key + "': " + type); + hash.types[key] = type; } if (id) { - hash.ids.push("'" + key + "': " + id); + hash.ids[key] = id; } - hash.values.push("'" + key + "': (" + value + ")"); + hash.values[key] = value; }, pushId: function(type, name) { @@ -697,13 +723,23 @@ JavaScriptCompiler.prototype = { } }, + push: function(expr) { + if (!(expr instanceof Literal)) { + expr = this.source.wrap(expr); + } + + this.inlineStack.push(expr); + return expr; + }, + pushStackLiteral: function(item) { this.push(new Literal(item)); }, pushSource: function(source) { if (this.pendingContent) { - this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent))); + this.source.push( + this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation)); this.pendingContent = undefined; } @@ -713,7 +749,7 @@ JavaScriptCompiler.prototype = { }, replaceStack: function(callback) { - var prefix = '', + var prefix = ['('], inline = this.isInline(), stack, createdStack, @@ -729,14 +765,15 @@ JavaScriptCompiler.prototype = { if (top instanceof Literal) { // Literals do not need to be inlined - prefix = stack = top.value; + stack = [top.value]; + prefix = ['(', stack]; usedLiteral = true; } else { // Get or create the current stack name for use by the inline createdStack = true; var name = this.incrStack(); - prefix = '(' + this.push(name) + ' = ' + top + ')'; + prefix = ['((', this.push(name), ' = ', top, ')']; stack = this.topStack(); } @@ -748,7 +785,7 @@ JavaScriptCompiler.prototype = { if (createdStack) { this.stackSlot--; } - this.push('(' + prefix + item + ')'); + this.push(prefix.concat(item, ')')); }, incrStack: function() { @@ -769,7 +806,7 @@ JavaScriptCompiler.prototype = { this.compileStack.push(entry); } else { var stack = this.incrStack(); - this.pushSource(stack + " = " + entry + ";"); + this.pushSource([stack, ' = ', entry, ';']); this.compileStack.push(stack); } } @@ -817,25 +854,11 @@ JavaScriptCompiler.prototype = { }, quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; + return this.source.quotedString(str); }, objectLiteral: function(obj) { - var pairs = []; - - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - pairs.push(this.quotedString(key) + ':' + obj[key]); - } - } - - return '{' + pairs.join(',') + '}'; + return this.source.objectLiteral(obj); }, setupHelper: function(paramSize, name, blockHelper) { @@ -847,7 +870,7 @@ JavaScriptCompiler.prototype = { params: params, paramsInit: paramsInit, name: foundHelper, - callParams: [this.contextName(0)].concat(params).join(", ") + callParams: [this.contextName(0)].concat(params) }; }, @@ -892,11 +915,11 @@ JavaScriptCompiler.prototype = { } if (this.trackIds) { - options.ids = "[" + ids.join(",") + "]"; + options.ids = this.source.generateArray(ids); } if (this.stringParams) { - options.types = "[" + types.join(",") + "]"; - options.contexts = "[" + contexts.join(",") + "]"; + options.types = this.source.generateArray(types); + options.contexts = this.source.generateArray(contexts); } if (this.options.data) { @@ -907,7 +930,7 @@ JavaScriptCompiler.prototype = { if (useRegister) { this.useRegister('options'); params.push('options'); - return 'options=' + options; + return ['options=', options]; } else { params.push(options); return ''; @@ -915,6 +938,7 @@ JavaScriptCompiler.prototype = { } }; + var reservedWords = ( "break else new var" + " case finally return void" + diff --git a/package.json b/package.json index 6ed9a5526..5de52c04f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "node": ">=0.4.7" }, "dependencies": { - "optimist": "~0.3" + "optimist": "~0.3", + "source-map": "^0.1.40" }, "optionalDependencies": { "uglify-js": "~2.3" diff --git a/spec/env/browser.js b/spec/env/browser.js index bcf4259d3..a0ce2431b 100644 --- a/spec/env/browser.js +++ b/spec/env/browser.js @@ -9,6 +9,8 @@ global.Handlebars = undefined; vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js'); global.CompilerContext = { + browser: true, + compile: function(template, options) { var templateSpec = handlebarsEnv.precompile(template, options); return handlebarsEnv.template(safeEval(templateSpec)); diff --git a/spec/expected/empty.amd.js b/spec/expected/empty.amd.js index 1cf3298eb..852733b2f 100644 --- a/spec/expected/empty.amd.js +++ b/spec/expected/empty.amd.js @@ -1,6 +1,6 @@ define(['handlebars.runtime'], function(Handlebars) { Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) { - return ""; + return ""; },"useData":true}); }); diff --git a/spec/helpers.js b/spec/helpers.js index e604e91c8..f23ee1403 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -60,6 +60,10 @@ describe('helpers', function() { }}; shouldCompileToWithPartials(string, [hash, helpers], true, "Goodbye"); }); + it('helper returning undefined value', function() { + shouldCompileTo(' {{nothere}}', [{}, {nothere: function() {}}], ' '); + shouldCompileTo(' {{#nothere}}{{/nothere}}', [{}, {nothere: function() {}}], ' '); + }); it("block helper", function() { var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; diff --git a/spec/javascript-compiler.js b/spec/javascript-compiler.js index 16058680b..426056729 100644 --- a/spec/javascript-compiler.js +++ b/spec/javascript-compiler.js @@ -63,7 +63,7 @@ describe('javascript-compiler api', function() { }); it('should allow append buffer override', function() { handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) { - return $superAppend.call(this, string + ' + "_foo"'); + return $superAppend.call(this, [string, ' + "_foo"']); }; shouldCompileTo("{{foo}}", { foo: "food" }, "food_foo"); }); diff --git a/spec/source-map.js b/spec/source-map.js new file mode 100644 index 000000000..4d3fa3e82 --- /dev/null +++ b/spec/source-map.js @@ -0,0 +1,45 @@ +/*global CompilerContext, Handlebars */ +var SourceMap = require('source-map'), + SourceMapConsumer = SourceMap.SourceMapConsumer; + +describe('source-map', function() { + if (!Handlebars.precompile) { + return; + } + + it('should safely include source map info', function() { + var template = Handlebars.precompile('{{hello}}', {destName: 'dest.js', srcName: 'src.hbs'}); + + equal(!!template.code, true); + equal(!!template.map, !CompilerContext.browser); + }); + it('should map source properly', function() { + var source = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}', + template = Handlebars.precompile(source, {destName: 'dest.js', srcName: 'src.hbs'}); + + if (template.map) { + var consumer = new SourceMapConsumer(template.map), + lines = template.code.split('\n'), + srcLines = source.split('\n'), + + generated = grepLine('" b"', lines), + source = grepLine(' b', srcLines); + + var mapped = consumer.originalPositionFor(generated); + equal(mapped.line, source.line); + equal(mapped.column, source.column); + } + }); +}); + +function grepLine(token, lines) { + for (var i = 0; i < lines.length; i++) { + var column = lines[i].indexOf(token); + if (column >= 0) { + return { + line: i+1, + column: column + }; + } + } +} From 0dbeeb450336522816335ecd188951f6079ff0d3 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 8 Nov 2014 17:03:14 -0600 Subject: [PATCH 25/99] Add source map output support to the CLI --- bin/handlebars | 4 +++ lib/precompiler.js | 77 +++++++++++++++++++++++++++++++++------------ spec/precompiler.js | 16 +++++++++- 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/bin/handlebars b/bin/handlebars index 4007cc725..79dfa6fb2 100755 --- a/bin/handlebars +++ b/bin/handlebars @@ -7,6 +7,10 @@ var optimist = require('optimist') 'description': 'Output File', 'alias': 'output' }, + 'map': { + 'type': 'string', + 'description': 'Source Map File' + }, 'a': { 'type': 'boolean', 'description': 'Exports amd style (require.js)', diff --git a/lib/precompiler.js b/lib/precompiler.js index 676402226..165c00881 100644 --- a/lib/precompiler.js +++ b/lib/precompiler.js @@ -2,6 +2,9 @@ var fs = require('fs'), Handlebars = require('./index'), basename = require('path').basename, + SourceMap = require('source-map'), + SourceMapConsumer = SourceMap.SourceMapConsumer, + SourceNode = SourceMap.SourceNode, uglify = require('uglify-js'); module.exports.cli = function(opts) { @@ -45,20 +48,23 @@ module.exports.cli = function(opts) { var extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; }); extension = new RegExp('\\.' + extension + '$'); - var output = []; + var output = new SourceNode(); if (!opts.simple) { if (opts.amd) { - output.push('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'); + output.add('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'); } else if (opts.commonjs) { - output.push('var Handlebars = require("' + opts.commonjs + '");'); + output.add('var Handlebars = require("' + opts.commonjs + '");'); } else { - output.push('(function() {\n'); + output.add('(function() {\n'); } - output.push(' var template = Handlebars.template, templates = '); - output.push(opts.namespace); - output.push(' = '); - output.push(opts.namespace); - output.push(' || {};\n'); + output.add(' var template = Handlebars.template, templates = '); + if (opts.namespace) { + output.add(opts.namespace); + output.add(' = '); + output.add(opts.namespace); + output.add(' || '); + } + output.add('{};\n'); } function processTemplate(template, root) { var path = template, @@ -83,6 +89,9 @@ module.exports.cli = function(opts) { knownHelpersOnly: opts.o }; + if (opts.map) { + options.srcName = path; + } if (opts.data) { options.data = true; } @@ -95,18 +104,26 @@ module.exports.cli = function(opts) { } template = template.replace(extension, ''); + var precompiled = Handlebars.precompile(data, options); + + // If we are generating a source map, we have to reconstruct the SourceNode object + if (opts.map) { + var consumer = new SourceMapConsumer(precompiled.map); + precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer); + } + if (opts.simple) { - output.push(Handlebars.precompile(data, options) + '\n'); + output.add([precompiled, '\n']); } else if (opts.partial) { if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) { - output.push('return '); + output.add('return '); } - output.push('Handlebars.partials[\'' + template + '\'] = template(' + Handlebars.precompile(data, options) + ');\n'); + output.add(['Handlebars.partials[\'', template, '\'] = template(', precompiled, ');\n']); } else { if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) { - output.push('return '); + output.add('return '); } - output.push('templates[\'' + template + '\'] = template(' + Handlebars.precompile(data, options) + ');\n'); + output.add(['templates[\'', template, '\'] = template(', precompiled, ');\n']); } } } @@ -120,21 +137,41 @@ module.exports.cli = function(opts) { if (opts.amd) { if(opts.templates.length > 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) { if(opts.partial){ - output.push('return Handlebars.partials;\n'); + output.add('return Handlebars.partials;\n'); } else { - output.push('return templates;\n'); + output.add('return templates;\n'); } } - output.push('});'); + output.add('});'); } else if (!opts.commonjs) { - output.push('})();'); + output.add('})();'); } } - output = output.join(''); + + + if (opts.map) { + output.add('\n//# sourceMappingURL=' + opts.map + '\n'); + } + + output = output.toStringWithSourceMap(); + output.map = output.map + ''; if (opts.min) { - output = uglify.minify(output, {fromString: true}).code; + output = uglify.minify(output.code, { + fromString: true, + + outSourceMap: opts.map, + inSourceMap: JSON.parse(output.map) + }); + if (opts.map) { + output.code += '\n//# sourceMappingURL=' + opts.map + '\n'; + } + } + + if (opts.map) { + fs.writeFileSync(opts.map, output.map, 'utf8'); } + output = output.code; if (opts.output) { fs.writeFileSync(opts.output, output, 'utf8'); diff --git a/spec/precompiler.js b/spec/precompiler.js index 9d883b6c0..fdad60eec 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -98,7 +98,8 @@ describe('precompiler', function() { }); it('should output multiple amd', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars'}); + Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars', namespace: 'foo'}); + equal(/templates = foo = foo \|\|/.test(log), true); equal(/return templates/.test(log), true); equal(/template\(amd\)/.test(log), true); }); @@ -156,4 +157,17 @@ describe('precompiler', function() { Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, extension: 'handlebars'}); equal(log, 'min'); }); + + it('should output map', function() { + fs.writeFileSync = function(_file, _content) { + console.error(arguments); + file = _file; + content = _content; + }; + + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'}); + + equal(file, 'foo.js.map'); + equal(/sourceMappingURL=/.test(log), true); + }); }); From 1429587dd433dbab18cead0b17d1b024ec5f1be3 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 8 Nov 2014 17:04:12 -0600 Subject: [PATCH 26/99] Provide default mapping for boilerplate code --- lib/handlebars/compiler/javascript-compiler.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index cb9b1cab4..869a67260 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -88,6 +88,7 @@ JavaScriptCompiler.prototype = { var opcodes = environment.opcodes, opcode, + firstLoc, i, l; @@ -95,11 +96,12 @@ JavaScriptCompiler.prototype = { opcode = opcodes[i]; this.source.currentLocation = opcode.loc; + firstLoc = firstLoc || opcode.loc; this[opcode.opcode].apply(this, opcode.args); } // Flush any trailing content that might be pending. - this.source.currentLocation = undefined; + this.source.currentLocation = firstLoc; this.pushSource(''); /* istanbul ignore next */ @@ -135,6 +137,8 @@ JavaScriptCompiler.prototype = { if (!asObject) { ret.compiler = JSON.stringify(ret.compiler); + + this.source.currentLocation = {firstLine: 1, firstColumn: 0}; ret = this.objectLiteral(ret); if (options.srcName) { From 6d7696ef6607687b845670a0d8beaa3259ce0a4e Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 8 Nov 2014 18:09:15 -0600 Subject: [PATCH 27/99] Remove unnecessary test stub --- spec/precompiler.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spec/precompiler.js b/spec/precompiler.js index fdad60eec..680246b16 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -159,12 +159,6 @@ describe('precompiler', function() { }); it('should output map', function() { - fs.writeFileSync = function(_file, _content) { - console.error(arguments); - file = _file; - content = _content; - }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'}); equal(file, 'foo.js.map'); From 8dfeabdd16ddbd940e17b053a5eddc36a7e26ee8 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 8 Nov 2014 18:11:29 -0600 Subject: [PATCH 28/99] Fix missing coverage --- spec/precompiler.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/precompiler.js b/spec/precompiler.js index 680246b16..c56bee634 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -164,4 +164,11 @@ describe('precompiler', function() { equal(file, 'foo.js.map'); equal(/sourceMappingURL=/.test(log), true); }); + + it('should output map', function() { + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, map: 'foo.js.map', extension: 'handlebars'}); + + equal(file, 'foo.js.map'); + equal(/sourceMappingURL=/.test(log), true); + }); }); From ac208b1bcd5f5661a4eabaeba1b6ef0c8767bad0 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 8 Nov 2014 18:53:43 -0600 Subject: [PATCH 29/99] Only provide aliases for multiple use calls Fixes #903 --- .../compiler/javascript-compiler.js | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 869a67260..70af1329e 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -20,9 +20,7 @@ JavaScriptCompiler.prototype = { } }, depthedLookup: function(name) { - this.aliases.lookup = 'this.lookup'; - - return ['lookup(depths, "', name, '")']; + return [this.aliasable('this.lookup'), '(depths, "', name, '")']; }, compilerInfo: function() { @@ -171,9 +169,18 @@ JavaScriptCompiler.prototype = { } // Generate minimizer alias mappings + // + // When using true SourceNodes, this will update all references to the given alias + // as the source nodes are reused in situ. For the non-source node compilation mode, + // aliases will not be used, but this case is already being run on the client and + // we aren't concern about minimizing the template size. + var aliasCount = 0; for (var alias in this.aliases) { - if (this.aliases.hasOwnProperty(alias)) { - varDeclarations += ', ' + alias + '=' + this.aliases[alias]; + var node = this.aliases[alias]; + + if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) { + varDeclarations += ', alias' + (++aliasCount) + '=' + alias; + node.children[0] = 'alias' + aliasCount; } } @@ -264,15 +271,14 @@ JavaScriptCompiler.prototype = { // replace it on the stack with the result of properly // invoking blockHelperMissing. blockValue: function(name) { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - - var params = [this.contextName(0)]; + var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), + params = [this.contextName(0)]; this.setupParams(name, 0, params); var blockName = this.popStack(); params.splice(1, 0, blockName); - this.push(this.source.functionCall('blockHelperMissing', 'call', params)); + this.push(this.source.functionCall(blockHelperMissing, 'call', params)); }, // [ambiguousBlockValue] @@ -282,10 +288,9 @@ JavaScriptCompiler.prototype = { // On stack, after, if no lastHelper: same as [blockValue] // On stack, after, if lastHelper: value ambiguousBlockValue: function() { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - // We're being a bit cheeky and reusing the options value from the prior exec - var params = [this.contextName(0)]; + var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), + params = [this.contextName(0)]; this.setupParams('', 0, params, true); this.flushInline(); @@ -295,7 +300,7 @@ JavaScriptCompiler.prototype = { this.pushSource([ 'if (!', this.lastHelper, ') { ', - current, ' = ', this.source.functionCall('blockHelperMissing', 'call', params), + current, ' = ', this.source.functionCall(blockHelperMissing, 'call', params), '}']); }, @@ -347,9 +352,8 @@ JavaScriptCompiler.prototype = { // // Escape `value` and append it to the buffer appendEscaped: function() { - this.aliases.escapeExpression = 'this.escapeExpression'; - - this.pushSource(this.appendToBuffer(['escapeExpression(', this.popStack(), ')'])); + this.pushSource(this.appendToBuffer( + [this.aliasable('this.escapeExpression'), '(', this.popStack(), ')'])); }, // [getContext] @@ -438,9 +442,7 @@ JavaScriptCompiler.prototype = { // If the `value` is a lambda, replace it on the stack by // the return value of the lambda resolvePossibleLambda: function() { - this.aliases.lambda = 'this.lambda'; - - this.push(['lambda(', this.popStack(), ', ', this.contextName(0), ')']); + this.push([this.aliasable('this.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']); }, // [pushStringParam] @@ -546,15 +548,13 @@ JavaScriptCompiler.prototype = { // // If the helper is not found, `helperMissing` is called. invokeHelper: function(paramSize, name, isSimple) { - this.aliases.helperMissing = 'helpers.helperMissing'; - var nonHelper = this.popStack(); var helper = this.setupHelper(paramSize, name); var simple = isSimple ? [helper.name, ' || '] : ''; this.push( this.source.functionCall( - ['('].concat(simple, nonHelper, ' || ', 'helperMissing)'), + ['('].concat(simple, nonHelper, ' || ', this.aliasable('helpers.helperMissing'), ')'), 'call', helper.callParams)); }, @@ -584,8 +584,6 @@ JavaScriptCompiler.prototype = { // and can be avoided by passing the `knownHelpers` and // `knownHelpersOnly` flags at compile-time. invokeAmbiguous: function(name, helperCall) { - this.aliases.functionType = '"function"'; - this.aliases.helperMissing = 'helpers.helperMissing'; this.useRegister('helper'); var nonHelper = this.popStack(); @@ -596,9 +594,10 @@ JavaScriptCompiler.prototype = { var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); this.push([ - '((helper = (helper = ', helperName, ' || ', nonHelper, ') != null ? helper : helperMissing', + '((helper = (helper = ', helperName, ' || ', nonHelper, ') != null ? helper : ', + this.aliasable('helpers.helperMissing'), (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),', - '(typeof helper === functionType ? ', + '(typeof helper === ', this.aliasable('"function"'), ' ? ', this.source.functionCall('helper','call', helper.callParams), ' : helper))' ]); }, @@ -865,6 +864,20 @@ JavaScriptCompiler.prototype = { return this.source.objectLiteral(obj); }, + aliasable: function(name) { + var ret = this.aliases[name]; + if (ret) { + ret.referenceCount++; + return ret; + } + + ret = this.aliases[name] = this.source.wrap(name); + ret.aliasable = true; + ret.referenceCount = 1; + + return ret; + }, + setupHelper: function(paramSize, name, blockHelper) { var params = [], paramsInit = this.setupParams(name, paramSize, params, blockHelper); From 4a2afa9d029a242015fbd1f8c867435f83d59329 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 8 Nov 2014 19:06:40 -0600 Subject: [PATCH 30/99] Propagate options for client compiled partials Fixes #901 --- lib/handlebars/compiler/javascript-compiler.js | 2 ++ lib/handlebars/runtime.js | 2 +- spec/partials.js | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 70af1329e..af9c5de77 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -145,6 +145,8 @@ JavaScriptCompiler.prototype = { } else { ret = ret.toString(); } + } else { + ret.compilerOptions = this.options; } return ret; diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 59701757c..e1b656153 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -44,7 +44,7 @@ export function template(templateSpec, env) { if (result == null && env.compile) { var options = { helpers: helpers, partials: partials, data: data, depths: depths }; - partials[name] = env.compile(partial, { data: data !== undefined, compat: templateSpec.compat }, env); + partials[name] = env.compile(partial, templateSpec.compilerOptions, env); result = partials[name](context, options); } if (result != null) { diff --git a/spec/partials.js b/spec/partials.js index c5b8fdc22..a1e05388e 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -158,6 +158,15 @@ describe('partials', function() { handlebarsEnv.compile = compile; }); + it('should pass compiler flags', function() { + if (Handlebars.compile) { + var env = Handlebars.create(); + env.registerPartial('partial', '{{foo}}'); + var template = env.compile('{{foo}} {{> partial}}', {noEscape: true}); + equal(template({foo: '<'}), '< <'); + } + }); + describe('standalone partials', function() { it("indented partials", function() { var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; From 80b5b24cce10e5c8219566ae65dcbf7c6aabab05 Mon Sep 17 00:00:00 2001 From: MajorBreakfast Date: Tue, 2 Sep 2014 20:30:44 +0200 Subject: [PATCH 31/99] jspm compatibility This will enable compatibilty with jspm as soon as https://github.com/wycats/handlebars.js/issues/855 has been addressed. --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 6ed9a5526..583a80d70 100644 --- a/package.json +++ b/package.json @@ -58,5 +58,10 @@ }, "scripts": { "test": "grunt" + }, + "jspm": { + "main": "handlebars", + "directories": { "lib": "dist/amd" }, + "buildConfig": { "minify": true } } } From b8a9f7264d3b6ac48514272bf35291736cedad00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Mu=C3=B1oz?= Date: Tue, 11 Nov 2014 21:35:10 -0500 Subject: [PATCH 32/99] Add parser support for block params --- lib/handlebars/compiler/ast.js | 3 ++- lib/handlebars/compiler/helpers.js | 4 +++- lib/handlebars/compiler/printer.js | 9 +++++++++ spec/ast.js | 2 +- spec/compiler.js | 4 ++-- spec/parser.js | 10 +++++++++- spec/tokenizer.js | 14 ++++++++++++++ src/handlebars.l | 4 +++- src/handlebars.yy | 13 ++++++++----- 9 files changed, 51 insertions(+), 12 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 35a60dbed..0bc70e93a 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -9,10 +9,11 @@ function LocationInfo(locInfo) { } var AST = { - ProgramNode: function(statements, strip, locInfo) { + ProgramNode: function(statements, blockParams, strip, locInfo) { LocationInfo.call(this, locInfo); this.type = "program"; this.statements = statements; + this.blockParams = blockParams; this.strip = strip; }, diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index d9b7b14cf..50a3c53a3 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -23,7 +23,7 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { throw new Exception(openRawBlock.sexpr.id.original + " doesn't match " + close, errorNode); } - var program = new this.ProgramNode([content], {}, locInfo); + var program = new this.ProgramNode([content], null, {}, locInfo); return new this.BlockNode(openRawBlock.sexpr, program, undefined, undefined, locInfo); } @@ -40,6 +40,8 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver throw new Exception(openBlock.sexpr.id.original + ' doesn\'t match ' + close.path.original, errorNode); } + program.blockParams = openBlock.blockParams; + // Safely handle a chained inverse that does not have a non-conditional inverse // (i.e. both inverseAndProgram AND close are undefined) if (!close) { diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index c329373ea..e93652c88 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -26,6 +26,15 @@ PrintVisitor.prototype.program = function(program) { statements = program.statements, i, l; + if (program.blockParams) { + var blockParams = "BLOCK PARAMS: ["; + for(i=0, l=program.blockParams.length; i@\[-\^`\{-~]+/{LOOKAHEAD} "true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; "false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; \-?[0-9]+(?:\.[0-9]+)?/{LITERAL_LOOKAHEAD} return 'NUMBER'; +"as"\s+"|" return 'OPEN_BLOCK_PARAMS'; +"|" return 'CLOSE_BLOCK_PARAMS'; {ID} return 'ID'; diff --git a/src/handlebars.yy b/src/handlebars.yy index 5448966f7..775d5ca0f 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -9,7 +9,7 @@ root ; program - : statement* -> new yy.ProgramNode(yy.prepareProgram($1), {}, @$) + : statement* -> new yy.ProgramNode(yy.prepareProgram($1), null, {}, @$) ; statement @@ -39,11 +39,11 @@ block ; openBlock - : OPEN_BLOCK sexpr CLOSE -> { sexpr: $2, strip: yy.stripFlags($1, $3) } + : OPEN_BLOCK sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } ; openInverse - : OPEN_INVERSE sexpr CLOSE -> { sexpr: $2, strip: yy.stripFlags($1, $3) } + : OPEN_INVERSE sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } ; openInverseChain @@ -57,7 +57,7 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), - program = new yy.ProgramNode(yy.prepareProgram([inverse]), {}, @$); + program = new yy.ProgramNode(yy.prepareProgram([inverse]), null, {}, @$); program.inverse = inverse; @@ -104,6 +104,10 @@ hashSegment : ID EQUALS param -> [$1, $3] ; +blockParams + : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> $2 + ; + partialName : path -> new yy.PartialNameNode($1, @$) | STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$) @@ -122,4 +126,3 @@ pathSegments : pathSegments SEP ID { $1.push({part: $3, separator: $2}); $$ = $1; } | ID -> [{part: $1}] ; - From d044a8c99dc81293f4e210aad4ac5a22edb1040c Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 25 Nov 2014 23:13:57 -0600 Subject: [PATCH 33/99] Optimize variable declaration --- lib/handlebars/compiler/javascript-compiler.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index af9c5de77..ba84a3ef7 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -428,8 +428,7 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral('this.data(data, ' + depth + ')'); } - var len = parts.length; - for (var i = 0; i < len; i++) { + for (var i = 0, len = parts.length; i < len; i++) { this.replaceStack(function(current) { return [' && ', this.nameLookup(current, parts[i], 'data')]; }); From 617dd571621a9feca5eff86741aa55acb0e0ab48 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 25 Nov 2014 23:14:22 -0600 Subject: [PATCH 34/99] Lookup partials when undefined --- lib/handlebars/runtime.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index e1b656153..05759cba9 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -39,6 +39,9 @@ export function template(templateSpec, env) { if (hash) { context = Utils.extend({}, context, hash); } + if (!partial) { + partial = partials[name]; + } var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths); From c4b3c901a535df62edd8e750bd600aec6cd89413 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 07:48:45 -0600 Subject: [PATCH 35/99] Provide Handlebars.noConflict implementation Allows for users who are loading via a global pattern to avoid conflicting with other instances of the library. Fixes #887 --- lib/handlebars.js | 9 +++++++++ lib/handlebars.runtime.js | 9 +++++++++ spec/env/browser.js | 2 +- spec/env/runtime.js | 4 +++- spec/runtime.js | 18 ++++++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/handlebars.js b/lib/handlebars.js index 039ab3e17..5a4b301ae 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -30,6 +30,15 @@ var create = function() { Handlebars = create(); Handlebars.create = create; +/*jshint -W040 */ +var root = typeof global !== 'undefined' ? global : window, + $Handlebars = root.Handlebars; +Handlebars.noConflict = function() { + if (root.Handlebars === Handlebars) { + root.Handlebars = $Handlebars; + } +}; + Handlebars['default'] = Handlebars; export default Handlebars; diff --git a/lib/handlebars.runtime.js b/lib/handlebars.runtime.js index bc07714f2..49f29632f 100644 --- a/lib/handlebars.runtime.js +++ b/lib/handlebars.runtime.js @@ -29,6 +29,15 @@ var create = function() { var Handlebars = create(); Handlebars.create = create; +/*jshint -W040 */ +var root = typeof global !== 'undefined' ? global : window, + $Handlebars = root.Handlebars; +Handlebars.noConflict = function() { + if (root.Handlebars === Handlebars) { + root.Handlebars = $Handlebars; + } +}; + Handlebars['default'] = Handlebars; export default Handlebars; diff --git a/spec/env/browser.js b/spec/env/browser.js index a0ce2431b..a9042e3fb 100644 --- a/spec/env/browser.js +++ b/spec/env/browser.js @@ -5,7 +5,7 @@ var _ = require('underscore'), fs = require('fs'), vm = require('vm'); -global.Handlebars = undefined; +global.Handlebars = 'no-conflict'; vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js'); global.CompilerContext = { diff --git a/spec/env/runtime.js b/spec/env/runtime.js index 5a2dcd9b3..0c144064e 100644 --- a/spec/env/runtime.js +++ b/spec/env/runtime.js @@ -5,7 +5,7 @@ var _ = require('underscore'), fs = require('fs'), vm = require('vm'); -global.Handlebars = undefined; +global.Handlebars = 'no-conflict'; vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js'); var parse = require('../../dist/cjs/handlebars/compiler/base').parse; @@ -13,6 +13,8 @@ var compiler = require('../../dist/cjs/handlebars/compiler/compiler'); var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler')['default']; global.CompilerContext = { + browser: true, + compile: function(template, options) { // Hack the compiler on to the environment for these specific tests handlebarsEnv.precompile = function(template, options) { diff --git a/spec/runtime.js b/spec/runtime.js index d33dd747c..48a22a99b 100644 --- a/spec/runtime.js +++ b/spec/runtime.js @@ -60,4 +60,22 @@ describe('runtime', function() { equal(template._child(1, undefined, [{bar: 'baz'}])(), 'baz'); }); }); + + describe('#noConflict', function() { + if (!CompilerContext.browser) { + return; + } + + it('should reset on no conflict', function() { + var reset = Handlebars; + Handlebars.noConflict(); + equal(Handlebars, 'no-conflict'); + + Handlebars = 'really, none'; + reset.noConflict(); + equal(Handlebars, 'really, none'); + + Handlebars = reset; + }); + }); }); From 5594416121734e91a6071ab23425c81eaf64fe86 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 07:49:29 -0600 Subject: [PATCH 36/99] Export the default object for handlebars/runtime Maintains consistency with the prior export pattern, while avoids the knarly default loading issues. --- runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime.js b/runtime.js index b7a7b12eb..1896fa9db 100644 --- a/runtime.js +++ b/runtime.js @@ -1,3 +1,3 @@ // Create a simple path alias to allow browserify to resolve // the runtime on a supported path. -module.exports = require('./dist/cjs/handlebars.runtime'); +module.exports = require('./dist/cjs/handlebars.runtime').default; From e4c874d1afe48cc77138acf7d3ee15261e27f6e4 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 08:00:23 -0600 Subject: [PATCH 37/99] Update to latest es6-module-packager --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 351f8e49f..61bce7125 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "benchmark": "~1.0", "dustjs-linkedin": "~2.0.2", "eco": "~1.1.0-rc-3", - "es6-module-packager": "1.x", + "es6-module-packager": "^2.0.0", "grunt": "~0.4.1", "grunt-cli": "~0.1.10", "grunt-contrib-clean": "~0.4.1", From 3a9440f954092558275cd4c05a35ba34bcbfa210 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 08:02:55 -0600 Subject: [PATCH 38/99] Make each helper data uniform Provide @key and @last value for all forms of iteration. Fixes #910 --- lib/handlebars/base.js | 45 ++++++++++++++++++++++++------------------ spec/builtins.js | 10 ++++++++++ 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index cec293791..50aa3f82b 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -117,36 +117,43 @@ function registerDefaultHelpers(instance) { data = createFrame(options.data); } + function execIteration(key, i, last) { + if (data) { + data.key = key; + data.index = i; + data.first = i === 0; + data.last = !!last; + + if (contextPath) { + data.contextPath = contextPath + key; + } + } + ret = ret + fn(context[key], { data: data }); + } + if(context && typeof context === 'object') { if (isArray(context)) { for(var j = context.length; i Date: Wed, 26 Nov 2014 09:01:03 -0600 Subject: [PATCH 39/99] Update AST location info to match SpiderMonkey Part of #889 --- lib/handlebars/compiler/ast.js | 36 ++++++-------- lib/handlebars/compiler/base.js | 7 ++- lib/handlebars/compiler/code-gen.js | 8 ++-- lib/handlebars/compiler/compiler.js | 10 ++-- lib/handlebars/compiler/helpers.js | 22 +++++---- .../compiler/javascript-compiler.js | 2 +- lib/handlebars/exception.js | 17 ++++--- spec/ast.js | 39 ++++++++------- src/handlebars.yy | 48 +++++++++---------- 9 files changed, 95 insertions(+), 94 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 0bc70e93a..363c25209 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,16 +1,8 @@ import Exception from "../exception"; -function LocationInfo(locInfo) { - locInfo = locInfo || {}; - this.firstLine = locInfo.first_line; - this.firstColumn = locInfo.first_column; - this.lastColumn = locInfo.last_column; - this.lastLine = locInfo.last_line; -} - var AST = { ProgramNode: function(statements, blockParams, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "program"; this.statements = statements; this.blockParams = blockParams; @@ -18,7 +10,7 @@ var AST = { }, MustacheNode: function(rawParams, hash, open, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "mustache"; this.strip = strip; @@ -47,7 +39,7 @@ var AST = { }, SexprNode: function(rawParams, hash, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "sexpr"; this.hash = hash; @@ -70,7 +62,7 @@ var AST = { }, PartialNode: function(partialName, context, hash, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "partial"; this.partialName = partialName; this.context = context; @@ -81,7 +73,7 @@ var AST = { }, BlockNode: function(sexpr, program, inverse, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = 'block'; this.sexpr = sexpr; @@ -95,19 +87,19 @@ var AST = { }, ContentNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "content"; this.original = this.string = string; }, HashNode: function(pairs, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "hash"; this.pairs = pairs; }, IdNode: function(parts, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "ID"; var original = "", @@ -147,13 +139,13 @@ var AST = { }, PartialNameNode: function(name, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "PARTIAL_NAME"; this.name = name.original; }, DataNode: function(id, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "DATA"; this.id = id; this.stringModeValue = id.stringModeValue; @@ -161,7 +153,7 @@ var AST = { }, StringNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "STRING"; this.original = this.string = @@ -169,7 +161,7 @@ var AST = { }, NumberNode: function(number, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "NUMBER"; this.original = this.number = number; @@ -177,14 +169,14 @@ var AST = { }, BooleanNode: function(bool, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "BOOLEAN"; this.bool = bool; this.stringModeValue = bool === "true"; }, CommentNode: function(comment, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "comment"; this.comment = comment; diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 13784637e..df6b92887 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -8,11 +8,16 @@ export { parser }; var yy = {}; extend(yy, Helpers, AST); -export function parse(input) { +export function parse(input, options) { // Just return if an already-compile AST was passed in. if (input.constructor === AST.ProgramNode) { return input; } parser.yy = yy; + // Altering the shared object here, but this is ok as parser is a sync operation + yy.locInfo = function(locInfo) { + return new yy.SourceLocation(options && options.srcName, locInfo); + }; + return parser.parse(input); } diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index 7d1b4ca2e..30f0735e1 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -79,18 +79,18 @@ CodeGen.prototype = { }, empty: function(loc) { - loc = loc || this.currentLocation || {}; - return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile); + loc = loc || this.currentLocation || {start:{}}; + return new SourceNode(loc.start.line, loc.start.column, this.srcFile); }, wrap: function(chunk, loc) { if (chunk instanceof SourceNode) { return chunk; } - loc = loc || this.currentLocation || {}; + loc = loc || this.currentLocation || {start:{}}; chunk = castChunk(chunk, this, loc); - return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile, chunk); + return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk); }, functionCall: function(fn, type, params) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 1e5d07afa..d80bfac11 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -298,11 +298,7 @@ Compiler.prototype = { // HELPERS opcode: function(name, node) { - var loc = { - firstLine: node.firstLine, firstColumn: node.firstColumn, - lastLine: node.lastLine, lastColumn: node.lastColumn - }; - this.opcodes.push({ opcode: name, args: slice.call(arguments, 2), loc: loc }); + this.opcodes.push({ opcode: name, args: slice.call(arguments, 2), loc: node.loc }); }, addDepth: function(depth) { @@ -393,7 +389,7 @@ export function precompile(input, options, env) { options.useDepths = true; } - var ast = env.parse(input); + var ast = env.parse(input, options); var environment = new env.Compiler().compile(ast, options); return new env.JavaScriptCompiler().compile(environment, options); } @@ -415,7 +411,7 @@ export function compile(input, options, env) { var compiled; function compileInput() { - var ast = env.parse(input); + var ast = env.parse(input, options); var environment = new env.Compiler().compile(ast, options); var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true); return env.template(templateSpec); diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 50a3c53a3..3a3ece661 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -1,5 +1,17 @@ import Exception from "../exception"; +export function SourceLocation(source, locInfo) { + this.source = source; + this.start = { + line: locInfo.first_line, + column: locInfo.first_column + }; + this.end = { + line: locInfo.last_line, + column: locInfo.last_column + }; +} + export function stripFlags(open, close) { return { left: open.charAt(2) === '~', @@ -15,10 +27,7 @@ export function stripComment(comment) { export function prepareRawBlock(openRawBlock, content, close, locInfo) { /*jshint -W040 */ if (openRawBlock.sexpr.id.original !== close) { - var errorNode = { - firstLine: openRawBlock.sexpr.firstLine, - firstColumn: openRawBlock.sexpr.firstColumn - }; + var errorNode = {loc: openRawBlock.sexpr.loc}; throw new Exception(openRawBlock.sexpr.id.original + " doesn't match " + close, errorNode); } @@ -32,10 +41,7 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver /*jshint -W040 */ // When we are chaining inverse calls, we will not have a close path if (close && close.path && openBlock.sexpr.id.original !== close.path.original) { - var errorNode = { - firstLine: openBlock.sexpr.firstLine, - firstColumn: openBlock.sexpr.firstColumn - }; + var errorNode = {loc: openBlock.sexpr.loc}; throw new Exception(openBlock.sexpr.id.original + ' doesn\'t match ' + close.path.original, errorNode); } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index ba84a3ef7..639aa4118 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -136,7 +136,7 @@ JavaScriptCompiler.prototype = { if (!asObject) { ret.compiler = JSON.stringify(ret.compiler); - this.source.currentLocation = {firstLine: 1, firstColumn: 0}; + this.source.currentLocation = {start: {line: 1, column: 0}}; ret = this.objectLiteral(ret); if (options.srcName) { diff --git a/lib/handlebars/exception.js b/lib/handlebars/exception.js index 8c5c2f669..3fde1c14f 100644 --- a/lib/handlebars/exception.js +++ b/lib/handlebars/exception.js @@ -2,11 +2,14 @@ var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; + var loc = node && node.loc, + line, + column; + if (loc) { + line = loc.start.line; + column = loc.start.column; + + message += ' - ' + line + ':' + column; } var tmp = Error.prototype.constructor.call(this, message); @@ -16,9 +19,9 @@ function Exception(message, node) { this[errorProps[idx]] = tmp[errorProps[idx]]; } - if (line) { + if (loc) { this.lineNumber = line; - this.column = node.firstColumn; + this.column = column; } } diff --git a/spec/ast.js b/spec/ast.js index ef2ef68f5..83b24b999 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -5,22 +5,21 @@ describe('ast', function() { } var LOCATION_INFO = { - last_line: 0, - first_line: 0, - first_column: 0, - last_column: 0 + start: { + line: 1, + column: 1 + }, + end: { + line: 1, + column: 1 + } }; function testLocationInfoStorage(node){ - var properties = [ 'firstLine', 'lastLine', 'firstColumn', 'lastColumn' ], - property, - propertiesLen = properties.length, - i; - - for (i = 0; i < propertiesLen; i++){ - property = properties[0]; - equals(node[property], 0); - } + equals(node.loc.start.line, 1); + equals(node.loc.start.column, 1); + equals(node.loc.end.line, 1); + equals(node.loc.end.column, 1); } describe('MustacheNode', function() { @@ -94,21 +93,21 @@ describe('ast', function() { {part: 'foo'}, {part: '..'}, {part: 'bar'} - ], {first_line: 1, first_column: 1}); + ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo.. - 1:1"); shouldThrow(function() { new handlebarsEnv.AST.IdNode([ {part: 'foo'}, {part: '.'}, {part: 'bar'} - ], {first_line: 1, first_column: 1}); + ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo. - 1:1"); shouldThrow(function() { new handlebarsEnv.AST.IdNode([ {part: 'foo'}, {part: 'this'}, {part: 'bar'} - ], {first_line: 1, first_column: 1}); + ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foothis - 1:1"); }); @@ -201,10 +200,10 @@ describe('ast', function() { var ast, statements; function testColumns(node, firstLine, lastLine, firstColumn, lastColumn){ - equals(node.firstLine, firstLine); - equals(node.lastLine, lastLine); - equals(node.firstColumn, firstColumn); - equals(node.lastColumn, lastColumn); + equals(node.loc.start.line, firstLine); + equals(node.loc.start.column, firstColumn); + equals(node.loc.end.line, lastLine); + equals(node.loc.end.column, lastColumn); } ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" + diff --git a/src/handlebars.yy b/src/handlebars.yy index 775d5ca0f..f5c69ff6f 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -9,7 +9,7 @@ root ; program - : statement* -> new yy.ProgramNode(yy.prepareProgram($1), null, {}, @$) + : statement* -> new yy.ProgramNode(yy.prepareProgram($1), null, {}, yy.locInfo(@$)) ; statement @@ -18,15 +18,15 @@ statement | rawBlock -> $1 | partial -> $1 | content -> $1 - | COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), @$) + | COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) ; content - : CONTENT -> new yy.ContentNode($1, @$) + : CONTENT -> new yy.ContentNode($1, yy.locInfo(@$)) ; rawBlock - : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) + : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, yy.locInfo(@$)) ; openRawBlock @@ -34,8 +34,8 @@ openRawBlock ; block - : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) - | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) + : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, yy.locInfo(@$)) + | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, yy.locInfo(@$)) ; openBlock @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; inverseAndProgram @@ -56,8 +56,8 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { - var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), - program = new yy.ProgramNode(yy.prepareProgram([inverse]), null, {}, @$); + var inverse = yy.prepareBlock($1, $2, $3, $3, false, yy.locInfo(@$)), + program = new yy.ProgramNode(yy.prepareProgram([inverse]), null, {}, yy.locInfo(@$)); program.inverse = inverse; @@ -73,31 +73,31 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; partial - : OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), @$) - | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), @$) + : OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) + | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), yy.locInfo(@$)) ; sexpr - : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, @$) - | dataName -> new yy.SexprNode([$1], null, @$) + : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, yy.locInfo(@$)) + | dataName -> new yy.SexprNode([$1], null, yy.locInfo(@$)) ; param : path -> $1 - | STRING -> new yy.StringNode($1, @$) - | NUMBER -> new yy.NumberNode($1, @$) - | BOOLEAN -> new yy.BooleanNode($1, @$) + | STRING -> new yy.StringNode($1, yy.locInfo(@$)) + | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) + | BOOLEAN -> new yy.BooleanNode($1, yy.locInfo(@$)) | dataName -> $1 | OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;} ; hash - : hashSegment+ -> new yy.HashNode($1, @$) + : hashSegment+ -> new yy.HashNode($1, yy.locInfo(@$)) ; hashSegment @@ -109,17 +109,17 @@ blockParams ; partialName - : path -> new yy.PartialNameNode($1, @$) - | STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$) - | NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, @$)) + : path -> new yy.PartialNameNode($1, yy.locInfo(@$)) + | STRING -> new yy.PartialNameNode(new yy.StringNode($1, yy.locInfo(@$)), yy.locInfo(@$)) + | NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, yy.locInfo(@$))) ; dataName - : DATA path -> new yy.DataNode($2, @$) + : DATA path -> new yy.DataNode($2, yy.locInfo(@$)) ; path - : pathSegments -> new yy.IdNode($1, @$) + : pathSegments -> new yy.IdNode($1, yy.locInfo(@$)) ; pathSegments From df421f1e4b43773c61bc7c2c3cd06e0ff9ec4905 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 09:18:33 -0600 Subject: [PATCH 40/99] Update ProgramNode to better match SpiderMonkey --- lib/handlebars/compiler/ast.js | 5 +- lib/handlebars/compiler/base.js | 2 +- lib/handlebars/compiler/compiler.js | 14 ++-- lib/handlebars/compiler/helpers.js | 78 +++++++++---------- lib/handlebars/compiler/printer.js | 16 ++-- lib/handlebars/compiler/visitor.js | 8 +- spec/ast.js | 112 ++++++++++++++-------------- src/handlebars.yy | 2 +- 8 files changed, 119 insertions(+), 118 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 363c25209..a0ed42e28 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -3,8 +3,9 @@ import Exception from "../exception"; var AST = { ProgramNode: function(statements, blockParams, strip, locInfo) { this.loc = locInfo; - this.type = "program"; - this.statements = statements; + this.type = 'Program'; + this.body = statements; + this.blockParams = blockParams; this.strip = strip; }, diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index df6b92887..786c37ebd 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -10,7 +10,7 @@ extend(yy, Helpers, AST); export function parse(input, options) { // Just return if an already-compile AST was passed in. - if (input.constructor === AST.ProgramNode) { return input; } + if (input.type === 'Program') { return input; } parser.yy = yy; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index d80bfac11..683e3e50b 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -74,12 +74,12 @@ Compiler.prototype = { return this[node.type](node); }, - program: function(program) { - var statements = program.statements; - - for(var i=0, l=statements.length; i partial }} '); - equals(ast.statements[1].string, ''); + equals(ast.body[1].string, ''); }); it('marks indented partial as standalone', function() { var ast = Handlebars.parse(' {{> partial }} '); - equals(ast.statements[0].string, ''); - equals(ast.statements[1].indent, ' '); - equals(ast.statements[2].string, ''); + equals(ast.body[0].string, ''); + equals(ast.body[1].indent, ' '); + equals(ast.body[2].string, ''); }); it('marks those around content as not standalone', function() { var ast = Handlebars.parse('a{{> partial }}'); - equals(ast.statements[0].omit, undefined); + equals(ast.body[0].omit, undefined); ast = Handlebars.parse('{{> partial }}a'); - equals(ast.statements[1].omit, undefined); + equals(ast.body[1].omit, undefined); }); }); describe('comments', function() { it('marks comment as standalone', function() { var ast = Handlebars.parse('{{! comment }} '); - equals(ast.statements[1].string, ''); + equals(ast.body[1].string, ''); }); it('marks indented comment as standalone', function() { var ast = Handlebars.parse(' {{! comment }} '); - equals(ast.statements[0].string, ''); - equals(ast.statements[2].string, ''); + equals(ast.body[0].string, ''); + equals(ast.body[2].string, ''); }); it('marks those around content as not standalone', function() { var ast = Handlebars.parse('a{{! comment }}'); - equals(ast.statements[0].omit, undefined); + equals(ast.body[0].omit, undefined); ast = Handlebars.parse('{{! comment }}a'); - equals(ast.statements[1].omit, undefined); + equals(ast.body[1].omit, undefined); }); }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index f5c69ff6f..27d12e565 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -5,7 +5,7 @@ %% root - : program EOF { yy.prepareProgram($1.statements, true); return $1; } + : program EOF { yy.prepareProgram($1.body, true); return $1; } ; program From 5cfe6a0be16a912a5f00af251753a3d5746a6577 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 09:31:46 -0600 Subject: [PATCH 41/99] Update MustacheNode for new AST structure --- lib/handlebars/compiler/ast.js | 21 +++++---------------- lib/handlebars/compiler/compiler.js | 2 +- lib/handlebars/compiler/printer.js | 8 ++++---- lib/handlebars/compiler/visitor.js | 8 ++++---- spec/ast.js | 13 ++++--------- src/handlebars.yy | 6 +++--- 6 files changed, 21 insertions(+), 37 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index a0ed42e28..2f95cfb96 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -10,10 +10,11 @@ var AST = { this.strip = strip; }, - MustacheNode: function(rawParams, hash, open, strip, locInfo) { + MustacheNode: function(rawParams, open, strip, locInfo) { this.loc = locInfo; - this.type = "mustache"; - this.strip = strip; + this.type = 'MustacheStatement'; + + this.sexpr = rawParams; // Open may be a string parsed from the parser or a passed boolean flag if (open != null && open.charAt) { @@ -24,19 +25,7 @@ var AST = { this.escaped = !!open; } - if (rawParams instanceof AST.SexprNode) { - this.sexpr = rawParams; - } else { - // Support old AST API - this.sexpr = new AST.SexprNode(rawParams, hash); - } - - // Support old AST API that stored this info in MustacheNode - this.id = this.sexpr.id; - this.params = this.sexpr.params; - this.hash = this.sexpr.hash; - this.eligibleHelper = this.sexpr.eligibleHelper; - this.isHelper = this.sexpr.isHelper; + this.strip = strip; }, SexprNode: function(rawParams, hash, locInfo) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 683e3e50b..69937a5a4 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -193,7 +193,7 @@ Compiler.prototype = { } }, - mustache: function(mustache) { + MustacheStatement: function(mustache) { this.sexpr(mustache.sexpr); if(mustache.escaped && !this.options.noEscape) { diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 8c2346ec1..481c7be10 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -44,6 +44,10 @@ PrintVisitor.prototype.Program = function(program) { return out; }; +PrintVisitor.prototype.MustacheStatement = function(mustache) { + return this.pad('{{ ' + this.accept(mustache.sexpr) + ' }}'); +}; + PrintVisitor.prototype.block = function(block) { var out = ""; @@ -83,10 +87,6 @@ PrintVisitor.prototype.sexpr = function(sexpr) { return this.accept(sexpr.id) + " " + params + hash; }; -PrintVisitor.prototype.mustache = function(mustache) { - return this.pad("{{ " + this.accept(mustache.sexpr) + " }}"); -}; - PrintVisitor.prototype.partial = function(partial) { var content = this.accept(partial.partialName); if(partial.context) { diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 12d822ce9..dc7f527fe 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -16,16 +16,16 @@ Visitor.prototype = { } }, + MustacheStatement: function(mustache) { + this.accept(mustache.sexpr); + }, + block: function(block) { this.accept(block.mustache); this.accept(block.program); this.accept(block.inverse); }, - mustache: function(mustache) { - this.accept(mustache.sexpr); - }, - sexpr: function(sexpr) { var params = sexpr.params, paramStrings = [], hash; diff --git a/spec/ast.js b/spec/ast.js index e13e9569e..d41a591da 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -24,21 +24,16 @@ describe('ast', function() { describe('MustacheNode', function() { function testEscape(open, expected) { - var mustache = new handlebarsEnv.AST.MustacheNode([{}], {}, open, false); + var mustache = new handlebarsEnv.AST.MustacheNode([{}], open, false); equals(mustache.escaped, expected); } it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false, LOCATION_INFO); - equals(mustache.type, 'mustache'); - equals(mustache.hash, hash); + mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], '', false, LOCATION_INFO); + equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); - equals(mustache.id, id); - equals(mustache.params.length, 1); - equals(mustache.params[0], 'param1'); - equals(!!mustache.isHelper, true); testLocationInfoStorage(mustache); }); it('should accept token for escape', function() { @@ -280,7 +275,7 @@ describe('ast', function() { block = ast.body[0]; equals(block.program.body[0].string, ''); - equals(block.program.body[1].id.original, 'foo'); + equals(block.program.body[1].sexpr.id.original, 'foo'); equals(block.program.body[2].string, '\n'); }); it('marks nested block mustaches as standalone', function() { diff --git a/src/handlebars.yy b/src/handlebars.yy index 27d12e565..d7d117cf1 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; inverseAndProgram @@ -73,8 +73,8 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN sexpr CLOSE -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; partial From 2a4819d75cba7513946af5cf28ee22881561814f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 12:46:38 -0600 Subject: [PATCH 42/99] Update statement node ASTs --- lib/handlebars/compiler/ast.js | 81 ++++++++++------------- lib/handlebars/compiler/compiler.js | 99 +++++++++++++++-------------- lib/handlebars/compiler/helpers.js | 22 +++---- lib/handlebars/compiler/printer.js | 51 +++++++-------- lib/handlebars/compiler/visitor.js | 27 ++++---- spec/ast.js | 63 ++++++++---------- spec/parser.js | 2 + spec/partials.js | 6 ++ spec/visitor.js | 14 ++-- src/handlebars.yy | 13 ++-- 10 files changed, 179 insertions(+), 199 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 2f95cfb96..26ea4ab5c 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -28,6 +28,41 @@ var AST = { this.strip = strip; }, + BlockNode: function(sexpr, program, inverse, strip, locInfo) { + this.loc = locInfo; + + this.type = 'BlockStatement'; + this.sexpr = sexpr; + this.program = program; + this.inverse = inverse; + this.strip = strip; + }, + + PartialNode: function(sexpr, strip, locInfo) { + this.loc = locInfo; + this.type = 'PartialStatement'; + this.sexpr = sexpr; + this.indent = ''; + + this.strip = strip; + this.strip.inlineStandalone = true; + }, + + ContentNode: function(string, locInfo) { + this.loc = locInfo; + this.type = 'ContentStatement'; + this.original = this.value = string; + }, + + CommentNode: function(comment, strip, locInfo) { + this.loc = locInfo; + this.type = 'CommentStatement'; + this.value = comment; + + this.strip = strip; + strip.inlineStandalone = true; + }, + SexprNode: function(rawParams, hash, locInfo) { this.loc = locInfo; @@ -51,37 +86,6 @@ var AST = { // pass or at runtime. }, - PartialNode: function(partialName, context, hash, strip, locInfo) { - this.loc = locInfo; - this.type = "partial"; - this.partialName = partialName; - this.context = context; - this.hash = hash; - this.strip = strip; - - this.strip.inlineStandalone = true; - }, - - BlockNode: function(sexpr, program, inverse, strip, locInfo) { - this.loc = locInfo; - - this.type = 'block'; - this.sexpr = sexpr; - this.program = program; - this.inverse = inverse; - this.strip = strip; - - if (inverse && !program) { - this.isInverse = true; - } - }, - - ContentNode: function(string, locInfo) { - this.loc = locInfo; - this.type = "content"; - this.original = this.string = string; - }, - HashNode: function(pairs, locInfo) { this.loc = locInfo; this.type = "hash"; @@ -128,12 +132,6 @@ var AST = { this.stringModeValue = this.string; }, - PartialNameNode: function(name, locInfo) { - this.loc = locInfo; - this.type = "PARTIAL_NAME"; - this.name = name.original; - }, - DataNode: function(id, locInfo) { this.loc = locInfo; this.type = "DATA"; @@ -163,15 +161,6 @@ var AST = { this.type = "BOOLEAN"; this.bool = bool; this.stringModeValue = bool === "true"; - }, - - CommentNode: function(comment, strip, locInfo) { - this.loc = locInfo; - this.type = "comment"; - this.comment = comment; - - this.strip = strip; - strip.inlineStandalone = true; } }; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 69937a5a4..ea2272a1b 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -107,24 +107,19 @@ Compiler.prototype = { return guid; }, - block: function(block) { + BlockStatement: function(block) { var sexpr = block.sexpr, program = block.program, inverse = block.inverse; - if (program) { - program = this.compileProgram(program); - } - - if (inverse) { - inverse = this.compileProgram(inverse); - } + program = program && this.compileProgram(program); + inverse = inverse && this.compileProgram(inverse); var type = this.classifySexpr(sexpr); - if (type === "helper") { + if (type === 'helper') { this.helperSexpr(sexpr, program, inverse); - } else if (type === "simple") { + } else if (type === 'simple') { this.simpleSexpr(sexpr); // now that the simple mustache is resolved, we need to @@ -147,32 +142,23 @@ Compiler.prototype = { this.opcode('append', block); }, - hash: function(hash) { - var pairs = hash.pairs, i, l; - - this.opcode('pushHash', hash); - - for(i=0, l=pairs.length; i 1) { + throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); + } + + this.pushParam(params[0]); } else { this.opcode('getContext', partial, 0); this.opcode('pushContext', partial); @@ -183,16 +169,10 @@ Compiler.prototype = { this.opcode('appendContent', partial, indent); indent = ''; } - this.opcode('invokePartial', partial, partialName.name, indent); + this.opcode('invokePartial', partial, partialName, indent); this.opcode('append', partial); }, - content: function(content) { - if (content.string) { - this.opcode('appendContent', content, content.string); - } - }, - MustacheStatement: function(mustache) { this.sexpr(mustache.sexpr); @@ -203,6 +183,25 @@ Compiler.prototype = { } }, + ContentStatement: function(content) { + if (content.value) { + this.opcode('appendContent', content, content.value); + } + }, + + CommentStatement: function() {}, + + sexpr: function(sexpr) { + var type = this.classifySexpr(sexpr); + + if (type === "simple") { + this.simpleSexpr(sexpr); + } else if (type === "helper") { + this.helperSexpr(sexpr); + } else { + this.ambiguousSexpr(sexpr); + } + }, ambiguousSexpr: function(sexpr, program, inverse) { var id = sexpr.id, name = id.parts[0], @@ -252,16 +251,18 @@ Compiler.prototype = { } }, - sexpr: function(sexpr) { - var type = this.classifySexpr(sexpr); + hash: function(hash) { + var pairs = hash.pairs, i, l; - if (type === "simple") { - this.simpleSexpr(sexpr); - } else if (type === "helper") { - this.helperSexpr(sexpr); - } else { - this.ambiguousSexpr(sexpr); + this.opcode('pushHash', hash); + + for(i=0, l=pairs.length; i ' + content + ' }}'); +}; + +PrintVisitor.prototype.ContentStatement = function(content) { + return this.pad("CONTENT[ '" + content.value + "' ]"); +}; + +PrintVisitor.prototype.CommentStatement = function(comment) { + return this.pad("{{! '" + comment.value + "' }}"); +}; + PrintVisitor.prototype.sexpr = function(sexpr) { var params = sexpr.params, paramStrings = [], hash; @@ -87,17 +107,6 @@ PrintVisitor.prototype.sexpr = function(sexpr) { return this.accept(sexpr.id) + " " + params + hash; }; -PrintVisitor.prototype.partial = function(partial) { - var content = this.accept(partial.partialName); - if(partial.context) { - content += " " + this.accept(partial.context); - } - if (partial.hash) { - content += " " + this.accept(partial.hash); - } - return this.pad("{{> " + content + " }}"); -}; - PrintVisitor.prototype.hash = function(hash) { var pairs = hash.pairs; var joinedPairs = [], left, right; @@ -132,19 +141,7 @@ PrintVisitor.prototype.ID = function(id) { } }; -PrintVisitor.prototype.PARTIAL_NAME = function(partialName) { - return "PARTIAL:" + partialName.name; -}; - PrintVisitor.prototype.DATA = function(data) { return "@" + this.accept(data.id); }; -PrintVisitor.prototype.content = function(content) { - return this.pad("CONTENT[ '" + content.string + "' ]"); -}; - -PrintVisitor.prototype.comment = function(comment) { - return this.pad("{{! '" + comment.comment + "' }}"); -}; - diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index dc7f527fe..e48c9bcee 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -4,7 +4,7 @@ Visitor.prototype = { constructor: Visitor, accept: function(object) { - return object && this[object.type] && this[object.type](object); + return object && this[object.type](object); }, Program: function(program) { @@ -20,12 +20,21 @@ Visitor.prototype = { this.accept(mustache.sexpr); }, - block: function(block) { - this.accept(block.mustache); + BlockStatement: function(block) { + this.accept(block.sexpr); this.accept(block.program); this.accept(block.inverse); }, + PartialStatement: function(partial) { + this.accept(partial.partialName); + this.accept(partial.context); + this.accept(partial.hash); + }, + + ContentStatement: function(content) {}, + CommentStatement: function(comment) {}, + sexpr: function(sexpr) { var params = sexpr.params, paramStrings = [], hash; @@ -44,13 +53,6 @@ Visitor.prototype = { } }, - partial: function(partial) { - this.accept(partial.partialName); - this.accept(partial.context); - this.accept(partial.hash); - }, - PARTIAL_NAME: function(partialName) {}, - DATA: function(data) { this.accept(data.id); }, @@ -58,10 +60,7 @@ Visitor.prototype = { STRING: function(string) {}, NUMBER: function(number) {}, BOOLEAN: function(bool) {}, - ID: function(id) {}, - - content: function(content) {}, - comment: function(comment) {} + ID: function(id) {} }; export default Visitor; diff --git a/spec/ast.js b/spec/ast.js index d41a591da..dc6e136ec 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -168,18 +168,9 @@ describe('ast', function() { }); }); - describe("PartialNameNode", function(){ - - it('stores location info', function(){ - var pnn = new handlebarsEnv.AST.PartialNameNode({original: "YES"}, LOCATION_INFO); - testLocationInfoStorage(pnn); - }); - }); - describe("PartialNode", function(){ - it('stores location info', function(){ - var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, {}, LOCATION_INFO); + var pn = new handlebarsEnv.AST.PartialNode('so_partial', {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); @@ -248,8 +239,8 @@ describe('ast', function() { describe('mustache', function() { it('does not mark mustaches as standalone', function() { var ast = Handlebars.parse(' {{comment}} '); - equals(!!ast.body[0].string, true); - equals(!!ast.body[2].string, true); + equals(!!ast.body[0].value, true); + equals(!!ast.body[2].value, true); }); }); describe('blocks', function() { @@ -257,38 +248,38 @@ describe('ast', function() { var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '), block = ast.body[1]; - equals(ast.body[0].string, ''); + equals(ast.body[0].value, ''); - equals(block.program.body[0].string, 'foo\n'); - equals(block.inverse.body[0].string, ' bar \n'); + equals(block.program.body[0].value, 'foo\n'); + equals(block.inverse.body[0].value, ' bar \n'); - equals(ast.body[2].string, ''); + equals(ast.body[2].value, ''); }); it('marks initial block mustaches as standalone', function() { var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'), block = ast.body[0]; - equals(block.program.body[0].string, 'foo\n'); + equals(block.program.body[0].value, 'foo\n'); }); it('marks mustaches with children as standalone', function() { var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'), block = ast.body[0]; - equals(block.program.body[0].string, ''); + equals(block.program.body[0].value, ''); equals(block.program.body[1].sexpr.id.original, 'foo'); - equals(block.program.body[2].string, '\n'); + equals(block.program.body[2].value, '\n'); }); it('marks nested block mustaches as standalone', function() { var ast = Handlebars.parse('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'), body = ast.body[0].program.body, block = body[1]; - equals(body[0].string, ''); + equals(body[0].value, ''); - equals(block.program.body[0].string, 'foo\n'); - equals(block.inverse.body[0].string, ' bar \n'); + equals(block.program.body[0].value, 'foo\n'); + equals(block.inverse.body[0].value, ' bar \n'); - equals(body[0].string, ''); + equals(body[0].value, ''); }); it('does not mark nested block mustaches as standalone', function() { var ast = Handlebars.parse('{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'), @@ -297,8 +288,8 @@ describe('ast', function() { equals(body[0].omit, undefined); - equals(block.program.body[0].string, ' \nfoo\n'); - equals(block.inverse.body[0].string, ' bar \n '); + equals(block.program.body[0].value, ' \nfoo\n'); + equals(block.inverse.body[0].value, ' bar \n '); equals(body[0].omit, undefined); }); @@ -307,8 +298,8 @@ describe('ast', function() { body = ast.body[0].program.body, block = body[0]; - equals(block.program.body[0].string, ' \nfoo\n'); - equals(block.inverse.body[0].string, ' bar \n '); + equals(block.program.body[0].value, ' \nfoo\n'); + equals(block.inverse.body[0].value, ' bar \n '); equals(body[0].omit, undefined); }); @@ -319,22 +310,22 @@ describe('ast', function() { equals(ast.body[0].omit, undefined); - equals(block.program.body[0].string, 'foo\n'); - equals(block.inverse.body[0].string, ' bar \n'); + equals(block.program.body[0].value, 'foo\n'); + equals(block.inverse.body[0].value, ' bar \n'); - equals(ast.body[2].string, ''); + equals(ast.body[2].value, ''); }); }); describe('partials', function() { it('marks partial as standalone', function() { var ast = Handlebars.parse('{{> partial }} '); - equals(ast.body[1].string, ''); + equals(ast.body[1].value, ''); }); it('marks indented partial as standalone', function() { var ast = Handlebars.parse(' {{> partial }} '); - equals(ast.body[0].string, ''); + equals(ast.body[0].value, ''); equals(ast.body[1].indent, ' '); - equals(ast.body[2].string, ''); + equals(ast.body[2].value, ''); }); it('marks those around content as not standalone', function() { var ast = Handlebars.parse('a{{> partial }}'); @@ -347,12 +338,12 @@ describe('ast', function() { describe('comments', function() { it('marks comment as standalone', function() { var ast = Handlebars.parse('{{! comment }} '); - equals(ast.body[1].string, ''); + equals(ast.body[1].value, ''); }); it('marks indented comment as standalone', function() { var ast = Handlebars.parse(' {{! comment }} '); - equals(ast.body[0].string, ''); - equals(ast.body[2].string, ''); + equals(ast.body[0].value, ''); + equals(ast.body[2].value, ''); }); it('marks those around content as not standalone', function() { var ast = Handlebars.parse('a{{! comment }}'); diff --git a/spec/parser.js b/spec/parser.js index 056922905..f2b6e41e4 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -82,6 +82,8 @@ describe('parser', function() { it('parses a partial', function() { equals(ast_for("{{> foo }}"), "{{> PARTIAL:foo }}\n"); + equals(ast_for("{{> 'foo' }}"), "{{> PARTIAL:foo }}\n"); + equals(ast_for("{{> 1 }}"), "{{> PARTIAL:1 }}\n"); }); it('parses a partial with context', function() { diff --git a/spec/partials.js b/spec/partials.js index a1e05388e..b1509423a 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -23,6 +23,12 @@ describe('partials', function() { shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Empty"); }); + it('partials with duplicate parameters', function() { + shouldThrow(function() { + CompilerContext.compile('Dudes: {{>dude dudes foo bar=baz}}'); + }, Error, 'Unsupported number of partial arguments: 2 - 1:7'); + }); + it("partials with parameters", function() { var string = "Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}"; var partial = "{{others.foo}}{{name}} ({{url}}) "; diff --git a/spec/visitor.js b/spec/visitor.js index b64dc5698..15a0f029b 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -14,16 +14,12 @@ describe('Visitor', function() { // Simply run the thing and make sure it does not fail and that all of the // stub methods are executed var visitor = new Handlebars.Visitor(); - visitor.accept(Handlebars.parse('{{#foo (bar 1 "1" true) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); + visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); }); it('should traverse to stubs', function() { var visitor = new Handlebars.Visitor(); - visitor.PARTIAL_NAME = function(partialName) { - equal(partialName.name, 'bar'); - }; - visitor.STRING = function(string) { equal(string.string, '2'); }; @@ -36,11 +32,11 @@ describe('Visitor', function() { visitor.ID = function(id) { equal(id.original, 'foo.bar'); }; - visitor.content = function(content) { - equal(content.string, ' '); + visitor.ContentStatement = function(content) { + equal(content.value, ' '); }; - visitor.comment = function(comment) { - equal(comment.comment, 'comment'); + visitor.CommentStatement = function(comment) { + equal(comment.value, 'comment'); }; visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}')); diff --git a/src/handlebars.yy b/src/handlebars.yy index d7d117cf1..0bd6fde87 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -78,12 +78,11 @@ mustache ; partial - : OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) - | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), yy.locInfo(@$)) + : OPEN_PARTIAL sexpr CLOSE -> new yy.PartialNode($2, yy.stripFlags($1, $3), yy.locInfo(@$)) ; sexpr - : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, yy.locInfo(@$)) + : helperName param* hash? -> new yy.SexprNode([$1].concat($2), $3, yy.locInfo(@$)) | dataName -> new yy.SexprNode([$1], null, yy.locInfo(@$)) ; @@ -108,10 +107,10 @@ blockParams : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> $2 ; -partialName - : path -> new yy.PartialNameNode($1, yy.locInfo(@$)) - | STRING -> new yy.PartialNameNode(new yy.StringNode($1, yy.locInfo(@$)), yy.locInfo(@$)) - | NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, yy.locInfo(@$))) +helperName + : path -> $1 + | STRING -> new yy.StringNode($1, yy.locInfo(@$)), yy.locInfo(@$) + | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) ; dataName From 697bbe59cad06bc74a945f7e26fc0af333a01d47 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 19:22:09 -0600 Subject: [PATCH 43/99] Update literal ast nodes for new spec --- lib/handlebars/compiler/ast.js | 15 ++++++--------- lib/handlebars/compiler/code-gen.js | 2 +- lib/handlebars/compiler/compiler.js | 14 +++++++------- lib/handlebars/compiler/printer.js | 12 ++++++------ lib/handlebars/compiler/visitor.js | 6 +++--- spec/string-params.js | 25 +++++++++++++------------ spec/visitor.js | 12 ++++++------ 7 files changed, 42 insertions(+), 44 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 26ea4ab5c..44b26ecbe 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -142,25 +142,22 @@ var AST = { StringNode: function(string, locInfo) { this.loc = locInfo; - this.type = "STRING"; + this.type = 'StringLiteral'; this.original = - this.string = - this.stringModeValue = string; + this.value = string; }, NumberNode: function(number, locInfo) { this.loc = locInfo; - this.type = "NUMBER"; + this.type = 'NumberLiteral'; this.original = - this.number = number; - this.stringModeValue = Number(number); + this.value = Number(number); }, BooleanNode: function(bool, locInfo) { this.loc = locInfo; - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "true"; + this.type = 'BooleanLiteral'; + this.value = bool === 'true'; } }; diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index 30f0735e1..a7c16590d 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -99,7 +99,7 @@ CodeGen.prototype = { }, quotedString: function(str) { - return '"' + str + return '"' + (str + '') .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index ea2272a1b..a5acb64c7 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -283,16 +283,16 @@ Compiler.prototype = { this.opcode('lookupData', data, data.id.depth, data.id.parts); }, - STRING: function(string) { - this.opcode('pushString', string, string.string); + StringLiteral: function(string) { + this.opcode('pushString', string, string.value); }, - NUMBER: function(number) { - this.opcode('pushLiteral', number, number.number); + NumberLiteral: function(number) { + this.opcode('pushLiteral', number, number.value); }, - BOOLEAN: function(bool) { - this.opcode('pushLiteral', bool, bool.bool); + BooleanLiteral: function(bool) { + this.opcode('pushLiteral', bool, bool.value); }, // HELPERS @@ -338,7 +338,7 @@ Compiler.prototype = { }, pushParam: function(val) { - var stringModeValue = val.stringModeValue != null ? val.stringModeValue : ''; + var stringModeValue = val.stringModeValue || (val.value != null ? val.value : ''); if (this.stringParams) { if(val.depth) { diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index dac8ec2d9..c86f7fa79 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -120,16 +120,16 @@ PrintVisitor.prototype.hash = function(hash) { return "HASH{" + joinedPairs.join(", ") + "}"; }; -PrintVisitor.prototype.STRING = function(string) { - return '"' + string.string + '"'; +PrintVisitor.prototype.StringLiteral = function(string) { + return '"' + string.value + '"'; }; -PrintVisitor.prototype.NUMBER = function(number) { - return "NUMBER{" + number.number + "}"; +PrintVisitor.prototype.NumberLiteral = function(number) { + return "NUMBER{" + number.value + "}"; }; -PrintVisitor.prototype.BOOLEAN = function(bool) { - return "BOOLEAN{" + bool.bool + "}"; +PrintVisitor.prototype.BooleanLiteral = function(bool) { + return "BOOLEAN{" + bool.value + "}"; }; PrintVisitor.prototype.ID = function(id) { diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index e48c9bcee..a2ad7bb5d 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -57,9 +57,9 @@ Visitor.prototype = { this.accept(data.id); }, - STRING: function(string) {}, - NUMBER: function(number) {}, - BOOLEAN: function(bool) {}, + StringLiteral: function(string) {}, + NumberLiteral: function(number) {}, + BooleanLiteral: function(bool) {}, ID: function(id) {} }; diff --git a/spec/string-params.js b/spec/string-params.js index 2e88cf15c..52ea5a8a2 100644 --- a/spec/string-params.js +++ b/spec/string-params.js @@ -1,3 +1,4 @@ +/*global CompilerContext */ 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}); @@ -56,9 +57,9 @@ describe('string params mode', function() { var helpers = { tomdale: function(desire, noun, trueBool, falseBool, options) { - equal(options.types[0], 'STRING', "the string type is passed"); + equal(options.types[0], 'StringLiteral', "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(options.types[2], 'BooleanLiteral', "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"); @@ -76,21 +77,21 @@ describe('string params mode', function() { 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(exclamation, 'he.says'); + equal(options.types[0], 'ID'); + + equal(options.hashTypes.desire, 'StringLiteral'); + equal(options.hashTypes.noun, 'ID'); + equal(options.hashTypes.bool, 'BooleanLiteral'); + equal(options.hash.desire, 'need'); + equal(options.hash.noun, 'dad.joke'); equal(options.hash.bool, true); - return "Helper called"; + return 'Helper called'; } }; var result = template({}, { helpers: helpers }); - equal(result, "Helper called"); + equal(result, 'Helper called'); }); it("hash parameters get context information", function() { diff --git a/spec/visitor.js b/spec/visitor.js index 15a0f029b..e6f80f349 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -20,14 +20,14 @@ describe('Visitor', function() { it('should traverse to stubs', function() { var visitor = new Handlebars.Visitor(); - visitor.STRING = function(string) { - equal(string.string, '2'); + visitor.StringLiteral = function(string) { + equal(string.value, '2'); }; - visitor.NUMBER = function(number) { - equal(number.stringModeValue, 1); + visitor.NumberLiteral = function(number) { + equal(number.value, 1); }; - visitor.BOOLEAN = function(bool) { - equal(bool.stringModeValue, true); + visitor.BooleanLiteral = function(bool) { + equal(bool.value, true); }; visitor.ID = function(id) { equal(id.original, 'foo.bar'); From 5c921cafebee438fa27d417ae701b24323373a30 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 20:35:33 -0600 Subject: [PATCH 44/99] Replace DataNode and IdNode with PathNode This is a breaking change for string mode users as there is no longer a distinct type for data parameters. Instead data consumers should look for the @ prefix value. --- lib/handlebars/compiler/ast.js | 27 ++---- lib/handlebars/compiler/compiler.js | 39 ++++---- .../compiler/javascript-compiler.js | 2 +- lib/handlebars/compiler/printer.js | 20 ++--- lib/handlebars/compiler/visitor.js | 9 +- spec/ast.js | 18 ++-- spec/parser.js | 88 +++++++++---------- spec/string-params.js | 12 +-- spec/subexpressions.js | 4 +- spec/visitor.js | 4 +- src/handlebars.yy | 4 +- 11 files changed, 103 insertions(+), 124 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 44b26ecbe..287a9e554 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -92,11 +92,11 @@ var AST = { this.pairs = pairs; }, - IdNode: function(parts, locInfo) { + PathNode: function(data, parts, locInfo) { this.loc = locInfo; - this.type = "ID"; + this.type = 'PathExpression'; - var original = "", + var original = '', dig = [], depth = 0, depthString = ''; @@ -105,10 +105,10 @@ var AST = { var part = parts[i].part; original += (parts[i].separator || '') + part; - if (part === ".." || part === "." || part === "this") { + if (part === '..' || part === '.' || part === 'this') { if (dig.length > 0) { - throw new Exception("Invalid path: " + original, this); - } else if (part === "..") { + throw new Exception('Invalid path: ' + original, this); + } else if (part === '..') { depth++; depthString += '../'; } else { @@ -119,25 +119,14 @@ var AST = { } } - this.original = original; + this.data = data; + this.original = (data ? '@' : '') + original; this.parts = dig; - this.string = dig.join('.'); this.depth = depth; - this.idName = depthString + this.string; // an ID is simple if it only has one part, and that part is not // `..` or `this`. this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; - - this.stringModeValue = this.string; - }, - - DataNode: function(id, locInfo) { - this.loc = locInfo; - this.type = "DATA"; - this.id = id; - this.stringModeValue = id.stringModeValue; - this.idName = '@' + id.stringModeValue; }, StringNode: function(string, locInfo) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index a5acb64c7..ea655838f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -212,7 +212,7 @@ Compiler.prototype = { this.opcode('pushProgram', sexpr, program); this.opcode('pushProgram', sexpr, inverse); - this.ID(id); + this.accept(id); this.opcode('invokeAmbiguous', sexpr, name, isBlock); }, @@ -220,10 +220,8 @@ Compiler.prototype = { simpleSexpr: function(sexpr) { var id = sexpr.id; - if (id.type === 'DATA') { - this.DATA(id); - } else if (id.parts.length) { - this.ID(id); + if (id.parts.length) { + this.accept(id); } else { // Simplified ID for `this` this.addDepth(id.depth); @@ -246,7 +244,7 @@ Compiler.prototype = { } else { id.falsy = true; - this.ID(id); + this.accept(id); this.opcode('invokeHelper', sexpr, params.length, id.original, id.isSimple); } }, @@ -265,7 +263,7 @@ Compiler.prototype = { this.opcode('popHash', hash); }, - ID: function(id) { + PathExpression: function(id) { this.addDepth(id.depth); this.opcode('getContext', id, id.depth); @@ -273,16 +271,14 @@ Compiler.prototype = { if (!name) { // Context reference, i.e. `{{foo .}}` or `{{foo ..}}` this.opcode('pushContext', id); + } else if (id.data) { + this.options.data = true; + this.opcode('lookupData', id, id.depth, id.parts); } else { this.opcode('lookupOnContext', id, id.parts, id.falsy, id.isScoped); } }, - DATA: function(data) { - this.options.data = true; - this.opcode('lookupData', data, data.id.depth, data.id.parts); - }, - StringLiteral: function(string) { this.opcode('pushString', string, string.value); }, @@ -338,14 +334,20 @@ Compiler.prototype = { }, pushParam: function(val) { - var stringModeValue = val.stringModeValue || (val.value != null ? val.value : ''); + var value = val.value != null ? val.value : val.original || ''; if (this.stringParams) { + if (value.replace) { + value = value + .replace(/^(\.?\.\/)*/g, '') + .replace(/\//g, '.'); + } + if(val.depth) { this.addDepth(val.depth); } this.opcode('getContext', val, val.depth || 0); - this.opcode('pushStringParam', val, stringModeValue, val.type); + this.opcode('pushStringParam', val, value, val.type); if (val.type === 'sexpr') { // Subexpressions get evaluated and passed in @@ -354,7 +356,14 @@ Compiler.prototype = { } } else { if (this.trackIds) { - this.opcode('pushId', val, val.type, val.idName || stringModeValue); + value = val.original || value; + if (value.replace) { + value = value + .replace(/^\.\//g, '') + .replace(/^\.$/g, ''); + } + + this.opcode('pushId', val, val.type, value); } this.accept(val); } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 639aa4118..2916a3221 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -659,7 +659,7 @@ JavaScriptCompiler.prototype = { }, pushId: function(type, name) { - if (type === 'ID' || type === 'DATA') { + if (type === 'PathExpression') { this.pushString(name); } else if (type === 'sexpr') { this.pushStackLiteral('true'); diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index c86f7fa79..05e8ee7be 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -107,6 +107,12 @@ PrintVisitor.prototype.sexpr = function(sexpr) { return this.accept(sexpr.id) + " " + params + hash; }; +PrintVisitor.prototype.PathExpression = function(id) { + var path = id.parts.join('/'); + return (id.data ? '@' : '') + 'PATH:' + path; +}; + + PrintVisitor.prototype.hash = function(hash) { var pairs = hash.pairs; var joinedPairs = [], left, right; @@ -131,17 +137,3 @@ PrintVisitor.prototype.NumberLiteral = function(number) { PrintVisitor.prototype.BooleanLiteral = function(bool) { return "BOOLEAN{" + bool.value + "}"; }; - -PrintVisitor.prototype.ID = function(id) { - var path = id.parts.join("/"); - if(id.parts.length > 1) { - return "PATH:" + path; - } else { - return "ID:" + path; - } -}; - -PrintVisitor.prototype.DATA = function(data) { - return "@" + this.accept(data.id); -}; - diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index a2ad7bb5d..e191bbdc4 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -45,6 +45,8 @@ Visitor.prototype = { this.accept(sexpr.hash); }, + PathExpression: function(id) {}, + hash: function(hash) { var pairs = hash.pairs; @@ -53,14 +55,9 @@ Visitor.prototype = { } }, - DATA: function(data) { - this.accept(data.id); - }, - StringLiteral: function(string) {}, NumberLiteral: function(number) {}, - BooleanLiteral: function(bool) {}, - ID: function(id) {} + BooleanLiteral: function(bool) {} }; export default Visitor; diff --git a/spec/ast.js b/spec/ast.js index dc6e136ec..064bf6411 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -81,24 +81,24 @@ describe('ast', function() { testLocationInfoStorage(block); }); }); - describe('IdNode', function() { + describe('PathNode', function() { it('should throw on invalid path', function() { shouldThrow(function() { - new handlebarsEnv.AST.IdNode([ + new handlebarsEnv.AST.PathNode(false, [ {part: 'foo'}, {part: '..'}, {part: 'bar'} ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo.. - 1:1"); shouldThrow(function() { - new handlebarsEnv.AST.IdNode([ + new handlebarsEnv.AST.PathNode(false, [ {part: 'foo'}, {part: '.'}, {part: 'bar'} ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo. - 1:1"); shouldThrow(function() { - new handlebarsEnv.AST.IdNode([ + new handlebarsEnv.AST.PathNode(false, [ {part: 'foo'}, {part: 'this'}, {part: 'bar'} @@ -107,7 +107,7 @@ describe('ast', function() { }); it('stores location info', function(){ - var idNode = new handlebarsEnv.AST.IdNode([], LOCATION_INFO); + var idNode = new handlebarsEnv.AST.PathNode(false, [], LOCATION_INFO); testLocationInfoStorage(idNode); }); }); @@ -160,14 +160,6 @@ describe('ast', function() { }); }); - describe("DataNode", function(){ - - it('stores location info', function(){ - var data = new handlebarsEnv.AST.DataNode("YES", LOCATION_INFO); - testLocationInfoStorage(data); - }); - }); - describe("PartialNode", function(){ it('stores location info', function(){ var pn = new handlebarsEnv.AST.PartialNode('so_partial', {}, LOCATION_INFO); diff --git a/spec/parser.js b/spec/parser.js index f2b6e41e4..b25f222f9 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -10,19 +10,19 @@ describe('parser', function() { } it('parses simple mustaches', function() { - equals(ast_for('{{foo}}'), "{{ ID:foo [] }}\n"); - equals(ast_for('{{foo?}}'), "{{ ID:foo? [] }}\n"); - equals(ast_for('{{foo_}}'), "{{ ID:foo_ [] }}\n"); - equals(ast_for('{{foo-}}'), "{{ ID:foo- [] }}\n"); - equals(ast_for('{{foo:}}'), "{{ ID:foo: [] }}\n"); + equals(ast_for('{{foo}}'), "{{ PATH:foo [] }}\n"); + equals(ast_for('{{foo?}}'), "{{ PATH:foo? [] }}\n"); + equals(ast_for('{{foo_}}'), "{{ PATH:foo_ [] }}\n"); + equals(ast_for('{{foo-}}'), "{{ PATH:foo- [] }}\n"); + equals(ast_for('{{foo:}}'), "{{ PATH:foo: [] }}\n"); }); it('parses simple mustaches with data', function() { - equals(ast_for("{{@foo}}"), "{{ @ID:foo [] }}\n"); + equals(ast_for("{{@foo}}"), "{{ @PATH:foo [] }}\n"); }); it('parses simple mustaches with data paths', function() { - equals(ast_for("{{@../foo}}"), "{{ @ID:foo [] }}\n"); + equals(ast_for("{{@../foo}}"), "{{ @PATH:foo [] }}\n"); }); it('parses mustaches with paths', function() { @@ -30,54 +30,54 @@ describe('parser', function() { }); it('parses mustaches with this/foo', function() { - equals(ast_for("{{this/foo}}"), "{{ ID:foo [] }}\n"); + equals(ast_for("{{this/foo}}"), "{{ PATH:foo [] }}\n"); }); it('parses mustaches with - in a path', function() { - equals(ast_for("{{foo-bar}}"), "{{ ID:foo-bar [] }}\n"); + equals(ast_for("{{foo-bar}}"), "{{ PATH:foo-bar [] }}\n"); }); it('parses mustaches with parameters', function() { - equals(ast_for("{{foo bar}}"), "{{ ID:foo [ID:bar] }}\n"); + equals(ast_for("{{foo bar}}"), "{{ PATH:foo [PATH:bar] }}\n"); }); it('parses mustaches with string parameters', function() { - equals(ast_for("{{foo bar \"baz\" }}"), '{{ ID:foo [ID:bar, "baz"] }}\n'); + equals(ast_for("{{foo bar \"baz\" }}"), '{{ PATH:foo [PATH:bar, "baz"] }}\n'); }); it('parses mustaches with NUMBER parameters', function() { - equals(ast_for("{{foo 1}}"), "{{ ID:foo [NUMBER{1}] }}\n"); + equals(ast_for("{{foo 1}}"), "{{ PATH:foo [NUMBER{1}] }}\n"); }); it('parses mustaches with BOOLEAN parameters', function() { - equals(ast_for("{{foo true}}"), "{{ ID:foo [BOOLEAN{true}] }}\n"); - equals(ast_for("{{foo false}}"), "{{ ID:foo [BOOLEAN{false}] }}\n"); + equals(ast_for("{{foo true}}"), "{{ PATH:foo [BOOLEAN{true}] }}\n"); + equals(ast_for("{{foo false}}"), "{{ PATH:foo [BOOLEAN{false}] }}\n"); }); it('parses mutaches with DATA parameters', function() { - equals(ast_for("{{foo @bar}}"), "{{ ID:foo [@ID:bar] }}\n"); + equals(ast_for("{{foo @bar}}"), "{{ PATH:foo [@PATH:bar] }}\n"); }); it('parses mustaches with hash arguments', function() { - equals(ast_for("{{foo bar=baz}}"), "{{ ID:foo [] HASH{bar=ID:baz} }}\n"); - equals(ast_for("{{foo bar=1}}"), "{{ ID:foo [] HASH{bar=NUMBER{1}} }}\n"); - equals(ast_for("{{foo bar=true}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n"); - equals(ast_for("{{foo bar=false}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n"); - equals(ast_for("{{foo bar=@baz}}"), "{{ ID:foo [] HASH{bar=@ID:baz} }}\n"); + equals(ast_for("{{foo bar=baz}}"), "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n"); + equals(ast_for("{{foo bar=1}}"), "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n"); + equals(ast_for("{{foo bar=true}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n"); + equals(ast_for("{{foo bar=false}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n"); + equals(ast_for("{{foo bar=@baz}}"), "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n"); - equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n"); - equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n'); + equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n"); + equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n'); - equals(ast_for("{{foo bat='bam'}}"), '{{ ID:foo [] HASH{bat="bam"} }}\n'); + equals(ast_for("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n'); - equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n'); - equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=NUMBER{1}} }}\n'); - equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n'); - equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n'); + equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n'); + equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n'); + equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n'); + equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n'); }); it('parses contents followed by a mustache', function() { - equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n"); + equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ PATH:baz [] }}\n"); }); it('parses a partial', function() { @@ -87,15 +87,15 @@ describe('parser', function() { }); it('parses a partial with context', function() { - equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo ID:bar }}\n"); + equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo PATH:bar }}\n"); }); it('parses a partial with hash', function() { - equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=ID:bat} }}\n"); + equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n"); }); it('parses a partial with context and hash', function() { - equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo ID:bar HASH{bat=ID:baz} }}\n"); + equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n"); }); it('parses a partial with a complex name', function() { @@ -111,47 +111,47 @@ describe('parser', function() { }); it('parses an inverse section', function() { - equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); + equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); it('parses an inverse (else-style) section', function() { - equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); + equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); it('parses multiple inverse sections', function() { - equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n ID:if [ID:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"); + equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); it('parses empty blocks', function() { - equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n"); + equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n"); }); it('parses empty blocks with empty inverse section', function() { - equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n"); + equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"); }); it('parses empty blocks with empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n"); + equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"); }); it('parses non-empty blocks with empty inverse section', function() { - equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); + equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); }); it('parses non-empty blocks with empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); + equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); }); it('parses empty blocks with non-empty inverse section', function() { - equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); + equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); }); it('parses empty blocks with non-empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); + equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); }); it('parses a standalone inverse section', function() { - equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n ID:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"); + equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"); }); it('throws on old inverse section', function() { shouldThrow(function() { @@ -160,11 +160,11 @@ describe('parser', function() { }); it('parses block with block params', function() { - equals(ast_for("{{#foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); + equals(ast_for("{{#foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); }); it('parses inverse block with block params', function() { - equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n ID:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); + equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); }); it("raises if there's a Parse error", function() { diff --git a/spec/string-params.js b/spec/string-params.js index 52ea5a8a2..4704a84f4 100644 --- a/spec/string-params.js +++ b/spec/string-params.js @@ -58,7 +58,7 @@ describe('string params mode', function() { var helpers = { tomdale: function(desire, noun, trueBool, falseBool, options) { equal(options.types[0], 'StringLiteral', "the string type is passed"); - equal(options.types[1], 'ID', "the expression type is passed"); + equal(options.types[1], 'PathExpression', "the expression type is passed"); equal(options.types[2], 'BooleanLiteral', "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"); @@ -78,10 +78,10 @@ describe('string params mode', function() { var helpers = { tomdale: function(exclamation, options) { equal(exclamation, 'he.says'); - equal(options.types[0], 'ID'); + equal(options.types[0], 'PathExpression'); equal(options.hashTypes.desire, 'StringLiteral'); - equal(options.hashTypes.noun, 'ID'); + equal(options.hashTypes.noun, 'PathExpression'); equal(options.hashTypes.bool, 'BooleanLiteral'); equal(options.hash.desire, 'need'); equal(options.hash.noun, 'dad.joke'); @@ -102,7 +102,7 @@ describe('string params mode', function() { var helpers = { tomdale: function(exclamation, options) { equal(exclamation, "he.says"); - equal(options.types[0], "ID"); + equal(options.types[0], 'PathExpression'); equal(options.contexts.length, 1); equal(options.hashContexts.noun, context); @@ -165,8 +165,8 @@ describe('string params mode', function() { var helpers = { foo: function(bar, options) { - equal(bar, 'bar'); - equal(options.types[0], 'DATA'); + equal(bar, '@bar'); + equal(options.types[0], 'PathExpression'); return 'Foo!'; } }; diff --git a/spec/subexpressions.js b/spec/subexpressions.js index 5c9fdfc30..d892bb513 100644 --- a/spec/subexpressions.js +++ b/spec/subexpressions.js @@ -171,13 +171,13 @@ describe('subexpressions', function() { equals(a, 'foo'); equals(options.types.length, 2, "string params for outer helper processed correctly"); equals(options.types[0], 'sexpr', "string params for outer helper processed correctly"); - equals(options.types[1], 'ID', "string params for outer helper processed correctly"); + equals(options.types[1], 'PathExpression', "string params for outer helper processed correctly"); return a + b; }, blorg: function(a, options) { equals(options.types.length, 1, "string params for inner helper processed correctly"); - equals(options.types[0], 'ID', "string params for inner helper processed correctly"); + equals(options.types[0], 'PathExpression', "string params for inner helper processed correctly"); return a; } }; diff --git a/spec/visitor.js b/spec/visitor.js index e6f80f349..66c3b688f 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -29,8 +29,8 @@ describe('Visitor', function() { visitor.BooleanLiteral = function(bool) { equal(bool.value, true); }; - visitor.ID = function(id) { - equal(id.original, 'foo.bar'); + visitor.PathExpression = function(id) { + equal(/foo\.bar$/.test(id.original), true); }; visitor.ContentStatement = function(content) { equal(content.value, ' '); diff --git a/src/handlebars.yy b/src/handlebars.yy index 0bd6fde87..fb0837af5 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -114,11 +114,11 @@ helperName ; dataName - : DATA path -> new yy.DataNode($2, yy.locInfo(@$)) + : DATA pathSegments -> new yy.PathNode(true, $2, yy.locInfo(@$)) ; path - : pathSegments -> new yy.IdNode($1, yy.locInfo(@$)) + : pathSegments -> new yy.PathNode(false, $1, yy.locInfo(@$)) ; pathSegments From e1cba432a68fe208782f8f943ebd57d7641d705a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 27 Nov 2014 07:32:15 -0600 Subject: [PATCH 45/99] Simplify Path and Sexpr calculated flags --- lib/handlebars/compiler/ast.js | 23 ++--------------- lib/handlebars/compiler/compiler.js | 40 ++++++++++++++++++++++++++--- src/handlebars.yy | 2 +- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 287a9e554..75300745e 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -69,21 +69,8 @@ var AST = { this.type = "sexpr"; this.hash = hash; - var id = this.id = rawParams[0]; - var params = this.params = rawParams.slice(1); - - // a mustache is definitely a helper if: - // * it is an eligible helper, and - // * it has at least one parameter or hash segment - this.isHelper = !!(params.length || hash); - - // a mustache is an eligible helper if: - // * its id is simple (a single part, not `this` or `..`) - this.eligibleHelper = this.isHelper || id.isSimple; - - // if a mustache is an eligible helper but not a definite - // helper, it is ambiguous, and will be resolved in a later - // pass or at runtime. + this.id = rawParams[0]; + this.params = rawParams.slice(1); }, HashNode: function(pairs, locInfo) { @@ -111,8 +98,6 @@ var AST = { } else if (part === '..') { depth++; depthString += '../'; - } else { - this.isScoped = true; } } else { dig.push(part); @@ -123,10 +108,6 @@ var AST = { this.original = (data ? '@' : '') + original; this.parts = dig; this.depth = depth; - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; }, StringNode: function(string, locInfo) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index ea655838f..5dbbfc44b 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -3,6 +3,26 @@ import {isArray} from "../utils"; var slice = [].slice; + +// a mustache is definitely a helper if: +// * it is an eligible helper, and +// * it has at least one parameter or hash segment +function helperExpr(sexpr) { + return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash); +} + +function scopedId(id) { + return (/^\.|this\b/).test(id.original); +} + +// an ID is simple if it only has one part, and that part is not +// `..` or `this`. +function simpleId(id) { + var part = id.parts[0]; + + return id.parts.length === 1 && !scopedId(id) && !id.depth; +} + export function Compiler() {} // the foundHelper register will disambiguate helper lookup from finding a @@ -245,7 +265,7 @@ Compiler.prototype = { id.falsy = true; this.accept(id); - this.opcode('invokeHelper', sexpr, params.length, id.original, id.isSimple); + this.opcode('invokeHelper', sexpr, params.length, id.original, simpleId(id)); } }, @@ -275,7 +295,7 @@ Compiler.prototype = { this.options.data = true; this.opcode('lookupData', id, id.depth, id.parts); } else { - this.opcode('lookupOnContext', id, id.parts, id.falsy, id.isScoped); + this.opcode('lookupOnContext', id, id.parts, id.falsy, scopedId(id)); } }, @@ -306,8 +326,15 @@ Compiler.prototype = { }, classifySexpr: function(sexpr) { - var isHelper = sexpr.isHelper; - var isEligible = sexpr.eligibleHelper; + // a mustache is an eligible helper if: + // * its id is simple (a single part, not `this` or `..`) + var isHelper = helperExpr(sexpr); + + // if a mustache is an eligible helper but not a definite + // helper, it is ambiguous, and will be resolved in a later + // pass or at runtime. + var isEligible = isHelper || simpleId(sexpr.id); + var options = this.options; // if ambiguous, we can possibly resolve the ambiguity now @@ -336,6 +363,11 @@ Compiler.prototype = { pushParam: function(val) { var value = val.value != null ? val.value : val.original || ''; + // Force helper evaluation + if (val.type === 'sexpr') { + val.isHelper = true; + } + if (this.stringParams) { if (value.replace) { value = value diff --git a/src/handlebars.yy b/src/handlebars.yy index fb0837af5..b51aad2d7 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -92,7 +92,7 @@ param | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) | BOOLEAN -> new yy.BooleanNode($1, yy.locInfo(@$)) | dataName -> $1 - | OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;} + | OPEN_SEXPR sexpr CLOSE_SEXPR -> $2 ; hash From 1124908d2a0d36493912eb2ce062545b1b5e5445 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 27 Nov 2014 08:37:48 -0600 Subject: [PATCH 46/99] Update subexpression and hash AST constructs --- lib/handlebars/compiler/ast.js | 25 ++-- lib/handlebars/compiler/compiler.js | 118 ++++++++---------- lib/handlebars/compiler/helpers.js | 8 +- .../compiler/javascript-compiler.js | 4 +- lib/handlebars/compiler/printer.js | 33 ++--- lib/handlebars/compiler/visitor.js | 21 ++-- spec/ast.js | 2 +- spec/subexpressions.js | 10 +- src/handlebars.yy | 2 +- 9 files changed, 111 insertions(+), 112 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 75300745e..243957143 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -66,17 +66,10 @@ var AST = { SexprNode: function(rawParams, hash, locInfo) { this.loc = locInfo; - this.type = "sexpr"; - this.hash = hash; - - this.id = rawParams[0]; + this.type = 'SubExpression'; + this.path = rawParams[0]; this.params = rawParams.slice(1); - }, - - HashNode: function(pairs, locInfo) { - this.loc = locInfo; - this.type = "hash"; - this.pairs = pairs; + this.hash = hash; }, PathNode: function(data, parts, locInfo) { @@ -128,6 +121,18 @@ var AST = { this.loc = locInfo; this.type = 'BooleanLiteral'; this.value = bool === 'true'; + }, + + HashNode: function(pairs, locInfo) { + this.loc = locInfo; + this.type = 'Hash'; + this.pairs = pairs; + }, + HashPair: function(key, value, locInfo) { + this.loc = locInfo; + this.type = 'HashPair'; + this.key = key; + this.value = value; } }; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 5dbbfc44b..e73ce37b4 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -11,16 +11,16 @@ function helperExpr(sexpr) { return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash); } -function scopedId(id) { - return (/^\.|this\b/).test(id.original); +function scopedId(path) { + return (/^\.|this\b/).test(path.original); } // an ID is simple if it only has one part, and that part is not // `..` or `this`. -function simpleId(id) { - var part = id.parts[0]; +function simpleId(path) { + var part = path.parts[0]; - return id.parts.length === 1 && !scopedId(id) && !id.depth; + return path.parts.length === 1 && !scopedId(path) && !path.depth; } export function Compiler() {} @@ -147,7 +147,7 @@ Compiler.prototype = { this.opcode('pushProgram', block, program); this.opcode('pushProgram', block, inverse); this.opcode('emptyHash', block); - this.opcode('blockValue', block, sexpr.id.original); + this.opcode('blockValue', block, sexpr.path.original); } else { this.ambiguousSexpr(sexpr, program, inverse); @@ -163,7 +163,7 @@ Compiler.prototype = { }, PartialStatement: function(partial) { - var partialName = partial.sexpr.id.original; + var partialName = partial.sexpr.path.original; this.usePartial = true; if (partial.sexpr.hash) { @@ -194,7 +194,7 @@ Compiler.prototype = { }, MustacheStatement: function(mustache) { - this.sexpr(mustache.sexpr); + this.accept(mustache.sexpr); if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped', mustache); @@ -211,91 +211,67 @@ Compiler.prototype = { CommentStatement: function() {}, - sexpr: function(sexpr) { + SubExpression: function(sexpr) { var type = this.classifySexpr(sexpr); - if (type === "simple") { + if (type === 'simple') { this.simpleSexpr(sexpr); - } else if (type === "helper") { + } else if (type === 'helper') { this.helperSexpr(sexpr); } else { this.ambiguousSexpr(sexpr); } }, ambiguousSexpr: function(sexpr, program, inverse) { - var id = sexpr.id, - name = id.parts[0], + var path = sexpr.path, + name = path.parts[0], isBlock = program != null || inverse != null; - this.opcode('getContext', sexpr, id.depth); + this.opcode('getContext', sexpr, path.depth); this.opcode('pushProgram', sexpr, program); this.opcode('pushProgram', sexpr, inverse); - this.accept(id); + this.accept(path); this.opcode('invokeAmbiguous', sexpr, name, isBlock); }, simpleSexpr: function(sexpr) { - var id = sexpr.id; - - if (id.parts.length) { - this.accept(id); - } else { - // Simplified ID for `this` - this.addDepth(id.depth); - this.opcode('getContext', sexpr, id.depth); - this.opcode('pushContext', sexpr); - } - + this.accept(sexpr.path); this.opcode('resolvePossibleLambda', sexpr); }, helperSexpr: function(sexpr, program, inverse) { var params = this.setupFullMustacheParams(sexpr, program, inverse), - id = sexpr.id, - name = id.parts[0]; + path = sexpr.path, + name = path.parts[0]; if (this.options.knownHelpers[name]) { this.opcode('invokeKnownHelper', sexpr, params.length, name); } else if (this.options.knownHelpersOnly) { throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr); } else { - id.falsy = true; - - this.accept(id); - this.opcode('invokeHelper', sexpr, params.length, id.original, simpleId(id)); - } - }, - - hash: function(hash) { - var pairs = hash.pairs, i, l; - - this.opcode('pushHash', hash); + path.falsy = true; - for(i=0, l=pairs.length; i'); }); @@ -159,7 +159,7 @@ describe('subexpressions', function() { t: function(defaultString) { return new Handlebars.SafeString(defaultString); } - } + }; shouldCompileTo(string, [context, helpers], ''); }); @@ -170,7 +170,7 @@ describe('subexpressions', function() { snog: function(a, b, options) { equals(a, 'foo'); equals(options.types.length, 2, "string params for outer helper processed correctly"); - equals(options.types[0], 'sexpr', "string params for outer helper processed correctly"); + equals(options.types[0], 'SubExpression', "string params for outer helper processed correctly"); equals(options.types[1], 'PathExpression', "string params for outer helper processed correctly"); return a + b; }, @@ -196,7 +196,7 @@ describe('subexpressions', function() { var helpers = { blog: function(options) { - equals(options.hashTypes.fun, 'sexpr'); + equals(options.hashTypes.fun, 'SubExpression'); return "val is " + options.hash.fun; }, bork: function() { diff --git a/src/handlebars.yy b/src/handlebars.yy index b51aad2d7..1975f63af 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -100,7 +100,7 @@ hash ; hashSegment - : ID EQUALS param -> [$1, $3] + : ID EQUALS param -> new yy.HashPair($1, $3, yy.locInfo(@$)) ; blockParams From f990cf006422fbcbbae7d8a8a866831e58300ea4 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 27 Nov 2014 09:11:03 -0600 Subject: [PATCH 47/99] Treat partial exec in a manner closer to helpers This helps unify the code handling and will also be needed to support string/id tracking on partials. --- lib/handlebars/compiler/code-gen.js | 5 ++- lib/handlebars/compiler/compiler.js | 25 ++++------- .../compiler/javascript-compiler.js | 41 +++++++++++-------- lib/handlebars/runtime.js | 27 ++++++------ 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index a7c16590d..0fddb7c65 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -113,7 +113,10 @@ CodeGen.prototype = { for (var key in obj) { if (obj.hasOwnProperty(key)) { - pairs.push([this.quotedString(key), ':', castChunk(obj[key], this)]); + var value = castChunk(obj[key], this); + if (value !== 'undefined') { + pairs.push([this.quotedString(key), ':', value]); + } } } diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index e73ce37b4..df5a10f4f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -166,24 +166,15 @@ Compiler.prototype = { var partialName = partial.sexpr.path.original; this.usePartial = true; - if (partial.sexpr.hash) { - this.accept(partial.sexpr.hash); - } else { - this.opcode('pushLiteral', partial, 'undefined'); - } - var params = partial.sexpr.params; - if (params.length) { - if (params.length > 1) { - throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); - } - - this.pushParam(params[0]); - } else { - this.opcode('getContext', partial, 0); - this.opcode('pushContext', partial); + if (params.length > 1) { + throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); + } else if (!params.length) { + params.push({type: 'PathExpression', parts: [], depth: 0}); } + this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true); + var indent = partial.indent || ''; if (this.options.preventIndent && indent) { this.opcode('appendContent', partial, indent); @@ -391,7 +382,7 @@ Compiler.prototype = { } }, - setupFullMustacheParams: function(sexpr, program, inverse) { + setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) { var params = sexpr.params; this.pushParams(params); @@ -401,7 +392,7 @@ Compiler.prototype = { if (sexpr.hash) { this.accept(sexpr.hash); } else { - this.opcode('emptyHash', sexpr); + this.opcode('emptyHash', sexpr, omitEmpty); } return params; diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index cde2ab507..db1977811 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -275,7 +275,7 @@ JavaScriptCompiler.prototype = { blockValue: function(name) { var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), params = [this.contextName(0)]; - this.setupParams(name, 0, params); + this.setupHelperArgs(name, 0, params); var blockName = this.popStack(); params.splice(1, 0, blockName); @@ -293,7 +293,7 @@ JavaScriptCompiler.prototype = { // We're being a bit cheeky and reusing the options value from the prior exec var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), params = [this.contextName(0)]; - this.setupParams('', 0, params, true); + this.setupHelperArgs('', 0, params, true); this.flushInline(); @@ -469,9 +469,7 @@ JavaScriptCompiler.prototype = { } }, - emptyHash: function() { - this.pushStackLiteral('{}'); - + emptyHash: function(omitEmpty) { if (this.trackIds) { this.push('{}'); // hashIds } @@ -479,6 +477,7 @@ JavaScriptCompiler.prototype = { this.push('{}'); // hashContexts this.push('{}'); // hashTypes } + this.pushStackLiteral(omitEmpty ? 'undefined' : '{}'); }, pushHash: function() { if (this.hash) { @@ -611,16 +610,22 @@ JavaScriptCompiler.prototype = { // This operation pops off a context, invokes a partial with that context, // and pushes the result of the invocation back. invokePartial: function(name, indent) { - var params = [this.nameLookup('partials', name, 'partial'), "'" + indent + "'", "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"]; + var params = [], + options = this.setupParams(name, 1, params, false); - if (this.options.data) { - params.push("data"); - } else if (this.options.compat) { - params.push('undefined'); + if (indent) { + options.indent = JSON.stringify(indent); } + options.helpers = 'helpers'; + options.partials = 'partials'; + + params.unshift(this.nameLookup('partials', name, 'partial')); + if (this.options.compat) { - params.push('depths'); + options.depths = 'depths'; } + options = this.objectLiteral(options); + params.push(options); this.push(this.source.functionCall('this.invokePartial', '', params)); }, @@ -881,7 +886,7 @@ JavaScriptCompiler.prototype = { setupHelper: function(paramSize, name, blockHelper) { var params = [], - paramsInit = this.setupParams(name, paramSize, params, blockHelper); + paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper); var foundHelper = this.nameLookup('helpers', name, 'helper'); return { @@ -892,8 +897,8 @@ JavaScriptCompiler.prototype = { }; }, - setupParams: function(helper, paramSize, params, useRegister) { - var options = {}, contexts = [], types = [], ids = [], param, inverse, program; + setupParams: function(helper, paramSize, params) { + var options = {}, contexts = [], types = [], ids = [], param; options.name = this.quotedString(helper); options.hash = this.popStack(); @@ -906,8 +911,8 @@ JavaScriptCompiler.prototype = { options.hashContexts = this.popStack(); } - inverse = this.popStack(); - program = this.popStack(); + var inverse = this.popStack(), + program = this.popStack(); // Avoid setting fn and inverse if neither are set. This allows // helpers to do a check for `if (options.fn)` @@ -943,7 +948,11 @@ JavaScriptCompiler.prototype = { if (this.options.data) { options.data = "data"; } + return options; + }, + setupHelperArgs: function(helper, paramSize, params, useRegister) { + var options = this.setupParams(helper, paramSize, params, true); options = this.objectLiteral(options); if (useRegister) { this.useRegister('options'); diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 05759cba9..455dd332a 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -35,36 +35,35 @@ export function template(templateSpec, env) { // for external users to override these as psuedo-supported APIs. env.VM.checkRevision(templateSpec.compiler); - var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) { - if (hash) { - context = Utils.extend({}, context, hash); + var invokePartialWrapper = function(partial, context, options) { + if (options.hash) { + context = Utils.extend({}, context, options.hash); } if (!partial) { - partial = partials[name]; + partial = options.partials[options.name]; } - var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths); + var result = env.VM.invokePartial.call(this, partial, context, options); if (result == null && env.compile) { - var options = { helpers: helpers, partials: partials, data: data, depths: depths }; - partials[name] = env.compile(partial, templateSpec.compilerOptions, env); - result = partials[name](context, options); + options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env); + result = options.partials[options.name](context, options); } if (result != null) { - if (indent) { + if (options.indent) { var lines = result.split('\n'); for (var i = 0, l = lines.length; i < l; i++) { if (!lines[i] && i + 1 === l) { break; } - lines[i] = indent + lines[i]; + lines[i] = options.indent + lines[i]; } result = lines.join('\n'); } return result; } else { - throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode"); + throw new Exception("The partial " + options.name + " could not be compiled when running in runtime-only mode"); } }; @@ -172,11 +171,11 @@ export function program(container, i, fn, data, depths) { return prog; } -export function invokePartial(partial, name, context, helpers, partials, data, depths) { - var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths }; +export function invokePartial(partial, context, options) { + options.partial = true; if(partial === undefined) { - throw new Exception("The partial " + name + " could not be found"); + throw new Exception("The partial " + options.name + " could not be found"); } else if(partial instanceof Function) { return partial(context, options); } From 6a7a8c803d48b8a5768d00e8d1c73cf5012106d2 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 14:42:24 -0600 Subject: [PATCH 48/99] Rename AST objects to match type names --- lib/handlebars/compiler/ast.js | 24 +++++----- lib/handlebars/compiler/helpers.js | 8 ++-- spec/ast.js | 70 ++++++++++++++---------------- spec/compiler.js | 4 +- spec/parser.js | 2 +- src/handlebars.yy | 36 +++++++-------- 6 files changed, 69 insertions(+), 75 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 243957143..69581f281 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,7 +1,7 @@ import Exception from "../exception"; var AST = { - ProgramNode: function(statements, blockParams, strip, locInfo) { + Program: function(statements, blockParams, strip, locInfo) { this.loc = locInfo; this.type = 'Program'; this.body = statements; @@ -10,7 +10,7 @@ var AST = { this.strip = strip; }, - MustacheNode: function(rawParams, open, strip, locInfo) { + MustacheStatement: function(rawParams, open, strip, locInfo) { this.loc = locInfo; this.type = 'MustacheStatement'; @@ -28,7 +28,7 @@ var AST = { this.strip = strip; }, - BlockNode: function(sexpr, program, inverse, strip, locInfo) { + BlockStatement: function(sexpr, program, inverse, strip, locInfo) { this.loc = locInfo; this.type = 'BlockStatement'; @@ -38,7 +38,7 @@ var AST = { this.strip = strip; }, - PartialNode: function(sexpr, strip, locInfo) { + PartialStatement: function(sexpr, strip, locInfo) { this.loc = locInfo; this.type = 'PartialStatement'; this.sexpr = sexpr; @@ -48,13 +48,13 @@ var AST = { this.strip.inlineStandalone = true; }, - ContentNode: function(string, locInfo) { + ContentStatement: function(string, locInfo) { this.loc = locInfo; this.type = 'ContentStatement'; this.original = this.value = string; }, - CommentNode: function(comment, strip, locInfo) { + CommentStatement: function(comment, strip, locInfo) { this.loc = locInfo; this.type = 'CommentStatement'; this.value = comment; @@ -63,7 +63,7 @@ var AST = { strip.inlineStandalone = true; }, - SexprNode: function(rawParams, hash, locInfo) { + SubExpression: function(rawParams, hash, locInfo) { this.loc = locInfo; this.type = 'SubExpression'; @@ -72,7 +72,7 @@ var AST = { this.hash = hash; }, - PathNode: function(data, parts, locInfo) { + PathExpression: function(data, parts, locInfo) { this.loc = locInfo; this.type = 'PathExpression'; @@ -103,27 +103,27 @@ var AST = { this.depth = depth; }, - StringNode: function(string, locInfo) { + StringLiteral: function(string, locInfo) { this.loc = locInfo; this.type = 'StringLiteral'; this.original = this.value = string; }, - NumberNode: function(number, locInfo) { + NumberLiteral: function(number, locInfo) { this.loc = locInfo; this.type = 'NumberLiteral'; this.original = this.value = Number(number); }, - BooleanNode: function(bool, locInfo) { + BooleanLiteral: function(bool, locInfo) { this.loc = locInfo; this.type = 'BooleanLiteral'; this.value = bool === 'true'; }, - HashNode: function(pairs, locInfo) { + Hash: function(pairs, locInfo) { this.loc = locInfo; this.type = 'Hash'; this.pairs = pairs; diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index f75c10b8c..3d5144f70 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -32,9 +32,9 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode); } - var program = new this.ProgramNode([content], null, {}, locInfo); + var program = new this.Program([content], null, {}, locInfo); - return new this.BlockNode(openRawBlock.sexpr, program, undefined, undefined, locInfo); + return new this.BlockStatement(openRawBlock.sexpr, program, undefined, undefined, locInfo); } export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { @@ -109,9 +109,9 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver } if (inverted) { - return new this.BlockNode(openBlock.sexpr, inverse, program, strip, locInfo); + return new this.BlockStatement(openBlock.sexpr, inverse, program, strip, locInfo); } else { - return new this.BlockNode(openBlock.sexpr, program, inverse, strip, locInfo); + return new this.BlockStatement(openBlock.sexpr, program, inverse, strip, locInfo); } } diff --git a/spec/ast.js b/spec/ast.js index 0efe96342..53267493d 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -22,16 +22,16 @@ describe('ast', function() { equals(node.loc.end.column, 1); } - describe('MustacheNode', function() { + describe('MustacheStatement', function() { function testEscape(open, expected) { - var mustache = new handlebarsEnv.AST.MustacheNode([{}], open, false); + var mustache = new handlebarsEnv.AST.MustacheStatement([{}], open, false); equals(mustache.escaped, expected); } it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], '', false, LOCATION_INFO); + mustache = new handlebarsEnv.AST.MustacheStatement([id, 'param1'], '', false, LOCATION_INFO); equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); testLocationInfoStorage(mustache); @@ -61,7 +61,7 @@ describe('ast', function() { testEscape(undefined, false); }); }); - describe('BlockNode', function() { + describe('BlockStatement', function() { it('should throw on mustache mismatch', function() { shouldThrow(function() { handlebarsEnv.parse("\n {{#foo}}{{/bar}}"); @@ -69,9 +69,9 @@ describe('ast', function() { }); it('stores location info', function(){ - var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null); - var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {}); - var block = new handlebarsEnv.AST.BlockNode(mustacheNode, + var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null); + var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, null, '{{', {}); + var block = new handlebarsEnv.AST.BlockStatement(mustacheNode, {body: [], strip: {}}, {body: [], strip: {}}, { strip: {}, @@ -81,24 +81,24 @@ describe('ast', function() { testLocationInfoStorage(block); }); }); - describe('PathNode', function() { + describe('PathExpression', function() { it('should throw on invalid path', function() { shouldThrow(function() { - new handlebarsEnv.AST.PathNode(false, [ + new handlebarsEnv.AST.PathExpression(false, [ {part: 'foo'}, {part: '..'}, {part: 'bar'} ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo.. - 1:1"); shouldThrow(function() { - new handlebarsEnv.AST.PathNode(false, [ + new handlebarsEnv.AST.PathExpression(false, [ {part: 'foo'}, {part: '.'}, {part: 'bar'} ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo. - 1:1"); shouldThrow(function() { - new handlebarsEnv.AST.PathNode(false, [ + new handlebarsEnv.AST.PathExpression(false, [ {part: 'foo'}, {part: 'this'}, {part: 'bar'} @@ -107,69 +107,63 @@ describe('ast', function() { }); it('stores location info', function(){ - var idNode = new handlebarsEnv.AST.PathNode(false, [], LOCATION_INFO); + var idNode = new handlebarsEnv.AST.PathExpression(false, [], LOCATION_INFO); testLocationInfoStorage(idNode); }); }); - describe("HashNode", function(){ - + describe('Hash', function(){ it('stores location info', function(){ - var hash = new handlebarsEnv.AST.HashNode([], LOCATION_INFO); + var hash = new handlebarsEnv.AST.Hash([], LOCATION_INFO); testLocationInfoStorage(hash); }); }); - describe("ContentNode", function(){ - + describe('ContentStatement', function(){ it('stores location info', function(){ - var content = new handlebarsEnv.AST.ContentNode("HI", LOCATION_INFO); + var content = new handlebarsEnv.AST.ContentStatement("HI", LOCATION_INFO); testLocationInfoStorage(content); }); }); - describe("CommentNode", function(){ - + describe('CommentStatement', function(){ it('stores location info', function(){ - var comment = new handlebarsEnv.AST.CommentNode("HI", {}, LOCATION_INFO); + var comment = new handlebarsEnv.AST.CommentStatement("HI", {}, LOCATION_INFO); testLocationInfoStorage(comment); }); }); - describe("NumberNode", function(){ - + describe('NumberLiteral', function(){ it('stores location info', function(){ - var integer = new handlebarsEnv.AST.NumberNode("6", LOCATION_INFO); + var integer = new handlebarsEnv.AST.NumberLiteral("6", LOCATION_INFO); testLocationInfoStorage(integer); }); }); - describe("StringNode", function(){ - + describe('StringLiteral', function(){ it('stores location info', function(){ - var string = new handlebarsEnv.AST.StringNode("6", LOCATION_INFO); + var string = new handlebarsEnv.AST.StringLiteral("6", LOCATION_INFO); testLocationInfoStorage(string); }); }); - describe("BooleanNode", function(){ - + describe('BooleanLiteral', function(){ it('stores location info', function(){ - var bool = new handlebarsEnv.AST.BooleanNode("true", LOCATION_INFO); + var bool = new handlebarsEnv.AST.BooleanLiteral("true", LOCATION_INFO); testLocationInfoStorage(bool); }); }); - describe("PartialNode", function(){ + describe('PartialStatement', function(){ it('stores location info', function(){ - var pn = new handlebarsEnv.AST.PartialNode('so_partial', {}, LOCATION_INFO); + var pn = new handlebarsEnv.AST.PartialStatement('so_partial', {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); - describe('ProgramNode', function(){ + describe('Program', function(){ it('storing location info', function(){ - var pn = new handlebarsEnv.AST.ProgramNode([], null, {}, LOCATION_INFO); + var pn = new handlebarsEnv.AST.Program([], null, {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); @@ -193,7 +187,7 @@ describe('ast', function() { testColumns(contentNode, 1, 1, 0, 7); }); - it('gets MustacheNode line numbers', function(){ + it('gets MustacheStatement line numbers', function(){ var mustacheNode = body[1]; testColumns(mustacheNode, 1, 1, 7, 21); }); @@ -202,9 +196,9 @@ describe('ast', function() { testColumns(body[2], 1, 2, 21, 8); }); - it('gets MustacheNode line numbers correct across newlines', function(){ - var secondMustacheNode = body[3]; - testColumns(secondMustacheNode, 2, 2, 8, 22); + it('gets MustacheStatement line numbers correct across newlines', function(){ + var secondMustacheStatement = body[3]; + testColumns(secondMustacheStatement, 2, 2, 8, 22); }); it('gets the block helper information correct', function(){ diff --git a/spec/compiler.js b/spec/compiler.js index eead00b58..f9eba28fb 100644 --- a/spec/compiler.js +++ b/spec/compiler.js @@ -41,7 +41,7 @@ describe('compiler', function() { }); it('can utilize AST instance', function() { - equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")], null, {}))(), 'Hello'); + equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")], null, {}))(), 'Hello'); }); it("can pass through an empty string", function() { @@ -60,7 +60,7 @@ describe('compiler', function() { }); it('can utilize AST instance', function() { - equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]), null, {})), true); + equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")]), null, {})), true); }); it("can pass through an empty string", function() { diff --git a/spec/parser.js b/spec/parser.js index b25f222f9..ad5273446 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -203,7 +203,7 @@ describe('parser', function() { describe('externally compiled AST', function() { it('can pass through an already-compiled AST', function() { - equals(ast_for(new Handlebars.AST.ProgramNode([new Handlebars.AST.ContentNode("Hello")], null)), "CONTENT[ \'Hello\' ]\n"); + equals(ast_for(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement("Hello")], null)), "CONTENT[ \'Hello\' ]\n"); }); }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index 1975f63af..8b63df045 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -9,7 +9,7 @@ root ; program - : statement* -> new yy.ProgramNode(yy.prepareProgram($1), null, {}, yy.locInfo(@$)) + : statement* -> new yy.Program(yy.prepareProgram($1), null, {}, yy.locInfo(@$)) ; statement @@ -18,11 +18,11 @@ statement | rawBlock -> $1 | partial -> $1 | content -> $1 - | COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) + | COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) ; content - : CONTENT -> new yy.ContentNode($1, yy.locInfo(@$)) + : CONTENT -> new yy.ContentStatement($1, yy.locInfo(@$)) ; rawBlock @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; inverseAndProgram @@ -57,7 +57,7 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { var inverse = yy.prepareBlock($1, $2, $3, $3, false, yy.locInfo(@$)), - program = new yy.ProgramNode(yy.prepareProgram([inverse]), null, {}, yy.locInfo(@$)); + program = new yy.Program(yy.prepareProgram([inverse]), null, {}, yy.locInfo(@$)); program.inverse = inverse; @@ -73,30 +73,30 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN sexpr CLOSE -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; partial - : OPEN_PARTIAL sexpr CLOSE -> new yy.PartialNode($2, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_PARTIAL sexpr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$)) ; sexpr - : helperName param* hash? -> new yy.SexprNode([$1].concat($2), $3, yy.locInfo(@$)) - | dataName -> new yy.SexprNode([$1], null, yy.locInfo(@$)) + : helperName param* hash? -> new yy.SubExpression([$1].concat($2), $3, yy.locInfo(@$)) + | dataName -> new yy.SubExpression([$1], null, yy.locInfo(@$)) ; param : path -> $1 - | STRING -> new yy.StringNode($1, yy.locInfo(@$)) - | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) - | BOOLEAN -> new yy.BooleanNode($1, yy.locInfo(@$)) + | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)) + | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) + | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) | dataName -> $1 | OPEN_SEXPR sexpr CLOSE_SEXPR -> $2 ; hash - : hashSegment+ -> new yy.HashNode($1, yy.locInfo(@$)) + : hashSegment+ -> new yy.Hash($1, yy.locInfo(@$)) ; hashSegment @@ -109,16 +109,16 @@ blockParams helperName : path -> $1 - | STRING -> new yy.StringNode($1, yy.locInfo(@$)), yy.locInfo(@$) - | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) + | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)), yy.locInfo(@$) + | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) ; dataName - : DATA pathSegments -> new yy.PathNode(true, $2, yy.locInfo(@$)) + : DATA pathSegments -> new yy.PathExpression(true, $2, yy.locInfo(@$)) ; path - : pathSegments -> new yy.PathNode(false, $1, yy.locInfo(@$)) + : pathSegments -> new yy.PathExpression(false, $1, yy.locInfo(@$)) ; pathSegments From ffc9fb5007f516350ddeca90640982e4e12503f5 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 16:55:41 -0600 Subject: [PATCH 49/99] Add original to BooleanLiteral --- lib/handlebars/compiler/ast.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 69581f281..efb4ece98 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -120,7 +120,8 @@ var AST = { BooleanLiteral: function(bool, locInfo) { this.loc = locInfo; this.type = 'BooleanLiteral'; - this.value = bool === 'true'; + this.original = + this.value = bool === 'true'; }, Hash: function(pairs, locInfo) { From 95b23095c097447ff4e1059720abfd2132cb9b2d Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 16:55:53 -0600 Subject: [PATCH 50/99] First crack at compiler API docs --- docs/compiler-api.md | 224 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 docs/compiler-api.md diff --git a/docs/compiler-api.md b/docs/compiler-api.md new file mode 100644 index 000000000..7e89b01e4 --- /dev/null +++ b/docs/compiler-api.md @@ -0,0 +1,224 @@ +# Handlebars Compiler APIs + +There are a number of formal APIs that tool implementors may interact with. + +## AST + +Other tools may interact with the formal AST as defined below. Any JSON structure matching this pattern may be used and passed into the `compile` and `precompile` methods in the same way as the text for a template. + +AST structures may be generated either with the `Handlebars.parse` method and then manipulated, via the `Handlebars.AST` objects of the same name, or constructed manually as a generic JavaScript object matching the structure defined below. + +```javascript +var ast = Handlebars.parse(myTemplate); + +// Modify ast + +Handlebars.precompile(ast); +``` + + +### Basic + +```java +interface Node { + type: string; + loc: SourceLocation | null; +} + +interface SourceLocation { + source: string | null; + start: Position; + end: Position; +} + +interface Position { + line: uint >= 1; + column: uint >= 0; +} +``` + +### Programs + +```java +interface Program <: Node { + type: "Program"; + body: [ Statement ]; + + blockParams: [ string ]; + strip: StripFlags | null; +} +``` + +### Statements + +```java +interface Statement <: Node { + strip: StripFlags | null; +} + +interface MustacheStatement <: Statement { + type: "MustacheStatement"; + sexpr: Subexpression; + escaped: boolean; +} + +interface BlockStatement <: Statement { + type: "BlockStatement"; + sexpr: Subexpression; + program: Program; + inverse: Program | null; +} + +interface PartialStatement <: Statement { + type: "PartialStatement"; + sexpr: Subexpression; + + indent: string; +} + +interface ContentStatement <: Statement { + type: "ContentStatement"; + value: string; + original: string; +} + +interface CommentStatement <: Statement { + type: "CommentStatement"; + value: string; +} +``` + +### Expressions + +```java +interface Expression <: Node { } +``` + +##### Subexpressions + +```java +interface SubExpression <: Expression { + type: "SubExpression"; + path: PathExpression; + params: [ Expression ]; + hash: Hash; + + isHelper: true | null; +} +``` + +`isHelper` is not required and is used to disambiguate between cases such as `{{foo}}` and `(foo)`, which have slightly different call behaviors. + +##### Paths + +```java +interface PathExpression <: Expression { + type: "PathExpression"; + data: boolean; + depth: uint >= 0; + parts: [ string ]; + original: string; +} +``` + +- `data` is true when the given expression is a `@data` reference. +- `depth` is an integer representation of which context the expression references. `0` represents the current context, `1` would be `../`, etc. +- `parts` is an array of the names in the path. `foo.bar` would be `['foo', 'bar']`. Scope references, `.`, `..`, and `this` should be omitted from this array. +- `original` is the path as entered by the user. Separator and scope references are left untouched. + + +##### Literals + +```java +interface Literal <: Expression { } + +interface StringLiteral <: Literal { + type: "StringLiteral"; + value: string; + original: string; +} + +interface BooleanLiteral <: Literal { + type: "BooleanLiteral"; + value: boolean; + original: boolean; +} + +interface NumberLiteral <: Literal { + type: "NumberLiteral"; + value: number; + original: number; +} +``` + + +### Miscellaneous + +```java +interface Hash <: Node { + type: "Hash"; + pairs: [ HashPair ]; +} + +interface HashPair <: Node { + type: "HashPair"; + key: string; + value: Expression; +} + +interface StripFlags { + left: boolean; + right: boolean; +} +``` + +`StripFlags` are used to signify whitespace control character that may have been entered on a given statement. + +TODO : Document what the flags mean or drop this from things like Program. + +## AST Visitor + +`Handlebars.Visitor` is available as a base class for general interaction with AST structures. This will by default traverse the entire tree and individual methods may be overridden to provide specific responses to particular nodes. + +Recording all referenced partial names: + +```javascript +var Visitor = Handlebars.Visitor; + +function ImportScanner() { + this.partials = []; +} +ImportScanner.prototype = new Visitor(); + +ImportScanner.prototype.PartialStatement = function(partial) { + this.partials.push({request: partial.sexpr.original}); + + Visitor.prototype.PartialStatement.call(this, partial); +}; + +var scanner = new ImportScanner(); +scanner.accept(ast); +``` + +## JavaScript Compiler + +The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler: + +```javascript +function MyCompiler() { + Handlebars.JavaScriptCompiler.apply(this, arguments); +} +MyCompiler.prototype = Object.create(Handlebars.JavaScriptCompiler); + +MyCompiler.nameLookup = function(parent, name, type) { + if (type === 'partial') { + return 'MyPartialList[' + JSON.stringify(name) ']'; + } else { + return Handlebars.JavaScriptCompiler.prototype.nameLookup.call(this, parent, name, type); + } +}; + +var env = Handlebars.create(); +env.JavaScriptCompiler = MyCompiler; +env.compile('my template'); +``` From 8a6796e5c09686b47945a35826d77680d589d07c Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 17:26:52 -0600 Subject: [PATCH 51/99] Move Jison parsing out of AST into helpers --- lib/handlebars/compiler/ast.js | 47 +++++------------------- lib/handlebars/compiler/helpers.js | 37 +++++++++++++++++++ spec/ast.js | 59 ++---------------------------- spec/parser.js | 13 ++++++- src/handlebars.yy | 14 +++---- 5 files changed, 68 insertions(+), 102 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index efb4ece98..9f2544362 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -10,20 +10,12 @@ var AST = { this.strip = strip; }, - MustacheStatement: function(rawParams, open, strip, locInfo) { + MustacheStatement: function(sexpr, escaped, strip, locInfo) { this.loc = locInfo; this.type = 'MustacheStatement'; - this.sexpr = rawParams; - - // Open may be a string parsed from the parser or a passed boolean flag - if (open != null && open.charAt) { - // Must use charAt to support IE pre-10 - var escapeFlag = open.charAt(3) || open.charAt(2); - this.escaped = escapeFlag !== '{' && escapeFlag !== '&'; - } else { - this.escaped = !!open; - } + this.sexpr = sexpr; + this.escaped = escaped; this.strip = strip; }, @@ -63,43 +55,22 @@ var AST = { strip.inlineStandalone = true; }, - SubExpression: function(rawParams, hash, locInfo) { + SubExpression: function(path, params, hash, locInfo) { this.loc = locInfo; this.type = 'SubExpression'; - this.path = rawParams[0]; - this.params = rawParams.slice(1); + this.path = path; + this.params = params || []; this.hash = hash; }, - PathExpression: function(data, parts, locInfo) { + PathExpression: function(data, depth, parts, original, locInfo) { this.loc = locInfo; this.type = 'PathExpression'; - var original = '', - dig = [], - depth = 0, - depthString = ''; - - for(var i=0,l=parts.length; i 0) { - throw new Exception('Invalid path: ' + original, this); - } else if (part === '..') { - depth++; - depthString += '../'; - } - } else { - dig.push(part); - } - } - this.data = data; - this.original = (data ? '@' : '') + original; - this.parts = dig; + this.original = original; + this.parts = parts; this.depth = depth; }, diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 3d5144f70..f215049f3 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -24,6 +24,43 @@ export function stripComment(comment) { .replace(/-?-?~?\}\}$/, ''); } +export function preparePath(data, parts, locInfo) { + /*jshint -W040 */ + locInfo = this.locInfo(locInfo); + + var original = data ? '@' : '', + dig = [], + depth = 0, + depthString = ''; + + for(var i=0,l=parts.length; i 0) { + throw new Exception('Invalid path: ' + original, {loc: locInfo}); + } else if (part === '..') { + depth++; + depthString += '../'; + } + } else { + dig.push(part); + } + } + + return new this.PathExpression(data, depth, dig, original, locInfo); +} + +export function prepareMustache(sexpr, open, strip, locInfo) { + /*jshint -W040 */ + // Must use charAt to support IE pre-10 + var escapeFlag = open.charAt(3) || open.charAt(2), + escaped = escapeFlag !== '{' && escapeFlag !== '&'; + + return new this.MustacheStatement(sexpr, escaped, strip, this.locInfo(locInfo)); +} + export function prepareRawBlock(openRawBlock, content, close, locInfo) { /*jshint -W040 */ if (openRawBlock.sexpr.path.original !== close) { diff --git a/spec/ast.js b/spec/ast.js index 53267493d..3e36730a4 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -23,43 +23,14 @@ describe('ast', function() { } describe('MustacheStatement', function() { - function testEscape(open, expected) { - var mustache = new handlebarsEnv.AST.MustacheStatement([{}], open, false); - equals(mustache.escaped, expected); - } - it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheStatement([id, 'param1'], '', false, LOCATION_INFO); + mustache = new handlebarsEnv.AST.MustacheStatement({}, true, {}, LOCATION_INFO); equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); testLocationInfoStorage(mustache); }); - it('should accept token for escape', function() { - testEscape('{{', true); - testEscape('{{~', true); - testEscape('{{#', true); - testEscape('{{~#', true); - testEscape('{{/', true); - testEscape('{{~/', true); - testEscape('{{^', true); - testEscape('{{~^', true); - testEscape('{', true); - testEscape('{', true); - - testEscape('{{&', false); - testEscape('{{~&', false); - testEscape('{{{', false); - testEscape('{{~{', false); - }); - it('should accept boolean for escape', function() { - testEscape(true, true); - testEscape({}, true); - - testEscape(false, false); - testEscape(undefined, false); - }); }); describe('BlockStatement', function() { it('should throw on mustache mismatch', function() { @@ -70,7 +41,7 @@ describe('ast', function() { it('stores location info', function(){ var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null); - var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, null, '{{', {}); + var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, false, {}); var block = new handlebarsEnv.AST.BlockStatement(mustacheNode, {body: [], strip: {}}, {body: [], strip: {}}, { @@ -82,32 +53,8 @@ describe('ast', function() { }); }); describe('PathExpression', function() { - it('should throw on invalid path', function() { - shouldThrow(function() { - new handlebarsEnv.AST.PathExpression(false, [ - {part: 'foo'}, - {part: '..'}, - {part: 'bar'} - ], {start: {line: 1, column: 1}}); - }, Handlebars.Exception, "Invalid path: foo.. - 1:1"); - shouldThrow(function() { - new handlebarsEnv.AST.PathExpression(false, [ - {part: 'foo'}, - {part: '.'}, - {part: 'bar'} - ], {start: {line: 1, column: 1}}); - }, Handlebars.Exception, "Invalid path: foo. - 1:1"); - shouldThrow(function() { - new handlebarsEnv.AST.PathExpression(false, [ - {part: 'foo'}, - {part: 'this'}, - {part: 'bar'} - ], {start: {line: 1, column: 1}}); - }, Handlebars.Exception, "Invalid path: foothis - 1:1"); - }); - it('stores location info', function(){ - var idNode = new handlebarsEnv.AST.PathExpression(false, [], LOCATION_INFO); + var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO); testLocationInfoStorage(idNode); }); }); diff --git a/spec/parser.js b/spec/parser.js index ad5273446..26eb4ddfb 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -166,7 +166,6 @@ describe('parser', function() { it('parses inverse block with block params', function() { equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); }); - it("raises if there's a Parse error", function() { shouldThrow(function() { ast_for("foo{{^}}bar"); @@ -186,6 +185,18 @@ describe('parser', function() { }, Error, /goodbyes doesn't match hellos/); }); + it('should handle invalid paths', function() { + shouldThrow(function() { + ast_for("{{foo/../bar}}"); + }, Error, /Invalid path: foo\/\.\. - 1:2/); + shouldThrow(function() { + ast_for("{{foo/./bar}}"); + }, Error, /Invalid path: foo\/\. - 1:2/); + shouldThrow(function() { + ast_for("{{foo/this/bar}}"); + }, Error, /Invalid path: foo\/this - 1:2/); + }); + it('knows how to report the correct line number in errors', function() { shouldThrow(function() { ast_for("hello\nmy\n{{foo}"); diff --git a/src/handlebars.yy b/src/handlebars.yy index 8b63df045..7f2765cbb 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_INVERSE_CHAIN sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) ; inverseAndProgram @@ -73,8 +73,8 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) ; partial @@ -82,8 +82,8 @@ partial ; sexpr - : helperName param* hash? -> new yy.SubExpression([$1].concat($2), $3, yy.locInfo(@$)) - | dataName -> new yy.SubExpression([$1], null, yy.locInfo(@$)) + : helperName param* hash? -> new yy.SubExpression($1, $2, $3, yy.locInfo(@$)) + | dataName -> new yy.SubExpression($1, null, null, yy.locInfo(@$)) ; param @@ -114,11 +114,11 @@ helperName ; dataName - : DATA pathSegments -> new yy.PathExpression(true, $2, yy.locInfo(@$)) + : DATA pathSegments -> yy.preparePath(true, $2, @$) ; path - : pathSegments -> new yy.PathExpression(false, $1, yy.locInfo(@$)) + : pathSegments -> yy.preparePath(false, $1, @$) ; pathSegments From 928ba56b9577fd6cd874f0a83178f1265a6d0526 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 22:58:21 -0600 Subject: [PATCH 52/99] Rework strip flags to make clearer at in AST level Rather than keeping state in the AST, which requires some gymnastics, we create a separate visitor flow which does the top down iteration necessary to calculate all of the state needed for proper whitespace control evaluation. --- docs/compiler-api.md | 22 +- lib/handlebars/compiler/ast.js | 9 +- lib/handlebars/compiler/base.js | 4 +- lib/handlebars/compiler/helpers.js | 204 ++--------------- lib/handlebars/compiler/whitespace-control.js | 210 ++++++++++++++++++ spec/ast.js | 15 +- src/handlebars.yy | 17 +- 7 files changed, 268 insertions(+), 213 deletions(-) create mode 100644 lib/handlebars/compiler/whitespace-control.js diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 7e89b01e4..89dcc4ccf 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -45,28 +45,31 @@ interface Program <: Node { body: [ Statement ]; blockParams: [ string ]; - strip: StripFlags | null; } ``` ### Statements ```java -interface Statement <: Node { - strip: StripFlags | null; -} +interface Statement <: Node { } interface MustacheStatement <: Statement { type: "MustacheStatement"; sexpr: Subexpression; escaped: boolean; + + strip: StripFlags | null; } interface BlockStatement <: Statement { type: "BlockStatement"; sexpr: Subexpression; - program: Program; + program: Program | null; inverse: Program | null; + + openStrip: StripFlags | null; + inverseStrip: StripFlags | null; + closeStrip: StripFlags | null; } interface PartialStatement <: Statement { @@ -74,6 +77,7 @@ interface PartialStatement <: Statement { sexpr: Subexpression; indent: string; + strip: StripFlags | null; } interface ContentStatement <: Statement { @@ -85,6 +89,8 @@ interface ContentStatement <: Statement { interface CommentStatement <: Statement { type: "CommentStatement"; value: string; + + strip: StripFlags | null; } ``` @@ -167,15 +173,13 @@ interface HashPair <: Node { } interface StripFlags { - left: boolean; - right: boolean; + open: boolean; + close: boolean; } ``` `StripFlags` are used to signify whitespace control character that may have been entered on a given statement. -TODO : Document what the flags mean or drop this from things like Program. - ## AST Visitor `Handlebars.Visitor` is available as a base class for general interaction with AST structures. This will by default traverse the entire tree and individual methods may be overridden to provide specific responses to particular nodes. diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 9f2544362..72a56aa3f 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -20,14 +20,17 @@ var AST = { this.strip = strip; }, - BlockStatement: function(sexpr, program, inverse, strip, locInfo) { + BlockStatement: function(sexpr, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { this.loc = locInfo; this.type = 'BlockStatement'; this.sexpr = sexpr; this.program = program; this.inverse = inverse; - this.strip = strip; + + this.openStrip = openStrip; + this.inverseStrip = inverseStrip; + this.closeStrip = closeStrip; }, PartialStatement: function(sexpr, strip, locInfo) { @@ -37,7 +40,6 @@ var AST = { this.indent = ''; this.strip = strip; - this.strip.inlineStandalone = true; }, ContentStatement: function(string, locInfo) { @@ -52,7 +54,6 @@ var AST = { this.value = comment; this.strip = strip; - strip.inlineStandalone = true; }, SubExpression: function(path, params, hash, locInfo) { diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 786c37ebd..ff237ec23 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -1,5 +1,6 @@ import parser from "./parser"; import AST from "./ast"; +import WhitespaceControl from "./whitespace-control"; module Helpers from "./helpers"; import { extend } from "../utils"; @@ -19,5 +20,6 @@ export function parse(input, options) { return new yy.SourceLocation(options && options.srcName, locInfo); }; - return parser.parse(input); + var strip = new WhitespaceControl(); + return strip.accept(parser.parse(input)); } diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index f215049f3..1daddf650 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -14,8 +14,8 @@ export function SourceLocation(source, locInfo) { export function stripFlags(open, close) { return { - left: open.charAt(2) === '~', - right: close.charAt(close.length-3) === '~' + open: open.charAt(2) === '~', + close: close.charAt(close.length-3) === '~' }; } @@ -69,9 +69,13 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode); } + locInfo = this.locInfo(locInfo); var program = new this.Program([content], null, {}, locInfo); - return new this.BlockStatement(openRawBlock.sexpr, program, undefined, undefined, locInfo); + return new this.BlockStatement( + openRawBlock.sexpr, program, undefined, + {}, {}, {}, + locInfo); } export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { @@ -85,192 +89,26 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver program.blockParams = openBlock.blockParams; - // Safely handle a chained inverse that does not have a non-conditional inverse - // (i.e. both inverseAndProgram AND close are undefined) - if (!close) { - close = {strip: {}}; - } - - // Find the inverse program that is involed with whitespace stripping. - var inverse = inverseAndProgram && inverseAndProgram.program, - firstInverse = inverse, - lastInverse = inverse; - if (inverse && inverse.inverse) { - firstInverse = inverse.body[0].program; + var inverse, + inverseStrip; - // Walk the inverse chain to find the last inverse that is actually in the chain. - while (lastInverse.inverse) { - lastInverse = lastInverse.body[lastInverse.body.length-1].program; + if (inverseAndProgram) { + if (inverseAndProgram.chain) { + inverseAndProgram.program.body[0].closeStrip = close.strip || close.openStrip; } - } - var strip = { - left: openBlock.strip.left, - right: close.strip.right, - - // Determine the standalone candiacy. Basically flag our content as being possibly standalone - // so our parent can determine if we actually are standalone - openStandalone: isNextWhitespace(program.body), - closeStandalone: isPrevWhitespace((firstInverse || program).body) - }; - - if (openBlock.strip.right) { - omitRight(program.body, null, true); - } - - if (inverse) { - var inverseStrip = inverseAndProgram.strip; - - if (inverseStrip.left) { - omitLeft(program.body, null, true); - } - - if (inverseStrip.right) { - omitRight(firstInverse.body, null, true); - } - if (close.strip.left) { - omitLeft(lastInverse.body, null, true); - } - - // Find standalone else statments - if (isPrevWhitespace(program.body) - && isNextWhitespace(firstInverse.body)) { - - omitLeft(program.body); - omitRight(firstInverse.body); - } - } else { - if (close.strip.left) { - omitLeft(program.body, null, true); - } + inverseStrip = inverseAndProgram.strip; + inverse = inverseAndProgram.program; } if (inverted) { - return new this.BlockStatement(openBlock.sexpr, inverse, program, strip, locInfo); - } else { - return new this.BlockStatement(openBlock.sexpr, program, inverse, strip, locInfo); - } -} - - -export function prepareProgram(body, isRoot) { - for (var i = 0, l = body.length; i < l; i++) { - var current = body[i], - strip = current.strip; - - if (!strip) { - continue; - } - - var _isPrevWhitespace = isPrevWhitespace(body, i, isRoot, current.type === 'partial'), - _isNextWhitespace = isNextWhitespace(body, i, isRoot), - - openStandalone = strip.openStandalone && _isPrevWhitespace, - closeStandalone = strip.closeStandalone && _isNextWhitespace, - inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; - - if (strip.right) { - omitRight(body, i, true); - } - if (strip.left) { - omitLeft(body, i, true); - } - - if (inlineStandalone) { - omitRight(body, i); - - if (omitLeft(body, i)) { - // If we are on a standalone node, save the indent info for partials - if (current.type === 'PartialStatement') { - // Pull out the whitespace from the final line - current.indent = (/([ \t]+$)/).exec(body[i-1].original)[1]; - } - } - } - if (openStandalone) { - omitRight((current.program || current.inverse).body); - - // Strip out the previous content node if it's whitespace only - omitLeft(body, i); - } - if (closeStandalone) { - // Always strip the next node - omitRight(body, i); - - omitLeft((current.inverse || current.program).body); - } - } - - return body; -} - -function isPrevWhitespace(body, i, isRoot) { - if (i === undefined) { - i = body.length; - } - - // Nodes that end with newlines are considered whitespace (but are special - // cased for strip operations) - var prev = body[i-1], - sibling = body[i-2]; - if (!prev) { - return isRoot; - } - - if (prev.type === 'ContentStatement') { - return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original); - } -} -function isNextWhitespace(body, i, isRoot) { - if (i === undefined) { - i = -1; - } - - var next = body[i+1], - sibling = body[i+2]; - if (!next) { - return isRoot; - } - - if (next.type === 'ContentStatement') { - return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original); - } -} - -// Marks the node to the right of the position as omitted. -// I.e. {{foo}}' ' will mark the ' ' node as omitted. -// -// If i is undefined, then the first child will be marked as such. -// -// If mulitple is truthy then all whitespace will be stripped out until non-whitespace -// content is met. -function omitRight(body, i, multiple) { - var current = body[i == null ? 0 : i + 1]; - if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) { - return; - } - - var original = current.value; - current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); - current.rightStripped = current.value !== original; -} - -// Marks the node to the left of the position as omitted. -// I.e. ' '{{foo}} will mark the ' ' node as omitted. -// -// If i is undefined then the last child will be marked as such. -// -// If mulitple is truthy then all whitespace will be stripped out until non-whitespace -// content is met. -function omitLeft(body, i, multiple) { - var current = body[i == null ? body.length - 1 : i - 1]; - if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) { - return; + inverted = inverse; + inverse = program; + program = inverted; } - // We omit the last node if it's whitespace only and not preceeded by a non-content node. - var original = current.value; - current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); - current.leftStripped = current.value !== original; - return current.leftStripped; + return new this.BlockStatement( + openBlock.sexpr, program, inverse, + openBlock.strip, inverseStrip, close && (close.strip || close.openStrip), + this.locInfo(locInfo)); } diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js new file mode 100644 index 000000000..e10eb1570 --- /dev/null +++ b/lib/handlebars/compiler/whitespace-control.js @@ -0,0 +1,210 @@ +import Visitor from "./visitor"; + +function WhitespaceControl() { +} +WhitespaceControl.prototype = new Visitor(); + +WhitespaceControl.prototype.Program = function(program) { + var isRoot = !this.isRootSeen; + this.isRootSeen = true; + + var body = program.body; + for (var i = 0, l = body.length; i < l; i++) { + var current = body[i], + strip = this.accept(current); + + if (!strip) { + continue; + } + + var _isPrevWhitespace = isPrevWhitespace(body, i, isRoot), + _isNextWhitespace = isNextWhitespace(body, i, isRoot), + + openStandalone = strip.openStandalone && _isPrevWhitespace, + closeStandalone = strip.closeStandalone && _isNextWhitespace, + inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; + + if (strip.close) { + omitRight(body, i, true); + } + if (strip.open) { + omitLeft(body, i, true); + } + + if (inlineStandalone) { + omitRight(body, i); + + if (omitLeft(body, i)) { + // If we are on a standalone node, save the indent info for partials + if (current.type === 'PartialStatement') { + // Pull out the whitespace from the final line + current.indent = (/([ \t]+$)/).exec(body[i-1].original)[1]; + } + } + } + if (openStandalone) { + omitRight((current.program || current.inverse).body); + + // Strip out the previous content node if it's whitespace only + omitLeft(body, i); + } + if (closeStandalone) { + // Always strip the next node + omitRight(body, i); + + omitLeft((current.inverse || current.program).body); + } + } + + return program; +}; +WhitespaceControl.prototype.BlockStatement = function(block) { + this.accept(block.program); + this.accept(block.inverse); + + // Find the inverse program that is involed with whitespace stripping. + var program = block.program || block.inverse, + inverse = block.program && block.inverse, + firstInverse = inverse, + lastInverse = inverse; + + if (inverse && inverse.chained) { + firstInverse = inverse.body[0].program; + + // Walk the inverse chain to find the last inverse that is actually in the chain. + while (lastInverse.chained) { + lastInverse = lastInverse.body[lastInverse.body.length-1].program; + } + } + + var strip = { + open: block.openStrip.open, + close: block.closeStrip.close, + + // Determine the standalone candiacy. Basically flag our content as being possibly standalone + // so our parent can determine if we actually are standalone + openStandalone: isNextWhitespace(program.body), + closeStandalone: isPrevWhitespace((firstInverse || program).body) + }; + + if (block.openStrip.close) { + omitRight(program.body, null, true); + } + + if (inverse) { + var inverseStrip = block.inverseStrip; + + if (inverseStrip.open) { + omitLeft(program.body, null, true); + } + + if (inverseStrip.close) { + omitRight(firstInverse.body, null, true); + } + if (block.closeStrip.open) { + omitLeft(lastInverse.body, null, true); + } + + // Find standalone else statments + if (isPrevWhitespace(program.body) + && isNextWhitespace(firstInverse.body)) { + + omitLeft(program.body); + omitRight(firstInverse.body); + } + } else { + if (block.closeStrip.open) { + omitLeft(program.body, null, true); + } + } + + return strip; +}; + +WhitespaceControl.prototype.MustacheStatement = function(mustache) { + return mustache.strip; +}; + +WhitespaceControl.prototype.PartialStatement = + WhitespaceControl.prototype.CommentStatement = function(node) { + var strip = node.strip || {}; + return { + inlineStandalone: true, + open: strip.open, + close: strip.close + }; +}; + + +function isPrevWhitespace(body, i, isRoot) { + if (i === undefined) { + i = body.length; + } + + // Nodes that end with newlines are considered whitespace (but are special + // cased for strip operations) + var prev = body[i-1], + sibling = body[i-2]; + if (!prev) { + return isRoot; + } + + if (prev.type === 'ContentStatement') { + return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original); + } +} +function isNextWhitespace(body, i, isRoot) { + if (i === undefined) { + i = -1; + } + + var next = body[i+1], + sibling = body[i+2]; + if (!next) { + return isRoot; + } + + if (next.type === 'ContentStatement') { + return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original); + } +} + +// Marks the node to the right of the position as omitted. +// I.e. {{foo}}' ' will mark the ' ' node as omitted. +// +// If i is undefined, then the first child will be marked as such. +// +// If mulitple is truthy then all whitespace will be stripped out until non-whitespace +// content is met. +function omitRight(body, i, multiple) { + var current = body[i == null ? 0 : i + 1]; + if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) { + return; + } + + var original = current.value; + current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); + current.rightStripped = current.value !== original; +} + +// Marks the node to the left of the position as omitted. +// I.e. ' '{{foo}} will mark the ' ' node as omitted. +// +// If i is undefined then the last child will be marked as such. +// +// If mulitple is truthy then all whitespace will be stripped out until non-whitespace +// content is met. +function omitLeft(body, i, multiple) { + var current = body[i == null ? body.length - 1 : i - 1]; + if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) { + return; + } + + // We omit the last node if it's whitespace only and not preceeded by a non-content node. + var original = current.value; + current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); + current.leftStripped = current.value !== original; + return current.leftStripped; +} + +export default WhitespaceControl; diff --git a/spec/ast.js b/spec/ast.js index 3e36730a4..d464cf110 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -42,13 +42,14 @@ describe('ast', function() { it('stores location info', function(){ var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null); var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, false, {}); - var block = new handlebarsEnv.AST.BlockStatement(mustacheNode, - {body: [], strip: {}}, {body: [], strip: {}}, - { - strip: {}, - path: {original: 'foo'} - }, - LOCATION_INFO); + var block = new handlebarsEnv.AST.BlockStatement( + mustacheNode, + {body: []}, + {body: []}, + {}, + {}, + {}, + LOCATION_INFO); testLocationInfoStorage(block); }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index 7f2765cbb..0b2062e39 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -5,11 +5,11 @@ %% root - : program EOF { yy.prepareProgram($1.body, true); return $1; } + : program EOF { return $1; } ; program - : statement* -> new yy.Program(yy.prepareProgram($1), null, {}, yy.locInfo(@$)) + : statement* -> new yy.Program($1, null, {}, yy.locInfo(@$)) ; statement @@ -26,7 +26,7 @@ content ; rawBlock - : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, yy.locInfo(@$)) + : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) ; openRawBlock @@ -34,8 +34,8 @@ openRawBlock ; block - : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, yy.locInfo(@$)) - | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, yy.locInfo(@$)) + : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) + | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) ; openBlock @@ -56,10 +56,9 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { - var inverse = yy.prepareBlock($1, $2, $3, $3, false, yy.locInfo(@$)), - program = new yy.Program(yy.prepareProgram([inverse]), null, {}, yy.locInfo(@$)); - - program.inverse = inverse; + var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), + program = new yy.Program([inverse], null, {}, yy.locInfo(@$)); + program.chained = true; $$ = { strip: $1.strip, program: program, chain: true }; } From a655aedb5cf523430b08ada5f8cc4730d1db3e5b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 29 Nov 2014 17:59:09 -0600 Subject: [PATCH 53/99] s/Subexpression/SubExpression/ --- docs/compiler-api.md | 8 ++++---- lib/handlebars/compiler/compiler.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 89dcc4ccf..5431a9830 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -55,7 +55,7 @@ interface Statement <: Node { } interface MustacheStatement <: Statement { type: "MustacheStatement"; - sexpr: Subexpression; + sexpr: SubExpression; escaped: boolean; strip: StripFlags | null; @@ -63,7 +63,7 @@ interface MustacheStatement <: Statement { interface BlockStatement <: Statement { type: "BlockStatement"; - sexpr: Subexpression; + sexpr: SubExpression; program: Program | null; inverse: Program | null; @@ -74,7 +74,7 @@ interface BlockStatement <: Statement { interface PartialStatement <: Statement { type: "PartialStatement"; - sexpr: Subexpression; + sexpr: SubExpression; indent: string; strip: StripFlags | null; @@ -100,7 +100,7 @@ interface CommentStatement <: Statement { interface Expression <: Node { } ``` -##### Subexpressions +##### SubExpressions ```java interface SubExpression <: Expression { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index df5a10f4f..5ba091650 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -363,7 +363,7 @@ Compiler.prototype = { this.opcode('pushStringParam', val, value, val.type); if (val.type === 'SubExpression') { - // Subexpressions get evaluated and passed in + // SubExpressions get evaluated and passed in // in string params mode. this.accept(val); } From 5b0efc7e91e6f563301fb0b1823a0fe9cf4cc40e Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 23:52:50 -0600 Subject: [PATCH 54/99] Track source node via accept call rather vs opcode --- lib/handlebars/compiler/compiler.js | 87 +++++++++++++++-------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 5ba091650..4b5b0af8f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -62,6 +62,7 @@ Compiler.prototype = { guid: 0, compile: function(program, options) { + this.sourceNode = []; this.opcodes = []; this.children = []; this.depths = {list: []}; @@ -91,11 +92,15 @@ Compiler.prototype = { }, accept: function(node) { - return this[node.type](node); + this.sourceNode.unshift(node); + var ret = this[node.type](node); + this.sourceNode.shift(); + return ret; }, Program: function(program) { var body = program.body; + for(var i=0, l=body.length; i Date: Fri, 28 Nov 2014 23:56:41 -0600 Subject: [PATCH 55/99] Move compileProgram out of visitor methods --- lib/handlebars/compiler/compiler.js | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 4b5b0af8f..534ddb85b 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -91,6 +91,24 @@ Compiler.prototype = { return this.accept(program); }, + compileProgram: function(program) { + var result = new this.compiler().compile(program, this.options); + var guid = this.guid++, depth; + + this.usePartial = this.usePartial || result.usePartial; + + this.children[guid] = result; + + for(var i=0, l=result.depths.list.length; i Date: Sat, 29 Nov 2014 18:11:58 -0600 Subject: [PATCH 56/99] Drop unused JavaScriptCompiler.namespace param --- lib/handlebars/compiler/javascript-compiler.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index db1977811..6703b4436 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -52,8 +52,6 @@ JavaScriptCompiler.prototype = { initializeBuffer: function() { return this.quotedString(""); }, - - namespace: "Handlebars", // END PUBLIC API compile: function(environment, options, context, asObject) { From f84f76f006a7992d9f82d14f4ebff64687fefa05 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 29 Nov 2014 18:19:52 -0600 Subject: [PATCH 57/99] Add basic docs for JavaScriptCompiler override API --- docs/compiler-api.md | 23 +++++++++++++++++++ .../compiler/javascript-compiler.js | 18 +++++++-------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 5431a9830..98ca8943c 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -208,6 +208,29 @@ scanner.accept(ast); The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler: +- `nameLookup(parent, name, type)` + Used to generate the code to resolve a give path component. + + - `parent` is the existing code in the path resolution + - `name` is the current path component + - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`. + +- `depthedLookup(name)` + Used to generate code that resolves parameters within any context in the stack. Is only used in `compat` mode. + +- `compilerInfo()` + Allows for custom compiler flags used in the runtime version checking logic. + +- `appendToBuffer(source, location, explicit)` + Allows for code buffer emitting code. Defaults behavior is string concatenation. + + - `source` is the source code whose result is to be appending + - `location` is the location of the source in the source map. + - `explicit` is a flag signaling that the emit operation must occur, vs. the lazy evaled options otherwise. + +- `initializeBuffer()` + Allows for buffers other than the default string buffer to be used. Generally needs to be paired with a custom `appendToBuffer` implementation. + ```javascript function MyCompiler() { Handlebars.JavaScriptCompiler.apply(this, arguments); diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 6703b4436..537d1b092 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -29,23 +29,23 @@ JavaScriptCompiler.prototype = { return [revision, versions]; }, - appendToBuffer: function(string, location, explicit) { - // Force a string as this simplifies the merge logic. - if (!isArray(string)) { - string = [string]; + appendToBuffer: function(source, location, explicit) { + // Force a source as this simplifies the merge logic. + if (!isArray(source)) { + source = [source]; } - string = this.source.wrap(string, location); + source = this.source.wrap(source, location); if (this.environment.isSimple) { - return ['return ', string, ';']; + return ['return ', source, ';']; } else if (explicit) { // This is a case where the buffer operation occurs as a child of another // construct, generally braces. We have to explicitly output these buffer // operations to ensure that the emitted code goes in the correct location. - return ['buffer += ', string, ';']; + return ['buffer += ', source, ';']; } else { - string.appendToBuffer = true; - return string; + source.appendToBuffer = true; + return source; } }, From 3238645f260a77b02baa9b686cf7d5de3a7d2694 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 29 Nov 2014 18:30:00 -0600 Subject: [PATCH 58/99] Allow blockParams on chained inverse statements --- spec/parser.js | 3 +++ spec/tokenizer.js | 10 +++++++--- src/handlebars.yy | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/parser.js b/spec/parser.js index 26eb4ddfb..73bff27d6 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -166,6 +166,9 @@ describe('parser', function() { it('parses inverse block with block params', function() { equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); }); + it('parses chained inverse block with block params', function() { + equals(ast_for("{{#foo}}{{else foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); + }); it("raises if there's a Parse error", function() { shouldThrow(function() { ast_for("foo{{^}}bar"); diff --git a/spec/tokenizer.js b/spec/tokenizer.js index 93ed97cfa..0a5143bef 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -1,3 +1,4 @@ +/*global Handlebars */ function shouldMatchTokens(result, tokens) { for (var index = 0; index < result.length; index++) { equals(result[index].name, tokens[index]); @@ -404,13 +405,16 @@ describe('Tokenizer', function() { var result = tokenize("{{#foo as |bar|}}"); shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); - var result = tokenize("{{#foo as |bar baz|}}"); + result = tokenize("{{#foo as |bar baz|}}"); shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); - var result = tokenize("{{#foo as | bar baz |}}"); + result = tokenize("{{#foo as | bar baz |}}"); shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); - var result = tokenize("{{#foo as as | bar baz |}}"); + result = tokenize("{{#foo as as | bar baz |}}"); shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); + + result = tokenize("{{else foo as |bar baz|}}"); + shouldMatchTokens(result, ['OPEN_INVERSE_CHAIN', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index 0b2062e39..27985b8d0 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) + : OPEN_INVERSE_CHAIN sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } ; inverseAndProgram From e7f51c2248340461ce0cdc5790f6af0203f4f1c4 Mon Sep 17 00:00:00 2001 From: Or Neeman Date: Sun, 7 Dec 2014 08:44:03 -0600 Subject: [PATCH 59/99] Add instructions for 'spec/mustache' to CONTRIBUTING.md, fix a few typos elsewere --- CONTRIBUTING.md | 2 ++ lib/handlebars/compiler/ast.js | 2 +- lib/handlebars/compiler/base.js | 2 +- lib/precompiler.js | 2 +- spec/precompiler.js | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c5256b41..b84fdb005 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,8 @@ To build Handlebars.js you'll need a few things installed. * Node.js * [Grunt](http://gruntjs.com/getting-started) +Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`. + Project dependencies may be installed via `npm install`. To build Handlebars.js from scratch, you'll want to run `grunt` diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 72a56aa3f..0020216f5 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -111,5 +111,5 @@ var AST = { // Must be exported as an object rather than the root of the module as the jison lexer -// most modify the object to operate properly. +// must modify the object to operate properly. export default AST; diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index ff237ec23..167b84438 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -10,7 +10,7 @@ var yy = {}; extend(yy, Helpers, AST); export function parse(input, options) { - // Just return if an already-compile AST was passed in. + // Just return if an already-compiled AST was passed in. if (input.type === 'Program') { return input; } parser.yy = yy; diff --git a/lib/precompiler.js b/lib/precompiler.js index 165c00881..64a6daba2 100644 --- a/lib/precompiler.js +++ b/lib/precompiler.js @@ -27,7 +27,7 @@ module.exports.cli = function(opts) { }); if (opts.simple && opts.min) { - throw new Handlebars.Exception('Unable to minimze simple output'); + throw new Handlebars.Exception('Unable to minimize simple output'); } if (opts.simple && (opts.templates.length !== 1 || fs.statSync(opts.templates[0]).isDirectory())) { throw new Handlebars.Exception('Unable to output multiple templates in simple mode'); diff --git a/spec/precompiler.js b/spec/precompiler.js index c56bee634..cbbfdf7ea 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -61,7 +61,7 @@ describe('precompiler', function() { it('should throw when combining simple and minimized', function() { shouldThrow(function() { Precompiler.cli({templates: [__dirname], simple: true, min: true}); - }, Handlebars.Exception, 'Unable to minimze simple output'); + }, Handlebars.Exception, 'Unable to minimize simple output'); }); it('should throw when combining simple and multiple templates', function() { shouldThrow(function() { From 0d396ccd30b51d3c5ffb7fb3e79670d89b610163 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 1 Dec 2014 01:23:15 -0600 Subject: [PATCH 60/99] Remove Compiler.depths parameter This is no longer necessary with the depths array tracking and adds compiler overhead. --- lib/handlebars/compiler/compiler.js | 20 +++++-------------- .../compiler/javascript-compiler.js | 6 ++---- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 534ddb85b..71e0ccf88 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -98,13 +98,7 @@ Compiler.prototype = { this.usePartial = this.usePartial || result.usePartial; this.children[guid] = result; - - for(var i=0, l=result.depths.list.length; i Date: Mon, 1 Dec 2014 01:32:41 -0600 Subject: [PATCH 61/99] Remove unused vars and add jshint checking --- .jshintrc | 1 + lib/handlebars/compiler/ast.js | 2 -- lib/handlebars/compiler/compiler.js | 4 +--- lib/handlebars/compiler/javascript-compiler.js | 1 - lib/handlebars/compiler/printer.js | 2 +- lib/handlebars/compiler/visitor.js | 1 + lib/handlebars/utils.js | 2 -- lib/precompiler.js | 1 - 8 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.jshintrc b/.jshintrc index bb29c76e8..313015ee7 100644 --- a/.jshintrc +++ b/.jshintrc @@ -45,6 +45,7 @@ "plusplus": false, "regexp": false, "undef": true, + "unused": true, "sub": true, "strict": false, "white": false diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 0020216f5..3f4639c70 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,5 +1,3 @@ -import Exception from "../exception"; - var AST = { Program: function(statements, blockParams, strip, locInfo) { this.loc = locInfo; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 71e0ccf88..2c0bd086a 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -18,8 +18,6 @@ function scopedId(path) { // an ID is simple if it only has one part, and that part is not // `..` or `this`. function simpleId(path) { - var part = path.parts[0]; - return path.parts.length === 1 && !scopedId(path) && !path.depth; } @@ -93,7 +91,7 @@ Compiler.prototype = { compileProgram: function(program) { var result = new this.compiler().compile(program, this.options); - var guid = this.guid++, depth; + var guid = this.guid++; this.usePartial = this.usePartial || result.usePartial; diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index dfe2df124..356424b79 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -755,7 +755,6 @@ JavaScriptCompiler.prototype = { replaceStack: function(callback) { var prefix = ['('], - inline = this.isInline(), stack, createdStack, usedLiteral; diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index b549d619f..ad276f19a 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -127,7 +127,7 @@ PrintVisitor.prototype.BooleanLiteral = function(bool) { PrintVisitor.prototype.Hash = function(hash) { var pairs = hash.pairs; - var joinedPairs = [], left, right; + var joinedPairs = []; for (var i=0, l=pairs.length; i Date: Mon, 1 Dec 2014 01:33:09 -0600 Subject: [PATCH 62/99] Fix remaining depth arg --- lib/handlebars/compiler/compiler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 2c0bd086a..4c0f90829 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -63,7 +63,6 @@ Compiler.prototype = { this.sourceNode = []; this.opcodes = []; this.children = []; - this.depths = {list: []}; this.options = options; this.stringParams = options.stringParams; this.trackIds = options.trackIds; From 69b1bdd32e99e31eec3079171818636812c8bda8 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 1 Dec 2014 01:33:31 -0600 Subject: [PATCH 63/99] Avoid unnecessary this. reference --- lib/handlebars/compiler/compiler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 4c0f90829..5e589154f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -68,8 +68,8 @@ Compiler.prototype = { this.trackIds = options.trackIds; // These changes will propagate to the other compiler components - var knownHelpers = this.options.knownHelpers; - this.options.knownHelpers = { + var knownHelpers = options.knownHelpers; + options.knownHelpers = { 'helperMissing': true, 'blockHelperMissing': true, 'each': true, @@ -81,7 +81,7 @@ Compiler.prototype = { }; if (knownHelpers) { for (var name in knownHelpers) { - this.options.knownHelpers[name] = knownHelpers[name]; + options.knownHelpers[name] = knownHelpers[name]; } } From 59e80c510391c4535aa07780dc407524f83a9f11 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 16 Dec 2014 17:58:26 -0600 Subject: [PATCH 64/99] Track depths on deduped children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a very specific error case where deduped children won’t receive the depths object due to it being omitted by the caller when optimizing. Fixes #926 --- .../compiler/javascript-compiler.js | 2 ++ spec/regressions.js | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 356424b79..9bd70befe 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -696,6 +696,8 @@ JavaScriptCompiler.prototype = { } else { child.index = index; child.name = 'program' + index; + + this.useDepths = this.useDepths || child.depths.list.length; } } }, diff --git a/spec/regressions.js b/spec/regressions.js index 11207fce3..84b9d0c52 100644 --- a/spec/regressions.js +++ b/spec/regressions.js @@ -1,4 +1,4 @@ -/*global CompilerContext, Handlebars, shouldCompileTo, shouldThrow */ +/*global CompilerContext, shouldCompileTo, shouldThrow */ 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"}]}; @@ -145,4 +145,21 @@ describe('Regressions', function() { shouldCompileTo('{{str bar.baz}}', [{}, helpers], 'undefined'); }); + + it('GH-926: Depths and de-dupe', function() { + var context = { + name: 'foo', + data: [ + 1 + ], + notData: [ + 1 + ] + }; + + var template = CompilerContext.compile('{{#if dater}}{{#each data}}{{../name}}{{/each}}{{else}}{{#each notData}}{{../name}}{{/each}}{{/if}}'); + + var result = template(context); + equals(result, 'foo'); + }); }); From 6d2239d8ac471d05086026e2bd622f879ccde52b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 17 Dec 2014 00:40:28 -0600 Subject: [PATCH 65/99] Update fix for #926 for 3.0 codeline --- lib/handlebars/compiler/javascript-compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 9bd70befe..3e4f5e1d0 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -697,7 +697,7 @@ JavaScriptCompiler.prototype = { child.index = index; child.name = 'program' + index; - this.useDepths = this.useDepths || child.depths.list.length; + this.useDepths = this.useDepths || child.useDepths; } } }, From 9e907e67854ea1ae208fe061452a9c9e2ce9468b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 21 Dec 2014 22:32:50 -0600 Subject: [PATCH 66/99] Expose AST helpers in public API --- lib/handlebars/compiler/ast.js | 21 +++++++++++++++++++++ lib/handlebars/compiler/compiler.js | 26 +++++--------------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 3f4639c70..113c03e79 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -104,6 +104,27 @@ var AST = { this.type = 'HashPair'; this.key = key; this.value = value; + }, + + // Public API used to evaluate derived attributes regarding AST nodes + helpers: { + // a mustache is definitely a helper if: + // * it is an eligible helper, and + // * it has at least one parameter or hash segment + // TODO: Make these public utility methods + helperExpression: function(sexpr) { + return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash); + }, + + scopedId: function(path) { + return (/^\.|this\b/).test(path.original); + }, + + // an ID is simple if it only has one part, and that part is not + // `..` or `this`. + simpleId: function(path) { + return path.parts.length === 1 && !AST.helpers.scopedId(path) && !path.depth; + } } }; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 5e589154f..7be96cb51 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -1,26 +1,10 @@ import Exception from "../exception"; import {isArray} from "../utils"; +import AST from "./ast"; var slice = [].slice; -// a mustache is definitely a helper if: -// * it is an eligible helper, and -// * it has at least one parameter or hash segment -function helperExpr(sexpr) { - return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash); -} - -function scopedId(path) { - return (/^\.|this\b/).test(path.original); -} - -// an ID is simple if it only has one part, and that part is not -// `..` or `this`. -function simpleId(path) { - return path.parts.length === 1 && !scopedId(path) && !path.depth; -} - export function Compiler() {} // the foundHelper register will disambiguate helper lookup from finding a @@ -239,7 +223,7 @@ Compiler.prototype = { path.falsy = true; this.accept(path); - this.opcode('invokeHelper', params.length, path.original, simpleId(path)); + this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path)); } }, @@ -255,7 +239,7 @@ Compiler.prototype = { this.options.data = true; this.opcode('lookupData', path.depth, path.parts); } else { - this.opcode('lookupOnContext', path.parts, path.falsy, scopedId(path)); + this.opcode('lookupOnContext', path.parts, path.falsy, AST.helpers.scopedId(path)); } }, @@ -301,12 +285,12 @@ Compiler.prototype = { classifySexpr: function(sexpr) { // a mustache is an eligible helper if: // * its id is simple (a single part, not `this` or `..`) - var isHelper = helperExpr(sexpr); + var isHelper = AST.helpers.helperExpression(sexpr); // if a mustache is an eligible helper but not a definite // helper, it is ambiguous, and will be resolved in a later // pass or at runtime. - var isEligible = isHelper || simpleId(sexpr.path); + var isEligible = isHelper || AST.helpers.simpleId(sexpr.path); var options = this.options; From 396795c983273bb5ca4dc67ddc74eb12f00bf110 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 26 Dec 2014 00:31:57 -0600 Subject: [PATCH 67/99] Implement block parameters Fixes #907 --- lib/handlebars/base.js | 6 +- lib/handlebars/compiler/compiler.js | 65 +++++++++++---- .../compiler/javascript-compiler.js | 82 +++++++++++++------ lib/handlebars/runtime.js | 28 +++++-- lib/handlebars/utils.js | 5 ++ spec/builtins.js | 10 +++ spec/helpers.js | 60 ++++++++++++++ spec/runtime.js | 12 ++- spec/track-ids.js | 32 ++++++++ 9 files changed, 248 insertions(+), 52 deletions(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 50aa3f82b..d75e965cb 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -128,7 +128,11 @@ function registerDefaultHelpers(instance) { data.contextPath = contextPath + key; } } - ret = ret + fn(context[key], { data: data }); + + ret = ret + fn(context[key], { + data: data, + blockParams: Utils.blockParams([context[key], key], [contextPath + key, null]) + }); } if(context && typeof context === 'object') { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 7be96cb51..13143cc8d 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -51,6 +51,8 @@ Compiler.prototype = { this.stringParams = options.stringParams; this.trackIds = options.trackIds; + options.blockParams = options.blockParams || []; + // These changes will propagate to the other compiler components var knownHelpers = options.knownHelpers; options.knownHelpers = { @@ -92,14 +94,17 @@ Compiler.prototype = { }, Program: function(program) { - var body = program.body; + this.options.blockParams.unshift(program.blockParams); + var body = program.body; for(var i=0, l=body.length; i= 0) { + return [depth, param]; + } + } } }; @@ -429,11 +462,11 @@ export function compile(input, options, env) { } return compiled._setup(options); }; - ret._child = function(i, data, depths) { + ret._child = function(i, data, blockParams, depths) { if (!compiled) { compiled = compileInput(); } - return compiled._child(i, data, depths); + return compiled._child(i, data, blockParams, depths); }; return ret; } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 3e4f5e1d0..9f407924f 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -77,10 +77,12 @@ JavaScriptCompiler.prototype = { this.hashes = []; this.compileStack = []; this.inlineStack = []; + this.blockParams = []; this.compileChildren(environment, options); this.useDepths = this.useDepths || environment.useDepths || this.options.compat; + this.useBlockParams = this.useBlockParams || environment.useBlockParams; var opcodes = environment.opcodes, opcode, @@ -127,6 +129,9 @@ JavaScriptCompiler.prototype = { if (this.useDepths) { ret.useDepths = true; } + if (this.useBlockParams) { + ret.useBlockParams = true; + } if (this.options.compat) { ret.compat = true; } @@ -186,6 +191,9 @@ JavaScriptCompiler.prototype = { var params = ["depth0", "helpers", "partials", "data"]; + if (this.useBlockParams || this.useDepths) { + params.push('blockParams'); + } if (this.useDepths) { params.push('depths'); } @@ -385,9 +393,7 @@ JavaScriptCompiler.prototype = { // Looks up the value of `name` on the current context and pushes // it onto the stack. lookupOnContext: function(parts, falsy, scoped) { - /*jshint -W083 */ - var i = 0, - len = parts.length; + var i = 0; if (!scoped && this.options.compat && !this.lastContext) { // The depthed query is expected to handle the undefined logic for the root level that @@ -397,19 +403,21 @@ JavaScriptCompiler.prototype = { this.pushContext(); } - for (; i < len; i++) { - this.replaceStack(function(current) { - var lookup = this.nameLookup(current, parts[i], 'context'); - // We want to ensure that zero and false are handled properly if the context (falsy flag) - // needs to have the special handling for these values. - if (!falsy) { - return [' != null ? ', lookup, ' : ', current]; - } else { - // Otherwise we can use generic falsy handling - return [' && ', lookup]; - } - }); - } + this.resolvePath('context', parts, i, falsy); + }, + + // [lookupBlockParam] + // + // On stack, before: ... + // On stack, after: blockParam[name], ... + // + // Looks up the value of `parts` on the given block param and pushes + // it onto the stack. + lookupBlockParam: function(blockParamId, parts) { + this.useBlockParams = true; + + this.push(['blockParams[', blockParamId[0], '][', blockParamId[1], ']']); + this.resolvePath('context', parts, 1); }, // [lookupData] @@ -426,9 +434,23 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral('this.data(data, ' + depth + ')'); } - for (var i = 0, len = parts.length; i < len; i++) { + this.resolvePath('data', parts, 0, true); + }, + + resolvePath: function(type, parts, i, falsy) { + /*jshint -W083 */ + var len = parts.length; + for (; i < len; i++) { this.replaceStack(function(current) { - return [' && ', this.nameLookup(current, parts[i], 'data')]; + var lookup = this.nameLookup(current, parts[i], type); + // We want to ensure that zero and false are handled properly if the context (falsy flag) + // needs to have the special handling for these values. + if (!falsy) { + return [' != null ? ', lookup, ' : ', current]; + } else { + // Otherwise we can use generic falsy handling + return [' && ', lookup]; + } }); } }, @@ -661,8 +683,12 @@ JavaScriptCompiler.prototype = { hash.values[key] = value; }, - pushId: function(type, name) { - if (type === 'PathExpression') { + pushId: function(type, name, child) { + if (type === 'BlockParam') { + this.pushStackLiteral( + 'blockParams[' + name[0] + '].path[' + name[1] + ']' + + (child ? ' + ' + JSON.stringify('.' + child) : '')); + } else if (type === 'PathExpression') { this.pushString(name); } else if (type === 'SubExpression') { this.pushStackLiteral('true'); @@ -693,11 +719,13 @@ JavaScriptCompiler.prototype = { this.context.environments[index] = child; this.useDepths = this.useDepths || compiler.useDepths; + this.useBlockParams = this.useBlockParams || compiler.useBlockParams; } else { child.index = index; child.name = 'program' + index; this.useDepths = this.useDepths || child.useDepths; + this.useBlockParams = this.useBlockParams || child.useBlockParams; } } }, @@ -712,11 +740,12 @@ JavaScriptCompiler.prototype = { programExpression: function(guid) { var child = this.environment.children[guid], - useDepths = this.useDepths; + programParams = [child.index, 'data', child.blockParams]; - var programParams = [child.index, 'data']; - - if (useDepths) { + if (this.useBlockParams || this.useDepths) { + programParams.push('blockParams'); + } + if (this.useDepths) { programParams.push('depths'); } @@ -943,7 +972,10 @@ JavaScriptCompiler.prototype = { } if (this.options.data) { - options.data = "data"; + options.data = 'data'; + } + if (this.useBlockParams) { + options.blockParams = 'blockParams'; } return options; }, diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 455dd332a..72f2e0dce 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -89,11 +89,11 @@ export function template(templateSpec, env) { }, programs: [], - program: function(i, data, depths) { + program: function(i, data, declaredBlockParams, blockParams, depths) { var programWrapper = this.programs[i], fn = this.fn(i); - if (data || depths) { - programWrapper = program(this, i, fn, data, depths); + if (data || depths || blockParams || declaredBlockParams) { + programWrapper = program(this, i, fn, data, declaredBlockParams, blockParams, depths); } else if (!programWrapper) { programWrapper = this.programs[i] = program(this, i, fn); } @@ -128,12 +128,13 @@ export function template(templateSpec, env) { if (!options.partial && templateSpec.useData) { data = initData(context, data); } - var depths; + var depths, + blockParams = templateSpec.useBlockParams ? [] : undefined; if (templateSpec.useDepths) { depths = options.depths ? [context].concat(options.depths) : [context]; } - return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths); + return templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths); }; ret.isTop = true; @@ -150,24 +151,33 @@ export function template(templateSpec, env) { } }; - ret._child = function(i, data, depths) { + ret._child = function(i, data, blockParams, depths) { + if (templateSpec.useBlockParams && !blockParams) { + throw new Exception('must pass block params'); + } if (templateSpec.useDepths && !depths) { throw new Exception('must pass parent depths'); } - return program(container, i, templateSpec[i], data, depths); + return program(container, i, templateSpec[i], data, 0, blockParams, depths); }; return ret; } -export function program(container, i, fn, data, depths) { +export function program(container, i, fn, data, declaredBlockParams, blockParams, depths) { var prog = function(context, options) { options = options || {}; - return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths)); + return fn.call(container, + context, + container.helpers, container.partials, + options.data || data, + blockParams && [options.blockParams].concat(blockParams), + depths && [context].concat(depths)); }; prog.program = i; prog.depth = depths ? depths.length : 0; + prog.blockParams = declaredBlockParams || 0; return prog; } diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index 0878cc4f1..8cea50d74 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -78,6 +78,11 @@ export function isEmpty(value) { } } +export function blockParams(params, ids) { + params.path = ids; + return params; +} + export function appendContextPath(contextPath, id) { return (contextPath ? contextPath + '.' : '') + id; } diff --git a/spec/builtins.js b/spec/builtins.js index 9aa916902..eb5157c35 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -122,6 +122,16 @@ describe('builtin helpers', function() { equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used"); }); + it('each with block params', function() { + var string = '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!'; + var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}], world: 'world'}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, '0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!'); + }); + it("each object with @index", function() { var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; var hash = {goodbyes: {'a': {text: "goodbye"}, b: {text: "Goodbye"}, c: {text: "GOODBYE"}}, world: "world"}; diff --git a/spec/helpers.js b/spec/helpers.js index f23ee1403..712bb00a0 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -657,4 +657,64 @@ describe('helpers', function() { equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed"); }); }); + + describe('block params', function() { + it('should take presedence over context values', function() { + var hash = {value: 'foo'}; + var helpers = { + goodbyes: function(options) { + equals(options.fn.blockParams, 1); + return options.fn({value: 'bar'}, {blockParams: [1, 2]}); + } + }; + shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo'); + }); + it('should take presedence over helper values', function() { + var hash = {}; + var helpers = { + value: function() { + return 'foo'; + }, + goodbyes: function(options) { + equals(options.fn.blockParams, 1); + return options.fn({}, {blockParams: [1, 2]}); + } + }; + shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo'); + }); + it('should not take presedence over pathed values', function() { + var hash = {value: 'bar'}; + var helpers = { + value: function() { + return 'foo'; + }, + goodbyes: function(options) { + equals(options.fn.blockParams, 1); + return options.fn(this, {blockParams: [1, 2]}); + } + }; + shouldCompileTo('{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}', [hash, helpers], 'barfoo'); + }); + it('should take presednece over parent block params', function() { + var hash = {value: 'foo'}, + value = 1; + var helpers = { + goodbyes: function(options) { + return options.fn({value: 'bar'}, {blockParams: options.fn.blockParams === 1 ? [value++, value++] : undefined}); + } + }; + shouldCompileTo('{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}', [hash, helpers], '13foo'); + }); + + it('should allow block params on chained helpers', function() { + var hash = {value: 'foo'}; + var helpers = { + goodbyes: function(options) { + equals(options.fn.blockParams, 1); + return options.fn({value: 'bar'}, {blockParams: [1, 2]}); + } + }; + shouldCompileTo('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}', [hash, helpers], '1foo'); + }); + }); }); diff --git a/spec/runtime.js b/spec/runtime.js index 48a22a99b..ce8b14c56 100644 --- a/spec/runtime.js +++ b/spec/runtime.js @@ -48,6 +48,16 @@ describe('runtime', function() { template._child(1); }, Error, 'must pass parent depths'); }); + + it('should throw for block param methods without params', function() { + shouldThrow(function() { + var template = Handlebars.compile('{{#foo as |foo|}}{{foo}}{{/foo}}'); + // Calling twice to hit the non-compiled case. + template._setup({}); + template._setup({}); + template._child(1); + }, Error, 'must pass block params'); + }); it('should expose child template', function() { var template = Handlebars.compile('{{#foo}}bar{{/foo}}'); // Calling twice to hit the non-compiled case. @@ -57,7 +67,7 @@ describe('runtime', function() { it('should render depthed content', function() { var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}'); // Calling twice to hit the non-compiled case. - equal(template._child(1, undefined, [{bar: 'baz'}])(), 'baz'); + equal(template._child(1, undefined, [], [{bar: 'baz'}])(), 'baz'); }); }); diff --git a/spec/track-ids.js b/spec/track-ids.js index 938f98bb5..f337fbe11 100644 --- a/spec/track-ids.js +++ b/spec/track-ids.js @@ -106,6 +106,27 @@ describe('track ids', function() { equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS 1'); }); + it('should use block param paths', function() { + var template = CompilerContext.compile('{{#doIt as |is|}}{{wycats is.a slave.driver is}}{{/doIt}}', {trackIds: true}); + + var helpers = { + doIt: function(options) { + var blockParams = [this.is]; + blockParams.path = ['zomg']; + return options.fn(this, {blockParams: blockParams}); + }, + wycats: function(passiveVoice, noun, blah, options) { + equal(options.ids[0], 'zomg.a'); + equal(options.ids[1], 'slave.driver'); + equal(options.ids[2], 'zomg'); + + return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun; + } + }; + + equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS zomg.a:foo slave.driver:bar'); + }); + describe('builtin helpers', function() { var helpers = { wycats: function(name, options) { @@ -129,6 +150,17 @@ describe('track ids', function() { equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:.array..0\nbar:.array..1\n'); }); + it('should handle block params', function() { + var helpers = { + wycats: function(name, options) { + return name + ':' + options.ids[0] + '\n'; + } + }; + + var template = CompilerContext.compile('{{#each array as |value|}}{{wycats value.name}}{{/each}}', {trackIds: true}); + + equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:array.0.name\nbar:array.1.name\n'); + }); }); describe('#with', function() { it('should track contextPath', function() { From 8babe059534acda388672491108c2d6f1ac2a431 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 26 Dec 2014 11:12:47 -0600 Subject: [PATCH 68/99] Bump code coverage --- lib/handlebars.js | 2 ++ lib/handlebars.runtime.js | 2 ++ lib/handlebars/compiler/helpers.js | 4 ++-- lib/handlebars/compiler/whitespace-control.js | 1 + spec/builtins.js | 3 +-- spec/visitor.js | 5 ----- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/handlebars.js b/lib/handlebars.js index 5a4b301ae..81b7cfbd2 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -31,8 +31,10 @@ Handlebars = create(); Handlebars.create = create; /*jshint -W040 */ +/* istanbul ignore next */ var root = typeof global !== 'undefined' ? global : window, $Handlebars = root.Handlebars; +/* istanbul ignore next */ Handlebars.noConflict = function() { if (root.Handlebars === Handlebars) { root.Handlebars = $Handlebars; diff --git a/lib/handlebars.runtime.js b/lib/handlebars.runtime.js index 49f29632f..ef00e2856 100644 --- a/lib/handlebars.runtime.js +++ b/lib/handlebars.runtime.js @@ -30,8 +30,10 @@ var Handlebars = create(); Handlebars.create = create; /*jshint -W040 */ +/* istanbul ignore next */ var root = typeof global !== 'undefined' ? global : window, $Handlebars = root.Handlebars; +/* istanbul ignore next */ Handlebars.noConflict = function() { if (root.Handlebars === Handlebars) { root.Handlebars = $Handlebars; diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 1daddf650..6cfc0e076 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -94,7 +94,7 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver if (inverseAndProgram) { if (inverseAndProgram.chain) { - inverseAndProgram.program.body[0].closeStrip = close.strip || close.openStrip; + inverseAndProgram.program.body[0].closeStrip = close.strip; } inverseStrip = inverseAndProgram.strip; @@ -109,6 +109,6 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver return new this.BlockStatement( openBlock.sexpr, program, inverse, - openBlock.strip, inverseStrip, close && (close.strip || close.openStrip), + openBlock.strip, inverseStrip, close && close.strip, this.locInfo(locInfo)); } diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js index e10eb1570..97866128c 100644 --- a/lib/handlebars/compiler/whitespace-control.js +++ b/lib/handlebars/compiler/whitespace-control.js @@ -127,6 +127,7 @@ WhitespaceControl.prototype.MustacheStatement = function(mustache) { WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.CommentStatement = function(node) { + /* istanbul ignore next */ var strip = node.strip || {}; return { inlineStandalone: true, diff --git a/spec/builtins.js b/spec/builtins.js index eb5157c35..e6af46bd7 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -98,8 +98,7 @@ describe('builtin helpers', function() { var expected2 = "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"; equals(actual === expected1 || actual === expected2, 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"); + shouldCompileTo(string, {goodbyes: {}, world: 'world'}, 'cruel world!'); }); it("each with @index", function() { diff --git a/spec/visitor.js b/spec/visitor.js index 66c3b688f..17a948ee9 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -5,11 +5,6 @@ describe('Visitor', function() { return; } - function ast_for(template) { - var ast = Handlebars.parse(template); - return Handlebars.print(ast); - } - it('should provide coverage', function() { // Simply run the thing and make sure it does not fail and that all of the // stub methods are executed From 9ff3daf78556a62829b8982c467243dc810a8a6b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 26 Dec 2014 11:32:30 -0600 Subject: [PATCH 69/99] Add parent tracking and mutation to AST visitors Fixes #916 --- docs/compiler-api.md | 6 ++ lib/handlebars/compiler/visitor.js | 113 ++++++++++++++++++++--------- spec/visitor.js | 111 +++++++++++++++++++++++++++- 3 files changed, 192 insertions(+), 38 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 98ca8943c..74af67246 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -204,6 +204,12 @@ var scanner = new ImportScanner(); scanner.accept(ast); ``` +The current node's ancestors will be maintained in the `parents` array, with the most recent parent listed first. + +The visitor may also be configured to operate in mutation mode by setting the `mutation` field to true. When in this mode, handler methods may return any valid AST node and it will replace the one they are currently operating on. Returning `false` will remove the given value (if valid) and returning `undefined` will leave the node in tact. This return structure only apply to mutation mode and non-mutation mode visitors are free to return whatever values they wish. + +Implementors that may need to support mutation mode are encouraged to utilize the `acceptKey`, `acceptRequired` and `acceptArray` helpers which provide the conditional overwrite behavior as well as implement sanity checks where pertinent. + ## JavaScript Compiler The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler: diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index c2480adc5..e98d0bf3b 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -1,66 +1,109 @@ -/*jshint unused: false */ -function Visitor() {} +import Exception from "../exception"; +import AST from "./AST"; + +function Visitor() { + this.parents = []; +} Visitor.prototype = { constructor: Visitor, + mutating: false, - accept: function(object) { - return object && this[object.type](object); + // Visits a given value. If mutating, will replace the value if necessary. + acceptKey: function(node, name) { + var value = this.accept(node[name]); + if (this.mutating) { + // Hacky sanity check: + if (value && (!value.type || !AST[value.type])) { + throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type); + } + node[name] = value; + } }, - Program: function(program) { - var body = program.body, - i, l; + // Performs an accept operation with added sanity check to ensure + // required keys are not removed. + acceptRequired: function(node, name) { + this.acceptKey(node, name); - for(i=0, l=body.length; i bar }} {{/foo.bar}}')); }); + + it('should return undefined'); + + describe('mutating', function() { + describe('fields', function() { + it('should replace value', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.StringLiteral = function(string) { + return new Handlebars.AST.NumberLiteral(42, string.locInfo); + }; + + var ast = Handlebars.parse('{{foo foo="foo"}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'); + }); + it('should treat undefined resonse as identity', function() { + var visitor = new Handlebars.Visitor(); + visitor.mutating = true; + + var ast = Handlebars.parse('{{foo foo=42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'); + }); + it('should remove false responses', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.Hash = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo foo=42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n'); + }); + it('should throw when removing required values', function() { + shouldThrow(function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.SubExpression = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + }, Handlebars.Exception, 'MustacheStatement requires sexpr'); + }); + it('should throw when returning non-node responses', function() { + shouldThrow(function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.SubExpression = function() { + return {}; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + }, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting sexpr on MustacheStatement'); + }); + }); + describe('arrays', function() { + it('should replace value', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.StringLiteral = function(string) { + return new Handlebars.AST.NumberLiteral(42, string.locInfo); + }; + + var ast = Handlebars.parse('{{foo "foo"}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n'); + }); + it('should treat undefined resonse as identity', function() { + var visitor = new Handlebars.Visitor(); + visitor.mutating = true; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n'); + }); + it('should remove false responses', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.NumberLiteral = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n'); + }); + }); + }); }); From be0ba759250489858cb6844c5de47cfa003b87ba Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 26 Dec 2014 11:34:48 -0600 Subject: [PATCH 70/99] Fix case-insensitive file system compile issue --- lib/handlebars/compiler/visitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index e98d0bf3b..3fb37fbfc 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -1,5 +1,5 @@ import Exception from "../exception"; -import AST from "./AST"; +import AST from "./ast"; function Visitor() { this.parents = []; From daa8f3dd0566a7ad734d5bdca109b221fb951453 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 27 Dec 2014 10:05:05 -0600 Subject: [PATCH 71/99] Fix duplicate return statement on simple templates --- lib/handlebars/compiler/javascript-compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 9f407924f..2e1108cf1 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -248,7 +248,7 @@ JavaScriptCompiler.prototype = { if (bufferStart) { bufferStart.prepend('return '); bufferEnd.add(';'); - } else { + } else if (!sourceSeen) { this.source.push('return "";'); } } else { From f1470505133377223db6d3e6f4e30d5665e7f23e Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 27 Dec 2014 12:12:21 -0600 Subject: [PATCH 72/99] Update dependencies --- package.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 61bce7125..c4711c99e 100644 --- a/package.json +++ b/package.json @@ -21,37 +21,37 @@ "node": ">=0.4.7" }, "dependencies": { - "optimist": "~0.3", + "optimist": "^0.6.1", "source-map": "^0.1.40" }, "optionalDependencies": { "uglify-js": "~2.3" }, "devDependencies": { - "async": "~0.2.9", + "async": "^0.9.0", "aws-sdk": "~1.5.0", "benchmark": "~1.0", - "dustjs-linkedin": "~2.0.2", + "dustjs-linkedin": "^2.0.2", "eco": "~1.1.0-rc-3", "es6-module-packager": "^2.0.0", "grunt": "~0.4.1", "grunt-cli": "~0.1.10", - "grunt-contrib-clean": "~0.4.1", - "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-connect": "~0.5.0", - "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-clean": "0.x", + "grunt-contrib-concat": "0.x", + "grunt-contrib-connect": "0.x", + "grunt-contrib-copy": "0.x", "grunt-contrib-jshint": "0.x", - "grunt-contrib-requirejs": "~0.4.1", - "grunt-contrib-uglify": "~0.2.2", - "grunt-contrib-watch": "~0.5.3", + "grunt-contrib-requirejs": "0.x", + "grunt-contrib-uglify": "0.x", + "grunt-contrib-watch": "0.x", "grunt-saucelabs": "8.x", "istanbul": "^0.3.0", "jison": "~0.3.0", "keen.io": "0.0.3", "mocha": "~1.20.0", - "mustache": "~0.7.2", - "semver": "~2.1.0", - "underscore": "~1.5.1" + "mustache": "0.x", + "semver": "^4.0.0", + "underscore": "^1.5.1" }, "main": "lib/index.js", "bin": { From b474630c7dc96f7a4e56c3530a89920e13bf26d4 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 27 Dec 2014 12:58:28 -0600 Subject: [PATCH 73/99] Strip unnecessary whitespace in template output --- lib/handlebars/compiler/javascript-compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 2e1108cf1..247f5e071 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -263,7 +263,7 @@ JavaScriptCompiler.prototype = { } if (varDeclarations) { - this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ')); + this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n')); } return this.source.merge(); From 9c9a972a0c5d00db3a896cdadcdde6f2c453f66f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 27 Dec 2014 13:01:13 -0600 Subject: [PATCH 74/99] Safely handle source map in browser tests --- spec/source-map.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/spec/source-map.js b/spec/source-map.js index 4d3fa3e82..a9e8ba358 100644 --- a/spec/source-map.js +++ b/spec/source-map.js @@ -1,9 +1,13 @@ /*global CompilerContext, Handlebars */ -var SourceMap = require('source-map'), - SourceMapConsumer = SourceMap.SourceMapConsumer; +try { + var SourceMap = require('source-map'), + SourceMapConsumer = SourceMap.SourceMapConsumer; +} catch (err) { + /* NOP for in browser */ +} describe('source-map', function() { - if (!Handlebars.precompile) { + if (!Handlebars.precompile || !SourceMap) { return; } @@ -14,8 +18,8 @@ describe('source-map', function() { equal(!!template.map, !CompilerContext.browser); }); it('should map source properly', function() { - var source = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}', - template = Handlebars.precompile(source, {destName: 'dest.js', srcName: 'src.hbs'}); + var template = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}'; + template = Handlebars.precompile(template, {destName: 'dest.js', srcName: 'src.hbs'}); if (template.map) { var consumer = new SourceMapConsumer(template.map), From 0a9fc171b00a12fa990d2f7d27310182ed51a41c Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 27 Dec 2014 13:13:52 -0600 Subject: [PATCH 75/99] Fix block param evaluation under older IE --- lib/handlebars/compiler/compiler.js | 4 ++-- lib/handlebars/utils.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 13143cc8d..95fe101b8 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -1,5 +1,5 @@ import Exception from "../exception"; -import {isArray} from "../utils"; +import {isArray, indexOf} from "../utils"; import AST from "./ast"; var slice = [].slice; @@ -400,7 +400,7 @@ Compiler.prototype = { blockParamIndex: function(name) { for (var depth = 0, len = this.options.blockParams.length; depth < len; depth++) { var blockParams = this.options.blockParams[depth], - param = blockParams && blockParams.indexOf(name); + param = blockParams && indexOf(blockParams, name); if (blockParams && param >= 0) { return [depth, param]; } diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index 8cea50d74..fc2b8464c 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -48,6 +48,16 @@ export var isArray = Array.isArray || function(value) { return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; }; +// Older IE versions do not directly support indexOf so we must implement our own, sadly. +export function indexOf(array, value) { + for (var i = 0, len = array.length; i < len; i++) { + if (array[i] === value) { + return i; + } + } + return -1; +} + export function escapeExpression(string) { // don't escape SafeStrings, since they're already safe From b764fb1ded3c2bb3c56796e6d7264981e63ba0d7 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 27 Dec 2014 13:17:55 -0600 Subject: [PATCH 76/99] Fix sourcemap test --- spec/source-map.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/source-map.js b/spec/source-map.js index a9e8ba358..6eced9ca0 100644 --- a/spec/source-map.js +++ b/spec/source-map.js @@ -18,8 +18,8 @@ describe('source-map', function() { equal(!!template.map, !CompilerContext.browser); }); it('should map source properly', function() { - var template = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}'; - template = Handlebars.precompile(template, {destName: 'dest.js', srcName: 'src.hbs'}); + var source = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}', + template = Handlebars.precompile(source, {destName: 'dest.js', srcName: 'src.hbs'}); if (template.map) { var consumer = new SourceMapConsumer(template.map), From 6650957f790f0ca42cd67c6bf72282e2cfc991f8 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 27 Dec 2014 14:57:09 -0600 Subject: [PATCH 77/99] Implement strict and assumeObject modes Causes templates to throw when lookup fields are not defined within the context. Strict mode will throw when any field is omitted. assumeObjects mode maintains the existing behavior of outputting an empty response when fields are not defined but allows for performance/size optimizations by not doing safety checks for intermediate objects in the lookup chain. Strict mode effectively disables the helperMissing and inverse handling for blockHelperMissing as templates will throw rather than call those features for missing data fields. Fixes #651 Fixes #805 --- .../compiler/javascript-compiler.js | 49 ++++++- lib/handlebars/runtime.js | 6 + spec/strict.js | 124 ++++++++++++++++++ 3 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 spec/strict.js diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 247f5e071..641daecb6 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -439,6 +439,11 @@ JavaScriptCompiler.prototype = { resolvePath: function(type, parts, i, falsy) { /*jshint -W083 */ + if (this.options.strict || this.options.assumeObjects) { + this.push(strictLookup(this.options.strict, this, parts, type)); + return; + } + var len = parts.length; for (; i < len; i++) { this.replaceStack(function(current) { @@ -572,11 +577,13 @@ JavaScriptCompiler.prototype = { var helper = this.setupHelper(paramSize, name); var simple = isSimple ? [helper.name, ' || '] : ''; - this.push( - this.source.functionCall( - ['('].concat(simple, nonHelper, ' || ', this.aliasable('helpers.helperMissing'), ')'), - 'call', - helper.callParams)); + var lookup = ['('].concat(simple, nonHelper); + if (!this.options.strict) { + lookup.push(' || ', this.aliasable('helpers.helperMissing')); + } + lookup.push(')'); + + this.push(this.source.functionCall(lookup, 'call', helper.callParams)); }, // [invokeKnownHelper] @@ -613,9 +620,17 @@ JavaScriptCompiler.prototype = { var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); + var lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')']; + if (!this.options.strict) { + lookup[0] = '(helper = '; + lookup.push( + ' != null ? helper : ', + this.aliasable('helpers.helperMissing') + ); + } + this.push([ - '((helper = (helper = ', helperName, ' || ', nonHelper, ') != null ? helper : ', - this.aliasable('helpers.helperMissing'), + '(', lookup, (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),', '(typeof helper === ', this.aliasable('"function"'), ' ? ', this.source.functionCall('helper','call', helper.callParams), ' : helper))' @@ -1023,4 +1038,24 @@ JavaScriptCompiler.isValidJavaScriptVariableName = function(name) { return !JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name); }; +function strictLookup(requireTerminal, compiler, parts, type) { + var stack = compiler.popStack(); + + var i = 0, + len = parts.length; + if (requireTerminal) { + len--; + } + + for (; i < len; i++) { + stack = compiler.nameLookup(stack, parts[i], type); + } + + if (requireTerminal) { + return [compiler.aliasable('this.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')']; + } else { + return stack; + } +} + export default JavaScriptCompiler; diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 72f2e0dce..2b9f47414 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -69,6 +69,12 @@ export function template(templateSpec, env) { // Just add water var container = { + strict: function(obj, name) { + if (!(name in obj)) { + throw new Exception('"' + name + '" not defined in ' + obj); + } + return obj[name]; + }, lookup: function(depths, name) { var len = depths.length; for (var i = 0; i < len; i++) { diff --git a/spec/strict.js b/spec/strict.js new file mode 100644 index 000000000..f701866fe --- /dev/null +++ b/spec/strict.js @@ -0,0 +1,124 @@ +/*global CompilerContext, Handlebars, shouldThrow */ +var Exception = Handlebars.Exception; + +describe('strict', function() { + describe('strict mode', function() { + it('should error on missing property lookup', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello}}', {strict: true}); + + template({}); + }, Exception, /"hello" not defined in/); + }); + it('should error on missing child', function() { + var template = CompilerContext.compile('{{hello.bar}}', {strict: true}); + equals(template({hello: {bar: 'foo'}}), 'foo'); + + shouldThrow(function() { + template({hello: {}}); + }, Exception, /"bar" not defined in/); + }); + it('should handle explicit undefined', function() { + var template = CompilerContext.compile('{{hello.bar}}', {strict: true}); + + equals(template({hello: {bar: undefined}}), ''); + }); + it('should error on missing property lookup in known helpers mode', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello}}', {strict: true, knownHelpersOnly: true}); + + template({}); + }, Exception, /"hello" not defined in/); + }); + it('should error on missing context', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello}}', {strict: true}); + + template(); + }, Error); + }); + + it('should error on missing data lookup', function() { + var template = CompilerContext.compile('{{@hello}}', {strict: true}); + equals(template(undefined, {data: {hello: 'foo'}}), 'foo'); + + shouldThrow(function() { + template(); + }, Error); + }); + + it('should not run helperMissing for helper calls', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello foo}}', {strict: true}); + + template({foo: true}); + }, Exception, /"hello" not defined in/); + + shouldThrow(function() { + var template = CompilerContext.compile('{{#hello foo}}{{/hello}}', {strict: true}); + + template({foo: true}); + }, Exception, /"hello" not defined in/); + }); + it('should throw on ambiguous blocks', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{#hello}}{{/hello}}', {strict: true}); + + template({}); + }, Exception, /"hello" not defined in/); + + shouldThrow(function() { + var template = CompilerContext.compile('{{^hello}}{{/hello}}', {strict: true}); + + template({}); + }, Exception, /"hello" not defined in/); + + shouldThrow(function() { + var template = CompilerContext.compile('{{#hello.bar}}{{/hello.bar}}', {strict: true}); + + template({hello: {}}); + }, Exception, /"bar" not defined in/); + }); + }); + + describe('assume objects', function() { + it('should ignore missing property', function() { + var template = CompilerContext.compile('{{hello}}', {assumeObjects: true}); + + equal(template({}), ''); + }); + it('should ignore missing child', function() { + var template = CompilerContext.compile('{{hello.bar}}', {assumeObjects: true}); + + equal(template({hello: {}}), ''); + }); + it('should error on missing object', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello.bar}}', {assumeObjects: true}); + + template({}); + }, Error); + }); + it('should error on missing context', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello}}', {assumeObjects: true}); + + template(); + }, Error); + }); + + it('should error on missing data lookup', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{@hello.bar}}', {assumeObjects: true}); + + template(); + }, Error); + }); + + it('should execute blockHelperMissing', function() { + var template = CompilerContext.compile('{{^hello}}foo{{/hello}}', {assumeObjects: true}); + + equals(template({}), 'foo'); + }); + }); +}); From 9c7641f63f61e2e42795dd6b1e377b9af887855c Mon Sep 17 00:00:00 2001 From: Dickson Wong Date: Wed, 14 Jan 2015 15:46:54 -0800 Subject: [PATCH 78/99] Fix example using #with helper Example using #with helper should close tag using {{/with}} --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 04d609701..7736b1515 100644 --- a/README.markdown +++ b/README.markdown @@ -134,7 +134,7 @@ into the person object you could still display the company's name with an expression like `{{../company.name}}`, so: ``` -{{#with person}}{{name}} - {{../company.name}}{{/person}} +{{#with person}}{{name}} - {{../company.name}}{{/with}} ``` would render: From c5fe2527b393a1cc83948bdfd86b8b3783332eef Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 17 Jan 2015 14:51:14 -0600 Subject: [PATCH 79/99] Ignore coverage dir in npm package Fixes #937 --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index 366b45408..6b8c8bfce 100644 --- a/.npmignore +++ b/.npmignore @@ -13,6 +13,7 @@ Gruntfile.js bench/* configurations/* components/* +coverage/* dist/cdnjs/* dist/components/* spec/* From c9d723e0fb18001273a076c6413c1f847812e508 Mon Sep 17 00:00:00 2001 From: Matthew Flaschen Date: Sat, 17 Jan 2015 17:08:01 -0500 Subject: [PATCH 80/99] Add missing reserved words so compiler knows to use array syntax: * await * null * true * false IE 8 was failing to compile Handlebars-generated source code because it had helpers.null. I came up with this list by diffing https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords against the ones Handlebars already had. I added two corresponding tests for nameLookup. --- lib/handlebars/compiler/javascript-compiler.js | 3 ++- spec/javascript-compiler.js | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 641daecb6..75f996071 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -1025,7 +1025,8 @@ var reservedWords = ( " class float package throws" + " const goto private transient" + " debugger implements protected volatile" + - " double import public let yield" + " double import public let yield await" + + " null true false" ).split(" "); var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; diff --git a/spec/javascript-compiler.js b/spec/javascript-compiler.js index 426056729..fb7865854 100644 --- a/spec/javascript-compiler.js +++ b/spec/javascript-compiler.js @@ -19,6 +19,12 @@ describe('javascript-compiler api', function() { }; shouldCompileTo("{{foo}}", { bar_foo: "food" }, "food"); }); + + // Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases + // to avoid errors in older browsers. + it('should handle reserved words', function() { + shouldCompileTo("{{foo}} {{~null~}}", { foo: "food" }, "food"); + }); }); describe('#compilerInfo', function() { var $superCheck, $superInfo; From 3cf47abe194219e74606994377486d7d3a033ee1 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 17 Jan 2015 17:42:02 -0600 Subject: [PATCH 81/99] Include error info when throwing type error --- spec/env/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/env/common.js b/spec/env/common.js index 62edf1d8b..9ffbc1462 100644 --- a/spec/env/common.js +++ b/spec/env/common.js @@ -45,7 +45,7 @@ global.shouldThrow = function(callback, type, msg) { failed = true; } catch (err) { if (type && !(err instanceof type)) { - throw new Error('Type failure'); + throw new Error('Type failure: ' + err); } if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) { equal(msg, err.message); From b0b522b4f81baf5ba4c190b59abd2b9cfe82bc77 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 17 Jan 2015 19:18:53 -0600 Subject: [PATCH 82/99] Add tests for partial comment whitespace control --- spec/basic.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/basic.js b/spec/basic.js index 139d6d08a..3aa10076e 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -1,4 +1,4 @@ -/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo */ +/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo, shouldThrow */ global.handlebarsEnv = null; beforeEach(function() { @@ -36,6 +36,10 @@ describe("basic context", function() { shouldCompileTo(' {{~! comment ~}} blah', {}, 'blah'); shouldCompileTo(' {{~!-- long-comment --~}} blah', {}, 'blah'); + shouldCompileTo(' {{! comment ~}} blah', {}, ' blah'); + shouldCompileTo(' {{!-- long-comment --~}} blah', {}, ' blah'); + shouldCompileTo(' {{~! comment}} blah', {}, ' blah'); + shouldCompileTo(' {{~!-- long-comment --}} blah', {}, ' blah'); }); it("boolean", function() { @@ -140,12 +144,12 @@ describe("basic context", function() { }); it("pathed block functions without context argument", function() { shouldCompileTo("{{#foo.awesome}}inner{{/foo.awesome}}", - {foo: {awesome: function(options) { return this; }}}, + {foo: {awesome: function() { return this; }}}, "inner", "block functions are called with options"); }); it("depthed block functions without context argument", function() { shouldCompileTo("{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}", - {value: true, awesome: function(options) { return this; }}, + {value: true, awesome: function() { return this; }}, "inner", "block functions are called with options"); }); From cb51b82b8e8114aaa3e1b19c41e5c4eebf0539d5 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 18 Jan 2015 13:23:45 -0600 Subject: [PATCH 83/99] Add support for dynamic partial names Uses the subexpression syntax to allow for dynamic partial lookups. Ex: ``` {{> (helper) }} ``` Fixes #933 --- docs/compiler-api.md | 13 ++++++++++ lib/handlebars/compiler/ast.js | 9 +++++++ lib/handlebars/compiler/compiler.js | 10 +++++-- .../compiler/javascript-compiler.js | 13 ++++++++-- lib/handlebars/compiler/printer.js | 2 +- lib/handlebars/compiler/visitor.js | 5 ++++ lib/handlebars/runtime.js | 15 ++++++++--- spec/partials.js | 26 +++++++++++++++++++ src/handlebars.yy | 8 +++++- 9 files changed, 92 insertions(+), 9 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 74af67246..4a0ec0149 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -115,6 +115,17 @@ interface SubExpression <: Expression { `isHelper` is not required and is used to disambiguate between cases such as `{{foo}}` and `(foo)`, which have slightly different call behaviors. +```java +interface PartialExpression <: Expression { + type: "PartialExpression"; + name: PathExpression | SubExpression; + params: [ Expression ]; + hash: Hash; +} +``` + +`path` may be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}` + ##### Paths ```java @@ -221,6 +232,8 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c - `name` is the current path component - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`. + Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases. + - `depthedLookup(name)` Used to generate code that resolves parameters within any context in the stack. Is only used in `compat` mode. diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 113c03e79..c193460fc 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -63,6 +63,15 @@ var AST = { this.hash = hash; }, + PartialExpression: function(name, params, hash, locInfo) { + this.loc = locInfo; + + this.type = 'PartialExpression'; + this.name = name; + this.params = params || []; + this.hash = hash; + }, + PathExpression: function(data, depth, parts, original, locInfo) { this.loc = locInfo; this.type = 'PathExpression'; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 95fe101b8..524f9aa2f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -145,7 +145,6 @@ Compiler.prototype = { }, PartialStatement: function(partial) { - var partialName = partial.sexpr.path.original; this.usePartial = true; var params = partial.sexpr.params; @@ -155,6 +154,12 @@ Compiler.prototype = { params.push({type: 'PathExpression', parts: [], depth: 0}); } + var partialName = partial.sexpr.name.original, + isDynamic = partial.sexpr.name.type === 'SubExpression'; + if (isDynamic) { + this.accept(partial.sexpr.name); + } + this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true); var indent = partial.indent || ''; @@ -162,7 +167,8 @@ Compiler.prototype = { this.opcode('appendContent', indent); indent = ''; } - this.opcode('invokePartial', partialName, indent); + + this.opcode('invokePartial', isDynamic, partialName, indent); this.opcode('append'); }, diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 75f996071..a027edb91 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -644,17 +644,26 @@ JavaScriptCompiler.prototype = { // // This operation pops off a context, invokes a partial with that context, // and pushes the result of the invocation back. - invokePartial: function(name, indent) { + invokePartial: function(isDynamic, name, indent) { var params = [], options = this.setupParams(name, 1, params, false); + if (isDynamic) { + name = this.popStack(); + delete options.name; + } + if (indent) { options.indent = JSON.stringify(indent); } options.helpers = 'helpers'; options.partials = 'partials'; - params.unshift(this.nameLookup('partials', name, 'partial')); + if (!isDynamic) { + params.unshift(this.nameLookup('partials', name, 'partial')); + } else { + params.unshift(name); + } if (this.options.compat) { options.depths = 'depths'; diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index ad276f19a..448331c03 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -75,7 +75,7 @@ PrintVisitor.prototype.BlockStatement = function(block) { PrintVisitor.prototype.PartialStatement = function(partial) { var sexpr = partial.sexpr, - content = 'PARTIAL:' + sexpr.path.original; + content = 'PARTIAL:' + sexpr.name.original; if(sexpr.params[0]) { content += ' ' + this.accept(sexpr.params[0]); } diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 3fb37fbfc..03af9153e 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -92,6 +92,11 @@ Visitor.prototype = { this.acceptArray(sexpr.params); this.acceptKey(sexpr, 'hash'); }, + PartialExpression: function(partial) { + this.acceptRequired(partial, 'name'); + this.acceptArray(partial.params); + this.acceptKey(partial, 'hash'); + }, PathExpression: function(/* path */) {}, diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 2b9f47414..4e8c33ae8 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -39,10 +39,8 @@ export function template(templateSpec, env) { if (options.hash) { context = Utils.extend({}, context, options.hash); } - if (!partial) { - partial = options.partials[options.name]; - } + partial = env.VM.resolvePartial.call(this, partial, context, options); var result = env.VM.invokePartial.call(this, partial, context, options); if (result == null && env.compile) { @@ -187,6 +185,17 @@ export function program(container, i, fn, data, declaredBlockParams, blockParams return prog; } +export function resolvePartial(partial, context, options) { + if (!partial) { + partial = options.partials[options.name]; + } else if (!partial.call && !options.name) { + // This is a dynamic partial that returned a string + options.name = partial; + partial = options.partials[partial]; + } + return partial; +} + export function invokePartial(partial, context, options) { options.partial = true; diff --git a/spec/partials.js b/spec/partials.js index b1509423a..0c9e0f6dc 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -8,6 +8,32 @@ describe('partials', function() { shouldCompileToWithPartials(string, [hash, {}, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); }); + it('dynamic partials', function() { + var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + var helpers = { + partial: function() { + return 'dude'; + } + }; + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + }); + it('failing dynamic partials', function() { + var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + var helpers = { + partial: function() { + return 'missing'; + } + }; + shouldThrow(function() { + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + }, Handlebars.Exception, 'The partial missing could not be found'); + }); + it("partials with context", function() { var string = "Dudes: {{>dude dudes}}"; var partial = "{{#this}}{{name}} ({{url}}) {{/this}}"; diff --git a/src/handlebars.yy b/src/handlebars.yy index 27985b8d0..6ca32d942 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -77,7 +77,13 @@ mustache ; partial - : OPEN_PARTIAL sexpr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_PARTIAL partial_expr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$)) + ; + +partial_expr + : helperName param* hash? -> new yy.PartialExpression($1, $2, $3, yy.locInfo(@$)) + | dataName -> new yy.PartialExpression($1, null, null, yy.locInfo(@$)) + | OPEN_SEXPR sexpr CLOSE_SEXPR param* hash? -> new yy.PartialExpression($2, $4, $5, yy.locInfo(@$)) ; sexpr From d567d9c34566a6adab1eaea40ade2729fbce8144 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 18 Jan 2015 13:29:15 -0600 Subject: [PATCH 84/99] Update PartialExpression AST docs --- docs/compiler-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 4a0ec0149..7a5db380e 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -118,13 +118,13 @@ interface SubExpression <: Expression { ```java interface PartialExpression <: Expression { type: "PartialExpression"; - name: PathExpression | SubExpression; + name: PathExpression | Literal | SubExpression; params: [ Expression ]; hash: Hash; } ``` -`path` may be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}` +`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial. ##### Paths From 999da739a66199483ffc4d82426550aee5ac798f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 18 Jan 2015 14:21:04 -0600 Subject: [PATCH 85/99] Update for proposed SubExpression dependency break Avoids parsing against SubExpressions and instead inlines the content that a subexpression otherwise would have. This can still be based via duck typing so should not add much overhead to the compiler. --- docs/compiler-api.md | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 7a5db380e..39bcacdaf 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -55,15 +55,21 @@ interface Statement <: Node { } interface MustacheStatement <: Statement { type: "MustacheStatement"; - sexpr: SubExpression; - escaped: boolean; + path: PathExpression; + params: [ Expression ]; + hash: Hash; + + escaped: boolean; strip: StripFlags | null; } interface BlockStatement <: Statement { type: "BlockStatement"; - sexpr: SubExpression; + path: PathExpression; + params: [ Expression ]; + hash: Hash; + program: Program | null; inverse: Program | null; @@ -74,12 +80,19 @@ interface BlockStatement <: Statement { interface PartialStatement <: Statement { type: "PartialStatement"; - sexpr: SubExpression; + name: PathExpression | SubExpression; + params: [ Expression ]; + hash: Hash; indent: string; strip: StripFlags | null; } +``` +`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial. + + +```java interface ContentStatement <: Statement { type: "ContentStatement"; value: string; @@ -108,24 +121,9 @@ interface SubExpression <: Expression { path: PathExpression; params: [ Expression ]; hash: Hash; - - isHelper: true | null; } ``` -`isHelper` is not required and is used to disambiguate between cases such as `{{foo}}` and `(foo)`, which have slightly different call behaviors. - -```java -interface PartialExpression <: Expression { - type: "PartialExpression"; - name: PathExpression | Literal | SubExpression; - params: [ Expression ]; - hash: Hash; -} -``` - -`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial. - ##### Paths ```java From 884bf1553663734f22ffcd9d758c9d71d4373bf9 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 18 Jan 2015 17:27:27 -0600 Subject: [PATCH 86/99] Avoid direct references to sexpr in statements This allows us to avoid creating unnecessary AST nodes and avoids things like isHelper. Side effect of these changes is that @data functions can now have data parameters passed to them. --- docs/compiler-api.md | 2 +- lib/handlebars/compiler/ast.js | 36 +++++++++++++--------------- lib/handlebars/compiler/compiler.js | 30 ++++++++++------------- lib/handlebars/compiler/helpers.js | 22 +++++++++-------- lib/handlebars/compiler/printer.js | 15 ++++++------ lib/handlebars/compiler/visitor.js | 13 +++++++--- spec/ast.js | 10 ++++---- spec/data.js | 11 +++++++++ spec/visitor.js | 15 ++++++------ src/handlebars.yy | 37 ++++++++++++++--------------- 10 files changed, 100 insertions(+), 91 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 39bcacdaf..3593228bc 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -204,7 +204,7 @@ function ImportScanner() { ImportScanner.prototype = new Visitor(); ImportScanner.prototype.PartialStatement = function(partial) { - this.partials.push({request: partial.sexpr.original}); + this.partials.push({request: partial.name.original}); Visitor.prototype.PartialStatement.call(this, partial); }; diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index c193460fc..45212b383 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -8,21 +8,25 @@ var AST = { this.strip = strip; }, - MustacheStatement: function(sexpr, escaped, strip, locInfo) { + MustacheStatement: function(path, params, hash, escaped, strip, locInfo) { this.loc = locInfo; this.type = 'MustacheStatement'; - this.sexpr = sexpr; + this.path = path; + this.params = params || []; + this.hash = hash; this.escaped = escaped; this.strip = strip; }, - BlockStatement: function(sexpr, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { + BlockStatement: function(path, params, hash, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { this.loc = locInfo; - this.type = 'BlockStatement'; - this.sexpr = sexpr; + + this.path = path; + this.params = params || []; + this.hash = hash; this.program = program; this.inverse = inverse; @@ -31,12 +35,15 @@ var AST = { this.closeStrip = closeStrip; }, - PartialStatement: function(sexpr, strip, locInfo) { + PartialStatement: function(name, params, hash, strip, locInfo) { this.loc = locInfo; this.type = 'PartialStatement'; - this.sexpr = sexpr; - this.indent = ''; + this.name = name; + this.params = params || []; + this.hash = hash; + + this.indent = ''; this.strip = strip; }, @@ -63,15 +70,6 @@ var AST = { this.hash = hash; }, - PartialExpression: function(name, params, hash, locInfo) { - this.loc = locInfo; - - this.type = 'PartialExpression'; - this.name = name; - this.params = params || []; - this.hash = hash; - }, - PathExpression: function(data, depth, parts, original, locInfo) { this.loc = locInfo; this.type = 'PathExpression'; @@ -121,8 +119,8 @@ var AST = { // * it is an eligible helper, and // * it has at least one parameter or hash segment // TODO: Make these public utility methods - helperExpression: function(sexpr) { - return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash); + helperExpression: function(node) { + return !!(node.type === 'SubExpression' || node.params.length || node.hash); }, scopedId: function(path) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 524f9aa2f..03bf8f430 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -110,28 +110,27 @@ Compiler.prototype = { }, BlockStatement: function(block) { - var sexpr = block.sexpr, - program = block.program, + var program = block.program, inverse = block.inverse; program = program && this.compileProgram(program); inverse = inverse && this.compileProgram(inverse); - var type = this.classifySexpr(sexpr); + var type = this.classifySexpr(block); if (type === 'helper') { - this.helperSexpr(sexpr, program, inverse); + this.helperSexpr(block, program, inverse); } else if (type === 'simple') { - this.simpleSexpr(sexpr); + this.simpleSexpr(block); // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` this.opcode('pushProgram', program); this.opcode('pushProgram', inverse); this.opcode('emptyHash'); - this.opcode('blockValue', sexpr.path.original); + this.opcode('blockValue', block.path.original); } else { - this.ambiguousSexpr(sexpr, program, inverse); + this.ambiguousSexpr(block, program, inverse); // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` @@ -147,20 +146,20 @@ Compiler.prototype = { PartialStatement: function(partial) { this.usePartial = true; - var params = partial.sexpr.params; + var params = partial.params; if (params.length > 1) { throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); } else if (!params.length) { params.push({type: 'PathExpression', parts: [], depth: 0}); } - var partialName = partial.sexpr.name.original, - isDynamic = partial.sexpr.name.type === 'SubExpression'; + var partialName = partial.name.original, + isDynamic = partial.name.type === 'SubExpression'; if (isDynamic) { - this.accept(partial.sexpr.name); + this.accept(partial.name); } - this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true); + this.setupFullMustacheParams(partial, undefined, undefined, true); var indent = partial.indent || ''; if (this.options.preventIndent && indent) { @@ -173,7 +172,7 @@ Compiler.prototype = { }, MustacheStatement: function(mustache) { - this.accept(mustache.sexpr); + this.SubExpression(mustache); if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped'); @@ -340,11 +339,6 @@ Compiler.prototype = { pushParam: function(val) { var value = val.value != null ? val.value : val.original || ''; - // Force helper evaluation - if (val.type === 'SubExpression') { - val.isHelper = true; - } - if (this.stringParams) { if (value.replace) { value = value diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 6cfc0e076..beaf98869 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -52,28 +52,29 @@ export function preparePath(data, parts, locInfo) { return new this.PathExpression(data, depth, dig, original, locInfo); } -export function prepareMustache(sexpr, open, strip, locInfo) { +export function prepareMustache(path, params, hash, open, strip, locInfo) { /*jshint -W040 */ // Must use charAt to support IE pre-10 var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== '{' && escapeFlag !== '&'; - return new this.MustacheStatement(sexpr, escaped, strip, this.locInfo(locInfo)); + return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo)); } export function prepareRawBlock(openRawBlock, content, close, locInfo) { /*jshint -W040 */ - if (openRawBlock.sexpr.path.original !== close) { - var errorNode = {loc: openRawBlock.sexpr.loc}; + if (openRawBlock.path.original !== close) { + var errorNode = {loc: openRawBlock.path.loc}; - throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode); + throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode); } locInfo = this.locInfo(locInfo); var program = new this.Program([content], null, {}, locInfo); return new this.BlockStatement( - openRawBlock.sexpr, program, undefined, + openRawBlock.path, openRawBlock.params, openRawBlock.hash, + program, undefined, {}, {}, {}, locInfo); } @@ -81,10 +82,10 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { /*jshint -W040 */ // When we are chaining inverse calls, we will not have a close path - if (close && close.path && openBlock.sexpr.path.original !== close.path.original) { - var errorNode = {loc: openBlock.sexpr.loc}; + if (close && close.path && openBlock.path.original !== close.path.original) { + var errorNode = {loc: openBlock.path.loc}; - throw new Exception(openBlock.sexpr.path.original + ' doesn\'t match ' + close.path.original, errorNode); + throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode); } program.blockParams = openBlock.blockParams; @@ -108,7 +109,8 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver } return new this.BlockStatement( - openBlock.sexpr, program, inverse, + openBlock.path, openBlock.params, openBlock.hash, + program, inverse, openBlock.strip, inverseStrip, close && close.strip, this.locInfo(locInfo)); } diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 448331c03..55232cc45 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -45,7 +45,7 @@ PrintVisitor.prototype.Program = function(program) { }; PrintVisitor.prototype.MustacheStatement = function(mustache) { - return this.pad('{{ ' + this.accept(mustache.sexpr) + ' }}'); + return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); }; PrintVisitor.prototype.BlockStatement = function(block) { @@ -53,7 +53,7 @@ PrintVisitor.prototype.BlockStatement = function(block) { out = out + this.pad('BLOCK:'); this.padding++; - out = out + this.pad(this.accept(block.sexpr)); + out = out + this.pad(this.SubExpression(block)); if (block.program) { out = out + this.pad('PROGRAM:'); this.padding++; @@ -74,13 +74,12 @@ PrintVisitor.prototype.BlockStatement = function(block) { }; PrintVisitor.prototype.PartialStatement = function(partial) { - var sexpr = partial.sexpr, - content = 'PARTIAL:' + sexpr.name.original; - if(sexpr.params[0]) { - content += ' ' + this.accept(sexpr.params[0]); + var content = 'PARTIAL:' + partial.name.original; + if(partial.params[0]) { + content += ' ' + this.accept(partial.params[0]); } - if (sexpr.hash) { - content += ' ' + this.accept(sexpr.hash); + if (partial.hash) { + content += ' ' + this.accept(partial.hash); } return this.pad('{{> ' + content + ' }}'); }; diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 03af9153e..4101a4f04 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -71,17 +71,24 @@ Visitor.prototype = { }, MustacheStatement: function(mustache) { - this.acceptRequired(mustache, 'sexpr'); + this.acceptRequired(mustache, 'path'); + this.acceptArray(mustache.params); + this.acceptKey(mustache, 'hash'); }, BlockStatement: function(block) { - this.acceptRequired(block, 'sexpr'); + this.acceptRequired(block, 'path'); + this.acceptArray(block.params); + this.acceptKey(block, 'hash'); + this.acceptKey(block, 'program'); this.acceptKey(block, 'inverse'); }, PartialStatement: function(partial) { - this.acceptRequired(partial, 'sexpr'); + this.acceptRequired(partial, 'name'); + this.acceptArray(partial.params); + this.acceptKey(partial, 'hash'); }, ContentStatement: function(/* content */) {}, diff --git a/spec/ast.js b/spec/ast.js index d464cf110..3b2a4ae20 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -26,7 +26,7 @@ describe('ast', function() { it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheStatement({}, true, {}, LOCATION_INFO); + mustache = new handlebarsEnv.AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO); equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); testLocationInfoStorage(mustache); @@ -40,10 +40,10 @@ describe('ast', function() { }); it('stores location info', function(){ - var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null); - var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, false, {}); + var mustacheNode = new handlebarsEnv.AST.MustacheStatement([{ original: 'foo'}], null, null, false, {}); var block = new handlebarsEnv.AST.BlockStatement( mustacheNode, + null, null, {body: []}, {body: []}, {}, @@ -104,7 +104,7 @@ describe('ast', function() { describe('PartialStatement', function(){ it('stores location info', function(){ - var pn = new handlebarsEnv.AST.PartialStatement('so_partial', {}, LOCATION_INFO); + var pn = new handlebarsEnv.AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); @@ -200,7 +200,7 @@ describe('ast', function() { block = ast.body[0]; equals(block.program.body[0].value, ''); - equals(block.program.body[1].sexpr.path.original, 'foo'); + equals(block.program.body[1].path.original, 'foo'); equals(block.program.body[2].value, '\n'); }); it('marks nested block mustaches as standalone', function() { diff --git a/spec/data.js b/spec/data.js index bb90df5a8..1678eea85 100644 --- a/spec/data.js +++ b/spec/data.js @@ -93,6 +93,17 @@ describe('data', function() { }, Error); }); + it('data can be functions', function() { + var template = CompilerContext.compile('{{@hello}}'); + var result = template({}, { data: { hello: function() { return 'hello'; } } }); + equals('hello', result); + }); + it('data can be functions with params', function() { + var template = CompilerContext.compile('{{@hello "hello"}}'); + var result = template({}, { data: { hello: function(arg) { return arg; } } }); + equals('hello', result); + }); + it("data is inherited downstream", function() { var template = CompilerContext.compile("{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}", { data: true }); var helpers = { diff --git a/spec/visitor.js b/spec/visitor.js index 0c23c0d64..32172309c 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -24,11 +24,10 @@ describe('Visitor', function() { visitor.BooleanLiteral = function(bool) { equal(bool.value, true); - equal(this.parents.length, 4); + equal(this.parents.length, 3); equal(this.parents[0].type, 'SubExpression'); - equal(this.parents[1].type, 'SubExpression'); - equal(this.parents[2].type, 'BlockStatement'); - equal(this.parents[3].type, 'Program'); + equal(this.parents[1].type, 'BlockStatement'); + equal(this.parents[2].type, 'Program'); }; visitor.PathExpression = function(id) { equal(/(foo\.)?bar$/.test(id.original), true); @@ -84,26 +83,26 @@ describe('Visitor', function() { var visitor = new Handlebars.Visitor(); visitor.mutating = true; - visitor.SubExpression = function() { + visitor.PathExpression = function() { return false; }; var ast = Handlebars.parse('{{foo 42}}'); visitor.accept(ast); - }, Handlebars.Exception, 'MustacheStatement requires sexpr'); + }, Handlebars.Exception, 'MustacheStatement requires path'); }); it('should throw when returning non-node responses', function() { shouldThrow(function() { var visitor = new Handlebars.Visitor(); visitor.mutating = true; - visitor.SubExpression = function() { + visitor.PathExpression = function() { return {}; }; var ast = Handlebars.parse('{{foo 42}}'); visitor.accept(ast); - }, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting sexpr on MustacheStatement'); + }, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting path on MustacheStatement'); }); }); describe('arrays', function() { diff --git a/src/handlebars.yy b/src/handlebars.yy index 6ca32d942..39f802757 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -30,7 +30,7 @@ rawBlock ; openRawBlock - : OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> { sexpr: $2 } + : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK -> { path: $2, params: $3, hash: $4 } ; block @@ -39,15 +39,15 @@ block ; openBlock - : OPEN_BLOCK sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } + : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; openInverse - : OPEN_INVERSE sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } + : OPEN_INVERSE helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } + : OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; inverseAndProgram @@ -72,23 +72,12 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) + : OPEN helperName param* hash? CLOSE -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) + | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) ; partial - : OPEN_PARTIAL partial_expr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$)) - ; - -partial_expr - : helperName param* hash? -> new yy.PartialExpression($1, $2, $3, yy.locInfo(@$)) - | dataName -> new yy.PartialExpression($1, null, null, yy.locInfo(@$)) - | OPEN_SEXPR sexpr CLOSE_SEXPR param* hash? -> new yy.PartialExpression($2, $4, $5, yy.locInfo(@$)) - ; - -sexpr - : helperName param* hash? -> new yy.SubExpression($1, $2, $3, yy.locInfo(@$)) - | dataName -> new yy.SubExpression($1, null, null, yy.locInfo(@$)) + : OPEN_PARTIAL partialName param* hash? CLOSE -> new yy.PartialStatement($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) ; param @@ -97,7 +86,11 @@ param | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) | dataName -> $1 - | OPEN_SEXPR sexpr CLOSE_SEXPR -> $2 + | sexpr -> $1 + ; + +sexpr + : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR -> new yy.SubExpression($2, $3, $4, yy.locInfo(@$)) ; hash @@ -114,10 +107,16 @@ blockParams helperName : path -> $1 + | dataName -> $1 | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)), yy.locInfo(@$) | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) ; +partialName + : helperName -> $1 + | sexpr -> $1 + ; + dataName : DATA pathSegments -> yy.preparePath(true, $2, @$) ; From 8626fa8bbee0ef0ec03445c6b937bc10b5060851 Mon Sep 17 00:00:00 2001 From: Oge Nnadi Date: Tue, 20 Jan 2015 10:59:38 -0800 Subject: [PATCH 87/99] Properly formatted dot --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 7736b1515..d16575431 100644 --- a/README.markdown +++ b/README.markdown @@ -110,7 +110,7 @@ templates easier and also changes a tiny detail of how partials work. ### Paths Handlebars.js supports an extended expression syntax that we call paths. -Paths are made up of typical expressions and . characters. Expressions +Paths are made up of typical expressions and `.` characters. Expressions allow you to not only display data from the current context, but to display data from contexts that are descendants and ancestors of the current context. From 1f6a94424dda67295407ea50f26ddb2bbf9cfc07 Mon Sep 17 00:00:00 2001 From: Oge Nnadi Date: Tue, 20 Jan 2015 11:01:11 -0800 Subject: [PATCH 88/99] Clarified a long sentence --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 7736b1515..e504df905 100644 --- a/README.markdown +++ b/README.markdown @@ -197,7 +197,7 @@ template(data); ``` Whenever the block helper is called it is given one or more parameters, -any arguments that are passed in the helper in the call and an `options` +any arguments that are passed into the helper in the call, and an `options` object containing the `fn` function which executes the block's child. The block's current context may be accessed through `this`. From 95421271e3badfcb340cda53434109feed44889a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 7 Feb 2015 12:27:02 -0600 Subject: [PATCH 89/99] Change default log level to info Eventually we want to fix this API so that you can provide per-instance levels, etc but for now this is a quick fix. Fixes wycats/handlebars-site#91 --- lib/handlebars/base.js | 2 +- spec/builtins.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index d75e965cb..f8ac50acf 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -221,7 +221,7 @@ export var logger = { INFO: 1, WARN: 2, ERROR: 3, - level: 3, + level: 1, // Can be overridden in the host environment log: function(level, message) { diff --git a/spec/builtins.js b/spec/builtins.js index e6af46bd7..8f20da1c9 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -276,15 +276,17 @@ describe('builtin helpers', function() { equals(3, levelArg); equals("whee", logArg); }); - it('should not output to console', function() { + it('should not output to info', function() { var string = "{{log blah}}"; var hash = { blah: "whee" }; - console.info = function() { - throw new Error(); + console.info = function(log) { + equals("whee", log); + called = true; }; - shouldCompileTo(string, hash, "", "log should not display"); + shouldCompileTo(string, hash, ""); + equals(true, called); }); it('should log at data level', function() { var string = "{{log blah}}"; From cfbef2585df36a92d2bcac0a025c39aea37fedb1 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 7 Feb 2015 13:22:31 -0600 Subject: [PATCH 90/99] Fail over to console.log if lacking console method This improves logger resiliency under older browsers. --- lib/handlebars/base.js | 6 ++---- spec/builtins.js | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index f8ac50acf..20c49aec1 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -225,11 +225,9 @@ export var logger = { // Can be overridden in the host environment log: function(level, message) { - if (logger.level <= level) { + if (typeof console !== 'undefined' && logger.level <= level) { var method = logger.methodMap[level]; - if (typeof console !== 'undefined' && console[method]) { - console[method].call(console, message); - } + (console[method] || console.log).call(console, message); } } }; diff --git a/spec/builtins.js b/spec/builtins.js index 8f20da1c9..75af23646 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -276,7 +276,7 @@ describe('builtin helpers', function() { equals(3, levelArg); equals("whee", logArg); }); - it('should not output to info', function() { + it('should output to info', function() { var string = "{{log blah}}"; var hash = { blah: "whee" }; @@ -284,6 +284,10 @@ describe('builtin helpers', function() { equals("whee", log); called = true; }; + console.log = function(log) { + equals("whee", log); + called = true; + }; shouldCompileTo(string, hash, ""); equals(true, called); From 810d86d474f2b288c3a43cb864bd867ab4625717 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 7 Feb 2015 13:28:27 -0600 Subject: [PATCH 91/99] Fix global leak in test --- spec/builtins.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/builtins.js b/spec/builtins.js index 75af23646..14cd8faa5 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -279,6 +279,7 @@ describe('builtin helpers', function() { it('should output to info', function() { var string = "{{log blah}}"; var hash = { blah: "whee" }; + var called; console.info = function(log) { equals("whee", log); From fd8484798c9bb001a5bcaf205252779ac24d5c23 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 7 Feb 2015 14:31:54 -0600 Subject: [PATCH 92/99] Fix console.log cleanup --- spec/builtins.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/builtins.js b/spec/builtins.js index 14cd8faa5..f3b4baaba 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -237,13 +237,16 @@ describe('builtin helpers', function() { return; } - var info, + var log, + info, error; beforeEach(function() { + log = console.log; info = console.info; error = console.error; }); afterEach(function() { + console.log = log; console.info = info; console.error = error; }); From f857471cc8dd8dca301cd78bfef03bd774b5d4c2 Mon Sep 17 00:00:00 2001 From: Marcio Junior Date: Sat, 7 Feb 2015 02:48:52 -0200 Subject: [PATCH 93/99] Boolean literals can be passed to mustaches --- spec/parser.js | 4 ++++ src/handlebars.yy | 1 + 2 files changed, 5 insertions(+) diff --git a/spec/parser.js b/spec/parser.js index 73bff27d6..3d3dccff3 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -10,6 +10,10 @@ describe('parser', function() { } it('parses simple mustaches', function() { + equals(ast_for('{{123}}'), "{{ NUMBER{123} [] }}\n"); + equals(ast_for('{{"foo"}}'), '{{ "foo" [] }}\n'); + equals(ast_for('{{false}}'), '{{ BOOLEAN{false} [] }}\n'); + equals(ast_for('{{true}}'), '{{ BOOLEAN{true} [] }}\n'); equals(ast_for('{{foo}}'), "{{ PATH:foo [] }}\n"); equals(ast_for('{{foo?}}'), "{{ PATH:foo? [] }}\n"); equals(ast_for('{{foo_}}'), "{{ PATH:foo_ [] }}\n"); diff --git a/src/handlebars.yy b/src/handlebars.yy index 39f802757..7de04dd83 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -110,6 +110,7 @@ helperName | dataName -> $1 | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)), yy.locInfo(@$) | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) + | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) ; partialName From 5cc326d42554da8a00c69d8a8c6a71e3e6a61371 Mon Sep 17 00:00:00 2001 From: Marcio Junior Date: Sat, 7 Feb 2015 18:23:03 -0200 Subject: [PATCH 94/99] Test compilation of literal values in mustaches --- lib/handlebars/compiler/compiler.js | 6 +++++- spec/basic.js | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 03bf8f430..75b2c1b10 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -172,7 +172,11 @@ Compiler.prototype = { }, MustacheStatement: function(mustache) { - this.SubExpression(mustache); + if (!mustache.path.type.match(/Literal$/)) { + this.SubExpression(mustache); + } else { + this.accept(mustache.path); + } if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped'); diff --git a/spec/basic.js b/spec/basic.js index 3aa10076e..e19808fa3 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -229,4 +229,20 @@ describe("basic context", function() { CompilerContext.compile(string); }, Error); }); + + it("pass string literals", function() { + shouldCompileTo('{{"foo"}}', {}, "foo", "works with strings"); + shouldCompileTo('{{"foo"}}', { foo: "bar" }, "foo", "uses the provided string instead of lookup in the context object"); + }); + + it("pass number literals", function() { + shouldCompileTo("{{12}}", {}, "12", "works with numbers"); + shouldCompileTo("{{12}}", { "12": "bar" }, "12", "uses the provided number instead of lookup in the context object"); + }); + + it("pass boolean literals", function() { + shouldCompileTo("{{true}}", {}, "true", "works with true"); + shouldCompileTo("{{true}}", { "true": "foo" }, "true", "uses the primitive true instead of lookup in the context object"); + shouldCompileTo("{{false}}", { "false": "foo" }, "false", "uses the primitive false instead of lookup in the context object"); + }); }); From 07f27843dc01058103d084c18a5db297b150d9cb Mon Sep 17 00:00:00 2001 From: Marcio Junior Date: Sun, 8 Feb 2015 19:31:27 -0200 Subject: [PATCH 95/99] Transform literals to path expressions in mustache nodes --- lib/handlebars/compiler/compiler.js | 14 +++++++++----- spec/basic.js | 16 +++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 75b2c1b10..605a54bda 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -172,11 +172,8 @@ Compiler.prototype = { }, MustacheStatement: function(mustache) { - if (!mustache.path.type.match(/Literal$/)) { - this.SubExpression(mustache); - } else { - this.accept(mustache.path); - } + transformLiteralToPath(mustache); + this.SubExpression(mustache); if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped'); @@ -489,3 +486,10 @@ function argEquals(a, b) { return true; } } + +function transformLiteralToPath(mustache) { + if (mustache.path.type.match(/Literal$/)) { + var literal = mustache.path; + mustache.path = { type: 'PathExpression', original: String(literal.original), parts: [String(literal.original)], depth: 0, data: false }; + } +} diff --git a/spec/basic.js b/spec/basic.js index e19808fa3..765e41dcc 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -231,18 +231,20 @@ describe("basic context", function() { }); it("pass string literals", function() { - shouldCompileTo('{{"foo"}}', {}, "foo", "works with strings"); - shouldCompileTo('{{"foo"}}', { foo: "bar" }, "foo", "uses the provided string instead of lookup in the context object"); + shouldCompileTo('{{"foo"}}', {}, ""); + shouldCompileTo('{{"foo"}}', { foo: "bar" }, "bar"); }); it("pass number literals", function() { - shouldCompileTo("{{12}}", {}, "12", "works with numbers"); - shouldCompileTo("{{12}}", { "12": "bar" }, "12", "uses the provided number instead of lookup in the context object"); + shouldCompileTo("{{12}}", {}, ""); + shouldCompileTo("{{12}}", { "12": "bar" }, "bar"); + shouldCompileTo("{{12.34}}", {}, ""); + shouldCompileTo("{{12.34}}", { "12.34": "bar" }, "bar"); }); it("pass boolean literals", function() { - shouldCompileTo("{{true}}", {}, "true", "works with true"); - shouldCompileTo("{{true}}", { "true": "foo" }, "true", "uses the primitive true instead of lookup in the context object"); - shouldCompileTo("{{false}}", { "false": "foo" }, "false", "uses the primitive false instead of lookup in the context object"); + shouldCompileTo("{{true}}", {}, ""); + shouldCompileTo("{{true}}", { "": "foo" }, ""); + shouldCompileTo("{{false}}", { "false": "foo" }, "foo"); }); }); From 39121cf8f50ec02b5a979f4911caefef8030161a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 9 Feb 2015 23:54:46 -0600 Subject: [PATCH 96/99] Handle all potential literal values Adds support for literal helper names in a few missing cases such as block expressions and subexpressions. --- lib/handlebars/compiler/compiler.js | 14 +++++++----- spec/basic.js | 35 +++++++++++++++++++---------- src/handlebars.yy | 10 +++------ 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 605a54bda..21de99ca4 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -110,6 +110,8 @@ Compiler.prototype = { }, BlockStatement: function(block) { + transformLiteralToPath(block); + var program = block.program, inverse = block.inverse; @@ -172,7 +174,6 @@ Compiler.prototype = { }, MustacheStatement: function(mustache) { - transformLiteralToPath(mustache); this.SubExpression(mustache); if(mustache.escaped && !this.options.noEscape) { @@ -191,6 +192,7 @@ Compiler.prototype = { CommentStatement: function() {}, SubExpression: function(sexpr) { + transformLiteralToPath(sexpr); var type = this.classifySexpr(sexpr); if (type === 'simple') { @@ -487,9 +489,11 @@ function argEquals(a, b) { } } -function transformLiteralToPath(mustache) { - if (mustache.path.type.match(/Literal$/)) { - var literal = mustache.path; - mustache.path = { type: 'PathExpression', original: String(literal.original), parts: [String(literal.original)], depth: 0, data: false }; +function transformLiteralToPath(sexpr) { + if (!sexpr.path.parts) { + var literal = sexpr.path; + // Casting to string here to make false and 0 literal values play nicely with the rest + // of the system. + sexpr.path = new AST.PathExpression(false, 0, [literal.original+''], literal.original+'', literal.log); } } diff --git a/spec/basic.js b/spec/basic.js index 765e41dcc..9b6678a5a 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -230,21 +230,32 @@ describe("basic context", function() { }, Error); }); - it("pass string literals", function() { - shouldCompileTo('{{"foo"}}', {}, ""); - shouldCompileTo('{{"foo"}}', { foo: "bar" }, "bar"); + it('pass string literals', function() { + shouldCompileTo('{{"foo"}}', {}, ''); + shouldCompileTo('{{"foo"}}', { foo: 'bar' }, 'bar'); + shouldCompileTo('{{#"foo"}}{{.}}{{/"foo"}}', { foo: ['bar', 'baz'] }, 'barbaz'); }); - it("pass number literals", function() { - shouldCompileTo("{{12}}", {}, ""); - shouldCompileTo("{{12}}", { "12": "bar" }, "bar"); - shouldCompileTo("{{12.34}}", {}, ""); - shouldCompileTo("{{12.34}}", { "12.34": "bar" }, "bar"); + it('pass number literals', function() { + shouldCompileTo('{{12}}', {}, ''); + shouldCompileTo('{{12}}', { '12': 'bar' }, 'bar'); + shouldCompileTo('{{12.34}}', {}, ''); + shouldCompileTo('{{12.34}}', { '12.34': 'bar' }, 'bar'); + shouldCompileTo('{{12.34 1}}', { '12.34': function(arg) { return 'bar' + arg; } }, 'bar1'); }); - it("pass boolean literals", function() { - shouldCompileTo("{{true}}", {}, ""); - shouldCompileTo("{{true}}", { "": "foo" }, ""); - shouldCompileTo("{{false}}", { "false": "foo" }, "foo"); + it('pass boolean literals', function() { + shouldCompileTo('{{true}}', {}, ''); + shouldCompileTo('{{true}}', { '': 'foo' }, ''); + shouldCompileTo('{{false}}', { 'false': 'foo' }, 'foo'); + }); + + it('should handle literals in subexpression', function() { + var helpers = { + foo: function(arg) { + return arg; + } + }; + shouldCompileTo('{{foo (false)}}', [{ 'false': function() { return 'bar'; } }, helpers], 'bar'); }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index 7de04dd83..18f03b902 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -66,7 +66,7 @@ inverseChain ; closeBlock - : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} + : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} ; mustache @@ -81,11 +81,7 @@ partial ; param - : path -> $1 - | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)) - | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) - | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) - | dataName -> $1 + : helperName -> $1 | sexpr -> $1 ; @@ -108,7 +104,7 @@ blockParams helperName : path -> $1 | dataName -> $1 - | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)), yy.locInfo(@$) + | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)) | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) ; From 55dc82d938c0f30a14acf5f015bf1b779f2de791 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 9 Feb 2015 23:55:01 -0600 Subject: [PATCH 97/99] Update AST docs for literal helper names --- docs/compiler-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 3593228bc..40ded8fd2 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -56,7 +56,7 @@ interface Statement <: Node { } interface MustacheStatement <: Statement { type: "MustacheStatement"; - path: PathExpression; + path: PathExpression | Literal; params: [ Expression ]; hash: Hash; From a8848d0af9498ad7a423f6f3b3eedc69b7130237 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 10 Feb 2015 00:14:42 -0600 Subject: [PATCH 98/99] Update release notes --- release-notes.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index 8d9b233ce..ad7b7f026 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,7 +2,57 @@ ## Development -[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0...master) +[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.0...master) + +## v3.0.0 - February 10th, 2015 +- [#941](https://github.com/wycats/handlebars.js/pull/941) - Add support for dynamic partial names ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#940](https://github.com/wycats/handlebars.js/pull/940) - Add missing reserved words so compiler knows to use array syntax: ([@mattflaschen](https://api.github.com/users/mattflaschen)) +- [#938](https://github.com/wycats/handlebars.js/pull/938) - Fix example using #with helper ([@diwo](https://api.github.com/users/diwo)) +- [#930](https://github.com/wycats/handlebars.js/pull/930) - Add parent tracking and mutation to AST visitors ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#926](https://github.com/wycats/handlebars.js/issues/926) - Depthed lookups fail when program duplicator runs ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#918](https://github.com/wycats/handlebars.js/pull/918) - Add instructions for 'spec/mustache' to CONTRIBUTING.md, fix a few typos ([@oneeman](https://api.github.com/users/oneeman)) +- [#915](https://github.com/wycats/handlebars.js/pull/915) - Ast update ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#910](https://github.com/wycats/handlebars.js/issues/910) - Different behavior of {{@last}} when {{#each}} in {{#each}} ([@zordius](https://api.github.com/users/zordius)) +- [#907](https://github.com/wycats/handlebars.js/issues/907) - Implement named helper variable references ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#906](https://github.com/wycats/handlebars.js/pull/906) - Add parser support for block params ([@mmun](https://api.github.com/users/mmun)) +- [#903](https://github.com/wycats/handlebars.js/issues/903) - Only provide aliases for multiple use calls ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#902](https://github.com/wycats/handlebars.js/pull/902) - Generate Source Maps ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#901](https://github.com/wycats/handlebars.js/issues/901) - Still escapes with noEscape enabled on isolated Handlebars environment ([@zedknight](https://api.github.com/users/zedknight)) +- [#896](https://github.com/wycats/handlebars.js/pull/896) - Simplify BlockNode by removing intermediate MustacheNode ([@mmun](https://api.github.com/users/mmun)) +- [#892](https://github.com/wycats/handlebars.js/pull/892) - Implement parser for else chaining of helpers ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#889](https://github.com/wycats/handlebars.js/issues/889) - Consider extensible parser API ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#887](https://github.com/wycats/handlebars.js/issues/887) - Handlebars.noConflict() option? ([@bradvogel](https://api.github.com/users/bradvogel)) +- [#886](https://github.com/wycats/handlebars.js/issues/886) - Add SafeString to context (or use duck-typing) ([@dominicbarnes](https://api.github.com/users/dominicbarnes)) +- [#870](https://github.com/wycats/handlebars.js/pull/870) - Registering undefined partial throws exception. ([@max-b](https://api.github.com/users/max-b)) +- [#866](https://github.com/wycats/handlebars.js/issues/866) - comments don't respect whitespace control ([@75lb](https://api.github.com/users/75lb)) +- [#863](https://github.com/wycats/handlebars.js/pull/863) - + jsDelivr CDN info ([@tomByrer](https://api.github.com/users/tomByrer)) +- [#858](https://github.com/wycats/handlebars.js/issues/858) - Disable new default auto-indent at included partials ([@majodev](https://api.github.com/users/majodev)) +- [#856](https://github.com/wycats/handlebars.js/pull/856) - jspm compatibility ([@MajorBreakfast](https://api.github.com/users/MajorBreakfast)) +- [#805](https://github.com/wycats/handlebars.js/issues/805) - Request: "strict" lookups ([@nzakas](https://api.github.com/users/nzakas)) + +- Export the default object for handlebars/runtime - 5594416 +- Lookup partials when undefined - 617dd57 + +Compatibility notes: +- Runtime breaking changes. Must match 3.x runtime and precompiler. +- The AST has been upgraded to a public API. + - There are a number of changes to this, but the format is now documented in docs/compiler-api.md + - The Visitor API has been expanded to support mutation and provide a base implementation +- The `JavaScriptCompiler` APIs have been formalized and documented. As part of the sourcemap handling these should be updated to return arrays for concatenation. +- `JavaScriptCompiler.namespace` has been removed as it was unused. +- `SafeString` is now duck typed on `toHTML` + +New Features: +- noConflict +- Source Maps +- Block Params +- Strict Mode +- @last and other each changes +- Chained else blocks +- @data methods can now have helper parameters passed to them +- Dynamic partials + +[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0...v3.0.0) ## v2.0.0 - September 1st, 2014 - Update jsfiddle to 2.0.0-beta.1 - 0670f65 From 0099e16a01bae4789c3560e2a658fdd7fecc9e12 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 10 Feb 2015 00:14:55 -0600 Subject: [PATCH 99/99] v3.0.0 --- components/bower.json | 2 +- components/handlebars.js.nuspec | 2 +- lib/handlebars/base.js | 2 +- package.json | 10 +++++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/components/bower.json b/components/bower.json index ec9280310..0e1f0a12b 100644 --- a/components/bower.json +++ b/components/bower.json @@ -1,6 +1,6 @@ { "name": "handlebars", - "version": "2.0.0", + "version": "3.0.0", "main": "handlebars.js", "dependencies": {} } diff --git a/components/handlebars.js.nuspec b/components/handlebars.js.nuspec index 671a32fba..8af75b07c 100644 --- a/components/handlebars.js.nuspec +++ b/components/handlebars.js.nuspec @@ -2,7 +2,7 @@ handlebars.js - 2.0.0 + 3.0.0 handlebars.js Authors https://github.com/wycats/handlebars.js/blob/master/LICENSE https://github.com/wycats/handlebars.js/ diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 20c49aec1..67f624cad 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,7 +1,7 @@ module Utils from "./utils"; import Exception from "./exception"; -export var VERSION = "2.0.0"; +export var VERSION = "3.0.0"; export var COMPILER_REVISION = 6; export var REVISION_CHANGES = { diff --git a/package.json b/package.json index c4711c99e..a0265507f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "handlebars", "barename": "handlebars", - "version": "2.0.0", + "version": "3.0.0", "description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration", "homepage": "http://www.handlebarsjs.com/", "keywords": [ @@ -62,7 +62,11 @@ }, "jspm": { "main": "handlebars", - "directories": { "lib": "dist/amd" }, - "buildConfig": { "minify": true } + "directories": { + "lib": "dist/amd" + }, + "buildConfig": { + "minify": true + } } }