@@ -128,7 +128,11 @@ function registerDefaultHelpers(instance) {
|
||||
data.contextPath = contextPath + key;
|
||||
}
|
||||
}
|
||||
ret = ret + fn(context[key], { data: data });
|
||||
|
||||
ret = ret + fn(context[key], {
|
||||
data: data,
|
||||
blockParams: Utils.blockParams([context[key], key], [contextPath + key, null])
|
||||
});
|
||||
}
|
||||
|
||||
if(context && typeof context === 'object') {
|
||||
|
||||
@@ -51,6 +51,8 @@ Compiler.prototype = {
|
||||
this.stringParams = options.stringParams;
|
||||
this.trackIds = options.trackIds;
|
||||
|
||||
options.blockParams = options.blockParams || [];
|
||||
|
||||
// These changes will propagate to the other compiler components
|
||||
var knownHelpers = options.knownHelpers;
|
||||
options.knownHelpers = {
|
||||
@@ -92,14 +94,17 @@ Compiler.prototype = {
|
||||
},
|
||||
|
||||
Program: function(program) {
|
||||
var body = program.body;
|
||||
this.options.blockParams.unshift(program.blockParams);
|
||||
|
||||
var body = program.body;
|
||||
for(var i=0, l=body.length; i<l; i++) {
|
||||
this.accept(body[i]);
|
||||
}
|
||||
|
||||
this.isSimple = l === 1;
|
||||
this.options.blockParams.shift();
|
||||
|
||||
this.isSimple = l === 1;
|
||||
this.blockParams = program.blockParams ? program.blockParams.length : 0;
|
||||
|
||||
return this;
|
||||
},
|
||||
@@ -231,15 +236,20 @@ Compiler.prototype = {
|
||||
this.addDepth(path.depth);
|
||||
this.opcode('getContext', path.depth);
|
||||
|
||||
var name = path.parts[0];
|
||||
if (!name) {
|
||||
var name = path.parts[0],
|
||||
scoped = AST.helpers.scopedId(path),
|
||||
blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
|
||||
|
||||
if (blockParamId) {
|
||||
this.opcode('lookupBlockParam', blockParamId, path.parts);
|
||||
} else if (!name) {
|
||||
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
|
||||
this.opcode('pushContext');
|
||||
} else if (path.data) {
|
||||
this.options.data = true;
|
||||
this.opcode('lookupData', path.depth, path.parts);
|
||||
} else {
|
||||
this.opcode('lookupOnContext', path.parts, path.falsy, AST.helpers.scopedId(path));
|
||||
this.opcode('lookupOnContext', path.parts, path.falsy, scoped);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -283,14 +293,18 @@ Compiler.prototype = {
|
||||
},
|
||||
|
||||
classifySexpr: function(sexpr) {
|
||||
var isSimple = AST.helpers.simpleId(sexpr.path);
|
||||
|
||||
var isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]);
|
||||
|
||||
// a mustache is an eligible helper if:
|
||||
// * its id is simple (a single part, not `this` or `..`)
|
||||
var isHelper = AST.helpers.helperExpression(sexpr);
|
||||
var isHelper = !isBlockParam && AST.helpers.helperExpression(sexpr);
|
||||
|
||||
// if a mustache is an eligible helper but not a definite
|
||||
// helper, it is ambiguous, and will be resolved in a later
|
||||
// pass or at runtime.
|
||||
var isEligible = isHelper || AST.helpers.simpleId(sexpr.path);
|
||||
var isEligible = !isBlockParam && (isHelper || isSimple);
|
||||
|
||||
var options = this.options;
|
||||
|
||||
@@ -345,14 +359,23 @@ Compiler.prototype = {
|
||||
}
|
||||
} else {
|
||||
if (this.trackIds) {
|
||||
value = val.original || value;
|
||||
if (value.replace) {
|
||||
value = value
|
||||
.replace(/^\.\//g, '')
|
||||
.replace(/^\.$/g, '');
|
||||
var blockParamIndex;
|
||||
if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
|
||||
blockParamIndex = this.blockParamIndex(val.parts[0]);
|
||||
}
|
||||
if (blockParamIndex) {
|
||||
var blockParamChild = val.parts.slice(1).join('.');
|
||||
this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild);
|
||||
} else {
|
||||
value = val.original || value;
|
||||
if (value.replace) {
|
||||
value = value
|
||||
.replace(/^\.\//g, '')
|
||||
.replace(/^\.$/g, '');
|
||||
}
|
||||
|
||||
this.opcode('pushId', val.type, value);
|
||||
this.opcode('pushId', val.type, value);
|
||||
}
|
||||
}
|
||||
this.accept(val);
|
||||
}
|
||||
@@ -372,6 +395,16 @@ Compiler.prototype = {
|
||||
}
|
||||
|
||||
return params;
|
||||
},
|
||||
|
||||
blockParamIndex: function(name) {
|
||||
for (var depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
|
||||
var blockParams = this.options.blockParams[depth],
|
||||
param = blockParams && blockParams.indexOf(name);
|
||||
if (blockParams && param >= 0) {
|
||||
return [depth, param];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -429,11 +462,11 @@ export function compile(input, options, env) {
|
||||
}
|
||||
return compiled._setup(options);
|
||||
};
|
||||
ret._child = function(i, data, depths) {
|
||||
ret._child = function(i, data, blockParams, depths) {
|
||||
if (!compiled) {
|
||||
compiled = compileInput();
|
||||
}
|
||||
return compiled._child(i, data, depths);
|
||||
return compiled._child(i, data, blockParams, depths);
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -77,10 +77,12 @@ JavaScriptCompiler.prototype = {
|
||||
this.hashes = [];
|
||||
this.compileStack = [];
|
||||
this.inlineStack = [];
|
||||
this.blockParams = [];
|
||||
|
||||
this.compileChildren(environment, options);
|
||||
|
||||
this.useDepths = this.useDepths || environment.useDepths || this.options.compat;
|
||||
this.useBlockParams = this.useBlockParams || environment.useBlockParams;
|
||||
|
||||
var opcodes = environment.opcodes,
|
||||
opcode,
|
||||
@@ -127,6 +129,9 @@ JavaScriptCompiler.prototype = {
|
||||
if (this.useDepths) {
|
||||
ret.useDepths = true;
|
||||
}
|
||||
if (this.useBlockParams) {
|
||||
ret.useBlockParams = true;
|
||||
}
|
||||
if (this.options.compat) {
|
||||
ret.compat = true;
|
||||
}
|
||||
@@ -186,6 +191,9 @@ JavaScriptCompiler.prototype = {
|
||||
|
||||
var params = ["depth0", "helpers", "partials", "data"];
|
||||
|
||||
if (this.useBlockParams || this.useDepths) {
|
||||
params.push('blockParams');
|
||||
}
|
||||
if (this.useDepths) {
|
||||
params.push('depths');
|
||||
}
|
||||
@@ -385,9 +393,7 @@ JavaScriptCompiler.prototype = {
|
||||
// Looks up the value of `name` on the current context and pushes
|
||||
// it onto the stack.
|
||||
lookupOnContext: function(parts, falsy, scoped) {
|
||||
/*jshint -W083 */
|
||||
var i = 0,
|
||||
len = parts.length;
|
||||
var i = 0;
|
||||
|
||||
if (!scoped && this.options.compat && !this.lastContext) {
|
||||
// The depthed query is expected to handle the undefined logic for the root level that
|
||||
@@ -397,19 +403,21 @@ JavaScriptCompiler.prototype = {
|
||||
this.pushContext();
|
||||
}
|
||||
|
||||
for (; i < len; i++) {
|
||||
this.replaceStack(function(current) {
|
||||
var lookup = this.nameLookup(current, parts[i], 'context');
|
||||
// We want to ensure that zero and false are handled properly if the context (falsy flag)
|
||||
// needs to have the special handling for these values.
|
||||
if (!falsy) {
|
||||
return [' != null ? ', lookup, ' : ', current];
|
||||
} else {
|
||||
// Otherwise we can use generic falsy handling
|
||||
return [' && ', lookup];
|
||||
}
|
||||
});
|
||||
}
|
||||
this.resolvePath('context', parts, i, falsy);
|
||||
},
|
||||
|
||||
// [lookupBlockParam]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: blockParam[name], ...
|
||||
//
|
||||
// Looks up the value of `parts` on the given block param and pushes
|
||||
// it onto the stack.
|
||||
lookupBlockParam: function(blockParamId, parts) {
|
||||
this.useBlockParams = true;
|
||||
|
||||
this.push(['blockParams[', blockParamId[0], '][', blockParamId[1], ']']);
|
||||
this.resolvePath('context', parts, 1);
|
||||
},
|
||||
|
||||
// [lookupData]
|
||||
@@ -426,9 +434,23 @@ JavaScriptCompiler.prototype = {
|
||||
this.pushStackLiteral('this.data(data, ' + depth + ')');
|
||||
}
|
||||
|
||||
for (var i = 0, len = parts.length; i < len; i++) {
|
||||
this.resolvePath('data', parts, 0, true);
|
||||
},
|
||||
|
||||
resolvePath: function(type, parts, i, falsy) {
|
||||
/*jshint -W083 */
|
||||
var len = parts.length;
|
||||
for (; i < len; i++) {
|
||||
this.replaceStack(function(current) {
|
||||
return [' && ', this.nameLookup(current, parts[i], 'data')];
|
||||
var lookup = this.nameLookup(current, parts[i], type);
|
||||
// We want to ensure that zero and false are handled properly if the context (falsy flag)
|
||||
// needs to have the special handling for these values.
|
||||
if (!falsy) {
|
||||
return [' != null ? ', lookup, ' : ', current];
|
||||
} else {
|
||||
// Otherwise we can use generic falsy handling
|
||||
return [' && ', lookup];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -661,8 +683,12 @@ JavaScriptCompiler.prototype = {
|
||||
hash.values[key] = value;
|
||||
},
|
||||
|
||||
pushId: function(type, name) {
|
||||
if (type === 'PathExpression') {
|
||||
pushId: function(type, name, child) {
|
||||
if (type === 'BlockParam') {
|
||||
this.pushStackLiteral(
|
||||
'blockParams[' + name[0] + '].path[' + name[1] + ']'
|
||||
+ (child ? ' + ' + JSON.stringify('.' + child) : ''));
|
||||
} else if (type === 'PathExpression') {
|
||||
this.pushString(name);
|
||||
} else if (type === 'SubExpression') {
|
||||
this.pushStackLiteral('true');
|
||||
@@ -693,11 +719,13 @@ JavaScriptCompiler.prototype = {
|
||||
this.context.environments[index] = child;
|
||||
|
||||
this.useDepths = this.useDepths || compiler.useDepths;
|
||||
this.useBlockParams = this.useBlockParams || compiler.useBlockParams;
|
||||
} else {
|
||||
child.index = index;
|
||||
child.name = 'program' + index;
|
||||
|
||||
this.useDepths = this.useDepths || child.useDepths;
|
||||
this.useBlockParams = this.useBlockParams || child.useBlockParams;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -712,11 +740,12 @@ JavaScriptCompiler.prototype = {
|
||||
|
||||
programExpression: function(guid) {
|
||||
var child = this.environment.children[guid],
|
||||
useDepths = this.useDepths;
|
||||
programParams = [child.index, 'data', child.blockParams];
|
||||
|
||||
var programParams = [child.index, 'data'];
|
||||
|
||||
if (useDepths) {
|
||||
if (this.useBlockParams || this.useDepths) {
|
||||
programParams.push('blockParams');
|
||||
}
|
||||
if (this.useDepths) {
|
||||
programParams.push('depths');
|
||||
}
|
||||
|
||||
@@ -943,7 +972,10 @@ JavaScriptCompiler.prototype = {
|
||||
}
|
||||
|
||||
if (this.options.data) {
|
||||
options.data = "data";
|
||||
options.data = 'data';
|
||||
}
|
||||
if (this.useBlockParams) {
|
||||
options.blockParams = 'blockParams';
|
||||
}
|
||||
return options;
|
||||
},
|
||||
|
||||
@@ -89,11 +89,11 @@ export function template(templateSpec, env) {
|
||||
},
|
||||
|
||||
programs: [],
|
||||
program: function(i, data, depths) {
|
||||
program: function(i, data, declaredBlockParams, blockParams, depths) {
|
||||
var programWrapper = this.programs[i],
|
||||
fn = this.fn(i);
|
||||
if (data || depths) {
|
||||
programWrapper = program(this, i, fn, data, depths);
|
||||
if (data || depths || blockParams || declaredBlockParams) {
|
||||
programWrapper = program(this, i, fn, data, declaredBlockParams, blockParams, depths);
|
||||
} else if (!programWrapper) {
|
||||
programWrapper = this.programs[i] = program(this, i, fn);
|
||||
}
|
||||
@@ -128,12 +128,13 @@ export function template(templateSpec, env) {
|
||||
if (!options.partial && templateSpec.useData) {
|
||||
data = initData(context, data);
|
||||
}
|
||||
var depths;
|
||||
var depths,
|
||||
blockParams = templateSpec.useBlockParams ? [] : undefined;
|
||||
if (templateSpec.useDepths) {
|
||||
depths = options.depths ? [context].concat(options.depths) : [context];
|
||||
}
|
||||
|
||||
return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths);
|
||||
return templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths);
|
||||
};
|
||||
ret.isTop = true;
|
||||
|
||||
@@ -150,24 +151,33 @@ export function template(templateSpec, env) {
|
||||
}
|
||||
};
|
||||
|
||||
ret._child = function(i, data, depths) {
|
||||
ret._child = function(i, data, blockParams, depths) {
|
||||
if (templateSpec.useBlockParams && !blockParams) {
|
||||
throw new Exception('must pass block params');
|
||||
}
|
||||
if (templateSpec.useDepths && !depths) {
|
||||
throw new Exception('must pass parent depths');
|
||||
}
|
||||
|
||||
return program(container, i, templateSpec[i], data, depths);
|
||||
return program(container, i, templateSpec[i], data, 0, blockParams, depths);
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function program(container, i, fn, data, depths) {
|
||||
export function program(container, i, fn, data, declaredBlockParams, blockParams, depths) {
|
||||
var prog = function(context, options) {
|
||||
options = options || {};
|
||||
|
||||
return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths));
|
||||
return fn.call(container,
|
||||
context,
|
||||
container.helpers, container.partials,
|
||||
options.data || data,
|
||||
blockParams && [options.blockParams].concat(blockParams),
|
||||
depths && [context].concat(depths));
|
||||
};
|
||||
prog.program = i;
|
||||
prog.depth = depths ? depths.length : 0;
|
||||
prog.blockParams = declaredBlockParams || 0;
|
||||
return prog;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,11 @@ export function isEmpty(value) {
|
||||
}
|
||||
}
|
||||
|
||||
export function blockParams(params, ids) {
|
||||
params.path = ids;
|
||||
return params;
|
||||
}
|
||||
|
||||
export function appendContextPath(contextPath, id) {
|
||||
return (contextPath ? contextPath + '.' : '') + id;
|
||||
}
|
||||
|
||||
@@ -122,6 +122,16 @@ describe('builtin helpers', function() {
|
||||
equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used");
|
||||
});
|
||||
|
||||
it('each with block params', function() {
|
||||
var string = '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!';
|
||||
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}], world: 'world'};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, '0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!');
|
||||
});
|
||||
|
||||
it("each object with @index", function() {
|
||||
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: {'a': {text: "goodbye"}, b: {text: "Goodbye"}, c: {text: "GOODBYE"}}, world: "world"};
|
||||
|
||||
@@ -657,4 +657,64 @@ describe('helpers', function() {
|
||||
equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed");
|
||||
});
|
||||
});
|
||||
|
||||
describe('block params', function() {
|
||||
it('should take presedence over context values', function() {
|
||||
var hash = {value: 'foo'};
|
||||
var helpers = {
|
||||
goodbyes: function(options) {
|
||||
equals(options.fn.blockParams, 1);
|
||||
return options.fn({value: 'bar'}, {blockParams: [1, 2]});
|
||||
}
|
||||
};
|
||||
shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo');
|
||||
});
|
||||
it('should take presedence over helper values', function() {
|
||||
var hash = {};
|
||||
var helpers = {
|
||||
value: function() {
|
||||
return 'foo';
|
||||
},
|
||||
goodbyes: function(options) {
|
||||
equals(options.fn.blockParams, 1);
|
||||
return options.fn({}, {blockParams: [1, 2]});
|
||||
}
|
||||
};
|
||||
shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo');
|
||||
});
|
||||
it('should not take presedence over pathed values', function() {
|
||||
var hash = {value: 'bar'};
|
||||
var helpers = {
|
||||
value: function() {
|
||||
return 'foo';
|
||||
},
|
||||
goodbyes: function(options) {
|
||||
equals(options.fn.blockParams, 1);
|
||||
return options.fn(this, {blockParams: [1, 2]});
|
||||
}
|
||||
};
|
||||
shouldCompileTo('{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}', [hash, helpers], 'barfoo');
|
||||
});
|
||||
it('should take presednece over parent block params', function() {
|
||||
var hash = {value: 'foo'},
|
||||
value = 1;
|
||||
var helpers = {
|
||||
goodbyes: function(options) {
|
||||
return options.fn({value: 'bar'}, {blockParams: options.fn.blockParams === 1 ? [value++, value++] : undefined});
|
||||
}
|
||||
};
|
||||
shouldCompileTo('{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}', [hash, helpers], '13foo');
|
||||
});
|
||||
|
||||
it('should allow block params on chained helpers', function() {
|
||||
var hash = {value: 'foo'};
|
||||
var helpers = {
|
||||
goodbyes: function(options) {
|
||||
equals(options.fn.blockParams, 1);
|
||||
return options.fn({value: 'bar'}, {blockParams: [1, 2]});
|
||||
}
|
||||
};
|
||||
shouldCompileTo('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}', [hash, helpers], '1foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+11
-1
@@ -48,6 +48,16 @@ describe('runtime', function() {
|
||||
template._child(1);
|
||||
}, Error, 'must pass parent depths');
|
||||
});
|
||||
|
||||
it('should throw for block param methods without params', function() {
|
||||
shouldThrow(function() {
|
||||
var template = Handlebars.compile('{{#foo as |foo|}}{{foo}}{{/foo}}');
|
||||
// Calling twice to hit the non-compiled case.
|
||||
template._setup({});
|
||||
template._setup({});
|
||||
template._child(1);
|
||||
}, Error, 'must pass block params');
|
||||
});
|
||||
it('should expose child template', function() {
|
||||
var template = Handlebars.compile('{{#foo}}bar{{/foo}}');
|
||||
// Calling twice to hit the non-compiled case.
|
||||
@@ -57,7 +67,7 @@ describe('runtime', function() {
|
||||
it('should render depthed content', function() {
|
||||
var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
|
||||
// Calling twice to hit the non-compiled case.
|
||||
equal(template._child(1, undefined, [{bar: 'baz'}])(), 'baz');
|
||||
equal(template._child(1, undefined, [], [{bar: 'baz'}])(), 'baz');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -106,6 +106,27 @@ describe('track ids', function() {
|
||||
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS 1');
|
||||
});
|
||||
|
||||
it('should use block param paths', function() {
|
||||
var template = CompilerContext.compile('{{#doIt as |is|}}{{wycats is.a slave.driver is}}{{/doIt}}', {trackIds: true});
|
||||
|
||||
var helpers = {
|
||||
doIt: function(options) {
|
||||
var blockParams = [this.is];
|
||||
blockParams.path = ['zomg'];
|
||||
return options.fn(this, {blockParams: blockParams});
|
||||
},
|
||||
wycats: function(passiveVoice, noun, blah, options) {
|
||||
equal(options.ids[0], 'zomg.a');
|
||||
equal(options.ids[1], 'slave.driver');
|
||||
equal(options.ids[2], 'zomg');
|
||||
|
||||
return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
|
||||
}
|
||||
};
|
||||
|
||||
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS zomg.a:foo slave.driver:bar');
|
||||
});
|
||||
|
||||
describe('builtin helpers', function() {
|
||||
var helpers = {
|
||||
wycats: function(name, options) {
|
||||
@@ -129,6 +150,17 @@ describe('track ids', function() {
|
||||
|
||||
equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:.array..0\nbar:.array..1\n');
|
||||
});
|
||||
it('should handle block params', function() {
|
||||
var helpers = {
|
||||
wycats: function(name, options) {
|
||||
return name + ':' + options.ids[0] + '\n';
|
||||
}
|
||||
};
|
||||
|
||||
var template = CompilerContext.compile('{{#each array as |value|}}{{wycats value.name}}{{/each}}', {trackIds: true});
|
||||
|
||||
equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:array.0.name\nbar:array.1.name\n');
|
||||
});
|
||||
});
|
||||
describe('#with', function() {
|
||||
it('should track contextPath', function() {
|
||||
|
||||
Reference in New Issue
Block a user