Skip to content

Commit

Permalink
Implement partial blocks
Browse files Browse the repository at this point in the history
This allows for failover for missing partials as well as limited templating ability through the `{{> @partial-block }}` partial special case.

Partial fix for #1018
  • Loading branch information
kpdecker committed Aug 15, 2015
1 parent b85cad8 commit c62da22
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 4 deletions.
14 changes: 13 additions & 1 deletion docs/compiler-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
14 changes: 14 additions & 0 deletions lib/handlebars/compiler/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ let AST = {
this.strip = strip;
},

PartialBlockStatement: function(name, params, hash, program, openStrip, closeStrip, locInfo) {
this.loc = locInfo;
this.type = 'PartialBlockStatement';

this.name = name;
this.params = params || [];
this.hash = hash;
this.program = program;

this.indent = '';
this.openStrip = openStrip;
this.closeStrip = closeStrip;
},

ContentStatement: function(string, locInfo) {
this.loc = locInfo;
this.type = 'ContentStatement';
Expand Down
14 changes: 12 additions & 2 deletions lib/handlebars/compiler/compiler.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable new-cap */

import Exception from '../exception';
import {isArray, indexOf} from '../utils';
import AST from './ast';
Expand Down Expand Up @@ -152,6 +154,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);
Expand All @@ -165,7 +172,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) {
Expand All @@ -176,9 +183,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');
Expand Down
14 changes: 14 additions & 0 deletions lib/handlebars/compiler/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,17 @@ 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 new this.PartialBlockStatement(
openPartialBlock.name, openPartialBlock.params, openPartialBlock.hash,
program,
openPartialBlock.strip, close && close.strip,
this.locInfo(locInfo));
}

16 changes: 16 additions & 0 deletions lib/handlebars/compiler/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "' ]");
Expand Down
7 changes: 7 additions & 0 deletions lib/handlebars/compiler/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ Visitor.prototype = {
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
},
PartialBlockStatement: function(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');

this.acceptKey(partial, 'program');
},

ContentStatement: function(/* content */) {},
CommentStatement: function(/* comment */) {},
Expand Down
15 changes: 14 additions & 1 deletion lib/handlebars/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions spec/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ describe('ast', function() {
testLocationInfoStorage(pn);
});
});
describe('PartialBlockStatement', function() {
it('provides default params', function() {
var pn = new AST.PartialBlockStatement('so_partial', undefined, {}, [], {}, {}, LOCATION_INFO);
equals(pn.params.length, 0);
});
it('stores location info', function() {
var pn = new AST.PartialBlockStatement('so_partial', [], {}, [], {}, {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});

describe('Program', function() {
it('storing location info', function() {
Expand Down
12 changes: 12 additions & 0 deletions spec/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
61 changes: 61 additions & 0 deletions spec/partials.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions spec/tokenizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down
1 change: 1 addition & 0 deletions src/handlebars.l
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 7 additions & 0 deletions src/handlebars.yy
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ statement
| block -> $1
| rawBlock -> $1
| partial -> $1
| partialBlock -> $1
| content -> $1
| COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$))
;
Expand Down Expand Up @@ -79,6 +80,12 @@ mustache
partial
: OPEN_PARTIAL partialName param* hash? CLOSE -> new yy.PartialStatement($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$))
;
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
Expand Down

0 comments on commit c62da22

Please sign in to comment.