diff --git a/mustache.js b/mustache.js index bfc7ea9bb..3d179e793 100644 --- a/mustache.js +++ b/mustache.js @@ -45,6 +45,19 @@ return obj != null && typeof obj === 'object' && (propName in obj); } + /** + * Safe way of detecting whether or not the given thing is a primitive and + * whether it has the given property + */ + function primitiveHasOwnProperty (primitive, propName) { + return ( + primitive != null + && typeof primitive !== 'object' + && primitive.hasOwnProperty + && primitive.hasOwnProperty(propName) + ); + } + // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 // See https://github.com/janl/mustache.js/issues/189 var regExpTest = RegExp.prototype.test; @@ -377,11 +390,11 @@ if (cache.hasOwnProperty(name)) { value = cache[name]; } else { - var context = this, names, index, lookupHit = false; + var context = this, intermediateValue, names, index, lookupHit = false; while (context) { if (name.indexOf('.') > 0) { - value = context.view; + intermediateValue = context.view; names = name.split('.'); index = 0; @@ -395,20 +408,51 @@ * * This is specially necessary for when the value has been set to * `undefined` and we want to avoid looking up parent contexts. + * + * In the case where dot notation is used, we consider the lookup + * to be successful even if the last "object" in the path is + * not actually an object but a primitive (e.g., a string, or an + * integer), because it is sometimes useful to access a property + * of an autoboxed primitive, such as the length of a string. **/ - while (value != null && index < names.length) { + while (intermediateValue != null && index < names.length) { if (index === names.length - 1) - lookupHit = hasProperty(value, names[index]); + lookupHit = ( + hasProperty(intermediateValue, names[index]) + || primitiveHasOwnProperty(intermediateValue, names[index]) + ); - value = value[names[index++]]; + intermediateValue = intermediateValue[names[index++]]; } } else { - value = context.view[name]; + intermediateValue = context.view[name]; + + /** + * Only checking against `hasProperty`, which always returns `false` if + * `context.view` is not an object. Deliberately omitting the check + * against `primitiveHasOwnProperty` if dot notation is not used. + * + * Consider this example: + * ``` + * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"}) + * ``` + * + * If we were to check also against `primitiveHasOwnProperty`, as we do + * in the dot notation case, then render call would return: + * + * "The length of a football field is 9." + * + * rather than the expected: + * + * "The length of a football field is 100 yards." + **/ lookupHit = hasProperty(context.view, name); } - if (lookupHit) + if (lookupHit) { + value = intermediateValue; break; + } context = context.parent; } diff --git a/test/_files/dot_notation.js b/test/_files/dot_notation.js index de06a03a5..955651fe6 100644 --- a/test/_files/dot_notation.js +++ b/test/_files/dot_notation.js @@ -19,5 +19,6 @@ truthy: { zero: 0, notTrue: false - } + }, + singletonList: [{singletonItem: "singleton item"}] }) diff --git a/test/_files/dot_notation.mustache b/test/_files/dot_notation.mustache index f89d70ba2..1ef2209fe 100644 --- a/test/_files/dot_notation.mustache +++ b/test/_files/dot_notation.mustache @@ -7,3 +7,6 @@

Test truthy false values:

Zero: {{truthy.zero}}

False: {{truthy.notTrue}}

+

length of string should be rendered: {{price.currency.name.length}}

+

length of string in a list should be rendered: {{#singletonList}}{{singletonItem.length}}{{/singletonList}}

+

length of an array should be rendered: {{authors.length}}

diff --git a/test/_files/dot_notation.txt b/test/_files/dot_notation.txt index 08afa0529..c71ab27dd 100644 --- a/test/_files/dot_notation.txt +++ b/test/_files/dot_notation.txt @@ -7,3 +7,6 @@

Test truthy false values:

Zero: 0

False: false

+

length of string should be rendered: 3

+

length of string in a list should be rendered: 14

+

length of an array should be rendered: 2