mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-18 14:00:39 -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 {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user