Skip to content

Commit

Permalink
Merge pull request #941 from wycats/dynamic-partial
Browse files Browse the repository at this point in the history
Add support for dynamic partial names
  • Loading branch information
kpdecker committed Jan 19, 2015
2 parents b0b522b + 884bf15 commit 9f8daf9
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 88 deletions.
29 changes: 20 additions & 9 deletions docs/compiler-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,21 @@ interface Statement <: Node { }

interface MustacheStatement <: Statement {
type: "MustacheStatement";
sexpr: SubExpression;
escaped: boolean;

path: PathExpression;
params: [ Expression ];
hash: Hash;

escaped: boolean;
strip: StripFlags | null;
}

interface BlockStatement <: Statement {
type: "BlockStatement";
sexpr: SubExpression;
path: PathExpression;
params: [ Expression ];
hash: Hash;

program: Program | null;
inverse: Program | null;

Expand All @@ -74,12 +80,19 @@ interface BlockStatement <: Statement {

interface PartialStatement <: Statement {
type: "PartialStatement";
sexpr: SubExpression;
name: PathExpression | SubExpression;
params: [ Expression ];
hash: Hash;

indent: string;
strip: 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.


```java
interface ContentStatement <: Statement {
type: "ContentStatement";
value: string;
Expand Down Expand Up @@ -108,13 +121,9 @@ interface SubExpression <: Expression {
path: PathExpression;
params: [ Expression ];
hash: Hash;

isHelper: true | null;
}
```

`isHelper` is not required and is used to disambiguate between cases such as `{{foo}}` and `(foo)`, which have slightly different call behaviors.

##### Paths

```java
Expand Down Expand Up @@ -195,7 +204,7 @@ function ImportScanner() {
ImportScanner.prototype = new Visitor();

ImportScanner.prototype.PartialStatement = function(partial) {
this.partials.push({request: partial.sexpr.original});
this.partials.push({request: partial.name.original});

Visitor.prototype.PartialStatement.call(this, partial);
};
Expand All @@ -221,6 +230,8 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c
- `name` is the current path component
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`.

Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases.

- `depthedLookup(name)`
Used to generate code that resolves parameters within any context in the stack. Is only used in `compat` mode.

Expand Down
27 changes: 17 additions & 10 deletions lib/handlebars/compiler/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,25 @@ var AST = {
this.strip = strip;
},

MustacheStatement: function(sexpr, escaped, strip, locInfo) {
MustacheStatement: function(path, params, hash, escaped, strip, locInfo) {
this.loc = locInfo;
this.type = 'MustacheStatement';

this.sexpr = sexpr;
this.path = path;
this.params = params || [];
this.hash = hash;
this.escaped = escaped;

this.strip = strip;
},

BlockStatement: function(sexpr, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) {
BlockStatement: function(path, params, hash, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) {
this.loc = locInfo;

this.type = 'BlockStatement';
this.sexpr = sexpr;

this.path = path;
this.params = params || [];
this.hash = hash;
this.program = program;
this.inverse = inverse;

Expand All @@ -31,12 +35,15 @@ var AST = {
this.closeStrip = closeStrip;
},

PartialStatement: function(sexpr, strip, locInfo) {
PartialStatement: function(name, params, hash, strip, locInfo) {
this.loc = locInfo;
this.type = 'PartialStatement';
this.sexpr = sexpr;
this.indent = '';

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

this.indent = '';
this.strip = strip;
},

Expand Down Expand Up @@ -112,8 +119,8 @@ var AST = {
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
// TODO: Make these public utility methods
helperExpression: function(sexpr) {
return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash);
helperExpression: function(node) {
return !!(node.type === 'SubExpression' || node.params.length || node.hash);
},

scopedId: function(path) {
Expand Down
34 changes: 17 additions & 17 deletions lib/handlebars/compiler/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,28 +110,27 @@ Compiler.prototype = {
},

BlockStatement: function(block) {
var sexpr = block.sexpr,
program = block.program,
var program = block.program,
inverse = block.inverse;

program = program && this.compileProgram(program);
inverse = inverse && this.compileProgram(inverse);

var type = this.classifySexpr(sexpr);
var type = this.classifySexpr(block);

if (type === 'helper') {
this.helperSexpr(sexpr, program, inverse);
this.helperSexpr(block, program, inverse);
} else if (type === 'simple') {
this.simpleSexpr(sexpr);
this.simpleSexpr(block);

// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('emptyHash');
this.opcode('blockValue', sexpr.path.original);
this.opcode('blockValue', block.path.original);
} else {
this.ambiguousSexpr(sexpr, program, inverse);
this.ambiguousSexpr(block, program, inverse);

// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
Expand All @@ -145,29 +144,35 @@ Compiler.prototype = {
},

PartialStatement: function(partial) {
var partialName = partial.sexpr.path.original;
this.usePartial = true;

var params = partial.sexpr.params;
var params = partial.params;
if (params.length > 1) {
throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
} else if (!params.length) {
params.push({type: 'PathExpression', parts: [], depth: 0});
}

this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true);
var partialName = partial.name.original,
isDynamic = partial.name.type === 'SubExpression';
if (isDynamic) {
this.accept(partial.name);
}

this.setupFullMustacheParams(partial, undefined, undefined, true);

var indent = partial.indent || '';
if (this.options.preventIndent && indent) {
this.opcode('appendContent', indent);
indent = '';
}
this.opcode('invokePartial', partialName, indent);

this.opcode('invokePartial', isDynamic, partialName, indent);
this.opcode('append');
},

MustacheStatement: function(mustache) {
this.accept(mustache.sexpr);
this.SubExpression(mustache);

if(mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped');
Expand Down Expand Up @@ -334,11 +339,6 @@ Compiler.prototype = {
pushParam: function(val) {
var value = val.value != null ? val.value : val.original || '';

// Force helper evaluation
if (val.type === 'SubExpression') {
val.isHelper = true;
}

if (this.stringParams) {
if (value.replace) {
value = value
Expand Down
22 changes: 12 additions & 10 deletions lib/handlebars/compiler/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,39 +52,40 @@ export function preparePath(data, parts, locInfo) {
return new this.PathExpression(data, depth, dig, original, locInfo);
}

export function prepareMustache(sexpr, open, strip, locInfo) {
export function prepareMustache(path, params, hash, open, strip, locInfo) {
/*jshint -W040 */
// Must use charAt to support IE pre-10
var escapeFlag = open.charAt(3) || open.charAt(2),
escaped = escapeFlag !== '{' && escapeFlag !== '&';

return new this.MustacheStatement(sexpr, escaped, strip, this.locInfo(locInfo));
return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo));
}

export function prepareRawBlock(openRawBlock, content, close, locInfo) {
/*jshint -W040 */
if (openRawBlock.sexpr.path.original !== close) {
var errorNode = {loc: openRawBlock.sexpr.loc};
if (openRawBlock.path.original !== close) {
var errorNode = {loc: openRawBlock.path.loc};

throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode);
throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode);
}

locInfo = this.locInfo(locInfo);
var program = new this.Program([content], null, {}, locInfo);

return new this.BlockStatement(
openRawBlock.sexpr, program, undefined,
openRawBlock.path, openRawBlock.params, openRawBlock.hash,
program, undefined,
{}, {}, {},
locInfo);
}

export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) {
/*jshint -W040 */
// When we are chaining inverse calls, we will not have a close path
if (close && close.path && openBlock.sexpr.path.original !== close.path.original) {
var errorNode = {loc: openBlock.sexpr.loc};
if (close && close.path && openBlock.path.original !== close.path.original) {
var errorNode = {loc: openBlock.path.loc};

throw new Exception(openBlock.sexpr.path.original + ' doesn\'t match ' + close.path.original, errorNode);
throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode);
}

program.blockParams = openBlock.blockParams;
Expand All @@ -108,7 +109,8 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver
}

return new this.BlockStatement(
openBlock.sexpr, program, inverse,
openBlock.path, openBlock.params, openBlock.hash,
program, inverse,
openBlock.strip, inverseStrip, close && close.strip,
this.locInfo(locInfo));
}
13 changes: 11 additions & 2 deletions lib/handlebars/compiler/javascript-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -644,17 +644,26 @@ JavaScriptCompiler.prototype = {
//
// This operation pops off a context, invokes a partial with that context,
// and pushes the result of the invocation back.
invokePartial: function(name, indent) {
invokePartial: function(isDynamic, name, indent) {
var params = [],
options = this.setupParams(name, 1, params, false);

if (isDynamic) {
name = this.popStack();
delete options.name;
}

if (indent) {
options.indent = JSON.stringify(indent);
}
options.helpers = 'helpers';
options.partials = 'partials';

params.unshift(this.nameLookup('partials', name, 'partial'));
if (!isDynamic) {
params.unshift(this.nameLookup('partials', name, 'partial'));
} else {
params.unshift(name);
}

if (this.options.compat) {
options.depths = 'depths';
Expand Down
15 changes: 7 additions & 8 deletions lib/handlebars/compiler/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ PrintVisitor.prototype.Program = function(program) {
};

PrintVisitor.prototype.MustacheStatement = function(mustache) {
return this.pad('{{ ' + this.accept(mustache.sexpr) + ' }}');
return this.pad('{{ ' + this.SubExpression(mustache) + ' }}');
};

PrintVisitor.prototype.BlockStatement = function(block) {
var out = "";

out = out + this.pad('BLOCK:');
this.padding++;
out = out + this.pad(this.accept(block.sexpr));
out = out + this.pad(this.SubExpression(block));
if (block.program) {
out = out + this.pad('PROGRAM:');
this.padding++;
Expand All @@ -74,13 +74,12 @@ PrintVisitor.prototype.BlockStatement = function(block) {
};

PrintVisitor.prototype.PartialStatement = function(partial) {
var sexpr = partial.sexpr,
content = 'PARTIAL:' + sexpr.path.original;
if(sexpr.params[0]) {
content += ' ' + this.accept(sexpr.params[0]);
var content = 'PARTIAL:' + partial.name.original;
if(partial.params[0]) {
content += ' ' + this.accept(partial.params[0]);
}
if (sexpr.hash) {
content += ' ' + this.accept(sexpr.hash);
if (partial.hash) {
content += ' ' + this.accept(partial.hash);
}
return this.pad('{{> ' + content + ' }}');
};
Expand Down
18 changes: 15 additions & 3 deletions lib/handlebars/compiler/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,24 @@ Visitor.prototype = {
},

MustacheStatement: function(mustache) {
this.acceptRequired(mustache, 'sexpr');
this.acceptRequired(mustache, 'path');
this.acceptArray(mustache.params);
this.acceptKey(mustache, 'hash');
},

BlockStatement: function(block) {
this.acceptRequired(block, 'sexpr');
this.acceptRequired(block, 'path');
this.acceptArray(block.params);
this.acceptKey(block, 'hash');

this.acceptKey(block, 'program');
this.acceptKey(block, 'inverse');
},

PartialStatement: function(partial) {
this.acceptRequired(partial, 'sexpr');
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
},

ContentStatement: function(/* content */) {},
Expand All @@ -92,6 +99,11 @@ Visitor.prototype = {
this.acceptArray(sexpr.params);
this.acceptKey(sexpr, 'hash');
},
PartialExpression: function(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
},

PathExpression: function(/* path */) {},

Expand Down
Loading

0 comments on commit 9f8daf9

Please sign in to comment.