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
+1 -1
View File
@@ -6,6 +6,6 @@ describe('.evalValue()', function () {
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () {
return expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/scope undefined/)
return expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/context not defined/)
})
})
+8 -3
View File
@@ -40,17 +40,17 @@ describe('tags/if', function () {
})
describe('expression as condition', function () {
it('should support ==', async function () {
const src = '{% if 2==3 %}yes{%else%}no{%endif%}'
const src = '{% if 2 == 3 %}yes{%else%}no{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal('no')
})
it('should support >=', async function () {
const src = '{% if 1>=2 and one<two %}a{%endif%}'
const src = '{% if 1 >= 2 and one<two %}a{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal('')
})
it('should support !=', async function () {
const src = '{% if one!=two %}yes{%else%}no{%endif%}'
const src = '{% if one != two %}yes{%else%}no{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal('yes')
})
@@ -60,6 +60,11 @@ describe('tags/if', function () {
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal('XY')
})
it('should evaluate right to left', async function () {
const src = `{% if false and false or true %}true{%endif%}`
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
})
describe('comparasion to null', function () {
it('should evaluate false for null < 10', async function () {
+4 -4
View File
@@ -20,17 +20,17 @@ describe('tags/unless', function () {
return expect(html).to.equal('yes')
})
it('should reject when tag not closed', function () {
const src = '{% unless 1>2 %}yes'
const src = '{% unless 1 > 2 %}yes'
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/tag {% unless 1>2 %} not closed/)
.to.be.rejectedWith(/tag {% unless 1 > 2 %} not closed/)
})
it('should render unless when predicate yields false and else undefined', async function () {
const src = '{% unless 1>2 %}yes{%endunless%}'
const src = '{% unless 1 > 2 %}yes{%endunless%}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('yes')
})
it('should render "" when predicate yields false and else undefined', async function () {
const src = '{% unless 1<2 %}yes{%endunless%}'
const src = '{% unless 1 < 2 %}yes{%endunless%}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
+27
View File
@@ -0,0 +1,27 @@
import { expect } from 'chai'
import { parseLiteral } from '../../../src/parser/literal'
import { NullDrop } from '../../../src/drop/null-drop'
describe('parseLiteral', function () {
it('should eval boolean literal', async function () {
expect(parseLiteral('true')).to.equal(true)
expect(parseLiteral('TrUE')).to.equal(undefined)
expect(parseLiteral('false')).to.equal(false)
})
it('should eval number literal', async function () {
expect(parseLiteral('2.3')).to.equal(2.3)
expect(parseLiteral('.32')).to.equal(0.32)
expect(parseLiteral('-23.')).to.equal(-23)
expect(parseLiteral('23')).to.equal(23)
})
it('should eval string literal', async function () {
expect(parseLiteral('"ab\'c"')).to.equal("ab'c")
expect(parseLiteral("'ab\"c'")).to.equal('ab"c')
})
it('should eval nil literal', async function () {
expect(parseLiteral('nil')).to.be.instanceOf(NullDrop)
})
it('should eval null literal', async function () {
expect(parseLiteral('null')).to.be.instanceOf(NullDrop)
})
})
+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
})
})