diff options
author | kpdecker <kpdecker@gmail.com> | 2014-12-26 00:31:57 -0600 |
---|---|---|
committer | kpdecker <kpdecker@gmail.com> | 2014-12-26 00:31:57 -0600 |
commit | 396795c983273bb5ca4dc67ddc74eb12f00bf110 (patch) | |
tree | 8fa69873ed195fc7b0aa1c909373fa7ccac6785a | |
parent | 9e907e67854ea1ae208fe061452a9c9e2ce9468b (diff) | |
download | handlebars.js-396795c983273bb5ca4dc67ddc74eb12f00bf110.zip handlebars.js-396795c983273bb5ca4dc67ddc74eb12f00bf110.tar.gz handlebars.js-396795c983273bb5ca4dc67ddc74eb12f00bf110.tar.bz2 |
Implement block parameters
Fixes #907
-rw-r--r-- | lib/handlebars/base.js | 6 | ||||
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 65 | ||||
-rw-r--r-- | lib/handlebars/compiler/javascript-compiler.js | 82 | ||||
-rw-r--r-- | lib/handlebars/runtime.js | 28 | ||||
-rw-r--r-- | lib/handlebars/utils.js | 5 | ||||
-rw-r--r-- | spec/builtins.js | 10 | ||||
-rw-r--r-- | spec/helpers.js | 60 | ||||
-rw-r--r-- | spec/runtime.js | 12 | ||||
-rw-r--r-- | 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 50aa3f8..d75e965 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 7be96cb..13143cc 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<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; } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 3e4f5e1..9f40792 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 455dd33..72f2e0d 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 0878cc4..8cea50d 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 9aa9169..eb5157c 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 f23ee14..712bb00 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 48a22a9..ce8b14c 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 938f98b..f337fbe 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() { |