mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-18 05:50:43 -07:00
Implement lenientIf option (resolve #265; default off)
See description of the option in liquid-options.ts.
This commit is contained in:
committed by
Jun Yang
parent
c920ebb282
commit
768fb79e32
@@ -31,7 +31,7 @@ export default {
|
|||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
|
|
||||||
for (const branch of this.branches) {
|
for (const branch of this.branches) {
|
||||||
const cond = yield new Expression(branch.cond).value(ctx)
|
const cond = yield new Expression(branch.cond, ctx.opts.lenientIf).value(ctx)
|
||||||
if (isTruthy(cond, ctx)) {
|
if (isTruthy(cond, ctx)) {
|
||||||
yield r.renderTemplates(branch.templates, ctx, emitter)
|
yield r.renderTemplates(branch.templates, ctx, emitter)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export interface LiquidOptions {
|
|||||||
strictFilters?: boolean;
|
strictFilters?: boolean;
|
||||||
/** Whether or not to assert variable existence. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause an exception. Defaults to `false`. */
|
/** Whether or not to assert variable existence. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause an exception. Defaults to `false`. */
|
||||||
strictVariables?: boolean;
|
strictVariables?: boolean;
|
||||||
|
/** Modifies the behavior of `strictVariables`. If set, a single undefined variable will *not* cause an exception in the context of the `if` tag and the `default` filter. Instead, it will evaluate to `false` and `null`, respectively. Irrelevant if `strictVariables` is not set. Defaults to `false`. **/
|
||||||
|
lenientIf?: boolean;
|
||||||
/** Strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
|
/** Strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
|
||||||
trimTagRight?: boolean;
|
trimTagRight?: boolean;
|
||||||
/** Similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
/** Similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
||||||
@@ -56,6 +58,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
|
|||||||
dynamicPartials: boolean;
|
dynamicPartials: boolean;
|
||||||
strictFilters: boolean;
|
strictFilters: boolean;
|
||||||
strictVariables: boolean;
|
strictVariables: boolean;
|
||||||
|
lenientIf: boolean;
|
||||||
trimTagRight: boolean;
|
trimTagRight: boolean;
|
||||||
trimTagLeft: boolean;
|
trimTagLeft: boolean;
|
||||||
trimOutputRight: boolean;
|
trimOutputRight: boolean;
|
||||||
@@ -85,6 +88,7 @@ export const defaultOptions: NormalizedFullOptions = {
|
|||||||
outputDelimiterRight: '}}',
|
outputDelimiterRight: '}}',
|
||||||
strictFilters: false,
|
strictFilters: false,
|
||||||
strictVariables: false,
|
strictVariables: false,
|
||||||
|
lenientIf: false,
|
||||||
globals: {}
|
globals: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,21 +16,32 @@ import { operatorImpls } from '../render/operator'
|
|||||||
export class Expression {
|
export class Expression {
|
||||||
private operands: any[] = []
|
private operands: any[] = []
|
||||||
private postfix: IterableIterator<Token>
|
private postfix: IterableIterator<Token>
|
||||||
|
private lenient: boolean
|
||||||
|
|
||||||
public constructor (str: string) {
|
public constructor (str: string, lenient: boolean = false) {
|
||||||
const tokenizer = new Tokenizer(str)
|
const tokenizer = new Tokenizer(str)
|
||||||
this.postfix = toPostfix(tokenizer.readExpression())
|
this.postfix = toPostfix(tokenizer.readExpression())
|
||||||
|
this.lenient = lenient
|
||||||
}
|
}
|
||||||
public evaluate (ctx: Context): any {
|
public evaluate (ctx: Context): any {
|
||||||
for (const token of this.postfix) {
|
// we manually loop over the iterator to tell if it's a single variable, for lenient.
|
||||||
|
let iterResult = this.postfix.next()
|
||||||
|
let isFirstToken = true
|
||||||
|
while (!iterResult.done) {
|
||||||
|
const token = iterResult.value
|
||||||
|
iterResult = this.postfix.next()
|
||||||
|
const isLastToken = iterResult.done
|
||||||
|
|
||||||
if (TypeGuards.isOperatorToken(token)) {
|
if (TypeGuards.isOperatorToken(token)) {
|
||||||
const r = this.operands.pop()
|
const r = this.operands.pop()
|
||||||
const l = this.operands.pop()
|
const l = this.operands.pop()
|
||||||
const result = evalOperatorToken(token, l, r, ctx)
|
const result = evalOperatorToken(token, l, r, ctx)
|
||||||
this.operands.push(result)
|
this.operands.push(result)
|
||||||
} else {
|
} else {
|
||||||
this.operands.push(evalToken(token, ctx))
|
this.operands.push(evalToken(token, ctx, this.lenient && isFirstToken && isLastToken))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFirstToken = false
|
||||||
}
|
}
|
||||||
return this.operands[0]
|
return this.operands[0]
|
||||||
}
|
}
|
||||||
@@ -39,12 +50,22 @@ export class Expression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function evalToken (token: Token | undefined, ctx: Context): any {
|
export function evalToken (token: Token | undefined, ctx: Context, lenient: boolean = false): any {
|
||||||
assert(ctx, () => 'unable to evaluate: context not defined')
|
assert(ctx, () => 'unable to evaluate: context not defined')
|
||||||
if (TypeGuards.isPropertyAccessToken(token)) {
|
if (TypeGuards.isPropertyAccessToken(token)) {
|
||||||
const variable = token.getVariableAsText()
|
const variable = token.getVariableAsText()
|
||||||
const props: string[] = token.props.map(prop => evalToken(prop, ctx))
|
const props: string[] = token.props.map(prop => evalToken(prop, ctx))
|
||||||
return ctx.get([variable, ...props])
|
try {
|
||||||
|
return ctx.get([variable, ...props])
|
||||||
|
} catch (e) {
|
||||||
|
// we catch the error thrown by Context.getFromScope() for undefined vars.
|
||||||
|
// Alt, we could make this more robust by setting a flag or using a separate error class.
|
||||||
|
if (lenient && e instanceof TypeError && e.message.startsWith("undefined variable:")) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
throw(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { ValueToken } from '../tokens/value-token'
|
||||||
|
import { assert } from '../util/assert'
|
||||||
|
|
||||||
export class Value {
|
export class Value {
|
||||||
public readonly filters: Filter[] = []
|
public readonly filters: Filter[] = []
|
||||||
@@ -18,7 +19,10 @@ export class Value {
|
|||||||
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args))
|
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args))
|
||||||
}
|
}
|
||||||
public * value (ctx: Context) {
|
public * value (ctx: Context) {
|
||||||
let val = yield evalToken(this.initial, ctx)
|
assert(ctx, () => 'unable to evaluate: context not defined')
|
||||||
|
const lenient = ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name == "default"
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,4 +30,30 @@ describe('LiquidOptions#strict*', function () {
|
|||||||
return expect(engine.parseAndRender(html, ctx, opts)).to
|
return expect(engine.parseAndRender(html, ctx, opts)).to
|
||||||
.be.rejectedWith(/undefined variable: notdefined/)
|
.be.rejectedWith(/undefined variable: notdefined/)
|
||||||
})
|
})
|
||||||
|
describe('with strictVariables and lenientIf', function() {
|
||||||
|
const strictLenientOpts = {
|
||||||
|
strictVariables: true,
|
||||||
|
lenientIf: true
|
||||||
|
}
|
||||||
|
it('should not throw in `if` with a single variable', async function () {
|
||||||
|
const tpl = engine.parse('before{% if notdefined %}{{notdefined}}{% endif %}after')
|
||||||
|
const html = await engine.render(tpl, ctx, strictLenientOpts)
|
||||||
|
return expect(html).to.equal('beforeafter')
|
||||||
|
})
|
||||||
|
it('should support elsif with undefined variables', async function () {
|
||||||
|
const tpl = engine.parse('{% if notdefined1 %}a{% elsif notdefined2 %}b{% elsif defined3 %}{{defined3}}{% else %}d{% endif %}')
|
||||||
|
const html = await engine.render(tpl, {'defined3': 'bla'}, strictLenientOpts)
|
||||||
|
return expect(html).to.equal('bla')
|
||||||
|
})
|
||||||
|
it('should still throw with an undefined variable in an expression', function () {
|
||||||
|
const tpl = engine.parse('{% if notdefined == 15 %}a{% endif %}')
|
||||||
|
const fhtml = engine.render(tpl, ctx, strictLenientOpts)
|
||||||
|
return expect(fhtml).to.be.rejectedWith(/undefined variable: notdefined/)
|
||||||
|
})
|
||||||
|
it('should allow an undefined variable when before the `default` filter', async function () {
|
||||||
|
const tpl = engine.parse('{{notdefined | default: "a" | tolower}}')
|
||||||
|
const html = await engine.render(tpl, ctx, strictLenientOpts)
|
||||||
|
return expect(html).to.equal('a')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user