Skip to content

Commit 6e6ea0a

Browse files
committed
fix: some filters on undefined variable throws, #140
1 parent dc9a6e0 commit 6e6ea0a

File tree

8 files changed

+229
-81
lines changed

8 files changed

+229
-81
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ though there are still some differences:
3131

3232
* Dynamic file locating (enabled by default), that means layout/partial names are treated as variables in liquidjs. See [#51](https://github.com/harttle/liquidjs/issues/51).
3333
* Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26](https://github.com/harttle/liquidjs/pull/26).
34-
* Number Rendering. Since JavaScript do not distinguish `float` and `integer`, we cannot either convert between them nor render regarding to their type. See [#59](https://github.com/harttle/liquidjs/issues/59).
34+
* Number. In JavaScript we cannot distinguish or convert between `float` and `integer`, see [#59](https://github.com/harttle/liquidjs/issues/59). And when applied `size` filter, numbers always return 0, which is 8 for integer in ruby, cause they do not have a `length` property.
3535
* [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops) is replaced by `.toLiquid()`
3636
* [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) is replaced by JavaScript `.toString()`
3737

src/builtin/filters/array.ts

+35-21
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,42 @@
1-
import { last } from '../../util/underscore'
1+
import { isArray, last } from '../../util/underscore'
22
import { isTruthy } from '../../render/syntax'
33

44
export default {
55
'join': (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg),
6-
'last': <T>(v: T[]): T => last(v),
7-
'first': <T>(v: T[]): T => v[0],
8-
'map': <T1, T2>(arr: {[key: string]: T1}[], arg: string): T1[] => arr.map(v => v[arg]),
6+
'last': (v: any) => isArray(v) ? last(v) : '',
7+
'first': (v: any) => isArray(v) ? v[0] : '',
8+
'map': map,
99
'reverse': (v: any[]) => [...v].reverse(),
1010
'sort': <T>(v: T[], arg: (lhs: T, rhs: T) => number) => v.sort(arg),
11-
'size': (v: string | any[]) => v.length,
12-
'concat': <T1, T2>(v: T1[], arg: T2[] | T2): (T1 | T2)[] => Array.prototype.concat.call(v, arg),
13-
'slice': <T>(v: T[], begin: number, length: number = 1): T[] => {
14-
begin = begin < 0 ? v.length + begin : begin
15-
return v.slice(begin, begin + length)
16-
},
17-
'uniq': function<T> (arr: T[]): T[] {
18-
const u = {}
19-
return (arr || []).filter(val => {
20-
if (u.hasOwnProperty(String(val))) return false
21-
u[String(val)] = true
22-
return true
23-
})
24-
},
25-
'where': function<T> (arr: T[], property: string, value?: any): T[] {
26-
return arr.filter(obj => value === undefined ? isTruthy(obj[property]) : obj[property] === value)
27-
}
11+
'size': (v: string | any[]) => (v && v.length) || 0,
12+
'concat': concat,
13+
'slice': slice,
14+
'uniq': uniq,
15+
'where': where
16+
}
17+
18+
function map<T1, T2> (arr: {[key: string]: T1}[], arg: string): T1[] {
19+
return arr.map(v => v[arg])
20+
}
21+
22+
function concat<T1, T2> (v: T1[], arg: T2[] | T2): (T1 | T2)[] {
23+
return Array.prototype.concat.call(v, arg)
24+
}
25+
26+
function slice<T> (v: T[], begin: number, length: number = 1): T[] {
27+
begin = begin < 0 ? v.length + begin : begin
28+
return v.slice(begin, begin + length)
29+
}
30+
31+
function where<T> (arr: T[], property: string, value?: any): T[] {
32+
return arr.filter(obj => value === undefined ? isTruthy(obj[property]) : obj[property] === value)
33+
}
34+
35+
function uniq<T> (arr: T[]): T[] {
36+
const u = {}
37+
return (arr || []).filter(val => {
38+
if (u.hasOwnProperty(String(val))) return false
39+
u[String(val)] = true
40+
return true
41+
})
2842
}

src/builtin/filters/string.ts

+49-26
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,51 @@
1+
/**
2+
* String related filters
3+
*
4+
* * prefer stringify() to String() since `undefined`, `null` should eval ''
5+
*/
6+
import { stringify } from '../../util/underscore'
7+
18
export default {
2-
'append': (v: string, arg: string) => v + arg,
3-
'prepend': (v: string, arg: string) => arg + v,
4-
'capitalize': (str: string) => String(str).charAt(0).toUpperCase() + str.slice(1),
5-
'lstrip': (v: string) => String(v).replace(/^\s+/, ''),
6-
'downcase': (v: string) => v.toLowerCase(),
7-
'upcase': (str: string) => String(str).toUpperCase(),
8-
'remove': (v: string, arg: string) => v.split(arg).join(''),
9-
'remove_first': (v: string, l: string) => v.replace(l, ''),
10-
'replace': (v: string, pattern: string, replacement: string) =>
11-
String(v).split(pattern).join(replacement),
12-
'replace_first': (v: string, arg1: string, arg2: string) => String(v).replace(arg1, arg2),
13-
'rstrip': (str: string) => String(str).replace(/\s+$/, ''),
14-
'split': (v: string, arg: string) => String(v).split(arg),
15-
'strip': (v: string) => String(v).trim(),
16-
'strip_newlines': (v: string) => String(v).replace(/\n/g, ''),
17-
'truncate': (v: string, l: number = 50, o: string = '...') => {
18-
v = String(v)
19-
if (v.length <= l) return v
20-
return v.substr(0, l - o.length) + o
21-
},
22-
'truncatewords': (v: string, l: number = 15, o: string = '...') => {
23-
const arr = v.split(/\s+/)
24-
let ret = arr.slice(0, l).join(' ')
25-
if (arr.length >= l) ret += o
26-
return ret
27-
}
9+
'append': (v: string, arg: string) => stringify(v) + arg,
10+
'prepend': (v: string, arg: string) => arg + stringify(v),
11+
'capitalize': capitalize,
12+
'lstrip': (v: string) => stringify(v).replace(/^\s+/, ''),
13+
'downcase': (v: string) => stringify(v).toLowerCase(),
14+
'upcase': (str: string) => stringify(str).toUpperCase(),
15+
'remove': (v: string, arg: string) => stringify(v).split(arg).join(''),
16+
'remove_first': (v: string, l: string) => stringify(v).replace(l, ''),
17+
'replace': replace,
18+
'replace_first': replaceFirst,
19+
'rstrip': (str: string) => stringify(str).replace(/\s+$/, ''),
20+
'split': (v: string, arg: string) => stringify(v).split(arg),
21+
'strip': (v: string) => stringify(v).trim(),
22+
'strip_newlines': (v: string) => stringify(v).replace(/\n/g, ''),
23+
'truncate': truncate,
24+
'truncatewords': truncateWords
25+
}
26+
27+
function capitalize (str: string) {
28+
str = stringify(str)
29+
return str.charAt(0).toUpperCase() + str.slice(1)
30+
}
31+
32+
function replace (v: string, pattern: string, replacement: string) {
33+
return stringify(v).split(pattern).join(replacement)
34+
}
35+
36+
function replaceFirst (v: string, arg1: string, arg2: string) {
37+
return stringify(v).replace(arg1, arg2)
38+
}
39+
40+
function truncate (v: string, l: number = 50, o: string = '...') {
41+
v = stringify(v)
42+
if (v.length <= l) return v
43+
return v.substr(0, l - o.length) + o
44+
}
45+
46+
function truncateWords (v: string, l: number = 15, o: string = '...') {
47+
const arr = v.split(/\s+/)
48+
let ret = arr.slice(0, l).join(' ')
49+
if (arr.length >= l) ret += o
50+
return ret
2851
}

src/util/underscore.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ export function promisify (fn: any) {
2828
}
2929

3030
export function stringify (value: any): string {
31-
if (isNil(value)) return ''
32-
value = toLiquid(value)
33-
return String(value)
31+
value = toValue(value)
32+
return isNil(value) ? '' : String(value)
3433
}
3534

3635
export function toValue (value: any): any {
@@ -42,7 +41,7 @@ export function isNumber (value: any): value is number {
4241
}
4342

4443
export function toLiquid (value: any): any {
45-
if (isFunction(value.toLiquid)) return toLiquid(value.toLiquid())
44+
if (value && isFunction(value.toLiquid)) return toLiquid(value.toLiquid())
4645
return value
4746
}
4847

test/integration/builtin/filters/array.ts

+79-11
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,85 @@ describe('filters/array', function () {
4040
})
4141
})
4242
describe('size', function () {
43-
it('should return string length',
44-
() => test('{{ "Ground control to Major Tom." | size }}', '28'))
45-
it('should return array size', function () {
46-
return test('{% assign my_array = "apples, oranges, peaches, plums"' +
47-
' | split: ", " %}{{ my_array | size }}',
48-
'4')
49-
})
50-
it('should be respected with <string>.size notation',
51-
() => test('{% assign my_string = "Ground control to Major Tom." %}{{ my_string.size }}', '28'))
52-
it('should be respected with <array>.size notation',
53-
() => test('{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.size }}', '4'))
43+
it('should return string length', async () => {
44+
const html = await liquid.parseAndRender('{{ "Ground control to Major Tom." | size }}')
45+
expect(html).to.equal('28')
46+
})
47+
it('should return array size', async () => {
48+
const html = await liquid.parseAndRender(
49+
'{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array | size }}')
50+
expect(html).to.equal('4')
51+
})
52+
it('should be respected with <string>.size notation', async () => {
53+
const html = await liquid.parseAndRender('{% assign my_string = "Ground control to Major Tom." %}{{ my_string.size }}')
54+
expect(html).to.equal('28')
55+
})
56+
it('should be respected with <array>.size notation', async () => {
57+
const html = await liquid.parseAndRender('{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.size }}')
58+
expect(html).to.equal('4')
59+
})
60+
it('should return 0 for false', async () => {
61+
const html = await liquid.parseAndRender('{{ false | size }}')
62+
expect(html).to.equal('0')
63+
})
64+
it('should return 0 for nil', async () => {
65+
const html = await liquid.parseAndRender('{{ nil | size }}')
66+
expect(html).to.equal('0')
67+
})
68+
it('should return 0 for undefined', async () => {
69+
const html = await liquid.parseAndRender('{{ foo | size }}')
70+
expect(html).to.equal('0')
71+
})
72+
})
73+
describe('first', function () {
74+
it('should support first', async () => {
75+
const html = await liquid.parseAndRender(
76+
'{{arr | first}}',
77+
{ arr: [ 'zebra', 'tiger' ] }
78+
)
79+
expect(html).to.equal('zebra')
80+
})
81+
it('should return empty for nil', async () => {
82+
const html = await liquid.parseAndRender('{{nil | first}}')
83+
expect(html).to.equal('')
84+
})
85+
it('should return empty for undefined', async () => {
86+
const html = await liquid.parseAndRender('{{foo | first}}')
87+
expect(html).to.equal('')
88+
})
89+
it('should return empty for false', async () => {
90+
const html = await liquid.parseAndRender('{{false | first}}')
91+
expect(html).to.equal('')
92+
})
93+
it('should return empty for string', async () => {
94+
const html = await liquid.parseAndRender('{{"zebra" | first}}')
95+
expect(html).to.equal('')
96+
})
97+
})
98+
describe('last', function () {
99+
it('should support last', async () => {
100+
const html = await liquid.parseAndRender(
101+
'{{arr | last}}',
102+
{ arr: [ 'zebra', 'tiger' ] }
103+
)
104+
expect(html).to.equal('tiger')
105+
})
106+
it('should return empty for nil', async () => {
107+
const html = await liquid.parseAndRender('{{nil | last}}')
108+
expect(html).to.equal('')
109+
})
110+
it('should return empty for undefined', async () => {
111+
const html = await liquid.parseAndRender('{{foo | last}}')
112+
expect(html).to.equal('')
113+
})
114+
it('should return empty for false', async () => {
115+
const html = await liquid.parseAndRender('{{false | last}}')
116+
expect(html).to.equal('')
117+
})
118+
it('should return empty for string', async () => {
119+
const html = await liquid.parseAndRender('{{"zebra" | last}}')
120+
expect(html).to.equal('')
121+
})
54122
})
55123
describe('slice', function () {
56124
it('should slice first char by 0', () => test('{{ "Liquid" | slice: 0 }}', 'L'))

test/integration/builtin/filters/math.ts

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ describe('filters/math', function () {
6060
() => test('{{ 183.357 | plus: 12 }}', '195.357'))
6161
it('should convert first arg as number', () => test('{{ "4" | plus: 2 }}', '6'))
6262
it('should convert both args as number', () => test('{{ "4" | plus: "2" }}', '6'))
63+
it('should support variable', async () => {
64+
const html = await l.parseAndRender('{{ 4 | plus: b }}', { b: 2 })
65+
expect(html).to.equal('6')
66+
})
6367
})
6468

6569
describe('sort_natural', function () {

test/integration/builtin/filters/string.ts

+58-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
import { test } from '../../../stub/render'
2+
import Liquid from '../../../../src/liquid'
3+
import { expect } from 'chai'
24

35
describe('filters/string', function () {
6+
let liquid: Liquid
7+
beforeEach(function () {
8+
liquid = new Liquid()
9+
})
410
describe('append', function () {
511
it('should return "-3abc" for -3, "abc"',
612
() => test('{{ -3 | append: "abc" }}', '-3abc'))
713
it('should return "abar" for "a",foo', () => test('{{ "a" | append: foo }}', 'abar'))
814
})
915
describe('capitalize', function () {
10-
it('should capitalize first', () => test('{{ "i am good" | capitalize }}', 'I am good'))
16+
it('should capitalize first', async () => {
17+
const html = await liquid.parseAndRender('{{ "i am good" | capitalize }}')
18+
expect(html).to.equal('I am good')
19+
})
20+
it('should return empty for nil', async () => {
21+
const html = await liquid.parseAndRender('{{ nil | capitalize }}')
22+
expect(html).to.equal('')
23+
})
24+
it('should return empty for undefined', async () => {
25+
const html = await liquid.parseAndRender('{{ foo | capitalize }}')
26+
expect(html).to.equal('')
27+
})
1128
})
1229
describe('concat', function () {
1330
it('should concat arrays', () => test(`
@@ -45,10 +62,18 @@ describe('filters/string', function () {
4562
`))
4663
})
4764
describe('downcase', function () {
48-
it('should return "parker moore" for "Parker Moore"',
49-
() => test('{{ "Parker Moore" | downcase }}', 'parker moore'))
50-
it('should return "apple" for "apple"',
51-
() => test('{{ "apple" | downcase }}', 'apple'))
65+
it('should return "parker moore" for "Parker Moore"', async () => {
66+
const html = await liquid.parseAndRender('{{ "Parker Moore" | downcase }}')
67+
expect(html).to.equal('parker moore')
68+
})
69+
it('should return "apple" for "apple"', async () => {
70+
const html = await liquid.parseAndRender('{{ "apple" | downcase }}')
71+
expect(html).to.equal('apple')
72+
})
73+
it('should return empty for undefined', async () => {
74+
const html = await liquid.parseAndRender('{{ foo | downcase }}')
75+
expect(html).to.equal('')
76+
})
5277
})
5378
describe('split', function () {
5479
it('should support split/first', function () {
@@ -57,7 +82,16 @@ describe('filters/string', function () {
5782
return test(src, 'apples')
5883
})
5984
})
60-
it('should support upcase', () => test('{{ "Parker Moore" | upcase }}', 'PARKER MOORE'))
85+
describe('upcase', function () {
86+
it('should support upcase', async () => {
87+
const html = await liquid.parseAndRender('{{ "Parker Moore" | upcase }}')
88+
expect(html).to.equal('PARKER MOORE')
89+
})
90+
it('should return empty for undefined', async () => {
91+
const html = await liquid.parseAndRender('{{ foo | upcase }}')
92+
expect(html).to.equal('')
93+
})
94+
})
6195
it('should support lstrip', function () {
6296
const src = '{{ " So much room for activities! " | lstrip }}'
6397
return test(src, 'So much room for activities! ')
@@ -78,13 +112,25 @@ describe('filters/string', function () {
78112
'{{ "/index.html" | prepend: url }}',
79113
'liquidmarkup.com/index.html')
80114
})
81-
it('should support remove', function () {
82-
return test('{{ "I strained to see the train through the rain" | remove: "rain" }}',
83-
'I sted to see the t through the ')
115+
describe('remove', function () {
116+
it('should support remove', async () => {
117+
const html = await liquid.parseAndRender('{{ "I strained to see the train through the rain" | remove: "rain" }}')
118+
expect(html).to.equal('I sted to see the t through the ')
119+
})
120+
it('should return empty for undefined', async () => {
121+
const html = await liquid.parseAndRender('{{ foo | remove: "rain" }}')
122+
expect(html).to.equal('')
123+
})
84124
})
85-
it('should support remove_first', function () {
86-
return test('{{ "I strained to see the train through the rain" | remove_first: "rain" }}',
87-
'I sted to see the train through the rain')
125+
describe('remove_first', function () {
126+
it('should support remove_first', async () => {
127+
const html = await liquid.parseAndRender('{{ "I strained to see the train through the rain" | remove_first: "rain" }}')
128+
expect(html).to.equal('I sted to see the train through the rain')
129+
})
130+
it('should return empty for undefined', async () => {
131+
const html = await liquid.parseAndRender('{{ foo | remove_first: "r" }}')
132+
expect(html).to.equal('')
133+
})
88134
})
89135
it('should support replace', function () {
90136
return test('{{ "Take my protein pills and put my helmet on" | replace: "my", "your" }}',

test/unit/util/underscore.ts

-6
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@ describe('util/underscore', function () {
2727
})
2828
})
2929
describe('.stringify()', function () {
30-
it('should respect to toLiquid() method', function () {
31-
expect(_.stringify({ toLiquid: () => 'foo' })).to.equal('foo')
32-
})
33-
it('should recursively call toLiquid()', function () {
34-
expect(_.stringify({ toLiquid: () => ({ toLiquid: () => 'foo' }) })).to.equal('foo')
35-
})
3630
it('should return "" for null', function () {
3731
expect(_.stringify(null)).to.equal('')
3832
})

0 commit comments

Comments
 (0)