Implement block decorators

These allow for a given block to be wrapped in helper methods or metadata and allow for more control over the current container and method before the code is run.
This commit is contained in:
kpdecker
2015-08-19 08:27:13 -07:00
parent 408192ba9f
commit 452afbf2ff
7 changed files with 244 additions and 8 deletions
+1 -1
View File
@@ -45,7 +45,7 @@
"no-extra-boolean-cast": 2,
"no-extra-parens": 0,
"no-extra-semi": 2,
"no-func-assign": 2,
"no-func-assign": 0,
// Stylistic... might consider disallowing in the future
"no-inner-declarations": 0,
+1 -1
View File
@@ -276,7 +276,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c
- `parent` is the existing code in the path resolution
- `name` is the current path component
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`.
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, 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.
+3
View File
@@ -69,6 +69,9 @@ function CodeGen(srcFile) {
}
CodeGen.prototype = {
isEmpty() {
return !this.source.length;
},
prepend: function(source, loc) {
this.source.unshift(this.wrap(source, loc));
},
+13
View File
@@ -156,6 +156,15 @@ Compiler.prototype = {
this.opcode('append');
},
DecoratorBlock(decorator) {
let program = decorator.program && this.compileProgram(decorator.program);
let params = this.setupFullMustacheParams(decorator, program, undefined),
path = decorator.path;
this.useDecorators = true;
this.opcode('registerDecorator', params.length, path.original);
},
PartialStatement: function(partial) {
this.usePartial = true;
@@ -201,6 +210,10 @@ Compiler.prototype = {
this.opcode('append');
}
},
Decorator(decorator) {
this.DecoratorBlock(decorator);
},
ContentStatement: function(content) {
if (content.value) {
+67 -4
View File
@@ -64,6 +64,7 @@ JavaScriptCompiler.prototype = {
this.name = this.environment.name;
this.isChild = !!context;
this.context = context || {
decorators: [],
programs: [],
environments: []
};
@@ -81,7 +82,7 @@ JavaScriptCompiler.prototype = {
this.compileChildren(environment, options);
this.useDepths = this.useDepths || environment.useDepths || this.options.compat;
this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat;
this.useBlockParams = this.useBlockParams || environment.useBlockParams;
let opcodes = environment.opcodes,
@@ -107,16 +108,43 @@ JavaScriptCompiler.prototype = {
throw new Exception('Compile completed with content left on stack');
}
if (!this.decorators.isEmpty()) {
this.useDecorators = true;
this.decorators.prepend('var decorators = container.decorators;\n');
this.decorators.push('return fn;');
if (asObject) {
this.decorators = Function.apply(this, ['fn', 'props', 'container', 'depth0', 'data', 'blockParams', 'depths', this.decorators.merge()]);
} else {
this.decorators.prepend('function(fn, props, container, depth0, data, blockParams, depths) {\n');
this.decorators.push('}\n');
this.decorators = this.decorators.merge();
}
} else {
this.decorators = undefined;
}
let fn = this.createFunctionContext(asObject);
if (!this.isChild) {
let ret = {
compiler: this.compilerInfo(),
main: fn
};
let programs = this.context.programs;
if (this.decorators) {
ret.main_d = this.decorators; // eslint-disable-line camelcase
ret.useDecorators = true;
}
let {programs, decorators} = this.context;
for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) {
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
}
}
@@ -163,6 +191,7 @@ JavaScriptCompiler.prototype = {
// getContext opcode when it would be a noop
this.lastContext = 0;
this.source = new CodeGen(this.options.srcName);
this.decorators = new CodeGen(this.options.srcName);
},
createFunctionContext: function(asObject) {
@@ -561,6 +590,24 @@ JavaScriptCompiler.prototype = {
}
},
// [registerDecorator]
//
// On stack, before: hash, program, params..., ...
// On stack, after: ...
//
// Pops off the decorator's parameters, invokes the decorator,
// and inserts the decorator into the decorators list.
registerDecorator(paramSize, name) {
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
options = this.setupHelperArgs(name, paramSize);
this.decorators.push([
'fn = ',
this.decorators.functionCall(foundDecorator, '', ['fn', 'props', 'container', options]),
' || fn;'
]);
},
// [invokeHelper]
//
// On stack, before: hash, inverse, program, params..., ...
@@ -738,6 +785,7 @@ JavaScriptCompiler.prototype = {
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
this.context.decorators[index] = compiler.decorators;
this.context.environments[index] = child;
this.useDepths = this.useDepths || compiler.useDepths;
@@ -946,7 +994,16 @@ JavaScriptCompiler.prototype = {
},
setupParams: function(helper, paramSize, params) {
let options = {}, contexts = [], types = [], ids = [], param;
let options = {},
contexts = [],
types = [],
ids = [],
objectArgs = !params,
param;
if (objectArgs) {
params = [];
}
options.name = this.quotedString(helper);
options.hash = this.popStack();
@@ -985,6 +1042,10 @@ JavaScriptCompiler.prototype = {
}
}
if (objectArgs) {
options.args = this.source.generateArray(params);
}
if (this.trackIds) {
options.ids = this.source.generateArray(ids);
}
@@ -1009,9 +1070,11 @@ JavaScriptCompiler.prototype = {
this.useRegister('options');
params.push('options');
return ['options=', options];
} else {
} else if (params) {
params.push(options);
return '';
} else {
return options;
}
}
};
+21 -2
View File
@@ -90,7 +90,9 @@ export function template(templateSpec, env) {
invokePartial: invokePartialWrapper,
fn: function(i) {
return templateSpec[i];
let ret = templateSpec[i];
ret.decorator = templateSpec[i + '_d'];
return ret;
},
programs: [],
@@ -142,7 +144,17 @@ export function template(templateSpec, env) {
}
}
return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths);
function main(context/*, options*/) {
return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths);
}
if (templateSpec.main_d) {
// Note that we are ignoring the props value here as we apply things slightly differently
// when applying decorators to the root function.
main = templateSpec.main_d(main, {}, container, undefined, data, blockParams, depths);
}
return main(context, options);
}
ret.isTop = true;
@@ -190,6 +202,13 @@ export function wrapProgram(container, i, fn, data, declaredBlockParams, blockPa
blockParams && [options.blockParams].concat(blockParams),
currentDepths);
}
if (fn.decorator) {
let props = {};
prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths);
Utils.extend(prog, props);
}
prog.program = i;
prog.depth = depths ? depths.length : 0;
prog.blockParams = declaredBlockParams || 0;
+138
View File
@@ -168,6 +168,144 @@ describe('blocks', function() {
});
describe('decorators', function() {
it('should apply mustache decorators', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn) {
fn.run = 'success';
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{*decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should apply allow undefined return', function() {
var helpers = {
helper: function(options) {
return options.fn() + options.fn.run;
}
};
var decorators = {
decorator: function(fn) {
fn.run = 'cess';
}
};
shouldCompileTo(
'{{#helper}}{{*decorator}}suc{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should apply block decorators', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = options.fn();
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should support nested decorators', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = options.fn.nested + options.fn();
return fn;
},
nested: function(fn, props, container, options) {
props.nested = options.fn();
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should apply multiple decorators', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = (fn.run || '') + options.fn();
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should access parent variables', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = options.args;
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{*decorator foo}}{{/helper}}',
{hash: {'foo': 'success'}, helpers: helpers, decorators: decorators},
'success');
});
it('should work with root program', function() {
var run;
var decorators = {
decorator: function(fn, props, container, options) {
equals(options.args[0], 'success');
run = true;
return fn;
}
};
shouldCompileTo(
'{{*decorator "success"}}',
{hash: {'foo': 'success'}, decorators: decorators},
'');
equals(run, true);
});
it('should fail when accessing variables from root', function() {
var run;
var decorators = {
decorator: function(fn, props, container, options) {
equals(options.args[0], undefined);
run = true;
return fn;
}
};
shouldCompileTo(
'{{*decorator foo}}',
{hash: {'foo': 'fail'}, decorators: decorators},
'');
equals(run, true);
});
describe('registration', function() {
it('unregisters', function() {
handlebarsEnv.decorators = {};