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

Resolve #589, and possibly fix other manifestations of this bug #618

Merged
merged 8 commits into from
Aug 25, 2018
58 changes: 51 additions & 7 deletions mustache.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesomee. 🙌

**/
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];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block could use some clarification on why we are only using hasProperty and not primitiveHasOwnProperty too.


/**
* 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;
}
Expand Down
3 changes: 2 additions & 1 deletion test/_files/dot_notation.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
truthy: {
zero: 0,
notTrue: false
}
},
singletonList: [{singletonItem: "singleton item"}]
})
3 changes: 3 additions & 0 deletions test/_files/dot_notation.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
<h2>Test truthy false values:</h2>
<p>Zero: {{truthy.zero}}</p>
<p>False: {{truthy.notTrue}}</p>
<p>length of string should be rendered: {{price.currency.name.length}}</p>
<p>length of string in a list should be rendered: {{#singletonList}}{{singletonItem.length}}{{/singletonList}}</p>
<p>length of an array should be rendered: {{authors.length}}</p>
3 changes: 3 additions & 0 deletions test/_files/dot_notation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
<h2>Test truthy false values:</h2>
<p>Zero: 0</p>
<p>False: false</p>
<p>length of string should be rendered: 3</p>
<p>length of string in a list should be rendered: 14</p>
<p>length of an array should be rendered: 2</p>