Skip to content

Commit

Permalink
Merge pull request #819 from wycats/compat-mode
Browse files Browse the repository at this point in the history
Implement recursive field lookup
  • Loading branch information
kpdecker committed Aug 14, 2014
2 parents 867322a + c98613b commit d95725d
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 48 deletions.
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

0 comments on commit d95725d

Please sign in to comment.