refactor: rewrite expression evaluation, fix #130

This commit is contained in:
harttle
2019-08-26 10:15:43 -05:00
committed by Jun Yang
parent 538610e169
commit 76019e9e18
31 changed files with 407 additions and 246 deletions
+38
View File
@@ -0,0 +1,38 @@
import { isTruthy } from '../../../src/render/boolean'
import { expect } from 'chai'
describe('boolean', async function () {
describe('.isTruthy()', async function () {
// Spec: https://shopify.github.io/liquid/basics/truthy-and-falsy/
it('true is truthy', function () {
expect(isTruthy(true)).to.be.true
})
it('false is falsy', function () {
expect(isTruthy(false)).to.be.false
})
it('null is falsy', function () {
expect(isTruthy(null)).to.be.false
})
it('"foo" is truthy', function () {
expect(isTruthy('foo')).to.be.true
})
it('"" is truthy', function () {
expect(isTruthy('')).to.be.true
})
it('0 is truthy', function () {
expect(isTruthy(0)).to.be.true
})
it('1 is truthy', function () {
expect(isTruthy(1)).to.be.true
})
it('1.1 is truthy', function () {
expect(isTruthy(1.1)).to.be.true
})
it('[1] is truthy', function () {
expect(isTruthy([1])).to.be.true
})
it('[] is truthy', function () {
expect(isTruthy([])).to.be.true
})
})
})
+67
View File
@@ -0,0 +1,67 @@
import { Expression } from '../../../src/render/expression'
import { expect } from 'chai'
import { Context } from '../../../src/context/context'
describe('Expression', function () {
let ctx: Context
beforeEach(function () {
ctx = new Context({
one: 1,
two: 2,
empty: '',
x: 'XXX',
y: undefined,
z: null
})
})
it('should throw when context not defined', async function () {
return expect(() => new Expression().value()).to.throw(/context not defined/)
})
it('should eval simple expression', async function () {
expect(new Expression('1 < 2').value(ctx)).to.equal(true)
expect(new Expression('2 <= 2').value(ctx)).to.equal(true)
expect(new Expression('one <= two').value(ctx)).to.equal(true)
expect(new Expression('x contains "x"').value(ctx)).to.equal(false)
expect(new Expression('x contains "X"').value(ctx)).to.equal(true)
expect(new Expression('1 contains "x"').value(ctx)).to.equal(false)
expect(new Expression('y contains "x"').value(ctx)).to.equal(false)
expect(new Expression('z contains "x"').value(ctx)).to.equal(false)
expect(new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
expect(new Expression('(1..5) contains 6').value(ctx)).to.equal(false)
expect(new Expression('"<=" == "<="').value(ctx)).to.equal(true)
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(new Expression('false or true').value(ctx)).to.equal(true)
})
it('should support < and contains', async function () {
expect(new Expression('1 < 2 and x contains "x"').value(ctx)).to.equal(false)
})
it('should support < or contains', async function () {
expect(new Expression('1 < 2 or x contains "x"').value(ctx)).to.equal(true)
})
it('should support value and !=', async function () {
expect(new Expression('empty and empty != ""').value(ctx)).to.equal(false)
})
it('should recognize quoted value', async function () {
expect(new Expression('">"').value(ctx)).to.equal('>')
})
it('should evaluate from right to left', function () {
expect(new Expression('true or false and false').value(ctx)).to.equal(true)
expect(new Expression('true and false and false or true').value(ctx)).to.equal(false)
})
it('should recognize property access', function () {
const ctx = new Context({ obj: { foo: true } })
expect(new Expression('obj["foo"] and true').value(ctx)).to.equal(true)
})
})
it('should eval range expression', async function () {
expect(new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4])
expect(new Expression('(two..4)').value(ctx)).to.deep.equal([2, 3, 4])
})
})
-102
View File
@@ -1,102 +0,0 @@
import { Context } from '../../../src/context/context'
import { expect } from 'chai'
import { evalExp, evalValue, isTruthy } from '../../../src/render/syntax'
describe('render/syntax', function () {
let ctx: Context
beforeEach(function () {
ctx = new Context({
one: 1,
two: 2,
empty: '',
x: 'XXX',
y: undefined,
z: null,
'has_value?': true
})
})
describe('.evalValue()', function () {
it('should eval boolean literal', async function () {
expect(await evalValue('true', ctx)).to.equal(true)
expect(await evalValue('TrUE', ctx)).to.equal(undefined)
expect(await evalValue('false', ctx)).to.equal(false)
})
it('should eval number literal', async function () {
expect(await evalValue('2.3', ctx)).to.equal(2.3)
expect(await evalValue('.32', ctx)).to.equal(0.32)
expect(await evalValue('-23.', ctx)).to.equal(-23)
expect(await evalValue('23', ctx)).to.equal(23)
})
it('should eval string literal', async function () {
expect(await evalValue('"ab\'c"', ctx)).to.equal("ab'c")
expect(await evalValue("'ab\"c'", ctx)).to.equal('ab"c')
})
it('should eval nil literal', async function () {
expect(await evalValue('nil', ctx)).to.be.null
})
it('should eval null literal', async function () {
expect(await evalValue('null', ctx)).to.be.null
})
it('should eval scope variables', async function () {
expect(await evalValue('one', ctx)).to.equal(1)
expect(await evalValue('has_value?', ctx)).to.equal(true)
expect(await evalValue('x', ctx)).to.equal('XXX')
})
})
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
expect(isTruthy(null)).to.be.false
expect(isTruthy('foo')).to.be.true
expect(isTruthy('')).to.be.true
expect(isTruthy(0)).to.be.true
expect(isTruthy(1)).to.be.true
expect(isTruthy(1.1)).to.be.true
expect(isTruthy([1])).to.be.true
expect(isTruthy([])).to.be.true
})
describe('.evalExp()', function () {
it('should throw when scope undefined', async function () {
return expect(() => (evalExp as any)('')).to.throw(/scope undefined/)
})
it('should eval simple expression', async function () {
expect(await evalExp('1<2', ctx)).to.equal(true)
expect(await evalExp('2<=2', ctx)).to.equal(true)
expect(await evalExp('one<=two', ctx)).to.equal(true)
expect(await evalExp('x contains "x"', ctx)).to.equal(false)
expect(await evalExp('x contains "X"', ctx)).to.equal(true)
expect(await evalExp('1 contains "x"', ctx)).to.equal(false)
expect(await evalExp('y contains "x"', ctx)).to.equal(false)
expect(await evalExp('z contains "x"', ctx)).to.equal(false)
expect(await evalExp('(1..5) contains 3', ctx)).to.equal(true)
expect(await evalExp('(1..5) contains 6', ctx)).to.equal(false)
expect(await evalExp('"<=" == "<="', ctx)).to.equal(true)
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(await evalExp('false or true', ctx)).to.equal(true)
})
it('should support < and contains', async function () {
expect(await evalExp('1<2 and x contains "x"', ctx)).to.equal(false)
})
it('should support < or contains', async function () {
expect(await evalExp('1<2 or x contains "x"', ctx)).to.equal(true)
})
it('should support value and !=', async function () {
expect(await evalExp('empty and empty != ""', ctx)).to.equal(false)
})
})
it('should eval range expression', async function () {
expect(await evalExp('(2..4)', ctx)).to.deep.equal([2, 3, 4])
expect(await evalExp('(two..4)', ctx)).to.deep.equal([2, 3, 4])
})
})
})
+24
View File
@@ -0,0 +1,24 @@
import { Value } from '../../../src/render/value'
import { Context } from '../../../src/context/context'
import { expect } from 'chai'
describe('Value', function () {
it('should eval number variable', async function () {
const ctx = new Context({ one: 1 })
expect(new Value('one').value(ctx)).to.equal(1)
})
it('question mark should be valid variable name', async function () {
const ctx = new Context({ 'has_value?': true })
expect(new Value('has_value?').value(ctx)).to.equal(true)
})
it('should eval string variable', async function () {
const ctx = new Context({ x: 'XXX' })
expect(new Value('x').value(ctx)).to.equal('XXX')
})
it('should eval null literal', async function () {
expect(new Value('null').value({})).to.be.null
})
it('should eval nil literal', async function () {
expect(new Value('nil').value({})).to.be.null
})
})