Skip to content

Commit

Permalink
Allow rendering properties of primitive types that are not objects (#618
Browse files Browse the repository at this point in the history
)

* prevent value from being returned by Context.prototype.lookup if lookupHit is false

* add test for renderability of Array.length via dot notation

* Remove `typeof obj === 'object'` constraint in prop lookup

Allows rendering properties of primitive types that are not objects, such as a string.

* pop lookup needs to use hasOwnProperty for non-objs

* re-add  constraint in prop lookup, but make property lookups for primitives possible through dot notation

* add test to address #589 specifically

* enhance readability of primitiveHasOwnProperty and add comments to explain why it is used in one case but not the other
  • Loading branch information
raymond-lam authored and phillipj committed Aug 25, 2018
1 parent efdeb55 commit a2699e4
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 8 deletions.
58 changes: 51 additions & 7 deletions mustache.js
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.
**/
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;
}
Expand Down
3 changes: 2 additions & 1 deletion test/_files/dot_notation.js
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
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
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>

0 comments on commit a2699e4

Please sign in to comment.