diff options
-rw-r--r-- | lib/handlebars/compiler/helpers.js | 33 | ||||
-rw-r--r-- | spec/blocks.js | 20 | ||||
-rw-r--r-- | spec/parser.js | 10 | ||||
-rw-r--r-- | src/handlebars.l | 2 | ||||
-rw-r--r-- | src/handlebars.yy | 18 |
5 files changed, 72 insertions, 11 deletions
diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 5aa0f2c..d236f7f 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -15,11 +15,29 @@ export function stripComment(comment) { export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { /*jshint -W040 */ - if (mustache.sexpr.id.original !== close.path.original) { + // When we are chaining inverse calls, we will not have a close path + if (close && close.path && (mustache.sexpr.id.original !== close.path.original)) { throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache); } - var inverse = inverseAndProgram && inverseAndProgram.program; + // Safely handle a chained inverse that does not have a non-conditional inverse + // (i.e. both inverseAndProgram AND close are undefined) + if (!close) { + close = {strip: {}}; + } + + // Find the inverse program that is involed with whitespace stripping. + var inverse = inverseAndProgram && inverseAndProgram.program, + firstInverse = inverse, + lastInverse = inverse; + if (inverse && inverse.inverse) { + firstInverse = inverse.statements[0].program; + + // Walk the inverse chain to find the last inverse that is actually in the chain. + while (lastInverse.inverse) { + lastInverse = lastInverse.statements[lastInverse.statements.length-1].program; + } + } var strip = { left: mustache.strip.left, @@ -28,7 +46,7 @@ export function prepareBlock(mustache, program, inverseAndProgram, close, invert // Determine the standalone candiacy. Basically flag our content as being possibly standalone // so our parent can determine if we actually are standalone openStandalone: isNextWhitespace(program.statements), - closeStandalone: isPrevWhitespace((inverse || program).statements) + closeStandalone: isPrevWhitespace((firstInverse || program).statements) }; if (mustache.strip.right) { @@ -41,19 +59,20 @@ export function prepareBlock(mustache, program, inverseAndProgram, close, invert if (inverseStrip.left) { omitLeft(program.statements, null, true); } + if (inverseStrip.right) { - omitRight(inverse.statements, null, true); + omitRight(firstInverse.statements, null, true); } if (close.strip.left) { - omitLeft(inverse.statements, null, true); + omitLeft(lastInverse.statements, null, true); } // Find standalone else statments if (isPrevWhitespace(program.statements) - && isNextWhitespace(inverse.statements)) { + && isNextWhitespace(firstInverse.statements)) { omitLeft(program.statements); - omitRight(inverse.statements); + omitRight(firstInverse.statements); } } else { if (close.strip.left) { diff --git a/spec/blocks.js b/spec/blocks.js index a172970..21fb718 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -89,6 +89,20 @@ describe('blocks', function() { shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, "No people"); }); + it("chained inverted sections", function() { + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/people}}", {none: "No people"}, + "No people"); + shouldCompileTo("{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}", {none: "No people"}, + "No people"); + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}", {none: "No people"}, + "No people"); + }); + it("chained inverted sections with mismatch", function() { + shouldThrow(function() { + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/if}}", {none: "No people"}, + "No people"); + }, Error); + }); it("block inverted sections with empty arrays", function() { shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, @@ -105,6 +119,12 @@ describe('blocks', function() { shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, 'No people\n'); }); + it('block standalone chained else sections', function() { + shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, + 'No people\n'); + shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n', {none: 'No people'}, + 'No people\n'); + }); it('should handle nesting', function() { shouldCompileTo('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.', {data: [1, 3, 5]}, '1\n3\n5\nOK.'); }); diff --git a/spec/parser.js b/spec/parser.js index 131160a..3686363 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -116,6 +116,10 @@ describe('parser', function() { equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); + it('parses multiple inverse sections', function() { + equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n {{ ID:if [ID:bar] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"); + }); + it('parses empty blocks', function() { equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n"); }); @@ -147,8 +151,10 @@ describe('parser', function() { it('parses a standalone inverse section', function() { equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); }); - it('parses a standalone inverse section', function() { - equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + it('throws on old inverse section', function() { + shouldThrow(function() { + equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + }, Error); }); it("raises if there's a Parse error", function() { diff --git a/src/handlebars.l b/src/handlebars.l index fbeec34..f3c925c 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -76,7 +76,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} <mu>"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; <mu>"{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; <mu>"{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE'; -<mu>"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE'; +<mu>"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE_CHAIN'; <mu>"{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED'; <mu>"{{"{LEFT_STRIP}?"&" return 'OPEN'; <mu>"{{"{LEFT_STRIP}?"!--" { diff --git a/src/handlebars.yy b/src/handlebars.yy index ccc0d22..3bd9abc 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -30,7 +30,7 @@ openRawBlock ; block - : openBlock program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) + : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) ; @@ -42,10 +42,26 @@ openInverse : OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) ; +openInverseChain + : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + ; + inverseAndProgram : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 } ; +inverseChain + : openInverseChain program inverseChain? { + var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), + program = new yy.ProgramNode(yy.prepareProgram([inverse]), {}, @$); + + program.inverse = inverse; + + $$ = { strip: $1.strip, program: program, chain: true }; + } + | inverseAndProgram -> $1 + ; + closeBlock : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} ; |