mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 12:20:40 -07:00
feat: support filters in if/unless/case, see #287
This commit is contained in:
@@ -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 {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
this.cond = tagToken.args
|
this.cond = new Value(tagToken.args, this.liquid)
|
||||||
this.cases = []
|
this.cases = []
|
||||||
this.elseTemplates = []
|
this.elseTemplates = []
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export default {
|
|||||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||||
.on('tag:when', (token: TagToken) => {
|
.on('tag:when', (token: TagToken) => {
|
||||||
this.cases.push({
|
this.cases.push({
|
||||||
val: token.args,
|
val: new Value(token.args, this.liquid),
|
||||||
templates: p = []
|
templates: p = []
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -26,11 +26,9 @@ export default {
|
|||||||
|
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
const { operators, operatorsTrie } = this.liquid.options
|
const cond = toValue(yield this.cond.value(ctx, ctx.opts.lenientIf))
|
||||||
const cond = yield new Expression(this.cond, operators, operatorsTrie).value(ctx)
|
for (const branch of this.cases) {
|
||||||
for (let i = 0; i < this.cases.length; i++) {
|
const val = toValue(yield branch.val.value(ctx, ctx.opts.lenientIf))
|
||||||
const branch = this.cases[i]
|
|
||||||
const val = yield new Expression(branch.val, operators, operatorsTrie).value(ctx)
|
|
||||||
if (val === cond) {
|
if (val === cond) {
|
||||||
yield r.renderTemplates(branch.templates, ctx, emitter)
|
yield r.renderTemplates(branch.templates, ctx, emitter)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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 {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
@@ -8,12 +8,12 @@ export default {
|
|||||||
let p
|
let p
|
||||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||||
.on('start', () => this.branches.push({
|
.on('start', () => this.branches.push({
|
||||||
cond: tagToken.args,
|
cond: new Value(tagToken.args, this.liquid),
|
||||||
templates: (p = [])
|
templates: (p = [])
|
||||||
}))
|
}))
|
||||||
.on('tag:elsif', (token: TagToken) => {
|
.on('tag:elsif', (token: TagToken) => {
|
||||||
this.branches.push({
|
this.branches.push({
|
||||||
cond: token.args,
|
cond: new Value(token.args, this.liquid),
|
||||||
templates: p = []
|
templates: p = []
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -29,10 +29,9 @@ export default {
|
|||||||
|
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
const { operators, operatorsTrie } = this.liquid.options
|
|
||||||
|
|
||||||
for (const branch of this.branches) {
|
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)) {
|
if (isTruthy(cond, ctx)) {
|
||||||
yield r.renderTemplates(branch.templates, ctx, emitter)
|
yield r.renderTemplates(branch.templates, ctx, emitter)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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 {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
@@ -9,11 +9,11 @@ export default {
|
|||||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||||
.on('start', () => {
|
.on('start', () => {
|
||||||
p = this.templates
|
p = this.templates
|
||||||
this.cond = tagToken.args
|
this.cond = new Value(tagToken.args, this.liquid)
|
||||||
})
|
})
|
||||||
.on('tag:elsif', (token: TagToken) => {
|
.on('tag:elsif', (token: TagToken) => {
|
||||||
this.branches.push({
|
this.branches.push({
|
||||||
cond: token.args,
|
cond: new Value(token.args, this.liquid),
|
||||||
templates: p = []
|
templates: p = []
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -29,8 +29,7 @@ export default {
|
|||||||
|
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
const { operators, operatorsTrie } = this.liquid.options
|
const cond = yield this.cond.value(ctx, ctx.opts.lenientIf)
|
||||||
const cond = yield new Expression(this.cond, operators, operatorsTrie, ctx.opts.lenientIf).value(ctx)
|
|
||||||
|
|
||||||
if (isFalsy(cond, ctx)) {
|
if (isFalsy(cond, ctx)) {
|
||||||
yield r.renderTemplates(this.templates, ctx, emitter)
|
yield r.renderTemplates(this.templates, ctx, emitter)
|
||||||
@@ -38,7 +37,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const branch of this.branches) {
|
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)) {
|
if (isTruthy(cond, ctx)) {
|
||||||
yield r.renderTemplates(branch.templates, ctx, emitter)
|
yield r.renderTemplates(branch.templates, ctx, emitter)
|
||||||
return
|
return
|
||||||
|
|||||||
+2
-2
@@ -101,8 +101,8 @@ export class Liquid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public _evalValue (str: string, ctx: Context): IterableIterator<any> {
|
public _evalValue (str: string, ctx: Context): IterableIterator<any> {
|
||||||
const value = new Value(str, this.filters, this)
|
const value = new Value(str, this)
|
||||||
return value.value(ctx)
|
return value.value(ctx, false)
|
||||||
}
|
}
|
||||||
public async evalValue (str: string, ctx: Context): Promise<any> {
|
public async evalValue (str: string, ctx: Context): Promise<any> {
|
||||||
return toPromise(this._evalValue(str, ctx))
|
return toPromise(this._evalValue(str, ctx))
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default class Parser {
|
|||||||
return new Tag(token, remainTokens, this.liquid)
|
return new Tag(token, remainTokens, this.liquid)
|
||||||
}
|
}
|
||||||
if (isOutputToken(token)) {
|
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)
|
return new HTML(token)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
|||||||
import { TYPES, QUOTE, BLANK, IDENTIFIER } from '../util/character'
|
import { TYPES, QUOTE, BLANK, IDENTIFIER } from '../util/character'
|
||||||
import { matchOperator } from './match-operator'
|
import { matchOperator } from './match-operator'
|
||||||
import { Trie } from '../util/operator-trie'
|
import { Trie } from '../util/operator-trie'
|
||||||
|
import { Expression } from '../render/expression'
|
||||||
|
|
||||||
export class Tokenizer {
|
export class Tokenizer {
|
||||||
p = 0
|
p = 0
|
||||||
@@ -37,7 +38,11 @@ export class Tokenizer {
|
|||||||
this.N = input.length
|
this.N = input.length
|
||||||
}
|
}
|
||||||
|
|
||||||
* readExpression (): IterableIterator<Token> {
|
readExpression () {
|
||||||
|
return new Expression(this.readExpressionTokens())
|
||||||
|
}
|
||||||
|
|
||||||
|
* readExpressionTokens (): IterableIterator<Token> {
|
||||||
const operand = this.readValue()
|
const operand = this.readValue()
|
||||||
if (!operand) return
|
if (!operand) return
|
||||||
|
|
||||||
|
|||||||
+25
-35
@@ -1,4 +1,5 @@
|
|||||||
import { QuotedToken } from '../tokens/quoted-token'
|
import { QuotedToken } from '../tokens/quoted-token'
|
||||||
|
import { PropertyAccessToken } from '../tokens/property-access-token'
|
||||||
import { NumberToken } from '../tokens/number-token'
|
import { NumberToken } from '../tokens/number-token'
|
||||||
import { assert } from '../util/assert'
|
import { assert } from '../util/assert'
|
||||||
import { literalValues } from '../util/literal'
|
import { literalValues } from '../util/literal'
|
||||||
@@ -9,57 +10,35 @@ import { OperatorToken } from '../tokens/operator-token'
|
|||||||
import { RangeToken } from '../tokens/range-token'
|
import { RangeToken } from '../tokens/range-token'
|
||||||
import { parseStringLiteral } from '../parser/parse-string-literal'
|
import { parseStringLiteral } from '../parser/parse-string-literal'
|
||||||
import { Context } from '../context/context'
|
import { Context } from '../context/context'
|
||||||
import { range, toValue } from '../util/underscore'
|
import { range } from '../util/underscore'
|
||||||
import { Tokenizer } from '../parser/tokenizer'
|
|
||||||
import { Operators } from '../render/operator'
|
import { Operators } from '../render/operator'
|
||||||
import { UndefinedVariableError, InternalUndefinedVariableError } from '../util/error'
|
import { UndefinedVariableError, InternalUndefinedVariableError } from '../util/error'
|
||||||
import { Trie } from '../util/operator-trie'
|
|
||||||
|
|
||||||
export class Expression {
|
export class Expression {
|
||||||
private operands: any[] = []
|
|
||||||
private postfix: Token[]
|
private postfix: Token[]
|
||||||
private lenient: boolean
|
|
||||||
private operators: Operators
|
|
||||||
|
|
||||||
public constructor (str: string, operators: Operators, operatorsTrie: Trie, lenient = false) {
|
public constructor (tokens: IterableIterator<Token>) {
|
||||||
const tokenizer = new Tokenizer(str, operatorsTrie)
|
this.postfix = [...toPostfix(tokens)]
|
||||||
this.postfix = [...toPostfix(tokenizer.readExpression())]
|
|
||||||
this.lenient = lenient
|
|
||||||
this.operators = operators
|
|
||||||
}
|
}
|
||||||
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) {
|
for (const token of this.postfix) {
|
||||||
if (TypeGuards.isOperatorToken(token)) {
|
if (TypeGuards.isOperatorToken(token)) {
|
||||||
const r = this.operands.pop()
|
const r = yield operands.pop()
|
||||||
const l = this.operands.pop()
|
const l = yield operands.pop()
|
||||||
const result = evalOperatorToken(this.operators, token, l, r, ctx)
|
const result = evalOperatorToken(ctx.opts.operators, token, l, r, ctx)
|
||||||
this.operands.push(result)
|
operands.push(result)
|
||||||
} else {
|
} 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]
|
return operands[0]
|
||||||
}
|
|
||||||
public * value (ctx: Context) {
|
|
||||||
return toValue(this.evaluate(ctx))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function evalToken (token: Token | undefined, ctx: Context, lenient = false): any {
|
export function evalToken (token: Token | undefined, ctx: Context, lenient = false): any {
|
||||||
assert(ctx, () => 'unable to evaluate: context not defined')
|
if (TypeGuards.isPropertyAccessToken(token)) return evalPropertyAccessToken(token, ctx, lenient)
|
||||||
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.isRangeToken(token)) return evalRangeToken(token, ctx)
|
if (TypeGuards.isRangeToken(token)) return evalRangeToken(token, ctx)
|
||||||
if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token)
|
if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token)
|
||||||
if (TypeGuards.isNumberToken(token)) return evalNumberToken(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)
|
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) {
|
function evalNumberToken (token: NumberToken) {
|
||||||
const str = token.whole.content + '.' + (token.decimal ? token.decimal.content : '')
|
const str = token.whole.content + '.' + (token.decimal ? token.decimal.content : '')
|
||||||
return Number(str)
|
return Number(str)
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ export class Filter {
|
|||||||
this.args = args
|
this.args = args
|
||||||
this.liquid = liquid
|
this.liquid = liquid
|
||||||
}
|
}
|
||||||
public * render (value: any, context: Context) {
|
public render (value: any, context: Context) {
|
||||||
const argv: any[] = []
|
const argv: any[] = []
|
||||||
for (const arg of this.args as FilterArg[]) {
|
for (const arg of this.args as FilterArg[]) {
|
||||||
if (isKeyValuePair(arg)) argv.push([arg[0], yield evalToken(arg[1], context)])
|
if (isKeyValuePair(arg)) argv.push([arg[0], evalToken(arg[1], context)])
|
||||||
else argv.push(yield evalToken(arg, 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])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Value } from './value'
|
import { Value } from './value'
|
||||||
import { FilterMap } from './filter/filter-map'
|
|
||||||
import { TemplateImpl } from '../template/template-impl'
|
import { TemplateImpl } from '../template/template-impl'
|
||||||
import { Template } from '../template/template'
|
import { Template } from '../template/template'
|
||||||
import { Context } from '../context/context'
|
import { Context } from '../context/context'
|
||||||
@@ -9,12 +8,12 @@ import { Liquid } from '../liquid'
|
|||||||
|
|
||||||
export class Output extends TemplateImpl<OutputToken> implements Template {
|
export class Output extends TemplateImpl<OutputToken> implements Template {
|
||||||
private value: Value
|
private value: Value
|
||||||
public constructor (token: OutputToken, filters: FilterMap, liquid: Liquid) {
|
public constructor (token: OutputToken, liquid: Liquid) {
|
||||||
super(token)
|
super(token)
|
||||||
this.value = new Value(token.content, filters, liquid)
|
this.value = new Value(token.content, liquid)
|
||||||
}
|
}
|
||||||
public * render (ctx: Context, emitter: Emitter) {
|
public * render (ctx: Context, emitter: Emitter) {
|
||||||
const val = yield this.value.value(ctx)
|
const val = yield this.value.value(ctx, false)
|
||||||
emitter.write(val)
|
emitter.write(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-10
@@ -1,27 +1,25 @@
|
|||||||
import { evalToken } from '../render/expression'
|
import { Expression } from '../render/expression'
|
||||||
import { Tokenizer } from '../parser/tokenizer'
|
import { Tokenizer } from '../parser/tokenizer'
|
||||||
import { FilterMap } from '../template/filter/filter-map'
|
|
||||||
import { Filter } from './filter/filter'
|
import { Filter } from './filter/filter'
|
||||||
import { Context } from '../context/context'
|
import { Context } from '../context/context'
|
||||||
import { ValueToken } from '../tokens/value-token'
|
|
||||||
import { Liquid } from '../liquid'
|
import { Liquid } from '../liquid'
|
||||||
|
|
||||||
export class Value {
|
export class Value {
|
||||||
public readonly filters: Filter[] = []
|
public readonly filters: Filter[] = []
|
||||||
public readonly initial?: ValueToken
|
public readonly initial: Expression
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param str the value to be valuated, eg.: "foobar" | truncate: 3
|
* @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)
|
const tokenizer = new Tokenizer(str, liquid.options.operatorsTrie)
|
||||||
this.initial = tokenizer.readValue()
|
this.initial = tokenizer.readExpression()
|
||||||
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args, liquid))
|
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, liquid.filters.get(name), args, liquid))
|
||||||
}
|
}
|
||||||
public * value (ctx: Context) {
|
public * value (ctx: Context, lenient: boolean) {
|
||||||
const lenient = ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default'
|
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) {
|
for (const filter of this.filters) {
|
||||||
val = yield filter.render(val, ctx)
|
val = yield filter.render(val, ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -17,6 +17,9 @@ export { Token } from './tokens/token'
|
|||||||
export { TopLevelToken } from './tokens/toplevel-token'
|
export { TopLevelToken } from './tokens/toplevel-token'
|
||||||
export { Tokenizer } from './parser/tokenizer'
|
export { Tokenizer } from './parser/tokenizer'
|
||||||
export { Hash } from './template/tag/hash'
|
export { Hash } from './template/tag/hash'
|
||||||
|
export { Value } from './template/value'
|
||||||
export { evalToken, evalQuotedToken } from './render/expression'
|
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 { defaultOperators, Operators } from './render/operator'
|
||||||
|
export { createTrie, Trie } from './util/operator-trie'
|
||||||
|
export { toValue } from './util/underscore'
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ describe('tags/assign', function () {
|
|||||||
return expect(html).to.equal('11')
|
return expect(html).to.equal('11')
|
||||||
})
|
})
|
||||||
it('should write to the root scope', async function () {
|
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 })
|
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 () {
|
it('should not change input 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%} {{num}}'
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ describe('tags/if', function () {
|
|||||||
return expect(html).to.equal('success')
|
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 () {
|
describe('compare to null', function () {
|
||||||
it('should evaluate false for null < 10', async function () {
|
it('should evaluate false for null < 10', async function () {
|
||||||
const src = '{% if null < 10 %}yes{% else %}no{% endif %}'
|
const src = '{% if null < 10 %}yes{% else %}no{% endif %}'
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Liquid } from '../../../src/liquid'
|
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 () {
|
describe('LiquidOptions#strict*', function () {
|
||||||
let engine: Liquid
|
let engine: Liquid
|
||||||
@@ -60,5 +64,9 @@ describe('LiquidOptions#strict*', function () {
|
|||||||
const html = await engine.render(tpl, ctx, strictLenientOpts)
|
const html = await engine.render(tpl, ctx, strictLenientOpts)
|
||||||
return expect(html).to.equal('a')
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ describe('Tokenizer', function () {
|
|||||||
const tokenizer = new Tokenizer(html, trie)
|
const tokenizer = new Tokenizer(html, trie)
|
||||||
const token = tokenizer.readOutputToken()
|
const token = tokenizer.readOutputToken()
|
||||||
|
|
||||||
console.log(token)
|
|
||||||
expect(token).instanceOf(OutputToken)
|
expect(token).instanceOf(OutputToken)
|
||||||
expect(token.content).to.equal('"%} {%" | append: "}} {{"')
|
expect(token.content).to.equal('"%} {%" | append: "}} {{"')
|
||||||
})
|
})
|
||||||
@@ -299,7 +298,7 @@ describe('Tokenizer', function () {
|
|||||||
|
|
||||||
const pa: PropertyAccessToken = token!.args[0] as any
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
|
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).to.have.lengthOf(1)
|
||||||
expect(pa.props[0]).to.be.instanceOf(NumberToken)
|
expect(pa.props[0]).to.be.instanceOf(NumberToken)
|
||||||
expect(pa.props[0].getText()).to.equal('0')
|
expect(pa.props[0].getText()).to.equal('0')
|
||||||
@@ -312,7 +311,7 @@ describe('Tokenizer', function () {
|
|||||||
|
|
||||||
const pa: PropertyAccessToken = token!.args[0] as any
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
|
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).to.have.lengthOf(1)
|
||||||
expect(pa.props[0]).to.be.instanceOf(IdentifierToken)
|
expect(pa.props[0]).to.be.instanceOf(IdentifierToken)
|
||||||
expect(pa.props[0].getText()).to.equal('foo')
|
expect(pa.props[0].getText()).to.equal('foo')
|
||||||
@@ -326,7 +325,7 @@ describe('Tokenizer', function () {
|
|||||||
const pa: PropertyAccessToken = token!.args[0] as any
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
|
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
expect(pa.getText()).to.equal('obj["good luck"]')
|
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"')
|
expect(pa.props[0].getText()).to.equal('"good luck"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -368,19 +367,19 @@ describe('Tokenizer', function () {
|
|||||||
})
|
})
|
||||||
describe('#readExpression()', () => {
|
describe('#readExpression()', () => {
|
||||||
it('should read expression `a `', () => {
|
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).to.have.lengthOf(1)
|
||||||
expect(exp[0]).to.be.instanceOf(PropertyAccessToken)
|
expect(exp[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
expect(exp[0].getText()).to.deep.equal('a')
|
expect(exp[0].getText()).to.deep.equal('a')
|
||||||
})
|
})
|
||||||
it('should read expression `a[][b]`', () => {
|
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)
|
expect(exp).to.have.lengthOf(1)
|
||||||
const pa = exp[0] as PropertyAccessToken
|
const pa = exp[0] as PropertyAccessToken
|
||||||
expect(pa).to.be.instanceOf(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)
|
expect(pa.props).to.have.lengthOf(2)
|
||||||
|
|
||||||
const [p1, p2] = pa.props
|
const [p1, p2] = pa.props
|
||||||
@@ -390,23 +389,23 @@ describe('Tokenizer', function () {
|
|||||||
expect(p2.getText()).to.equal('b')
|
expect(p2.getText()).to.equal('b')
|
||||||
})
|
})
|
||||||
it('should read expression `a.`', () => {
|
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).to.have.lengthOf(1)
|
||||||
const pa = exp[0] as PropertyAccessToken
|
const pa = exp[0] as PropertyAccessToken
|
||||||
expect(pa).to.be.instanceOf(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)
|
expect(pa.props).to.have.lengthOf(0)
|
||||||
})
|
})
|
||||||
it('should read expression `a ==`', () => {
|
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).to.have.lengthOf(1)
|
||||||
expect(exp[0]).to.be.instanceOf(PropertyAccessToken)
|
expect(exp[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
expect(exp[0].getText()).to.deep.equal('a')
|
expect(exp[0].getText()).to.deep.equal('a')
|
||||||
})
|
})
|
||||||
it('should read expression `a==b`', () => {
|
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
|
const [a, equals, b] = exp
|
||||||
|
|
||||||
expect(a).to.be.instanceOf(PropertyAccessToken)
|
expect(a).to.be.instanceOf(PropertyAccessToken)
|
||||||
@@ -419,11 +418,11 @@ describe('Tokenizer', function () {
|
|||||||
expect(b.getText()).to.deep.equal('b')
|
expect(b.getText()).to.deep.equal('b')
|
||||||
})
|
})
|
||||||
it('should read expression `^`', () => {
|
it('should read expression `^`', () => {
|
||||||
const exp = new Tokenizer('^', trie).readExpression()
|
const exp = new Tokenizer('^', trie).readExpressionTokens()
|
||||||
expect([...exp]).to.deep.equal([])
|
expect([...exp]).to.deep.equal([])
|
||||||
})
|
})
|
||||||
it('should read expression `a == b`', () => {
|
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
|
const [a, equals, b] = exp
|
||||||
|
|
||||||
expect(a).to.be.instanceOf(PropertyAccessToken)
|
expect(a).to.be.instanceOf(PropertyAccessToken)
|
||||||
@@ -436,7 +435,7 @@ describe('Tokenizer', function () {
|
|||||||
expect(b.getText()).to.deep.equal('b')
|
expect(b.getText()).to.deep.equal('b')
|
||||||
})
|
})
|
||||||
it('should read expression `(1..3) contains 3`', () => {
|
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
|
const [range, contains, rhs] = exp
|
||||||
|
|
||||||
expect(range).to.be.instanceOf(RangeToken)
|
expect(range).to.be.instanceOf(RangeToken)
|
||||||
@@ -449,7 +448,7 @@ describe('Tokenizer', function () {
|
|||||||
expect(rhs.getText()).to.deep.equal('3')
|
expect(rhs.getText()).to.deep.equal('3')
|
||||||
})
|
})
|
||||||
it('should read expression `a[b] == c`', () => {
|
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
|
const [lhs, contains, rhs] = exp
|
||||||
|
|
||||||
expect(lhs).to.be.instanceOf(PropertyAccessToken)
|
expect(lhs).to.be.instanceOf(PropertyAccessToken)
|
||||||
@@ -462,7 +461,7 @@ describe('Tokenizer', function () {
|
|||||||
expect(rhs.getText()).to.deep.equal('c')
|
expect(rhs.getText()).to.deep.equal('c')
|
||||||
})
|
})
|
||||||
it('should read expression `c[a["b"]] >= 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
|
const [lhs, op, rhs] = exp
|
||||||
|
|
||||||
expect(lhs).to.be.instanceOf(PropertyAccessToken)
|
expect(lhs).to.be.instanceOf(PropertyAccessToken)
|
||||||
@@ -475,7 +474,7 @@ describe('Tokenizer', function () {
|
|||||||
expect(rhs.getText()).to.deep.equal('c')
|
expect(rhs.getText()).to.deep.equal('c')
|
||||||
})
|
})
|
||||||
it('should read expression `"][" == var`', () => {
|
it('should read expression `"][" == var`', () => {
|
||||||
const exp = new Tokenizer('"][" == var', trie).readExpression()
|
const exp = new Tokenizer('"][" == var', trie).readExpressionTokens()
|
||||||
const [lhs, equals, rhs] = exp
|
const [lhs, equals, rhs] = exp
|
||||||
|
|
||||||
expect(lhs).to.be.instanceOf(QuotedToken)
|
expect(lhs).to.be.instanceOf(QuotedToken)
|
||||||
@@ -488,7 +487,7 @@ describe('Tokenizer', function () {
|
|||||||
expect(rhs.getText()).to.deep.equal('var')
|
expect(rhs.getText()).to.deep.equal('var')
|
||||||
})
|
})
|
||||||
it('should read expression `"\\\'" == "\\""`', () => {
|
it('should read expression `"\\\'" == "\\""`', () => {
|
||||||
const exp = new Tokenizer('"\\\'" == "\\""', trie).readExpression()
|
const exp = new Tokenizer('"\\\'" == "\\""', trie).readExpressionTokens()
|
||||||
const [lhs, equals, rhs] = exp
|
const [lhs, equals, rhs] = exp
|
||||||
|
|
||||||
expect(lhs).to.be.instanceOf(QuotedToken)
|
expect(lhs).to.be.instanceOf(QuotedToken)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Expression } from '../../../src/render/expression'
|
import { Tokenizer } from '../../../src/parser/tokenizer'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { Context } from '../../../src/context/context'
|
import { Context } from '../../../src/context/context'
|
||||||
import { toThenable } from '../../../src/util/async'
|
import { toThenable } from '../../../src/util/async'
|
||||||
@@ -8,9 +8,10 @@ import { createTrie } from '../../../src/util/operator-trie'
|
|||||||
describe('Expression', function () {
|
describe('Expression', function () {
|
||||||
const ctx = new Context({})
|
const ctx = new Context({})
|
||||||
const trie = createTrie(defaultOperators)
|
const trie = createTrie(defaultOperators)
|
||||||
|
const create = (str: string) => new Tokenizer(str, trie).readExpression()
|
||||||
|
|
||||||
it('should throw when context not defined', done => {
|
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')))
|
.then(() => done(new Error('should not resolved')))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
expect(err.message).to.match(/context not defined/)
|
expect(err.message).to.match(/context not defined/)
|
||||||
@@ -20,19 +21,19 @@ describe('Expression', function () {
|
|||||||
|
|
||||||
describe('single value', function () {
|
describe('single value', function () {
|
||||||
it('should eval literal', async 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(create('2.4').evaluate(ctx, false))).to.equal(2.4)
|
||||||
expect(await toThenable(new Expression('"foo"', defaultOperators, trie).value(ctx))).to.equal('foo')
|
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo')
|
||||||
expect(await toThenable(new Expression('false', defaultOperators, trie).value(ctx))).to.equal(false)
|
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false)
|
||||||
})
|
})
|
||||||
it('should eval range expression', async function () {
|
it('should eval range expression', async function () {
|
||||||
const ctx = new Context({ two: 2 })
|
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(create('(2..4)').evaluate(ctx, false))).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('(two..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
|
||||||
})
|
})
|
||||||
it('should eval literal', async 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(create('2.4').evaluate(ctx, false))).to.equal(2.4)
|
||||||
expect(await toThenable(new Expression('"foo"', defaultOperators, trie).value(ctx))).to.equal('foo')
|
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo')
|
||||||
expect(await toThenable(new Expression('false', defaultOperators, trie).value(ctx))).to.equal(false)
|
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should eval property access', async function () {
|
it('should eval property access', async function () {
|
||||||
@@ -41,112 +42,112 @@ describe('Expression', function () {
|
|||||||
coo: 'bar',
|
coo: 'bar',
|
||||||
doo: { foo: 'bar', bar: { foo: 'bar' } }
|
doo: { foo: 'bar', bar: { foo: 'bar' } }
|
||||||
})
|
})
|
||||||
expect(await toThenable(new Expression('foo.bar', defaultOperators, trie).value(ctx))).to.equal('BAR')
|
expect(await toThenable(create('foo.bar').evaluate(ctx, false))).to.equal('BAR')
|
||||||
expect(await toThenable(new Expression('foo["bar"]', defaultOperators, trie).value(ctx))).to.equal('BAR')
|
expect(await toThenable(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR')
|
||||||
expect(await toThenable(new Expression('foo[coo]', defaultOperators, trie).value(ctx))).to.equal('BAR')
|
expect(await toThenable(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR')
|
||||||
expect(await toThenable(new Expression('foo[doo.foo]', defaultOperators, trie).value(ctx))).to.equal('BAR')
|
expect(await toThenable(create('foo[doo.foo]').evaluate(ctx, false))).to.equal('BAR')
|
||||||
expect(await toThenable(new Expression('foo[doo["foo"]]', defaultOperators, trie).value(ctx))).to.equal('BAR')
|
expect(await toThenable(create('foo[doo["foo"]]').evaluate(ctx, false))).to.equal('BAR')
|
||||||
expect(await toThenable(new Expression('doo[coo].foo', defaultOperators, trie).value(ctx))).to.equal('bar')
|
expect(await toThenable(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('simple expression', function () {
|
describe('simple expression', function () {
|
||||||
it('should return false for "1==2"', async () => {
|
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 () => {
|
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 () => {
|
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 () => {
|
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 () => {
|
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 () => {
|
it('should return true for "one <= two"', async () => {
|
||||||
const ctx = new Context({ one: 1, two: 2 })
|
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 () => {
|
it('should return false for "x contains "x""', async () => {
|
||||||
const ctx = new Context({ x: 'XXX' })
|
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 () => {
|
it('should return true for "x contains "X""', async () => {
|
||||||
const ctx = new Context({ x: 'XXX' })
|
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 () => {
|
it('should return false for "1 contains "x""', async () => {
|
||||||
const ctx = new Context({ x: 'XXX' })
|
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 () => {
|
it('should return false for "y contains "x""', async () => {
|
||||||
const ctx = new Context({ x: 'XXX' })
|
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 () => {
|
it('should return false for "z contains "x""', async () => {
|
||||||
const ctx = new Context({ x: 'XXX' })
|
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 () => {
|
it('should return true for "(1..5) contains 3"', async () => {
|
||||||
const ctx = new Context({ x: 'XXX' })
|
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 () => {
|
it('should return false for "(1..5) contains 6"', async () => {
|
||||||
const ctx = new Context({ x: 'XXX' })
|
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 () => {
|
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 () {
|
it('should allow space in quoted value', async function () {
|
||||||
const ctx = new Context({ space: ' ' })
|
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', () => {
|
describe('escape', () => {
|
||||||
it('should escape quote', async function () {
|
it('should escape quote', async function () {
|
||||||
const ctx = new Context({ quote: '"' })
|
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 () {
|
it('should escape square bracket', async function () {
|
||||||
const ctx = new Context({ obj: { ']': 'bracket' } })
|
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 () {
|
describe('complex expression', function () {
|
||||||
it('should support value or value', async 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 () {
|
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 () {
|
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 () {
|
it('should support value and !=', async function () {
|
||||||
const ctx = new Context({ empty: '' })
|
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 () {
|
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 () {
|
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(create('true or false and false').evaluate(ctx, false))).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 and false and false or true').evaluate(ctx, false))).to.equal(false)
|
||||||
})
|
})
|
||||||
it('should recognize property access', async function () {
|
it('should recognize property access', async function () {
|
||||||
const ctx = new Context({ obj: { foo: true } })
|
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 () {
|
it('should allow nested property access', async function () {
|
||||||
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
|
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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { toThenable } from '../../../src/util/async'
|
|||||||
import { Context } from '../../../src/context/context'
|
import { Context } from '../../../src/context/context'
|
||||||
import { Output } from '../../../src/template/output'
|
import { Output } from '../../../src/template/output'
|
||||||
import { OutputToken } from '../../../src/tokens/output-token'
|
import { OutputToken } from '../../../src/tokens/output-token'
|
||||||
import { FilterMap } from '../../../src/template/filter/filter-map'
|
|
||||||
import { defaultOptions } from '../../../src/liquid-options'
|
import { defaultOptions } from '../../../src/liquid-options'
|
||||||
import { createTrie } from '../../../src/util/operator-trie'
|
import { createTrie } from '../../../src/util/operator-trie'
|
||||||
import { defaultOperators } from '../../../src/types'
|
import { defaultOperators } from '../../../src/types'
|
||||||
@@ -15,35 +14,31 @@ describe('Output', function () {
|
|||||||
const liquid = {
|
const liquid = {
|
||||||
options: { operatorsTrie: createTrie(defaultOperators) }
|
options: { operatorsTrie: createTrie(defaultOperators) }
|
||||||
} as any
|
} as any
|
||||||
let filters: FilterMap
|
beforeEach(() => { emitter.html = '' })
|
||||||
beforeEach(function () {
|
|
||||||
filters = new FilterMap(false, liquid)
|
|
||||||
emitter.html = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should stringify objects', async function () {
|
it('should stringify objects', async function () {
|
||||||
const scope = new Context({
|
const scope = new Context({
|
||||||
foo: { obj: { arr: ['a', 2] } }
|
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))
|
await toThenable(output.render(scope, emitter))
|
||||||
return expect(emitter.html).to.equal('[object Object]')
|
return expect(emitter.html).to.equal('[object Object]')
|
||||||
})
|
})
|
||||||
it('should skip function property', async function () {
|
it('should skip function property', async function () {
|
||||||
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
|
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))
|
await toThenable(output.render(scope, emitter))
|
||||||
return expect(emitter.html).to.equal('[object Object]')
|
return expect(emitter.html).to.equal('[object Object]')
|
||||||
})
|
})
|
||||||
it('should respect to .toString()', async () => {
|
it('should respect to .toString()', async () => {
|
||||||
const scope = new Context({ obj: { toString: () => 'FOO' } })
|
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))
|
await toThenable(output.render(scope, emitter))
|
||||||
return expect(emitter.html).to.equal('FOO')
|
return expect(emitter.html).to.equal('FOO')
|
||||||
})
|
})
|
||||||
it('should respect to .toString()', async () => {
|
it('should respect to .toString()', async () => {
|
||||||
const scope = new Context({ obj: { toString: () => 'FOO' } })
|
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))
|
await toThenable(output.render(scope, emitter))
|
||||||
return expect(emitter.html).to.equal('FOO')
|
return expect(emitter.html).to.equal('FOO')
|
||||||
})
|
})
|
||||||
@@ -60,16 +55,13 @@ describe('Output', function () {
|
|||||||
keepOutputType: true
|
keepOutputType: true
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => { emitter.html = '' })
|
||||||
filters = new FilterMap(false, liquid)
|
|
||||||
emitter.html = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respect output variable number type', async () => {
|
it('should respect output variable number type', async () => {
|
||||||
const scope = new Context({
|
const scope = new Context({
|
||||||
foo: 42
|
foo: 42
|
||||||
}, { ...defaultOptions, keepOutputType: 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))
|
await toThenable(output.render(scope, emitter))
|
||||||
return expect(emitter.html).to.equal(42)
|
return expect(emitter.html).to.equal(42)
|
||||||
})
|
})
|
||||||
@@ -77,7 +69,7 @@ describe('Output', function () {
|
|||||||
const scope = new Context({
|
const scope = new Context({
|
||||||
foo: true
|
foo: true
|
||||||
}, { ...defaultOptions, keepOutputType: 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))
|
await toThenable(output.render(scope, emitter))
|
||||||
return expect(emitter.html).to.equal(true)
|
return expect(emitter.html).to.equal(true)
|
||||||
})
|
})
|
||||||
@@ -85,7 +77,7 @@ describe('Output', function () {
|
|||||||
const scope = new Context({
|
const scope = new Context({
|
||||||
foo: 'test'
|
foo: 'test'
|
||||||
}, { ...defaultOptions, keepOutputType: 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))
|
await toThenable(output.render(scope, emitter))
|
||||||
return expect(emitter.html).to.equal('test')
|
return expect(emitter.html).to.equal('test')
|
||||||
})
|
})
|
||||||
@@ -93,7 +85,7 @@ describe('Output', function () {
|
|||||||
const scope = new Context({
|
const scope = new Context({
|
||||||
foo: { a: { b: 42 } }
|
foo: { a: { b: 42 } }
|
||||||
}, { ...defaultOptions, keepOutputType: 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))
|
await toThenable(output.render(scope, emitter))
|
||||||
return expect(emitter.html).to.deep.equal({ a: { b: 42 } })
|
return expect(emitter.html).to.deep.equal({ a: { b: 42 } })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,32 +1,22 @@
|
|||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
|
import { Liquid } from '../../../src/liquid'
|
||||||
import { QuotedToken } from '../../../src/tokens/quoted-token'
|
import { QuotedToken } from '../../../src/tokens/quoted-token'
|
||||||
import { toThenable } from '../../../src/util/async'
|
import { toThenable } from '../../../src/util/async'
|
||||||
import { FilterMap } from '../../../src/template/filter/filter-map'
|
|
||||||
import * as sinonChai from 'sinon-chai'
|
import * as sinonChai from 'sinon-chai'
|
||||||
import * as sinon from 'sinon'
|
import * as sinon from 'sinon'
|
||||||
import { Context } from '../../../src/context/context'
|
import { Context } from '../../../src/context/context'
|
||||||
import { Value } from '../../../src/template/value'
|
import { Value } from '../../../src/template/value'
|
||||||
import { createTrie } from '../../../src/util/operator-trie'
|
|
||||||
import { defaultOperators } from '../../../src/types'
|
|
||||||
|
|
||||||
chai.use(sinonChai)
|
chai.use(sinonChai)
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('Value', function () {
|
describe('Value', function () {
|
||||||
const liquid = {
|
const liquid = new Liquid()
|
||||||
options: { operatorsTrie: createTrie(defaultOperators) }
|
|
||||||
} as any
|
|
||||||
|
|
||||||
describe('#constructor()', function () {
|
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 () {
|
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].name).to.equal('foo')
|
||||||
expect(f.filters[0].args).to.have.lengthOf(1)
|
expect(f.filters[0].args).to.have.lengthOf(1)
|
||||||
const [k, v] = f.filters[0].args[0] as any
|
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 () {
|
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()
|
||||||
const filterMap = new FilterMap(false, liquid)
|
liquid.registerFilter('date', date)
|
||||||
filterMap.set('date', date)
|
liquid.registerFilter('time', time)
|
||||||
filterMap.set('time', time)
|
const tpl = new Value('foo.bar | date: "b" | time:2', liquid)
|
||||||
const tpl = new Value('foo.bar | date: "b" | time:2', filterMap, liquid)
|
|
||||||
const scope = new Context({
|
const scope = new Context({
|
||||||
foo: { bar: 'bar' }
|
foo: { bar: 'bar' }
|
||||||
})
|
})
|
||||||
await toThenable(tpl.value(scope))
|
await toThenable(tpl.value(scope, false))
|
||||||
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)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user