Added jsTruthy flag to constuctor in order to enable JS style truthiness evaluation

This commit is contained in:
Amit Gupta
2020-10-08 12:43:37 +08:00
committed by Jun Yang
parent b11df79b91
commit 02bfed1c57
10 changed files with 89 additions and 17 deletions
+6
View File
@@ -65,6 +65,12 @@ Before 2.0.1, <code>extname</code> is set to `.liquid` by default. To change tha
**globals** is used to define global variables available to all templates even in cases of [render tag][render]. See [3185][185] for details.
## jsTruthy
**jsTruthy** is used to use standard Javascript truthiness rather than the Shopify.
it defaults to false. For example, when set to true, a blank string would evaluate to false with jsTruthy. With Shopify's truthiness, a blank string is true.
## Trimming
**greedy**, **trimOutputLeft**, **trimOutputRight**, **trimTagLeft**, **trimTagRight** options are used to eliminate extra newlines and indents in templates arround Liquid Constructs. See [Whitespace Control][wc] for details.
+1 -1
View File
@@ -36,7 +36,7 @@ export function slice<T> (v: T[], begin: number, length = 1): T[] {
export function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
return toArray(arr).filter(obj => {
const value = this.context.getFromScope(obj, String(property).split('.'))
return expected === undefined ? isTruthy(value) : value === expected
return expected === undefined ? isTruthy(value, this.context) : value === expected
})
}
+3 -2
View File
@@ -1,9 +1,10 @@
import { isFalsy } from '../../render/boolean'
import { isArray, isString, toValue } from '../../util/underscore'
import { FilterImpl } from '../../template/filter/filter-impl'
export function Default<T1, T2> (v: string | T1, arg: T2): string | T1 | T2 {
export function Default<T1, T2> (this: FilterImpl, v: string | T1, arg: T2): string | T1 | T2 {
if (isArray(v) || isString(v)) return v.length ? v : arg
return isFalsy(toValue(v)) ? arg : v
return isFalsy(toValue(v), this.context) ? arg : v
}
export function json (v: any) {
return JSON.stringify(v)
+1 -1
View File
@@ -32,7 +32,7 @@ export default {
for (const branch of this.branches) {
const cond = yield new Expression(branch.cond).value(ctx)
if (isTruthy(cond)) {
if (isTruthy(cond, ctx)) {
yield r.renderTemplates(branch.templates, ctx, emitter)
return
}
+1 -1
View File
@@ -23,7 +23,7 @@ export default {
render: function * (ctx: Context, emitter: Emitter) {
const r = this.liquid.renderer
const cond = yield new Expression(this.cond).value(ctx)
yield (isFalsy(cond)
yield (isFalsy(cond, ctx)
? r.renderTemplates(this.templates, ctx, emitter)
: r.renderTemplates(this.elseTemplates, ctx, emitter))
}
+4
View File
@@ -11,6 +11,8 @@ export interface LiquidOptions {
extname?: string;
/** Whether or not to cache resolved templates. Defaults to `false`. */
cache?: boolean | number | Cache<Template[]>;
/** Use Javascript Truthiness.Defaults to `false`. */
jsTruthy?: boolean;
/** If set, treat the `filepath` parameter in `{%include filepath %}` and `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
dynamicPartials?: boolean;
/** Whether or not to assert filter existence. If set to `false`, undefined filters will be skipped. Otherwise, undefined filters will cause an exception. Defaults to `false`. */
@@ -50,6 +52,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
root: string[];
extname: string;
cache: undefined | Cache<Template[]>;
jsTruthy: boolean;
dynamicPartials: boolean;
strictFilters: boolean;
strictVariables: boolean;
@@ -70,6 +73,7 @@ export const defaultOptions: NormalizedFullOptions = {
cache: undefined,
extname: '',
dynamicPartials: true,
jsTruthy: false,
trimTagRight: false,
trimTagLeft: false,
trimOutputRight: false,
+13 -4
View File
@@ -1,6 +1,15 @@
export function isTruthy (val: any): boolean {
return !isFalsy(val)
import { Context } from '../context/context'
export function isTruthy (val: any, ctx: Context): boolean {
if ((val === null) && ctx && ctx.opts && ctx.opts.jsTruthy ) return false;
return !isFalsy(val, ctx)
}
export function isFalsy (val: any): boolean {
return val === false || undefined === val || val === null
export function isFalsy (val: any, ctx: Context): boolean {
if(ctx && ctx.opts && ctx.opts.jsTruthy) {
return val == false
}
else {
return val === false || undefined === val || val === null
}
}
+3 -3
View File
@@ -26,7 +26,7 @@ export class Expression {
if (TypeGuards.isOperatorToken(token)) {
const r = this.operands.pop()
const l = this.operands.pop()
const result = evalOperatorToken(token, l, r)
const result = evalOperatorToken(token, l, r, ctx)
this.operands.push(result)
} else {
this.operands.push(evalToken(token, ctx))
@@ -62,9 +62,9 @@ export function evalQuotedToken (token: QuotedToken) {
return parseStringLiteral(token.getText())
}
function evalOperatorToken (token: OperatorToken, lhs: any, rhs: any) {
function evalOperatorToken (token: OperatorToken, lhs: any, rhs: any, ctx: Context) {
const impl = operatorImpls[token.operator]
return impl(lhs, rhs)
return impl(lhs, rhs, ctx)
}
function evalLiteralToken (token: LiteralToken) {
+5 -3
View File
@@ -1,8 +1,10 @@
import { isComparable } from '../drop/comparable'
import { Context } from '../context/context'
import { isFunction } from '../util/underscore'
import { isTruthy } from '../render/boolean'
export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
export const operatorImpls: {[key: string]: (lhs: any, rhs: any, ctx: Context) => boolean} = {
'==': (l: any, r: any) => {
if (isComparable(l)) return l.equals(r)
if (isComparable(r)) return r.equals(l)
@@ -36,6 +38,6 @@ export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
'contains': (l: any, r: any) => {
return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false
},
'and': (l: any, r: any) => isTruthy(l) && isTruthy(r),
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
'and': (l: any, r: any, ctx: Context) => isTruthy(l, ctx) && isTruthy(r, ctx),
'or': (l: any, r: any, ctx: Context) => isTruthy(l, ctx) || isTruthy(r, ctx)
}
+52 -2
View File
@@ -1,7 +1,7 @@
import { isTruthy } from '../../../src/render/boolean'
import { isTruthy, isFalsy } from '../../../src/render/boolean'
import { expect } from 'chai'
describe('boolean', async function () {
describe('boolean Shopify', async function () {
describe('.isTruthy()', async function () {
// Spec: https://shopify.github.io/liquid/basics/truthy-and-falsy/
it('true is truthy', function () {
@@ -36,3 +36,53 @@ describe('boolean', async function () {
})
})
})
describe('boolean jsTruthy', async function () {
const ctx = {
opts: {
jsTruthy: true
}
}
describe('.isFalsy()', async function () {
it('null is always falsy', function () {
expect(isFalsy(null, ctx)).to.be.false
})
})
describe('.isTruthy()', async function () {
it('true is truthy', function () {
expect(isTruthy(true, ctx)).to.be.true
})
it('false is falsy', function () {
expect(isTruthy(false, ctx)).to.be.false
})
it('null is always falsy', function () {
expect(isTruthy(null, ctx)).to.be.false
})
it('null is always falsy', function () {
expect(isTruthy(null, ctx)).to.be.false
})
it('"foo" is truthy', function () {
expect(isTruthy('foo', ctx)).to.be.true
})
it('"" is falsy', function () {
expect(isTruthy('', ctx)).to.be.false
})
it('0 is falsy', function () {
expect(isTruthy(0, ctx)).to.be.false
})
it('1 is truthy', function () {
expect(isTruthy(1, ctx)).to.be.true
})
it('1.1 is truthy', function () {
expect(isTruthy(1.1, ctx)).to.be.true
})
it('[1] is truthy', function () {
expect(isTruthy([1], ctx)).to.be.true
})
it('[] is falsy', function () {
expect(isTruthy([], ctx)).to.be.false
})
})
})