diff --git a/docs/source/tutorials/options.md b/docs/source/tutorials/options.md
index 1dbe9ae5e7..3b22523f15 100644
--- a/docs/source/tutorials/options.md
+++ b/docs/source/tutorials/options.md
@@ -65,6 +65,12 @@ Before 2.0.1, extname
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.
diff --git a/src/builtin/filters/array.ts b/src/builtin/filters/array.ts
index 2243112122..31cff4142b 100644
--- a/src/builtin/filters/array.ts
+++ b/src/builtin/filters/array.ts
@@ -36,7 +36,7 @@ export function slice (v: T[], begin: number, length = 1): T[] {
export function where (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
})
}
diff --git a/src/builtin/filters/object.ts b/src/builtin/filters/object.ts
index e3c818a2cc..26a880724c 100644
--- a/src/builtin/filters/object.ts
+++ b/src/builtin/filters/object.ts
@@ -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 (v: string | T1, arg: T2): string | T1 | T2 {
+export function Default (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)
diff --git a/src/builtin/tags/if.ts b/src/builtin/tags/if.ts
index 2dc50167a7..b0be01eb5b 100644
--- a/src/builtin/tags/if.ts
+++ b/src/builtin/tags/if.ts
@@ -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
}
diff --git a/src/builtin/tags/unless.ts b/src/builtin/tags/unless.ts
index 643e48b19c..3bae426124 100644
--- a/src/builtin/tags/unless.ts
+++ b/src/builtin/tags/unless.ts
@@ -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))
}
diff --git a/src/liquid-options.ts b/src/liquid-options.ts
index c283f4517a..735c067c3a 100644
--- a/src/liquid-options.ts
+++ b/src/liquid-options.ts
@@ -11,6 +11,8 @@ export interface LiquidOptions {
extname?: string;
/** Whether or not to cache resolved templates. Defaults to `false`. */
cache?: boolean | number | Cache;
+ /** 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;
+ 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,
diff --git a/src/render/boolean.ts b/src/render/boolean.ts
index e561252b7c..6c51d63bf6 100644
--- a/src/render/boolean.ts
+++ b/src/render/boolean.ts
@@ -1,6 +1,14 @@
-export function isTruthy (val: any): boolean {
- return !isFalsy(val)
+import { Context } from '../context/context'
+
+export function isTruthy (val: any, ctx: Context): boolean {
+ 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.opts.jsTruthy) {
+ return !val
+ }
+ else {
+ return val === false || undefined === val || val === null
+ }
}
diff --git a/src/render/expression.ts b/src/render/expression.ts
index 26655dcda8..ffe8ff3e5f 100644
--- a/src/render/expression.ts
+++ b/src/render/expression.ts
@@ -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) {
diff --git a/src/render/operator.ts b/src/render/operator.ts
index 0c0156b8d0..1863cc1cac 100644
--- a/src/render/operator.ts
+++ b/src/render/operator.ts
@@ -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)
}
diff --git a/test/unit/render/boolean.ts b/test/unit/render/boolean.ts
index af4000c9f8..4115a4912b 100644
--- a/test/unit/render/boolean.ts
+++ b/test/unit/render/boolean.ts
@@ -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.true
+ })
+ 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
+ })
+ })
+})