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