pref: remove await/async from internal async calls

This commit is contained in:
harttle
2019-10-26 08:57:33 -05:00
committed by Jun Yang
parent d4ad4b8a8c
commit b82fa9ed2b
40 changed files with 416 additions and 380 deletions
+1
View File
@@ -21,6 +21,7 @@
"no-dupe-class-members": "off",
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-object-literal-type-assertion": "off",
"@typescript-eslint/no-use-before-define": "off",
+2 -4
View File
@@ -11,9 +11,7 @@ export default {
this.key = match[1]
this.value = match[2]
},
render: async function (ctx: Context) {
ctx.front()[this.key] = ctx.sync
? this.liquid.evalValueSync(this.value, ctx)
: await this.liquid.evalValue(this.value, ctx)
render: function * (ctx: Context) {
ctx.front()[this.key] = yield this.liquid._evalValue(this.value, ctx)
}
} as ITagImplOptions
+2 -5
View File
@@ -14,16 +14,13 @@ export default {
})
stream.start()
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
const blocks = ctx.getRegister('blocks')
const childDefined = blocks[this.block]
const r = this.liquid.renderer
const html = childDefined !== undefined
? childDefined
: (ctx.sync
? r.renderTemplatesSync(this.tpls, ctx)
: await r.renderTemplates(this.tpls, ctx)
)
: yield r.renderTemplates(this.tpls, ctx)
if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
blocks[this.block] = html
+1 -1
View File
@@ -1,7 +1,7 @@
import { Emitter, Context, Hash } from '../../types'
export default {
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function (ctx: Context, hash: Hash, emitter: Emitter) {
emitter.break = true
}
}
+2 -4
View File
@@ -20,11 +20,9 @@ export default {
})
stream.start()
},
render: async function (ctx: Context) {
render: function * (ctx: Context) {
const r = this.liquid.renderer
const html = ctx.sync
? r.renderTemplatesSync(this.templates, ctx)
: await r.renderTemplates(this.templates, ctx)
const html = yield r.renderTemplates(this.templates, ctx)
ctx.front()[this.variable] = html
}
} as ITagImplOptions
+5 -19
View File
@@ -24,31 +24,17 @@ export default {
stream.start()
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = await new Expression(branch.val).value(ctx)
const cond = await new Expression(this.cond).value(ctx)
const val = yield new Expression(branch.val).value(ctx)
const cond = yield new Expression(this.cond).value(ctx)
if (val === cond) {
await r.renderTemplates(branch.templates, ctx, emitter)
yield r.renderTemplates(branch.templates, ctx, emitter)
return
}
}
await r.renderTemplates(this.elseTemplates, ctx, emitter)
},
renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = new Expression(branch.val).valueSync(ctx)
const cond = new Expression(this.cond).valueSync(ctx)
if (val === cond) {
r.renderTemplatesSync(branch.templates, ctx, emitter)
return
}
}
r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
}
} as ITagImplOptions
+1 -1
View File
@@ -1,7 +1,7 @@
import { Emitter, Context, Hash } from '../../types'
export default {
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function (ctx: Context, hash: Hash, emitter: Emitter) {
emitter.continue = true
}
}
+3 -7
View File
@@ -21,10 +21,8 @@ export default {
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const group = ctx.sync
? this.group.valueSync(ctx)
: await this.group.value(ctx)
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
const group = yield this.group.value(ctx)
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = ctx.getRegister('cycle')
let idx = groups[fingerprint]
@@ -36,9 +34,7 @@ export default {
const candidate = this.candidates[idx]
idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx
const html = ctx.sync
? new Expression(candidate).valueSync(ctx)
: await new Expression(candidate).value(ctx)
const html = yield new Expression(candidate).value(ctx)
emitter.write(html)
}
} as ITagImplOptions
+4 -10
View File
@@ -36,11 +36,9 @@ export default {
stream.start()
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
let collection = ctx.sync
? new Expression(this.collection).valueSync(ctx)
: await new Expression(this.collection).value(ctx)
let collection = yield new Expression(this.collection).value(ctx)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
@@ -50,9 +48,7 @@ export default {
}
}
if (!isArray(collection) || !collection.length) {
ctx.sync
? r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
: await r.renderTemplates(this.elseTemplates, ctx, emitter)
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
return
}
@@ -66,9 +62,7 @@ export default {
ctx.push(scope)
for (const item of collection) {
scope[this.variable] = item
ctx.sync
? r.renderTemplatesSync(this.templates, ctx, emitter)
: await r.renderTemplates(this.templates, ctx, emitter)
yield r.renderTemplates(this.templates, ctx, emitter)
if (emitter.break) {
emitter.break = false
break
+4 -17
View File
@@ -27,29 +27,16 @@ export default {
stream.start()
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (const branch of this.branches) {
const cond = await new Expression(branch.cond).value(ctx)
const cond = yield new Expression(branch.cond).value(ctx)
if (isTruthy(cond)) {
await r.renderTemplates(branch.templates, ctx, emitter)
yield r.renderTemplates(branch.templates, ctx, emitter)
return
}
}
await r.renderTemplates(this.elseTemplates, ctx, emitter)
},
renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (const branch of this.branches) {
const cond = new Expression(branch.cond).valueSync(ctx)
if (isTruthy(cond)) {
r.renderTemplatesSync(branch.templates, ctx, emitter)
return
}
}
r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
}
} as ITagImplOptions
+6 -36
View File
@@ -17,14 +17,14 @@ export default {
match = withRE.exec(token.args)
if (match) this.with = match[1]
},
renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
let filepath
if (ctx.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1)
filepath = this.liquid.parseAndRenderSync(template, ctx.getAll(), ctx.opts)
filepath = yield this.liquid._parseAndRender(template, ctx.getAll(), ctx.opts, ctx.sync)
} else {
filepath = new Expression(this.value).valueSync(ctx)
filepath = yield new Expression(this.value).value(ctx)
}
} else {
filepath = this.staticValue
@@ -37,41 +37,11 @@ export default {
ctx.setRegister('blocks', {})
ctx.setRegister('blockMode', BlockMode.OUTPUT)
if (this.with) {
hash[filepath] = new Expression(this.with).evaluateSync(ctx)
hash[filepath] = yield new Expression(this.with).evaluate(ctx)
}
const templates = this.liquid.parseFileSync(filepath, ctx.opts)
const templates = yield this.liquid._parseFile(filepath, ctx.opts, ctx.sync)
ctx.push(hash)
this.liquid.renderer.renderTemplatesSync(templates, ctx, emitter)
ctx.pop()
ctx.setRegister('blocks', originBlocks)
ctx.setRegister('blockMode', originBlockMode)
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
let filepath
if (ctx.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1)
filepath = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts)
} else {
filepath = await new Expression(this.value).value(ctx)
}
} else {
filepath = this.staticValue
}
assert(filepath, `cannot include with empty filename`)
const originBlocks = ctx.getRegister('blocks')
const originBlockMode = ctx.getRegister('blockMode')
ctx.setRegister('blocks', {})
ctx.setRegister('blockMode', BlockMode.OUTPUT)
if (this.with) {
hash[filepath] = await new Expression(this.with).evaluate(ctx)
}
const templates = await this.liquid.parseFile(filepath, ctx.opts)
ctx.push(hash)
await this.liquid.renderer.renderTemplates(templates, ctx, emitter)
yield this.liquid.renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
ctx.setRegister('blocks', originBlocks)
ctx.setRegister('blockMode', originBlockMode)
+5 -14
View File
@@ -19,12 +19,9 @@ export default {
this.tpls = this.liquid.parser.parse(remainTokens)
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
const layout = ctx.opts.dynamicPartials
? (ctx.sync
? new Expression(this.layout).valueSync(ctx)
: await new Expression(this.layout).value(ctx)
)
? yield new Expression(this.layout).value(ctx)
: this.staticLayout
assert(layout, `cannot apply layout with empty filename`)
@@ -32,20 +29,14 @@ export default {
ctx.setRegister('blockMode', BlockMode.STORE)
const blocks = ctx.getRegister('blocks')
const r = this.liquid.renderer
const html = ctx.sync
? r.renderTemplatesSync(this.tpls, ctx)
: await r.renderTemplates(this.tpls, ctx)
const html = yield r.renderTemplates(this.tpls, ctx)
if (blocks[''] === undefined) {
blocks[''] = html
}
const templates = ctx.sync
? this.liquid.parseFileSync(layout, ctx.opts)
: await this.liquid.parseFile(layout, ctx.opts)
const templates = yield this.liquid._parseFile(layout, ctx.opts, ctx.sync)
ctx.push(hash)
ctx.setRegister('blockMode', BlockMode.OUTPUT)
const partial = ctx.sync
? r.renderTemplatesSync(templates, ctx)
: await r.renderTemplates(templates, ctx)
const partial = yield r.renderTemplates(templates, ctx)
ctx.pop()
emitter.write(partial)
}
+3 -3
View File
@@ -1,4 +1,4 @@
import { TagToken, Token, ITagImplOptions, Context } from '../../types'
import { Hash, Emitter, TagToken, Token, ITagImplOptions, Context } from '../../types'
export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
@@ -15,7 +15,7 @@ export default {
})
stream.start()
},
render: function (ctx: Context) {
return this.tokens.map((token: Token) => token.raw).join('')
render: function (ctx: Context, hash: Hash, emitter: Emitter) {
emitter.write(this.tokens.map((token: Token) => token.raw).join(''))
}
} as ITagImplOptions
+3 -7
View File
@@ -28,10 +28,8 @@ export default {
stream.start()
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
let collection = ctx.sync
? new Expression(this.collection).valueSync(ctx) || []
: await new Expression(this.collection).value(ctx) || []
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
let collection = (yield new Expression(this.collection).value(ctx)) || []
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
@@ -50,9 +48,7 @@ export default {
emitter.write(`<tr class="row${tablerowloop.row()}">`)
}
emitter.write(`<td class="col${tablerowloop.col()}">`)
ctx.sync
? r.renderTemplatesSync(this.templates, ctx, emitter)
: await r.renderTemplates(this.templates, ctx, emitter)
yield r.renderTemplates(this.templates, ctx, emitter)
emitter.write('</td>')
}
if (collection.length) emitter.write('</tr>')
+3 -11
View File
@@ -20,18 +20,10 @@ export default {
stream.start()
},
renderSync: async function (ctx: Context, hash: Hash, emitter: Emitter) {
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
const cond = new Expression(this.cond).valueSync(ctx)
isFalsy(cond)
? r.renderTemplatesSync(this.templates, ctx, emitter)
: r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
const cond = await new Expression(this.cond).value(ctx)
await (isFalsy(cond)
const cond = yield new Expression(this.cond).value(ctx)
yield (isFalsy(cond)
? r.renderTemplates(this.templates, ctx, emitter)
: r.renderTemplates(this.elseTemplates, ctx, emitter))
}
+42 -55
View File
@@ -14,6 +14,7 @@ import builtinFilters from './builtin/filters'
import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import IFS from './fs/ifs'
import { toThenable, toValue } from './util/async'
type nullableTemplates = ITemplate[] | null
@@ -41,62 +42,47 @@ export class Liquid {
const tokens = this.tokenizer.tokenize(html, filepath)
return this.parser.parse(tokens)
}
public render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): Promise<string> {
public _render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> {
const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options)
const ctx = new Context(scope, options, sync)
return this.renderer.renderTemplates(tpl, ctx)
}
public async render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): Promise<string> {
return toThenable(this._render(tpl, scope, opts, false))
}
public renderSync (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): string {
const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options, true)
return this.renderer.renderTemplatesSync(tpl, ctx)
return toValue(this._render(tpl, scope, opts, true))
}
public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> {
const tpl = this.parse(html)
return this._render(tpl, scope, opts, sync)
}
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<string> {
const tpl = this.parse(html)
return this.render(tpl, scope, opts)
return toThenable(this._parseAndRender(html, scope, opts, false))
}
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): string {
const tpl = this.parse(html)
return this.renderSync(tpl, scope, opts)
return toValue(this._parseAndRender(html, scope, opts, true))
}
public parseFileSync (file: string, opts?: LiquidOptions): ITemplate[] {
public * _parseFile (file: string, opts?: LiquidOptions, sync?: boolean) {
const options = { ...this.options, ...normalize(opts) }
const paths = options.root.map(root => this.fs.resolve(root, file, options.extname))
for (const filepath of paths) {
const tpl = this.respectCache(filepath, () => {
if (!(this.fs.existsSync(filepath))) return null
return this.parse(this.fs.readFileSync(filepath), filepath)
})
if (tpl !== null) return tpl
if (this.options.cache && this.cache[filepath]) return this.cache[filepath]
if (!(sync ? this.fs.existsSync(filepath) : yield this.fs.exists(filepath))) continue
const tpl = this.parse(sync ? fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath)
return (this.cache[filepath] = tpl)
}
throw this.lookupError(file, options.root)
}
public async parseFile (file: string, opts?: LiquidOptions): Promise<ITemplate[]> {
const options = { ...this.options, ...normalize(opts) }
const paths = options.root.map(root => this.fs.resolve(root, file, options.extname))
for (const filepath of paths) {
const tpl = await this.respectCache(filepath, async () => {
if (!(await this.fs.exists(filepath))) return null
return this.parse(await this.fs.readFile(filepath), filepath)
})
if (tpl !== null) return tpl
}
throw this.lookupError(file, options.root)
return toThenable(this._parseFile(file, opts, false))
}
/**
* @deprecated use parseFile instead
*/
public async getTemplate (file: string, opts?: LiquidOptions): Promise<ITemplate[]> {
return this.parseFile(file, opts)
}
/**
* @deprecated use parseFileSync instead
*/
public getTemplateSync (file: string, opts?: LiquidOptions): ITemplate[] {
return this.parseFileSync(file, opts)
public parseFileSync (file: string, opts?: LiquidOptions): ITemplate[] {
return toValue(this._parseFile(file, opts, true))
}
public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
const templates = await this.parseFile(file, opts)
@@ -107,12 +93,16 @@ export class Liquid {
const templates = this.parseFileSync(file, options)
return this.renderSync(templates, ctx, opts)
}
public async evalValue (str: string, ctx: Context): Promise<any> {
return new Value(str, this.options.strictFilters).value(ctx)
}
public _evalValue (str: string, ctx: Context): IterableIterator<any> {
const value = new Value(str, this.options.strictFilters)
return value.value(ctx)
}
public async evalValue (str: string, ctx: Context): Promise<any> {
return toThenable(this._evalValue(str, ctx))
}
public evalValueSync (str: string, ctx: Context): any {
return new Value(str, this.options.strictFilters).valueSync(ctx)
return toValue(this._evalValue(str, ctx))
}
public registerFilter (name: string, filter: FilterImplOptions) {
@@ -139,19 +129,16 @@ export class Liquid {
return err
}
private setCache<T extends nullableTemplates> (filepath: string, tpl: T): T {
this.cache[filepath] = tpl
return tpl
/**
* @deprecated use parseFile instead
*/
public async getTemplate (file: string, opts?: LiquidOptions): Promise<ITemplate[]> {
return this.parseFile(file, opts)
}
private respectCache (filepath: string, resolver: () => nullableTemplates): nullableTemplates
private respectCache (filepath: string, resolver: () => Promise<nullableTemplates>): Promise<nullableTemplates>
private respectCache (filepath: string, resolver: () => nullableTemplates | Promise<nullableTemplates>): nullableTemplates | Promise<nullableTemplates> {
if (!this.options.cache) return resolver()
if (this.cache[filepath]) return this.cache[filepath]
const tpl = resolver()
const setCacheIfDefined = (tpl: nullableTemplates) => tpl === null ? null : this.setCache(filepath, tpl)
return tpl instanceof Promise ? tpl.then(setCacheIfDefined) : setCacheIfDefined(tpl)
/**
* @deprecated use parseFileSync instead
*/
public getTemplateSync (file: string, opts?: LiquidOptions): ITemplate[] {
return this.parseFileSync(file, opts)
}
}
+6 -24
View File
@@ -1,5 +1,5 @@
import { assert } from '../util/assert'
import { isRange, rangeValue, rangeValueSync } from './range'
import { isRange, rangeValue } from './range'
import { Value } from './value'
import { Context } from '../context/context'
import { toValue } from '../util/underscore'
@@ -12,38 +12,20 @@ export class Expression {
public constructor (str = '') {
this.postfix = [...toPostfix(str)]
}
public async evaluate (ctx: Context): Promise<any> {
public * evaluate (ctx: Context) {
assert(ctx, 'unable to evaluate: context not defined')
for (const token of this.postfix) {
if (isOperator(token)) {
this.evaluateOnce(token)
} else if (isRange(token)) {
this.operands.push(await rangeValue(token, ctx))
} else this.operands.push(await new Value(token).evaluate(ctx))
this.operands.push(yield rangeValue(token, ctx))
} else this.operands.push(yield new Value(token).evaluate(ctx))
}
return this.operands[0]
}
public evaluateSync (ctx: Context): any {
assert(ctx, 'unable to evaluate: context not defined')
for (const token of this.postfix) {
if (isOperator(token)) {
this.evaluateOnce(token)
} else if (isRange(token)) {
this.operands.push(rangeValueSync(token, ctx))
} else {
const val = new Value(token).evaluateSync(ctx)
this.operands.push(val)
}
}
return this.operands[0]
}
public async value (ctx: Context): Promise<any> {
return toValue(await this.evaluate(ctx))
}
public valueSync (ctx: Context): any {
return toValue(this.evaluateSync(ctx))
public * value (ctx: Context) {
return toValue(yield this.evaluate(ctx))
}
private evaluateOnce (token: string) {
const r = this.operands.pop()
+3 -12
View File
@@ -7,20 +7,11 @@ export function isRange (token: string) {
return token[0] === '(' && token[token.length - 1] === ')'
}
export async function rangeValue (token: string, ctx: Context) {
export function * rangeValue (token: string, ctx: Context) {
let match
if ((match = token.match(rangeLine))) {
const low = await new Value(match[1]).value(ctx)
const high = await new Value(match[2]).value(ctx)
return range(+low, +high + 1)
}
}
export function rangeValueSync (token: string, ctx: Context) {
let match
if ((match = token.match(rangeLine))) {
const low = new Value(match[1]).valueSync(ctx)
const high = new Value(match[2]).valueSync(ctx)
const low = yield new Value(match[1]).value(ctx)
const high = yield new Value(match[2]).value(ctx)
return range(+low, +high + 1)
}
}
+4 -15
View File
@@ -4,26 +4,15 @@ import { ITemplate } from '../template/itemplate'
import { Emitter } from './emitter'
export class Render {
public async renderTemplates (templates: ITemplate[], ctx: Context, emitter = new Emitter()): Promise<string> {
public * renderTemplates (templates: ITemplate[], ctx: Context, emitter = new Emitter()): IterableIterator<string> {
for (const tpl of templates) {
try {
const html = await tpl.render(ctx, emitter)
const html = yield tpl.render(ctx, emitter)
html && emitter.write(html)
if (emitter.break || emitter.continue) break
} catch (e) {
throw RenderError.is(e) ? e : new RenderError(e, tpl)
}
}
return emitter.html
}
public renderTemplatesSync (templates: ITemplate[], ctx: Context, emitter = new Emitter()): string {
for (const tpl of templates) {
try {
const html = tpl.renderSync(ctx, emitter)
html && !(html instanceof Promise) && emitter.write(html)
if (emitter.break || emitter.continue) break
} catch (e) {
throw RenderError.is(e) ? e : new RenderError(e, tpl)
const err = RenderError.is(e) ? e : new RenderError(e, tpl)
throw err
}
}
return emitter.html
+3 -11
View File
@@ -9,11 +9,7 @@ export class Value {
this.str = str
}
public async evaluate (ctx: Context) {
return this.evaluateSync(ctx)
}
public evaluateSync (ctx: Context) {
public evaluate (ctx: Context) {
const literalValue = parseLiteral(this.str)
if (literalValue !== undefined) {
return literalValue
@@ -21,11 +17,7 @@ export class Value {
return ctx.get(this.str)
}
public async value (ctx: Context) {
return toValue(await this.evaluate(ctx))
}
public valueSync (ctx: Context) {
return toValue(this.evaluateSync(ctx))
public value (ctx: Context) {
return toValue(this.evaluate(ctx))
}
}
+3 -11
View File
@@ -21,19 +21,11 @@ export class Filter {
this.impl = impl || (x => x)
this.args = args
}
public async render (value: any, context: Context) {
public * render (value: any, context: Context) {
const argv: any[] = []
for (const arg of this.args) {
if (isKeyValuePair(arg)) argv.push([arg[0], await new Expression(arg[1]).evaluate(context)])
else argv.push(await new Expression(arg).evaluate(context))
}
return this.impl.apply({ context }, [value, ...argv])
}
public renderSync (value: any, context: Context) {
const argv: any[] = []
for (const arg of this.args) {
if (isKeyValuePair(arg)) argv.push([arg[0], new Expression(arg[1]).evaluateSync(context)])
else argv.push(new Expression(arg).evaluateSync(context))
if (isKeyValuePair(arg)) argv.push([arg[0], yield new Expression(arg[1]).evaluate(context)])
else argv.push(yield new Expression(arg).evaluate(context))
}
return this.impl.apply({ context }, [value, ...argv])
}
+1 -4
View File
@@ -10,10 +10,7 @@ export class HTML extends Template<HTMLToken> implements ITemplate {
super(token)
this.str = token.value
}
public renderSync (ctx: Context, emitter: Emitter) {
public * render (ctx: Context, emitter: Emitter): IterableIterator<void> {
emitter.write(this.str)
}
public async render (ctx: Context, emitter: Emitter) {
this.renderSync(ctx, emitter)
}
}
+1 -2
View File
@@ -4,6 +4,5 @@ import { Emitter } from '../render/emitter'
export interface ITemplate {
token: Token;
render(ctx: Context, emitter: Emitter): Promise<any>;
renderSync(ctx: Context, emitter: Emitter): any;
render(ctx: Context, emitter: Emitter): any;
}
+2 -6
View File
@@ -12,12 +12,8 @@ export class Output extends Template<OutputToken> implements ITemplate {
super(token)
this.value = new Value(token.value, strictFilters)
}
public renderSync (ctx: Context, emitter: Emitter) {
const val = this.value.valueSync(ctx)
emitter.write(stringify(toValue(val)))
}
public async render (ctx: Context, emitter: Emitter) {
const val = await this.value.value(ctx)
public * render (ctx: Context, emitter: Emitter) {
const val = yield this.value.value(ctx)
emitter.write(stringify(toValue(val)))
}
}
+2 -9
View File
@@ -21,17 +21,10 @@ export class Hash {
}
return instance
}
public static createSync (markup: string, ctx: Context) {
public static * create (markup: string, ctx: Context) {
const instance = Hash.parse(markup)
for (const key of Object.keys(instance)) {
instance[key] = new Expression(instance[key]).evaluateSync(ctx)
}
return instance
}
public static async create (markup: string, ctx: Context) {
const instance = Hash.parse(markup)
for (const key of Object.keys(instance)) {
instance[key] = await new Expression(instance[key]).evaluate(ctx)
instance[key] = yield new Expression(instance[key]).evaluate(ctx)
}
return instance
}
+1 -2
View File
@@ -7,6 +7,5 @@ import { Emitter } from '../../render/emitter'
export interface ITagImplOptions {
parse?: (this: ITagImpl, token: TagToken, remainingTokens: Token[]) => void;
render: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => void;
renderSync?: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => void;
render: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => any;
}
+3 -9
View File
@@ -23,16 +23,10 @@ export class Tag extends Template<TagToken> implements ITemplate {
this.impl.parse(token, tokens)
}
}
public renderSync (ctx: Context, emitter: Emitter) {
const hash = Hash.createSync(this.token.args, ctx)
public * render (ctx: Context, emitter: Emitter) {
const hash = yield Hash.create(this.token.args, ctx)
const impl = this.impl
if (isFunction(impl.renderSync)) return impl.renderSync(ctx, hash, emitter)
if (isFunction(impl.render)) return impl.render(ctx, hash, emitter)
}
public async render (ctx: Context, emitter: Emitter) {
const hash = await Hash.create(this.token.args, ctx)
const impl = this.impl
if (isFunction(impl.render)) return impl.render(ctx, hash, emitter)
if (isFunction(impl.render)) return yield impl.render(ctx, hash, emitter)
}
public static register (name: string, tag: ITagImplOptions) {
Tag.impls[name] = tag
+3 -10
View File
@@ -47,17 +47,10 @@ export class Value {
}
this.filters.push(new Filter(name, args, this.strictFilters))
}
public async value (ctx: Context) {
let val = await new Expression(this.initial).evaluate(ctx)
public * value (ctx: Context) {
let val = yield new Expression(this.initial).evaluate(ctx)
for (const filter of this.filters) {
val = await filter.render(val, ctx)
}
return val
}
public valueSync (ctx: Context) {
let val = new Expression(this.initial).evaluateSync(ctx)
for (const filter of this.filters) {
val = filter.renderSync(val, ctx)
val = yield filter.render(val, ctx)
}
return val
}
+75
View File
@@ -0,0 +1,75 @@
import { isFunction } from './underscore'
type resolver = (x?: any) => Thenable
interface Thenable {
then (resolve: resolver, reject?: resolver): Thenable;
catch (reject: resolver): Thenable;
}
function mkResolve (value: any) {
const ret = {
then: (resolve: resolver) => resolve(value),
catch: () => ret
}
return ret
}
function mkReject (err: Error) {
const ret = {
then: (resolve: resolver, reject?: resolver) => {
if (reject) return reject(err)
return ret
},
catch: (reject: resolver) => reject(err)
}
return ret
}
function isThenable (val: any): val is Thenable {
return val && isFunction(val.then)
}
function isCustomIterable (val: any): val is IterableIterator<any> {
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
}
export function toThenable (val: IterableIterator<any> | Thenable): Thenable {
if (isThenable(val)) return val
if (isCustomIterable(val)) return reduce()
return mkResolve(val)
function reduce (prev?: any): Thenable {
let state
try {
state = (val as IterableIterator<any>).next(prev)
} catch (err) {
return mkReject(err)
}
if (state.done) return mkResolve(state.value)
return toThenable(state.value!).then(reduce, err => {
let state
try {
state = (val as IterableIterator<any>).throw!(err)
} catch (e) {
return mkReject(e)
}
if (state.done) return mkResolve(state.value)
return reduce(state.value)
})
}
}
export function toValue (val: IterableIterator<any> | Thenable) {
let ret: any
toThenable(val)
.then((x: any) => {
ret = x
return mkResolve(ret)
})
.catch((err: Error) => {
throw err
})
return ret
}
+1
View File
@@ -33,6 +33,7 @@ describe('tags/include', function () {
'/parent.html': '{%include%}'
})
return liquid.renderFile('/parent.html').catch(function (e) {
console.log(e)
expect(e.name).to.equal('RenderError')
expect(e.message).to.match(/cannot include with empty filename/)
})
+50
View File
@@ -77,4 +77,54 @@ describe('Liquid', function () {
.be.rejectedWith(/Failed to lookup "\/not\/exist.html" in "\/boo,\/root\/"/)
})
})
describe('#parseFile', function () {
it('should throw with lookup list when file not exist', function () {
const engine = new Liquid({
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(engine.parseFile('/not/exist.html')).to
.be.rejectedWith(/Failed to lookup "\/not\/exist.html" in "\/boo,\/root\/"/)
})
it('should throw with lookup list when file not exist', function () {
const engine = new Liquid({
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(engine.getTemplate('/not/exist.html')).to
.be.rejectedWith(/Failed to lookup "\/not\/exist.html" in "\/boo,\/root\/"/)
})
})
describe('#evalValue', function () {
it('should eval string literal', async function () {
const engine = new Liquid()
const str = await engine.evalValue('"foo"', {} as any)
expect(str).to.equal('foo')
})
})
describe('#evalValueSync', function () {
it('should eval string literal', function () {
const engine = new Liquid()
const str = engine.evalValueSync('"foo"', {} as any)
expect(str).to.equal('foo')
})
})
describe('#parseFileSync', function () {
it('should throw with lookup list when file not exist', function () {
const engine = new Liquid({
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(() => engine.parseFileSync('/not/exist.html'))
.to.throw(/Failed to lookup "\/not\/exist.html" in "\/boo,\/root\/"/)
})
it('should throw with lookup list when file not exist', function () {
const engine = new Liquid({
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(() => engine.getTemplateSync('/not/exist.html'))
.to.throw(/Failed to lookup "\/not\/exist.html" in "\/boo,\/root\/"/)
})
})
})
+29 -28
View File
@@ -1,6 +1,7 @@
import { Expression } from '../../../src/render/expression'
import { expect } from 'chai'
import { Context } from '../../../src/context/context'
import { toThenable } from '../../../src/util/async'
describe('Expression', function () {
let ctx: Context
@@ -16,57 +17,57 @@ describe('Expression', function () {
})
})
it('should throw when context not defined', async function () {
return expect(new Expression().value()).to.be.rejectedWith(/context not defined/)
it('should throw when context not defined', done => {
toThenable(new Expression().value(undefined!)).catch(err => {
expect(err.message).to.match(/context not defined/)
done()
return 0 as any
})
})
it('should eval simple expression', async function () {
expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
expect(await new Expression('2 <= 2').value(ctx)).to.equal(true)
expect(await new Expression('one <= two').value(ctx)).to.equal(true)
expect(await new Expression('x contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('x contains "X"').value(ctx)).to.equal(true)
expect(await new Expression('1 contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('y contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('z contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
expect(await new Expression('(1..5) contains 6').value(ctx)).to.equal(false)
expect(await new Expression('"<=" == "<="').value(ctx)).to.equal(true)
expect(await toThenable(new Expression('1 < 2').value(ctx))).to.equal(true)
expect(await toThenable(new Expression('1 < 2').value(ctx))).to.equal(true)
expect(await toThenable(new Expression('2 <= 2').value(ctx))).to.equal(true)
expect(await toThenable(new Expression('one <= two').value(ctx))).to.equal(true)
expect(await toThenable(new Expression('x contains "x"').value(ctx))).to.equal(false)
expect(await toThenable(new Expression('x contains "X"').value(ctx))).to.equal(true)
expect(await toThenable(new Expression('1 contains "x"').value(ctx))).to.equal(false)
expect(await toThenable(new Expression('y contains "x"').value(ctx))).to.equal(false)
expect(await toThenable(new Expression('z contains "x"').value(ctx))).to.equal(false)
expect(await toThenable(new Expression('(1..5) contains 3').value(ctx))).to.equal(true)
expect(await toThenable(new Expression('(1..5) contains 6').value(ctx))).to.equal(false)
expect(await toThenable(new Expression('"<=" == "<="').value(ctx))).to.equal(true)
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(await new Expression('false or true').value(ctx)).to.equal(true)
expect(await toThenable(new Expression('false or true').value(ctx))).to.equal(true)
})
it('should support < and contains', async function () {
expect(await new Expression('1 < 2 and x contains "x"').value(ctx)).to.equal(false)
expect(await toThenable(new Expression('1 < 2 and x contains "x"').value(ctx))).to.equal(false)
})
it('should support < or contains', async function () {
expect(await new Expression('1 < 2 or x contains "x"').value(ctx)).to.equal(true)
expect(await toThenable(new Expression('1 < 2 or x contains "x"').value(ctx))).to.equal(true)
})
it('should support value and !=', async function () {
expect(await new Expression('empty and empty != ""').value(ctx)).to.equal(false)
expect(await toThenable(new Expression('empty and empty != ""').value(ctx))).to.equal(false)
})
it('should recognize quoted value', async function () {
expect(await new Expression('">"').value(ctx)).to.equal('>')
expect(await toThenable(new Expression('">"').value(ctx))).to.equal('>')
})
it('should evaluate from right to left', async function () {
expect(await new Expression('true or false and false').value(ctx)).to.equal(true)
expect(await new Expression('true and false and false or true').value(ctx)).to.equal(false)
expect(await toThenable(new Expression('true or false and false').value(ctx))).to.equal(true)
expect(await toThenable(new Expression('true and false and false or true').value(ctx))).to.equal(false)
})
it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } })
expect(await new Expression('obj["foo"] and true').value(ctx)).to.equal(true)
expect(await toThenable(new Expression('obj["foo"] and true').value(ctx))).to.equal(true)
})
})
it('should eval range expression', async function () {
expect(await new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4])
expect(await new Expression('(two..4)').value(ctx)).to.deep.equal([2, 3, 4])
})
it('should support sync', function () {
expect(new Expression('empty and empty != ""').valueSync(ctx)).to.equal(false)
expect(await toThenable(new Expression('(2..4)').value(ctx))).to.deep.equal([2, 3, 4])
expect(await toThenable(new Expression('(two..4)').value(ctx))).to.deep.equal([2, 3, 4])
})
})
+2 -5
View File
@@ -5,6 +5,7 @@ import { Tag } from '../../../src/template/tag/tag'
import { Filter } from '../../../src/template/filter/filter'
import { Render } from '../../../src/render/render'
import { HTML } from '../../../src/template/html'
import { toThenable } from '../../../src/util/async'
describe('render', function () {
let render: Render
@@ -15,14 +16,10 @@ describe('render', function () {
})
describe('.renderTemplates()', function () {
it('should throw when scope undefined', function () {
expect(render.renderTemplates([], null as any)).to.be.rejectedWith(/scope undefined/)
})
it('should render html', async function () {
const scope = new Context()
const token = { type: 'html', value: '<p>' } as Token
const html = await render.renderTemplates([new HTML(token)], scope)
const html = await toThenable(render.renderTemplates([new HTML(token)], scope))
return expect(html).to.equal('<p>')
})
})
+5 -5
View File
@@ -5,20 +5,20 @@ import { expect } from 'chai'
describe('Value', function () {
it('should eval number variable', async function () {
const ctx = new Context({ one: 1 })
expect(new Value('one').valueSync(ctx)).to.equal(1)
expect(new Value('one').value(ctx)).to.equal(1)
})
it('question mark should be valid variable name', async function () {
const ctx = new Context({ 'has_value?': true })
expect(new Value('has_value?').valueSync(ctx)).to.equal(true)
expect(new Value('has_value?').value(ctx)).to.equal(true)
})
it('should eval string variable', async function () {
const ctx = new Context({ x: 'XXX' })
expect(new Value('x').valueSync(ctx)).to.equal('XXX')
expect(new Value('x').value(ctx)).to.equal('XXX')
})
it('should eval null literal', async function () {
expect(new Value('null').valueSync({})).to.be.null
expect(new Value('null').value({} as any)).to.be.null
})
it('should eval nil literal', async function () {
expect(new Value('nil').valueSync({})).to.be.null
expect(new Value('nil').value({} as any)).to.be.null
})
})
+10 -14
View File
@@ -3,6 +3,7 @@ import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai'
import { Filter } from '../../../../src/template/filter/filter'
import { Context } from '../../../../src/context/context'
import { toThenable } from '../../../../src/util/async'
chai.use(sinonChai)
const expect = chai.expect
@@ -19,40 +20,40 @@ describe('filter', function () {
})
it('should render input if filter not registered', async function () {
expect(await new Filter('undefined', [], false).render('foo', ctx)).to.equal('foo')
expect(await toThenable(new Filter('undefined', [], false).render('foo', ctx))).to.equal('foo')
})
it('should call filter impl with correct arguments', async function () {
const spy = sinon.spy()
Filter.register('foo', spy)
await new Filter('foo', ['33'], false).render('foo', ctx)
await toThenable(new Filter('foo', ['33'], false).render('foo', ctx))
expect(spy).to.have.been.calledWith('foo', 33)
})
it('should call filter impl with correct this arg', async function () {
const spy = sinon.spy()
Filter.register('foo', spy)
await new Filter('foo', ['33'], false).render('foo', ctx)
await toThenable(new Filter('foo', ['33'], false).render('foo', ctx))
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
})
it('should render a simple filter', async function () {
Filter.register('upcase', x => x.toUpperCase())
expect(await new Filter('upcase', [], false).render('foo', ctx)).to.equal('FOO')
expect(await toThenable(new Filter('upcase', [], false).render('foo', ctx))).to.equal('FOO')
})
it('should render filters with argument', async function () {
Filter.register('add', (a, b) => a + b)
expect(await new Filter('add', ['2'], false).render(3, ctx)).to.equal(5)
expect(await toThenable(new Filter('add', ['2'], false).render(3, ctx))).to.equal(5)
})
it('should render filters with multiple arguments', async function () {
Filter.register('add', (a, b, c) => a + b + c)
expect(await new Filter('add', ['2', '"c"'], false).render(3, ctx)).to.equal('5c')
expect(await toThenable(new Filter('add', ['2', '"c"'], false).render(3, ctx))).to.equal('5c')
})
it('should pass Objects/Drops as it is', async function () {
Filter.register('name', a => a.constructor.name)
class Foo {}
expect(await new Filter('name', [], false).render(new Foo(), ctx)).to.equal('Foo')
expect(await toThenable(new Filter('name', [], false).render(new Foo(), ctx))).to.equal('Foo')
})
it('should not throw when filter name illegal', function () {
@@ -61,13 +62,8 @@ describe('filter', function () {
}).to.not.throw()
})
it('should support sync', function () {
Filter.register('add', (a, b) => a + b)
expect(new Filter('add', ['2'], false).renderSync(3, ctx)).to.equal(5)
})
it('should support key value pairs', function () {
it('should support key value pairs', async function () {
Filter.register('add', (a, b) => b[0] + ':' + (a + b[1]))
expect(new Filter('add', [['num', '2']], false).renderSync(3, ctx)).to.equal('num:5')
expect(await toThenable((new Filter('add', [['num', '2']], false).render(3, ctx)))).to.equal('num:5')
})
})
+3 -6
View File
@@ -1,4 +1,5 @@
import * as chai from 'chai'
import { toThenable } from '../../../src/util/async'
import { Hash } from '../../../src/template/tag/hash'
import { Context } from '../../../src/context/context'
@@ -6,15 +7,11 @@ const expect = chai.expect
describe('Hash', function () {
it('should parse variable', async function () {
const hash = await Hash.create('num:foo', new Context({ foo: 3 }))
const hash = await toThenable(Hash.create('num:foo', new Context({ foo: 3 })))
expect(hash.num).to.equal(3)
})
it('should parse literals', async function () {
const hash = await Hash.create('num:3', new Context())
expect(hash.num).to.equal(3)
})
it('should support sync', function () {
const hash = Hash.createSync('num:3', new Context())
const hash = await toThenable(Hash.create('num:3', new Context()))
expect(hash.num).to.equal(3)
})
})
+6 -5
View File
@@ -1,4 +1,5 @@
import * as chai from 'chai'
import { toThenable } from '../../../src/util/async'
import { Context } from '../../../src/context/context'
import { Output } from '../../../src/template/output'
import { OutputToken } from '../../../src/parser/output-token'
@@ -7,7 +8,7 @@ import { Filter } from '../../../src/template/filter/filter'
const expect = chai.expect
describe('Output', function () {
const emitter = { write: (html: string) => (emitter.html += html), html: '' }
const emitter: any = { write: (html: string) => (emitter.html += html), html: '' }
beforeEach(function () {
Filter.clear()
emitter.html = ''
@@ -18,25 +19,25 @@ describe('Output', function () {
foo: { obj: { arr: ['a', 2] } }
})
const output = new Output({ value: 'foo' } as OutputToken, false)
await output.render(scope, emitter)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]')
})
it('should skip function property', async function () {
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ value: 'obj' } as OutputToken, false)
await output.render(scope, emitter)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ value: 'obj' } as OutputToken, false)
await output.render(scope, emitter)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ value: 'obj' } as OutputToken, false)
await output.render(scope, emitter)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO')
})
})
+8 -7
View File
@@ -5,6 +5,7 @@ import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai'
import { Liquid } from '../../../src/liquid'
import { TagToken } from '../../../src/parser/tag-token'
import { toThenable } from '../../../src/util/async'
chai.use(sinonChai)
const expect = chai.expect
@@ -12,7 +13,7 @@ const liquid = new Liquid()
describe('Tag', function () {
let ctx: Context
const emitter = { write: (html: string) => (emitter.html += html), html: '' }
const emitter: any = { write: (html: string) => (emitter.html += html), html: '' }
before(function () {
ctx = new Context({
foo: 'bar',
@@ -55,7 +56,7 @@ describe('Tag', function () {
value: 'foo',
name: 'foo'
} as TagToken
await new Tag(token, [], liquid).render(ctx, emitter)
await toThenable(new Tag(token, [], liquid).render(ctx, emitter))
expect(spy).to.have.been.called
})
@@ -74,29 +75,29 @@ describe('Tag', function () {
} as TagToken
})
it('should call tag.render with scope', async function () {
await new Tag(token, [], liquid).render(ctx, emitter)
await toThenable(new Tag(token, [], liquid).render(ctx, emitter))
expect(spy).to.have.been.calledWithMatch(ctx)
})
it('should resolve identifier hash', async function () {
await new Tag(token, [], liquid).render(ctx, emitter)
await toThenable(new Tag(token, [], liquid).render(ctx, emitter))
expect(spy).to.have.been.calledWithMatch({}, {
aa: 'bar'
})
})
it('should accept space between key/value', async function () {
await new Tag(token, [], liquid).render(ctx, emitter)
await toThenable(new Tag(token, [], liquid).render(ctx, emitter))
expect(spy).to.have.been.calledWithMatch({}, {
bb: 2
})
})
it('should resolve number value hash', async function () {
await new Tag(token, [], liquid).render(ctx, emitter)
await toThenable(new Tag(token, [], liquid).render(ctx, emitter))
expect(spy).to.have.been.calledWithMatch(ctx, {
cc: 2.3
})
})
it('should resolve property access hash', async function () {
await new Tag(token, [], liquid).render(ctx, emitter)
await toThenable(new Tag(token, [], liquid).render(ctx, emitter))
expect(spy).to.have.been.calledWithMatch(ctx, {
dd: 'uoo'
})
+2 -1
View File
@@ -1,4 +1,5 @@
import * as chai from 'chai'
import { toThenable } from '../../../src/util/async'
import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon'
import { Context } from '../../../src/context/context'
@@ -120,7 +121,7 @@ describe('Value', function () {
const scope = new Context({
foo: { bar: 'bar' }
})
await tpl.value(scope)
await toThenable(tpl.value(scope))
expect(date).to.have.been.calledWith('bar', 'b')
expect(time).to.have.been.calledWith('y', 2)
})
+106
View File
@@ -0,0 +1,106 @@
import { toThenable, toValue } from '../../../src/util/async'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
describe('utils/async', () => {
describe('#toThenable()', function () {
it('should support iterable with single return statement', async () => {
function * foo () {
return 'foo'
}
const result = await toThenable(foo())
expect(result).to.equal('foo')
})
it('should support promise', async () => {
function foo () {
return Promise.resolve('foo')
}
const result = await toThenable(foo())
expect(result).to.equal('foo')
})
it('should resolve dependency', async () => {
function * foo () {
return yield bar()
}
function * bar () {
return 'bar'
}
const result = await toThenable(foo())
expect(result).to.equal('bar')
})
it('should support promise dependency', async () => {
function * foo () {
return yield Promise.resolve('foo')
}
const result = await toThenable(foo())
expect(result).to.equal('foo')
})
it('should reject Promise if dependency throws syncly', done => {
function * foo () {
return yield bar()
}
function * bar (): IterableIterator<any> {
throw new Error('bar')
}
toThenable(foo()).catch(err => {
expect(err.message).to.equal('bar')
done()
return 0 as any
})
})
it('should resume promise after catch', async () => {
function * foo () {
let ret = ''
try {
yield bar()
} catch (e) {
ret += 'bar'
}
ret += 'foo'
return ret
}
function * bar (): IterableIterator<any> {
throw new Error('bar')
}
const ret = await toThenable(foo())
expect(ret).to.equal('barfoo')
})
})
describe('#toValue()', function () {
it('should throw Error if dependency throws syncly', () => {
function * foo () {
return yield bar()
}
function * bar (): IterableIterator<any> {
throw new Error('bar')
}
expect(() => toValue(foo())).to.throw('bar')
})
it('should resume yield after catch', () => {
function * foo () {
try {
yield bar()
} catch (e) {}
return yield 'foo'
}
function * bar (): IterableIterator<any> {
throw new Error('bar')
}
expect(toValue(foo())).to.equal('foo')
})
it('should resume return after catch', () => {
function * foo () {
try {
yield bar()
} catch (e) {}
return 'foo'
}
function * bar (): IterableIterator<any> {
throw new Error('bar')
}
expect(toValue(foo())).to.equal('foo')
})
})
})