feat: support filters in if/unless/case, see #287

This commit is contained in:
harttle
2021-02-12 22:22:41 +08:00
parent 4e82da689e
commit 2f059f6a25
18 changed files with 164 additions and 176 deletions
+17 -18
View File
@@ -227,7 +227,6 @@ describe('Tokenizer', function () {
const tokenizer = new Tokenizer(html, trie)
const token = tokenizer.readOutputToken()
console.log(token)
expect(token).instanceOf(OutputToken)
expect(token.content).to.equal('"%} {%" | append: "}} {{"')
})
@@ -299,7 +298,7 @@ describe('Tokenizer', function () {
const pa: PropertyAccessToken = token!.args[0] as any
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
expect(pa.variable.content).to.equal('arr')
expect((pa.variable as any).content).to.equal('arr')
expect(pa.props).to.have.lengthOf(1)
expect(pa.props[0]).to.be.instanceOf(NumberToken)
expect(pa.props[0].getText()).to.equal('0')
@@ -312,7 +311,7 @@ describe('Tokenizer', function () {
const pa: PropertyAccessToken = token!.args[0] as any
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
expect(pa.variable.content).to.equal('obj')
expect((pa.variable as any).content).to.equal('obj')
expect(pa.props).to.have.lengthOf(1)
expect(pa.props[0]).to.be.instanceOf(IdentifierToken)
expect(pa.props[0].getText()).to.equal('foo')
@@ -326,7 +325,7 @@ describe('Tokenizer', function () {
const pa: PropertyAccessToken = token!.args[0] as any
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
expect(pa.getText()).to.equal('obj["good luck"]')
expect(pa.variable.content).to.equal('obj')
expect((pa.variable as any).content).to.equal('obj')
expect(pa.props[0].getText()).to.equal('"good luck"')
})
})
@@ -368,19 +367,19 @@ describe('Tokenizer', function () {
})
describe('#readExpression()', () => {
it('should read expression `a `', () => {
const exp = [...new Tokenizer('a ', trie).readExpression()]
const exp = [...new Tokenizer('a ', trie).readExpressionTokens()]
expect(exp).to.have.lengthOf(1)
expect(exp[0]).to.be.instanceOf(PropertyAccessToken)
expect(exp[0].getText()).to.deep.equal('a')
})
it('should read expression `a[][b]`', () => {
const exp = [...new Tokenizer('a[][b]', trie).readExpression()]
const exp = [...new Tokenizer('a[][b]', trie).readExpressionTokens()]
expect(exp).to.have.lengthOf(1)
const pa = exp[0] as PropertyAccessToken
expect(pa).to.be.instanceOf(PropertyAccessToken)
expect(pa.variable.content).to.deep.equal('a')
expect((pa.variable as any).content).to.deep.equal('a')
expect(pa.props).to.have.lengthOf(2)
const [p1, p2] = pa.props
@@ -390,23 +389,23 @@ describe('Tokenizer', function () {
expect(p2.getText()).to.equal('b')
})
it('should read expression `a.`', () => {
const exp = [...new Tokenizer('a.', trie).readExpression()]
const exp = [...new Tokenizer('a.', trie).readExpressionTokens()]
expect(exp).to.have.lengthOf(1)
const pa = exp[0] as PropertyAccessToken
expect(pa).to.be.instanceOf(PropertyAccessToken)
expect(pa.variable.content).to.deep.equal('a')
expect((pa.variable as any).content).to.deep.equal('a')
expect(pa.props).to.have.lengthOf(0)
})
it('should read expression `a ==`', () => {
const exp = [...new Tokenizer('a ==', trie).readExpression()]
const exp = [...new Tokenizer('a ==', trie).readExpressionTokens()]
expect(exp).to.have.lengthOf(1)
expect(exp[0]).to.be.instanceOf(PropertyAccessToken)
expect(exp[0].getText()).to.deep.equal('a')
})
it('should read expression `a==b`', () => {
const exp = new Tokenizer('a==b', trie).readExpression()
const exp = new Tokenizer('a==b', trie).readExpressionTokens()
const [a, equals, b] = exp
expect(a).to.be.instanceOf(PropertyAccessToken)
@@ -419,11 +418,11 @@ describe('Tokenizer', function () {
expect(b.getText()).to.deep.equal('b')
})
it('should read expression `^`', () => {
const exp = new Tokenizer('^', trie).readExpression()
const exp = new Tokenizer('^', trie).readExpressionTokens()
expect([...exp]).to.deep.equal([])
})
it('should read expression `a == b`', () => {
const exp = new Tokenizer('a == b', trie).readExpression()
const exp = new Tokenizer('a == b', trie).readExpressionTokens()
const [a, equals, b] = exp
expect(a).to.be.instanceOf(PropertyAccessToken)
@@ -436,7 +435,7 @@ describe('Tokenizer', function () {
expect(b.getText()).to.deep.equal('b')
})
it('should read expression `(1..3) contains 3`', () => {
const exp = new Tokenizer('(1..3) contains 3', trie).readExpression()
const exp = new Tokenizer('(1..3) contains 3', trie).readExpressionTokens()
const [range, contains, rhs] = exp
expect(range).to.be.instanceOf(RangeToken)
@@ -449,7 +448,7 @@ describe('Tokenizer', function () {
expect(rhs.getText()).to.deep.equal('3')
})
it('should read expression `a[b] == c`', () => {
const exp = new Tokenizer('a[b] == c', trie).readExpression()
const exp = new Tokenizer('a[b] == c', trie).readExpressionTokens()
const [lhs, contains, rhs] = exp
expect(lhs).to.be.instanceOf(PropertyAccessToken)
@@ -462,7 +461,7 @@ describe('Tokenizer', function () {
expect(rhs.getText()).to.deep.equal('c')
})
it('should read expression `c[a["b"]] >= c`', () => {
const exp = new Tokenizer('c[a["b"]] >= c', trie).readExpression()
const exp = new Tokenizer('c[a["b"]] >= c', trie).readExpressionTokens()
const [lhs, op, rhs] = exp
expect(lhs).to.be.instanceOf(PropertyAccessToken)
@@ -475,7 +474,7 @@ describe('Tokenizer', function () {
expect(rhs.getText()).to.deep.equal('c')
})
it('should read expression `"][" == var`', () => {
const exp = new Tokenizer('"][" == var', trie).readExpression()
const exp = new Tokenizer('"][" == var', trie).readExpressionTokens()
const [lhs, equals, rhs] = exp
expect(lhs).to.be.instanceOf(QuotedToken)
@@ -488,7 +487,7 @@ describe('Tokenizer', function () {
expect(rhs.getText()).to.deep.equal('var')
})
it('should read expression `"\\\'" == "\\""`', () => {
const exp = new Tokenizer('"\\\'" == "\\""', trie).readExpression()
const exp = new Tokenizer('"\\\'" == "\\""', trie).readExpressionTokens()
const [lhs, equals, rhs] = exp
expect(lhs).to.be.instanceOf(QuotedToken)
+43 -42
View File
@@ -1,4 +1,4 @@
import { Expression } from '../../../src/render/expression'
import { Tokenizer } from '../../../src/parser/tokenizer'
import { expect } from 'chai'
import { Context } from '../../../src/context/context'
import { toThenable } from '../../../src/util/async'
@@ -8,9 +8,10 @@ import { createTrie } from '../../../src/util/operator-trie'
describe('Expression', function () {
const ctx = new Context({})
const trie = createTrie(defaultOperators)
const create = (str: string) => new Tokenizer(str, trie).readExpression()
it('should throw when context not defined', done => {
toThenable(new Expression('foo', defaultOperators, trie).value(undefined!))
toThenable(create('foo').evaluate(undefined!, false))
.then(() => done(new Error('should not resolved')))
.catch(err => {
expect(err.message).to.match(/context not defined/)
@@ -20,19 +21,19 @@ describe('Expression', function () {
describe('single value', function () {
it('should eval literal', async function () {
expect(await toThenable(new Expression('2.4', defaultOperators, trie).value(ctx))).to.equal(2.4)
expect(await toThenable(new Expression('"foo"', defaultOperators, trie).value(ctx))).to.equal('foo')
expect(await toThenable(new Expression('false', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false)
})
it('should eval range expression', async function () {
const ctx = new Context({ two: 2 })
expect(await toThenable(new Expression('(2..4)', defaultOperators, trie).value(ctx))).to.deep.equal([2, 3, 4])
expect(await toThenable(new Expression('(two..4)', defaultOperators, trie).value(ctx))).to.deep.equal([2, 3, 4])
expect(await toThenable(create('(2..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
expect(await toThenable(create('(two..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
})
it('should eval literal', async function () {
expect(await toThenable(new Expression('2.4', defaultOperators, trie).value(ctx))).to.equal(2.4)
expect(await toThenable(new Expression('"foo"', defaultOperators, trie).value(ctx))).to.equal('foo')
expect(await toThenable(new Expression('false', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false)
})
it('should eval property access', async function () {
@@ -41,112 +42,112 @@ describe('Expression', function () {
coo: 'bar',
doo: { foo: 'bar', bar: { foo: 'bar' } }
})
expect(await toThenable(new Expression('foo.bar', defaultOperators, trie).value(ctx))).to.equal('BAR')
expect(await toThenable(new Expression('foo["bar"]', defaultOperators, trie).value(ctx))).to.equal('BAR')
expect(await toThenable(new Expression('foo[coo]', defaultOperators, trie).value(ctx))).to.equal('BAR')
expect(await toThenable(new Expression('foo[doo.foo]', defaultOperators, trie).value(ctx))).to.equal('BAR')
expect(await toThenable(new Expression('foo[doo["foo"]]', defaultOperators, trie).value(ctx))).to.equal('BAR')
expect(await toThenable(new Expression('doo[coo].foo', defaultOperators, trie).value(ctx))).to.equal('bar')
expect(await toThenable(create('foo.bar').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[doo.foo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[doo["foo"]]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar')
})
})
describe('simple expression', function () {
it('should return false for "1==2"', async () => {
expect(await toThenable(new Expression('1==2', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('1==2').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "1<2"', async () => {
expect(await toThenable(new Expression('1<2', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('1<2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "1 < 2"', async () => {
expect(await toThenable(new Expression('1 < 2', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('1 < 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "1 < 2"', async () => {
expect(await toThenable(new Expression('1 < 2', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('1 < 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "2 <= 2"', async () => {
expect(await toThenable(new Expression('2 <= 2', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('2 <= 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "one <= two"', async () => {
const ctx = new Context({ one: 1, two: 2 })
expect(await toThenable(new Expression('one <= two', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('one <= two').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "x contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(new Expression('x contains "x"', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('x contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "x contains "X""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(new Expression('x contains "X"', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('x contains "X"').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "1 contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(new Expression('1 contains "x"', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('1 contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return false for "y contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(new Expression('y contains "x"', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('y contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return false for "z contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(new Expression('z contains "x"', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('z contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "(1..5) contains 3"', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(new Expression('(1..5) contains 3', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('(1..5) contains 3').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "(1..5) contains 6"', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(new Expression('(1..5) contains 6', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('(1..5) contains 6').evaluate(ctx, false))).to.equal(false)
})
it('should return true for ""<=" == "<=""', async () => {
expect(await toThenable(new Expression('"<=" == "<="', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('"<=" == "<="').evaluate(ctx, false))).to.equal(true)
})
})
it('should allow space in quoted value', async function () {
const ctx = new Context({ space: ' ' })
expect(await toThenable(new Expression('" " == space', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('" " == space').evaluate(ctx, false))).to.equal(true)
})
describe('escape', () => {
it('should escape quote', async function () {
const ctx = new Context({ quote: '"' })
expect(await toThenable(new Expression('"\\"" == quote', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('"\\"" == quote').evaluate(ctx, false))).to.equal(true)
})
it('should escape square bracket', async function () {
const ctx = new Context({ obj: { ']': 'bracket' } })
expect(await toThenable(new Expression('obj["]"] == "bracket"', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('obj["]"] == "bracket"').evaluate(ctx, false))).to.equal(true)
})
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(await toThenable(new Expression('false or true', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('false or true').evaluate(ctx, false))).to.equal(true)
})
it('should support < and contains', async function () {
expect(await toThenable(new Expression('1 < 2 and x contains "x"', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('1 < 2 and x contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should support < or contains', async function () {
expect(await toThenable(new Expression('1 < 2 or x contains "x"', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('1 < 2 or x contains "x"').evaluate(ctx, false))).to.equal(true)
})
it('should support value and !=', async function () {
const ctx = new Context({ empty: '' })
expect(await toThenable(new Expression('empty and empty != ""', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('empty and empty != ""').evaluate(ctx, false))).to.equal(false)
})
it('should recognize quoted value', async function () {
expect(await toThenable(new Expression('">"', defaultOperators, trie).value(ctx))).to.equal('>')
expect(await toThenable(create('">"').evaluate(ctx, false))).to.equal('>')
})
it('should evaluate from right to left', async function () {
expect(await toThenable(new Expression('true or false and false', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(new Expression('true and false and false or true', defaultOperators, trie).value(ctx))).to.equal(false)
expect(await toThenable(create('true or false and false').evaluate(ctx, false))).to.equal(true)
expect(await toThenable(create('true and false and false or true').evaluate(ctx, false))).to.equal(false)
})
it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } })
expect(await toThenable(new Expression('obj["foo"] and true', defaultOperators, trie).value(ctx))).to.equal(true)
expect(await toThenable(create('obj["foo"] and true').evaluate(ctx, false))).to.equal(true)
})
it('should allow nested property access', async function () {
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
expect(await toThenable(new Expression('obj[keys["what\'s this"]]', defaultOperators, trie).value(ctx))).to.equal('FOO')
expect(await toThenable(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).to.equal('FOO')
})
})
})
+10 -18
View File
@@ -3,7 +3,6 @@ import { toThenable } from '../../../src/util/async'
import { Context } from '../../../src/context/context'
import { Output } from '../../../src/template/output'
import { OutputToken } from '../../../src/tokens/output-token'
import { FilterMap } from '../../../src/template/filter/filter-map'
import { defaultOptions } from '../../../src/liquid-options'
import { createTrie } from '../../../src/util/operator-trie'
import { defaultOperators } from '../../../src/types'
@@ -15,35 +14,31 @@ describe('Output', function () {
const liquid = {
options: { operatorsTrie: createTrie(defaultOperators) }
} as any
let filters: FilterMap
beforeEach(function () {
filters = new FilterMap(false, liquid)
emitter.html = ''
})
beforeEach(() => { emitter.html = '' })
it('should stringify objects', async function () {
const scope = new Context({
foo: { obj: { arr: ['a', 2] } }
})
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]')
})
it('should skip function property', async function () {
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO')
})
@@ -60,16 +55,13 @@ describe('Output', function () {
keepOutputType: true
}
beforeEach(function () {
filters = new FilterMap(false, liquid)
emitter.html = ''
})
beforeEach(() => { emitter.html = '' })
it('should respect output variable number type', async () => {
const scope = new Context({
foo: 42
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal(42)
})
@@ -77,7 +69,7 @@ describe('Output', function () {
const scope = new Context({
foo: true
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal(true)
})
@@ -85,7 +77,7 @@ describe('Output', function () {
const scope = new Context({
foo: 'test'
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('test')
})
@@ -93,7 +85,7 @@ describe('Output', function () {
const scope = new Context({
foo: { a: { b: 42 } }
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.deep.equal({ a: { b: 42 } })
})
+7 -18
View File
@@ -1,32 +1,22 @@
import * as chai from 'chai'
import { Liquid } from '../../../src/liquid'
import { QuotedToken } from '../../../src/tokens/quoted-token'
import { toThenable } from '../../../src/util/async'
import { FilterMap } from '../../../src/template/filter/filter-map'
import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon'
import { Context } from '../../../src/context/context'
import { Value } from '../../../src/template/value'
import { createTrie } from '../../../src/util/operator-trie'
import { defaultOperators } from '../../../src/types'
chai.use(sinonChai)
const expect = chai.expect
describe('Value', function () {
const liquid = {
options: { operatorsTrie: createTrie(defaultOperators) }
} as any
const liquid = new Liquid()
describe('#constructor()', function () {
const filterMap = new FilterMap(false, liquid)
it('should parse "foo', function () {
const tpl = new Value('foo', filterMap, liquid)
expect(tpl.initial!.getText()).to.equal('foo')
expect(tpl.filters).to.deep.equal([])
})
it('should parse filters in value content', function () {
const f = new Value('o | foo: a: "a"', filterMap, liquid)
const f = new Value('o | foo: a: "a"', liquid)
expect(f.filters[0].name).to.equal('foo')
expect(f.filters[0].args).to.have.lengthOf(1)
const [k, v] = f.filters[0].args[0] as any
@@ -40,14 +30,13 @@ describe('Value', function () {
it('should call chained filters correctly', async function () {
const date = sinon.stub().returns('y')
const time = sinon.spy()
const filterMap = new FilterMap(false, liquid)
filterMap.set('date', date)
filterMap.set('time', time)
const tpl = new Value('foo.bar | date: "b" | time:2', filterMap, liquid)
liquid.registerFilter('date', date)
liquid.registerFilter('time', time)
const tpl = new Value('foo.bar | date: "b" | time:2', liquid)
const scope = new Context({
foo: { bar: 'bar' }
})
await toThenable(tpl.value(scope))
await toThenable(tpl.value(scope, false))
expect(date).to.have.been.calledWith('bar', 'b')
expect(time).to.have.been.calledWith('y', 2)
})