fix: some filters throw on nil input, see #481

This commit is contained in:
Harttle
2022-02-27 02:07:59 +08:00
parent 21b78d9ba0
commit 7dfb620d30
14 changed files with 182 additions and 93 deletions
+30 -12
View File
@@ -1,17 +1,18 @@
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)
@@ -19,28 +20,44 @@ export function sort<T> (this: FilterImpl, arr: T[], property?: string) {
})
}
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
@@ -48,9 +65,10 @@ export function where<T extends object> (this: FilterImpl, arr: T[], property: s
}
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
})
+3 -1
View File
@@ -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)) {
+4 -4
View File
@@ -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, '')
}
+14 -20
View File
@@ -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)
}
+1 -1
View File
@@ -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
}
+7 -3
View File
@@ -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) {
@@ -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
+2 -1
View File
@@ -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
@@ -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 ]
}
+7 -1
View File
@@ -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'
}
@@ -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
}
}
@@ -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))
}