diff --git a/src/builtin/tags/case.ts b/src/builtin/tags/case.ts index 807f14c61b..b0756a80d4 100644 --- a/src/builtin/tags/case.ts +++ b/src/builtin/tags/case.ts @@ -1,8 +1,8 @@ -import { Expression, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types' +import { toValue, Value, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types' export default { parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { - this.cond = tagToken.args + this.cond = new Value(tagToken.args, this.liquid) this.cases = [] this.elseTemplates = [] @@ -10,7 +10,7 @@ export default { const stream: ParseStream = this.liquid.parser.parseStream(remainTokens) .on('tag:when', (token: TagToken) => { this.cases.push({ - val: token.args, + val: new Value(token.args, this.liquid), templates: p = [] }) }) @@ -26,11 +26,9 @@ export default { render: function * (ctx: Context, emitter: Emitter) { const r = this.liquid.renderer - const { operators, operatorsTrie } = this.liquid.options - const cond = yield new Expression(this.cond, operators, operatorsTrie).value(ctx) - for (let i = 0; i < this.cases.length; i++) { - const branch = this.cases[i] - const val = yield new Expression(branch.val, operators, operatorsTrie).value(ctx) + const cond = toValue(yield this.cond.value(ctx, ctx.opts.lenientIf)) + for (const branch of this.cases) { + const val = toValue(yield branch.val.value(ctx, ctx.opts.lenientIf)) if (val === cond) { yield r.renderTemplates(branch.templates, ctx, emitter) return diff --git a/src/builtin/tags/if.ts b/src/builtin/tags/if.ts index f9fc745183..1cf06d3b0d 100644 --- a/src/builtin/tags/if.ts +++ b/src/builtin/tags/if.ts @@ -1,4 +1,4 @@ -import { Emitter, isTruthy, Expression, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types' +import { Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types' export default { parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { @@ -8,12 +8,12 @@ export default { let p const stream: ParseStream = this.liquid.parser.parseStream(remainTokens) .on('start', () => this.branches.push({ - cond: tagToken.args, + cond: new Value(tagToken.args, this.liquid), templates: (p = []) })) .on('tag:elsif', (token: TagToken) => { this.branches.push({ - cond: token.args, + cond: new Value(token.args, this.liquid), templates: p = [] }) }) @@ -29,10 +29,9 @@ export default { render: function * (ctx: Context, emitter: Emitter) { const r = this.liquid.renderer - const { operators, operatorsTrie } = this.liquid.options for (const branch of this.branches) { - const cond = yield new Expression(branch.cond, operators, operatorsTrie, ctx.opts.lenientIf).value(ctx) + const cond = yield branch.cond.value(ctx, ctx.opts.lenientIf) if (isTruthy(cond, ctx)) { yield r.renderTemplates(branch.templates, ctx, emitter) return diff --git a/src/builtin/tags/unless.ts b/src/builtin/tags/unless.ts index 171a824609..442a75827e 100644 --- a/src/builtin/tags/unless.ts +++ b/src/builtin/tags/unless.ts @@ -1,4 +1,4 @@ -import { TopLevelToken, Template, Emitter, Expression, isTruthy, isFalsy, ParseStream, Context, TagImplOptions, TagToken } from '../../types' +import { Value, TopLevelToken, Template, Emitter, isTruthy, isFalsy, ParseStream, Context, TagImplOptions, TagToken } from '../../types' export default { parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { @@ -9,11 +9,11 @@ export default { const stream: ParseStream = this.liquid.parser.parseStream(remainTokens) .on('start', () => { p = this.templates - this.cond = tagToken.args + this.cond = new Value(tagToken.args, this.liquid) }) .on('tag:elsif', (token: TagToken) => { this.branches.push({ - cond: token.args, + cond: new Value(token.args, this.liquid), templates: p = [] }) }) @@ -29,8 +29,7 @@ export default { render: function * (ctx: Context, emitter: Emitter) { const r = this.liquid.renderer - const { operators, operatorsTrie } = this.liquid.options - const cond = yield new Expression(this.cond, operators, operatorsTrie, ctx.opts.lenientIf).value(ctx) + const cond = yield this.cond.value(ctx, ctx.opts.lenientIf) if (isFalsy(cond, ctx)) { yield r.renderTemplates(this.templates, ctx, emitter) @@ -38,7 +37,7 @@ export default { } for (const branch of this.branches) { - const cond = yield new Expression(branch.cond, operators, operatorsTrie, ctx.opts.lenientIf).value(ctx) + const cond = yield branch.cond.value(ctx, ctx.opts.lenientIf) if (isTruthy(cond, ctx)) { yield r.renderTemplates(branch.templates, ctx, emitter) return diff --git a/src/liquid.ts b/src/liquid.ts index 21405bdfba..8bcf9d1ba1 100644 --- a/src/liquid.ts +++ b/src/liquid.ts @@ -101,8 +101,8 @@ export class Liquid { } public _evalValue (str: string, ctx: Context): IterableIterator { - const value = new Value(str, this.filters, this) - return value.value(ctx) + const value = new Value(str, this) + return value.value(ctx, false) } public async evalValue (str: string, ctx: Context): Promise { return toPromise(this._evalValue(str, ctx)) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index fdbdd87474..754ee315fc 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -29,7 +29,7 @@ export default class Parser { return new Tag(token, remainTokens, this.liquid) } if (isOutputToken(token)) { - return new Output(token as OutputToken, this.liquid.filters, this.liquid) + return new Output(token as OutputToken, this.liquid) } return new HTML(token) } catch (e) { diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index 927a8ccc6a..21cebfcddb 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -23,6 +23,7 @@ import { NormalizedFullOptions, defaultOptions } from '../liquid-options' import { TYPES, QUOTE, BLANK, IDENTIFIER } from '../util/character' import { matchOperator } from './match-operator' import { Trie } from '../util/operator-trie' +import { Expression } from '../render/expression' export class Tokenizer { p = 0 @@ -37,7 +38,11 @@ export class Tokenizer { this.N = input.length } - * readExpression (): IterableIterator { + readExpression () { + return new Expression(this.readExpressionTokens()) + } + + * readExpressionTokens (): IterableIterator { const operand = this.readValue() if (!operand) return diff --git a/src/render/expression.ts b/src/render/expression.ts index 53146dcf96..d73eb357c0 100644 --- a/src/render/expression.ts +++ b/src/render/expression.ts @@ -1,4 +1,5 @@ import { QuotedToken } from '../tokens/quoted-token' +import { PropertyAccessToken } from '../tokens/property-access-token' import { NumberToken } from '../tokens/number-token' import { assert } from '../util/assert' import { literalValues } from '../util/literal' @@ -9,57 +10,35 @@ import { OperatorToken } from '../tokens/operator-token' import { RangeToken } from '../tokens/range-token' import { parseStringLiteral } from '../parser/parse-string-literal' import { Context } from '../context/context' -import { range, toValue } from '../util/underscore' -import { Tokenizer } from '../parser/tokenizer' +import { range } from '../util/underscore' import { Operators } from '../render/operator' import { UndefinedVariableError, InternalUndefinedVariableError } from '../util/error' -import { Trie } from '../util/operator-trie' export class Expression { - private operands: any[] = [] private postfix: Token[] - private lenient: boolean - private operators: Operators - public constructor (str: string, operators: Operators, operatorsTrie: Trie, lenient = false) { - const tokenizer = new Tokenizer(str, operatorsTrie) - this.postfix = [...toPostfix(tokenizer.readExpression())] - this.lenient = lenient - this.operators = operators + public constructor (tokens: IterableIterator) { + this.postfix = [...toPostfix(tokens)] } - public evaluate (ctx: Context): any { + public * evaluate (ctx: Context, lenient: boolean): any { + assert(ctx, () => 'unable to evaluate: context not defined') + const operands: any[] = [] for (const token of this.postfix) { if (TypeGuards.isOperatorToken(token)) { - const r = this.operands.pop() - const l = this.operands.pop() - const result = evalOperatorToken(this.operators, token, l, r, ctx) - this.operands.push(result) + const r = yield operands.pop() + const l = yield operands.pop() + const result = evalOperatorToken(ctx.opts.operators, token, l, r, ctx) + operands.push(result) } else { - this.operands.push(evalToken(token, ctx, this.lenient && this.postfix.length === 1)) + operands.push(yield evalToken(token, ctx, lenient && this.postfix.length === 1)) } } - return this.operands[0] - } - public * value (ctx: Context) { - return toValue(this.evaluate(ctx)) + return operands[0] } } export function evalToken (token: Token | undefined, ctx: Context, lenient = false): any { - assert(ctx, () => 'unable to evaluate: context not defined') - if (TypeGuards.isPropertyAccessToken(token)) { - const variable = token.getVariableAsText() - const props: string[] = token.props.map(prop => evalToken(prop, ctx)) - try { - return ctx.get([variable, ...props]) - } catch (e) { - if (lenient && e instanceof InternalUndefinedVariableError) { - return null - } else { - throw (new UndefinedVariableError(e, token)) - } - } - } + if (TypeGuards.isPropertyAccessToken(token)) return evalPropertyAccessToken(token, ctx, lenient) if (TypeGuards.isRangeToken(token)) return evalRangeToken(token, ctx) if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token) if (TypeGuards.isNumberToken(token)) return evalNumberToken(token) @@ -67,6 +46,17 @@ export function evalToken (token: Token | undefined, ctx: Context, lenient = fal if (TypeGuards.isQuotedToken(token)) return evalQuotedToken(token) } +function evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean) { + const variable = token.getVariableAsText() + const props: string[] = token.props.map(prop => evalToken(prop, ctx, false)) + try { + return ctx.get([variable, ...props]) + } catch (e) { + if (lenient && e instanceof InternalUndefinedVariableError) return null + throw (new UndefinedVariableError(e, token)) + } +} + function evalNumberToken (token: NumberToken) { const str = token.whole.content + '.' + (token.decimal ? token.decimal.content : '') return Number(str) diff --git a/src/template/filter/filter.ts b/src/template/filter/filter.ts index 246c4f9c79..771f877777 100644 --- a/src/template/filter/filter.ts +++ b/src/template/filter/filter.ts @@ -17,12 +17,12 @@ export class Filter { this.args = args this.liquid = liquid } - public * render (value: any, context: Context) { + public render (value: any, context: Context) { const argv: any[] = [] for (const arg of this.args as FilterArg[]) { - if (isKeyValuePair(arg)) argv.push([arg[0], yield evalToken(arg[1], context)]) - else argv.push(yield evalToken(arg, context)) + if (isKeyValuePair(arg)) argv.push([arg[0], evalToken(arg[1], context)]) + else argv.push(evalToken(arg, context)) } - return yield this.impl.apply({ context, liquid: this.liquid }, [value, ...argv]) + return this.impl.apply({ context, liquid: this.liquid }, [value, ...argv]) } } diff --git a/src/template/output.ts b/src/template/output.ts index d95fcdee5a..1622f862b7 100644 --- a/src/template/output.ts +++ b/src/template/output.ts @@ -1,5 +1,4 @@ import { Value } from './value' -import { FilterMap } from './filter/filter-map' import { TemplateImpl } from '../template/template-impl' import { Template } from '../template/template' import { Context } from '../context/context' @@ -9,12 +8,12 @@ import { Liquid } from '../liquid' export class Output extends TemplateImpl implements Template { private value: Value - public constructor (token: OutputToken, filters: FilterMap, liquid: Liquid) { + public constructor (token: OutputToken, liquid: Liquid) { super(token) - this.value = new Value(token.content, filters, liquid) + this.value = new Value(token.content, liquid) } public * render (ctx: Context, emitter: Emitter) { - const val = yield this.value.value(ctx) + const val = yield this.value.value(ctx, false) emitter.write(val) } } diff --git a/src/template/value.ts b/src/template/value.ts index cd64a7e17a..a9c1cabe3e 100644 --- a/src/template/value.ts +++ b/src/template/value.ts @@ -1,27 +1,25 @@ -import { evalToken } from '../render/expression' +import { Expression } from '../render/expression' import { Tokenizer } from '../parser/tokenizer' -import { FilterMap } from '../template/filter/filter-map' import { Filter } from './filter/filter' import { Context } from '../context/context' -import { ValueToken } from '../tokens/value-token' import { Liquid } from '../liquid' export class Value { public readonly filters: Filter[] = [] - public readonly initial?: ValueToken + public readonly initial: Expression /** * @param str the value to be valuated, eg.: "foobar" | truncate: 3 */ - public constructor (str: string, private readonly filterMap: FilterMap, liquid: Liquid) { + public constructor (str: string, liquid: Liquid) { const tokenizer = new Tokenizer(str, liquid.options.operatorsTrie) - this.initial = tokenizer.readValue() - this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args, liquid)) + this.initial = tokenizer.readExpression() + this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, liquid.filters.get(name), args, liquid)) } - public * value (ctx: Context) { - const lenient = ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default' + public * value (ctx: Context, lenient: boolean) { + lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default') + let val = yield this.initial.evaluate(ctx, lenient) - let val = yield evalToken(this.initial, ctx, lenient) for (const filter of this.filters) { val = yield filter.render(val, ctx) } diff --git a/src/types.ts b/src/types.ts index 9e5da28cdb..90bbec7c6e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,9 @@ export { Token } from './tokens/token' export { TopLevelToken } from './tokens/toplevel-token' export { Tokenizer } from './parser/tokenizer' export { Hash } from './template/tag/hash' +export { Value } from './template/value' export { evalToken, evalQuotedToken } from './render/expression' -export { toPromise, toThenable, toValue } from './util/async' +export { toPromise, toThenable } from './util/async' export { defaultOperators, Operators } from './render/operator' +export { createTrie, Trie } from './util/operator-trie' +export { toValue } from './util/underscore' diff --git a/test/integration/builtin/tags/assign.ts b/test/integration/builtin/tags/assign.ts index eeeaf977e5..a102983eef 100644 --- a/test/integration/builtin/tags/assign.ts +++ b/test/integration/builtin/tags/assign.ts @@ -76,9 +76,9 @@ describe('tags/assign', function () { return expect(html).to.equal('11') }) it('should write to the root scope', async function () { - const src = '{%for a in (1..2)%}{%assign num = a%}{{a}}{%endfor%} {{num}}' + const src = '{%for a in (1..2)%}{%assign num = a%}{{a}}{%endfor%}' const html = await liquid.parseAndRender(src, { num: 1 }) - return expect(html).to.equal('12 2') + return expect(html).to.equal('12') }) it('should not change input scope', async function () { const src = '{%for a in (1..2)%}{%assign num = a%}{{a}}{%endfor%} {{num}}' diff --git a/test/integration/builtin/tags/if.ts b/test/integration/builtin/tags/if.ts index 2930ba17f4..634db5762a 100644 --- a/test/integration/builtin/tags/if.ts +++ b/test/integration/builtin/tags/if.ts @@ -80,6 +80,14 @@ describe('tags/if', function () { return expect(html).to.equal('success') }) }) + describe('filters as condition', function () { + it('should support filter on expression', async function () { + liquid.registerFilter('negate', (val) => !val) + const src = '{% if 2 == 3 | negate %}yes{%else%}no{%endif%}' + const html = await liquid.parseAndRender(src, scope) + return expect(html).to.equal('yes') + }) + }) describe('compare to null', function () { it('should evaluate false for null < 10', async function () { const src = '{% if null < 10 %}yes{% else %}no{% endif %}' diff --git a/test/integration/liquid/strict.ts b/test/integration/liquid/strict.ts index 1fca49f858..a693a11177 100644 --- a/test/integration/liquid/strict.ts +++ b/test/integration/liquid/strict.ts @@ -1,5 +1,9 @@ import { Liquid } from '../../../src/liquid' -import { expect } from 'chai' +import * as chai from 'chai' +import * as chaiAsPromised from 'chai-as-promised' + +chai.use(chaiAsPromised) +const expect = chai.expect describe('LiquidOptions#strict*', function () { let engine: Liquid @@ -60,5 +64,9 @@ describe('LiquidOptions#strict*', function () { const html = await engine.render(tpl, ctx, strictLenientOpts) return expect(html).to.equal('a') }) + it('should not allow undefined variable even if `lenientIf` set', async function () { + const tpl = engine.parse('{{notdefined | tolower}}') + return expect(() => engine.renderSync(tpl, ctx, strictLenientOpts)).to.throw('undefined variable: notdefined') + }) }) }) diff --git a/test/unit/parser/tokenizer.ts b/test/unit/parser/tokenizer.ts index 5b44bdb6e7..5d413cdc8b 100644 --- a/test/unit/parser/tokenizer.ts +++ b/test/unit/parser/tokenizer.ts @@ -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) diff --git a/test/unit/render/expression.ts b/test/unit/render/expression.ts index 7c48c7a5c3..6f497aae39 100644 --- a/test/unit/render/expression.ts +++ b/test/unit/render/expression.ts @@ -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') }) }) }) diff --git a/test/unit/template/output.ts b/test/unit/template/output.ts index d5849fe7f6..64d15aa30d 100644 --- a/test/unit/template/output.ts +++ b/test/unit/template/output.ts @@ -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 } }) }) diff --git a/test/unit/template/value.ts b/test/unit/template/value.ts index 8c88e02bd6..86faa611c1 100644 --- a/test/unit/template/value.ts +++ b/test/unit/template/value.ts @@ -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) })