diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 51cf999c..6e7a0c12 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -1175,20 +1175,34 @@ function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) { stack = compiler.nameLookup(stack, parts[i], type); } - if (requireTerminal) { + if (!requireTerminal) { + return stack; + } + + if (startPartIndex > len) { + // Compat mode already consumed all parts via depthedLookup; the stack + // holds the resolved value, not an object to look the property up on. + // Use container.strictLookup to traverse depths with a strict error on miss. return [ - compiler.aliasable('container.strict'), - '(', - stack, - ', ', + compiler.aliasable('container.strictLookup'), + '(depths, ', compiler.quotedString(parts[len]), ', ', JSON.stringify(compiler.source.currentLocation), ' )', ]; - } else { - return stack; } + + return [ + compiler.aliasable('container.strict'), + '(', + stack, + ', ', + compiler.quotedString(parts[len]), + ', ', + JSON.stringify(compiler.source.currentLocation), + ' )', + ]; } export default JavaScriptCompiler; diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 9f6b66db..6aef63bf 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -116,6 +116,22 @@ export function template(templateSpec, env) { } return container.lookupProperty(obj, name); }, + strictLookup: function (depths, name, loc) { + const len = depths.length; + let depth; + for (let i = 0; i < len; i++) { + const d = depths[i]; + if ( + d && + (typeof d === 'object' || typeof d === 'function') && + name in d + ) { + depth = d; + break; + } + } + return container.strict(depth, name, loc); + }, lookupProperty: function (parent, propertyName) { if (Utils.isMap(parent)) { return parent.get(propertyName); diff --git a/spec/strict.js b/spec/strict.js index ad788463..bfcb0a9a 100644 --- a/spec/strict.js +++ b/spec/strict.js @@ -121,6 +121,63 @@ describe('strict', function () { }); }); + describe('strict and compat mode', function () { + it('GH-1741: should render a simple variable', function () { + expectTemplate('{{v}}') + .withCompileOptions({ strict: true, compat: true }) + .withInput({ v: 'a' }) + .toCompileTo('a'); + }); + + it('should throw for a missing variable', function () { + expectTemplate('{{v}}') + .withCompileOptions({ strict: true, compat: true }) + .toThrow(Exception, /"v" not defined in/); + }); + + it('GH-2149: should render correctly when block context is a boolean', function () { + expectTemplate('{{#foo}}Hello {{bar}}{{/foo}}') + .withCompileOptions({ strict: true, compat: true }) + .withInput({ foo: true, bar: 'World' }) + .toCompileTo('Hello World'); + }); + + it('GH-2149: should render correctly when looking up a property on each item', function () { + expectTemplate('{{#each items}}{{name}}{{/each}}') + .withCompileOptions({ strict: true, compat: true }) + .withInput({ items: [{ name: 'Hello' }] }) + .toCompileTo('Hello'); + }); + + it('should still throw when a property is missing at all depths', function () { + expectTemplate('{{#each items}}{{name}}{{/each}}') + .withCompileOptions({ strict: true, compat: true }) + .withInput({ items: [1, 2] }) + .toThrow(Exception, /"name" not defined in/); + }); + + it('should still perform recursive lookup when a property is not in the current context', function () { + expectTemplate('{{#each items}}{{name}}{{/each}}') + .withCompileOptions({ strict: true, compat: true }) + .withInput({ name: 'root', items: [{}] }) + .toCompileTo('root'); + }); + + it('should still perform recursive lookup with a multi-part path not in context', function () { + expectTemplate('{{#with child}}{{name.first}}{{/with}}') + .withCompileOptions({ strict: true, compat: true }) + .withInput({ name: { first: 'root' }, child: { name: null } }) + .toCompileTo('root'); + }); + + it('should directly return an explicitly null property', function () { + expectTemplate('{{#each items}}{{name}}{{/each}}') + .withCompileOptions({ strict: true, compat: true }) + .withInput({ name: 'root', items: [{ name: null }] }) + .toCompileTo(''); + }); + }); + describe('assume objects', function () { it('should ignore missing property', function () { expectTemplate('{{hello}}')