Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement recursive field lookup #819

Merged
merged 4 commits into from
Aug 14, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bench/throughput.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ function makeSuite(bench, name, template, handlebarsOnly) {
partials = template.partials,

handlebarsOut,
compatOut,
dustOut,
ecoOut,
mustacheOut;

var handlebar = Handlebars.compile(template.handlebars, {data: false}),
compat = Handlebars.compile(template.handlebars, {data: false, compat: true}),
options = {helpers: template.helpers};
_.each(template.partials && template.partials.handlebars, function(partial, name) {
Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false}));
Expand All @@ -43,6 +45,11 @@ function makeSuite(bench, name, template, handlebarsOnly) {
handlebar(context, options);
});

compatOut = compat(context, options);
bench("compat", function() {
compat(context, options);
});

if (handlebarsOnly) {
return;
}
Expand Down Expand Up @@ -107,6 +114,7 @@ function makeSuite(bench, name, template, handlebarsOnly) {
}
}

compare(compatOut, 'compat');
compare(dustOut, 'dust');
compare(ecoOut, 'eco');
compare(mustacheOut, 'mustache');
Expand Down
11 changes: 9 additions & 2 deletions lib/handlebars/compiler/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ Compiler.prototype = {
if (partial.context) {
this.accept(partial.context);
} else {
this.opcode('push', 'depth0');
this.opcode('getContext', 0);
this.opcode('pushContext');
}

this.opcode('invokePartial', partialName.name, partial.indent || '');
Expand Down Expand Up @@ -307,7 +308,7 @@ Compiler.prototype = {
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
this.opcode('pushContext');
} else {
this.opcode('lookupOnContext', id.parts, id.falsy);
this.opcode('lookupOnContext', id.parts, id.falsy, id.isScoped);
}
},

Expand Down Expand Up @@ -423,6 +424,9 @@ export function precompile(input, options, env) {
if (!('data' in options)) {
options.data = true;
}
if (options.compat) {
options.useDepths = true;
}

var ast = env.parse(input);
var environment = new env.Compiler().compile(ast, options);
Expand All @@ -439,6 +443,9 @@ export function compile(input, options, env) {
if (!('data' in options)) {
options.data = true;
}
if (options.compat) {
options.useDepths = true;
}

var compiled;

Expand Down
66 changes: 45 additions & 21 deletions lib/handlebars/compiler/javascript-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ JavaScriptCompiler.prototype = {
return parent + "['" + name + "']";
}
},
depthedLookup: function(name) {
this.aliases.lookup = 'this.lookup';

return 'lookup(depths, "' + name + '")';
},

compilerInfo: function() {
var revision = COMPILER_REVISION,
Expand Down Expand Up @@ -69,6 +74,8 @@ JavaScriptCompiler.prototype = {

this.compileChildren(environment, options);

this.useDepths = this.useDepths || environment.depths.list.length || this.options.compat;

var opcodes = environment.opcodes,
opcode,
i,
Expand Down Expand Up @@ -115,6 +122,9 @@ JavaScriptCompiler.prototype = {
if (this.options.data) {
ret.useData = true;
}
if (this.useDepths) {
ret.depths = true;
}

if (!asObject) {
ret.compiler = JSON.stringify(ret.compiler);
Expand Down Expand Up @@ -151,8 +161,8 @@ JavaScriptCompiler.prototype = {

var params = ["depth0", "helpers", "partials", "data"];

for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
params.push("depth" + this.environment.depths.list[i]);
if (this.useDepths) {
params.push('depths');
}

// Perform a second pass over the output to merge content when possible
Expand Down Expand Up @@ -230,7 +240,7 @@ JavaScriptCompiler.prototype = {
blockValue: function(name) {
this.aliases.blockHelperMissing = 'helpers.blockHelperMissing';

var params = ["depth0"];
var params = [this.contextName(0)];
this.setupParams(name, 0, params);

this.replaceStack(function(current) {
Expand All @@ -249,7 +259,7 @@ JavaScriptCompiler.prototype = {
this.aliases.blockHelperMissing = 'helpers.blockHelperMissing';

// We're being a bit cheeky and reusing the options value from the prior exec
var params = ["depth0"];
var params = [this.contextName(0)];
this.setupParams('', 0, params, true);

this.flushInline();
Expand Down Expand Up @@ -341,7 +351,7 @@ JavaScriptCompiler.prototype = {
//
// Pushes the value of the current context onto the stack.
pushContext: function() {
this.pushStackLiteral('depth' + this.lastContext);
this.pushStackLiteral(this.contextName(this.lastContext));
},

// [lookupOnContext]
Expand All @@ -351,15 +361,20 @@ JavaScriptCompiler.prototype = {
//
// Looks up the value of `name` on the current context and pushes
// it onto the stack.
lookupOnContext: function(parts, falsy) {
lookupOnContext: function(parts, falsy, scoped) {
/*jshint -W083 */
this.pushContext();
if (!parts) {
return;
var i = 0,
len = parts.length;

if (!scoped && this.isChild && this.options.compat && !this.lastContext) {
// The depthed query is expected to handle the undefined logic for the root level that
// is implemented below, so we evaluate that directly in compat mode
this.pushStackLiteral(this.depthedLookup(parts[i++]));
} else {
this.pushContext();
}

var len = parts.length;
for (var i = 0; i < len; i++) {
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 for the first element
Expand Down Expand Up @@ -410,7 +425,7 @@ JavaScriptCompiler.prototype = {
resolvePossibleLambda: function() {
this.aliases.lambda = 'this.lambda';

this.push('lambda(' + this.popStack() + ', depth0)');
this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')');
},

// [pushStringParam]
Expand All @@ -422,8 +437,7 @@ JavaScriptCompiler.prototype = {
// provides the string value of a parameter along with its
// depth rather than resolving it immediately.
pushStringParam: function(string, type) {
this.pushStackLiteral('depth' + this.lastContext);

this.pushContext();
this.pushString(type);

// If it's a subexpression, the string result
Expand Down Expand Up @@ -669,6 +683,8 @@ JavaScriptCompiler.prototype = {
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
this.context.environments[index] = child;

this.useDepths = this.useDepths || compiler.useDepths;
} else {
child.index = index;
child.name = 'program' + index;
Expand All @@ -690,17 +706,17 @@ JavaScriptCompiler.prototype = {
}

var child = this.environment.children[guid],
depths = child.depths.list, depth;
depths = child.depths.list,
useDepths = this.useDepths,
depth;

var programParams = [child.index, 'data'];

for(var i=0, l = depths.length; i<l; i++) {
depth = depths[i];

programParams.push('depth' + (depth - 1));
if (useDepths) {
programParams.push('depths');
}

return (depths.length === 0 ? 'this.program(' : 'this.programWithDepth(') + programParams.join(', ') + ')';
return 'this.program(' + programParams.join(', ') + ')';
},

useRegister: function(name) {
Expand Down Expand Up @@ -847,6 +863,14 @@ JavaScriptCompiler.prototype = {
}
},

contextName: function(context) {
if (this.useDepths && context) {
return 'depths[' + context + ']';
} else {
return 'depth' + context;
}
},

quotedString: function(str) {
return '"' + str
.replace(/\\/g, '\\\\')
Expand Down Expand Up @@ -878,7 +902,7 @@ JavaScriptCompiler.prototype = {
params: params,
paramsInit: paramsInit,
name: foundHelper,
callParams: ["depth0"].concat(params).join(", ")
callParams: [this.contextName(0)].concat(params).join(", ")
};
},

Expand Down
48 changes: 24 additions & 24 deletions lib/handlebars/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ export function template(templateSpec, env) {

// Just add water
var container = {
lookup: function(depths, name) {
var len = depths.length;
for (var i = 0; i < len; i++) {
if (depths[i] && depths[i][name] != null) {
return depths[i][name];
}
}
},
lambda: function(current, context) {
return typeof current === 'function' ? current.call(context) : current;
},
Expand All @@ -75,17 +83,16 @@ export function template(templateSpec, env) {
},

programs: [],
program: function(i, data) {
program: function(i, data, depths) {
var programWrapper = this.programs[i],
fn = this.fn(i);
if(data) {
programWrapper = program(this, i, fn, data);
if (data || depths) {
programWrapper = program(this, i, fn, data, depths);
} else if (!programWrapper) {
programWrapper = this.programs[i] = program(this, i, fn);
}
return programWrapper;
},
programWithDepth: env.VM.programWithDepth,

data: function(data, depth) {
while (data && depth--) {
Expand Down Expand Up @@ -117,7 +124,12 @@ export function template(templateSpec, env) {
if (!options.partial && templateSpec.useData) {
data = initData(context, data);
}
return templateSpec.main.call(container, context, container.helpers, container.partials, data);
var depths;
if (templateSpec.depths) {
depths = [context];
}

return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths);
};

ret._setup = function(options) {
Expand All @@ -134,35 +146,23 @@ export function template(templateSpec, env) {
};

ret._child = function(i) {
if (templateSpec.depth) {
throw new Exception('_child can not be used with depthed methods');
}

return container.programWithDepth(i);
};
return ret;
}

export function programWithDepth(i, data /*, $depth */) {
/*jshint -W040 */
var args = Array.prototype.slice.call(arguments, 2),
container = this,
fn = container.fn(i);

var prog = function(context, options) {
options = options || {};

return fn.apply(container, [context, container.helpers, container.partials, options.data || data].concat(args));
};
prog.program = i;
prog.depth = args.length;
return prog;
}

export function program(container, i, fn, data) {
export function program(container, i, fn, data, depths) {
var prog = function(context, options) {
options = options || {};

return fn.call(container, context, container.helpers, container.partials, options.data || data);
return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths));
};
prog.program = i;
prog.depth = 0;
prog.depth = depths ? depths.length : 0;
return prog;
}

Expand Down
21 changes: 21 additions & 0 deletions spec/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,25 @@ describe('blocks', function() {
'No people\n');
});
});

describe('compat mode', function() {
it("block with deep recursive lookup lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}";
var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };

shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!");
});
it("block with deep recursive pathed lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}";
var hash = {omg: {yes: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] };

shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!");
});
it("block with missed recursive lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}";
var hash = {omg: {no: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] };

shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel ");
});
});
});
2 changes: 1 addition & 1 deletion spec/env/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
ary = [];
ary.push(hashOrArray[0]);
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2], compat: hashOrArray[3] });
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
options = {compat: hashOrArray[3]};
} else {
ary = [hashOrArray];
Expand Down