diff --git a/lib/dust.js b/lib/dust.js index fa38d4bc..7a664b43 100644 --- a/lib/dust.js +++ b/lib/dust.js @@ -248,16 +248,25 @@ }; /** - * Decide somewhat-naively if something is a Thenable. + * Decide somewhat-naively if something is a Thenable. Matches Promises A+ Spec, section 1.2 “thenable” is an object or function that defines a then method." * @param elem {*} object to inspect * @return {Boolean} is `elem` a Thenable? */ dust.isThenable = function(elem) { - return elem && - typeof elem === 'object' && + return elem && /* Beware: `typeof null` is `object` */ + (typeof elem === 'object' || typeof elem === 'function') && typeof elem.then === 'function'; }; + /** + * Decide if an element is a function but not Thenable; it is prefereable to resolve a thenable function by its `.then` method. + * @param elem {*} target of inspection + * @return {Boolean} is `elem` a function without a `.then` property? + */ + dust.isNonThenableFunction = function(elem) { + return typeof elem === 'function' && !dust.isThenable(elem); + }; + /** * Decide very naively if something is a Stream. * @param elem {*} object to inspect @@ -430,7 +439,7 @@ } } - if (typeof ctx === 'function') { + if (dust.isNonThenableFunction(ctx)) { fn = function() { try { return ctx.apply(ctxThis, arguments); @@ -747,7 +756,7 @@ }; Chunk.prototype.reference = function(elem, context, auto, filters) { - if (typeof elem === 'function') { + if (dust.isNonThenableFunction(elem)) { elem = elem.apply(context.current(), [this, context, null, {auto: auto, filters: filters}]); if (elem instanceof Chunk) { return elem; @@ -772,7 +781,7 @@ chunk = this, i, len, head; - if (typeof elem === 'function' && !dust.isTemplateFn(elem)) { + if (dust.isNonThenableFunction(elem) && !dust.isTemplateFn(elem)) { try { elem = elem.apply(context.current(), [this, context, bodies, params]); } catch(err) { diff --git a/test/templates/all.js b/test/templates/all.js index 664d8e82..9886f6ad 100755 --- a/test/templates/all.js +++ b/test/templates/all.js @@ -24,6 +24,16 @@ function FalsePromise(err, data) { return defer.promise; } +/** + * A naive function factory that adds a thenable that resolves to the given value. + * @param resolvesTo {*} The value passed to the `resolve` then argument +*/ +function createThenableFunction(resolvesTo) { + var fn = function () {}; + fn.then = function (res) { res(resolvesTo) }; + return fn; +} + /** * A naive Stream constructor that streams the provided array asynchronously * @param arr {Array|String} items to be streamed @@ -914,6 +924,20 @@ return [ expected: "Eventually magic!", message: "should reserve an async section for a thenable returned from a function" }, + { + name: "thenable section from thenable function", + source: "{#thenable}Eventually poof!{/thenable}", + context: { "thenable": createThenableFunction("poof!") }, + expected: "Eventually poof!", + message: "should reserve an async section for a thenable function" + }, + { + name: "thenable reference from thenable function", + source: "A {thenable} thing", + context: { "thenable": createThenableFunction("real") }, + expected: "A real thing", + message: "should reserve an async reference for a thenable function" + }, { name: "thenable deep section", source: "Eventually my {#magic.ally}{delicious}{/magic.ally} will come",