feat: promise support for drops, working on #65

This commit is contained in:
Jun Yang
2019-03-10 18:05:52 +08:00
parent ad0930f152
commit 4a8088d4e4
25 changed files with 260 additions and 250 deletions
+2 -3
View File
@@ -14,10 +14,9 @@ export default {
this.key = match[1]
this.value = match[2]
},
render: function (scope: Scope) {
render: async function (scope: Scope) {
const ctx = new AssignScope()
ctx[this.key] = this.liquid.evalValue(this.value, scope)
ctx[this.key] = await this.liquid.evalValue(this.value, scope)
scope.push(ctx)
return Promise.resolve('')
}
} as ITagImplOptions
+3 -3
View File
@@ -30,11 +30,11 @@ export default {
stream.start()
},
render: function (scope: Scope) {
render: async function (scope: Scope) {
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = evalExp(branch.val, scope)
const cond = evalExp(this.cond, scope)
const val = await evalExp(branch.val, scope)
const cond = await evalExp(this.cond, scope)
if (val === cond) {
return this.liquid.renderer.renderTemplates(branch.templates, scope)
}
+2 -2
View File
@@ -24,8 +24,8 @@ export default <ITagImplOptions>{
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
},
render: function (scope: Scope) {
const group = evalValue(this.group, scope)
render: async function (scope: Scope) {
const group = await evalValue(this.group, scope)
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = scope.groups
let idx = groups[fingerprint]
+1 -1
View File
@@ -42,7 +42,7 @@ export default <ITagImplOptions>{
stream.start()
},
render: async function (scope: Scope, hash: Hash) {
let collection = evalExp(this.collection, scope)
let collection = await evalExp(this.collection, scope)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
+2 -2
View File
@@ -33,9 +33,9 @@ export default {
stream.start()
},
render: function (scope: Scope) {
render: async function (scope: Scope) {
for (const branch of this.branches) {
const cond = evalExp(branch.cond, scope)
const cond = await evalExp(branch.cond, scope)
if (isTruthy(cond)) {
return this.liquid.renderer.renderTemplates(branch.templates, scope)
}
+2 -2
View File
@@ -34,7 +34,7 @@ export default <ITagImplOptions>{
const template = this.value.slice(1, -1)
filepath = await this.liquid.parseAndRender(template, scope.getAll(), scope.opts)
} else {
filepath = evalValue(this.value, scope)
filepath = await evalValue(this.value, scope)
}
} else {
filepath = this.staticValue
@@ -47,7 +47,7 @@ export default <ITagImplOptions>{
scope.blocks = {}
scope.blockMode = BlockMode.OUTPUT
if (this.with) {
hash[filepath] = evalValue(this.with, scope)
hash[filepath] = await evalValue(this.with, scope)
}
const templates = await this.liquid.getTemplate(filepath, scope.opts)
scope.push(hash)
+1 -1
View File
@@ -26,7 +26,7 @@ export default {
},
render: async function (scope: Scope, hash: Hash) {
const layout = scope.opts.dynamicPartials
? evalValue(this.layout, scope)
? await evalValue(this.layout, scope)
: this.staticLayout
assert(layout, `cannot apply layout with empty filename`)
+1 -1
View File
@@ -36,7 +36,7 @@ export default {
},
render: async function (scope: Scope, hash: Hash) {
let collection = evalExp(this.collection, scope) || []
let collection = await evalExp(this.collection, scope) || []
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
+2 -2
View File
@@ -25,8 +25,8 @@ export default {
stream.start()
},
render: function (scope: Scope) {
const cond = evalExp(this.cond, scope)
render: async function (scope: Scope) {
const cond = await evalExp(this.cond, scope)
return isFalsy(cond)
? this.liquid.renderer.renderTemplates(this.templates, scope)
: this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
+1 -1
View File
@@ -3,7 +3,7 @@ export abstract class Drop {
return undefined
}
liquidMethodMissing (key: string): string | undefined {
liquidMethodMissing (key: string): Promise<string | undefined> | string | undefined {
return undefined
}
}
+11 -11
View File
@@ -48,7 +48,7 @@ const binaryOperators: {[key: string]: (lhs: any, rhs: any) => boolean} = {
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
}
export function parseExp (exp: string, scope: Scope): any {
export async function parseExp (exp: string, scope: Scope): Promise<any> {
assert(scope, 'unable to parseExp: scope undefined')
const operatorREs = lexical.operators
let match
@@ -56,28 +56,28 @@ export function parseExp (exp: string, scope: Scope): any {
const operatorRE = operatorREs[i]
const expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`)
if ((match = exp.match(expRE))) {
const l = parseExp(match[1], scope)
const l = await parseExp(match[1], scope)
const op = binaryOperators[match[2].trim()]
const r = parseExp(match[3], scope)
const r = await parseExp(match[3], scope)
return op(l, r)
}
}
if ((match = exp.match(lexical.rangeLine))) {
const low = evalValue(match[1], scope)
const high = evalValue(match[2], scope)
return range(low, high + 1)
const low = await evalValue(match[1], scope)
const high = await evalValue(match[2], scope)
return range(+low, +high + 1)
}
return parseValue(exp, scope)
}
export function evalExp (str: string, scope: Scope): any {
const value = parseExp(str, scope)
export async function evalExp (str: string, scope: Scope): Promise<any> {
const value = await parseExp(str, scope)
return value instanceof Drop ? value.valueOf() : value
}
function parseValue (str: string | undefined, scope: Scope): any {
async function parseValue (str: string | undefined, scope: Scope): Promise<any> {
if (!str) return null
str = str.trim()
@@ -91,8 +91,8 @@ function parseValue (str: string | undefined, scope: Scope): any {
return scope.get(str)
}
export function evalValue (str: string | undefined, scope: Scope): any {
const value = parseValue(str, scope)
export async function evalValue (str: string | undefined, scope: Scope) {
const value = await parseValue(str, scope)
return value instanceof Drop ? value.valueOf() : value
}
+10
View File
@@ -0,0 +1,10 @@
import { Drop } from '../drop/drop'
type PlainObject = {
[key: string]: any
liquid_method_missing?: (key: string) => any // eslint-disable-line
to_liquid?: () => any // eslint-disable-line
toLiquid?: () => any // eslint-disable-line
}
export type Context = PlainObject | Drop
+16 -22
View File
@@ -4,13 +4,7 @@ import { __assign } from 'tslib'
import assert from '../util/assert'
import { NormalizedFullOptions, applyDefault } from '../liquid-options'
import BlockMode from './block-mode'
export type Context = {
[key: string]: any
liquid_method_missing?: (key: string) => any // eslint-disable-line
to_liquid?: () => any // eslint-disable-line
toLiquid?: () => any // eslint-disable-line
}
import { Context } from './context'
export default class Scope {
opts: NormalizedFullOptions
@@ -25,19 +19,19 @@ export default class Scope {
getAll () {
return this.contexts.reduce((ctx, val) => __assign(ctx, val), {})
}
get (path: string): any {
const paths = this.propertyAccessSeq(path)
const scope = this.findContextFor(paths[0]) || _.last(this.contexts)
return paths.reduce((value, key) => {
const val = this.readProperty(value, key)
if (_.isNil(val) && this.opts.strictVariables) {
throw new TypeError(`undefined variable: ${key}`)
async get (path: string) {
const paths = await this.propertyAccessSeq(path)
let ctx = this.findContextFor(paths[0]) || _.last(this.contexts)
for (let path of paths) {
ctx = this.readProperty(ctx, path)
if (_.isNil(ctx) && this.opts.strictVariables) {
throw new TypeError(`undefined variable: ${path}`)
}
return val
}, scope)
}
return ctx
}
set (path: string, v: any): void {
const paths = this.propertyAccessSeq(path)
async set (path: string, v: any) {
const paths = await this.propertyAccessSeq(path)
let scope = this.findContextFor(paths[0]) || _.last(this.contexts)
paths.some((key, i) => {
if (!_.isObject(scope)) {
@@ -99,7 +93,7 @@ export default class Scope {
* accessSeq("foo['b]r']") // ['foo', 'b]r']
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/
propertyAccessSeq (str: string) {
async propertyAccessSeq (str: string) {
str = String(str)
const seq: string[] = []
let name = ''
@@ -122,7 +116,7 @@ export default class Scope {
assert(j !== -1, `unbalanced []: ${str}`)
name = str.slice(i + 1, j)
if (!/^[+-]?\d+$/.test(name)) { // foo[bar] vs. foo[1]
name = String(this.get(name))
name = String(await this.get(name))
}
push()
i = j + 1
@@ -152,9 +146,9 @@ export default class Scope {
}
function readSize (obj: Context) {
if (!_.isNil(obj.size)) return obj.size
if (!_.isNil(obj['size'])) return obj['size']
if (_.isArray(obj) || _.isString(obj)) return obj.length
return obj.size
return obj['size']
}
function matchRightBracket (str: string, begin: number) {
+7 -3
View File
@@ -19,9 +19,13 @@ export class Filter {
this.impl = impl || (x => x)
this.args = args
}
render (value: any, scope: Scope): any {
const args = this.args.map(arg => isArray(arg) ? [arg[0], evalValue(arg[1], scope)] : evalValue(arg, scope))
return this.impl.apply(null, [value, ...args])
async render (value: any, scope: Scope) {
const argv: any[] = []
for(let arg of this.args) {
if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], scope)])
else argv.push(await evalValue(arg, scope))
}
return this.impl.apply(null, [value, ...argv])
}
static register (name: string, filter: FilterImpl) {
Filter.impls[name] = filter
+4 -2
View File
@@ -10,13 +10,15 @@ import Scope from '../../scope/scope'
*/
export default class Hash {
[key: string]: any
constructor (markup: string, scope: Scope) {
static async create (markup: string, scope: Scope) {
const instance = new Hash()
let match
hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) {
const k = match[1]
const v = match[2]
this[k] = evalValue(v, scope)
instance[k] = await evalValue(v, scope)
}
return instance
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ export default class Tag extends Template<TagToken> implements ITemplate {
}
}
async render (scope: Scope) {
const hash = new Hash(this.token.args, scope)
const hash = await Hash.create(this.token.args, scope)
const impl = this.impl
if (typeof impl.render !== 'function') {
return ''
+6 -4
View File
@@ -47,10 +47,12 @@ export default class Value {
}
this.filters.push(new Filter(name, args, this.strictFilters))
}
value (scope: Scope) {
return this.filters.reduce(
(prev, filter) => filter.render(prev, scope),
evalExp(this.initial, scope))
async value (scope: Scope) {
let val = await evalExp(this.initial, scope)
for (let filter of this.filters) {
val = await filter.render(val, scope)
}
return val
}
static tokenize (str: string): Array<'|' | ',' | ':' | string> {
const tokens = []
+2 -2
View File
@@ -5,7 +5,7 @@ describe('.evalValue()', function () {
var engine: Liquid
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', function () {
expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/scope undefined/)
it('should throw when scope undefined', async function () {
return expect(engine.evalValue('{{"foo"}}', null as any)).to.be.rejectedWith(/scope undefined/)
})
})
+2 -2
View File
@@ -33,9 +33,9 @@ describe('filters/array', function () {
' | split: ", " %}{{ my_array | size }}',
'4')
})
it('should also be used with dot notation - string',
it('should be respected with <string>.size notation',
() => test('{% assign my_string = "Ground control to Major Tom." %}{{ my_string.size }}', '28'))
it('should also be used with dot notation - array',
it('should be respected with <array>.size notation',
() => test('{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.size }}', '4'))
})
describe('slice', function () {
+1 -1
View File
@@ -1,7 +1,7 @@
import Liquid from '../../../../src/liquid'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
import { Context } from '../../../../src/scope/scope'
import { Context } from '../../../../src/scope/context'
use(chaiAsPromised)
+23 -2
View File
@@ -8,7 +8,7 @@ describe('drop/drop', function () {
class CustomDrop extends Liquid.Types.Drop {
name: string = 'NAME'
getName () {
return 'GETNAME'
return 'GET NAME'
}
}
class CustomDropWithMethodMissing extends CustomDrop {
@@ -16,9 +16,18 @@ describe('drop/drop', function () {
return key.toUpperCase()
}
}
class PromiseDrop extends Liquid.Types.Drop {
name = Promise.resolve('NAME')
async getName () {
return 'GET NAME'
}
async liquidMethodMissing (key: string) {
return key.toUpperCase()
}
}
it('should call corresponding method', async function () {
const html = await liquid.parseAndRender(`{{obj.getName}}`, { obj: new CustomDrop() })
expect(html).to.equal('GETNAME')
expect(html).to.equal('GET NAME')
})
it('should read corresponding property', async function () {
const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new CustomDrop() })
@@ -32,4 +41,16 @@ describe('drop/drop', function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new CustomDropWithMethodMissing() })
expect(html).to.equal('FOO')
})
it('should call corresponding promise method', async function () {
const html = await liquid.parseAndRender(`{{obj.getName}}`, { obj: new PromiseDrop() })
expect(html).to.equal('GET NAME')
})
it('should read corresponding promise property', async function () {
const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new PromiseDrop() })
expect(html).to.equal('NAME')
})
it('should support promise returned by liquidMethodMissing', async function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() })
expect(html).to.equal('FOO')
})
})
+46 -48
View File
@@ -18,35 +18,35 @@ describe('render/syntax', function () {
})
describe('.evalValue()', function () {
it('should eval boolean literal', function () {
expect(evalValue('true', scope)).to.equal(true)
expect(evalValue('TrUE', scope)).to.equal(undefined)
expect(evalValue('false', scope)).to.equal(false)
it('should eval boolean literal', async function () {
expect(await evalValue('true', scope)).to.equal(true)
expect(await evalValue('TrUE', scope)).to.equal(undefined)
expect(await evalValue('false', scope)).to.equal(false)
})
it('should eval number literal', function () {
expect(evalValue('2.3', scope)).to.equal(2.3)
expect(evalValue('.32', scope)).to.equal(0.32)
expect(evalValue('-23.', scope)).to.equal(-23)
expect(evalValue('23', scope)).to.equal(23)
it('should eval number literal', async function () {
expect(await evalValue('2.3', scope)).to.equal(2.3)
expect(await evalValue('.32', scope)).to.equal(0.32)
expect(await evalValue('-23.', scope)).to.equal(-23)
expect(await evalValue('23', scope)).to.equal(23)
})
it('should eval string literal', function () {
expect(evalValue('"ab\'c"', scope)).to.equal("ab'c")
expect(evalValue("'ab\"c'", scope)).to.equal('ab"c')
it('should eval string literal', async function () {
expect(await evalValue('"ab\'c"', scope)).to.equal("ab'c")
expect(await evalValue("'ab\"c'", scope)).to.equal('ab"c')
})
it('should eval nil literal', function () {
expect(evalValue('nil', scope)).to.be.null
it('should eval nil literal', async function () {
expect(await evalValue('nil', scope)).to.be.null
})
it('should eval null literal', function () {
expect(evalValue('null', scope)).to.be.null
it('should eval null literal', async function () {
expect(await evalValue('null', scope)).to.be.null
})
it('should eval scope variables', function () {
expect(evalValue('one', scope)).to.equal(1)
expect(evalValue('has_value?', scope)).to.equal(true)
expect(evalValue('x', scope)).to.equal('XXX')
it('should eval scope variables', async function () {
expect(await evalValue('one', scope)).to.equal(1)
expect(await evalValue('has_value?', scope)).to.equal(true)
expect(await evalValue('x', scope)).to.equal('XXX')
})
})
describe('.isTruthy()', function () {
describe('.isTruthy()', async function () {
// Spec: https://shopify.github.io/liquid/basics/truthy-and-falsy/
expect(isTruthy(true)).to.be.true
expect(isTruthy(false)).to.be.false
@@ -61,44 +61,42 @@ describe('render/syntax', function () {
})
describe('.evalExp()', function () {
it('should throw when scope undefined', function () {
expect(function () {
(evalExp as any)('')
}).to.throw(/scope undefined/)
it('should throw when scope undefined', async function () {
return expect((evalExp as any)('')).to.be.rejectedWith(/scope undefined/)
})
it('should eval simple expression', function () {
expect(evalExp('1<2', scope)).to.equal(true)
expect(evalExp('2<=2', scope)).to.equal(true)
expect(evalExp('one<=two', scope)).to.equal(true)
expect(evalExp('x contains "x"', scope)).to.equal(false)
expect(evalExp('x contains "X"', scope)).to.equal(true)
expect(evalExp('1 contains "x"', scope)).to.equal(false)
expect(evalExp('y contains "x"', scope)).to.equal(false)
expect(evalExp('z contains "x"', scope)).to.equal(false)
expect(evalExp('(1..5) contains 3', scope)).to.equal(true)
expect(evalExp('(1..5) contains 6', scope)).to.equal(false)
expect(evalExp('"<=" == "<="', scope)).to.equal(true)
it('should eval simple expression', async function () {
expect(await evalExp('1<2', scope)).to.equal(true)
expect(await evalExp('2<=2', scope)).to.equal(true)
expect(await evalExp('one<=two', scope)).to.equal(true)
expect(await evalExp('x contains "x"', scope)).to.equal(false)
expect(await evalExp('x contains "X"', scope)).to.equal(true)
expect(await evalExp('1 contains "x"', scope)).to.equal(false)
expect(await evalExp('y contains "x"', scope)).to.equal(false)
expect(await evalExp('z contains "x"', scope)).to.equal(false)
expect(await evalExp('(1..5) contains 3', scope)).to.equal(true)
expect(await evalExp('(1..5) contains 6', scope)).to.equal(false)
expect(await evalExp('"<=" == "<="', scope)).to.equal(true)
})
describe('complex expression', function () {
it('should support value or value', function () {
expect(evalExp('false or true', scope)).to.equal(true)
it('should support value or value', async function () {
expect(await evalExp('false or true', scope)).to.equal(true)
})
it('should support < and contains', function () {
expect(evalExp('1<2 and x contains "x"', scope)).to.equal(false)
it('should support < and contains', async function () {
expect(await evalExp('1<2 and x contains "x"', scope)).to.equal(false)
})
it('should support < or contains', function () {
expect(evalExp('1<2 or x contains "x"', scope)).to.equal(true)
it('should support < or contains', async function () {
expect(await evalExp('1<2 or x contains "x"', scope)).to.equal(true)
})
it('should support value and !=', function () {
expect(evalExp('empty and empty != ""', scope)).to.equal(false)
it('should support value and !=', async function () {
expect(await evalExp('empty and empty != ""', scope)).to.equal(false)
})
})
it('should eval range expression', function () {
expect(evalExp('(2..4)', scope)).to.deep.equal([2, 3, 4])
expect(evalExp('(two..4)', scope)).to.deep.equal([2, 3, 4])
it('should eval range expression', async function () {
expect(await evalExp('(2..4)', scope)).to.deep.equal([2, 3, 4])
expect(await evalExp('(two..4)', scope)).to.deep.equal([2, 3, 4])
})
})
})
+101 -121
View File
@@ -1,5 +1,6 @@
import * as chai from 'chai'
import Scope, { Context } from '../../../src/scope/scope'
import Scope from '../../../src/scope/scope'
import { Context } from '../../../src/scope/context'
const expect = chai.expect
@@ -20,168 +21,156 @@ describe('scope', function () {
})
describe('#propertyAccessSeq()', function () {
it('should handle dot syntax', function () {
expect(scope.propertyAccessSeq('foo.bar'))
it('should handle dot syntax', async function () {
expect(await scope.propertyAccessSeq('foo.bar'))
.to.deep.equal(['foo', 'bar'])
})
it('should handle [<String>] syntax', function () {
expect(scope.propertyAccessSeq('foo["bar"]'))
it('should handle [<String>] syntax', async function () {
expect(await scope.propertyAccessSeq('foo["bar"]'))
.to.deep.equal(['foo', 'bar'])
})
it('should handle [<Identifier>] syntax', function () {
expect(scope.propertyAccessSeq('foo[foo]'))
it('should handle [<Identifier>] syntax', async function () {
expect(await scope.propertyAccessSeq('foo[foo]'))
.to.deep.equal(['foo', 'zoo'])
})
it('should handle nested access 1', function () {
expect(scope.propertyAccessSeq('foo[bar.zoo]'))
it('should handle nested access 1', async function () {
expect(await scope.propertyAccessSeq('foo[bar.zoo]'))
.to.deep.equal(['foo', 'coo'])
})
it('should handle nested access 2', function () {
expect(scope.propertyAccessSeq('foo[bar["zoo"]]'))
it('should handle nested access 2', async function () {
expect(await scope.propertyAccessSeq('foo[bar["zoo"]]'))
.to.deep.equal(['foo', 'coo'])
})
it('should handle nested access 3', function () {
expect(scope.propertyAccessSeq('bar["foo"].zoo'))
it('should handle nested access 3', async function () {
expect(await scope.propertyAccessSeq('bar["foo"].zoo'))
.to.deep.equal(['bar', 'foo', 'zoo'])
})
it('should handle nested access 4', function () {
expect(scope.propertyAccessSeq('foo[0].bar'))
it('should handle nested access 4', async function () {
expect(await scope.propertyAccessSeq('foo[0].bar'))
.to.deep.equal(['foo', '0', 'bar'])
})
it('should handle nested access 5', function () {
expect(scope.propertyAccessSeq('foo[one].bar'))
it('should handle nested access 5', async function () {
expect(await scope.propertyAccessSeq('foo[one].bar'))
.to.deep.equal(['foo', '1', 'bar'])
})
it('should handle nested access 6', function () {
expect(scope.propertyAccessSeq('foo[two].bar'))
it('should handle nested access 6', async function () {
expect(await scope.propertyAccessSeq('foo[two].bar'))
.to.deep.equal(['foo', 'undefined', 'bar'])
})
})
describe('#get()', function () {
it('should get direct property', function () {
expect(scope.get('foo')).equal('zoo')
it('should get direct property', async function () {
expect(await await scope.get('foo')).equal('zoo')
})
it('undefined property should yield undefined', function () {
function fn () {
scope.get('notdefined')
}
expect(fn).to.not.throw()
expect(scope.get('notdefined')).to.equal(undefined)
expect(scope.get(false as any)).to.equal(undefined)
it('undefined property should yield undefined', async function () {
expect(scope.get('notdefined')).to.be.rejected
expect(await scope.get('notdefined')).to.equal(undefined)
expect(await scope.get(false as any)).to.equal(undefined)
})
it('should throw for invalid path', function () {
function fn () {
scope.get('')
}
expect(fn).to.throw('invalid path:""')
it('should throw for invalid path', async function () {
expect(scope.get('')).to.be.rejectedWith('invalid path:""')
})
it('should throw when [] unbalanced', function () {
expect(function () {
scope.get('foo[bar')
}).to.throw(/unbalanced \[\]/)
it('should throw when [] unbalanced', async function () {
expect(scope.get('foo[bar')).to.be.rejectedWith(/unbalanced \[\]/)
})
it('should throw when "" unbalanced', function () {
expect(function () {
scope.get('foo["bar]')
}).to.throw(/unbalanced "/)
it('should throw when "" unbalanced', async function () {
expect(scope.get('foo["bar]')).to.be.rejectedWith(/unbalanced "/)
})
it("should throw when '' unbalanced", function () {
expect(function () {
scope.get("foo['bar]")
}).to.throw(/unbalanced '/)
it("should throw when '' unbalanced", async function () {
expect(scope.get("foo['bar]")).to.be.rejectedWith(/unbalanced '/)
})
it('should respect to to_liquid', function () {
it('should respect to to_liquid', async function () {
const scope = new Scope({ foo: {
to_liquid: () => ({ bar: 'BAR' }),
bar: 'bar'
} })
expect(scope.get('foo.bar')).to.equal('BAR')
expect(await scope.get('foo.bar')).to.equal('BAR')
})
it('should respect to toLiquid', function () {
it('should respect to toLiquid', async function () {
const scope = new Scope({ foo: {
toLiquid: () => ({ bar: 'BAR' }),
bar: 'bar'
} })
expect(scope.get('foo.bar')).to.equal('BAR')
expect(await scope.get('foo.bar')).to.equal('BAR')
})
it('should access child property via dot syntax', function () {
expect(scope.get('bar.zoo')).to.equal('coo')
expect(scope.get('bar.arr')).to.deep.equal(['a', 'b'])
it('should access child property via dot syntax', async function () {
expect(await scope.get('bar.zoo')).to.equal('coo')
expect(await scope.get('bar.arr')).to.deep.equal(['a', 'b'])
})
it('should access child property via [<String>] syntax', function () {
expect(scope.get('bar["zoo"]')).to.equal('coo')
it('should access child property via [<String>] syntax', async function () {
expect(await scope.get('bar["zoo"]')).to.equal('coo')
})
it('should access child property via [<Number>] syntax', function () {
expect(scope.get('bar.arr[0]')).to.equal('a')
it('should access child property via [<Number>] syntax', async function () {
expect(await scope.get('bar.arr[0]')).to.equal('a')
})
it('should access child property via [<Identifier>] syntax', function () {
expect(scope.get('bar[foo]')).to.equal('coo')
it('should access child property via [<Identifier>] syntax', async function () {
expect(await scope.get('bar[foo]')).to.equal('coo')
})
it('should return undefined when not exist', function () {
expect(scope.get('foo.foo.foo')).to.be.undefined
it('should return undefined when not exist', async function () {
expect(await scope.get('foo.foo.foo')).to.be.undefined
})
it('should return string length as size', function () {
expect(scope.get('foo.size')).to.equal(3)
it('should return string length as size', async function () {
expect(await scope.get('foo.size')).to.equal(3)
})
it('should return array length as size', function () {
expect(scope.get('bar.arr.size')).to.equal(2)
it('should return array length as size', async function () {
expect(await scope.get('bar.arr.size')).to.equal(2)
})
it('should return size property if exists', function () {
expect(scope.get('zoo.size')).to.equal(4)
it('should return size property if exists', async function () {
expect(await scope.get('zoo.size')).to.equal(4)
})
it('should return undefined if do not have size and length', function () {
expect(scope.get('one.size')).to.equal(undefined)
it('should return undefined if do not have size and length', async function () {
expect(await scope.get('one.size')).to.equal(undefined)
})
})
describe('#set', function () {
it('should set nested value', function () {
scope.set('posts', {
it('should set nested value', async function () {
await scope.set('posts', {
'first': {
'name': 'A Nice Day'
}
})
scope.set('category', {
await scope.set('category', {
'diary': ['first']
})
expect(scope.get('posts[category.diary[0]].name'), 'A Nice Day')
expect(await scope.get('posts[category.diary[0]].name'), 'A Nice Day')
})
it('should create parent if needed', function () {
scope.set('a.b.c.d', 'COO')
expect(scope.get('a.b.c.d')).to.equal('COO')
it('should create parent if needed', async function () {
await scope.set('a.b.c.d', 'COO')
expect(await scope.get('a.b.c.d')).to.equal('COO')
})
it('should keep other properties of parent', function () {
it('should keep other properties of parent', async function () {
scope.push({ obj: { foo: 'FOO' } })
scope.set('obj.bar', 'BAR')
expect(scope.get('obj.foo')).to.equal('FOO')
await scope.set('obj.bar', 'BAR')
expect(await scope.get('obj.foo')).to.equal('FOO')
})
it('should abort if property cannot be set', function () {
it('should abort if property cannot be set', async function () {
scope.push({ obj: { foo: 'FOO' } })
scope.set('obj.foo.bar', 'BAR')
expect(scope.get('obj.foo')).to.equal('FOO')
await scope.set('obj.foo.bar', 'BAR')
expect(await scope.get('obj.foo')).to.equal('FOO')
})
it("should set parents' corresponding value", function () {
it("should set parents' corresponding value", async function () {
scope.push({})
scope.set('foo', 'bar')
await scope.set('foo', 'bar')
scope.pop()
expect(scope.get('foo')).to.equal('bar')
expect(await scope.get('foo')).to.equal('bar')
})
})
describe('strictVariables', function () {
describe('strictVariables', async function () {
let scope: Scope
beforeEach(function () {
scope = new Scope(ctx, {
@@ -189,66 +178,57 @@ describe('scope', function () {
} as any)
})
it('should throw when variable not defined', function () {
function fn () {
scope.get('notdefined')
}
expect(fn).to.throw(/undefined variable: notdefined/)
return expect(scope.get('notdefined')).to.be.rejectedWith(/undefined variable: notdefined/)
})
it('should throw when deep variable not exist', function () {
scope.set('foo', 'FOO')
function fn () {
scope.get('foo.bar.not.defined')
}
expect(fn).to.throw(/undefined variable: bar/)
it('should throw when deep variable not exist', async function () {
await scope.set('foo', 'FOO')
return expect(scope.get('foo.bar.not.defined')).to.be.rejectedWith(/undefined variable: bar/)
})
it('should throw when itself not defined', function () {
scope.set('foo', 'bar')
function fn () {
scope.get('foo.BAR')
}
expect(fn).to.throw(/undefined variable: BAR/)
it('should throw when itself not defined', async function () {
await scope.set('foo', 'bar')
return expect(scope.get('foo.BAR')).to.be.rejectedWith(/undefined variable: BAR/)
})
it('should find variable in parent scope', function () {
scope.set('foo', 'foo')
it('should find variable in parent scope', async function () {
await scope.set('foo', 'foo')
scope.push({
'bar': 'bar'
})
expect(scope.get('foo')).to.equal('foo')
expect(await scope.get('foo')).to.equal('foo')
})
})
describe('.getAll()', function () {
it('should get all properties when arguments empty', function () {
expect(scope.getAll()).deep.equal(ctx)
it('should get all properties when arguments empty', async function () {
expect(await scope.getAll()).deep.equal(ctx)
})
})
describe('.push()', function () {
it('should push scope', function () {
scope.set('bar', 'bar')
it('should push scope', async function () {
await scope.set('bar', 'bar')
scope.push({
foo: 'foo'
})
expect(scope.get('foo')).to.equal('foo')
expect(scope.get('bar')).to.equal('bar')
expect(await scope.get('foo')).to.equal('foo')
expect(await scope.get('bar')).to.equal('bar')
})
it('should hide deep properties by push', function () {
scope.set('bar', { bar: 'bar' })
it('should hide deep properties by push', async function () {
await scope.set('bar', { bar: 'bar' })
scope.push({ bar: { foo: 'foo' } })
expect(scope.get('bar.foo')).to.equal('foo')
expect(scope.get('bar.bar')).to.equal(undefined)
expect(await scope.get('bar.foo')).to.equal('foo')
expect(await scope.get('bar.bar')).to.equal(undefined)
})
})
describe('.pop()', function () {
it('should pop scope', function () {
it('should pop scope', async function () {
scope.push({
foo: 'foo'
})
scope.pop()
expect(scope.get('foo')).to.equal('zoo')
expect(await scope.get('foo')).to.equal('zoo')
})
})
it('should pop specified scope', function () {
it('should pop specified scope', async function () {
const scope1 = {
foo: 'foo'
}
@@ -257,11 +237,11 @@ describe('scope', function () {
}
scope.push(scope1)
scope.push(scope2)
expect(scope.get('foo')).to.equal('foo')
expect(scope.get('bar')).to.equal('bar')
expect(await scope.get('foo')).to.equal('foo')
expect(await scope.get('bar')).to.equal('bar')
scope.pop(scope1)
expect(scope.get('foo')).to.equal('zoo')
expect(scope.get('bar')).to.equal('bar')
expect(await scope.get('foo')).to.equal('zoo')
expect(await scope.get('bar')).to.equal('bar')
})
it('should throw when specified scope not found', function () {
const scope1 = {
+11 -11
View File
@@ -13,34 +13,34 @@ describe('filter', function () {
Filter.clear()
scope = new Scope()
})
it('should create default filter if not registered', function () {
it('should create default filter if not registered', async function () {
const result = new Filter('foo', [], false)
expect(result.name).to.equal('foo')
})
it('should render input if filter not registered', function () {
expect(new Filter('undefined', [], false).render('foo', scope)).to.equal('foo')
it('should render input if filter not registered', async function () {
expect(await new Filter('undefined', [], false).render('foo', scope)).to.equal('foo')
})
it('should call filter impl with corrct arguments', function () {
it('should call filter impl with corrct arguments', async function () {
const spy = sinon.spy()
Filter.register('foo', spy)
new Filter('foo', ['33'], false).render('foo', scope)
await new Filter('foo', ['33'], false).render('foo', scope)
expect(spy).to.have.been.calledWith('foo', 33)
})
it('should render a simple filter', function () {
it('should render a simple filter', async function () {
Filter.register('upcase', x => x.toUpperCase())
expect(new Filter('upcase', [], false).render('foo', scope)).to.equal('FOO')
expect(await new Filter('upcase', [], false).render('foo', scope)).to.equal('FOO')
})
it('should render filters with argument', function () {
it('should render filters with argument', async function () {
Filter.register('add', (a, b) => a + b)
expect(new Filter('add', ['2'], false).render(3, scope)).to.equal(5)
expect(await new Filter('add', ['2'], false).render(3, scope)).to.equal(5)
})
it('should render filters with multiple arguments', function () {
it('should render filters with multiple arguments', async function () {
Filter.register('add', (a, b, c) => a + b + c)
expect(new Filter('add', ['2', '"c"'], false).render(3, scope)).to.equal('5c')
expect(await new Filter('add', ['2', '"c"'], false).render(3, scope)).to.equal('5c')
})
it('should not throw when filter name illegal', function () {
+2 -2
View File
@@ -99,7 +99,7 @@ describe('Value', function () {
})
describe('#value()', function () {
it('should call chained filters correctly', function () {
it('should call chained filters correctly', async function () {
const date = sinon.stub().returns('y')
const time = sinon.spy()
Filter.register('date', date)
@@ -108,7 +108,7 @@ describe('Value', function () {
const scope = new Scope({
foo: { bar: 'bar' }
})
tpl.value(scope)
await tpl.value(scope)
expect(date).to.have.been.calledWith('bar', 'b')
expect(time).to.have.been.calledWith('y', 2)
})