fix: stack overflow on large number of templates, #513

This commit is contained in:
Harttle
2022-07-08 01:46:51 +08:00
parent 2f87708989
commit 3dc4290b56
16 changed files with 172 additions and 180 deletions
+1
View File
@@ -55,6 +55,7 @@
<td align="center"><a href="https://github.com/ameyaapte1"><img src="https://avatars.githubusercontent.com/u/16054747?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="https://github.com/tbdrz"><img src="https://avatars.githubusercontent.com/u/50599116?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="http://santialbo.com"><img src="https://avatars.githubusercontent.com/u/1557563?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="https://github.com/YahangWu"><img src="https://avatars.githubusercontent.com/u/12295975?v=4?s=100" width="100px;" alt=""/></a></td>
</tr>
</table>
+4
View File
@@ -1,5 +1,9 @@
import type { Template } from '../template/template'
export interface Cache<T> {
write (key: string, value: T): void | Promise<void>;
read (key: string): T | undefined | Promise<T | undefined>;
remove (key: string): void | Promise<void>;
}
export type LiquidCache = Cache<Template[] | Promise<Template[]>>
+5 -7
View File
@@ -1,12 +1,10 @@
import { snakeCase, forOwn, isArray, isString, isFunction } from './util/underscore'
import { Template } from './template/template'
import { Cache } from './cache/cache'
import { LiquidCache } from './cache/cache'
import { LRU } from './cache/lru'
import { FS } from './fs/fs'
import * as fs from './fs/node'
import { defaultOperators, Operators } from './render/operator'
import { createTrie, Trie } from './util/operator-trie'
import { Thenable } from './util/async'
import * as builtinFilters from './builtin/filters'
import { assert, FilterImplOptions } from './types'
@@ -32,7 +30,7 @@ export interface LiquidOptions {
/** Add a extname (if filepath doesn't include one) before template file lookup. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */
extname?: string;
/** Whether or not to cache resolved templates. Defaults to `false`. */
cache?: boolean | number | Cache<Thenable<Template[]>>;
cache?: boolean | number | LiquidCache;
/** Use Javascript Truthiness. Defaults to `false`. */
jsTruthy?: boolean;
/** If set, treat the `filepath` parameter in `{%include filepath %}` and `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
@@ -104,7 +102,7 @@ interface NormalizedOptions extends LiquidOptions {
root?: string[];
partials?: string[];
layouts?: string[];
cache?: Cache<Thenable<Template[]>>;
cache?: LiquidCache;
outputEscape?: OutputEscape;
operatorsTrie?: Trie;
}
@@ -116,7 +114,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
relativeReference: boolean;
jekyllInclude: boolean;
extname: string;
cache: undefined | Cache<Thenable<Template[]>>;
cache?: LiquidCache;
jsTruthy: boolean;
dynamicPartials: boolean;
fs: FS;
@@ -180,7 +178,7 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
if (!options.hasOwnProperty('layouts')) options.layouts = options.root
}
if (options.hasOwnProperty('cache')) {
let cache: Cache<Thenable<Template[]>> | undefined
let cache: LiquidCache | undefined
if (typeof options.cache === 'number') cache = options.cache > 0 ? new LRU(options.cache) : undefined
else if (typeof options.cache === 'object') cache = options.cache
else cache = options.cache ? new LRU(1024) : undefined
+13 -16
View File
@@ -8,17 +8,17 @@ import { Output } from '../template/output'
import { HTML } from '../template/html'
import { Template } from '../template/template'
import { TopLevelToken } from '../tokens/toplevel-token'
import { Cache } from '../cache/cache'
import { LiquidCache } from '../cache/cache'
import { Loader, LookupType } from '../fs/loader'
import { toPromise } from '../util/async'
import { FS } from '../fs/fs'
import { toThenable, Thenable } from '../util/async'
export default class Parser {
public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Generator<unknown, Template[], Template[] | string>
private liquid: Liquid
private fs: FS
private cache: Cache<Thenable<Template[]>> | undefined
private cache?: LiquidCache
private loader: Loader
public constructor (liquid: Liquid) {
@@ -58,21 +58,18 @@ export default class Parser {
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
}
private * _parseFileCached (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], Template[]> {
const key = this.loader.shouldLoadRelative(file)
? currentFile + ',' + file
: type + ':' + file
const tpls = yield this.cache!.read(key)
const cache = this.cache!
const key = this.loader.shouldLoadRelative(file) ? currentFile + ',' + file : type + ':' + file
const tpls = yield cache.read(key)
if (tpls) return tpls
const task = toThenable(this._parseFile(file, sync, type, currentFile))
this.cache!.write(key, task)
try {
return yield task
} catch (e) {
// remove cached task if failed
this.cache!.remove(key)
}
return []
const task = this._parseFile(file, sync, type, currentFile)
// sync mode: exec the task and cache the result
// async mode: cache the task before exec
const taskOrTpl = sync ? yield task : toPromise(task)
cache.write(key, taskOrTpl as any)
// note: concurrent tasks will be reused, cache for failed task is removed until its end
try { return yield taskOrTpl } catch (err) { cache.remove(key); throw err }
}
private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], string> {
const filepath = yield this.loader.lookup(file, type, sync, currentFile)
+2 -2
View File
@@ -4,13 +4,13 @@ import { Template } from '../template/template'
import { Emitter } from '../emitters/emitter'
import { SimpleEmitter } from '../emitters/simple-emitter'
import { StreamedEmitter } from '../emitters/streamed-emitter'
import { toThenable } from '../util/async'
import { toPromise } from '../util/async'
import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter'
export class Render {
public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream {
const emitter = new StreamedEmitter()
Promise.resolve().then(() => toThenable(this.renderTemplates(templates, ctx, emitter)))
Promise.resolve().then(() => toPromise(this.renderTemplates(templates, ctx, emitter)))
.then(() => emitter.end(), err => emitter.error(err))
return emitter.stream
}
+38 -56
View File
@@ -7,25 +7,6 @@ export interface Thenable<T> {
catch (reject: resolver): Thenable<T>;
}
function createResolvedThenable<T> (value: T): Thenable<T> {
const ret = {
then: (resolve: resolver) => resolve(value),
catch: () => ret
}
return ret
}
function createRejectedThenable<T> (err: Error): Thenable<T> {
const ret = {
then: (resolve: resolver, reject?: resolver) => {
if (reject) return reject(err)
return ret
},
catch: (reject: resolver) => reject(err)
}
return ret
}
function isThenable<T> (val: any): val is Thenable<T> {
return val && isFunction(val.then)
}
@@ -34,48 +15,49 @@ function isAsyncIterator (val: any): val is IterableIterator<any> {
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
}
// convert an async iterator to a thenable (Promise compatible)
export function toThenable<T> (val: IteratorResult<unknown, T> | Thenable<T> | any): Thenable<T> {
if (isThenable(val)) return val
if (isAsyncIterator(val)) return reduce()
return createResolvedThenable(val)
function reduce<T> (prev?: T): Thenable<T> {
let state
// convert an async iterator to a Promise
export async function toPromise<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): Promise<T> {
if (!isAsyncIterator(val)) return val
let value: unknown
let done = false
let next = 'next'
do {
const state = val[next](value)
done = state.done
value = state.value
next = 'next'
try {
state = val.next(prev)
if (isAsyncIterator(value)) value = toPromise(value)
if (isThenable(value)) value = await value
} catch (err) {
return createRejectedThenable(err as Error)
next = 'throw'
value = err
}
} while (!done)
return value as T
}
if (state.done) return createResolvedThenable(state.value)
return toThenable(state.value!).then(reduce, err => {
let state
// convert an async iterator to a value in a synchronous maner
export function toValue<T> (val: Generator<unknown, T, unknown> | T): T {
if (!isAsyncIterator(val)) return val
let value: any
let done = false
let next = 'next'
do {
const state = val[next](value)
done = state.done
value = state.value
next = 'next'
if (isAsyncIterator(value)) {
try {
state = val.throw!(err)
} catch (e) {
return createRejectedThenable(e as Error)
value = toValue(value)
} catch (err) {
next = 'throw'
value = err
}
if (state.done) return createResolvedThenable(state.value)
return reduce(state.value)
})
}
}
} while (!done)
return value
}
export function toPromise<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): Promise<T> {
return Promise.resolve(toThenable(val))
}
// get the value of async iterator in synchronous manner
export function toValue<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): T {
let ret: T
toThenable(val)
.then((x: any) => {
ret = x
return createResolvedThenable(ret)
})
.catch((err: Error) => {
throw err
})
return ret!
}
export const toThenable = toPromise
+10
View File
@@ -246,4 +246,14 @@ describe('Issues', function () {
const html = await engine.parseAndRender(`{% if template contains "product" %}contains{%endif%}`, ctx)
expect(html).to.equal('contains')
})
it('#513 should support large number of templates [async]', async () => {
const engine = new Liquid()
const html = await engine.parseAndRender(`{% for i in (1..10000) %}{{ i }}{% endfor %}`)
expect(html).to.have.lengthOf(38894)
})
it('#513 should support large number of templates [sync]', () => {
const engine = new Liquid()
const html = engine.parseAndRenderSync(`{% for i in (1..10000) %}{{ i }}{% endfor %}`)
expect(html).to.have.lengthOf(38894)
})
})
+5 -3
View File
@@ -126,11 +126,13 @@ describe('LiquidOptions#cache', function () {
extname: '.html',
cache: true
})
try { await engine.renderFile('foo') } catch (err) {}
try {
await engine.renderFile('foo')
} catch (err) {}
mock({ '/root/foo.html': 'foo' })
const y = await engine.renderFile('foo')
expect(y).to.equal('foo')
const html = await engine.renderFile('foo')
expect(html).to.equal('foo')
})
})
+43 -43
View File
@@ -2,7 +2,7 @@ import { Tokenizer } from '../../../src/parser/tokenizer'
import { expect } from 'chai'
import { Drop } from '../../../src/drop/drop'
import { Context } from '../../../src/context/context'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import { defaultOperators } from '../../../src/render/operator'
import { createTrie } from '../../../src/util/operator-trie'
@@ -12,7 +12,7 @@ describe('Expression', function () {
const create = (str: string) => new Tokenizer(str, trie).readExpression()
it('should throw when context not defined', done => {
toThenable(create('foo').evaluate(undefined!, false))
toPromise(create('foo').evaluate(undefined!, false))
.then(() => done(new Error('should not resolved')))
.catch(err => {
expect(err.message).to.match(/context not defined/)
@@ -22,19 +22,19 @@ describe('Expression', function () {
describe('single value', function () {
it('should eval literal', async function () {
expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toPromise(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toPromise(create('false').evaluate(ctx, false))).to.equal(false)
})
it('should eval range expression', async function () {
const ctx = new Context({ two: 2 })
expect(await toThenable(create('(2..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
expect(await toThenable(create('(two..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
expect(await toPromise(create('(2..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
expect(await toPromise(create('(two..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
})
it('should eval literal', async function () {
expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toPromise(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toPromise(create('false').evaluate(ctx, false))).to.equal(false)
})
it('should eval property access', async function () {
@@ -43,119 +43,119 @@ describe('Expression', function () {
coo: 'bar',
doo: { foo: 'bar', bar: { foo: 'bar' } }
})
expect(await toThenable(create('foo.bar').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[doo.foo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[doo["foo"]]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar')
expect(await toPromise(create('foo.bar').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('foo[doo.foo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('foo[doo["foo"]]').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar')
})
})
describe('simple expression', function () {
it('should return false for "1==2"', async () => {
expect(await toThenable(create('1==2').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('1==2').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "1<2"', async () => {
expect(await toThenable(create('1<2').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('1<2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "1 < 2"', async () => {
expect(await toThenable(create('1 < 2').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('1 < 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "1 < 2"', async () => {
expect(await toThenable(create('1 < 2').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('1 < 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "2 <= 2"', async () => {
expect(await toThenable(create('2 <= 2').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('2 <= 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "one <= two"', async () => {
const ctx = new Context({ one: 1, two: 2 })
expect(await toThenable(create('one <= two').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('one <= two').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "x contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('x contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('x contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "x contains "X""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('x contains "X"').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('x contains "X"').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "1 contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('1 contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('1 contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return false for "y contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('y contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('y contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return false for "z contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('z contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('z contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "(1..5) contains 3"', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('(1..5) contains 3').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('(1..5) contains 3').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "(1..5) contains 6"', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('(1..5) contains 6').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('(1..5) contains 6').evaluate(ctx, false))).to.equal(false)
})
it('should return true for ""<=" == "<=""', async () => {
expect(await toThenable(create('"<=" == "<="').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('"<=" == "<="').evaluate(ctx, false))).to.equal(true)
})
})
it('should allow space in quoted value', async function () {
const ctx = new Context({ space: ' ' })
expect(await toThenable(create('" " == space').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('" " == space').evaluate(ctx, false))).to.equal(true)
})
describe('escape', () => {
it('should escape quote', async function () {
const ctx = new Context({ quote: '"' })
expect(await toThenable(create('"\\"" == quote').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('"\\"" == quote').evaluate(ctx, false))).to.equal(true)
})
it('should escape square bracket', async function () {
const ctx = new Context({ obj: { ']': 'bracket' } })
expect(await toThenable(create('obj["]"] == "bracket"').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('obj["]"] == "bracket"').evaluate(ctx, false))).to.equal(true)
})
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(await toThenable(create('false or true').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('false or true').evaluate(ctx, false))).to.equal(true)
})
it('should support < and contains', async function () {
expect(await toThenable(create('1 < 2 and x contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('1 < 2 and x contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should support < or contains', async function () {
expect(await toThenable(create('1 < 2 or x contains "x"').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('1 < 2 or x contains "x"').evaluate(ctx, false))).to.equal(true)
})
it('should support Drops for "x contains "x""', async () => {
class TemplateDrop extends Drop {
valueOf () { return 'X' }
}
const ctx = new Context({ x: 'XXX', X: new TemplateDrop() })
expect(await toThenable(create('x contains X').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('x contains X').evaluate(ctx, false))).to.equal(true)
})
it('should support value and !=', async function () {
const ctx = new Context({ empty: '' })
expect(await toThenable(create('empty and empty != ""').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('empty and empty != ""').evaluate(ctx, false))).to.equal(false)
})
it('should recognize quoted value', async function () {
expect(await toThenable(create('">"').evaluate(ctx, false))).to.equal('>')
expect(await toPromise(create('">"').evaluate(ctx, false))).to.equal('>')
})
it('should evaluate from right to left', async function () {
expect(await toThenable(create('true or false and false').evaluate(ctx, false))).to.equal(true)
expect(await toThenable(create('true and false and false or true').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('true or false and false').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('true and false and false or true').evaluate(ctx, false))).to.equal(false)
})
it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } })
expect(await toThenable(create('obj["foo"] and true').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('obj["foo"] and true').evaluate(ctx, false))).to.equal(true)
})
it('should allow nested property access', async function () {
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
expect(await toThenable(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).to.equal('FOO')
expect(await toPromise(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).to.equal('FOO')
})
})
})
+2 -2
View File
@@ -4,7 +4,7 @@ import { HTMLToken } from '../../../src/tokens/html-token'
import { Render } from '../../../src/render/render'
import { HTML } from '../../../src/template/html'
import { SimpleEmitter } from '../../../src/emitters/simple-emitter'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import { Tag } from '../../../src/template/tag/tag'
import { TagToken } from '../../../src/types'
@@ -18,7 +18,7 @@ describe('render', function () {
it('should render html', async function () {
const scope = new Context()
const token = { getContent: () => '<p>' } as HTMLToken
const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter()))
const html = await toPromise(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter()))
return expect(html).to.equal('<p>')
})
})
+9 -9
View File
@@ -2,7 +2,7 @@ import * as chai from 'chai'
import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai'
import { Context } from '../../../../src/context/context'
import { toThenable } from '../../../../src/util/async'
import { toPromise } from '../../../../src/util/async'
import { NumberToken } from '../../../../src/tokens/number-token'
import { QuotedToken } from '../../../../src/tokens/quoted-token'
import { IdentifierToken } from '../../../../src/tokens/identifier-token'
@@ -25,46 +25,46 @@ describe('filter', function () {
})
it('should render input if filter not registered', async function () {
expect(await toThenable(filters.create('undefined', []).render('foo', ctx))).to.equal('foo')
expect(await toPromise(filters.create('undefined', []).render('foo', ctx))).to.equal('foo')
})
it('should call filter impl with correct arguments', async function () {
const spy = sinon.spy()
filters.set('foo', spy)
const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined)
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
await toPromise(filters.create('foo', [thirty]).render('foo', ctx))
expect(spy).to.have.been.calledWith('foo', 30)
})
it('should call filter impl with correct this', async function () {
const spy = sinon.spy()
filters.set('foo', spy)
const thirty = new NumberToken(new IdentifierToken('33', 0, 2), undefined)
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
await toPromise(filters.create('foo', [thirty]).render('foo', ctx))
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
expect(spy).to.have.been.calledOn(sinon.match.has('liquid', liquid))
})
it('should render a simple filter', async function () {
filters.set('upcase', x => x.toUpperCase())
expect(await toThenable(filters.create('upcase', []).render('foo', ctx))).to.equal('FOO')
expect(await toPromise(filters.create('upcase', []).render('foo', ctx))).to.equal('FOO')
})
it('should render filters with argument', async function () {
filters.set('add', (a, b) => a + b)
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toThenable(filters.create('add', [two]).render(3, ctx))).to.equal(5)
expect(await toPromise(filters.create('add', [two]).render(3, ctx))).to.equal(5)
})
it('should render filters with multiple arguments', async function () {
filters.set('add', (a, b, c) => a + b + c)
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
const c = new QuotedToken('"c"', 0, 3)
expect(await toThenable(filters.create('add', [two, c]).render(3, ctx))).to.equal('5c')
expect(await toPromise(filters.create('add', [two, c]).render(3, ctx))).to.equal('5c')
})
it('should pass Objects/Drops as it is', async function () {
filters.set('name', a => a.constructor.name)
class Foo {}
expect(await toThenable(filters.create('name', []).render(new Foo(), ctx))).to.equal('Foo')
expect(await toPromise(filters.create('name', []).render(new Foo(), ctx))).to.equal('Foo')
})
it('should not throw when filter name illegal', function () {
@@ -76,6 +76,6 @@ describe('filter', function () {
it('should support key value pairs', async function () {
filters.set('add', (a, b) => b[0] + ':' + (a + b[1]))
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toThenable((filters.create('add', [['num', two]]).render(3, ctx)))).to.equal('num:5')
expect(await toPromise((filters.create('add', [['num', two]]).render(3, ctx)))).to.equal('num:5')
})
})
+8 -8
View File
@@ -1,5 +1,5 @@
import * as chai from 'chai'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import { Hash } from '../../../src/template/tag/hash'
import { Context } from '../../../src/context/context'
@@ -7,34 +7,34 @@ const expect = chai.expect
describe('Hash', function () {
it('should parse "reverse"', async function () {
const hash = await toThenable(new Hash('reverse').render(new Context({ foo: 3 })))
const hash = await toPromise(new Hash('reverse').render(new Context({ foo: 3 })))
expect(hash).to.haveOwnProperty('reverse')
expect(hash.reverse).to.be.true
})
it('should parse "num:foo"', async function () {
const hash = await toThenable(new Hash('num:foo').render(new Context({ foo: 3 })))
const hash = await toPromise(new Hash('num:foo').render(new Context({ foo: 3 })))
expect(hash.num).to.equal(3)
})
it('should parse "num:3"', async function () {
const hash = await toThenable(new Hash('num:3').render(new Context()))
const hash = await toPromise(new Hash('num:3').render(new Context()))
expect(hash.num).to.equal(3)
})
it('should parse "num: arr[0]"', async function () {
const hash = await toThenable(new Hash('num:3').render(new Context({ arr: [3] })))
const hash = await toPromise(new Hash('num:3').render(new Context({ arr: [3] })))
expect(hash.num).to.equal(3)
})
it('should parse "num: 2.3"', async function () {
const hash = await toThenable(new Hash('num:2.3').render(new Context()))
const hash = await toPromise(new Hash('num:2.3').render(new Context()))
expect(hash.num).to.equal(2.3)
})
it('should parse "num:bar.coo"', async function () {
const pending = new Hash('num:bar.coo').render(new Context({ bar: { coo: 3 } }))
const hash = await toThenable(pending)
const hash = await toPromise(pending)
expect(hash.num).to.equal(3)
})
it('should parse "num1:2.3 reverse,num2:bar.coo\n num3: arr[0]"', async function () {
const ctx = new Context({ bar: { coo: 3 }, arr: [4] })
const hash = await toThenable(new Hash('num1:2.3 reverse,num2:bar.coo\n num3: arr[0]').render(ctx))
const hash = await toPromise(new Hash('num1:2.3 reverse,num2:bar.coo\n num3: arr[0]').render(ctx))
expect(hash).to.deep.equal({
num1: 2.3,
reverse: true,
+9 -9
View File
@@ -1,5 +1,5 @@
import * as chai from 'chai'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import { Context } from '../../../src/context/context'
import { Output } from '../../../src/template/output'
import { OutputToken } from '../../../src/tokens/output-token'
@@ -21,25 +21,25 @@ describe('Output', function () {
foo: { obj: { arr: ['a', 2] } }
})
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(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({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(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({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(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({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO')
})
context('when keepOutputType is enabled', () => {
@@ -62,7 +62,7 @@ describe('Output', function () {
foo: 42
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal(42)
})
it('should respect output variable boolean type', async () => {
@@ -70,7 +70,7 @@ describe('Output', function () {
foo: true
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal(true)
})
it('should respect output variable object type', async () => {
@@ -78,7 +78,7 @@ describe('Output', function () {
foo: 'test'
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('test')
})
it('should respect output variable string type', async () => {
@@ -86,7 +86,7 @@ describe('Output', function () {
foo: { a: { b: 42 } }
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.deep.equal({ a: { b: 42 } })
})
})
+2 -2
View File
@@ -4,7 +4,7 @@ import { Context } from '../../../src/context/context'
import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai'
import { TagToken } from '../../../src/tokens/tag-token'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
chai.use(sinonChai)
const expect = chai.expect
@@ -20,7 +20,7 @@ describe('Tag', function () {
args: '',
name: 'foo'
} as TagToken
await toThenable(new Tag(token, [], {
await toPromise(new Tag(token, [], {
tags: {
get: () => ({ render: spy })
}
+2 -2
View File
@@ -1,7 +1,7 @@
import * as chai from 'chai'
import { Liquid } from '../../../src/liquid'
import { QuotedToken } from '../../../src/tokens/quoted-token'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon'
import { Context } from '../../../src/context/context'
@@ -36,7 +36,7 @@ describe('Value', function () {
const scope = new Context({
foo: { bar: 'bar' }
})
await toThenable(tpl.value(scope, false))
await toPromise(tpl.value(scope, false))
expect(date).to.have.been.calledWith('bar', 'b')
expect(time).to.have.been.calledWith('y', 2)
})
+19 -21
View File
@@ -1,4 +1,4 @@
import { toThenable, toPromise, toValue } from '../../../src/util/async'
import { toPromise, toValue } from '../../../src/util/async'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
@@ -13,47 +13,45 @@ describe('utils/async', () => {
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
})
describe('#toThenable()', function () {
it('should support iterable with single return statement', async () => {
function * foo () {
return 'foo'
}
const result = await toThenable(foo())
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
it('should support promise', async () => {
function foo () {
return Promise.resolve('foo')
}
const result = await toThenable(foo())
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
it('should resolve dependency', async () => {
function * foo () {
function * foo (): Generator<Generator<string>> {
return yield bar()
}
function * bar () {
function * bar (): Generator<string> {
return 'bar'
}
const result = await toThenable(foo())
const result = await toPromise(foo())
expect(result).to.equal('bar')
})
it('should support promise dependency', async () => {
function * foo () {
function * foo (): Generator<Promise<string>> {
return yield Promise.resolve('foo')
}
const result = await toThenable(foo())
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
it('should reject Promise if dependency throws syncly', done => {
function * foo () {
function * foo (): Generator<Generator<never>> {
return yield bar()
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
toThenable(foo()).catch(err => {
toPromise(foo()).catch(err => {
expect(err.message).to.equal('bar')
done()
return 0 as any
@@ -70,43 +68,43 @@ describe('utils/async', () => {
ret += 'foo'
return ret
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
const ret = await toThenable(foo())
const ret = await toPromise(foo())
expect(ret).to.equal('barfoo')
})
})
describe('#toValue()', function () {
it('should throw Error if dependency throws syncly', () => {
function * foo () {
function * foo (): Generator<Generator<never>> {
return yield bar()
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(() => toValue(foo())).to.throw('bar')
})
it('should resume yield after catch', () => {
function * foo () {
function * foo (): Generator<unknown, never, never> {
try {
yield bar()
} catch (e) {}
return yield 'foo'
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(toValue(foo())).to.equal('foo')
})
it('should resume return after catch', () => {
function * foo () {
function * foo (): Generator<Generator<never>, string> {
try {
yield bar()
} catch (e) {}
return 'foo'
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(toValue(foo())).to.equal('foo')