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
This commit is contained in:
+13
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 + "' ]");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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']);
|
||||
|
||||
+1
-2
@@ -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() {
|
||||
|
||||
@@ -80,6 +80,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
|
||||
return 'CLOSE_RAW_BLOCK';
|
||||
}
|
||||
<mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL';
|
||||
<mu>"{{"{LEFT_STRIP}?"#>" return 'OPEN_PARTIAL_BLOCK';
|
||||
<mu>"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK';
|
||||
<mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK';
|
||||
<mu>"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user