From 91ffd32cad32b2d1cd310ff94f65b28c428206ac Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 14 Aug 2015 15:18:52 -0500 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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; } ```