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 + }) + }) +})