From 396795c983273bb5ca4dc67ddc74eb12f00bf110 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 26 Dec 2014 00:31:57 -0600 Subject: [PATCH] Implement block parameters Fixes #907 --- lib/handlebars/base.js | 6 +- lib/handlebars/compiler/compiler.js | 65 +++++++++++---- .../compiler/javascript-compiler.js | 82 +++++++++++++------ lib/handlebars/runtime.js | 28 +++++-- lib/handlebars/utils.js | 5 ++ spec/builtins.js | 10 +++ spec/helpers.js | 60 ++++++++++++++ spec/runtime.js | 12 ++- spec/track-ids.js | 32 ++++++++ 9 files changed, 248 insertions(+), 52 deletions(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 50aa3f82b..d75e965cb 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -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') { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 7be96cb51..13143cc8d 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -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= 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; } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 3e4f5e1d0..9f407924f 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -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; }, diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 455dd332a..72f2e0dce 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -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; } diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index 0878cc4f1..8cea50d74 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -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; } diff --git a/spec/builtins.js b/spec/builtins.js index 9aa916902..eb5157c35 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -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"}; diff --git a/spec/helpers.js b/spec/helpers.js index f23ee1403..712bb00a0 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -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'); + }); + }); }); diff --git a/spec/runtime.js b/spec/runtime.js index 48a22a99b..ce8b14c56 100644 --- a/spec/runtime.js +++ b/spec/runtime.js @@ -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'); }); }); diff --git a/spec/track-ids.js b/spec/track-ids.js index 938f98bb5..f337fbe11 100644 --- a/spec/track-ids.js +++ b/spec/track-ids.js @@ -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() {