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

This commit is contained in:
harttle
2021-02-12 22:22:41 +08:00
parent 4e82da689e
commit 2f059f6a25
18 changed files with 164 additions and 176 deletions
+6 -8
View File
@@ -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
+4 -5
View File
@@ -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
+5 -6
View File
@@ -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
+2 -2
View File
@@ -101,8 +101,8 @@ export class Liquid {
}
public _evalValue (str: string, ctx: Context): IterableIterator<any> {
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<any> {
return toPromise(this._evalValue(str, ctx))
+1 -1
View File
@@ -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) {
+6 -1
View File
@@ -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<Token> {
readExpression () {
return new Expression(this.readExpressionTokens())
}
* readExpressionTokens (): IterableIterator<Token> {
const operand = this.readValue()
if (!operand) return
+25 -35
View File
@@ -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<Token>) {
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)
+4 -4
View File
@@ -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])
}
}
+3 -4
View File
@@ -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<OutputToken> 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)
}
}
+8 -10
View File
@@ -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)
}
+4 -1
View File
@@ -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'