Skip to content

Commit

Permalink
Merge pull request #1076 from wycats/partial-block
Browse files Browse the repository at this point in the history
Implement partial blocks
  • Loading branch information
kpdecker committed Aug 24, 2015
2 parents 2571dd8 + 1c27408 commit c727e1f
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 38 deletions.
15 changes: 14 additions & 1 deletion docs/compiler-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,23 @@ 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;
openStrip: StripFlags | null;
closeStrip: StripFlags | null;
}
```

`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial.
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 @@ -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);
Expand All @@ -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) {
Expand All @@ -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');
Expand Down
38 changes: 28 additions & 10 deletions lib/handlebars/compiler/helpers.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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;
Expand Down Expand Up @@ -185,3 +188,18 @@ export function prepareProgram(statements, loc) {
}


export function preparePartialBlock(open, program, close, locInfo) {
validateClose(open, close);

return {
type: 'PartialBlockStatement',
name: open.path,
params: open.params,
hash: open.hash,
program,
openStrip: open.strip,
closeStrip: close && close.strip,
loc: 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
45 changes: 24 additions & 21 deletions lib/handlebars/compiler/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */) {},

Expand All @@ -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;
4 changes: 3 additions & 1 deletion lib/handlebars/compiler/whitespace-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

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
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
3 changes: 1 addition & 2 deletions spec/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -40,8 +41,6 @@ describe('Visitor', function() {
visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) [email protected]}}{{!comment}}{{> bar }} {{/foo.bar}}'));
});

it('should return undefined');

describe('mutating', function() {
describe('fields', function() {
it('should replace value', function() {
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 {
$$ = {
Expand Down Expand Up @@ -101,6 +102,12 @@ partial
};
}
;
partialBlock
: openPartialBlock program closeBlock -> yy.preparePartialBlock($1, $2, $3, @$)
;
openPartialBlock
: OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { path: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) }
;

param
: helperName -> $1
Expand Down

0 comments on commit c727e1f

Please sign in to comment.