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