From afd595bd6e19d8b64e8c3af7a29c157e55797ca6 Mon Sep 17 00:00:00 2001 From: Aymerick Date: Fri, 1 May 2015 11:35:25 +0200 Subject: [PATCH 01/82] Fix tokenizer test for double quoted strings There is two consecutive tests with the same input data: "{{ foo bar \'baz\' }}" I suppose the first test should be about testing double quoted string. --- spec/tokenizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tokenizer.js b/spec/tokenizer.js index ad71dc9b2..a474dfb16 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -264,7 +264,7 @@ describe('Tokenizer', function() { }); it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() { - var result = tokenize('{{ foo bar \'baz\' }}'); + var result = tokenize('{{ foo bar \"baz\" }}'); shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']); shouldBeToken(result[3], 'STRING', 'baz'); }); From 00bfdd782f69a085bec0aeeb4c5bd7d21a0521cc Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 5 May 2015 10:20:56 -0500 Subject: [PATCH 02/82] Fix VERSION update script This broke after updating to es6 formatted code. Fixes #1016 --- tasks/version.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tasks/version.js b/tasks/version.js index e6bfe5943..3912266fb 100644 --- a/tasks/version.js +++ b/tasks/version.js @@ -18,9 +18,9 @@ module.exports = function(grunt) { grunt.log.writeln('Updating to version ' + version); async.each([ - ['lib/handlebars/base.js', /var VERSION = ['"](.*)['"];/, 'var VERSION = "' + version + '";'], - ['components/bower.json', /"version":.*/, '"version": "' + version + '",'], - ['components/handlebars.js.nuspec', /.*<\/version>/, '' + version + ''] + ['lib/handlebars/base.js', (/const VERSION = ['"](.*)['"];/), 'const VERSION = \'' + version + '\';'], + ['components/bower.json', (/"version":.*/), '"version": "' + version + '",'], + ['components/handlebars.js.nuspec', (/.*<\/version>/), '' + version + ''] ], function(args, callback) { replace.apply(undefined, args); From d0d2168ec29092dccde043f13de7b912252162a8 Mon Sep 17 00:00:00 2001 From: Aymerick Date: Wed, 6 May 2015 14:03:14 +0200 Subject: [PATCH 03/82] Fixes typo in tests mutache => mustache --- spec/parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/parser.js b/spec/parser.js index c37887414..fa8c5b7dc 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -65,7 +65,7 @@ describe('parser', function() { equals(astFor('{{foo undefined null}}'), '{{ PATH:foo [UNDEFINED, NULL] }}\n'); }); - it('parses mutaches with DATA parameters', function() { + it('parses mustaches with DATA parameters', function() { equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n'); }); From 569f2885513d078bbd56128edbb948f130de3097 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 6 May 2015 10:47:56 -0500 Subject: [PATCH 04/82] Add tests for string contexts Fixes #1013 --- spec/basic.js | 4 ++++ spec/partials.js | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/spec/basic.js b/spec/basic.js index f9b781b4a..49ebbe52b 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -22,6 +22,10 @@ describe('basic context', function() { 'It works if all the required keys are provided'); }); + it('compiling with a string context', function() { + shouldCompileTo('{{.}}{{length}}', 'bye', 'bye3'); + }); + it('compiling with an undefined context', function() { shouldCompileTo('Goodbye\n{{cruel}}\n{{world.bar}}!', undefined, 'Goodbye\n\n!'); diff --git a/spec/partials.js b/spec/partials.js index 22d2dc959..b2fc9e074 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -41,6 +41,13 @@ describe('partials', function() { 'Partials can be passed a context'); }); + it('partials with string context', function() { + var string = 'Dudes: {{>dude "dudes"}}'; + var partial = '{{.}}'; + var hash = {}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: dudes'); + }); + it('partials with undefined context', function() { var string = 'Dudes: {{>dude dudes}}'; var partial = '{{foo}} Empty'; From d0805e9cfa46b167b083e2d48bac058137877100 Mon Sep 17 00:00:00 2001 From: "Tom X. Tobin" Date: Mon, 8 Jun 2015 18:37:41 -0400 Subject: [PATCH 05/82] Fix minor typos in README --- README.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index d6aa6bd5c..6500966af 100644 --- a/README.markdown +++ b/README.markdown @@ -287,8 +287,8 @@ You can also use real html comments if you want them to end up in the output. There are a few Mustache behaviors that Handlebars does not implement. - Handlebars deviates from Mustache slightly in that it does not perform recursive lookup by default. The compile time `compat` flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references. -- The optional Mustache-style lambdas are not supported. Instead Handlebars provides it's own lambda resolution that follows the behaviors of helpers. -- Alternative delimeters are not supported. +- The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers. +- Alternative delimiters are not supported. Precompiling Templates From 93faffa549166c492267cc96d3e6848923760d90 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 26 Jun 2015 14:30:34 -0500 Subject: [PATCH 06/82] Fix location information for programs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There appears to be a bug in our use of jison causing the parent location information to be reported to programs. I wasn’t able to work through what might be causing this so instead using the location information of the statements collection to generate the proper location information. This is a bit of a hack but we are very far behind on the Jison release train and upgrading will likely be a less than pleasant task that doesn’t provide us much benefit. Fixes #1024 --- lib/handlebars/compiler/helpers.js | 25 +++++++++++++++++++++++++ spec/ast.js | 27 +++++++++++++++++++++++---- spec/parser.js | 24 ++++++++++++++++++++++++ src/handlebars.yy | 4 ++-- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index fc0120c4f..e3eb7864a 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -121,3 +121,28 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver openBlock.strip, inverseStrip, close && close.strip, this.locInfo(locInfo)); } + +export function prepareProgram(statements, loc) { + if (!loc && statements.length) { + const first = statements[0].loc, + last = statements[statements.length - 1].loc; + + if (first && last) { + loc = { + source: first.source, + start: { + line: first.start.line, + column: first.start.column + }, + end: { + line: last.end.line, + column: last.end.column + } + }; + } + } + + return new this.Program(statements, null, {}, loc); +} + + diff --git a/spec/ast.js b/spec/ast.js index 6f492fddd..ce4c0909a 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -123,8 +123,18 @@ describe('ast', function() { equals(node.loc.end.column, lastColumn); } - ast = Handlebars.parse('line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n' + - 'line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}'); + ast = Handlebars.parse( + 'line 1 {{line1Token}}\n' // 1 + + ' line 2 {{line2token}}\n' // 2 + + ' line 3 {{#blockHelperOnLine3}}\n' // 3 + + 'line 4{{line4token}}\n' // 4 + + 'line5{{else}}\n' // 5 + + '{{line6Token}}\n' // 6 + + '{{/blockHelperOnLine3}}\n' // 7 + + '{{#open}}\n' // 8 + + '{{else inverse}}\n' // 9 + + '{{else}}\n' // 10 + + '{{/open}}'); // 11 body = ast.body; it('gets ContentNode line numbers', function() { @@ -155,14 +165,23 @@ describe('ast', function() { var blockHelperNode = body[5], program = blockHelperNode.program; - testColumns(program, 3, 5, 8, 5); + testColumns(program, 3, 5, 31, 5); }); it('correctly records the line numbers of an inverse of a block helper', function() { var blockHelperNode = body[5], inverse = blockHelperNode.inverse; - testColumns(inverse, 5, 7, 5, 0); + testColumns(inverse, 5, 7, 13, 0); + }); + + it('correctly records the line number of chained inverses', function() { + var chainInverseNode = body[7]; + + testColumns(chainInverseNode.program, 8, 9, 9, 0); + testColumns(chainInverseNode.inverse, 9, 10, 16, 0); + testColumns(chainInverseNode.inverse.body[0].program, 9, 10, 16, 0); + testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0); }); }); diff --git a/spec/parser.js b/spec/parser.js index fa8c5b7dc..424e2d178 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -231,4 +231,28 @@ describe('parser', function() { equals(astFor(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement('Hello')], null)), 'CONTENT[ \'Hello\' ]\n'); }); }); + + it('GH1024 - should track program location properly', function() { + var p = Handlebars.parse('\n' + + ' {{#if foo}}\n' + + ' {{bar}}\n' + + ' {{else}} {{baz}}\n' + + '\n' + + ' {{/if}}\n' + + ' '); + + // We really need a deep equals but for now this should be stable... + equals(JSON.stringify(p.loc), JSON.stringify({ + start: { line: 1, column: 0 }, + end: { line: 7, column: 4 } + })); + equals(JSON.stringify(p.body[1].program.loc), JSON.stringify({ + start: { line: 2, column: 13 }, + end: { line: 4, column: 7 } + })); + equals(JSON.stringify(p.body[1].inverse.loc), JSON.stringify({ + start: { line: 4, column: 15 }, + end: { line: 6, column: 5 } + })); + }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index d67a7da7e..2424e27fd 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -9,7 +9,7 @@ root ; program - : statement* -> new yy.Program($1, null, {}, yy.locInfo(@$)) + : statement* -> yy.prepareProgram($1) ; statement @@ -57,7 +57,7 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), - program = new yy.Program([inverse], null, {}, yy.locInfo(@$)); + program = yy.prepareProgram([inverse], $2.loc); program.chained = true; $$ = { strip: $1.strip, program: program, chain: true }; From 9bbc177fd5d08d0e3a1c584805e637851da0b2e9 Mon Sep 17 00:00:00 2001 From: AQNOUCH Mohammed Date: Thu, 2 Jul 2015 15:53:10 +0000 Subject: [PATCH 07/82] Updated year in License --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index a2d22cbb4..4effa3916 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2011-2014 by Yehuda Katz +Copyright (C) 2011-2015 by Yehuda Katz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 868ef4b309720f77586834e7a73e4b341ed4737a Mon Sep 17 00:00:00 2001 From: Eric Nielsen Date: Wed, 15 Jul 2015 14:40:48 -0300 Subject: [PATCH 08/82] #1056 Fixed grammar for nested raw blocks --- lib/handlebars/compiler/helpers.js | 4 ++-- src/handlebars.l | 12 +++++++++--- src/handlebars.yy | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index e3eb7864a..1c8ab0d3b 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -70,7 +70,7 @@ export function prepareMustache(path, params, hash, open, strip, locInfo) { return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo)); } -export function prepareRawBlock(openRawBlock, content, close, locInfo) { +export function prepareRawBlock(openRawBlock, contents, close, locInfo) { if (openRawBlock.path.original !== close) { let errorNode = {loc: openRawBlock.path.loc}; @@ -78,7 +78,7 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { } locInfo = this.locInfo(locInfo); - let program = new this.Program([content], null, {}, locInfo); + let program = new this.Program(contents, null, {}, locInfo); return new this.BlockStatement( openRawBlock.path, openRawBlock.params, openRawBlock.hash, diff --git a/src/handlebars.l b/src/handlebars.l index ff2128355..4d2cd6294 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -49,12 +49,18 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} return 'CONTENT'; } +// nested raw block will create stacked 'raw' condition +"{{{{"/[^/] this.begin('raw'); return 'CONTENT'; "{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" { - yytext = yytext.substr(5, yyleng-9); this.popState(); - return 'END_RAW_BLOCK'; + if (this.conditionStack[this.conditionStack.length-1] === 'raw') { + return 'CONTENT'; + } else { + yytext = yytext.substr(5, yyleng-9); + return 'END_RAW_BLOCK'; + } } -[^\x00]*?/("{{{{/") { return 'CONTENT'; } +[^\x00]*?/("{{{{") { return 'CONTENT'; } [\s\S]*?"--"{RIGHT_STRIP}?"}}" { this.popState(); diff --git a/src/handlebars.yy b/src/handlebars.yy index 2424e27fd..ecc79afc6 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -26,7 +26,7 @@ content ; rawBlock - : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) + : openRawBlock content+ END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) ; openRawBlock From b9fe7ce618a9bf4f969a2b97f455fccb2ac42101 Mon Sep 17 00:00:00 2001 From: Eric Nielsen Date: Wed, 15 Jul 2015 14:41:43 -0300 Subject: [PATCH 09/82] #1056 Added spec for nested raw block --- spec/helpers.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/helpers.js b/spec/helpers.js index 54ef0f288..f3257a502 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -28,6 +28,16 @@ describe('helpers', function() { 'raw block helper gets raw content'); }); + it('helper for nested raw block gets raw content', function() { + var string = '{{{{a}}}} {{{{b}}}} {{{{/b}}}} {{{{/a}}}}'; + var helpers = { + a: function(options) { + return options.fn(); + } + }; + shouldCompileTo(string, [{}, helpers], ' {{{{b}}}} {{{{/b}}}} '); + }); + it('helper block with complex lookup expression', function() { var string = '{{#goodbyes}}{{../name}}{{/goodbyes}}'; var hash = {name: 'Alan'}; From 2f9495c1df52eb47b0e84d77fc964cef4581581c Mon Sep 17 00:00:00 2001 From: Eric Nielsen Date: Thu, 16 Jul 2015 15:15:06 -0300 Subject: [PATCH 10/82] Added spec message --- spec/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers.js b/spec/helpers.js index f3257a502..00bcb79cb 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -35,7 +35,7 @@ describe('helpers', function() { return options.fn(); } }; - shouldCompileTo(string, [{}, helpers], ' {{{{b}}}} {{{{/b}}}} '); + shouldCompileTo(string, [{}, helpers], ' {{{{b}}}} {{{{/b}}}} ', 'raw block helper should get nested raw block as raw content'); }); it('helper block with complex lookup expression', function() { From a458fe26f423edad5be98199c993285f7a09c1a7 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 19 Jul 2015 11:28:24 +0300 Subject: [PATCH 11/82] Update jsfiddle link to 3.0.3 --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b84fdb005..79db3ba81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,4 +77,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/26/ +[jsfiddle]: http://jsfiddle.net/9D88g/46/ From aa7a45b44350caf35cd47a46a818271fb355b3f3 Mon Sep 17 00:00:00 2001 From: Eric Nielsen Date: Sun, 19 Jul 2015 12:34:44 -0300 Subject: [PATCH 12/82] Added comment about Jison's topState() --- src/handlebars.l | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/handlebars.l b/src/handlebars.l index 4d2cd6294..f7df8f55c 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -53,6 +53,9 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} "{{{{"/[^/] this.begin('raw'); return 'CONTENT'; "{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" { this.popState(); + // Should be using `this.topState()` below, but it currently + // returns the second top instead of the first top. Opened an + // issue about it at https://github.com/zaach/jison/issues/291 if (this.conditionStack[this.conditionStack.length-1] === 'raw') { return 'CONTENT'; } else { From fe057168aea96fa48aadaaa614d65f7f3fee73b9 Mon Sep 17 00:00:00 2001 From: Saleh Batati <0xack13@gmail.com> Date: Thu, 30 Jul 2015 06:02:51 +0300 Subject: [PATCH 13/82] Fix typo --- 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 cfe1e917c..a1847fc4b 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -61,7 +61,7 @@ HandlebarsEnvironment.prototype = { function registerDefaultHelpers(instance) { instance.registerHelper('helperMissing', function(/* [args, ]options */) { if (arguments.length === 1) { - // A missing field in a {{foo}} constuct. + // A missing field in a {{foo}} construct. return undefined; } else { // Someone is actually trying to call something, blow up. From 410141c31e547694746f3ce9427d1dde30070777 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 21 Jul 2015 02:16:49 +0300 Subject: [PATCH 14/82] Fix escaping of non-javascript identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ‘ character would cause invalid javascript to be generated as it was not properly escaped. Switching to JSON.stringify safely handles all potential unescaped cases. --- lib/handlebars/compiler/javascript-compiler.js | 2 +- spec/basic.js | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 883066165..d39ecb2bf 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -16,7 +16,7 @@ JavaScriptCompiler.prototype = { if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { return [parent, '.', name]; } else { - return [parent, "['", name, "']"]; + return [parent, '[', JSON.stringify(name), ']']; } }, depthedLookup: function(name) { diff --git a/spec/basic.js b/spec/basic.js index 49ebbe52b..8859545d9 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -207,8 +207,12 @@ describe('basic context', function() { }); it('literal references', function() { - shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'}, - 'Goodbye beautiful world!', 'Literal paths can be used'); + shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo('Goodbye {{"foo bar"}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo("Goodbye {{'foo bar'}} world!", {'foo bar': 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo('Goodbye {{"foo[bar"}} world!', {'foo[bar': 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo('Goodbye {{"foo\'bar"}} world!', {"foo'bar": 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo("Goodbye {{'foo\"bar'}} world!", {'foo"bar': 'beautiful'}, 'Goodbye beautiful world!'); }); it("that current context path ({{.}}) doesn't hit helpers", function() { From 2a851067b99c3932e471f5fc1a2d40dc25a084c4 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 15:48:16 -0500 Subject: [PATCH 15/82] Add with block parameter support Fixes #1042 --- lib/handlebars/base.js | 9 ++++++--- spec/builtins.js | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index a1847fc4b..c2f14eb4d 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -194,13 +194,16 @@ function registerDefaultHelpers(instance) { let fn = options.fn; if (!Utils.isEmpty(context)) { + let data = options.data; if (options.data && options.ids) { - let data = createFrame(options.data); + data = createFrame(options.data); data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]); - options = {data: data}; } - return fn(context, options); + return fn(context, { + data: data, + blockParams: Utils.blockParams([context], [data.contextPath]) + }); } else { return options.inverse(this); } diff --git a/spec/builtins.js b/spec/builtins.js index 46d70baac..959874326 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -47,6 +47,10 @@ describe('builtin helpers', function() { var string = '{{#with person}}Person is present{{else}}Person is not present{{/with}}'; shouldCompileTo(string, {}, 'Person is not present'); }); + it('with provides block parameter', function() { + var string = '{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}'; + shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson'); + }); }); describe('#each', function() { From 1bb640be412f20d74db7af476152a081073aa21a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 16:04:40 -0500 Subject: [PATCH 16/82] Allow empty key name in each iteration Fixes #1021 --- lib/handlebars/base.js | 2 +- spec/regressions.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index c2f14eb4d..c7dc08151 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -151,7 +151,7 @@ function registerDefaultHelpers(instance) { // We're running the iterations one step out of sync so we can detect // the last iteration without have to scan the object twice and create // an itermediate keys array. - if (priorKey) { + if (priorKey !== undefined) { execIteration(priorKey, i - 1); } priorKey = key; diff --git a/spec/regressions.js b/spec/regressions.js index 247c1c9b3..f04107046 100644 --- a/spec/regressions.js +++ b/spec/regressions.js @@ -172,4 +172,14 @@ describe('Regressions', function() { var result = template(context); equals(result, 'foo'); }); + + it('GH-1021: Each empty string key', function() { + var data = { + '': 'foo', + 'name': 'Chris', + 'value': 10000 + }; + + shouldCompileTo('{{#each data}}Key: {{@key}}\n{{/each}}', {data: data}, 'Key: \nKey: name\nKey: value\n'); + }); }); From e2ba22eaad24575ab3cb235b8fc36683acf610c2 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 16:08:57 -0500 Subject: [PATCH 17/82] Pull sauce tests out of CI Disabling these until #1069 can resolve whatever the root issue is. --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 70239bf8b..ad50db918 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -227,7 +227,7 @@ module.exports = function(grunt) { grunt.registerTask('bench', ['metrics']); grunt.registerTask('sauce', process.env.SAUCE_USERNAME ? ['tests', 'connect', 'saucelabs-mocha'] : []); - grunt.registerTask('travis', process.env.PUBLISH ? ['default', 'sauce', 'metrics', 'publish:latest'] : ['default']); + grunt.registerTask('travis', process.env.PUBLISH ? ['default', 'metrics', 'publish:latest'] : ['default']); grunt.registerTask('dev', ['clean', 'connect', 'watch']); grunt.registerTask('default', ['clean', 'build', 'test', 'release']); From 231a8d7256d24c1a0287d67d393f06faa32751e8 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 16:29:45 -0500 Subject: [PATCH 18/82] Fix with operator in no @data mode --- lib/handlebars/base.js | 2 +- spec/builtins.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index c7dc08151..756fb775c 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -202,7 +202,7 @@ function registerDefaultHelpers(instance) { return fn(context, { data: data, - blockParams: Utils.blockParams([context], [data.contextPath]) + blockParams: Utils.blockParams([context], [data && data.contextPath]) }); } else { return options.inverse(this); diff --git a/spec/builtins.js b/spec/builtins.js index 959874326..e5d923f87 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -51,6 +51,12 @@ describe('builtin helpers', function() { var string = '{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}'; shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson'); }); + it('works when data is disabled', function() { + var template = CompilerContext.compile('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}', {data: false}); + + var result = template({person: {first: 'Alan', last: 'Johnson'}}); + equals(result, 'Alan Johnson'); + }); }); describe('#each', function() { From 15b55a307b4f95d4a861df8b32c3c1ddb4825414 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 17:54:47 -0500 Subject: [PATCH 19/82] Move helpers into separate modules --- lib/handlebars/base.js | 179 +----------------- lib/handlebars/helpers.js | 17 ++ .../helpers/block-helper-missing.js | 32 ++++ lib/handlebars/helpers/each.js | 77 ++++++++ lib/handlebars/helpers/helper-missing.js | 13 ++ lib/handlebars/helpers/if.js | 20 ++ lib/handlebars/helpers/log.js | 6 + lib/handlebars/helpers/lookup.js | 5 + lib/handlebars/helpers/with.js | 24 +++ lib/handlebars/utils.js | 6 + 10 files changed, 206 insertions(+), 173 deletions(-) create mode 100644 lib/handlebars/helpers.js create mode 100644 lib/handlebars/helpers/block-helper-missing.js create mode 100644 lib/handlebars/helpers/each.js create mode 100644 lib/handlebars/helpers/helper-missing.js create mode 100644 lib/handlebars/helpers/if.js create mode 100644 lib/handlebars/helpers/log.js create mode 100644 lib/handlebars/helpers/lookup.js create mode 100644 lib/handlebars/helpers/with.js diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 756fb775c..cc3d2face 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,5 +1,6 @@ -import * as Utils from './utils'; +import {createFrame, extend, toString} from './utils'; import Exception from './exception'; +import {registerDefaultHelpers} from './helpers'; export const VERSION = '3.0.1'; export const COMPILER_REVISION = 6; @@ -13,10 +14,7 @@ export const REVISION_CHANGES = { 6: '>= 2.0.0-beta.1' }; -const isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; +const objectType = '[object Object]'; export function HandlebarsEnvironment(helpers, partials) { this.helpers = helpers || {}; @@ -34,7 +32,7 @@ HandlebarsEnvironment.prototype = { registerHelper: function(name, fn) { if (toString.call(name) === objectType) { if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); + extend(this.helpers, name); } else { this.helpers[name] = fn; } @@ -45,7 +43,7 @@ HandlebarsEnvironment.prototype = { registerPartial: function(name, partial) { if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); + extend(this.partials, name); } else { if (typeof partial === 'undefined') { throw new Exception('Attempting to register a partial as undefined'); @@ -58,167 +56,6 @@ HandlebarsEnvironment.prototype = { } }; -function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if (arguments.length === 1) { - // A missing field in a {{foo}} construct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception('Missing helper: "' + arguments[arguments.length - 1].name + '"'); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - let inverse = options.inverse, - fn = options.fn; - - if (context === true) { - return fn(this); - } else if (context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if (context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - let data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - let fn = options.fn, - inverse = options.inverse, - i = 0, - ret = '', - data, - contextPath; - - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - function execIteration(field, index, last) { - if (data) { - data.key = field; - data.index = index; - data.first = index === 0; - data.last = !!last; - - if (contextPath) { - data.contextPath = contextPath + field; - } - } - - ret = ret + fn(context[field], { - data: data, - blockParams: Utils.blockParams([context[field], field], [contextPath + field, null]) - }); - } - - if (context && typeof context === 'object') { - if (isArray(context)) { - for (let j = context.length; i < j; i++) { - execIteration(i, i, i === context.length - 1); - } - } else { - let priorKey; - - for (let key in context) { - if (context.hasOwnProperty(key)) { - // We're running the iterations one step out of sync so we can detect - // the last iteration without have to scan the object twice and create - // an itermediate keys array. - if (priorKey !== undefined) { - execIteration(priorKey, i - 1); - } - priorKey = key; - i++; - } - } - if (priorKey) { - execIteration(priorKey, i - 1, true); - } - } - } - - if (i === 0) { - ret = inverse(this); - } - - return ret; - }); - - instance.registerHelper('if', function(conditional, options) { - if (isFunction(conditional)) { conditional = conditional.call(this); } - - // Default behavior is to render the positive path if the value is truthy and not empty. - // The `includeZero` option may be set to treat the condtional as purely not empty based on the - // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative. - if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) { - return options.inverse(this); - } else { - return options.fn(this); - } - }); - - instance.registerHelper('unless', function(conditional, options) { - return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash}); - }); - - instance.registerHelper('with', function(context, options) { - if (isFunction(context)) { context = context.call(this); } - - let fn = options.fn; - - if (!Utils.isEmpty(context)) { - let data = options.data; - if (options.data && options.ids) { - data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]); - } - - return fn(context, { - data: data, - blockParams: Utils.blockParams([context], [data && data.contextPath]) - }); - } else { - return options.inverse(this); - } - }); - - instance.registerHelper('log', function(message, options) { - let level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1; - instance.log(level, message); - }); - - instance.registerHelper('lookup', function(obj, field) { - return obj && obj[field]; - }); -} - export let logger = { methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' }, @@ -240,8 +77,4 @@ export let logger = { export let log = logger.log; -export function createFrame(object) { - let frame = Utils.extend({}, object); - frame._parent = object; - return frame; -} +export {createFrame}; diff --git a/lib/handlebars/helpers.js b/lib/handlebars/helpers.js new file mode 100644 index 000000000..7a4365aea --- /dev/null +++ b/lib/handlebars/helpers.js @@ -0,0 +1,17 @@ +import registerBlockHelperMissing from './helpers/block-helper-missing'; +import registerEach from './helpers/each'; +import registerHelperMissing from './helpers/helper-missing'; +import registerIf from './helpers/if'; +import registerLog from './helpers/log'; +import registerLookup from './helpers/lookup'; +import registerWith from './helpers/with'; + +export function registerDefaultHelpers(instance) { + registerBlockHelperMissing(instance); + registerEach(instance); + registerHelperMissing(instance); + registerIf(instance); + registerLog(instance); + registerLookup(instance); + registerWith(instance); +} diff --git a/lib/handlebars/helpers/block-helper-missing.js b/lib/handlebars/helpers/block-helper-missing.js new file mode 100644 index 000000000..6639ddb9d --- /dev/null +++ b/lib/handlebars/helpers/block-helper-missing.js @@ -0,0 +1,32 @@ +import {appendContextPath, createFrame, isArray} from '../utils'; + +export default function(instance) { + instance.registerHelper('blockHelperMissing', function(context, options) { + let inverse = options.inverse, + fn = options.fn; + + if (context === true) { + return fn(this); + } else if (context === false || context == null) { + return inverse(this); + } else if (isArray(context)) { + if (context.length > 0) { + if (options.ids) { + options.ids = [options.name]; + } + + return instance.helpers.each(context, options); + } else { + return inverse(this); + } + } else { + if (options.data && options.ids) { + let data = createFrame(options.data); + data.contextPath = appendContextPath(options.data.contextPath, options.name); + options = {data: data}; + } + + return fn(context, options); + } + }); +} diff --git a/lib/handlebars/helpers/each.js b/lib/handlebars/helpers/each.js new file mode 100644 index 000000000..9fc5a095d --- /dev/null +++ b/lib/handlebars/helpers/each.js @@ -0,0 +1,77 @@ +import {appendContextPath, blockParams, createFrame, isArray, isFunction} from '../utils'; +import Exception from '../exception'; + +export default function(instance) { + instance.registerHelper('each', function(context, options) { + if (!options) { + throw new Exception('Must pass iterator to #each'); + } + + let fn = options.fn, + inverse = options.inverse, + i = 0, + ret = '', + data, + contextPath; + + if (options.data && options.ids) { + contextPath = appendContextPath(options.data.contextPath, options.ids[0]) + '.'; + } + + if (isFunction(context)) { context = context.call(this); } + + if (options.data) { + data = createFrame(options.data); + } + + function execIteration(field, index, last) { + if (data) { + data.key = field; + data.index = index; + data.first = index === 0; + data.last = !!last; + + if (contextPath) { + data.contextPath = contextPath + field; + } + } + + ret = ret + fn(context[field], { + data: data, + blockParams: blockParams([context[field], field], [contextPath + field, null]) + }); + } + + if (context && typeof context === 'object') { + if (isArray(context)) { + for (let j = context.length; i < j; i++) { + execIteration(i, i, i === context.length - 1); + } + } else { + let priorKey; + + for (let key in context) { + if (context.hasOwnProperty(key)) { + // We're running the iterations one step out of sync so we can detect + // the last iteration without have to scan the object twice and create + // an itermediate keys array. + if (priorKey !== undefined) { + execIteration(priorKey, i - 1); + } + priorKey = key; + i++; + } + } + if (priorKey) { + execIteration(priorKey, i - 1, true); + } + } + } + + if (i === 0) { + ret = inverse(this); + } + + return ret; + }); +} diff --git a/lib/handlebars/helpers/helper-missing.js b/lib/handlebars/helpers/helper-missing.js new file mode 100644 index 000000000..ec32e8245 --- /dev/null +++ b/lib/handlebars/helpers/helper-missing.js @@ -0,0 +1,13 @@ +import Exception from '../exception'; + +export default function(instance) { + instance.registerHelper('helperMissing', function(/* [args, ]options */) { + if (arguments.length === 1) { + // A missing field in a {{foo}} construct. + return undefined; + } else { + // Someone is actually trying to call something, blow up. + throw new Exception('Missing helper: "' + arguments[arguments.length - 1].name + '"'); + } + }); +} diff --git a/lib/handlebars/helpers/if.js b/lib/handlebars/helpers/if.js new file mode 100644 index 000000000..11d08df91 --- /dev/null +++ b/lib/handlebars/helpers/if.js @@ -0,0 +1,20 @@ +import {isEmpty, isFunction} from '../utils'; + +export default function(instance) { + instance.registerHelper('if', function(conditional, options) { + if (isFunction(conditional)) { conditional = conditional.call(this); } + + // Default behavior is to render the positive path if the value is truthy and not empty. + // The `includeZero` option may be set to treat the condtional as purely not empty based on the + // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative. + if ((!options.hash.includeZero && !conditional) || isEmpty(conditional)) { + return options.inverse(this); + } else { + return options.fn(this); + } + }); + + instance.registerHelper('unless', function(conditional, options) { + return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash}); + }); +} diff --git a/lib/handlebars/helpers/log.js b/lib/handlebars/helpers/log.js new file mode 100644 index 000000000..ab83604b2 --- /dev/null +++ b/lib/handlebars/helpers/log.js @@ -0,0 +1,6 @@ +export default function(instance) { + instance.registerHelper('log', function(message, options) { + let level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1; + instance.log(level, message); + }); +} diff --git a/lib/handlebars/helpers/lookup.js b/lib/handlebars/helpers/lookup.js new file mode 100644 index 000000000..a52e77a04 --- /dev/null +++ b/lib/handlebars/helpers/lookup.js @@ -0,0 +1,5 @@ +export default function(instance) { + instance.registerHelper('lookup', function(obj, field) { + return obj && obj[field]; + }); +} diff --git a/lib/handlebars/helpers/with.js b/lib/handlebars/helpers/with.js new file mode 100644 index 000000000..7418cd066 --- /dev/null +++ b/lib/handlebars/helpers/with.js @@ -0,0 +1,24 @@ +import {appendContextPath, blockParams, createFrame, isEmpty, isFunction} from '../utils'; + +export default function(instance) { + instance.registerHelper('with', function(context, options) { + if (isFunction(context)) { context = context.call(this); } + + let fn = options.fn; + + if (!isEmpty(context)) { + let data = options.data; + if (options.data && options.ids) { + data = createFrame(options.data); + data.contextPath = appendContextPath(options.data.contextPath, options.ids[0]); + } + + return fn(context, { + data: data, + blockParams: blockParams([context], [data && data.contextPath]) + }); + } else { + return options.inverse(this); + } + }); +} diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index c5223947d..c7a5762be 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -91,6 +91,12 @@ export function isEmpty(value) { } } +export function createFrame(object) { + let frame = extend({}, object); + frame._parent = object; + return frame; +} + export function blockParams(params, ids) { params.path = ids; return params; From f3e8b189254b86d2c852342d69167c35a08598a9 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 21:46:45 -0500 Subject: [PATCH 20/82] Add istanbul ignore to babel boilerplate --- Gruntfile.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index ad50db918..c2ff15e65 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -43,7 +43,8 @@ module.exports = function(grunt) { babel: { options: { - loose: ['es6.modules'] + loose: ['es6.modules'], + auxiliaryCommentBefore: 'istanbul ignore next' }, amd: { options: { @@ -75,7 +76,7 @@ module.exports = function(grunt) { module: { loaders: [ // the optional 'runtime' transformer tells babel to require the runtime instead of inlining it. - { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?optional=runtime&loose=es6.modules' } + { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?optional=runtime&loose=es6.modules&auxiliaryCommentBefore=istanbul%20ignore%20next' } ] }, output: { From efddc3c09cacd6719a8206eeb3787a0c3aabb174 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 21:47:13 -0500 Subject: [PATCH 21/82] Increase code coverage --- lib/handlebars/compiler/compiler.js | 1 + lib/handlebars/exception.js | 1 + lib/handlebars/utils.js | 8 ++++---- src/parser-suffix.js | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 457542162..2448443e6 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -66,6 +66,7 @@ Compiler.prototype = { }; if (knownHelpers) { for (let name in knownHelpers) { + /* istanbul ignore else */ if (name in knownHelpers) { options.knownHelpers[name] = knownHelpers[name]; } diff --git a/lib/handlebars/exception.js b/lib/handlebars/exception.js index 46ce18eae..52499c0ca 100644 --- a/lib/handlebars/exception.js +++ b/lib/handlebars/exception.js @@ -19,6 +19,7 @@ function Exception(message, node) { this[errorProps[idx]] = tmp[errorProps[idx]]; } + /* istanbul ignore else */ if (Error.captureStackTrace) { Error.captureStackTrace(this, Exception); } diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index c7a5762be..81050f999 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -30,8 +30,8 @@ export let toString = Object.prototype.toString; // Sourced from lodash // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt -/*eslint-disable func-style, no-var */ -var isFunction = function(value) { +/*eslint-disable func-style */ +let isFunction = function(value) { return typeof value === 'function'; }; // fallback for older versions of Chrome and Safari @@ -41,8 +41,8 @@ if (isFunction(/x/)) { return typeof value === 'function' && toString.call(value) === '[object Function]'; }; } -export var isFunction; -/*eslint-enable func-style, no-var */ +export {isFunction}; +/*eslint-enable func-style */ /* istanbul ignore next */ export const isArray = Array.isArray || function(value) { diff --git a/src/parser-suffix.js b/src/parser-suffix.js index 6e4aa20d6..e0f37eb1f 100644 --- a/src/parser-suffix.js +++ b/src/parser-suffix.js @@ -1 +1,2 @@ -export default handlebars; +exports.__esModule = true; +module.exports['default'] = handlebars; From ac82842cb63858a5e95ff9c7a9330acdf0f7b836 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 22:01:36 -0500 Subject: [PATCH 22/82] Add rest params to es6 supported list --- .eslintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc b/.eslintrc index 253a1bb29..c1c00d064 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,7 @@ "objectLiteralDuplicateProperties": true, "objectLiteralShorthandMethods": true, "objectLiteralShorthandProperties": true, + "restParams": true, "spread": true, "templateStrings": true }, From b664997dc37ab46eff678b802bb57c84160f46ad Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 1 Aug 2015 22:03:11 -0500 Subject: [PATCH 23/82] Move logger into separate module --- lib/handlebars/base.js | 24 +++--------------------- lib/handlebars/logger.js | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 lib/handlebars/logger.js diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index cc3d2face..41bb98d41 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,6 +1,7 @@ import {createFrame, extend, toString} from './utils'; import Exception from './exception'; import {registerDefaultHelpers} from './helpers'; +import logger from './logger'; export const VERSION = '3.0.1'; export const COMPILER_REVISION = 6; @@ -27,7 +28,7 @@ HandlebarsEnvironment.prototype = { constructor: HandlebarsEnvironment, logger: logger, - log: log, + log: logger.log, registerHelper: function(name, fn) { if (toString.call(name) === objectType) { @@ -56,25 +57,6 @@ HandlebarsEnvironment.prototype = { } }; -export let logger = { - methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' }, - - // State enum - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3, - level: 1, - - // Can be overridden in the host environment - log: function(level, message) { - if (typeof console !== 'undefined' && logger.level <= level) { - let method = logger.methodMap[level]; - (console[method] || console.log).call(console, message); // eslint-disable-line no-console - } - } -}; - export let log = logger.log; -export {createFrame}; +export {createFrame, logger}; diff --git a/lib/handlebars/logger.js b/lib/handlebars/logger.js new file mode 100644 index 000000000..823d71599 --- /dev/null +++ b/lib/handlebars/logger.js @@ -0,0 +1,20 @@ +let logger = { + methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' }, + + // State enum + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + level: 1, + + // Can be overridden in the host environment + log: function(level, message) { + if (typeof console !== 'undefined' && logger.level <= level) { + let method = logger.methodMap[level]; + (console[method] || console.log).call(console, message); // eslint-disable-line no-console + } + } +}; + +export default logger; From 9a49d350231edbe82c9982df9a99f39596fb96b7 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 11:36:36 -0500 Subject: [PATCH 24/82] Improve logging API Adds multiple variable support and the ability to set statement level logging semantics. This breaks that logger API, cleaning up the manner in which enums are set, but the other behaviors are backwards compatible. Fixes #956 --- lib/handlebars/helpers/log.js | 19 +++++++++-- lib/handlebars/logger.js | 33 +++++++++++++------ spec/builtins.js | 62 ++++++++++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 14 deletions(-) diff --git a/lib/handlebars/helpers/log.js b/lib/handlebars/helpers/log.js index ab83604b2..4bde4a10d 100644 --- a/lib/handlebars/helpers/log.js +++ b/lib/handlebars/helpers/log.js @@ -1,6 +1,19 @@ export default function(instance) { - instance.registerHelper('log', function(message, options) { - let level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1; - instance.log(level, message); + instance.registerHelper('log', function(/* message, options */) { + let args = [undefined], + options = arguments[arguments.length - 1]; + for (let i = 0; i < arguments.length - 1; i++) { + args.push(arguments[i]); + } + + let level = 1; + if (options.hash.level != null) { + level = options.hash.level; + } else if (options.data && options.data.level != null) { + level = options.data.level; + } + args[0] = level; + + instance.log(... args); }); } diff --git a/lib/handlebars/logger.js b/lib/handlebars/logger.js index 823d71599..1d583ddb9 100644 --- a/lib/handlebars/logger.js +++ b/lib/handlebars/logger.js @@ -1,18 +1,31 @@ let logger = { - methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' }, + methodMap: ['debug', 'info', 'warn', 'error'], + level: 'info', - // State enum - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3, - level: 1, + // Maps a given level value to the `methodMap` indexes above. + lookupLevel: function(level) { + if (typeof level === 'string') { + let levelMap = logger.methodMap.indexOf(level.toLowerCase()); + if (levelMap >= 0) { + level = levelMap; + } else { + level = parseInt(level, 10); + } + } + + return level; + }, // Can be overridden in the host environment - log: function(level, message) { - if (typeof console !== 'undefined' && logger.level <= level) { + log: function(level, ...message) { + level = logger.lookupLevel(level); + + if (typeof console !== 'undefined' && logger.lookupLevel(logger.level) <= level) { let method = logger.methodMap[level]; - (console[method] || console.log).call(console, message); // eslint-disable-line no-console + if (!console[method]) { // eslint-disable-line no-console + method = 'log'; + } + console[method](...message); // eslint-disable-line no-console } } }; diff --git a/spec/builtins.js b/spec/builtins.js index e5d923f87..6a02dc627 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -286,7 +286,7 @@ describe('builtin helpers', function() { }; shouldCompileTo(string, [hash,,,, {level: '03'}], ''); - equals(3, levelArg); + equals('03', levelArg); equals('whee', logArg); }); it('should output to info', function() { @@ -327,6 +327,66 @@ describe('builtin helpers', function() { shouldCompileTo(string, [hash,,,, {level: '03'}], ''); }); + + it('should handle string log levels', function() { + var string = '{{log blah}}'; + var hash = { blah: 'whee' }; + var called; + + console.error = function(log) { + equals('whee', log); + called = true; + }; + + shouldCompileTo(string, [hash,,,, {level: 'error'}], ''); + equals(true, called); + + called = false; + + shouldCompileTo(string, [hash,,,, {level: 'ERROR'}], ''); + equals(true, called); + }); + it('should handle hash log levels', function() { + var string = '{{log blah level="error"}}'; + var hash = { blah: 'whee' }; + var called; + + console.error = function(log) { + equals('whee', log); + called = true; + }; + + shouldCompileTo(string, hash, ''); + equals(true, called); + }); + it('should handle hash log levels', function() { + var string = '{{log blah level="debug"}}'; + var hash = { blah: 'whee' }; + var called = false; + + console.info = console.log = console.error = console.debug = function(log) { + equals('whee', log); + called = true; + }; + + shouldCompileTo(string, hash, ''); + equals(false, called); + }); + it('should pass multiple log arguments', function() { + var string = '{{log blah "foo" 1}}'; + var hash = { blah: 'whee' }; + var called; + + console.info = console.log = function(log1, log2, log3) { + equals('whee', log1); + equals('foo', log2); + equals(1, log3); + called = true; + }; + + shouldCompileTo(string, hash, ''); + equals(true, called); + }); /*eslint-enable no-console */ }); From 1f11cc0186e22fa7073e64b8d2ffa4d5e4b32b59 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 12:12:30 -0500 Subject: [PATCH 25/82] Remove out of date TODO --- lib/handlebars/runtime.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 874728fb5..5f73897a1 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -20,8 +20,6 @@ export function checkRevision(compilerInfo) { } } -// TODO: Remove this line and break up compilePartial - export function template(templateSpec, env) { /* istanbul ignore next */ if (!env) { From 0aa54f49de111d117e7d3b4a21e99b9fcaf483d1 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 12:13:00 -0500 Subject: [PATCH 26/82] Avoid log output in test --- spec/builtins.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/builtins.js b/spec/builtins.js index 6a02dc627..bcacf5908 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -321,11 +321,17 @@ describe('builtin helpers', function() { }); it('should handle missing logger', function() { var string = '{{log blah}}'; - var hash = { blah: 'whee' }; + var hash = { blah: 'whee' }, + called = false; console.error = undefined; + console.log = function(log) { + equals('whee', log); + called = true; + }; shouldCompileTo(string, [hash,,,, {level: '03'}], ''); + equals(true, called); }); it('should handle string log levels', function() { From 9f265b97614e5c4763dc3d2d7343fc2472a6cc1a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 12:13:24 -0500 Subject: [PATCH 27/82] Handle this references properly in track id mode --- lib/handlebars/compiler/compiler.js | 5 +++-- spec/track-ids.js | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 2448443e6..c1ef47ed7 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -390,8 +390,9 @@ Compiler.prototype = { value = val.original || value; if (value.replace) { value = value - .replace(/^\.\//g, '') - .replace(/^\.$/g, ''); + .replace(/^this(?:\.|$)/, '') + .replace(/^\.\//, '') + .replace(/^\.$/, ''); } this.opcode('pushId', val.type, value); diff --git a/spec/track-ids.js b/spec/track-ids.js index 7a8b59ee4..88db789be 100644 --- a/spec/track-ids.js +++ b/spec/track-ids.js @@ -47,12 +47,14 @@ describe('track ids', function() { equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo slave.driver:bar'); }); it('should note ../ and ./ references', function() { - var template = CompilerContext.compile('{{wycats ./is.a ../slave.driver}}', {trackIds: true}); + var template = CompilerContext.compile('{{wycats ./is.a ../slave.driver this.is.a this}}', {trackIds: true}); var helpers = { - wycats: function(passiveVoice, noun, options) { + wycats: function(passiveVoice, noun, thiz, thiz2, options) { equal(options.ids[0], 'is.a'); equal(options.ids[1], '../slave.driver'); + equal(options.ids[2], 'is.a'); + equal(options.ids[3], ''); return 'HELP ME MY BOSS ' + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun; } From 1c08771215af7377fb8f33a26f64b3af0e06c168 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 12:16:02 -0500 Subject: [PATCH 28/82] Fix track id handling in partials Fixes #914 --- lib/handlebars/runtime.js | 6 ++++++ spec/track-ids.js | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 5f73897a1..d41b42a44 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -36,6 +36,9 @@ export function template(templateSpec, env) { function invokePartialWrapper(partial, context, options) { if (options.hash) { context = Utils.extend({}, context, options.hash); + if (options.ids) { + options.ids[0] = true; + } } partial = env.VM.resolvePartial.call(this, partial, context, options); @@ -193,6 +196,9 @@ export function resolvePartial(partial, context, options) { export function invokePartial(partial, context, options) { options.partial = true; + if (options.ids) { + options.data.contextPath = options.ids[0] || options.data.contextPath; + } if (partial === undefined) { throw new Exception('The partial ' + options.name + ' could not be found'); diff --git a/spec/track-ids.js b/spec/track-ids.js index 88db789be..30a46617d 100644 --- a/spec/track-ids.js +++ b/spec/track-ids.js @@ -190,4 +190,48 @@ describe('track ids', function() { }); }); }); + + describe('partials', function() { + var helpers = { + blockParams: function(name, options) { + return name + ':' + options.ids[0] + '\n'; + }, + wycats: function(name, options) { + return name + ':' + options.data.contextPath + '\n'; + } + }; + + it('should pass track id for basic partial', function() { + var template = CompilerContext.compile('Dudes: {{#dudes}}{{> dude}}{{/dudes}}', {trackIds: true}), + hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + + var partials = { + dude: CompilerContext.compile('{{wycats name}}', {trackIds: true}) + }; + + equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:dudes.0\nAlan:dudes.1\n'); + }); + + it('should pass track id for context partial', function() { + var template = CompilerContext.compile('Dudes: {{> dude dudes}}', {trackIds: true}), + hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + + var partials = { + dude: CompilerContext.compile('{{#each this}}{{wycats name}}{{/each}}', {trackIds: true}) + }; + + equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:dudes..0\nAlan:dudes..1\n'); + }); + + it('should invalidate context for partials with parameters', function() { + var template = CompilerContext.compile('Dudes: {{#dudes}}{{> dude . bar="foo"}}{{/dudes}}', {trackIds: true}), + hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + + var partials = { + dude: CompilerContext.compile('{{wycats name}}', {trackIds: true}) + }; + + equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:true\nAlan:true\n'); + }); + }); }); From 279e038ba7ff7e5966a60e36d326e2d4c310f0c6 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 15:15:09 -0500 Subject: [PATCH 29/82] Avoid depth creation when context remains the same MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creating a new depth value seems to confuse users as they don’t expect things like `if` to require multiple `..` to break out of. With the change, we avoid pushing a context to the depth list if it’s already on the top of the stack, effectively removing cases where `.` and `..` are the same object and multiple `..` references are required. This is a breaking change and all templates that utilize `..` will have to check their usage and confirm that this does not break desired behavior. Helper authors now need to take care to return the same context value whenever it is conceptually the same and to avoid behaviors that may execute children under the current context in some situations and under different contexts under other situations. Fixes #1028 --- lib/handlebars/runtime.js | 13 +++++++++++-- spec/builtins.js | 5 +++++ spec/helpers.js | 15 ++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index d41b42a44..bc12fc9fd 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -135,7 +135,11 @@ export function template(templateSpec, env) { let depths, blockParams = templateSpec.useBlockParams ? [] : undefined; if (templateSpec.useDepths) { - depths = options.depths ? [context].concat(options.depths) : [context]; + if (options.depths) { + depths = context !== options.depths[0] ? [context].concat(depths) : options.depths; + } else { + depths = [context]; + } } return templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths); @@ -170,12 +174,17 @@ export function template(templateSpec, env) { export function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) { function prog(context, options = {}) { + let currentDepths = depths; + if (depths && context !== depths[0]) { + currentDepths = [context].concat(depths); + } + return fn.call(container, context, container.helpers, container.partials, options.data || data, blockParams && [options.blockParams].concat(blockParams), - depths && [context].concat(depths)); + currentDepths); } prog.program = i; prog.depth = depths ? depths.length : 0; diff --git a/spec/builtins.js b/spec/builtins.js index bcacf5908..a7f6204f2 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -32,6 +32,11 @@ describe('builtin helpers', function() { shouldCompileTo(string, {goodbye: function() {return this.foo; }, world: 'world'}, 'cruel world!', 'if with function does not show the contents when returns undefined'); }); + + it('should not change the depth list', function() { + var string = '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}'; + shouldCompileTo(string, {foo: {goodbye: true}, world: 'world'}, 'GOODBYE cruel world!'); + }); }); describe('#with', function() { diff --git a/spec/helpers.js b/spec/helpers.js index 00bcb79cb..94e503f12 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -38,6 +38,19 @@ describe('helpers', function() { shouldCompileTo(string, [{}, helpers], ' {{{{b}}}} {{{{/b}}}} ', 'raw block helper should get nested raw block as raw content'); }); + it('helper block with identical context', function() { + var string = '{{#goodbyes}}{{name}}{{/goodbyes}}'; + var hash = {name: 'Alan'}; + var helpers = {goodbyes: function(options) { + var out = ''; + var byes = ['Goodbye', 'goodbye', 'GOODBYE']; + for (var i = 0, j = byes.length; i < j; i++) { + out += byes[i] + ' ' + options.fn(this) + '! '; + } + return out; + }}; + shouldCompileTo(string, [hash, helpers], 'Goodbye Alan! goodbye Alan! GOODBYE Alan! '); + }); it('helper block with complex lookup expression', function() { var string = '{{#goodbyes}}{{../name}}{{/goodbyes}}'; var hash = {name: 'Alan'}; @@ -45,7 +58,7 @@ describe('helpers', function() { var out = ''; var byes = ['Goodbye', 'goodbye', 'GOODBYE']; for (var i = 0, j = byes.length; i < j; i++) { - out += byes[i] + ' ' + options.fn(this) + '! '; + out += byes[i] + ' ' + options.fn({}) + '! '; } return out; }}; From 5d4b8da344ab5060205678375df298a3d738e862 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 15:59:53 -0500 Subject: [PATCH 30/82] Pass undefined fields to helpers in strict mode This allows for `{{helper foo}}` to still operate under strict mode when `foo` is not defined on the context. This allows helpers to perform whatever existence checks they please so patterns like `{{#if foo}}{{foo}}{{/if}}` can be used to protect against missing values. Fixes #1063 --- lib/handlebars/compiler/compiler.js | 10 +++++++--- lib/handlebars/compiler/javascript-compiler.js | 12 ++++++------ spec/strict.js | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index c1ef47ed7..59a425f47 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -217,13 +217,16 @@ Compiler.prototype = { this.opcode('pushProgram', program); this.opcode('pushProgram', inverse); + path.strict = true; this.accept(path); this.opcode('invokeAmbiguous', name, isBlock); }, simpleSexpr: function(sexpr) { - this.accept(sexpr.path); + let path = sexpr.path; + path.strict = true; + this.accept(path); this.opcode('resolvePossibleLambda'); }, @@ -237,6 +240,7 @@ Compiler.prototype = { } else if (this.options.knownHelpersOnly) { throw new Exception('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr); } else { + path.strict = true; path.falsy = true; this.accept(path); @@ -259,9 +263,9 @@ Compiler.prototype = { this.opcode('pushContext'); } else if (path.data) { this.options.data = true; - this.opcode('lookupData', path.depth, path.parts); + this.opcode('lookupData', path.depth, path.parts, path.strict); } else { - this.opcode('lookupOnContext', path.parts, path.falsy, scoped); + this.opcode('lookupOnContext', path.parts, path.falsy, path.strict, scoped); } }, diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index d39ecb2bf..28f27fd1c 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -390,7 +390,7 @@ JavaScriptCompiler.prototype = { // // Looks up the value of `name` on the current context and pushes // it onto the stack. - lookupOnContext: function(parts, falsy, scoped) { + lookupOnContext: function(parts, falsy, strict, scoped) { let i = 0; if (!scoped && this.options.compat && !this.lastContext) { @@ -401,7 +401,7 @@ JavaScriptCompiler.prototype = { this.pushContext(); } - this.resolvePath('context', parts, i, falsy); + this.resolvePath('context', parts, i, falsy, strict); }, // [lookupBlockParam] @@ -424,19 +424,19 @@ JavaScriptCompiler.prototype = { // On stack, after: data, ... // // Push the data lookup operator - lookupData: function(depth, parts) { + lookupData: function(depth, parts, strict) { if (!depth) { this.pushStackLiteral('data'); } else { this.pushStackLiteral('this.data(data, ' + depth + ')'); } - this.resolvePath('data', parts, 0, true); + this.resolvePath('data', parts, 0, true, strict); }, - resolvePath: function(type, parts, i, falsy) { + resolvePath: function(type, parts, i, falsy, strict) { if (this.options.strict || this.options.assumeObjects) { - this.push(strictLookup(this.options.strict, this, parts, type)); + this.push(strictLookup(this.options.strict && strict, this, parts, type)); return; } diff --git a/spec/strict.js b/spec/strict.js index 2aef13442..05ce35d9e 100644 --- a/spec/strict.js +++ b/spec/strict.js @@ -78,6 +78,23 @@ describe('strict', function() { template({hello: {}}); }, Exception, /"bar" not defined in/); }); + + it('should allow undefined parameters when passed to helpers', function() { + var template = CompilerContext.compile('{{#unless foo}}success{{/unless}}', {strict: true}); + equals(template({}), 'success'); + }); + + it('should allow undefined hash when passed to helpers', function() { + var template = CompilerContext.compile('{{helper value=@foo}}', {strict: true}); + var helpers = { + helper: function(options) { + equals('value' in options.hash, true); + equals(options.hash.value, undefined); + return 'success'; + } + }; + equals(template({}, {helpers: helpers}), 'success'); + }); }); describe('assume objects', function() { From 8e868ab22509c6ca5f5e7419e61c10e6e771b954 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 16:08:23 -0500 Subject: [PATCH 31/82] Always return string responses Certain optimizations for simple templates could result in objects returned by helpers returned rather than their string representation, resulting in some odd edge cases. This ensures that strings are always returned from the API for consistency. Fixes #1054. --- lib/handlebars/runtime.js | 2 +- spec/regressions.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index bc12fc9fd..9dae28407 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -142,7 +142,7 @@ export function template(templateSpec, env) { } } - return templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths); + return '' + templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths); } ret.isTop = true; diff --git a/spec/regressions.js b/spec/regressions.js index f04107046..009fec90b 100644 --- a/spec/regressions.js +++ b/spec/regressions.js @@ -182,4 +182,18 @@ describe('Regressions', function() { shouldCompileTo('{{#each data}}Key: {{@key}}\n{{/each}}', {data: data}, 'Key: \nKey: name\nKey: value\n'); }); + + it('GH-1054: Should handle simple safe string responses', function() { + var root = '{{#wrap}}{{>partial}}{{/wrap}}'; + var partials = { + partial: '{{#wrap}}{{/wrap}}' + }; + var helpers = { + wrap: function(options) { + return new Handlebars.SafeString(options.fn()); + } + }; + + shouldCompileToWithPartials(root, [{}, helpers, partials], true, ''); + }); }); From 9b1f9c7e4434d99f0dd013db4370b1b59c3754b7 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 16:43:09 -0500 Subject: [PATCH 32/82] Style updates --- Gruntfile.js | 2 + bench/.eslintrc | 14 ++++++ bench/dist-size.js | 3 +- bench/index.js | 2 +- bench/templates/arguments.js | 2 +- bench/templates/array-each.js | 10 ++-- bench/templates/array-mustache.js | 6 +-- bench/templates/complex.js | 8 ++-- bench/templates/data.js | 6 +-- bench/templates/depth-1.js | 8 ++-- bench/templates/depth-2.js | 8 ++-- bench/templates/index.js | 2 +- bench/templates/object-mustache.js | 4 +- bench/templates/object.js | 10 ++-- bench/templates/partial-recursion.js | 10 ++-- bench/templates/partial.js | 12 ++--- bench/templates/paths.js | 10 ++-- bench/templates/string.js | 8 ++-- bench/templates/subexpression.js | 6 +-- bench/templates/variables.js | 10 ++-- bench/throughput.js | 29 ++++++------ bench/util/benchwarmer.js | 68 ++++++++++++++-------------- bench/util/template-runner.js | 4 +- tasks/.eslintrc | 16 +++++++ tasks/metrics.js | 2 +- tasks/parser.js | 2 +- tasks/publish.js | 2 +- tasks/util/git.js | 10 ++-- tasks/version.js | 4 +- 29 files changed, 154 insertions(+), 124 deletions(-) create mode 100644 bench/.eslintrc create mode 100644 tasks/.eslintrc diff --git a/Gruntfile.js b/Gruntfile.js index c2ff15e65..945101c79 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,6 +9,8 @@ module.exports = function(grunt) { }, files: [ '*.js', + 'bench/**/*.js', + 'tasks/**/*.js', 'lib/**/!(*.min|parser).js', 'spec/**/!(*.amd|json2|require).js' ] diff --git a/bench/.eslintrc b/bench/.eslintrc new file mode 100644 index 000000000..e03f181ed --- /dev/null +++ b/bench/.eslintrc @@ -0,0 +1,14 @@ +{ + "globals": { + "require": true + }, + "rules": { + // Disabling for tests, for now. + "no-path-concat": 0, + + "no-var": 0, + "no-shadow": 0, + "handle-callback-err": 0, + "no-console": 0 + } +} \ No newline at end of file diff --git a/bench/dist-size.js b/bench/dist-size.js index 9e5fdc0f5..9176054d5 100644 --- a/bench/dist-size.js +++ b/bench/dist-size.js @@ -1,5 +1,4 @@ -var _ = require('underscore'), - async = require('async'), +var async = require('async'), fs = require('fs'), zlib = require('zlib'); diff --git a/bench/index.js b/bench/index.js index 462b046f5..3e357e54a 100644 --- a/bench/index.js +++ b/bench/index.js @@ -2,7 +2,7 @@ var fs = require('fs'); var metrics = fs.readdirSync(__dirname); metrics.forEach(function(metric) { - if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) { + if (metric === 'index.js' || !(/(.*)\.js$/.test(metric))) { return; } diff --git a/bench/templates/arguments.js b/bench/templates/arguments.js index 5480c8d8e..aaa034686 100644 --- a/bench/templates/arguments.js +++ b/bench/templates/arguments.js @@ -1,6 +1,6 @@ module.exports = { helpers: { - foo: function(options) { + foo: function() { return ''; } }, diff --git a/bench/templates/array-each.js b/bench/templates/array-each.js index f1eb1e8e9..50e1c02b8 100644 --- a/bench/templates/array-each.js +++ b/bench/templates/array-each.js @@ -1,7 +1,7 @@ module.exports = { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, - handlebars: "{{#each names}}{{name}}{{/each}}", - dust: "{#names}{name}{/names}", - mustache: "{{#names}}{{name}}{{/names}}", - eco: "<% for item in @names: %><%= item.name %><% end %>" + context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, + handlebars: '{{#each names}}{{name}}{{/each}}', + dust: '{#names}{name}{/names}', + mustache: '{{#names}}{{name}}{{/names}}', + eco: '<% for item in @names: %><%= item.name %><% end %>' }; diff --git a/bench/templates/array-mustache.js b/bench/templates/array-mustache.js index 908f805ea..220c6fe47 100644 --- a/bench/templates/array-mustache.js +++ b/bench/templates/array-mustache.js @@ -1,4 +1,4 @@ module.exports = { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, - handlebars: "{{#names}}{{name}}{{/names}}" -} + context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, + handlebars: '{{#names}}{{name}}{{/names}}' +}; diff --git a/bench/templates/complex.js b/bench/templates/complex.js index ddf361b59..feba874dd 100644 --- a/bench/templates/complex.js +++ b/bench/templates/complex.js @@ -3,13 +3,13 @@ var fs = require('fs'); module.exports = { context: { header: function() { - return "Colors"; + return 'Colors'; }, hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays items: [ - {name: "red", current: true, url: "#Red"}, - {name: "green", current: false, url: "#Green"}, - {name: "blue", current: false, url: "#Blue"} + {name: 'red', current: true, url: '#Red'}, + {name: 'green', current: false, url: '#Green'}, + {name: 'blue', current: false, url: '#Blue'} ] }, diff --git a/bench/templates/data.js b/bench/templates/data.js index f532decd5..be10e8399 100644 --- a/bench/templates/data.js +++ b/bench/templates/data.js @@ -1,4 +1,4 @@ module.exports = { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, - handlebars: "{{#each names}}{{@index}}{{name}}{{/each}}" -} + context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, + handlebars: '{{#each names}}{{@index}}{{name}}{{/each}}' +}; diff --git a/bench/templates/depth-1.js b/bench/templates/depth-1.js index 74809bca8..0f2576f43 100644 --- a/bench/templates/depth-1.js +++ b/bench/templates/depth-1.js @@ -1,6 +1,6 @@ module.exports = { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}], foo: 'bar' }, - handlebars: "{{#each names}}{{../foo}}{{/each}}", - mustache: "{{#names}}{{foo}}{{/names}}", - eco: "<% for item in @names: %><%= @foo %><% end %>" + context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}], foo: 'bar' }, + handlebars: '{{#each names}}{{../foo}}{{/each}}', + mustache: '{{#names}}{{foo}}{{/names}}', + eco: '<% for item in @names: %><%= @foo %><% end %>' }; diff --git a/bench/templates/depth-2.js b/bench/templates/depth-2.js index 1d38baa4e..bff6ce850 100644 --- a/bench/templates/depth-2.js +++ b/bench/templates/depth-2.js @@ -1,6 +1,6 @@ module.exports = { - context: { names: [{bat: 'foo', name: ["Moe"]}, {bat: 'foo', name: ["Larry"]}, {bat: 'foo', name: ["Curly"]}, {bat: 'foo', name: ["Shemp"]}], foo: 'bar' }, - handlebars: "{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}", - mustache: "{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}", - eco: "<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>" + context: { names: [{bat: 'foo', name: ['Moe']}, {bat: 'foo', name: ['Larry']}, {bat: 'foo', name: ['Curly']}, {bat: 'foo', name: ['Shemp']}], foo: 'bar' }, + handlebars: '{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}', + mustache: '{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}', + eco: '<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>' }; diff --git a/bench/templates/index.js b/bench/templates/index.js index a718ea388..943f9cdfe 100644 --- a/bench/templates/index.js +++ b/bench/templates/index.js @@ -2,7 +2,7 @@ var fs = require('fs'); var templates = fs.readdirSync(__dirname); templates.forEach(function(template) { - if (template === 'index.js' || !/(.*)\.js$/.test(template)) { + if (template === 'index.js' || !(/(.*)\.js$/.test(template))) { return; } module.exports[RegExp.$1] = require('./' + RegExp.$1); diff --git a/bench/templates/object-mustache.js b/bench/templates/object-mustache.js index 52dbc26e3..41774b73a 100644 --- a/bench/templates/object-mustache.js +++ b/bench/templates/object-mustache.js @@ -1,4 +1,4 @@ module.exports = { - context: { person: { name: "Larry", age: 45 } }, - handlebars: "{{#person}}{{name}}{{age}}{{/person}}" + context: { person: { name: 'Larry', age: 45 } }, + handlebars: '{{#person}}{{name}}{{age}}{{/person}}' }; diff --git a/bench/templates/object.js b/bench/templates/object.js index fef127ada..084c070ad 100644 --- a/bench/templates/object.js +++ b/bench/templates/object.js @@ -1,7 +1,7 @@ module.exports = { - context: { person: { name: "Larry", age: 45 } }, - handlebars: "{{#with person}}{{name}}{{age}}{{/with}}", - dust: "{#person}{name}{age}{/person}", - eco: "<%= @person.name %><%= @person.age %>", - mustache: "{{#person}}{{name}}{{age}}{{/person}}" + context: { person: { name: 'Larry', age: 45 } }, + handlebars: '{{#with person}}{{name}}{{age}}{{/with}}', + dust: '{#person}{name}{age}{/person}', + eco: '<%= @person.name %><%= @person.age %>', + mustache: '{{#person}}{{name}}{{age}}{{/person}}' }; diff --git a/bench/templates/partial-recursion.js b/bench/templates/partial-recursion.js index 9d604fd02..b903553b5 100644 --- a/bench/templates/partial-recursion.js +++ b/bench/templates/partial-recursion.js @@ -1,10 +1,10 @@ module.exports = { context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] }, partials: { - mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" }, - handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" } + mustache: { recursion: '{{name}}{{#kids}}{{>recursion}}{{/kids}}' }, + handlebars: { recursion: '{{name}}{{#each kids}}{{>recursion}}{{/each}}' } }, - handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}", - dust: "{name}{#kids}{>recursion:./}{/kids}", - mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" + handlebars: '{{name}}{{#each kids}}{{>recursion}}{{/each}}', + dust: '{name}{#kids}{>recursion:./}{/kids}', + mustache: '{{name}}{{#kids}}{{>recursion}}{{/kids}}' }; diff --git a/bench/templates/partial.js b/bench/templates/partial.js index a6e663156..949e9c075 100644 --- a/bench/templates/partial.js +++ b/bench/templates/partial.js @@ -1,11 +1,11 @@ module.exports = { - context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] }, + context: { peeps: [{name: 'Moe', count: 15}, {name: 'Larry', count: 5}, {name: 'Curly', count: 1}] }, partials: { - mustache: { variables: "Hello {{name}}! You have {{count}} new messages." }, - handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." } + mustache: { variables: 'Hello {{name}}! You have {{count}} new messages.' }, + handlebars: { variables: 'Hello {{name}}! You have {{count}} new messages.' } }, - handlebars: "{{#each peeps}}{{>variables}}{{/each}}", - dust: "{#peeps}{>variables/}{/peeps}", - mustache: "{{#peeps}}{{>variables}}{{/peeps}}" + handlebars: '{{#each peeps}}{{>variables}}{{/each}}', + dust: '{#peeps}{>variables/}{/peeps}', + mustache: '{{#peeps}}{{>variables}}{{/peeps}}' }; diff --git a/bench/templates/paths.js b/bench/templates/paths.js index d84e06152..fed039d51 100644 --- a/bench/templates/paths.js +++ b/bench/templates/paths.js @@ -1,7 +1,7 @@ module.exports = { - context: { person: { name: {bar: {baz: "Larry"}}, age: 45 } }, - handlebars: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}", - dust: "{person.name.bar.baz}{person.age}{person.foo}{animal.age}", - eco: "<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>", - mustache: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}" + context: { person: { name: {bar: {baz: 'Larry'}}, age: 45 } }, + handlebars: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}', + dust: '{person.name.bar.baz}{person.age}{person.foo}{animal.age}', + eco: '<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>', + mustache: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}' }; diff --git a/bench/templates/string.js b/bench/templates/string.js index 335e37cf9..6b0e94a74 100644 --- a/bench/templates/string.js +++ b/bench/templates/string.js @@ -1,7 +1,7 @@ module.exports = { context: {}, - handlebars: "Hello world", - dust: "Hello world", - mustache: "Hello world", - eco: "Hello world" + handlebars: 'Hello world', + dust: 'Hello world', + mustache: 'Hello world', + eco: 'Hello world' }; diff --git a/bench/templates/subexpression.js b/bench/templates/subexpression.js index 261c22d01..659b53041 100644 --- a/bench/templates/subexpression.js +++ b/bench/templates/subexpression.js @@ -4,11 +4,11 @@ module.exports = { return 'foo ' + value; }, header: function() { - return "Colors"; + return 'Colors'; } }, - handlebars: "{{echo (header)}}", - eco: "<%= @echo(@header()) %>" + handlebars: '{{echo (header)}}', + eco: '<%= @echo(@header()) %>' }; module.exports.context = module.exports.helpers; diff --git a/bench/templates/variables.js b/bench/templates/variables.js index d354238b1..41e4feafe 100644 --- a/bench/templates/variables.js +++ b/bench/templates/variables.js @@ -1,8 +1,8 @@ module.exports = { - context: {name: "Mick", count: 30}, - handlebars: "Hello {{name}}! You have {{count}} new messages.", - dust: "Hello {name}! You have {count} new messages.", - mustache: "Hello {{name}}! You have {{count}} new messages.", - eco: "Hello <%= @name %>! You have <%= @count %> new messages." + context: {name: 'Mick', count: 30}, + handlebars: 'Hello {{name}}! You have {{count}} new messages.', + dust: 'Hello {name}! You have {count} new messages.', + mustache: 'Hello {{name}}! You have {{count}} new messages.', + eco: 'Hello <%= @name %>! You have <%= @count %> new messages.' }; diff --git a/bench/throughput.js b/bench/throughput.js index d27a94d31..b0b229fae 100644 --- a/bench/throughput.js +++ b/bench/throughput.js @@ -1,23 +1,22 @@ var _ = require('underscore'), runner = require('./util/template-runner'), - templates = require('./templates'), eco, dust, Handlebars, Mustache, eco; try { - dust = require("dustjs-linkedin"); + dust = require('dustjs-linkedin'); } catch (err) { /* NOP */ } try { - Mustache = require("mustache"); + Mustache = require('mustache'); } catch (err) { /* NOP */ } try { - eco = require("eco"); + eco = require('eco'); } catch (err) { /* NOP */ } function error() { - throw new Error("EWOT"); + throw new Error('EWOT'); } function makeSuite(bench, name, template, handlebarsOnly) { @@ -34,19 +33,19 @@ function makeSuite(bench, name, template, handlebarsOnly) { mustacheOut; var handlebar = Handlebars.compile(template.handlebars, {data: false}), - compat = Handlebars.compile(template.handlebars, {data: false, compat: true}), + compat = Handlebars.compile(template.handlebars, {data: false, compat: true}), options = {helpers: template.helpers}; - _.each(template.partials && template.partials.handlebars, function(partial, name) { + _.each(template.partials && template.partials.handlebars, function(partial) { Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false})); }); handlebarsOut = handlebar(context, options); - bench("handlebars", function() { + bench('handlebars', function() { handlebar(context, options); }); compatOut = compat(context, options); - bench("compat", function() { + bench('compat', function() { compat(context, options); }); @@ -61,8 +60,8 @@ function makeSuite(bench, name, template, handlebarsOnly) { dust.render(templateName, context, function(err, out) { dustOut = out; }); - bench("dust", function() { - dust.render(templateName, context, function(err, out) { }); + bench('dust', function() { + dust.render(templateName, context, function() {}); }); } else { bench('dust', error); @@ -75,11 +74,11 @@ function makeSuite(bench, name, template, handlebarsOnly) { ecoOut = ecoTemplate(context); - bench("eco", function() { + bench('eco', function() { ecoTemplate(context); }); } else { - bench("eco", error); + bench('eco', error); } } @@ -90,11 +89,11 @@ function makeSuite(bench, name, template, handlebarsOnly) { if (mustacheSource) { mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials); - bench("mustache", function() { + bench('mustache', function() { Mustache.to_html(mustacheSource, context, mustachePartials); }); } else { - bench("mustache", error); + bench('mustache', error); } } diff --git a/bench/util/benchwarmer.js b/bench/util/benchwarmer.js index 7496a3e9e..78b1a347e 100644 --- a/bench/util/benchwarmer.js +++ b/bench/util/benchwarmer.js @@ -1,7 +1,7 @@ var _ = require('underscore'), - Benchmark = require("benchmark"); + Benchmark = require('benchmark'); -var BenchWarmer = function(names) { +function BenchWarmer() { this.benchmarks = []; this.currentBenches = []; this.names = []; @@ -9,9 +9,9 @@ var BenchWarmer = function(names) { this.minimum = Infinity; this.maximum = -Infinity; this.errors = {}; -}; +} -var print = require("sys").print; +var print = require('sys').print; BenchWarmer.prototype = { winners: function(benches) { @@ -29,7 +29,7 @@ BenchWarmer.prototype = { }); }, push: function(name, fn) { - if(this.names.indexOf(name) == -1) { + if (this.names.indexOf(name) == -1) { this.names.push(name); } @@ -37,9 +37,9 @@ BenchWarmer.prototype = { this.first = false; var bench = new Benchmark(fn, { - name: this.suiteName + ": " + name, + name: this.suiteName + ': ' + name, onComplete: function() { - if(first) { self.startLine(suiteName); } + if (first) { self.startLine(suiteName); } self.writeBench(bench); self.currentBenches.push(bench); }, onError: function() { @@ -58,7 +58,7 @@ BenchWarmer.prototype = { this.printHeader('ops/msec', true); Benchmark.invoke(this.benchmarks, { - name: "run", + name: 'run', onComplete: function() { self.scaleTimes(); @@ -76,7 +76,7 @@ BenchWarmer.prototype = { print('\n'); var errors = false, prop, bench; - for(prop in self.errors) { + for (prop in self.errors) { if (self.errors.hasOwnProperty(prop) && self.errors[prop].error.message !== 'EWOT') { errors = true; @@ -84,18 +84,18 @@ BenchWarmer.prototype = { } } - if(errors) { - print("\n\nErrors:\n"); - for(prop in self.errors) { + if (errors) { + print('\n\nErrors:\n'); + for (prop in self.errors) { if (self.errors.hasOwnProperty(prop) && self.errors[prop].error.message !== 'EWOT') { bench = self.errors[prop]; - print("\n" + bench.name + ":\n"); + print('\n' + bench.name + ':\n'); print(bench.error.message); - if(bench.error.stack) { - print(bench.error.stack.join("\n")); + if (bench.error.stack) { + print(bench.error.stack.join('\n')); } - print("\n"); + print('\n'); } } } @@ -104,7 +104,7 @@ BenchWarmer.prototype = { } }); - print("\n"); + print('\n'); }, scaleTimes: function() { @@ -121,10 +121,10 @@ BenchWarmer.prototype = { printHeader: function(title, winners) { var benchSize = 0, names = this.names, i, l; - for(i=0, l=names.length; i Date: Mon, 3 Aug 2015 17:27:38 -0500 Subject: [PATCH 33/82] Fix partial handling with different context --- lib/handlebars/runtime.js | 2 +- spec/partials.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 9dae28407..744e6eb76 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -136,7 +136,7 @@ export function template(templateSpec, env) { blockParams = templateSpec.useBlockParams ? [] : undefined; if (templateSpec.useDepths) { if (options.depths) { - depths = context !== options.depths[0] ? [context].concat(depths) : options.depths; + depths = context !== options.depths[0] ? [context].concat(options.depths) : options.depths; } else { depths = [context]; } diff --git a/spec/partials.js b/spec/partials.js index b2fc9e074..a9cd3dd99 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -238,6 +238,12 @@ describe('partials', function() { var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '); }); + it('partials can access parents with custom context', function() { + var string = 'Dudes: {{#dudes}}{{> dude "test"}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) {{root}} '; + var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '); + }); it('partials can access parents without data', function() { var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}'; var partial = '{{name}} ({{url}}) {{root}} '; From 324d61572655e251ad2b06032a04cfab09bb0076 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 17:28:05 -0500 Subject: [PATCH 34/82] Enforce 100% code coverage --- lib/handlebars/compiler/code-gen.js | 13 +++++++------ lib/handlebars/compiler/helpers.js | 17 +++++++++-------- spec/ast.js | 11 +++++++++++ spec/blocks.js | 7 +++++++ tasks/test.js | 14 +++++++++++++- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index bc7bc0703..3af4f8cb1 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -90,7 +90,8 @@ CodeGen.prototype = { } }, - empty: function(loc = this.currentLocation || {start: {}}) { + empty: function() { + let loc = this.currentLocation || {start: {}}; return new SourceNode(loc.start.line, loc.start.column, this.srcFile); }, wrap: function(chunk, loc = this.currentLocation || {start: {}}) { @@ -137,22 +138,22 @@ CodeGen.prototype = { }, - generateList: function(entries, loc) { - let ret = this.empty(loc); + generateList: function(entries) { + let ret = this.empty(); for (let i = 0, len = entries.length; i < len; i++) { if (i) { ret.add(','); } - ret.add(castChunk(entries[i], this, loc)); + ret.add(castChunk(entries[i], this)); } return ret; }, - generateArray: function(entries, loc) { - let ret = this.generateList(entries, loc); + generateArray: function(entries) { + let ret = this.generateList(entries); ret.prepend('['); ret.add(']'); diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 1c8ab0d3b..f2edfa110 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -124,19 +124,20 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver export function prepareProgram(statements, loc) { if (!loc && statements.length) { - const first = statements[0].loc, - last = statements[statements.length - 1].loc; + const firstLoc = statements[0].loc, + lastLoc = statements[statements.length - 1].loc; - if (first && last) { + /* istanbul ignore else */ + if (firstLoc && lastLoc) { loc = { - source: first.source, + source: firstLoc.source, start: { - line: first.start.line, - column: first.start.column + line: firstLoc.start.line, + column: firstLoc.start.column }, end: { - line: last.end.line, - column: last.end.column + line: lastLoc.end.line, + column: lastLoc.end.column } }; } diff --git a/spec/ast.js b/spec/ast.js index ce4c0909a..627554cbb 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -100,6 +100,10 @@ describe('ast', function() { }); describe('PartialStatement', function() { + it('provides default params', function() { + var pn = new handlebarsEnv.AST.PartialStatement('so_partial', undefined, {}, {}, LOCATION_INFO); + equals(pn.params.length, 0); + }); it('stores location info', function() { var pn = new handlebarsEnv.AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO); testLocationInfoStorage(pn); @@ -113,6 +117,13 @@ describe('ast', function() { }); }); + describe('SubExpression', function() { + it('provides default params', function() { + var pn = new handlebarsEnv.AST.SubExpression('path', undefined, {}, LOCATION_INFO); + equals(pn.params.length, 0); + }); + }); + describe('Line Numbers', function() { var ast, body; diff --git a/spec/blocks.js b/spec/blocks.js index 80f1580e6..3584ed788 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -65,6 +65,13 @@ describe('blocks', function() { shouldCompileTo(string, hash, 'Goodbye cruel sad OMG!'); }); + it('works with cached blocks', function() { + var template = CompilerContext.compile('{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}', {data: false}); + + var result = template({person: [{first: 'Alan', last: 'Johnson'}, {first: 'Alan', last: 'Johnson'}]}); + equals(result, 'Alan JohnsonAlan Johnson'); + }); + describe('inverted sections', function() { it('inverted sections with unset value', function() { var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'; diff --git a/tasks/test.js b/tasks/test.js index ad8a911d3..74473244a 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -40,5 +40,17 @@ module.exports = function(grunt) { done(); }); }); - grunt.registerTask('test', ['test:bin', 'test:cov']); + + grunt.registerTask('test:check-cov', function() { + var done = this.async(); + + var runner = childProcess.fork('node_modules/.bin/istanbul', ['check-coverage', '--statements', '100', '--functions', '100', '--branches', '100', '--lines 100'], {stdio: 'inherit'}); + runner.on('close', function(code) { + if (code != 0) { + grunt.fatal('Coverage check failed: ' + code); + } + done(); + }); + }); + grunt.registerTask('test', ['test:bin', 'test:cov', 'test:check-cov']); }; From fac71ce700a8e8a4a64e4ebc68e8e94270210dee Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 17:59:23 -0500 Subject: [PATCH 35/82] Fix incorrect variable removal --- bench/throughput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bench/throughput.js b/bench/throughput.js index b0b229fae..9f1f79858 100644 --- a/bench/throughput.js +++ b/bench/throughput.js @@ -35,8 +35,8 @@ function makeSuite(bench, name, template, handlebarsOnly) { var handlebar = Handlebars.compile(template.handlebars, {data: false}), compat = Handlebars.compile(template.handlebars, {data: false, compat: true}), options = {helpers: template.helpers}; - _.each(template.partials && template.partials.handlebars, function(partial) { - Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false})); + _.each(template.partials && template.partials.handlebars, function(partial, partialName) { + Handlebars.registerPartial(partialName, Handlebars.compile(partial, {data: false})); }); handlebarsOut = handlebar(context, options); From 85750f8d0a8a02bfb9c05eba90b635899c253f91 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 18:51:46 -0500 Subject: [PATCH 36/82] Use += in printer --- lib/handlebars/compiler/printer.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 691a3567c..107d4b652 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -15,10 +15,10 @@ PrintVisitor.prototype.pad = function(string) { let out = ''; for (let i = 0, l = this.padding; i < l; i++) { - out = out + ' '; + out += ' '; } - out = out + string + '\n'; + out += string + '\n'; return out; }; @@ -37,7 +37,7 @@ PrintVisitor.prototype.Program = function(program) { } for (i = 0, l = body.length; i < l; i++) { - out = out + this.accept(body[i]); + out += this.accept(body[i]); } this.padding--; @@ -52,20 +52,20 @@ PrintVisitor.prototype.MustacheStatement = function(mustache) { PrintVisitor.prototype.BlockStatement = function(block) { let out = ''; - out = out + this.pad('BLOCK:'); + out += this.pad('BLOCK:'); this.padding++; - out = out + this.pad(this.SubExpression(block)); + out += this.pad(this.SubExpression(block)); if (block.program) { - out = out + this.pad('PROGRAM:'); + out += this.pad('PROGRAM:'); this.padding++; - out = out + this.accept(block.program); + out += this.accept(block.program); this.padding--; } if (block.inverse) { if (block.program) { this.padding++; } - out = out + this.pad('{{^}}'); + out += this.pad('{{^}}'); this.padding++; - out = out + this.accept(block.inverse); + out += this.accept(block.inverse); this.padding--; if (block.program) { this.padding--; } } From 93b07605cddfe3736cf758e9f0e4fa0a92b0bb3d Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 20:31:57 -0500 Subject: [PATCH 37/82] Bulletproof AST.helpers.helperExpression Avoid undefined values and potential false positives from other type values such as partials. Fixes #1055 --- lib/handlebars/compiler/ast.js | 4 ++- spec/ast.js | 66 ++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 08b127f77..599dab8d2 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -131,7 +131,9 @@ let AST = { // * it is an eligible helper, and // * it has at least one parameter or hash segment helperExpression: function(node) { - return !!(node.type === 'SubExpression' || node.params.length || node.hash); + return (node.type === 'SubExpression') + || ((node.type === 'MustacheStatement' || node.type === 'BlockStatement') + && !!((node.params && node.params.length) || node.hash)); }, scopedId: function(path) { diff --git a/spec/ast.js b/spec/ast.js index 627554cbb..dc5410fa2 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -3,6 +3,8 @@ describe('ast', function() { return; } + var AST = Handlebars.AST; + var LOCATION_INFO = { start: { line: 1, @@ -23,7 +25,7 @@ describe('ast', function() { describe('MustacheStatement', function() { it('should store args', function() { - var mustache = new handlebarsEnv.AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO); + var mustache = new AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO); equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); testLocationInfoStorage(mustache); @@ -37,8 +39,8 @@ describe('ast', function() { }); it('stores location info', function() { - var mustacheNode = new handlebarsEnv.AST.MustacheStatement([{ original: 'foo'}], null, null, false, {}); - var block = new handlebarsEnv.AST.BlockStatement( + var mustacheNode = new AST.MustacheStatement([{ original: 'foo'}], null, null, false, {}); + var block = new AST.BlockStatement( mustacheNode, null, null, {body: []}, @@ -52,78 +54,114 @@ describe('ast', function() { }); describe('PathExpression', function() { it('stores location info', function() { - var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO); + var idNode = new AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO); testLocationInfoStorage(idNode); }); }); describe('Hash', function() { it('stores location info', function() { - var hash = new handlebarsEnv.AST.Hash([], LOCATION_INFO); + var hash = new AST.Hash([], LOCATION_INFO); testLocationInfoStorage(hash); }); }); describe('ContentStatement', function() { it('stores location info', function() { - var content = new handlebarsEnv.AST.ContentStatement('HI', LOCATION_INFO); + var content = new AST.ContentStatement('HI', LOCATION_INFO); testLocationInfoStorage(content); }); }); describe('CommentStatement', function() { it('stores location info', function() { - var comment = new handlebarsEnv.AST.CommentStatement('HI', {}, LOCATION_INFO); + var comment = new AST.CommentStatement('HI', {}, LOCATION_INFO); testLocationInfoStorage(comment); }); }); describe('NumberLiteral', function() { it('stores location info', function() { - var integer = new handlebarsEnv.AST.NumberLiteral('6', LOCATION_INFO); + var integer = new AST.NumberLiteral('6', LOCATION_INFO); testLocationInfoStorage(integer); }); }); describe('StringLiteral', function() { it('stores location info', function() { - var string = new handlebarsEnv.AST.StringLiteral('6', LOCATION_INFO); + var string = new AST.StringLiteral('6', LOCATION_INFO); testLocationInfoStorage(string); }); }); describe('BooleanLiteral', function() { it('stores location info', function() { - var bool = new handlebarsEnv.AST.BooleanLiteral('true', LOCATION_INFO); + var bool = new AST.BooleanLiteral('true', LOCATION_INFO); testLocationInfoStorage(bool); }); }); describe('PartialStatement', function() { it('provides default params', function() { - var pn = new handlebarsEnv.AST.PartialStatement('so_partial', undefined, {}, {}, LOCATION_INFO); + var pn = new AST.PartialStatement('so_partial', undefined, {}, {}, LOCATION_INFO); equals(pn.params.length, 0); }); it('stores location info', function() { - var pn = new handlebarsEnv.AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO); + var pn = new AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); describe('Program', function() { it('storing location info', function() { - var pn = new handlebarsEnv.AST.Program([], null, {}, LOCATION_INFO); + var pn = new AST.Program([], null, {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); describe('SubExpression', function() { it('provides default params', function() { - var pn = new handlebarsEnv.AST.SubExpression('path', undefined, {}, LOCATION_INFO); + var pn = new AST.SubExpression('path', undefined, {}, LOCATION_INFO); equals(pn.params.length, 0); }); }); + describe('helpers', function() { + describe('#helperExpression', function() { + it('should handle mustache statements', function() { + equals(AST.helpers.helperExpression(new AST.MustacheStatement('foo', [], undefined, false, {}, LOCATION_INFO)), false); + equals(AST.helpers.helperExpression(new AST.MustacheStatement('foo', [1], undefined, false, {}, LOCATION_INFO)), true); + equals(AST.helpers.helperExpression(new AST.MustacheStatement('foo', [], {}, false, {}, LOCATION_INFO)), true); + }); + it('should handle block statements', function() { + equals(AST.helpers.helperExpression(new AST.BlockStatement('foo', [], undefined, false, {}, LOCATION_INFO)), false); + equals(AST.helpers.helperExpression(new AST.BlockStatement('foo', [1], undefined, false, {}, LOCATION_INFO)), true); + equals(AST.helpers.helperExpression(new AST.BlockStatement('foo', [], {}, false, {}, LOCATION_INFO)), true); + }); + it('should handle subexpressions', function() { + equals(AST.helpers.helperExpression(new AST.SubExpression()), true); + }); + it('should work with non-helper nodes', function() { + equals(AST.helpers.helperExpression(new AST.Program([], [], {}, LOCATION_INFO)), false); + + equals(AST.helpers.helperExpression(new AST.PartialStatement()), false); + equals(AST.helpers.helperExpression(new AST.ContentStatement('a', LOCATION_INFO)), false); + equals(AST.helpers.helperExpression(new AST.CommentStatement('a', {}, LOCATION_INFO)), false); + + equals(AST.helpers.helperExpression(new AST.PathExpression(false, 0, ['a'], 'a', LOCATION_INFO)), false); + + equals(AST.helpers.helperExpression(new AST.StringLiteral('a', LOCATION_INFO)), false); + equals(AST.helpers.helperExpression(new AST.NumberLiteral(1, LOCATION_INFO)), false); + equals(AST.helpers.helperExpression(new AST.BooleanLiteral(true, LOCATION_INFO)), false); + equals(AST.helpers.helperExpression(new AST.UndefinedLiteral(LOCATION_INFO)), false); + equals(AST.helpers.helperExpression(new AST.NullLiteral(LOCATION_INFO)), false); + + equals(AST.helpers.helperExpression(new AST.Hash([], LOCATION_INFO)), false); + equals(AST.helpers.helperExpression(new AST.HashPair('foo', 'bar', LOCATION_INFO)), false); + }); + }); + }); + describe('Line Numbers', function() { var ast, body; From bd643ce12b4ce5223f4db5cc124456994f4b481b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 21:13:47 -0500 Subject: [PATCH 38/82] Pull out duplicated documentation from readme Fixes #1006 --- README.markdown | 281 ++---------------------------------------------- 1 file changed, 10 insertions(+), 271 deletions(-) diff --git a/README.markdown b/README.markdown index 6500966af..d39d1312e 100644 --- a/README.markdown +++ b/README.markdown @@ -57,231 +57,25 @@ var result = template(data); // ``` +Full documentation and more examples are at [handlebarsjs.com](http://handlebarsjs.com/). -Registering Helpers -------------------- - -You can register helpers that Handlebars will use when evaluating your -template. Here's an example, which assumes that your objects have a URL -embedded in them, as well as the text for a link: - -```js -Handlebars.registerHelper('link_to', function() { - return new Handlebars.SafeString("" + Handlebars.Utils.escapeExpression(this.body) + ""); -}); - -var context = { posts: [{url: "/hello-world", body: "Hello World!"}] }; -var source = "
    {{#posts}}
  • {{link_to}}
  • {{/posts}}
" - -var template = Handlebars.compile(source); -template(context); - -// Would render: -// -// -``` - -Helpers take precedence over fields defined on the context. To access a field -that is masked by a helper, a path reference may be used. In the example above -a field named `link_to` on the `context` object would be referenced using: - -``` -{{./link_to}} -``` - -Escaping --------- - -By default, the `{{expression}}` syntax will escape its contents. This -helps to protect you against accidental XSS problems caused by malicious -data passed from the server as JSON. - -To explicitly *not* escape the contents, use the triple-mustache -(`{{{}}}`). You have seen this used in the above example. +Precompiling Templates +---------------------- +Handlebars allows templates to be precompiled and included as javascript code rather than the handlebars template allowing for faster startup time. Full details are located [here](http://handlebarsjs.com/precompilation.html). Differences Between Handlebars.js and Mustache ---------------------------------------------- Handlebars.js adds a couple of additional features to make writing 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 -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. - -To display data from descendant contexts, use the `.` character. So, for -example, if your data were structured like: - -```js -var data = {"person": { "name": "Alan" }, "company": {"name": "Rad, Inc." } }; -``` - -You could display the person's name from the top-level context with the -following expression: - -``` -{{person.name}} -``` - -You can backtrack using `../`. For example, if you've already traversed -into the person object you could still display the company's name with -an expression like `{{../company.name}}`, so: - -``` -{{#with person}}{{name}} - {{../company.name}}{{/with}} -``` - -would render: - -``` -Alan - Rad, Inc. -``` - -### Strings - -When calling a helper, you can pass paths or Strings as parameters. For -instance: - -```js -Handlebars.registerHelper('link_to', function(title, options) { - return "" + title + "!" -}); - -var context = { posts: [{url: "/hello-world", body: "Hello World!"}] }; -var source = '
    {{#posts}}
  • {{{link_to "Post"}}}
  • {{/posts}}
' - -var template = Handlebars.compile(source); -template(context); - -// Would render: -// -// -``` - -When you pass a String as a parameter to a helper, the literal String -gets passed to the helper function. - - -### Block Helpers - -Handlebars.js also adds the ability to define block helpers. Block -helpers are functions that can be called from anywhere in the template. -Here's an example: - -```js -var source = "
    {{#people}}
  • {{#link}}{{name}}{{/link}}
  • {{/people}}
"; -Handlebars.registerHelper('link', function(options) { - return '' + options.fn(this) + ''; -}); -var template = Handlebars.compile(source); - -var data = { "people": [ - { "name": "Alan", "id": 1 }, - { "name": "Yehuda", "id": 2 } - ]}; -template(data); - -// Should render: -// -``` - -Whenever the block helper is called it is given one or more parameters, -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`. - -Block helpers have the same syntax as mustache sections but should not be -confused with one another. Sections are akin to an implicit `each` or -`with` statement depending on the input data and helpers are explicit -pieces of code that are free to implement whatever behavior they like. -The [mustache spec](http://mustache.github.io/mustache.5.html) -defines the exact behavior of sections. In the case of name conflicts, -helpers are given priority. - -### Partials - -You can register additional templates as partials, which will be used by -Handlebars when it encounters a partial (`{{> partialName}}`). Partials -can either be String templates or compiled template functions. Here's an -example: - -```js -var source = "
    {{#people}}
  • {{> link}}
  • {{/people}}
"; - -Handlebars.registerPartial('link', '{{name}}') -var template = Handlebars.compile(source); - -var data = { "people": [ - { "name": "Alan", "id": 1 }, - { "name": "Yehuda", "id": 2 } - ]}; - -template(data); - -// Should render: -// -``` - -Partials can also accept parameters - -```js -var source = "
{{> roster rosterProperties people=listOfPeople}}
"; - -Handlebars.registerPartial('roster', '

{{rosterName}}

{{#people}}{{id}}: {{name}}{{/people}}') -var template = Handlebars.compile(source); - -var data = { - "listOfPeople": [ - { "name": "Alan", "id": 1 }, - { "name": "Yehuda", "id": 2 } - ], - "rosterProperties": { - "rosterName": "Cool People" - } -}; - -template(data); - -// Should render: -//
-//

Cool People

-// 1: Alan -// 2: Yehuda -//
- -``` - -### Comments - -You can add comments to your templates with the following syntax: - -```js -{{! This is a comment }} -``` - -You can also use real html comments if you want them to end up in the output. - -```html -
- {{! This comment will not end up in the output }} - -
-``` +- [Nested Paths](http://handlebarsjs.com/#paths) +- [Helpers](http://handlebarsjs.com/#helpers) +- [Block Expressions](http://handlebarsjs.com/#block-expressions) +- [Literal Values](http://handlebarsjs.com/#literals) +- [Delimited Comments](http://handlebarsjs.com/#comments) +Block expressions have the same syntax as mustache sections but should not be confused with one another. Sections are akin to an implicit `each` or `with` statement depending on the input data and helpers are explicit pieces of code that are free to implement whatever behavior they like. The [mustache spec](http://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority. ### Compatibility @@ -291,61 +85,6 @@ There are a few Mustache behaviors that Handlebars does not implement. - Alternative delimiters are not supported. -Precompiling Templates ----------------------- - -Handlebars allows templates to be precompiled and included as javascript -code rather than the handlebars template allowing for faster startup time. - -### Installation -The precompiler script may be installed via npm using the `npm install -g handlebars` -command. - -### Usage - -
-Precompile handlebar templates.
-Usage: handlebars template...
-
-Options:
-  -a, --amd            Create an AMD format function (allows loading with RequireJS)          [boolean]
-  -f, --output         Output File                                                            [string]
-  -k, --known          Known helpers                                                          [string]
-  -o, --knownOnly      Known helpers only                                                     [boolean]
-  -m, --min            Minimize output                                                        [boolean]
-  -s, --simple         Output template function only.                                         [boolean]
-  -r, --root           Template root. Base value that will be stripped from template names.   [string]
-  -c, --commonjs       Exports CommonJS style, path to Handlebars module                      [string]
-  -h, --handlebarPath  Path to handlebar.js (only valid for amd-style)                        [string]
-  -n, --namespace      Template namespace                                                     [string]
-  -p, --partial        Compiling a partial template                                           [boolean]
-  -d, --data           Include data when compiling                                            [boolean]
-  -e, --extension      Template extension.                                                    [string]
-  -b, --bom            Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
-
- -If using the precompiler's normal mode, the resulting templates will be -stored to the `Handlebars.templates` object using the relative template -name sans the extension. These templates may be executed in the same -manner as templates. - -If using the simple mode the precompiler will generate a single -javascript method. To execute this method it must be passed to -the `Handlebars.template` method and the resulting object may be used as normal. - -### Optimizations - -- Rather than using the full _handlebars.js_ library, implementations that - do not need to compile templates at runtime may include _handlebars.runtime.js_ - whose min+gzip size is approximately 1k. -- If a helper is known to exist in the target environment they may be defined - using the `--known name` argument may be used to optimize accesses to these - helpers for size and speed. -- When all helpers are known in advance the `--knownOnly` argument may be used - to optimize all block helper references. -- Implementations that do not use `@data` variables can improve performance of - iteration centric templates by specifying `{data: false}` in the compiler options. - Supported Environments ---------------------- From 72eb10ca4b737cc40cd712e26e8e3cf3e1ba22ce Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 22:27:25 -0500 Subject: [PATCH 39/82] Revert "Pull sauce tests out of CI" This reverts commit e2ba22eaad24575ab3cb235b8fc36683acf610c2. --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 945101c79..fc2342191 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -230,7 +230,7 @@ module.exports = function(grunt) { grunt.registerTask('bench', ['metrics']); grunt.registerTask('sauce', process.env.SAUCE_USERNAME ? ['tests', 'connect', 'saucelabs-mocha'] : []); - grunt.registerTask('travis', process.env.PUBLISH ? ['default', 'metrics', 'publish:latest'] : ['default']); + grunt.registerTask('travis', process.env.PUBLISH ? ['default', 'sauce', 'metrics', 'publish:latest'] : ['default']); grunt.registerTask('dev', ['clean', 'connect', 'watch']); grunt.registerTask('default', ['clean', 'build', 'test', 'release']); From c532b89af655c88aeb4147aac2552415f9f220c3 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 22:45:39 -0500 Subject: [PATCH 40/82] Fix parser declaration under amd builds --- src/parser-suffix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser-suffix.js b/src/parser-suffix.js index e0f37eb1f..1f69f7a44 100644 --- a/src/parser-suffix.js +++ b/src/parser-suffix.js @@ -1,2 +1,2 @@ exports.__esModule = true; -module.exports['default'] = handlebars; +exports['default'] = handlebars; From bb9831b73eb4fdab94caaceb373e4fd266989d76 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 23:42:50 -0500 Subject: [PATCH 41/82] Specify platform for firefox in sauce tests Works around what appears to be an init issue within Sauce. --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index fc2342191..9b19e5b9b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -164,7 +164,7 @@ module.exports = function(grunt) { concurrency: 2, browsers: [ {browserName: 'chrome'}, - {browserName: 'firefox'}, + {browserName: 'firefox', platform: 'Linux'}, {browserName: 'safari', version: 7, platform: 'OS X 10.9'}, {browserName: 'safari', version: 6, platform: 'OS X 10.8'}, {browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'}, From a44fe470b4c8b62c474a6450f87cadb7ad7bfc25 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 23:43:00 -0500 Subject: [PATCH 42/82] Include doctype in amd harness --- spec/amd.html | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/amd.html b/spec/amd.html index 5de33c1cf..1149dc706 100644 --- a/spec/amd.html +++ b/spec/amd.html @@ -1,3 +1,4 @@ + Mocha From ba31ef8ae4964f2cc9cb6bedd44d7dbf829693ab Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 3 Aug 2015 23:43:13 -0500 Subject: [PATCH 43/82] Increase sauce test concurrency --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 9b19e5b9b..7bf94a91a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -161,7 +161,7 @@ module.exports = function(grunt) { build: process.env.TRAVIS_JOB_ID, urls: ['http://localhost:9999/spec/?headless=true', 'http://localhost:9999/spec/amd.html?headless=true'], detailedError: true, - concurrency: 2, + concurrency: 4, browsers: [ {browserName: 'chrome'}, {browserName: 'firefox', platform: 'Linux'}, From a62cbad95acdf544dc9daae9834588453ee1f835 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 4 Aug 2015 09:26:19 -0500 Subject: [PATCH 44/82] Refactor precompiler API into two phase Load templates and then parse them in a distinct operation. This will allow us to use other input sources such as stdin and strings. --- bin/handlebars | 14 ++-- lib/precompiler.js | 154 ++++++++++++++++++++++++-------------------- spec/precompiler.js | 98 ++++++++++++++++------------ 3 files changed, 149 insertions(+), 117 deletions(-) diff --git a/bin/handlebars b/bin/handlebars index 4ed98b372..10cc6c591 100755 --- a/bin/handlebars +++ b/bin/handlebars @@ -26,7 +26,7 @@ var optimist = require('optimist') 'type': 'string', 'description': 'Path to handlebar.js (only valid for amd-style)', 'alias': 'handlebarPath', - 'default': '' + 'default': '' }, 'k': { 'type': 'string', @@ -59,12 +59,12 @@ var optimist = require('optimist') 'description': 'Template root. Base value that will be stripped from template names.', 'alias': 'root' }, - 'p' : { + 'p': { 'type': 'boolean', 'description': 'Compiling a partial template', 'alias': 'partial' }, - 'd' : { + 'd': { 'type': 'boolean', 'description': 'Include data when compiling', 'alias': 'data' @@ -103,9 +103,11 @@ var argv = optimist.argv; argv.templates = argv._; delete argv._; +var Precompiler = require('../dist/cjs/precompiler'); +Precompiler.loadTemplates(argv); + if (argv.help || (!argv.templates.length && !argv.version)) { optimist.showHelp(); - return; +} else { + Precompiler.cli(argv); } - -return require('../dist/cjs/precompiler').cli(argv); diff --git a/lib/precompiler.js b/lib/precompiler.js index 48cfebd1a..2809b1b50 100644 --- a/lib/precompiler.js +++ b/lib/precompiler.js @@ -5,28 +5,73 @@ import {basename} from 'path'; import {SourceMapConsumer, SourceNode} from 'source-map'; import uglify from 'uglify-js'; +module.exports.loadTemplates = function(opts) { + // Build file extension pattern + let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; }); + extension = new RegExp('\\.' + extension + '$'); + + let ret = []; + function processTemplate(template, root) { + let path = template, + stat; + try { + stat = fs.statSync(template); + } catch (err) { + throw new Handlebars.Exception(`Unable to open template file "${template}"`); + } + + if (stat.isDirectory()) { + opts.hasDirectory = true; + + fs.readdirSync(template).map(function(file) { + let childPath = template + '/' + file; + + if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) { + processTemplate(childPath, root || template); + } + }); + } else { + let data = fs.readFileSync(path, 'utf8'); + + if (opts.bom && data.indexOf('\uFEFF') === 0) { + data = data.substring(1); + } + + // Clean the template name + if (!root) { + template = basename(template); + } else if (template.indexOf(root) === 0) { + template = template.substring(root.length + 1); + } + template = template.replace(extension, ''); + + ret.push({ + path: path, + name: template, + source: data + }); + } + } + opts.templates.forEach(function(template) { + processTemplate(template, opts.root); + }); + opts.templates = ret; +}; + module.exports.cli = function(opts) { if (opts.version) { console.log(Handlebars.VERSION); return; } - if (!opts.templates.length) { + if (!opts.templates.length && !opts.hasDirectory) { throw new Handlebars.Exception('Must define at least one template or directory.'); } - opts.templates.forEach(function(template) { - try { - fs.statSync(template); - } catch (err) { - throw new Handlebars.Exception(`Unable to open template file "${template}"`); - } - }); - if (opts.simple && opts.min) { throw new Handlebars.Exception('Unable to minimize simple output'); } - if (opts.simple && (opts.templates.length !== 1 || fs.statSync(opts.templates[0]).isDirectory())) { + if (opts.simple && (opts.templates.length !== 1 || opts.hasDirectory)) { throw new Handlebars.Exception('Unable to output multiple templates in simple mode'); } @@ -41,10 +86,6 @@ module.exports.cli = function(opts) { } } - // Build file extension pattern - let extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; }); - extension = new RegExp('\\.' + extension + '$'); - let output = new SourceNode(); if (!opts.simple) { if (opts.amd) { @@ -63,76 +104,47 @@ module.exports.cli = function(opts) { } output.add('{};\n'); } - function processTemplate(template, root) { - let path = template, - stat = fs.statSync(path); - if (stat.isDirectory()) { - fs.readdirSync(template).map(function(file) { - let childPath = template + '/' + file; - if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) { - processTemplate(childPath, root || template); - } - }); - } else { - let data = fs.readFileSync(path, 'utf8'); - - if (opts.bom && data.indexOf('\uFEFF') === 0) { - data = data.substring(1); - } - - let options = { - knownHelpers: known, - knownHelpersOnly: opts.o - }; + opts.templates.forEach(function(template) { + let options = { + knownHelpers: known, + knownHelpersOnly: opts.o + }; - if (opts.map) { - options.srcName = path; - } - if (opts.data) { - options.data = true; - } + if (opts.map) { + options.srcName = template.path; + } + if (opts.data) { + options.data = true; + } - // Clean the template name - if (!root) { - template = basename(template); - } else if (template.indexOf(root) === 0) { - template = template.substring(root.length + 1); - } - template = template.replace(extension, ''); + let precompiled = Handlebars.precompile(template.source, options); - let precompiled = Handlebars.precompile(data, options); + // If we are generating a source map, we have to reconstruct the SourceNode object + if (opts.map) { + let consumer = new SourceMapConsumer(precompiled.map); + precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer); + } - // If we are generating a source map, we have to reconstruct the SourceNode object - if (opts.map) { - let consumer = new SourceMapConsumer(precompiled.map); - precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer); + if (opts.simple) { + output.add([precompiled, '\n']); + } else if (opts.partial) { + if (opts.amd && (opts.templates.length == 1 && !opts.hasDirectory)) { + output.add('return '); } - - if (opts.simple) { - output.add([precompiled, '\n']); - } else if (opts.partial) { - if (opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) { - output.add('return '); - } - output.add(['Handlebars.partials[\'', template, '\'] = template(', precompiled, ');\n']); - } else { - if (opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) { - output.add('return '); - } - output.add(['templates[\'', template, '\'] = template(', precompiled, ');\n']); + output.add(['Handlebars.partials[\'', template.name, '\'] = template(', precompiled, ');\n']); + } else { + if (opts.amd && (opts.templates.length == 1 && !opts.hasDirectory)) { + output.add('return '); } + output.add(['templates[\'', template.name, '\'] = template(', precompiled, ');\n']); } - } - - opts.templates.forEach(function(template) { - processTemplate(template, opts.root); }); // Output the content if (!opts.simple) { if (opts.amd) { - if (opts.templates.length > 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) { + if (opts.templates.length > 1 || (opts.templates.length == 1 && opts.hasDirectory)) { if (opts.partial) { output.add('return Handlebars.partials;\n'); } else { diff --git a/spec/precompiler.js b/spec/precompiler.js index 21e25b06e..f9cc8fd73 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -16,6 +16,12 @@ describe('precompiler', function() { precompile, minify, + emptyTemplate = { + path: __dirname + '/artifacts/empty.handlebars', + name: 'empty', + source: '' + }, + file, content, writeFileSync; @@ -51,10 +57,9 @@ describe('precompiler', function() { Precompiler.cli({templates: []}); }, Handlebars.Exception, 'Must define at least one template or directory.'); }); - it('should throw on missing template', function() { - shouldThrow(function() { - Precompiler.cli({templates: ['foo']}); - }, Handlebars.Exception, 'Unable to open template file "foo"'); + it('should handle empty/filtered directories', function() { + Precompiler.cli({hasDirectory: true, templates: []}); + // Success is not throwing }); it('should throw when combining simple and minimized', function() { shouldThrow(function() { @@ -68,105 +73,118 @@ describe('precompiler', function() { }); it('should throw when combining simple and directories', function() { shouldThrow(function() { - Precompiler.cli({templates: [__dirname], simple: true}); + Precompiler.cli({hasDirectory: true, templates: [1], simple: true}); }, Handlebars.Exception, 'Unable to output multiple templates in simple mode'); }); - it('should enumerate directories by extension', function() { - Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hbs'}); - equal(/'example_2'/.test(log), true); - log = ''; - Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'handlebars'}); - equal(/'empty'/.test(log), true); - equal(/'example_1'/.test(log), true); - }); - it('should protect from regexp patterns', function() { - Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hb(s'}); - // Success is not throwing - }); it('should output simple templates', function() { Handlebars.precompile = function() { return 'simple'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], simple: true}); equal(log, 'simple\n'); }); it('should output amd templates', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], amd: true}); equal(/template\(amd\)/.test(log), true); }); it('should output multiple amd', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars', namespace: 'foo'}); + Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, namespace: 'foo'}); equal(/templates = foo = foo \|\|/.test(log), true); equal(/return templates/.test(log), true); equal(/template\(amd\)/.test(log), true); }); it('should output amd partials', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, partial: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], amd: true, partial: true}); equal(/return Handlebars\.partials\['empty'\]/.test(log), true); equal(/template\(amd\)/.test(log), true); }); it('should output multiple amd partials', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, partial: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, partial: true}); equal(/return Handlebars\.partials\[/.test(log), false); equal(/template\(amd\)/.test(log), true); }); it('should output commonjs templates', function() { Handlebars.precompile = function() { return 'commonjs'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], commonjs: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], commonjs: true}); equal(/template\(commonjs\)/.test(log), true); }); it('should set data flag', function() { Handlebars.precompile = function(data, options) { equal(options.data, true); return 'simple'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', data: true}); + Precompiler.cli({templates: [emptyTemplate], simple: true, data: true}); equal(log, 'simple\n'); }); it('should set known helpers', function() { Handlebars.precompile = function(data, options) { equal(options.knownHelpers.foo, true); return 'simple'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', known: 'foo'}); - 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/'}); + Precompiler.cli({templates: [emptyTemplate], simple: true, known: '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!'}); + Precompiler.cli({templates: [emptyTemplate], simple: true, 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'}; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], min: true}); equal(log, 'min'); }); it('should output map', function() { - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], map: 'foo.js.map'}); 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'}); + Precompiler.cli({templates: [emptyTemplate], min: true, map: 'foo.js.map'}); equal(file, 'foo.js.map'); equal(/sourceMappingURL=/.test(log), true); }); + + describe('#loadTemplates', function() { + it('should throw on missing template', function() { + shouldThrow(function() { + Precompiler.loadTemplates({templates: ['foo']}); + }, Handlebars.Exception, 'Unable to open template file "foo"'); + }); + it('should enumerate directories by extension', function() { + var opts = {templates: [__dirname + '/artifacts'], extension: 'hbs'}; + Precompiler.loadTemplates(opts); + equal(opts.templates.length, 1); + equal(opts.templates[0].name, 'example_2'); + + opts = {templates: [__dirname + '/artifacts'], extension: 'handlebars'}; + Precompiler.loadTemplates(opts); + equal(opts.templates.length, 3); + equal(opts.templates[0].name, 'bom'); + equal(opts.templates[1].name, 'empty'); + equal(opts.templates[2].name, 'example_1'); + }); + it('should handle regular expression characters in extensions', function() { + Precompiler.loadTemplates({templates: [__dirname + '/artifacts'], extension: 'hb(s'}); + // Success is not throwing + }); + it('should handle BOM', function() { + var opts = {templates: [__dirname + '/artifacts/bom.handlebars'], extension: 'handlebars', bom: true}; + Precompiler.loadTemplates(opts); + equal(opts.templates[0].source, 'a'); + }); + + it('should handle different root', function() { + var opts = {templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, root: 'foo/'}; + Precompiler.loadTemplates(opts); + equal(opts.templates[0].name, __dirname + '/artifacts/empty'); + }); + }); }); From 00f74420f949a42bedd99af022c20cdc5027228d Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 4 Aug 2015 10:55:51 -0500 Subject: [PATCH 45/82] Convert precompiler template loading to async --- bin/handlebars | 18 +++++--- lib/precompiler.js | 105 +++++++++++++++++++++++++++----------------- package.json | 1 + spec/precompiler.js | 64 +++++++++++++++------------ 4 files changed, 113 insertions(+), 75 deletions(-) diff --git a/bin/handlebars b/bin/handlebars index 10cc6c591..bfe6680d3 100755 --- a/bin/handlebars +++ b/bin/handlebars @@ -100,14 +100,18 @@ var optimist = require('optimist') var argv = optimist.argv; -argv.templates = argv._; +argv.files = argv._; delete argv._; var Precompiler = require('../dist/cjs/precompiler'); -Precompiler.loadTemplates(argv); +Precompiler.loadTemplates(argv, function(err, opts) { + if (err) { + throw err; + } -if (argv.help || (!argv.templates.length && !argv.version)) { - optimist.showHelp(); -} else { - Precompiler.cli(argv); -} + if (opts.help || (!opts.templates.length && !opts.version)) { + optimist.showHelp(); + } else { + Precompiler.cli(opts); + } +}); diff --git a/lib/precompiler.js b/lib/precompiler.js index 2809b1b50..8a2019dfa 100644 --- a/lib/precompiler.js +++ b/lib/precompiler.js @@ -1,62 +1,85 @@ /*eslint-disable no-console */ +import Async from 'async'; import fs from 'fs'; import * as Handlebars from './handlebars'; import {basename} from 'path'; import {SourceMapConsumer, SourceNode} from 'source-map'; import uglify from 'uglify-js'; -module.exports.loadTemplates = function(opts) { +module.exports.loadTemplates = function(opts, callback) { // Build file extension pattern let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; }); extension = new RegExp('\\.' + extension + '$'); - let ret = []; - function processTemplate(template, root) { - let path = template, - stat; - try { - stat = fs.statSync(template); - } catch (err) { - throw new Handlebars.Exception(`Unable to open template file "${template}"`); - } - - if (stat.isDirectory()) { - opts.hasDirectory = true; - - fs.readdirSync(template).map(function(file) { - let childPath = template + '/' + file; - - if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) { - processTemplate(childPath, root || template); - } - }); - } else { - let data = fs.readFileSync(path, 'utf8'); + let ret = [], + queue = opts.files.map((template) => ({template, root: opts.root})); + Async.whilst(() => queue.length, function(callback) { + let {template: path, root} = queue.shift(); - if (opts.bom && data.indexOf('\uFEFF') === 0) { - data = data.substring(1); + fs.stat(path, function(err, stat) { + if (err) { + return callback(new Handlebars.Exception(`Unable to open template file "${path}"`)); } - // Clean the template name - if (!root) { - template = basename(template); - } else if (template.indexOf(root) === 0) { - template = template.substring(root.length + 1); + if (stat.isDirectory()) { + opts.hasDirectory = true; + + fs.readdir(path, function(err, children) { + /* istanbul ignore next : Race condition that being too lazy to test */ + if (err) { + return callback(err); + } + children.forEach(function(file) { + let childPath = path + '/' + file; + + if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) { + queue.push({template: childPath, root: root || path}); + } + }); + + callback(); + }); + } else { + fs.readFile(path, 'utf8', function(err, data) { + /* istanbul ignore next : Race condition that being too lazy to test */ + if (err) { + return callback(err); + } + + if (opts.bom && data.indexOf('\uFEFF') === 0) { + data = data.substring(1); + } + + // Clean the template name + let name = path; + if (!root) { + name = basename(name); + } else if (name.indexOf(root) === 0) { + name = name.substring(root.length + 1); + } + name = name.replace(extension, ''); + + ret.push({ + path: path, + name: name, + source: data + }); + + callback(); + }); } - template = template.replace(extension, ''); + }); + }, + function(err) { + if (err) { + callback(err); + } else { + opts.templates = ret; - ret.push({ - path: path, - name: template, - source: data - }); + callback(undefined, opts); } - } - opts.templates.forEach(function(template) { - processTemplate(template, opts.root); }); - opts.templates = ret; -}; +} module.exports.cli = function(opts) { if (opts.version) { diff --git a/package.json b/package.json index 83f428d0f..b1cafeeae 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "node": ">=0.4.7" }, "dependencies": { + "async": "^1.4.0", "optimist": "^0.6.1", "source-map": "^0.1.40" }, diff --git a/spec/precompiler.js b/spec/precompiler.js index f9cc8fd73..72a7ce649 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -153,38 +153,48 @@ describe('precompiler', function() { }); describe('#loadTemplates', function() { - it('should throw on missing template', function() { - shouldThrow(function() { - Precompiler.loadTemplates({templates: ['foo']}); - }, Handlebars.Exception, 'Unable to open template file "foo"'); + it('should throw on missing template', function(done) { + Precompiler.loadTemplates({files: ['foo']}, function(err) { + equal(err.message, 'Unable to open template file "foo"'); + done(); + }); }); - it('should enumerate directories by extension', function() { - var opts = {templates: [__dirname + '/artifacts'], extension: 'hbs'}; - Precompiler.loadTemplates(opts); - equal(opts.templates.length, 1); - equal(opts.templates[0].name, 'example_2'); - - opts = {templates: [__dirname + '/artifacts'], extension: 'handlebars'}; - Precompiler.loadTemplates(opts); - equal(opts.templates.length, 3); - equal(opts.templates[0].name, 'bom'); - equal(opts.templates[1].name, 'empty'); - equal(opts.templates[2].name, 'example_1'); + it('should enumerate directories by extension', function(done) { + Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'hbs'}, function(err, opts) { + equal(opts.templates.length, 1); + equal(opts.templates[0].name, 'example_2'); + done(err); + }); }); - it('should handle regular expression characters in extensions', function() { - Precompiler.loadTemplates({templates: [__dirname + '/artifacts'], extension: 'hb(s'}); - // Success is not throwing + it('should enumerate all templates by extension', function(done) { + Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'handlebars'}, function(err, opts) { + equal(opts.templates.length, 3); + equal(opts.templates[0].name, 'bom'); + equal(opts.templates[1].name, 'empty'); + equal(opts.templates[2].name, 'example_1'); + done(err); + }); }); - it('should handle BOM', function() { - var opts = {templates: [__dirname + '/artifacts/bom.handlebars'], extension: 'handlebars', bom: true}; - Precompiler.loadTemplates(opts); - equal(opts.templates[0].source, 'a'); + it('should handle regular expression characters in extensions', function(done) { + Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'hb(s'}, function(err) { + // Success is not throwing + done(err); + }); + }); + it('should handle BOM', function(done) { + var opts = {files: [__dirname + '/artifacts/bom.handlebars'], extension: 'handlebars', bom: true}; + Precompiler.loadTemplates(opts, function(err, opts) { + equal(opts.templates[0].source, 'a'); + done(err); + }); }); - it('should handle different root', function() { - var opts = {templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, root: 'foo/'}; - Precompiler.loadTemplates(opts); - equal(opts.templates[0].name, __dirname + '/artifacts/empty'); + it('should handle different root', function(done) { + var opts = {files: [__dirname + '/artifacts/empty.handlebars'], simple: true, root: 'foo/'}; + Precompiler.loadTemplates(opts, function(err, opts) { + equal(opts.templates[0].name, __dirname + '/artifacts/empty'); + done(err); + }); }); }); }); From 77e6bfc5a1c62336b1eb67427bc194b79226127a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 4 Aug 2015 12:33:05 -0500 Subject: [PATCH 46/82] Simplify object assignment generation logic --- lib/precompiler.js | 23 +++++++++-------------- spec/precompiler.js | 7 +++++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/precompiler.js b/lib/precompiler.js index 8a2019dfa..12a5a1f96 100644 --- a/lib/precompiler.js +++ b/lib/precompiler.js @@ -94,7 +94,9 @@ module.exports.cli = function(opts) { if (opts.simple && opts.min) { throw new Handlebars.Exception('Unable to minimize simple output'); } - if (opts.simple && (opts.templates.length !== 1 || opts.hasDirectory)) { + + const multiple = opts.templates.length !== 1 || opts.hasDirectory; + if (opts.simple && multiple) { throw new Handlebars.Exception('Unable to output multiple templates in simple mode'); } @@ -109,6 +111,8 @@ module.exports.cli = function(opts) { } } + const objectName = opts.partial ? 'Handlebars.partials' : 'templates'; + let output = new SourceNode(); if (!opts.simple) { if (opts.amd) { @@ -151,28 +155,19 @@ module.exports.cli = function(opts) { if (opts.simple) { output.add([precompiled, '\n']); - } else if (opts.partial) { - if (opts.amd && (opts.templates.length == 1 && !opts.hasDirectory)) { - output.add('return '); - } - output.add(['Handlebars.partials[\'', template.name, '\'] = template(', precompiled, ');\n']); } else { - if (opts.amd && (opts.templates.length == 1 && !opts.hasDirectory)) { + if (opts.amd && !multiple) { output.add('return '); } - output.add(['templates[\'', template.name, '\'] = template(', precompiled, ');\n']); + output.add([objectName, '[\'', template.name, '\'] = template(', precompiled, ');\n']); } }); // Output the content if (!opts.simple) { if (opts.amd) { - if (opts.templates.length > 1 || (opts.templates.length == 1 && opts.hasDirectory)) { - if (opts.partial) { - output.add('return Handlebars.partials;\n'); - } else { - output.add('return templates;\n'); - } + if (multiple) { + output.add(['return ', objectName, ';\n']); } output.add('});'); } else if (!opts.commonjs) { diff --git a/spec/precompiler.js b/spec/precompiler.js index 72a7ce649..25c57a846 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -196,5 +196,12 @@ describe('precompiler', function() { done(err); }); }); + + it('should complete when no args are passed', function(done) { + Precompiler.loadTemplates({}, function(err, opts) { + equal(opts.templates.length, 0); + done(err); + }); + }); }); }); From d716fd01c546593d1f4c06cd7e1a6ba81a116586 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 4 Aug 2015 12:33:19 -0500 Subject: [PATCH 47/82] Remove no-shadow rule --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index c1c00d064..997093302 100644 --- a/.eslintrc +++ b/.eslintrc @@ -129,7 +129,7 @@ "no-catch-shadow": 2, "no-delete-var": 2, "no-label-var": 2, - "no-shadow": 2, + "no-shadow": 0, "no-shadow-restricted-names": 2, "no-undef": 2, "no-undef-init": 2, From 0de8dac702f7b06161a0c5464a80bd327d708258 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 4 Aug 2015 12:34:28 -0500 Subject: [PATCH 48/82] Add support for string and stdin precompilation Fixes #1071 --- bin/handlebars | 11 +++++++ lib/precompiler.js | 76 ++++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + spec/precompiler.js | 46 +++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 4 deletions(-) diff --git a/bin/handlebars b/bin/handlebars index bfe6680d3..7645adf36 100755 --- a/bin/handlebars +++ b/bin/handlebars @@ -54,6 +54,16 @@ var optimist = require('optimist') 'description': 'Output template function only.', 'alias': 'simple' }, + 'N': { + 'type': 'string', + 'description': 'Name of passed string templates. Optional if running in a simple mode. Required when operating on multiple templates.', + 'alias': 'name' + }, + 'i': { + 'type': 'string', + 'description': 'Generates a template from the passed CLI argument.\n"-" is treated as a special value and causes stdin to be read for the template value.', + 'alias': 'string' + }, 'r': { 'type': 'string', 'description': 'Template root. Base value that will be stripped from template names.', @@ -92,6 +102,7 @@ var optimist = require('optimist') } }) + .wrap(120) .check(function(argv) { if (argv.version) { return; diff --git a/lib/precompiler.js b/lib/precompiler.js index 12a5a1f96..9f23ef661 100644 --- a/lib/precompiler.js +++ b/lib/precompiler.js @@ -7,12 +7,64 @@ import {SourceMapConsumer, SourceNode} from 'source-map'; import uglify from 'uglify-js'; module.exports.loadTemplates = function(opts, callback) { + loadStrings(opts, function(err, strings) { + if (err) { + callback(err); + } else { + loadFiles(opts, function(err, files) { + if (err) { + callback(err); + } else { + opts.templates = strings.concat(files); + callback(undefined, opts); + } + }); + } + }); +}; + +function loadStrings(opts, callback) { + let strings = arrayCast(opts.string), + names = arrayCast(opts.name); + + if (names.length !== strings.length + && strings.length > 1) { + return callback(new Handlebars.Exception('Number of names did not match the number of string inputs')); + } + + Async.map(strings, function(string, callback) { + if (string !== '-') { + callback(undefined, string); + } else { + // Load from stdin + let buffer = ''; + process.stdin.setEncoding('utf8'); + + process.stdin.on('data', function(chunk) { + buffer += chunk; + }); + process.stdin.on('end', function() { + callback(undefined, buffer); + }); + } + }, + function(err, strings) { + strings = strings.map((string, index) => ({ + name: names[index], + path: names[index], + source: string + })); + callback(err, strings); + }); +} + +function loadFiles(opts, callback) { // Build file extension pattern let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; }); extension = new RegExp('\\.' + extension + '$'); let ret = [], - queue = opts.files.map((template) => ({template, root: opts.root})); + queue = (opts.files || []).map((template) => ({template, root: opts.root})); Async.whilst(() => queue.length, function(callback) { let {template: path, root} = queue.shift(); @@ -74,9 +126,7 @@ module.exports.loadTemplates = function(opts, callback) { if (err) { callback(err); } else { - opts.templates = ret; - - callback(undefined, opts); + callback(undefined, ret); } }); } @@ -100,6 +150,12 @@ module.exports.cli = function(opts) { throw new Handlebars.Exception('Unable to output multiple templates in simple mode'); } + // Force simple mode if we have only one template and it's unnamed. + if (!opts.amd && !opts.commonjs && opts.templates.length === 1 + && !opts.templates[0].name) { + opts.simple = true; + } + // Convert the known list into a hash let known = {}; if (opts.known && !Array.isArray(opts.known)) { @@ -156,6 +212,10 @@ module.exports.cli = function(opts) { if (opts.simple) { output.add([precompiled, '\n']); } else { + if (!template.name) { + throw new Handlebars.Exception('Name missing for template'); + } + if (opts.amd && !multiple) { output.add('return '); } @@ -206,3 +266,11 @@ module.exports.cli = function(opts) { console.log(output); } }; + +function arrayCast(value) { + value = value != null ? value : []; + if (!Array.isArray(value)) { + value = [value]; + } + return value; +} diff --git a/package.json b/package.json index b1cafeeae..8548cd867 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "jison": "~0.3.0", "keen.io": "0.0.3", "mocha": "~1.20.0", + "mock-stdin": "^0.3.0", "mustache": "0.x", "semver": "^4.0.0", "underscore": "^1.5.1" diff --git a/spec/precompiler.js b/spec/precompiler.js index 25c57a846..e1ad5ade9 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -71,6 +71,11 @@ describe('precompiler', function() { Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true}); }, Handlebars.Exception, 'Unable to output multiple templates in simple mode'); }); + it('should throw when missing name', function() { + shouldThrow(function() { + Precompiler.cli({templates: [{source: ''}], amd: true}); + }, Handlebars.Exception, 'Name missing for template'); + }); it('should throw when combining simple and directories', function() { shouldThrow(function() { Precompiler.cli({hasDirectory: true, templates: [1], simple: true}); @@ -82,6 +87,11 @@ describe('precompiler', function() { Precompiler.cli({templates: [emptyTemplate], simple: true}); equal(log, 'simple\n'); }); + it('should default to simple templates', function() { + Handlebars.precompile = function() { return 'simple'; }; + Precompiler.cli({templates: [{source: ''}]}); + equal(log, 'simple\n'); + }); it('should output amd templates', function() { Handlebars.precompile = function() { return 'amd'; }; Precompiler.cli({templates: [emptyTemplate], amd: true}); @@ -197,6 +207,42 @@ describe('precompiler', function() { }); }); + it('should accept string inputs', function(done) { + var opts = {string: ''}; + Precompiler.loadTemplates(opts, function(err, opts) { + equal(opts.templates[0].name, undefined); + equal(opts.templates[0].source, ''); + done(err); + }); + }); + it('should accept string array inputs', function(done) { + var opts = {string: ['', 'bar'], name: ['beep', 'boop']}; + Precompiler.loadTemplates(opts, function(err, opts) { + equal(opts.templates[0].name, 'beep'); + equal(opts.templates[0].source, ''); + equal(opts.templates[1].name, 'boop'); + equal(opts.templates[1].source, 'bar'); + done(err); + }); + }); + it('should accept stdin input', function(done) { + var stdin = require('mock-stdin').stdin(); + Precompiler.loadTemplates({string: '-'}, function(err, opts) { + equal(opts.templates[0].source, 'foo'); + done(err); + }); + stdin.send('fo'); + stdin.send('o'); + stdin.end(); + }); + it('error on name missing', function(done) { + var opts = {string: ['', 'bar']}; + Precompiler.loadTemplates(opts, function(err) { + equal(err.message, 'Number of names did not match the number of string inputs'); + done(); + }); + }); + it('should complete when no args are passed', function(done) { Precompiler.loadTemplates({}, function(err, opts) { equal(opts.templates.length, 0); From 06d515a89d18b50805a5fe4eec8f1156bbe92d45 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 4 Aug 2015 12:40:33 -0500 Subject: [PATCH 49/82] Ignore empty when iterating on sparse arrays Fixes #1065 --- lib/handlebars/helpers/each.js | 6 ++++++ spec/regressions.js | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/lib/handlebars/helpers/each.js b/lib/handlebars/helpers/each.js index 9fc5a095d..d39a30098 100644 --- a/lib/handlebars/helpers/each.js +++ b/lib/handlebars/helpers/each.js @@ -25,6 +25,12 @@ export default function(instance) { } function execIteration(field, index, last) { + // Don't iterate over undefined values since we can't execute blocks against them + // in non-strict (js) mode. + if (context[field] == null) { + return; + } + if (data) { data.key = field; data.index = index; diff --git a/spec/regressions.js b/spec/regressions.js index 009fec90b..e8942a484 100644 --- a/spec/regressions.js +++ b/spec/regressions.js @@ -196,4 +196,11 @@ describe('Regressions', function() { shouldCompileToWithPartials(root, [{}, helpers, partials], true, ''); }); + + it('GH-1065: Sparse arrays', function() { + var array = []; + array[1] = 'foo'; + array[3] = 'bar'; + shouldCompileTo('{{#each array}}{{@index}}{{.}}{{/each}}', {array: array}, '1foo3bar'); + }); }); From 2ae0ef6eef2e8fb43eba1cc6ebe4be8ca480233f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 12 Aug 2015 02:45:51 -0500 Subject: [PATCH 50/82] Add license information to bower.json Fixes #1074 --- components/bower.json | 1 + 1 file changed, 1 insertion(+) diff --git a/components/bower.json b/components/bower.json index fe064c285..9b3f8a978 100644 --- a/components/bower.json +++ b/components/bower.json @@ -2,5 +2,6 @@ "name": "handlebars", "version": "3.0.3", "main": "handlebars.js", + "license": "MIT", "dependencies": {} } From 269dd492bb3ccb54516a3f7f6caba80dbaaad862 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 12 Aug 2015 22:37:26 -0500 Subject: [PATCH 51/82] Link to installation docs --- README.markdown | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/README.markdown b/README.markdown index d39d1312e..508ac9608 100644 --- a/README.markdown +++ b/README.markdown @@ -14,20 +14,8 @@ Checkout the official Handlebars docs site at 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 -a `handlebars-latest.js` file from the [builds page][builds-page]. When the -build is published, it is also available as a `handlebars-gitSHA.js` file on -the builds page if you need a version to refer to others. -`handlebars-runtime.js` builds are also available. - -**Note**: The S3 builds page is provided as a convenience for the community, -but you should not use it for hosting Handlebars in production. +See our [installation documentation](http://handlebarsjs.com/installation.html). Usage ----- @@ -172,6 +160,4 @@ License ------- Handlebars.js is released under the MIT license. -[bower-repo]: https://github.com/components/handlebars.js -[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/bucket-listing.html?sort=lastmod&sortdir=desc [pull-request]: https://github.com/wycats/handlebars.js/pull/new/master From ea3a5a1eb6e488f8dc0189c68a6019c76ffad740 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 13 Aug 2015 01:50:51 -0500 Subject: [PATCH 52/82] Add ignoreStandalone compiler option Fixes #1072 --- lib/handlebars/compiler/base.js | 2 +- lib/handlebars/compiler/whitespace-control.js | 14 +++++++++----- spec/blocks.js | 10 ++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 7075d9bd8..85c2997b4 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -20,6 +20,6 @@ export function parse(input, options) { return new yy.SourceLocation(options && options.srcName, locInfo); }; - let strip = new WhitespaceControl(); + let strip = new WhitespaceControl(options); return strip.accept(parser.parse(input)); } diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js index 5b76944dc..d1b743d7e 100644 --- a/lib/handlebars/compiler/whitespace-control.js +++ b/lib/handlebars/compiler/whitespace-control.js @@ -1,10 +1,13 @@ import Visitor from './visitor'; -function WhitespaceControl() { +function WhitespaceControl(options = {}) { + this.options = options; } WhitespaceControl.prototype = new Visitor(); WhitespaceControl.prototype.Program = function(program) { + const doStandalone = !this.options.ignoreStandalone; + let isRoot = !this.isRootSeen; this.isRootSeen = true; @@ -31,7 +34,7 @@ WhitespaceControl.prototype.Program = function(program) { omitLeft(body, i, true); } - if (inlineStandalone) { + if (doStandalone && inlineStandalone) { omitRight(body, i); if (omitLeft(body, i)) { @@ -42,13 +45,13 @@ WhitespaceControl.prototype.Program = function(program) { } } } - if (openStandalone) { + if (doStandalone && openStandalone) { omitRight((current.program || current.inverse).body); // Strip out the previous content node if it's whitespace only omitLeft(body, i); } - if (closeStandalone) { + if (doStandalone && closeStandalone) { // Always strip the next node omitRight(body, i); @@ -106,7 +109,8 @@ WhitespaceControl.prototype.BlockStatement = function(block) { } // Find standalone else statments - if (isPrevWhitespace(program.body) + if (!this.options.ignoreStandalone + && isPrevWhitespace(program.body) && isNextWhitespace(firstInverse.body)) { omitLeft(program.body); omitRight(firstInverse.body); diff --git a/spec/blocks.js b/spec/blocks.js index 3584ed788..71c9045d2 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -125,6 +125,16 @@ describe('blocks', function() { shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, 'No people\n'); }); + it('block standalone else sections can be disabled', function() { + shouldCompileTo( + '{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', + [{none: 'No people'}, {}, {}, {ignoreStandalone: true}], + '\nNo people\n\n'); + shouldCompileTo( + '{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n', + [{none: 'No people'}, {}, {}, {ignoreStandalone: true}], + '\nNo people\n\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'); From 3ce04ddd23fe92796a6581b3de10658d9e7ac3c6 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 14 Aug 2015 15:19:37 -0500 Subject: [PATCH 53/82] Include inline source maps in babel build --- Gruntfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Gruntfile.js b/Gruntfile.js index 7bf94a91a..2850a9052 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -45,6 +45,7 @@ module.exports = function(grunt) { babel: { options: { + sourceMaps: 'inline', loose: ['es6.modules'], auxiliaryCommentBefore: 'istanbul ignore next' }, From 624a4c71aa3ec2206e2338d939c0ae8df7eec97a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 14 Aug 2015 15:20:09 -0500 Subject: [PATCH 54/82] Cleanup stack traces for test assertions --- spec/env/common.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/spec/env/common.js b/spec/env/common.js index e4c8ae537..f2c9dc670 100644 --- a/spec/env/common.js +++ b/spec/env/common.js @@ -1,11 +1,27 @@ +var AssertError; +if (Error.captureStackTrace) { + AssertError = function AssertError(message, caller) { + Error.prototype.constructor.call(this, message); + this.message = message; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, caller || AssertError); + } + }; + + AssertError.prototype = new Error(); +} else { + AssertError = Error; +} + global.shouldCompileTo = function(string, hashOrArray, expected, message) { shouldCompileToWithPartials(string, hashOrArray, false, expected, message); }; -global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) { +global.shouldCompileToWithPartials = function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) { var result = compileWithPartials(string, hashOrArray, partials); if (result !== expected) { - throw new Error("'" + result + "' should === '" + expected + "': " + message); + throw new AssertError("'" + result + "' should === '" + expected + "': " + message, shouldCompileToWithPartials); } }; @@ -31,9 +47,9 @@ global.compileWithPartials = function(string, hashOrArray, partials) { }; -global.equals = global.equal = function(a, b, msg) { +global.equals = global.equal = function equals(a, b, msg) { if (a !== b) { - throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : '')); + throw new AssertError("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''), equals); } }; @@ -44,13 +60,13 @@ global.shouldThrow = function(callback, type, msg) { failed = true; } catch (err) { if (type && !(err instanceof type)) { - throw new Error('Type failure: ' + err); + throw new AssertError('Type failure: ' + err); } if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) { equal(msg, err.message); } } if (failed) { - throw new Error('It failed to throw'); + throw new AssertError('It failed to throw', shouldThrow); } }; From 519fac508146a3844bcc3db5317069008d899b93 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 14 Aug 2015 15:20:45 -0500 Subject: [PATCH 55/82] Link to try handlebars from readme --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 508ac9608..363b5bcc0 100644 --- a/README.markdown +++ b/README.markdown @@ -10,7 +10,7 @@ Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be. Checkout the official Handlebars docs site at -[http://www.handlebarsjs.com](http://www.handlebarsjs.com). +[http://www.handlebarsjs.com](http://www.handlebarsjs.com) and the live demo at [http://tryhandlebarsjs.com/](http://tryhandlebarsjs.com/). Installing ---------- From d21de5d5697b4b721ea2b45b842a0b65002c0e02 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 14 Aug 2015 15:45:41 -0500 Subject: [PATCH 56/82] Increase travis git window to avoid test failures https://twitter.com/travisci/status/288390896339267584 --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index ecbc1a435..8c112bc79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,3 +21,6 @@ matrix: cache: directories: - node_modules + +git: + deoth: 100 From b85cad8df6e2b4934fcfcfdc95f62d0d930d1707 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 14 Aug 2015 15:45:41 -0500 Subject: [PATCH 57/82] Fix travis config typo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8c112bc79..b9ecde273 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,4 +23,4 @@ cache: - node_modules git: - deoth: 100 + depth: 100 From 1c2b74e537409e5672d587f1df19730d9766151d Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 18 Aug 2015 23:06:23 -0700 Subject: [PATCH 58/82] Run node tests last This covers all of the test cases and generally have better stack traces so we want to have these featured more prominently. --- spec/env/runner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/env/runner.js b/spec/env/runner.js index 56fc8d443..98d2482e8 100644 --- a/spec/env/runner.js +++ b/spec/env/runner.js @@ -13,9 +13,9 @@ var files = fs.readdirSync(testDir) .filter(function(name) { return (/.*\.js$/).test(name); }) .map(function(name) { return testDir + '/' + name; }); -run('./node', function() { +run('./runtime', function() { run('./browser', function() { - run('./runtime', function() { + run('./node', function() { /*eslint-disable no-process-exit */ process.exit(errors); /*eslint-enable no-process-exit */ From 958273c2e6407b1174b212c4eccbcb8f72494c99 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 18 Aug 2015 23:13:15 -0700 Subject: [PATCH 59/82] Add object option to test runner --- spec/env/common.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/env/common.js b/spec/env/common.js index f2c9dc670..111294c68 100644 --- a/spec/env/common.js +++ b/spec/env/common.js @@ -29,7 +29,10 @@ global.compileWithPartials = function(string, hashOrArray, partials) { var template, ary, options; - if (Object.prototype.toString.call(hashOrArray) === '[object Array]') { + if (hashOrArray && hashOrArray.hash) { + ary = [hashOrArray.hash, hashOrArray]; + delete hashOrArray.hash; + } else if (Object.prototype.toString.call(hashOrArray) === '[object Array]') { ary = []; ary.push(hashOrArray[0]); ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); From 08093d72f086c93441e77c7605c07156e7555cad Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 18 Aug 2015 23:14:09 -0700 Subject: [PATCH 60/82] Remove unused parameters --- lib/handlebars/compiler/javascript-compiler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 28f27fd1c..bed2add3e 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -644,7 +644,7 @@ JavaScriptCompiler.prototype = { // and pushes the result of the invocation back. invokePartial: function(isDynamic, name, indent) { let params = [], - options = this.setupParams(name, 1, params, false); + options = this.setupParams(name, 1, params); if (isDynamic) { name = this.popStack(); @@ -1003,7 +1003,7 @@ JavaScriptCompiler.prototype = { }, setupHelperArgs: function(helper, paramSize, params, useRegister) { - let options = this.setupParams(helper, paramSize, params, true); + let options = this.setupParams(helper, paramSize, params); options = this.objectLiteral(options); if (useRegister) { this.useRegister('options'); From 9a2d1d6009406915d1ca177ed5321e4727b9776f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 18 Aug 2015 23:54:04 -0700 Subject: [PATCH 61/82] Pass container rather than exec as context There is no real need for us to do `.call(container` other than for backwards compatibility with legacy versions. Using the 4.x release as a chance to optimize this behavior. --- .../compiler/javascript-compiler.js | 20 +++++++++---------- lib/handlebars/runtime.js | 4 ++-- spec/expected/empty.amd.js | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index bed2add3e..b8fc9761f 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -20,7 +20,7 @@ JavaScriptCompiler.prototype = { } }, depthedLookup: function(name) { - return [this.aliasable('this.lookup'), '(depths, "', name, '")']; + return [this.aliasable('container.lookup'), '(depths, "', name, '")']; }, compilerInfo: function() { @@ -189,7 +189,7 @@ JavaScriptCompiler.prototype = { } } - let params = ['depth0', 'helpers', 'partials', 'data']; + let params = ['container', 'depth0', 'helpers', 'partials', 'data']; if (this.useBlockParams || this.useDepths) { params.push('blockParams'); @@ -359,7 +359,7 @@ JavaScriptCompiler.prototype = { // Escape `value` and append it to the buffer appendEscaped: function() { this.pushSource(this.appendToBuffer( - [this.aliasable('this.escapeExpression'), '(', this.popStack(), ')'])); + [this.aliasable('container.escapeExpression'), '(', this.popStack(), ')'])); }, // [getContext] @@ -428,7 +428,7 @@ JavaScriptCompiler.prototype = { if (!depth) { this.pushStackLiteral('data'); } else { - this.pushStackLiteral('this.data(data, ' + depth + ')'); + this.pushStackLiteral('container.data(data, ' + depth + ')'); } this.resolvePath('data', parts, 0, true, strict); @@ -466,7 +466,7 @@ JavaScriptCompiler.prototype = { // If the `value` is a lambda, replace it on the stack by // the return value of the lambda resolvePossibleLambda: function() { - this.push([this.aliasable('this.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']); + this.push([this.aliasable('container.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']); }, // [pushStringParam] @@ -669,7 +669,7 @@ JavaScriptCompiler.prototype = { options = this.objectLiteral(options); params.push(options); - this.push(this.source.functionCall('this.invokePartial', '', params)); + this.push(this.source.functionCall('container.invokePartial', '', params)); }, // [assignToHash] @@ -771,7 +771,7 @@ JavaScriptCompiler.prototype = { programParams.push('depths'); } - return 'this.program(' + programParams.join(', ') + ')'; + return 'container.program(' + programParams.join(', ') + ')'; }, useRegister: function(name) { @@ -965,8 +965,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) { - options.fn = program || 'this.noop'; - options.inverse = inverse || 'this.noop'; + options.fn = program || 'container.noop'; + options.inverse = inverse || 'container.noop'; } // The parameters go on to the stack in order (making sure that they are evaluated in order) @@ -1061,7 +1061,7 @@ function strictLookup(requireTerminal, compiler, parts, type) { } if (requireTerminal) { - return [compiler.aliasable('this.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')']; + return [compiler.aliasable('container.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')']; } else { return stack; } diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 744e6eb76..fed72fb07 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -142,7 +142,7 @@ export function template(templateSpec, env) { } } - return '' + templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths); + return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); } ret.isTop = true; @@ -179,7 +179,7 @@ export function wrapProgram(container, i, fn, data, declaredBlockParams, blockPa currentDepths = [context].concat(depths); } - return fn.call(container, + return fn(container, context, container.helpers, container.partials, options.data || data, diff --git a/spec/expected/empty.amd.js b/spec/expected/empty.amd.js index 852733b2f..0b39884f5 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 templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(container,depth0,helpers,partials,data) { return ""; },"useData":true}); }); From 95d84badcae89aa72a6f1433b851304700320920 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 18 Aug 2015 23:57:27 -0700 Subject: [PATCH 62/82] Drop AST constructors in favor of JSON These were little more than object literal statements that were less clear due to their use of index-based arguments. Fixes #1077 --- lib/handlebars/compiler/ast.js | 126 ----------------------- lib/handlebars/compiler/base.js | 3 +- lib/handlebars/compiler/compiler.js | 9 +- lib/handlebars/compiler/helpers.js | 72 +++++++++---- lib/handlebars/compiler/visitor.js | 3 +- spec/ast.js | 151 ++++------------------------ spec/compiler.js | 10 +- spec/parser.js | 5 +- spec/visitor.js | 4 +- src/handlebars.yy | 57 ++++++++--- 10 files changed, 141 insertions(+), 299 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 599dab8d2..1699569ba 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,130 +1,4 @@ let AST = { - Program: function(statements, blockParams, strip, locInfo) { - this.loc = locInfo; - this.type = 'Program'; - this.body = statements; - - this.blockParams = blockParams; - this.strip = strip; - }, - - MustacheStatement: function(path, params, hash, escaped, strip, locInfo) { - this.loc = locInfo; - this.type = 'MustacheStatement'; - - this.path = path; - this.params = params || []; - this.hash = hash; - this.escaped = escaped; - - this.strip = strip; - }, - - BlockStatement: function(path, params, hash, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { - this.loc = locInfo; - this.type = 'BlockStatement'; - - this.path = path; - this.params = params || []; - this.hash = hash; - this.program = program; - this.inverse = inverse; - - this.openStrip = openStrip; - this.inverseStrip = inverseStrip; - this.closeStrip = closeStrip; - }, - - PartialStatement: function(name, params, hash, strip, locInfo) { - this.loc = locInfo; - this.type = 'PartialStatement'; - - this.name = name; - this.params = params || []; - this.hash = hash; - - this.indent = ''; - this.strip = strip; - }, - - ContentStatement: function(string, locInfo) { - this.loc = locInfo; - this.type = 'ContentStatement'; - this.original = this.value = string; - }, - - CommentStatement: function(comment, strip, locInfo) { - this.loc = locInfo; - this.type = 'CommentStatement'; - this.value = comment; - - this.strip = strip; - }, - - SubExpression: function(path, params, hash, locInfo) { - this.loc = locInfo; - - this.type = 'SubExpression'; - this.path = path; - this.params = params || []; - this.hash = hash; - }, - - PathExpression: function(data, depth, parts, original, locInfo) { - this.loc = locInfo; - this.type = 'PathExpression'; - - this.data = data; - this.original = original; - this.parts = parts; - this.depth = depth; - }, - - StringLiteral: function(string, locInfo) { - this.loc = locInfo; - this.type = 'StringLiteral'; - this.original = - this.value = string; - }, - - NumberLiteral: function(number, locInfo) { - this.loc = locInfo; - this.type = 'NumberLiteral'; - this.original = - this.value = Number(number); - }, - - BooleanLiteral: function(bool, locInfo) { - this.loc = locInfo; - this.type = 'BooleanLiteral'; - this.original = - this.value = bool === 'true'; - }, - - UndefinedLiteral: function(locInfo) { - this.loc = locInfo; - this.type = 'UndefinedLiteral'; - this.original = this.value = undefined; - }, - - NullLiteral: function(locInfo) { - this.loc = locInfo; - this.type = 'NullLiteral'; - this.original = this.value = null; - }, - - Hash: 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; - }, - // Public API used to evaluate derived attributes regarding AST nodes helpers: { // a mustache is definitely a helper if: diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 85c2997b4..c6871d399 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -1,5 +1,4 @@ import parser from './parser'; -import AST from './ast'; import WhitespaceControl from './whitespace-control'; import * as Helpers from './helpers'; import { extend } from '../utils'; @@ -7,7 +6,7 @@ import { extend } from '../utils'; export { parser }; let yy = {}; -extend(yy, Helpers, AST); +extend(yy, Helpers); export function parse(input, options) { // Just return if an already-compiled AST was passed in. diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 59a425f47..c8db7c955 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -514,6 +514,13 @@ function transformLiteralToPath(sexpr) { let 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.loc); + sexpr.path = { + type: 'PathExpression', + data: false, + depth: 0, + parts: [literal.original + ''], + original: literal.original + '', + loc: literal.loc + }; } } diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index f2edfa110..bf72034a0 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -32,8 +32,8 @@ export function stripComment(comment) { .replace(/-?-?~?\}\}$/, ''); } -export function preparePath(data, parts, locInfo) { - locInfo = this.locInfo(locInfo); +export function preparePath(data, parts, loc) { + loc = this.locInfo(loc); let original = data ? '@' : '', dig = [], @@ -49,7 +49,7 @@ export function preparePath(data, parts, locInfo) { if (!isLiteral && (part === '..' || part === '.' || part === 'this')) { if (dig.length > 0) { - throw new Exception('Invalid path: ' + original, {loc: locInfo}); + throw new Exception('Invalid path: ' + original, {loc}); } else if (part === '..') { depth++; depthString += '../'; @@ -59,7 +59,14 @@ export function preparePath(data, parts, locInfo) { } } - return new this.PathExpression(data, depth, dig, original, locInfo); + return { + type: 'PathExpression', + data, + depth, + parts: dig, + original, + loc + }; } export function prepareMustache(path, params, hash, open, strip, locInfo) { @@ -67,7 +74,15 @@ export function prepareMustache(path, params, hash, open, strip, locInfo) { let escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== '{' && escapeFlag !== '&'; - return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo)); + return { + type: 'MustacheStatement', + path, + params, + hash, + escaped, + strip, + loc: this.locInfo(locInfo) + }; } export function prepareRawBlock(openRawBlock, contents, close, locInfo) { @@ -78,13 +93,24 @@ export function prepareRawBlock(openRawBlock, contents, close, locInfo) { } locInfo = this.locInfo(locInfo); - let program = new this.Program(contents, null, {}, locInfo); + let program = { + type: 'Program', + body: contents, + strip: {}, + loc: locInfo + }; - return new this.BlockStatement( - openRawBlock.path, openRawBlock.params, openRawBlock.hash, - program, undefined, - {}, {}, {}, - locInfo); + return { + type: 'BlockStatement', + path: openRawBlock.path, + params: openRawBlock.params, + hash: openRawBlock.hash, + program, + openStrip: {}, + inverseStrip: {}, + closeStrip: {}, + loc: locInfo + }; } export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { @@ -115,11 +141,18 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver program = inverted; } - return new this.BlockStatement( - openBlock.path, openBlock.params, openBlock.hash, - program, inverse, - openBlock.strip, inverseStrip, close && close.strip, - this.locInfo(locInfo)); + return { + type: 'BlockStatement', + path: openBlock.path, + params: openBlock.params, + hash: openBlock.hash, + program, + inverse, + openStrip: openBlock.strip, + inverseStrip, + closeStrip: close && close.strip, + loc: this.locInfo(locInfo) + }; } export function prepareProgram(statements, loc) { @@ -143,7 +176,12 @@ export function prepareProgram(statements, loc) { } } - return new this.Program(statements, null, {}, loc); + return { + type: 'Program', + body: statements, + strip: {}, + loc: loc + }; } diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index ba7b3760c..47f86e0d3 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -1,5 +1,4 @@ import Exception from '../exception'; -import AST from './ast'; function Visitor() { this.parents = []; @@ -14,7 +13,7 @@ Visitor.prototype = { let value = this.accept(node[name]); if (this.mutating) { // Hacky sanity check: - if (value && (!value.type || !AST[value.type])) { + if (value && typeof value.type !== 'string') { throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type); } node[name] = value; diff --git a/spec/ast.js b/spec/ast.js index dc5410fa2..8f346d882 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -5,159 +5,46 @@ describe('ast', function() { var AST = Handlebars.AST; - var LOCATION_INFO = { - start: { - line: 1, - column: 1 - }, - end: { - line: 1, - column: 1 - } - }; - - function testLocationInfoStorage(node) { - 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('MustacheStatement', function() { - it('should store args', function() { - var mustache = new AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO); - equals(mustache.type, 'MustacheStatement'); - equals(mustache.escaped, true); - testLocationInfoStorage(mustache); - }); - }); describe('BlockStatement', function() { it('should throw on mustache mismatch', function() { shouldThrow(function() { handlebarsEnv.parse('\n {{#foo}}{{/bar}}'); }, Handlebars.Exception, "foo doesn't match bar - 2:5"); }); - - it('stores location info', function() { - var mustacheNode = new AST.MustacheStatement([{ original: 'foo'}], null, null, false, {}); - var block = new AST.BlockStatement( - mustacheNode, - null, null, - {body: []}, - {body: []}, - {}, - {}, - {}, - LOCATION_INFO); - testLocationInfoStorage(block); - }); - }); - describe('PathExpression', function() { - it('stores location info', function() { - var idNode = new AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO); - testLocationInfoStorage(idNode); - }); - }); - - describe('Hash', function() { - it('stores location info', function() { - var hash = new AST.Hash([], LOCATION_INFO); - testLocationInfoStorage(hash); - }); - }); - - describe('ContentStatement', function() { - it('stores location info', function() { - var content = new AST.ContentStatement('HI', LOCATION_INFO); - testLocationInfoStorage(content); - }); - }); - - describe('CommentStatement', function() { - it('stores location info', function() { - var comment = new AST.CommentStatement('HI', {}, LOCATION_INFO); - testLocationInfoStorage(comment); - }); - }); - - describe('NumberLiteral', function() { - it('stores location info', function() { - var integer = new AST.NumberLiteral('6', LOCATION_INFO); - testLocationInfoStorage(integer); - }); - }); - - describe('StringLiteral', function() { - it('stores location info', function() { - var string = new AST.StringLiteral('6', LOCATION_INFO); - testLocationInfoStorage(string); - }); - }); - - describe('BooleanLiteral', function() { - it('stores location info', function() { - var bool = new AST.BooleanLiteral('true', LOCATION_INFO); - testLocationInfoStorage(bool); - }); - }); - - describe('PartialStatement', function() { - it('provides default params', function() { - var pn = new AST.PartialStatement('so_partial', undefined, {}, {}, LOCATION_INFO); - equals(pn.params.length, 0); - }); - it('stores location info', function() { - var pn = new AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO); - testLocationInfoStorage(pn); - }); - }); - - describe('Program', function() { - it('storing location info', function() { - var pn = new AST.Program([], null, {}, LOCATION_INFO); - testLocationInfoStorage(pn); - }); - }); - - describe('SubExpression', function() { - it('provides default params', function() { - var pn = new AST.SubExpression('path', undefined, {}, LOCATION_INFO); - equals(pn.params.length, 0); - }); }); describe('helpers', function() { describe('#helperExpression', function() { it('should handle mustache statements', function() { - equals(AST.helpers.helperExpression(new AST.MustacheStatement('foo', [], undefined, false, {}, LOCATION_INFO)), false); - equals(AST.helpers.helperExpression(new AST.MustacheStatement('foo', [1], undefined, false, {}, LOCATION_INFO)), true); - equals(AST.helpers.helperExpression(new AST.MustacheStatement('foo', [], {}, false, {}, LOCATION_INFO)), true); + equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: undefined}), false); + equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [1], hash: undefined}), true); + equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: {}}), true); }); it('should handle block statements', function() { - equals(AST.helpers.helperExpression(new AST.BlockStatement('foo', [], undefined, false, {}, LOCATION_INFO)), false); - equals(AST.helpers.helperExpression(new AST.BlockStatement('foo', [1], undefined, false, {}, LOCATION_INFO)), true); - equals(AST.helpers.helperExpression(new AST.BlockStatement('foo', [], {}, false, {}, LOCATION_INFO)), true); + equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: undefined}), false); + equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [1], hash: undefined}), true); + equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: {}}), true); }); it('should handle subexpressions', function() { - equals(AST.helpers.helperExpression(new AST.SubExpression()), true); + equals(AST.helpers.helperExpression({type: 'SubExpression'}), true); }); it('should work with non-helper nodes', function() { - equals(AST.helpers.helperExpression(new AST.Program([], [], {}, LOCATION_INFO)), false); + equals(AST.helpers.helperExpression({type: 'Program'}), false); - equals(AST.helpers.helperExpression(new AST.PartialStatement()), false); - equals(AST.helpers.helperExpression(new AST.ContentStatement('a', LOCATION_INFO)), false); - equals(AST.helpers.helperExpression(new AST.CommentStatement('a', {}, LOCATION_INFO)), false); + equals(AST.helpers.helperExpression({type: 'PartialStatement'}), false); + equals(AST.helpers.helperExpression({type: 'ContentStatement'}), false); + equals(AST.helpers.helperExpression({type: 'CommentStatement'}), false); - equals(AST.helpers.helperExpression(new AST.PathExpression(false, 0, ['a'], 'a', LOCATION_INFO)), false); + equals(AST.helpers.helperExpression({type: 'PathExpression'}), false); - equals(AST.helpers.helperExpression(new AST.StringLiteral('a', LOCATION_INFO)), false); - equals(AST.helpers.helperExpression(new AST.NumberLiteral(1, LOCATION_INFO)), false); - equals(AST.helpers.helperExpression(new AST.BooleanLiteral(true, LOCATION_INFO)), false); - equals(AST.helpers.helperExpression(new AST.UndefinedLiteral(LOCATION_INFO)), false); - equals(AST.helpers.helperExpression(new AST.NullLiteral(LOCATION_INFO)), false); + equals(AST.helpers.helperExpression({type: 'StringLiteral'}), false); + equals(AST.helpers.helperExpression({type: 'NumberLiteral'}), false); + equals(AST.helpers.helperExpression({type: 'BooleanLiteral'}), false); + equals(AST.helpers.helperExpression({type: 'UndefinedLiteral'}), false); + equals(AST.helpers.helperExpression({type: 'NullLiteral'}), false); - equals(AST.helpers.helperExpression(new AST.Hash([], LOCATION_INFO)), false); - equals(AST.helpers.helperExpression(new AST.HashPair('foo', 'bar', LOCATION_INFO)), false); + equals(AST.helpers.helperExpression({type: 'Hash'}), false); + equals(AST.helpers.helperExpression({type: 'HashPair'}), false); }); }); }); diff --git a/spec/compiler.js b/spec/compiler.js index fe4b63a30..be1fb007d 100644 --- a/spec/compiler.js +++ b/spec/compiler.js @@ -39,7 +39,10 @@ describe('compiler', function() { }); it('can utilize AST instance', function() { - equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement('Hello')], null, {}))(), 'Hello'); + equal(Handlebars.compile({ + type: 'Program', + body: [ {type: 'ContentStatement', value: 'Hello'}] + })(), 'Hello'); }); it('can pass through an empty string', function() { @@ -58,7 +61,10 @@ describe('compiler', function() { }); it('can utilize AST instance', function() { - equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement('Hello')]), null, {})), true); + equal(/return "Hello"/.test(Handlebars.precompile({ + type: 'Program', + body: [ {type: 'ContentStatement', value: 'Hello'}] + })), true); }); it('can pass through an empty string', function() { diff --git a/spec/parser.js b/spec/parser.js index 424e2d178..82c32d05d 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -228,7 +228,10 @@ describe('parser', function() { describe('externally compiled AST', function() { it('can pass through an already-compiled AST', function() { - equals(astFor(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement('Hello')], null)), 'CONTENT[ \'Hello\' ]\n'); + equals(astFor({ + type: 'Program', + body: [ {type: 'ContentStatement', value: 'Hello'}] + }), 'CONTENT[ \'Hello\' ]\n'); }); }); diff --git a/spec/visitor.js b/spec/visitor.js index 1f50d79c1..23113cf77 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -49,7 +49,7 @@ describe('Visitor', function() { visitor.mutating = true; visitor.StringLiteral = function(string) { - return new Handlebars.AST.NumberLiteral(42, string.locInfo); + return {type: 'NumberLiteral', value: 42, loc: string.loc}; }; var ast = Handlebars.parse('{{foo foo="foo"}}'); @@ -109,7 +109,7 @@ describe('Visitor', function() { visitor.mutating = true; visitor.StringLiteral = function(string) { - return new Handlebars.AST.NumberLiteral(42, string.locInfo); + return {type: 'NumberLiteral', value: 42, loc: string.locInfo}; }; var ast = Handlebars.parse('{{foo "foo"}}'); diff --git a/src/handlebars.yy b/src/handlebars.yy index ecc79afc6..a05b477a3 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -18,12 +18,24 @@ statement | rawBlock -> $1 | partial -> $1 | content -> $1 - | COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) - ; + | COMMENT { + $$ = { + type: 'CommentStatement', + value: yy.stripComment($1), + strip: yy.stripFlags($1, $1), + loc: yy.locInfo(@$) + }; + }; content - : CONTENT -> new yy.ContentStatement($1, yy.locInfo(@$)) - ; + : CONTENT { + $$ = { + type: 'ContentStatement', + original: $1, + value: $1, + loc: yy.locInfo(@$) + }; + }; rawBlock : openRawBlock content+ END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) @@ -77,7 +89,17 @@ mustache ; partial - : OPEN_PARTIAL partialName param* hash? CLOSE -> new yy.PartialStatement($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) + : OPEN_PARTIAL partialName param* hash? CLOSE { + $$ = { + type: 'PartialStatement', + name: $2, + params: $3, + hash: $4, + indent: '', + strip: yy.stripFlags($1, $5), + loc: yy.locInfo(@$) + }; + } ; param @@ -86,15 +108,22 @@ param ; sexpr - : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR -> new yy.SubExpression($2, $3, $4, yy.locInfo(@$)) - ; + : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR { + $$ = { + type: 'SubExpression', + path: $2, + params: $3, + hash: $4, + loc: yy.locInfo(@$) + }; + }; hash - : hashSegment+ -> new yy.Hash($1, yy.locInfo(@$)) + : hashSegment+ -> {type: 'Hash', pairs: $1, loc: yy.locInfo(@$)} ; hashSegment - : ID EQUALS param -> new yy.HashPair(yy.id($1), $3, yy.locInfo(@$)) + : ID EQUALS param -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)} ; blockParams @@ -104,11 +133,11 @@ blockParams helperName : path -> $1 | dataName -> $1 - | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)) - | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) - | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) - | UNDEFINED -> new yy.UndefinedLiteral(yy.locInfo(@$)) - | NULL -> new yy.NullLiteral(yy.locInfo(@$)) + | STRING -> {type: 'StringLiteral', value: $1, original: $1, loc: yy.locInfo(@$)} + | NUMBER -> {type: 'NumberLiteral', value: Number($1), original: Number($1), loc: yy.locInfo(@$)} + | BOOLEAN -> {type: 'BooleanLiteral', value: $1 === 'true', original: $1 === 'true', loc: yy.locInfo(@$)} + | UNDEFINED -> {type: 'UndefinedLiteral', original: undefined, value: undefined, loc: yy.locInfo(@$)} + | NULL -> {type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(@$)} ; partialName From a634cd3219af25360eb5896fcd9f4c9dc0f08fdf Mon Sep 17 00:00:00 2001 From: hashchange Date: Fri, 21 Aug 2015 13:32:20 +0200 Subject: [PATCH 63/82] Add Marionette.Handlebars to "in the Wild" list --- README.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/README.markdown b/README.markdown index 363b5bcc0..61c0bdcf9 100644 --- a/README.markdown +++ b/README.markdown @@ -141,6 +141,7 @@ Handlebars in the Wild Handlebars.js with [jQuery](http://jquery.com/). * [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for handlebars projects. +* [Marionette.Handlebars](https://github.com/hashchange/marionette.handlebars) adds support for Handlebars and Mustache templates to Marionette. * [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins. * [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main From 2571dd8e8e43fd320672763564b16a5b3ae33966 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 22 Aug 2015 10:57:24 -0700 Subject: [PATCH 64/82] Improve sanity checks in compiler and visitor --- lib/handlebars/compiler/compiler.js | 5 +++++ lib/handlebars/compiler/visitor.js | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index c8db7c955..ad6b86145 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -90,6 +90,11 @@ Compiler.prototype = { }, accept: function(node) { + /* istanbul ignore next: Sanity code */ + if (!this[node.type]) { + throw new Exception('Unknown type: ' + node.type, node); + } + this.sourceNode.unshift(node); let ret = this[node.type](node); this.sourceNode.shift(); diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 47f86e0d3..89dd632d0 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -12,8 +12,9 @@ Visitor.prototype = { acceptKey: function(node, name) { let value = this.accept(node[name]); if (this.mutating) { - // Hacky sanity check: - if (value && typeof value.type !== 'string') { + // Hacky sanity check: This may have a few false positives for type for the helper + // methods but will generally do the right thing without a lot of overhead. + if (value && !Visitor.prototype[value.type]) { throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type); } node[name] = value; @@ -49,6 +50,11 @@ Visitor.prototype = { return; } + /* istanbul ignore next: Sanity code */ + if (!this[object.type]) { + throw new Exception('Unknown type: ' + object.type, object); + } + if (this.current) { this.parents.unshift(this.current); } From 91ffd32cad32b2d1cd310ff94f65b28c428206ac Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 14 Aug 2015 15:18:52 -0500 Subject: [PATCH 65/82] Implement partial blocks This allows for failover for missing partials as well as limited templating ability through the `{{> @partial-block }}` partial special case. Partial fix for #1018 --- docs/compiler-api.md | 14 ++++++- lib/handlebars/compiler/compiler.js | 14 ++++++- lib/handlebars/compiler/helpers.js | 19 +++++++++ lib/handlebars/compiler/printer.js | 16 ++++++++ lib/handlebars/compiler/visitor.js | 45 +++++++++++---------- lib/handlebars/runtime.js | 15 ++++++- spec/parser.js | 12 ++++++ spec/partials.js | 61 +++++++++++++++++++++++++++++ spec/tokenizer.js | 4 ++ spec/visitor.js | 3 +- src/handlebars.l | 1 + src/handlebars.yy | 7 ++++ 12 files changed, 184 insertions(+), 27 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index c09414f0b..abf837336 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -83,7 +83,19 @@ interface PartialStatement <: Statement { name: PathExpression | SubExpression; params: [ Expression ]; hash: Hash; - + + indent: string; + strip: StripFlags | null; +} + +interface PartialBlockStatement <: Statement { + type: "PartialBlockStatement"; + name: PathExpression | SubExpression; + params: [ Expression ]; + hash: Hash; + + program: Program | null; + indent: string; strip: StripFlags | null; } diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index ad6b86145..a689e7d4b 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -1,3 +1,5 @@ +/* eslint-disable new-cap */ + import Exception from '../exception'; import {isArray, indexOf} from '../utils'; import AST from './ast'; @@ -157,6 +159,11 @@ Compiler.prototype = { PartialStatement: function(partial) { this.usePartial = true; + let program = partial.program; + if (program) { + program = this.compileProgram(partial.program); + } + let params = partial.params; if (params.length > 1) { throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); @@ -170,7 +177,7 @@ Compiler.prototype = { this.accept(partial.name); } - this.setupFullMustacheParams(partial, undefined, undefined, true); + this.setupFullMustacheParams(partial, program, undefined, true); let indent = partial.indent || ''; if (this.options.preventIndent && indent) { @@ -181,9 +188,12 @@ Compiler.prototype = { this.opcode('invokePartial', isDynamic, partialName, indent); this.opcode('append'); }, + PartialBlockStatement: function(partialBlock) { + this.PartialStatement(partialBlock); + }, MustacheStatement: function(mustache) { - this.SubExpression(mustache); // eslint-disable-line new-cap + this.SubExpression(mustache); if (mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped'); diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index bf72034a0..e04f4dd90 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -185,3 +185,22 @@ export function prepareProgram(statements, loc) { } +export function preparePartialBlock(openPartialBlock, program, close, locInfo) { + if (openPartialBlock.name.original !== close.path.original) { + let errorNode = {loc: openPartialBlock.name.loc}; + + throw new Exception(openPartialBlock.name.original + " doesn't match " + close.path.original, errorNode); + } + + return { + type: 'PartialBlockStatement', + path: openPartialBlock.name, + params: openPartialBlock.params, + hash: openPartialBlock.hash, + program, + openStrip: openPartialBlock.strip, + closeStrip: close && close.strip, + loc: this.locInfo(locInfo) + }; +} + diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 107d4b652..cf7aa4848 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -84,6 +84,22 @@ PrintVisitor.prototype.PartialStatement = function(partial) { } return this.pad('{{> ' + content + ' }}'); }; +PrintVisitor.prototype.PartialBlockStatement = function(partial) { + let content = 'PARTIAL BLOCK:' + partial.name.original; + if (partial.params[0]) { + content += ' ' + this.accept(partial.params[0]); + } + if (partial.hash) { + content += ' ' + this.accept(partial.hash); + } + + content += ' ' + this.pad('PROGRAM:'); + this.padding++; + content += this.accept(partial.program); + this.padding--; + + return this.pad('{{> ' + content + ' }}'); +}; PrintVisitor.prototype.ContentStatement = function(content) { return this.pad("CONTENT[ '" + content.value + "' ]"); diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 89dd632d0..cc54c532a 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -75,35 +75,21 @@ Visitor.prototype = { this.acceptArray(program.body); }, - MustacheStatement: function(mustache) { - this.acceptRequired(mustache, 'path'); - this.acceptArray(mustache.params); - this.acceptKey(mustache, 'hash'); - }, + MustacheStatement: visitSubExpression, - BlockStatement: function(block) { - this.acceptRequired(block, 'path'); - this.acceptArray(block.params); - this.acceptKey(block, 'hash'); + BlockStatement: visitBlock, - this.acceptKey(block, 'program'); - this.acceptKey(block, 'inverse'); - }, + PartialStatement: visitPartial, + PartialBlockStatement: function(partial) { + visitPartial.call(this, partial); - PartialStatement: function(partial) { - this.acceptRequired(partial, 'name'); - this.acceptArray(partial.params); - this.acceptKey(partial, 'hash'); + this.acceptKey(partial, 'program'); }, ContentStatement: function(/* content */) {}, CommentStatement: function(/* comment */) {}, - SubExpression: function(sexpr) { - this.acceptRequired(sexpr, 'path'); - this.acceptArray(sexpr.params); - this.acceptKey(sexpr, 'hash'); - }, + SubExpression: visitSubExpression, PathExpression: function(/* path */) {}, @@ -121,4 +107,21 @@ Visitor.prototype = { } }; +function visitSubExpression(mustache) { + this.acceptRequired(mustache, 'path'); + this.acceptArray(mustache.params); + this.acceptKey(mustache, 'hash'); +} +function visitBlock(block) { + visitSubExpression.call(this, block); + + this.acceptKey(block, 'program'); + this.acceptKey(block, 'inverse'); +} +function visitPartial(partial) { + this.acceptRequired(partial, 'name'); + this.acceptArray(partial.params); + this.acceptKey(partial, 'hash'); +} + export default Visitor; diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index fed72fb07..de427523a 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -194,7 +194,11 @@ export function wrapProgram(container, i, fn, data, declaredBlockParams, blockPa export function resolvePartial(partial, context, options) { if (!partial) { - partial = options.partials[options.name]; + if (options.name === '@partial-block') { + partial = options.data['partial-block']; + } else { + partial = options.partials[options.name]; + } } else if (!partial.call && !options.name) { // This is a dynamic partial that returned a string options.name = partial; @@ -209,6 +213,15 @@ export function invokePartial(partial, context, options) { options.data.contextPath = options.ids[0] || options.data.contextPath; } + let partialBlock; + if (options.fn && options.fn !== noop) { + partialBlock = options.data['partial-block'] = options.fn; + } + + if (partial === undefined && partialBlock) { + partial = partialBlock; + } + if (partial === undefined) { throw new Exception('The partial ' + options.name + ' could not be found'); } else if (partial instanceof Function) { diff --git a/spec/parser.js b/spec/parser.js index 82c32d05d..5b60f931d 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -113,6 +113,18 @@ describe('parser', function() { equals(astFor('{{> shared/partial?.bar}}'), '{{> PARTIAL:shared/partial?.bar }}\n'); }); + it('parsers partial blocks', function() { + equals(astFor('{{#> foo}}bar{{/foo}}'), '{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ \'bar\' ]\n }}\n'); + }); + it('should handle parser block mismatch', function() { + shouldThrow(function() { + astFor('{{#> goodbyes}}{{/hellos}}'); + }, Error, (/goodbyes doesn't match hellos/)); + }); + it('parsers partial blocks with arguments', function() { + equals(astFor('{{#> foo context hash=value}}bar{{/foo}}'), '{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n CONTENT[ \'bar\' ]\n }}\n'); + }); + it('parses a comment', function() { equals(astFor('{{! this is a comment }}'), "{{! ' this is a comment ' }}\n"); }); diff --git a/spec/partials.js b/spec/partials.js index a9cd3dd99..314cca256 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -196,6 +196,67 @@ describe('partials', function() { handlebarsEnv.compile = compile; }); + describe('partial blocks', function() { + it('should render partial block as default', function() { + shouldCompileToWithPartials( + '{{#> dude}}success{{/dude}}', + [{}, {}, {}], + true, + 'success'); + }); + it('should execute default block with proper context', function() { + shouldCompileToWithPartials( + '{{#> dude context}}{{value}}{{/dude}}', + [{context: {value: 'success'}}, {}, {}], + true, + 'success'); + }); + it('should propagate block parameters to default block', function() { + shouldCompileToWithPartials( + '{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}', + [{context: {value: 'success'}}, {}, {}], + true, + 'success'); + }); + + it('should not use partial block if partial exists', function() { + shouldCompileToWithPartials( + '{{#> dude}}fail{{/dude}}', + [{}, {}, {dude: 'success'}], + true, + 'success'); + }); + + it('should render block from partial', function() { + shouldCompileToWithPartials( + '{{#> dude}}success{{/dude}}', + [{}, {}, {dude: '{{> @partial-block }}'}], + true, + 'success'); + }); + it('should render block from partial with context', function() { + shouldCompileToWithPartials( + '{{#> dude}}{{value}}{{/dude}}', + [{context: {value: 'success'}}, {}, {dude: '{{#with context}}{{> @partial-block }}{{/with}}'}], + true, + 'success'); + }); + it('should render block from partial with context', function() { + shouldCompileToWithPartials( + '{{#> dude}}{{../context/value}}{{/dude}}', + [{context: {value: 'success'}}, {}, {dude: '{{#with context}}{{> @partial-block }}{{/with}}'}], + true, + 'success'); + }); + it('should render block from partial with block params', function() { + shouldCompileToWithPartials( + '{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}', + [{context: {value: 'success'}}, {}, {dude: '{{> @partial-block }}'}], + true, + 'success'); + }); + }); + it('should pass compiler flags', function() { if (Handlebars.compile) { var env = Handlebars.create(); diff --git a/spec/tokenizer.js b/spec/tokenizer.js index a474dfb16..f17070473 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -214,6 +214,10 @@ describe('Tokenizer', function() { shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']); }); + it('tokenizes partial block declarations', function() { + var result = tokenize('{{#> foo}}'); + shouldMatchTokens(result, ['OPEN_PARTIAL_BLOCK', 'ID', 'CLOSE']); + }); 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']); diff --git a/spec/visitor.js b/spec/visitor.js index 23113cf77..3e2d5238b 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -8,6 +8,7 @@ describe('Visitor', function() { // stub methods are executed var visitor = new Handlebars.Visitor(); visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); + visitor.accept(Handlebars.parse('{{#> bar }} {{/bar}}')); }); it('should traverse to stubs', function() { @@ -40,8 +41,6 @@ describe('Visitor', function() { visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}')); }); - it('should return undefined'); - describe('mutating', function() { describe('fields', function() { it('should replace value', function() { diff --git a/src/handlebars.l b/src/handlebars.l index f7df8f55c..39a7884f3 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -80,6 +80,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} return 'CLOSE_RAW_BLOCK'; } "{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; +"{{"{LEFT_STRIP}?"#>" return 'OPEN_PARTIAL_BLOCK'; "{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; "{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; "{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; diff --git a/src/handlebars.yy b/src/handlebars.yy index a05b477a3..ee0eddb51 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -17,6 +17,7 @@ statement | block -> $1 | rawBlock -> $1 | partial -> $1 + | partialBlock -> $1 | content -> $1 | COMMENT { $$ = { @@ -101,6 +102,12 @@ partial }; } ; +partialBlock + : openPartialBlock program closeBlock -> yy.preparePartialBlock($1, $2, $3, @$) + ; +openPartialBlock + : OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { name: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) } + ; param : helperName -> $1 From 233caf3f7c78b310b723fe80473e932418d01c65 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 15 Aug 2015 14:19:02 -0500 Subject: [PATCH 66/82] Create validateClose helper method Avoid duplicating the logic needed to check for close block mismatches. --- lib/handlebars/compiler/helpers.js | 39 +++++++++++++++--------------- src/handlebars.yy | 2 +- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index e04f4dd90..9c40f0d5d 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -1,5 +1,15 @@ import Exception from '../exception'; +function validateClose(open, close) { + close = close.path ? close.path.original : close; + + if (open.path.original !== close) { + let errorNode = {loc: open.path.loc}; + + throw new Exception(open.path.original + " doesn't match " + close, errorNode); + } +} + export function SourceLocation(source, locInfo) { this.source = source; this.start = { @@ -86,11 +96,7 @@ export function prepareMustache(path, params, hash, open, strip, locInfo) { } export function prepareRawBlock(openRawBlock, contents, close, locInfo) { - if (openRawBlock.path.original !== close) { - let errorNode = {loc: openRawBlock.path.loc}; - - throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode); - } + validateClose(openRawBlock, close); locInfo = this.locInfo(locInfo); let program = { @@ -114,11 +120,8 @@ export function prepareRawBlock(openRawBlock, contents, close, locInfo) { } export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { - // When we are chaining inverse calls, we will not have a close path - if (close && close.path && openBlock.path.original !== close.path.original) { - let errorNode = {loc: openBlock.path.loc}; - - throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode); + if (close && close.path) { + validateClose(openBlock, close); } program.blockParams = openBlock.blockParams; @@ -185,20 +188,16 @@ export function prepareProgram(statements, loc) { } -export function preparePartialBlock(openPartialBlock, program, close, locInfo) { - if (openPartialBlock.name.original !== close.path.original) { - let errorNode = {loc: openPartialBlock.name.loc}; - - throw new Exception(openPartialBlock.name.original + " doesn't match " + close.path.original, errorNode); - } +export function preparePartialBlock(open, program, close, locInfo) { + validateClose(open, close); return { type: 'PartialBlockStatement', - path: openPartialBlock.name, - params: openPartialBlock.params, - hash: openPartialBlock.hash, + name: open.path, + params: open.params, + hash: open.hash, program, - openStrip: openPartialBlock.strip, + openStrip: open.strip, closeStrip: close && close.strip, loc: this.locInfo(locInfo) }; diff --git a/src/handlebars.yy b/src/handlebars.yy index ee0eddb51..e94ab5116 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -106,7 +106,7 @@ partialBlock : openPartialBlock program closeBlock -> yy.preparePartialBlock($1, $2, $3, @$) ; openPartialBlock - : OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { name: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) } + : OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { path: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) } ; param From 0b5e82e1c34b5f765b16490ac1de59a0d1b651c3 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 18 Aug 2015 22:48:00 -0700 Subject: [PATCH 67/82] Add whitespace control to partial block statements --- lib/handlebars/compiler/whitespace-control.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js index d1b743d7e..6c8a9864f 100644 --- a/lib/handlebars/compiler/whitespace-control.js +++ b/lib/handlebars/compiler/whitespace-control.js @@ -61,7 +61,9 @@ WhitespaceControl.prototype.Program = function(program) { return program; }; -WhitespaceControl.prototype.BlockStatement = function(block) { + +WhitespaceControl.prototype.BlockStatement = +WhitespaceControl.prototype.PartialBlockStatement = function(block) { this.accept(block.program); this.accept(block.inverse); From 1c274088c1ea9969f7a676fd5bebd11698f73116 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 22 Aug 2015 10:56:40 -0700 Subject: [PATCH 68/82] Update AST doc for partial block --- docs/compiler-api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index abf837336..81438d20d 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -97,7 +97,8 @@ interface PartialBlockStatement <: Statement { program: Program | null; indent: string; - strip: StripFlags | null; + openStrip: StripFlags | null; + closeStrip: StripFlags | null; } ``` From 2a4a5447f560723a2c898e0a4d97cd929131bba6 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 19 Aug 2015 06:35:32 -0700 Subject: [PATCH 69/82] Implement decorator environment and registration --- lib/handlebars/base.js | 17 ++++++++++++++- lib/handlebars/decorators.js | 5 +++++ lib/handlebars/runtime.js | 4 ++++ spec/blocks.js | 40 ++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 lib/handlebars/decorators.js diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 41bb98d41..e59f5e782 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,6 +1,7 @@ import {createFrame, extend, toString} from './utils'; import Exception from './exception'; import {registerDefaultHelpers} from './helpers'; +import {registerDefaultDecorators} from './decorators'; import logger from './logger'; export const VERSION = '3.0.1'; @@ -17,11 +18,13 @@ export const REVISION_CHANGES = { const objectType = '[object Object]'; -export function HandlebarsEnvironment(helpers, partials) { +export function HandlebarsEnvironment(helpers, partials, decorators) { this.helpers = helpers || {}; this.partials = partials || {}; + this.decorators = decorators || {}; registerDefaultHelpers(this); + registerDefaultDecorators(this); } HandlebarsEnvironment.prototype = { @@ -54,6 +57,18 @@ HandlebarsEnvironment.prototype = { }, unregisterPartial: function(name) { delete this.partials[name]; + }, + + registerDecorator: function(name, fn) { + if (toString.call(name) === objectType) { + if (fn) { throw new Exception('Arg not supported with multiple decorators'); } + extend(this.decorators, name); + } else { + this.decorators[name] = fn; + } + }, + unregisterDecorator: function(name) { + delete this.decorators[name]; } }; diff --git a/lib/handlebars/decorators.js b/lib/handlebars/decorators.js new file mode 100644 index 000000000..d5caefb48 --- /dev/null +++ b/lib/handlebars/decorators.js @@ -0,0 +1,5 @@ +import registerInline from './decorators/inline'; + +export function registerDefaultDecorators(instance) { +} + diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index de427523a..2300439f2 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -153,9 +153,13 @@ export function template(templateSpec, env) { if (templateSpec.usePartial) { container.partials = container.merge(options.partials, env.partials); } + if (templateSpec.useDecorators) { + container.decorators = container.merge(options.decorators, env.decorators); + } } else { container.helpers = options.helpers; container.partials = options.partials; + container.decorators = options.decorators; } }; diff --git a/spec/blocks.js b/spec/blocks.js index 71c9045d2..762263437 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -166,4 +166,44 @@ describe('blocks', function() { shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel '); }); }); + + describe('decorators', function() { + describe('registration', function() { + it('unregisters', function() { + handlebarsEnv.decorators = {}; + + handlebarsEnv.registerDecorator('foo', function() { + return 'fail'; + }); + + equals(!!handlebarsEnv.decorators.foo, true); + handlebarsEnv.unregisterDecorator('foo'); + equals(handlebarsEnv.decorators.foo, undefined); + }); + + it('allows multiple globals', function() { + handlebarsEnv.decorators = {}; + + handlebarsEnv.registerDecorator({ + foo: function() {}, + bar: function() {} + }); + + equals(!!handlebarsEnv.decorators.foo, true); + equals(!!handlebarsEnv.decorators.bar, true); + handlebarsEnv.unregisterDecorator('foo'); + handlebarsEnv.unregisterDecorator('bar'); + equals(handlebarsEnv.decorators.foo, undefined); + equals(handlebarsEnv.decorators.bar, undefined); + }); + it('fails with multiple and args', function() { + shouldThrow(function() { + handlebarsEnv.registerDecorator({ + world: function() { return 'world!'; }, + testHelper: function() { return 'found it!'; } + }, {}); + }, Error, 'Arg not supported with multiple decorators'); + }); + }); + }); }); From 408192ba9f262bb82be88091ab3ec3c16dc02c6d Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 19 Aug 2015 06:59:32 -0700 Subject: [PATCH 70/82] Add decorator parsing --- docs/compiler-api.md | 27 +++++++++++++++++++ lib/handlebars/compiler/helpers.js | 11 ++++++-- lib/handlebars/compiler/printer.js | 8 ++++-- lib/handlebars/compiler/visitor.js | 2 ++ lib/handlebars/compiler/whitespace-control.js | 2 ++ spec/parser.js | 14 ++++++++++ spec/tokenizer.js | 9 +++++++ spec/visitor.js | 2 ++ src/handlebars.l | 4 +-- src/handlebars.yy | 2 +- 10 files changed, 74 insertions(+), 7 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 81438d20d..5722dccff 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -120,6 +120,33 @@ interface CommentStatement <: Statement { } ``` + +```java +interface Decorator <: Statement { + type: "Decorator"; + + path: PathExpression | Literal; + params: [ Expression ]; + hash: Hash; + + strip: StripFlags | null; +} + +interface DecoratorBlock <: Statement { + type: "DecoratorBlock"; + path: PathExpression | Literal; + params: [ Expression ]; + hash: Hash; + + program: Program | null; + + openStrip: StripFlags | null; + closeStrip: StripFlags | null; +} +``` + +Decorator paths only utilize the `path.original` value and as a consequence do not support depthed evaluation. + ### Expressions ```java diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 9c40f0d5d..e09a08df9 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -84,8 +84,9 @@ export function prepareMustache(path, params, hash, open, strip, locInfo) { let escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== '{' && escapeFlag !== '&'; + let decorator = (/\*/.test(open)); return { - type: 'MustacheStatement', + type: decorator ? 'Decorator' : 'MustacheStatement', path, params, hash, @@ -124,12 +125,18 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver validateClose(openBlock, close); } + let decorator = (/\*/.test(openBlock.open)); + program.blockParams = openBlock.blockParams; let inverse, inverseStrip; if (inverseAndProgram) { + if (decorator) { + throw new Exception('Unexpected inverse block on decorator', inverseAndProgram); + } + if (inverseAndProgram.chain) { inverseAndProgram.program.body[0].closeStrip = close.strip; } @@ -145,7 +152,7 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver } return { - type: 'BlockStatement', + type: decorator ? 'DecoratorBlock' : 'BlockStatement', path: openBlock.path, params: openBlock.params, hash: openBlock.hash, diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index cf7aa4848..66e7c7d4b 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -48,11 +48,15 @@ PrintVisitor.prototype.Program = function(program) { PrintVisitor.prototype.MustacheStatement = function(mustache) { return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); }; +PrintVisitor.prototype.Decorator = function(mustache) { + return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}'); +}; -PrintVisitor.prototype.BlockStatement = function(block) { +PrintVisitor.prototype.BlockStatement = +PrintVisitor.prototype.DecoratorBlock = function(block) { let out = ''; - out += this.pad('BLOCK:'); + out += this.pad((block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:'); this.padding++; out += this.pad(this.SubExpression(block)); if (block.program) { diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index cc54c532a..2c504d1b2 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -76,8 +76,10 @@ Visitor.prototype = { }, MustacheStatement: visitSubExpression, + Decorator: visitSubExpression, BlockStatement: visitBlock, + DecoratorBlock: visitBlock, PartialStatement: visitPartial, PartialBlockStatement: function(partial) { diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js index 6c8a9864f..e11483c91 100644 --- a/lib/handlebars/compiler/whitespace-control.js +++ b/lib/handlebars/compiler/whitespace-control.js @@ -63,6 +63,7 @@ WhitespaceControl.prototype.Program = function(program) { }; WhitespaceControl.prototype.BlockStatement = +WhitespaceControl.prototype.DecoratorBlock = WhitespaceControl.prototype.PartialBlockStatement = function(block) { this.accept(block.program); this.accept(block.inverse); @@ -124,6 +125,7 @@ WhitespaceControl.prototype.PartialBlockStatement = function(block) { return strip; }; +WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function(mustache) { return mustache.strip; }; diff --git a/spec/parser.js b/spec/parser.js index 5b60f931d..3b7e3e45f 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -247,6 +247,20 @@ describe('parser', function() { }); }); + describe('directives', function() { + it('should parse block directives', function() { + equals(astFor('{{#* foo}}{{/foo}}'), 'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n'); + }); + it('should parse directives', function() { + equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n'); + }); + it('should fail if directives have inverse', function() { + shouldThrow(function() { + astFor('{{#* foo}}{{^}}{{/foo}}'); + }, Error, /Unexpected inverse/); + }); + }); + it('GH1024 - should track program location properly', function() { var p = Handlebars.parse('\n' + ' {{#if foo}}\n' diff --git a/spec/tokenizer.js b/spec/tokenizer.js index f17070473..dc077ce72 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -241,6 +241,15 @@ describe('Tokenizer', function() { shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']); }); + it('tokenizes directives', function() { + shouldMatchTokens( + tokenize('{{#*foo}}content{{/foo}}'), + ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']); + shouldMatchTokens( + tokenize('{{*foo}}'), + ['OPEN', 'ID', 'CLOSE']); + }); + it('tokenizes inverse sections as "INVERSE"', function() { shouldMatchTokens(tokenize('{{^}}'), ['INVERSE']); shouldMatchTokens(tokenize('{{else}}'), ['INVERSE']); diff --git a/spec/visitor.js b/spec/visitor.js index 3e2d5238b..d3fb795e2 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -9,6 +9,8 @@ describe('Visitor', function() { var visitor = new Handlebars.Visitor(); visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); visitor.accept(Handlebars.parse('{{#> bar }} {{/bar}}')); + visitor.accept(Handlebars.parse('{{#* bar }} {{/bar}}')); + visitor.accept(Handlebars.parse('{{* bar }}')); }); it('should traverse to stubs', function() { diff --git a/src/handlebars.l b/src/handlebars.l index 39a7884f3..4c3c30421 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -81,7 +81,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} } "{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; "{{"{LEFT_STRIP}?"#>" return 'OPEN_PARTIAL_BLOCK'; -"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; +"{{"{LEFT_STRIP}?"#""*"? return 'OPEN_BLOCK'; "{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; "{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; "{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; @@ -98,7 +98,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} this.popState(); return 'COMMENT'; } -"{{"{LEFT_STRIP}? return 'OPEN'; +"{{"{LEFT_STRIP}?"*"? return 'OPEN'; "=" return 'EQUALS'; ".." return 'ID'; diff --git a/src/handlebars.yy b/src/handlebars.yy index e94ab5116..ce0649838 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -52,7 +52,7 @@ block ; openBlock - : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } + : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { open: $1, path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; openInverse From 452afbf2ffdf32a6d7112b727cc95cc6b415e8a5 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 19 Aug 2015 08:27:13 -0700 Subject: [PATCH 71/82] Implement block decorators These allow for a given block to be wrapped in helper methods or metadata and allow for more control over the current container and method before the code is run. --- .eslintrc | 2 +- docs/compiler-api.md | 2 +- lib/handlebars/compiler/code-gen.js | 3 + lib/handlebars/compiler/compiler.js | 13 ++ .../compiler/javascript-compiler.js | 71 ++++++++- lib/handlebars/runtime.js | 23 ++- spec/blocks.js | 138 ++++++++++++++++++ 7 files changed, 244 insertions(+), 8 deletions(-) diff --git a/.eslintrc b/.eslintrc index 997093302..237b5ee6d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -45,7 +45,7 @@ "no-extra-boolean-cast": 2, "no-extra-parens": 0, "no-extra-semi": 2, - "no-func-assign": 2, + "no-func-assign": 0, // Stylistic... might consider disallowing in the future "no-inner-declarations": 0, diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 5722dccff..29382191e 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -276,7 +276,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c - `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`. + - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, 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. diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index 3af4f8cb1..6541fe873 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -69,6 +69,9 @@ function CodeGen(srcFile) { } CodeGen.prototype = { + isEmpty() { + return !this.source.length; + }, prepend: function(source, loc) { this.source.unshift(this.wrap(source, loc)); }, diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index a689e7d4b..64af5daeb 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -156,6 +156,15 @@ Compiler.prototype = { this.opcode('append'); }, + DecoratorBlock(decorator) { + let program = decorator.program && this.compileProgram(decorator.program); + let params = this.setupFullMustacheParams(decorator, program, undefined), + path = decorator.path; + + this.useDecorators = true; + this.opcode('registerDecorator', params.length, path.original); + }, + PartialStatement: function(partial) { this.usePartial = true; @@ -201,6 +210,10 @@ Compiler.prototype = { this.opcode('append'); } }, + Decorator(decorator) { + this.DecoratorBlock(decorator); + }, + ContentStatement: function(content) { if (content.value) { diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index b8fc9761f..ede0b5e49 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -64,6 +64,7 @@ JavaScriptCompiler.prototype = { this.name = this.environment.name; this.isChild = !!context; this.context = context || { + decorators: [], programs: [], environments: [] }; @@ -81,7 +82,7 @@ JavaScriptCompiler.prototype = { this.compileChildren(environment, options); - this.useDepths = this.useDepths || environment.useDepths || this.options.compat; + this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat; this.useBlockParams = this.useBlockParams || environment.useBlockParams; let opcodes = environment.opcodes, @@ -107,16 +108,43 @@ JavaScriptCompiler.prototype = { throw new Exception('Compile completed with content left on stack'); } + if (!this.decorators.isEmpty()) { + this.useDecorators = true; + + this.decorators.prepend('var decorators = container.decorators;\n'); + this.decorators.push('return fn;'); + + if (asObject) { + this.decorators = Function.apply(this, ['fn', 'props', 'container', 'depth0', 'data', 'blockParams', 'depths', this.decorators.merge()]); + } else { + this.decorators.prepend('function(fn, props, container, depth0, data, blockParams, depths) {\n'); + this.decorators.push('}\n'); + this.decorators = this.decorators.merge(); + } + } else { + this.decorators = undefined; + } + let fn = this.createFunctionContext(asObject); if (!this.isChild) { let ret = { compiler: this.compilerInfo(), main: fn }; - let programs = this.context.programs; + + if (this.decorators) { + ret.main_d = this.decorators; // eslint-disable-line camelcase + ret.useDecorators = true; + } + + let {programs, decorators} = this.context; for (i = 0, l = programs.length; i < l; i++) { if (programs[i]) { ret[i] = programs[i]; + if (decorators[i]) { + ret[i + '_d'] = decorators[i]; + ret.useDecorators = true; + } } } @@ -163,6 +191,7 @@ JavaScriptCompiler.prototype = { // getContext opcode when it would be a noop this.lastContext = 0; this.source = new CodeGen(this.options.srcName); + this.decorators = new CodeGen(this.options.srcName); }, createFunctionContext: function(asObject) { @@ -561,6 +590,24 @@ JavaScriptCompiler.prototype = { } }, + // [registerDecorator] + // + // On stack, before: hash, program, params..., ... + // On stack, after: ... + // + // Pops off the decorator's parameters, invokes the decorator, + // and inserts the decorator into the decorators list. + registerDecorator(paramSize, name) { + let foundDecorator = this.nameLookup('decorators', name, 'decorator'), + options = this.setupHelperArgs(name, paramSize); + + this.decorators.push([ + 'fn = ', + this.decorators.functionCall(foundDecorator, '', ['fn', 'props', 'container', options]), + ' || fn;' + ]); + }, + // [invokeHelper] // // On stack, before: hash, inverse, program, params..., ... @@ -738,6 +785,7 @@ JavaScriptCompiler.prototype = { child.index = index; child.name = 'program' + index; this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile); + this.context.decorators[index] = compiler.decorators; this.context.environments[index] = child; this.useDepths = this.useDepths || compiler.useDepths; @@ -946,7 +994,16 @@ JavaScriptCompiler.prototype = { }, setupParams: function(helper, paramSize, params) { - let options = {}, contexts = [], types = [], ids = [], param; + let options = {}, + contexts = [], + types = [], + ids = [], + objectArgs = !params, + param; + + if (objectArgs) { + params = []; + } options.name = this.quotedString(helper); options.hash = this.popStack(); @@ -985,6 +1042,10 @@ JavaScriptCompiler.prototype = { } } + if (objectArgs) { + options.args = this.source.generateArray(params); + } + if (this.trackIds) { options.ids = this.source.generateArray(ids); } @@ -1009,9 +1070,11 @@ JavaScriptCompiler.prototype = { this.useRegister('options'); params.push('options'); return ['options=', options]; - } else { + } else if (params) { params.push(options); return ''; + } else { + return options; } } }; diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 2300439f2..e1b069eba 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -90,7 +90,9 @@ export function template(templateSpec, env) { invokePartial: invokePartialWrapper, fn: function(i) { - return templateSpec[i]; + let ret = templateSpec[i]; + ret.decorator = templateSpec[i + '_d']; + return ret; }, programs: [], @@ -142,7 +144,17 @@ export function template(templateSpec, env) { } } - return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); + function main(context/*, options*/) { + return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); + } + + if (templateSpec.main_d) { + // Note that we are ignoring the props value here as we apply things slightly differently + // when applying decorators to the root function. + main = templateSpec.main_d(main, {}, container, undefined, data, blockParams, depths); + } + + return main(context, options); } ret.isTop = true; @@ -190,6 +202,13 @@ export function wrapProgram(container, i, fn, data, declaredBlockParams, blockPa blockParams && [options.blockParams].concat(blockParams), currentDepths); } + + if (fn.decorator) { + let props = {}; + prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths); + Utils.extend(prog, props); + } + prog.program = i; prog.depth = depths ? depths.length : 0; prog.blockParams = declaredBlockParams || 0; diff --git a/spec/blocks.js b/spec/blocks.js index 762263437..2fbaee7cc 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -168,6 +168,144 @@ describe('blocks', function() { }); describe('decorators', function() { + it('should apply mustache decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn) { + fn.run = 'success'; + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should apply allow undefined return', function() { + var helpers = { + helper: function(options) { + return options.fn() + options.fn.run; + } + }; + var decorators = { + decorator: function(fn) { + fn.run = 'cess'; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator}}suc{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should apply block decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.fn(); + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should support nested decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.fn.nested + options.fn(); + return fn; + }, + nested: function(fn, props, container, options) { + props.nested = options.fn(); + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should apply multiple decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = (fn.run || '') + options.fn(); + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should access parent variables', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.args; + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator foo}}{{/helper}}', + {hash: {'foo': 'success'}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should work with root program', function() { + var run; + var decorators = { + decorator: function(fn, props, container, options) { + equals(options.args[0], 'success'); + run = true; + return fn; + } + }; + shouldCompileTo( + '{{*decorator "success"}}', + {hash: {'foo': 'success'}, decorators: decorators}, + ''); + equals(run, true); + }); + it('should fail when accessing variables from root', function() { + var run; + var decorators = { + decorator: function(fn, props, container, options) { + equals(options.args[0], undefined); + run = true; + return fn; + } + }; + shouldCompileTo( + '{{*decorator foo}}', + {hash: {'foo': 'fail'}, decorators: decorators}, + ''); + equals(run, true); + }); + describe('registration', function() { it('unregisters', function() { handlebarsEnv.decorators = {}; From 495cd05a7e27bf0485ffc07cb32324402422868b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 22 Aug 2015 10:54:54 -0700 Subject: [PATCH 72/82] Implement inline partials Allows for partials to be defined within the current template to allow for localized code reuse as well as for conditional behavior within nested partials. Fixes #1018 --- lib/handlebars/decorators.js | 1 + lib/handlebars/decorators/inline.js | 22 ++++++++++++++++ lib/handlebars/runtime.js | 4 +++ spec/partials.js | 39 +++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 lib/handlebars/decorators/inline.js diff --git a/lib/handlebars/decorators.js b/lib/handlebars/decorators.js index d5caefb48..6f5a61525 100644 --- a/lib/handlebars/decorators.js +++ b/lib/handlebars/decorators.js @@ -1,5 +1,6 @@ import registerInline from './decorators/inline'; export function registerDefaultDecorators(instance) { + registerInline(instance); } diff --git a/lib/handlebars/decorators/inline.js b/lib/handlebars/decorators/inline.js new file mode 100644 index 000000000..214246620 --- /dev/null +++ b/lib/handlebars/decorators/inline.js @@ -0,0 +1,22 @@ +import {extend} from '../utils'; + +export default function(instance) { + instance.registerDecorator('inline', function(fn, props, container, options) { + let ret = fn; + if (!props.partials) { + props.partials = {}; + ret = function(context, options) { + // Create a new partials stack frame prior to exec. + let original = container.partials; + container.partials = extend({}, original, props.partials); + let ret = fn(context, options); + container.partials = original; + return ret; + }; + } + + props.partials[options.args[0]] = options.fn; + + return ret; + }); +} diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index e1b069eba..6ee5c84e7 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -239,6 +239,10 @@ export function invokePartial(partial, context, options) { let partialBlock; if (options.fn && options.fn !== noop) { partialBlock = options.data['partial-block'] = options.fn; + + if (partialBlock.partials) { + options.partials = Utils.extend({}, options.partials, partialBlock.partials); + } } if (partial === undefined && partialBlock) { diff --git a/spec/partials.js b/spec/partials.js index 314cca256..f3283ba58 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -257,6 +257,45 @@ describe('partials', function() { }); }); + describe('inline partials', function() { + it('should define inline partials for template', function() { + shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success'); + }); + it('should overwrite multiple partials in the same template', function() { + shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success'); + }); + it('should define inline partials for block', function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success'); + shouldThrow(function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}', {}, 'success'); + }, Error, /myPartial could not/); + }); + it('should override global partials', function() { + shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {hash: {}, partials: {myPartial: function() { return 'fail'; }}}, 'success'); + }); + it('should override template partials', function() { + shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success'); + }); + it('should override partials down the entire stack', function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}', {}, 'success'); + }); + + it('should define inline partials for partial call', function() { + shouldCompileToWithPartials( + '{{#*inline "myPartial"}}success{{/inline}}{{> dude}}', + [{}, {}, {dude: '{{> myPartial }}'}], + true, + 'success'); + }); + it('should define inline partials in partial block call', function() { + shouldCompileToWithPartials( + '{{#> dude}}{{#*inline "myPartial"}}success{{/inline}}{{/dude}}', + [{}, {}, {dude: '{{> myPartial }}'}], + true, + 'success'); + }); + }); + it('should pass compiler flags', function() { if (Handlebars.compile) { var env = Handlebars.create(); From 6c45f49b24d63acda37072df464bd670af97a072 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 22 Aug 2015 10:56:10 -0700 Subject: [PATCH 73/82] Implement decorator helper method --- lib/handlebars/runtime.js | 25 +++++++++++++------------ spec/runtime.js | 6 +++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 6ee5c84e7..6b31a7ba4 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -29,6 +29,8 @@ export function template(templateSpec, env) { throw new Exception('Unknown template object: ' + typeof templateSpec); } + templateSpec.main.decorator = templateSpec.main_d; + // Note: Using env.VM references rather than local var references throughout this section to allow // for external users to override these as psuedo-supported APIs. env.VM.checkRevision(templateSpec.compiler); @@ -147,13 +149,7 @@ export function template(templateSpec, env) { function main(context/*, options*/) { return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); } - - if (templateSpec.main_d) { - // Note that we are ignoring the props value here as we apply things slightly differently - // when applying decorators to the root function. - main = templateSpec.main_d(main, {}, container, undefined, data, blockParams, depths); - } - + main = executeDecorators(templateSpec.main, main, container, options.depths || [], data, blockParams); return main(context, options); } ret.isTop = true; @@ -203,11 +199,7 @@ export function wrapProgram(container, i, fn, data, declaredBlockParams, blockPa currentDepths); } - if (fn.decorator) { - let props = {}; - prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths); - Utils.extend(prog, props); - } + prog = executeDecorators(fn, prog, container, depths, data, blockParams); prog.program = i; prog.depth = depths ? depths.length : 0; @@ -265,3 +257,12 @@ function initData(context, data) { } return data; } + +function executeDecorators(fn, prog, container, depths, data, blockParams) { + if (fn.decorator) { + let props = {}; + prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths); + Utils.extend(prog, props); + } + return prog; +} diff --git a/spec/runtime.js b/spec/runtime.js index 502a8436b..a4830ad0c 100644 --- a/spec/runtime.js +++ b/spec/runtime.js @@ -14,19 +14,19 @@ describe('runtime', function() { it('should throw on version mismatch', function() { shouldThrow(function() { Handlebars.template({ - main: true, + main: {}, compiler: [Handlebars.COMPILER_REVISION + 1] }); }, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/); shouldThrow(function() { Handlebars.template({ - main: true, + main: {}, compiler: [Handlebars.COMPILER_REVISION - 1] }); }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); shouldThrow(function() { Handlebars.template({ - main: true + main: {} }); }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); }); From 9d4353c35cc93cbc44125efcb2c4d348cb51cf06 Mon Sep 17 00:00:00 2001 From: John Steidley Date: Mon, 24 Aug 2015 17:06:40 -0700 Subject: [PATCH 74/82] Bump uglify version to fix vulnerability --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8548cd867..8cea0263c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "source-map": "^0.1.40" }, "optionalDependencies": { - "uglify-js": "~2.3" + "uglify-js": "~2.4" }, "devDependencies": { "async": "^0.9.0", From 7c896a074dad733dd60cd789b8f1cec3025d7a4a Mon Sep 17 00:00:00 2001 From: Dennis Kuczynski Date: Sun, 30 Aug 2015 10:43:24 -0400 Subject: [PATCH 75/82] Fix #each when last object entry has empty key --- lib/handlebars/helpers/each.js | 2 +- spec/builtins.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/helpers/each.js b/lib/handlebars/helpers/each.js index d39a30098..9b1629b40 100644 --- a/lib/handlebars/helpers/each.js +++ b/lib/handlebars/helpers/each.js @@ -68,7 +68,7 @@ export default function(instance) { i++; } } - if (priorKey) { + if (priorKey !== undefined) { execIteration(priorKey, i - 1, true); } } diff --git a/spec/builtins.js b/spec/builtins.js index a7f6204f2..f06a1ad23 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -225,6 +225,16 @@ describe('builtin helpers', function() { 'each with array function argument ignores the contents when empty'); }); + it('each object when last key is an empty string', function() { + var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'; + var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, '': {text: 'GOODBYE'}}, world: 'world'}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'Empty string key is not skipped'); + }); + it('data passed to helpers', function() { var string = '{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}'; var hash = {letters: ['a', 'b', 'c']}; From b63d74a7b3be48e388d47752676e26136ad7ff10 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 1 Sep 2015 01:05:00 -0500 Subject: [PATCH 76/82] Add explicitPartialContext compiler flag Fixes #1032 --- lib/handlebars/compiler/compiler.js | 6 +++++- spec/partials.js | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 64af5daeb..987d0d459 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -177,7 +177,11 @@ Compiler.prototype = { 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}); + if (this.options.explicitPartialContext) { + this.opcode('pushLiteral', 'undefined'); + } else { + params.push({type: 'PathExpression', parts: [], depth: 0}); + } } let partialName = partial.name.original, diff --git a/spec/partials.js b/spec/partials.js index f3283ba58..cc2c266e7 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -41,6 +41,21 @@ describe('partials', function() { 'Partials can be passed a context'); }); + it('partials with no context', function() { + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + shouldCompileToWithPartials( + 'Dudes: {{#dudes}}{{>dude}}{{/dudes}}', + [hash, {}, {dude: partial}, {explicitPartialContext: true}], + true, + 'Dudes: () () '); + shouldCompileToWithPartials( + 'Dudes: {{#dudes}}{{>dude name="foo"}}{{/dudes}}', + [hash, {}, {dude: partial}, {explicitPartialContext: true}], + true, + 'Dudes: foo () foo () '); + }); + it('partials with string context', function() { var string = 'Dudes: {{>dude "dudes"}}'; var partial = '{{.}}'; From c13c7df73d5d15580e8c3c7bc32ee41a3908540d Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 1 Sep 2015 01:29:25 -0500 Subject: [PATCH 77/82] Add basic decorators docs Fixes #1088 --- docs/decorators-api.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/decorators-api.md diff --git a/docs/decorators-api.md b/docs/decorators-api.md new file mode 100644 index 000000000..e14a33ff5 --- /dev/null +++ b/docs/decorators-api.md @@ -0,0 +1,19 @@ +# Decorators + +Decorators allow for blocks to be annotated with metadata or wrapped in functionality prior to execution of the block. This may be used to communicate with the containing helper or to setup a particular state in the system prior to running the block. + +Decorators are registered through similar methods as helpers, `registerDecorators` and `unregisterDecorators`. These can then be referenced via the friendly name in the template using the `{{* decorator}}` and `{{#* decorator}}{/decorator}}` syntaxes. These syntaxs are derivitives of the normal mustache syntax and as such have all of the same argument and whitespace behaviors. + +Decorators are executed when the block program is instantiated and are passed `(program, props, container, context, data, blockParams, depths)` + +- `program`: The block to wrap +- `props`: Object used to set metadata on the final function. Any values set on this object will be set on the function, regardless of if the original function is replaced or not. Metadata should be applied using this object as values applied to `program` may be masked by subsequent decorators that may wrap `program`. +- `container`: The current runtime container +- `context`: The current context. Since the decorator is run before the block that contains it, this is the parent context. +- `data`: The current `@data` values +- `blockParams`: The current block parameters stack +- `depths`: The current context stack + +Decorators may set values on `props` or return a modified function that wraps `program` in particular behaviors. If the decorator returns nothing, then `program` is left unaltered. + +The [inline partial](https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/decorators/inline.js) implementation provides an example of decorators being used for both metadata and wrapping behaviors. From b0d217e13df11cb8cc3e23b242d6e8e20b9c1f30 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 1 Sep 2015 01:30:13 -0500 Subject: [PATCH 78/82] Rev runtime compiler revision --- lib/handlebars/base.js | 5 +++-- spec/expected/empty.amd.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index e59f5e782..7bd248e68 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -5,7 +5,7 @@ import {registerDefaultDecorators} from './decorators'; import logger from './logger'; export const VERSION = '3.0.1'; -export const COMPILER_REVISION = 6; +export const COMPILER_REVISION = 7; export const REVISION_CHANGES = { 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it @@ -13,7 +13,8 @@ export const REVISION_CHANGES = { 3: '== 1.0.0-rc.4', 4: '== 1.x.x', 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' + 6: '>= 2.0.0-beta.1', + 7: '>= 4.0.0' }; const objectType = '[object Object]'; diff --git a/spec/expected/empty.amd.js b/spec/expected/empty.amd.js index 0b39884f5..1cf1eabc9 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(container,depth0,helpers,partials,data) { +return templates['empty'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { return ""; },"useData":true}); }); From 83b8e846a3569bd366cf0b6bdc1e4604d1a2077e Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 1 Sep 2015 01:44:35 -0500 Subject: [PATCH 79/82] Escape = in HTML content There was a potential XSS exploit when using unquoted attributes that this should help reduce. Fixes #1083 --- lib/handlebars/utils.js | 7 ++++--- spec/utils.js | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index 81050f999..d34646b7d 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -4,11 +4,12 @@ const escape = { '>': '>', '"': '"', "'": ''', - '`': '`' + '`': '`', + '=': '=' }; -const badChars = /[&<>"'`]/g, - possible = /[&<>"'`]/; +const badChars = /[&<>"'`=]/g, + possible = /[&<>"'`=]/; function escapeChar(chr) { return escape[chr]; diff --git a/spec/utils.js b/spec/utils.js index 81732c5e7..7248ac447 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -18,6 +18,7 @@ describe('utils', function() { describe('#escapeExpression', function() { it('shouhld escape html', function() { equals(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo<&"'>'); + equals(Handlebars.Utils.escapeExpression('foo='), 'foo='); }); it('should not escape SafeString', function() { var string = new Handlebars.SafeString('foo<&"\'>'); From 6c07367cd6b424eabdc5316dcda0b630a13e26de Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 1 Sep 2015 08:15:48 -0500 Subject: [PATCH 80/82] Update release notes --- release-notes.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index 21a5346e9..2f0214025 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,7 +2,50 @@ ## Development -[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...master) +[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.0...master) + +## v4.0.0 - September 1st, 2015 +- [#1082](https://github.com/wycats/handlebars.js/pull/1082) - Decorators and Inline Partials ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#1076](https://github.com/wycats/handlebars.js/pull/1076) - Implement partial blocks ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#1087](https://github.com/wycats/handlebars.js/pull/1087) - Fix #each when last object entry has empty key ([@denniskuczynski](https://api.github.com/users/denniskuczynski)) +- [#1084](https://github.com/wycats/handlebars.js/pull/1084) - Bump uglify version to fix vulnerability ([@John-Steidley](https://api.github.com/users/John-Steidley)) +- [#1068](https://github.com/wycats/handlebars.js/pull/1068) - Fix typo ([@0xack13](https://api.github.com/users/0xack13)) +- [#1060](https://github.com/wycats/handlebars.js/pull/1060) - #1056 Fixed grammar for nested raw blocks ([@ericbn](https://api.github.com/users/ericbn)) +- [#1052](https://github.com/wycats/handlebars.js/pull/1052) - Updated year in License ([@maqnouch](https://api.github.com/users/maqnouch)) +- [#1037](https://github.com/wycats/handlebars.js/pull/1037) - Fix minor typos in README ([@tomxtobin](https://api.github.com/users/tomxtobin)) +- [#1032](https://github.com/wycats/handlebars.js/issues/1032) - Is it possible to render a partial without the parent scope? ([@aputinski](https://api.github.com/users/aputinski)) +- [#1019](https://github.com/wycats/handlebars.js/pull/1019) - Fixes typo in tests ([@aymerick](https://api.github.com/users/aymerick)) +- [#1016](https://github.com/wycats/handlebars.js/issues/1016) - Version mis-match ([@mayankdedhia](https://api.github.com/users/mayankdedhia)) +- [#1023](https://github.com/wycats/handlebars.js/issues/1023) - is it possible for nested custom helpers to communicate between each other? +- [#893](https://github.com/wycats/handlebars.js/issues/893) - [Proposal] Section blocks. +- [#792](https://github.com/wycats/handlebars.js/issues/792) - feature request: inline partial definitions +- [#583](https://github.com/wycats/handlebars.js/issues/583) - Parent path continues to drill down depth with multiple conditionals +- [#404](https://github.com/wycats/handlebars.js/issues/404) - Add named child helpers that can be referenced by block helpers +- Escape = in HTML content - [83b8e84](https://github.com/wycats/handlebars.js/commit/83b8e84) +- Drop AST constructors in favor of JSON - [95d84ba](https://github.com/wycats/handlebars.js/commit/95d84ba) +- Pass container rather than exec as context - [9a2d1d6](https://github.com/wycats/handlebars.js/commit/9a2d1d6) +- Add ignoreStandalone compiler option - [ea3a5a1](https://github.com/wycats/handlebars.js/commit/ea3a5a1) +- Ignore empty when iterating on sparse arrays - [06d515a](https://github.com/wycats/handlebars.js/commit/06d515a) +- Add support for string and stdin precompilation - [0de8dac](https://github.com/wycats/handlebars.js/commit/0de8dac) +- Simplify object assignment generation logic - [77e6bfc](https://github.com/wycats/handlebars.js/commit/77e6bfc) +- Bulletproof AST.helpers.helperExpression - [93b0760](https://github.com/wycats/handlebars.js/commit/93b0760) +- Always return string responses - [8e868ab](https://github.com/wycats/handlebars.js/commit/8e868ab) +- Pass undefined fields to helpers in strict mode - [5d4b8da](https://github.com/wycats/handlebars.js/commit/5d4b8da) +- Avoid depth creation when context remains the same - [279e038](https://github.com/wycats/handlebars.js/commit/279e038) +- Improve logging API - [9a49d35](https://github.com/wycats/handlebars.js/commit/9a49d35) +- Fix with operator in no @data mode - [231a8d7](https://github.com/wycats/handlebars.js/commit/231a8d7) +- Allow empty key name in each iteration - [1bb640b](https://github.com/wycats/handlebars.js/commit/1bb640b) +- Add with block parameter support - [2a85106](https://github.com/wycats/handlebars.js/commit/2a85106) +- Fix escaping of non-javascript identifiers - [410141c](https://github.com/wycats/handlebars.js/commit/410141c) +- Fix location information for programs - [93faffa](https://github.com/wycats/handlebars.js/commit/93faffa) + +Compatibility notes: +- Depthed paths are now conditional pushed on to the stack. If the helper uses the same context, then a new stack is not created. This leads to behavior the better matches expectations for helpers like `if` that do not seem to alter the context. Any instances of `../` in templates will need to be checked for the correct behavior under 4.0.0. In general templates will either reduce the number of `../` instances or leave them as is. See [#1028](https://github.com/wycats/handlebars.js/issues/1028). +- The `=` character is now HTML escaped. This closes a potential exploit case when using unquoted attributes, i.e. `
`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces. +- AST constructors have been dropped in favor of plain old javascript objects +- The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0. + +[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...v4.0.0) ## v3.0.3 - April 28th, 2015 - [#1004](https://github.com/wycats/handlebars.js/issues/1004) - Latest version breaks with RequireJS (global is undefined) ([@boskee](https://api.github.com/users/boskee)) From f97b99e0cd1fa6afb7721c2ce1838e17aff0d57a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 1 Sep 2015 08:18:41 -0500 Subject: [PATCH 81/82] Update release notes --- release-notes.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/release-notes.md b/release-notes.md index 2f0214025..dba8a55ec 100644 --- a/release-notes.md +++ b/release-notes.md @@ -45,6 +45,49 @@ Compatibility notes: - AST constructors have been dropped in favor of plain old javascript objects - The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0. +[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.0...v4.0.0) + +## v4.0.0 - September 1st, 2015 +- [#1082](https://github.com/wycats/handlebars.js/pull/1082) - Decorators and Inline Partials ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#1076](https://github.com/wycats/handlebars.js/pull/1076) - Implement partial blocks ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#1087](https://github.com/wycats/handlebars.js/pull/1087) - Fix #each when last object entry has empty key ([@denniskuczynski](https://api.github.com/users/denniskuczynski)) +- [#1084](https://github.com/wycats/handlebars.js/pull/1084) - Bump uglify version to fix vulnerability ([@John-Steidley](https://api.github.com/users/John-Steidley)) +- [#1068](https://github.com/wycats/handlebars.js/pull/1068) - Fix typo ([@0xack13](https://api.github.com/users/0xack13)) +- [#1060](https://github.com/wycats/handlebars.js/pull/1060) - #1056 Fixed grammar for nested raw blocks ([@ericbn](https://api.github.com/users/ericbn)) +- [#1052](https://github.com/wycats/handlebars.js/pull/1052) - Updated year in License ([@maqnouch](https://api.github.com/users/maqnouch)) +- [#1037](https://github.com/wycats/handlebars.js/pull/1037) - Fix minor typos in README ([@tomxtobin](https://api.github.com/users/tomxtobin)) +- [#1032](https://github.com/wycats/handlebars.js/issues/1032) - Is it possible to render a partial without the parent scope? ([@aputinski](https://api.github.com/users/aputinski)) +- [#1019](https://github.com/wycats/handlebars.js/pull/1019) - Fixes typo in tests ([@aymerick](https://api.github.com/users/aymerick)) +- [#1016](https://github.com/wycats/handlebars.js/issues/1016) - Version mis-match ([@mayankdedhia](https://api.github.com/users/mayankdedhia)) +- [#1023](https://github.com/wycats/handlebars.js/issues/1023) - is it possible for nested custom helpers to communicate between each other? +- [#893](https://github.com/wycats/handlebars.js/issues/893) - [Proposal] Section blocks. +- [#792](https://github.com/wycats/handlebars.js/issues/792) - feature request: inline partial definitions +- [#583](https://github.com/wycats/handlebars.js/issues/583) - Parent path continues to drill down depth with multiple conditionals +- [#404](https://github.com/wycats/handlebars.js/issues/404) - Add named child helpers that can be referenced by block helpers +- Escape = in HTML content - [83b8e84](https://github.com/wycats/handlebars.js/commit/83b8e84) +- Drop AST constructors in favor of JSON - [95d84ba](https://github.com/wycats/handlebars.js/commit/95d84ba) +- Pass container rather than exec as context - [9a2d1d6](https://github.com/wycats/handlebars.js/commit/9a2d1d6) +- Add ignoreStandalone compiler option - [ea3a5a1](https://github.com/wycats/handlebars.js/commit/ea3a5a1) +- Ignore empty when iterating on sparse arrays - [06d515a](https://github.com/wycats/handlebars.js/commit/06d515a) +- Add support for string and stdin precompilation - [0de8dac](https://github.com/wycats/handlebars.js/commit/0de8dac) +- Simplify object assignment generation logic - [77e6bfc](https://github.com/wycats/handlebars.js/commit/77e6bfc) +- Bulletproof AST.helpers.helperExpression - [93b0760](https://github.com/wycats/handlebars.js/commit/93b0760) +- Always return string responses - [8e868ab](https://github.com/wycats/handlebars.js/commit/8e868ab) +- Pass undefined fields to helpers in strict mode - [5d4b8da](https://github.com/wycats/handlebars.js/commit/5d4b8da) +- Avoid depth creation when context remains the same - [279e038](https://github.com/wycats/handlebars.js/commit/279e038) +- Improve logging API - [9a49d35](https://github.com/wycats/handlebars.js/commit/9a49d35) +- Fix with operator in no @data mode - [231a8d7](https://github.com/wycats/handlebars.js/commit/231a8d7) +- Allow empty key name in each iteration - [1bb640b](https://github.com/wycats/handlebars.js/commit/1bb640b) +- Add with block parameter support - [2a85106](https://github.com/wycats/handlebars.js/commit/2a85106) +- Fix escaping of non-javascript identifiers - [410141c](https://github.com/wycats/handlebars.js/commit/410141c) +- Fix location information for programs - [93faffa](https://github.com/wycats/handlebars.js/commit/93faffa) + +Compatibility notes: +- Depthed paths are now conditional pushed on to the stack. If the helper uses the same context, then a new stack is not created. This leads to behavior the better matches expectations for helpers like `if` that do not seem to alter the context. Any instances of `../` in templates will need to be checked for the correct behavior under 4.0.0. In general templates will either reduce the number of `../` instances or leave them as is. See [#1028](https://github.com/wycats/handlebars.js/issues/1028). +- The `=` character is now HTML escaped. This closes a potential exploit case when using unquoted attributes, i.e. `
`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces. +- AST constructors have been dropped in favor of plain old javascript objects +- The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0. + [Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...v4.0.0) ## v3.0.3 - April 28th, 2015 From bff5fab8f9d42e21950be00dcf1cedf4dc1a565b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 1 Sep 2015 08:19:14 -0500 Subject: [PATCH 82/82] v4.0.0 --- components/bower.json | 2 +- components/handlebars.js.nuspec | 2 +- lib/handlebars/base.js | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/bower.json b/components/bower.json index 9b3f8a978..fce893b41 100644 --- a/components/bower.json +++ b/components/bower.json @@ -1,6 +1,6 @@ { "name": "handlebars", - "version": "3.0.3", + "version": "4.0.0", "main": "handlebars.js", "license": "MIT", "dependencies": {} diff --git a/components/handlebars.js.nuspec b/components/handlebars.js.nuspec index ae298147a..b259400bd 100644 --- a/components/handlebars.js.nuspec +++ b/components/handlebars.js.nuspec @@ -2,7 +2,7 @@ handlebars.js - 3.0.3 + 4.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 7bd248e68..e68031ee6 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -4,7 +4,7 @@ import {registerDefaultHelpers} from './helpers'; import {registerDefaultDecorators} from './decorators'; import logger from './logger'; -export const VERSION = '3.0.1'; +export const VERSION = '4.0.0'; export const COMPILER_REVISION = 7; export const REVISION_CHANGES = { diff --git a/package.json b/package.json index 8cea0263c..814e5277e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "handlebars", "barename": "handlebars", - "version": "3.0.3", + "version": "4.0.0", "description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration", "homepage": "http://www.handlebarsjs.com/", "keywords": [