Skip to content

Commit

Permalink
fix: some filters throw on nil input, see #481
Browse files Browse the repository at this point in the history
  • Loading branch information
harttle committed Feb 26, 2022
1 parent 21b78d9 commit 7dfb620
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 93 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 16 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,21 @@
}
],
[
"@semantic-release/github", {
"@semantic-release/github",
{
"assets": [
{"path": "dist/*.umd.js", "label": "liquid.js"},
{"path": "dist/*.min.js", "label": "liquid.min.js"},
{"path": "dist/*.min.js.map", "label": "liquid.min.js.map"}
{
"path": "dist/*.umd.js",
"label": "liquid.js"
},
{
"path": "dist/*.min.js",
"label": "liquid.min.js"
},
{
"path": "dist/*.min.js.map",
"label": "liquid.min.js.map"
}
]
}
]
Expand All @@ -149,5 +159,6 @@
"pre-commit": "npm run check",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
},
"dependencies": {}
}
42 changes: 30 additions & 12 deletions src/builtin/filters/array.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,74 @@
import { isArray, isNil, last as arrayLast } from '../../util/underscore'
import { argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, hasOwnProperty } from '../../util/underscore'
import { toArray } from '../../util/collection'
import { isTruthy } from '../../render/boolean'
import { FilterImpl } from '../../template/filter/filter-impl'
import { Scope } from '../../context/scope'
import { isComparable } from '../../drop/comparable'

export const join = (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg)
export const last = (v: any) => isArray(v) ? arrayLast(v) : ''
export const first = (v: any) => isArray(v) ? v[0] : ''
export const reverse = (v: any[]) => [...v].reverse()
export const join = argumentsToValue((v: any[], arg: string) => toArray(v).join(arg === undefined ? ' ' : arg))
export const last = argumentsToValue((v: any) => isArray(v) ? arrayLast(v) : '')
export const first = argumentsToValue((v: any) => isArray(v) ? v[0] : '')
export const reverse = argumentsToValue((v: any[]) => [...toArray(v)].reverse())

export function sort<T> (this: FilterImpl, arr: T[], property?: string) {
const getValue = (obj: Scope) => property ? this.context.getFromScope(obj, property.split('.')) : obj
arr = toValue(arr)
const getValue = (obj: Scope) => property ? this.context.getFromScope(obj, stringify(property).split('.')) : obj
return [...toArray(arr)].sort((lhs, rhs) => {
lhs = getValue(lhs)
rhs = getValue(rhs)
return lhs < rhs ? -1 : (lhs > rhs ? 1 : 0)
})
}

export function sortNatural<T> (input: T[], property?: string) {
input = toValue(input)
const propertyString = stringify(property)
const compare = property === undefined
? caseInsensitiveCompare
: (lhs: T, rhs: T) => caseInsensitiveCompare(lhs[propertyString], rhs[propertyString])
return [...toArray(input)].sort(compare)
}

export const size = (v: string | any[]) => (v && v.length) || 0

export function map (this: FilterImpl, arr: Scope[], property: string) {
return toArray(arr).map(obj => this.context.getFromScope(obj, property.split('.')))
arr = toValue(arr)
return toArray(arr).map(obj => this.context.getFromScope(obj, stringify(property).split('.')))
}

export function compact<T> (this: FilterImpl, arr: T[]) {
return toArray(arr).filter(x => !isNil(x))
arr = toValue(arr)
return toArray(arr).filter(x => !isNil(toValue(x)))
}

export function concat<T1, T2> (v: T1[], arg: T2[] | T2): (T1 | T2)[] {
export function concat<T1, T2> (v: T1[], arg: T2[]): (T1 | T2)[] {
v = toValue(v)
return toArray(v).concat(arg)
}

export function slice<T> (v: T[], begin: number, length = 1): T[] {
export function slice<T> (v: T[] | string, begin: number, length = 1): T[] | string {
v = toValue(v)
if (isNil(v)) return []
if (!isArray(v)) v = stringify(v)
begin = begin < 0 ? v.length + begin : begin
return v.slice(begin, begin + length)
}

export function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
arr = toValue(arr)
return toArray(arr).filter(obj => {
const value = this.context.getFromScope(obj, String(property).split('.'))
const value = this.context.getFromScope(obj, stringify(property).split('.'))
if (expected === undefined) return isTruthy(value, this.context)
if (isComparable(expected)) return expected.equals(value)
return value === expected
})
}

export function uniq<T> (arr: T[]): T[] {
arr = toValue(arr)
const u = {}
return (arr || []).filter(val => {
if (u.hasOwnProperty(String(val))) return false
if (hasOwnProperty.call(u, String(val))) return false
u[String(val)] = true
return true
})
Expand Down
4 changes: 3 additions & 1 deletion src/builtin/filters/date.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import strftime from '../../util/strftime'
import { LiquidDate } from '../../util/liquid-date'
import { isString, isNumber } from '../../util/underscore'
import { toValue, stringify, isString, isNumber } from '../../util/underscore'
import { FilterImpl } from '../../template/filter/filter-impl'
import { TimezoneDate } from '../../util/timezone-date'

export function date (this: FilterImpl, v: string | Date, arg: string) {
const opts = this.context.opts
let date: LiquidDate
v = toValue(v)
arg = stringify(arg)
if (v === 'now' || v === 'today') {
date = new Date()
} else if (isNumber(v)) {
Expand Down
8 changes: 4 additions & 4 deletions src/builtin/filters/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ export function escape (str: string) {
}

function unescape (str: string) {
return String(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
}

export function escapeOnce (str: string) {
return escape(unescape(str))
return escape(unescape(stringify(str)))
}

export function newlineToBr (v: string) {
return v.replace(/\n/g, '<br />\n')
return stringify(v).replace(/\n/g, '<br />\n')
}

export function stripHtml (v: string) {
return v.replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
return stringify(v).replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
}
34 changes: 14 additions & 20 deletions src/builtin/filters/math.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
import { caseInsensitiveCompare } from '../../util/underscore'
import { toValue, argumentsToValue } from '../../util/underscore'

export const abs = Math.abs
export const atLeast = Math.max
export const atMost = Math.min
export const ceil = Math.ceil
export const dividedBy = (v: number, arg: number) => v / arg
export const floor = Math.floor
export const minus = (v: number, arg: number) => v - arg
export const modulo = (v: number, arg: number) => v % arg
export const times = (v: number, arg: number) => v * arg
export const abs = argumentsToValue(Math.abs)
export const atLeast = argumentsToValue(Math.max)
export const atMost = argumentsToValue(Math.min)
export const ceil = argumentsToValue(Math.ceil)
export const dividedBy = argumentsToValue((v: number, arg: number) => v / arg)
export const floor = argumentsToValue(Math.floor)
export const minus = argumentsToValue((v: number, arg: number) => v - arg)
export const modulo = argumentsToValue((v: number, arg: number) => v % arg)
export const times = argumentsToValue((v: number, arg: number) => v * arg)

export function round (v: number, arg = 0) {
v = toValue(v)
arg = toValue(arg)
const amp = Math.pow(10, arg)
return Math.round(v * amp) / amp
}

export function plus (v: number, arg: number) {
v = toValue(v)
arg = toValue(arg)
return Number(v) + Number(arg)
}

export function sortNatural (input: any[], property?: string) {
if (!input || !input.sort) return []
if (property !== undefined) {
return [...input].sort(
(lhs, rhs) => caseInsensitiveCompare(lhs[property], rhs[property])
)
}
return [...input].sort(caseInsensitiveCompare)
}
2 changes: 1 addition & 1 deletion src/builtin/filters/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { isArray, isString, toValue } from '../../util/underscore'
import { FilterImpl } from '../../template/filter/filter-impl'

export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 {
if (isArray(value) || isString(value)) return value.length ? value : defaultValue
value = toValue(value)
if (isArray(value) || isString(value)) return value.length ? value : defaultValue
if (value === false && (new Map(args)).get('allow_false')) return false as T1
return isFalsy(value, this.context) ? defaultValue : value
}
Expand Down
10 changes: 7 additions & 3 deletions src/builtin/filters/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export function rstrip (str: string) {
}

export function split (v: string, arg: string) {
return stringify(v).split(String(arg))
const arr = stringify(v).split(String(arg))
// align to ruby split, which is the behavior of shopify/liquid
// see: https://ruby-doc.org/core-2.4.0/String.html#method-i-split
while (arr.length && arr[arr.length - 1] === '') arr.pop()
return arr
}

export function strip (v: string) {
Expand All @@ -68,11 +72,11 @@ export function replaceFirst (v: string, arg1: string, arg2: string) {
export function truncate (v: string, l = 50, o = '...') {
v = stringify(v)
if (v.length <= l) return v
return v.substr(0, l - o.length) + o
return v.substring(0, l - o.length) + o
}

export function truncatewords (v: string, l = 15, o = '...') {
const arr = v.split(/\s+/)
const arr = stringify(v).split(/\s+/)
let ret = arr.slice(0, l).join(' ')
if (arr.length >= l) ret += o
return ret
Expand Down
3 changes: 2 additions & 1 deletion src/util/collection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isString, isObject, isArray } from './underscore'
import { isNil, isString, isObject, isArray } from './underscore'

export function toEnumerable (val: any) {
if (isArray(val)) return val
Expand All @@ -8,6 +8,7 @@ export function toEnumerable (val: any) {
}

export function toArray (val: any) {
if (isNil(val)) return []
if (isArray(val)) return val
return [ val ]
}
8 changes: 7 additions & 1 deletion src/util/underscore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Drop } from '../drop/drop'
const toStr = Object.prototype.toString
const toLowerCase = String.prototype.toLowerCase

export const hasOwnProperty = Object.hasOwnProperty

export function isString (value: any): value is string {
return typeof value === 'string'
}
Expand Down Expand Up @@ -72,7 +74,7 @@ export function forOwn <T> (
) {
obj = obj || {}
for (const k in obj) {
if (Object.hasOwnProperty.call(obj, k)) {
if (hasOwnProperty.call(obj, k)) {
if (iteratee(obj[k], k, obj) === false) break
}
}
Expand Down Expand Up @@ -150,3 +152,7 @@ export function caseInsensitiveCompare (a: any, b: any) {
if (a > b) return 1
return 0
}

export function argumentsToValue<F extends (...args: any) => any> (fn: F) {
return (...args: Parameters<F>) => fn(...args.map(toValue))
}
14 changes: 14 additions & 0 deletions test/e2e/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,18 @@ describe('Issues', function () {
const html = await engine.render(tpl, { v: undefined })
expect(html).to.equal('')
})
it('#481 filters that should not throw', async () => {
const engine = new Liquid()
const tpl = engine.parse(`
{{ foo | join }}
{{ foo | map: "k" }}
{{ foo | reverse }}
{{ foo | slice: 2 }}
{{ foo | newline_to_br }}
{{ foo | strip_html }}
{{ foo | truncatewords }}
`)
const html = await engine.render(tpl, { foo: undefined })
expect(html.trim()).to.equal('')
})
})
Loading

0 comments on commit 7dfb620

Please sign in to comment.