Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/source/tutorials/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/filters/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}

Expand Down
5 changes: 3 additions & 2 deletions src/builtin/filters/object.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/unless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
4 changes: 4 additions & 0 deletions src/liquid-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`. */
Expand Down Expand Up @@ -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;
Expand All @@ -70,6 +73,7 @@ export const defaultOptions: NormalizedFullOptions = {
cache: undefined,
extname: '',
dynamicPartials: true,
jsTruthy: false,
trimTagRight: false,
trimTagLeft: false,
trimOutputRight: false,
Expand Down
17 changes: 13 additions & 4 deletions src/render/boolean.ts
Original file line number Diff line number Diff line change
@@ -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;
amit777 marked this conversation as resolved.
Show resolved Hide resolved
amit777 marked this conversation as resolved.
Show resolved Hide resolved
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
amit777 marked this conversation as resolved.
Show resolved Hide resolved
}
else {
return val === false || undefined === val || val === null
}
}
6 changes: 3 additions & 3 deletions src/render/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 5 additions & 3 deletions src/render/operator.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)
}
54 changes: 52 additions & 2 deletions test/unit/render/boolean.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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
})
})
})