diff --git a/src/liquid.ts b/src/liquid.ts index d3893bab8d..a19b65456e 100644 --- a/src/liquid.ts +++ b/src/liquid.ts @@ -14,7 +14,7 @@ import { FilterMap } from './template/filter/filter-map' import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options' import { FilterImplOptions } from './template/filter/filter-impl-options' import { FS } from './fs/fs' -import { toThenable, toValue } from './util/async' +import { toPromise, toValue } from './util/async' export * from './types' @@ -49,7 +49,7 @@ export class Liquid { return this.renderer.renderTemplates(tpl, ctx) } public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise { - return toThenable(this._render(tpl, scope, opts, false)) + return toPromise(this._render(tpl, scope, opts, false)) } public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): string { return toValue(this._render(tpl, scope, opts, true)) @@ -60,7 +60,7 @@ export class Liquid { return this._render(tpl, scope, opts, sync) } public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise { - return toThenable(this._parseAndRender(html, scope, opts, false)) + return toPromise(this._parseAndRender(html, scope, opts, false)) } public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): string { return toValue(this._parseAndRender(html, scope, opts, true)) @@ -88,7 +88,7 @@ export class Liquid { throw this.lookupError(file, options.root) } public async parseFile (file: string, opts?: LiquidOptions): Promise { - return toThenable(this._parseFile(file, opts, false)) + return toPromise(this._parseFile(file, opts, false)) } public parseFileSync (file: string, opts?: LiquidOptions): Template[] { return toValue(this._parseFile(file, opts, true)) @@ -108,7 +108,7 @@ export class Liquid { return value.value(ctx) } public async evalValue (str: string, ctx: Context): Promise { - return toThenable(this._evalValue(str, ctx)) + return toPromise(this._evalValue(str, ctx)) } public evalValueSync (str: string, ctx: Context): any { return toValue(this._evalValue(str, ctx)) diff --git a/src/template/tag/hash.ts b/src/template/tag/hash.ts index f48cbac009..ae15b99873 100644 --- a/src/template/tag/hash.ts +++ b/src/template/tag/hash.ts @@ -21,7 +21,7 @@ export class Hash { * render (ctx: Context) { const hash = {} for (const key of Object.keys(this.hash)) { - hash[key] = evalToken(this.hash[key], ctx) + hash[key] = yield evalToken(this.hash[key], ctx) } return hash } diff --git a/src/types.ts b/src/types.ts index 3b2e04a23a..063b94e164 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,3 +17,4 @@ export { TopLevelToken } from './tokens/toplevel-token' export { Tokenizer } from './parser/tokenizer' export { Hash } from './template/tag/hash' export { evalToken, evalQuotedToken } from './render/expression' +export { toPromise, toThenable, toValue } from './util/async' diff --git a/src/util/async.ts b/src/util/async.ts index a79a7aefc5..72c6e63c72 100644 --- a/src/util/async.ts +++ b/src/util/async.ts @@ -7,7 +7,7 @@ interface Thenable { catch (reject: resolver): Thenable; } -function mkResolve (value: any) { +function createResolvedThenable (value: any): Thenable { const ret = { then: (resolve: resolver) => resolve(value), catch: () => ret @@ -15,7 +15,7 @@ function mkResolve (value: any) { return ret } -function mkReject (err: Error) { +function createRejectedThenable (err: Error): Thenable { const ret = { then: (resolve: resolver, reject?: resolver) => { if (reject) return reject(err) @@ -30,43 +30,49 @@ function isThenable (val: any): val is Thenable { return val && isFunction(val.then) } -function isCustomIterable (val: any): val is IterableIterator { +function isAsyncIterator (val: any): val is IterableIterator { return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return) } +// convert an async iterator to a thenable (Promise compatible) export function toThenable (val: IterableIterator | Thenable | any): Thenable { if (isThenable(val)) return val - if (isCustomIterable(val)) return reduce() - return mkResolve(val) + if (isAsyncIterator(val)) return reduce() + return createResolvedThenable(val) function reduce (prev?: any): Thenable { let state try { state = (val as IterableIterator).next(prev) } catch (err) { - return mkReject(err) + return createRejectedThenable(err) } - if (state.done) return mkResolve(state.value) + if (state.done) return createResolvedThenable(state.value) return toThenable(state.value!).then(reduce, err => { let state try { state = (val as IterableIterator).throw!(err) } catch (e) { - return mkReject(e) + return createRejectedThenable(e) } - if (state.done) return mkResolve(state.value) + if (state.done) return createResolvedThenable(state.value) return reduce(state.value) }) } } +export function toPromise (val: IterableIterator | Thenable | any): Promise { + return Promise.resolve(toThenable(val)) +} + +// get the value of async iterator in synchronous manner export function toValue (val: IterableIterator | Thenable | any) { let ret: any toThenable(val) .then((x: any) => { ret = x - return mkResolve(ret) + return createResolvedThenable(ret) }) .catch((err: Error) => { throw err diff --git a/test/unit/util/async.ts b/test/unit/util/async.ts index c37713b039..9952bf8c27 100644 --- a/test/unit/util/async.ts +++ b/test/unit/util/async.ts @@ -1,10 +1,19 @@ -import { toThenable, toValue } from '../../../src/util/async' +import { toThenable, toPromise, toValue } from '../../../src/util/async' import { expect, use } from 'chai' import * as chaiAsPromised from 'chai-as-promised' use(chaiAsPromised) describe('utils/async', () => { + describe('#toPromise()', function () { + it('should return a promise', async () => { + function * foo () { + return 'foo' + } + const result = await toPromise(foo()) + expect(result).to.equal('foo') + }) + }) describe('#toThenable()', function () { it('should support iterable with single return statement', async () => { function * foo () {