Skip to content

Commit a2699e4

Browse files
raymond-lamphillipj
authored andcommitted
Allow rendering properties of primitive types that are not objects (#618)
* 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
1 parent efdeb55 commit a2699e4

File tree

4 files changed

+59
-8
lines changed

4 files changed

+59
-8
lines changed

mustache.js

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@
4545
return obj != null && typeof obj === 'object' && (propName in obj);
4646
}
4747

48+
/**
49+
* Safe way of detecting whether or not the given thing is a primitive and
50+
* whether it has the given property
51+
*/
52+
function primitiveHasOwnProperty (primitive, propName) {
53+
return (
54+
primitive != null
55+
&& typeof primitive !== 'object'
56+
&& primitive.hasOwnProperty
57+
&& primitive.hasOwnProperty(propName)
58+
);
59+
}
60+
4861
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
4962
// See https://github.com/janl/mustache.js/issues/189
5063
var regExpTest = RegExp.prototype.test;
@@ -377,11 +390,11 @@
377390
if (cache.hasOwnProperty(name)) {
378391
value = cache[name];
379392
} else {
380-
var context = this, names, index, lookupHit = false;
393+
var context = this, intermediateValue, names, index, lookupHit = false;
381394

382395
while (context) {
383396
if (name.indexOf('.') > 0) {
384-
value = context.view;
397+
intermediateValue = context.view;
385398
names = name.split('.');
386399
index = 0;
387400

@@ -395,20 +408,51 @@
395408
*
396409
* This is specially necessary for when the value has been set to
397410
* `undefined` and we want to avoid looking up parent contexts.
411+
*
412+
* In the case where dot notation is used, we consider the lookup
413+
* to be successful even if the last "object" in the path is
414+
* not actually an object but a primitive (e.g., a string, or an
415+
* integer), because it is sometimes useful to access a property
416+
* of an autoboxed primitive, such as the length of a string.
398417
**/
399-
while (value != null && index < names.length) {
418+
while (intermediateValue != null && index < names.length) {
400419
if (index === names.length - 1)
401-
lookupHit = hasProperty(value, names[index]);
420+
lookupHit = (
421+
hasProperty(intermediateValue, names[index])
422+
|| primitiveHasOwnProperty(intermediateValue, names[index])
423+
);
402424

403-
value = value[names[index++]];
425+
intermediateValue = intermediateValue[names[index++]];
404426
}
405427
} else {
406-
value = context.view[name];
428+
intermediateValue = context.view[name];
429+
430+
/**
431+
* Only checking against `hasProperty`, which always returns `false` if
432+
* `context.view` is not an object. Deliberately omitting the check
433+
* against `primitiveHasOwnProperty` if dot notation is not used.
434+
*
435+
* Consider this example:
436+
* ```
437+
* Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"})
438+
* ```
439+
*
440+
* If we were to check also against `primitiveHasOwnProperty`, as we do
441+
* in the dot notation case, then render call would return:
442+
*
443+
* "The length of a football field is 9."
444+
*
445+
* rather than the expected:
446+
*
447+
* "The length of a football field is 100 yards."
448+
**/
407449
lookupHit = hasProperty(context.view, name);
408450
}
409451

410-
if (lookupHit)
452+
if (lookupHit) {
453+
value = intermediateValue;
411454
break;
455+
}
412456

413457
context = context.parent;
414458
}

test/_files/dot_notation.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
truthy: {
2020
zero: 0,
2121
notTrue: false
22-
}
22+
},
23+
singletonList: [{singletonItem: "singleton item"}]
2324
})

test/_files/dot_notation.mustache

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
<h2>Test truthy false values:</h2>
88
<p>Zero: {{truthy.zero}}</p>
99
<p>False: {{truthy.notTrue}}</p>
10+
<p>length of string should be rendered: {{price.currency.name.length}}</p>
11+
<p>length of string in a list should be rendered: {{#singletonList}}{{singletonItem.length}}{{/singletonList}}</p>
12+
<p>length of an array should be rendered: {{authors.length}}</p>

test/_files/dot_notation.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
<h2>Test truthy false values:</h2>
88
<p>Zero: 0</p>
99
<p>False: false</p>
10+
<p>length of string should be rendered: 3</p>
11+
<p>length of string in a list should be rendered: 14</p>
12+
<p>length of an array should be rendered: 2</p>

0 commit comments

Comments
 (0)