From 258780e9a87ce87534a6bb4336725cb1d38a2998 Mon Sep 17 00:00:00 2001 From: Jun Yang Date: Sun, 14 Apr 2024 14:03:04 +0800 Subject: [PATCH] feat: pop/shift/unshift filters from Jekyll --- docs/source/_data/sidebar.yml | 3 ++ docs/source/filters/overview.md | 2 +- docs/source/filters/pop.md | 24 ++++++++++++ docs/source/filters/shift.md | 24 ++++++++++++ docs/source/filters/unshift.md | 25 +++++++++++++ docs/source/zh-cn/filters/overview.md | 2 +- docs/source/zh-cn/filters/pop.md | 24 ++++++++++++ docs/source/zh-cn/filters/shift.md | 24 ++++++++++++ docs/source/zh-cn/filters/unshift.md | 25 +++++++++++++ src/filters/array.ts | 32 +++++++++++----- src/tags/for.ts | 2 +- src/tags/tablerow.ts | 2 +- src/util/collection.ts | 16 -------- src/util/index.ts | 1 - src/util/underscore.ts | 16 ++++++++ test/integration/filters/array.spec.ts | 51 ++++++++++++++++++++++++-- test/integration/tags/for.spec.ts | 2 +- 17 files changed, 240 insertions(+), 35 deletions(-) create mode 100644 docs/source/filters/pop.md create mode 100644 docs/source/filters/shift.md create mode 100644 docs/source/filters/unshift.md create mode 100644 docs/source/zh-cn/filters/pop.md create mode 100644 docs/source/zh-cn/filters/shift.md create mode 100644 docs/source/zh-cn/filters/unshift.md delete mode 100644 src/util/collection.ts diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml index 97708c68c4..3ee44dcad2 100644 --- a/docs/source/_data/sidebar.yml +++ b/docs/source/_data/sidebar.yml @@ -51,6 +51,7 @@ filters: modulo: modulo.html newline_to_br: newline_to_br.html plus: plus.html + pop: pop.html push: push.html prepend: prepend.html raw: raw.html @@ -63,6 +64,7 @@ filters: reverse: reverse.html round: round.html rstrip: rstrip.html + shift: shift.html size: size.html slice: slice.html sort: sort.html @@ -76,6 +78,7 @@ filters: truncate: truncate.html truncatewords: truncatewords.html uniq: uniq.html + unshift: unshift.html upcase: upcase.html url_decode: url_decode.html url_encode: url_encode.html diff --git a/docs/source/filters/overview.md b/docs/source/filters/overview.md index e0503f379e..24f0b6591b 100644 --- a/docs/source/filters/overview.md +++ b/docs/source/filters/overview.md @@ -12,7 +12,7 @@ Categories | Filters Math | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least, at_most String | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last,remove, remove_first, remove_last, truncate, truncatewords HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br -Array | slice, map, sort, sort_natural, uniq, where, first, last, join, reverse, concat, compact, size, push +Array | slice, map, sort, sort_natural, uniq, where, first, last, join, reverse, concat, compact, size, push, pop, shift, unshift Date | date Misc | default, json, raw diff --git a/docs/source/filters/pop.md b/docs/source/filters/pop.md new file mode 100644 index 0000000000..b55154f984 --- /dev/null +++ b/docs/source/filters/pop.md @@ -0,0 +1,24 @@ +--- +title: pop +--- + +{% since %}v10.11.0{% endsince %} + +Pop an element from the array. It's NON-DESTRUCTIVE, i.e. it does not mutate the array, but rather make a copy and mutate that. + +Input +```liquid +{% assign fruits = "apples, oranges, peaches" | split: ", " %} + +{% assign everything = fruits | pop %} + +{% for item in everything %} +- {{ item }} +{% endfor %} +``` + +Output +```text +- apples +- oranges +``` diff --git a/docs/source/filters/shift.md b/docs/source/filters/shift.md new file mode 100644 index 0000000000..63e5475629 --- /dev/null +++ b/docs/source/filters/shift.md @@ -0,0 +1,24 @@ +--- +title: shift +--- + +{% since %}v10.11.0{% endsince %} + +Shift an element from the array. It's NON-DESTRUCTIVE, i.e. it does not mutate the array, but rather make a copy and mutate that. + +Input +```liquid +{% assign fruits = "apples, oranges, peaches" | split: ", " %} + +{% assign everything = fruits | shift %} + +{% for item in everything %} +- {{ item }} +{% endfor %} +``` + +Output +```text +- oranges +- peaches +``` diff --git a/docs/source/filters/unshift.md b/docs/source/filters/unshift.md new file mode 100644 index 0000000000..77b568c1b5 --- /dev/null +++ b/docs/source/filters/unshift.md @@ -0,0 +1,25 @@ +--- +title: unshift +--- + +{% since %}v10.11.0{% endsince %} + +Unshift an element to the front of the array. It's NON-DESTRUCTIVE, i.e. it does not mutate the array, but rather make a copy and mutate that. + +Input +```liquid +{% assign fruits = "oranges, peaches" | split: ", " %} + +{% assign everything = fruits | unshift: "apples" %} + +{% for item in everything %} +- {{ item }} +{% endfor %} +``` + +Output +```text +- apples +- oranges +- peaches +``` diff --git a/docs/source/zh-cn/filters/overview.md b/docs/source/zh-cn/filters/overview.md index 3a27e43a6f..848c2e68a4 100644 --- a/docs/source/zh-cn/filters/overview.md +++ b/docs/source/zh-cn/filters/overview.md @@ -12,7 +12,7 @@ LiquidJS 共支持 40+ 个过滤器,可以分为如下几类: 数学 | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least, at_most 字符串 | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last, remove, remove_first, remove_last, truncate, truncatewords HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br -数组 | slice, map, sort, sort_natural, uniq, wheres, first, last, join, reverse, concat, compact, size, push +数组 | slice, map, sort, sort_natural, uniq, wheres, first, last, join, reverse, concat, compact, size, push, pop, shift, unshift 日期 | date 其他 | default, json diff --git a/docs/source/zh-cn/filters/pop.md b/docs/source/zh-cn/filters/pop.md new file mode 100644 index 0000000000..fb72a97882 --- /dev/null +++ b/docs/source/zh-cn/filters/pop.md @@ -0,0 +1,24 @@ +--- +title: pop +--- + +{% since %}v10.11.0{% endsince %} + +从数组末尾弹出一个元素。注意该操作不会改变原数组,而是在一份拷贝上操作。 + +输入 +```liquid +{% assign fruits = "apples, oranges, peaches" | split: ", " %} + +{% assign everything = fruits | pop %} + +{% for item in everything %} +- {{ item }} +{% endfor %} +``` + +输出 +```text +- apples +- oranges +``` diff --git a/docs/source/zh-cn/filters/shift.md b/docs/source/zh-cn/filters/shift.md new file mode 100644 index 0000000000..e2bb70481b --- /dev/null +++ b/docs/source/zh-cn/filters/shift.md @@ -0,0 +1,24 @@ +--- +title: shift +--- + +{% since %}v10.11.0{% endsince %} + +从数组头部弹出一个元素。注意该操作不会改变原数组,而是在一份拷贝上操作。 + +输入 +```liquid +{% assign fruits = "apples, oranges, peaches" | split: ", " %} + +{% assign everything = fruits | shift %} + +{% for item in everything %} +- {{ item }} +{% endfor %} +``` + +输出 +```text +- oranges +- peaches +``` diff --git a/docs/source/zh-cn/filters/unshift.md b/docs/source/zh-cn/filters/unshift.md new file mode 100644 index 0000000000..e29ebd2f78 --- /dev/null +++ b/docs/source/zh-cn/filters/unshift.md @@ -0,0 +1,25 @@ +--- +title: unshift +--- + +{% since %}v10.11.0{% endsince %} + +往数组头部添加一个元素。注意该操作不会改变原数组,而是在一份拷贝上操作。 + +输入 +```liquid +{% assign fruits = "oranges, peaches" | split: ", " %} + +{% assign everything = fruits | unshift: "apples" %} + +{% for item in everything %} +- {{ item }} +{% endfor %} +``` + +输出 +```text +- apples +- oranges +- peaches +``` diff --git a/src/filters/array.ts b/src/filters/array.ts index ed98b861f9..d34c09923e 100644 --- a/src/filters/array.ts +++ b/src/filters/array.ts @@ -11,7 +11,7 @@ export const reverse = argumentsToValue((v: any[]) => [...toArray(v)].reverse()) export function * sort (this: FilterImpl, arr: T[], property?: string): IterableIterator { const values: [T, string | number][] = [] - for (const item of toArray(toValue(arr))) { + for (const item of toArray(arr)) { values.push([ item, property ? yield this.context._getFromScope(item, stringify(property).split('.'), false) : item @@ -25,7 +25,6 @@ export function * sort (this: FilterImpl, arr: T[], property?: string): Itera } export function sort_natural (input: T[], property?: string) { - input = toValue(input) const propertyString = stringify(property) const compare = property === undefined ? caseInsensitiveCompare @@ -37,7 +36,7 @@ export const size = (v: string | any[]) => (v && v.length) || 0 export function * map (this: FilterImpl, arr: Scope[], property: string): IterableIterator { const results = [] - for (const item of toArray(toValue(arr))) { + for (const item of toArray(arr)) { results.push(yield this.context._getFromScope(item, stringify(property), false)) } return results @@ -45,7 +44,7 @@ export function * map (this: FilterImpl, arr: Scope[], property: string): Iterab export function * sum (this: FilterImpl, arr: Scope[], property?: string): IterableIterator { let sum = 0 - for (const item of toArray(toValue(arr))) { + for (const item of toArray(arr)) { const data = Number(property ? yield this.context._getFromScope(item, stringify(property), false) : item) sum += Number.isNaN(data) ? 0 : data } @@ -53,20 +52,35 @@ export function * sum (this: FilterImpl, arr: Scope[], property?: string): Itera } export function compact (this: FilterImpl, arr: T[]) { - arr = toValue(arr) return toArray(arr).filter(x => !isNil(toValue(x))) } export function concat (v: T1[], arg: T2[] = []): (T1 | T2)[] { - v = toValue(v) - arg = toArray(arg).map(v => toValue(v)) - return toArray(v).concat(arg) + return toArray(v).concat(toArray(arg)) } export function push (v: T[], arg: T): T[] { return concat(v, [arg]) } +export function unshift (v: T[], arg: T): T[] { + const clone = [...toArray(v)] + clone.unshift(arg) + return clone +} + +export function pop (v: T[]): T[] { + const clone = [...toArray(v)] + clone.pop() + return clone +} + +export function shift (v: T[]): T[] { + const clone = [...toArray(v)] + clone.shift() + return clone +} + export function slice (v: T[] | string, begin: number, length = 1): T[] | string { v = toValue(v) if (isNil(v)) return [] @@ -77,7 +91,7 @@ export function slice (v: T[] | string, begin: number, length = 1): T[] | str export function * where (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator { const values: unknown[] = [] - arr = toArray(toValue(arr)) + arr = toArray(arr) for (const item of arr) { values.push(yield this.context._getFromScope(item, stringify(property).split('.'), false)) } diff --git a/src/tags/for.ts b/src/tags/for.ts index 2e65ed810a..4d1f2f3bdc 100644 --- a/src/tags/for.ts +++ b/src/tags/for.ts @@ -1,5 +1,5 @@ import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..' -import { toEnumerable } from '../util/collection' +import { toEnumerable } from '../util' import { ForloopDrop } from '../drop/forloop-drop' const MODIFIERS = ['offset', 'limit', 'reversed'] diff --git a/src/tags/tablerow.ts b/src/tags/tablerow.ts index f0f6062ebf..59e167a490 100644 --- a/src/tags/tablerow.ts +++ b/src/tags/tablerow.ts @@ -1,4 +1,4 @@ -import { toEnumerable } from '../util/collection' +import { toEnumerable } from '../util' import { ValueToken, Liquid, Tag, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, ParseStream } from '..' import { TablerowloopDrop } from '../drop/tablerowloop-drop' diff --git a/src/util/collection.ts b/src/util/collection.ts deleted file mode 100644 index 43b7bfb346..0000000000 --- a/src/util/collection.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { isNil, isString, isObject, isArray, isIterable, toValue } from './underscore' - -export function toEnumerable (val: any): T[] { - val = toValue(val) - if (isArray(val)) return val - if (isString(val) && val.length > 0) return [val] as unknown as T[] - if (isIterable(val)) return Array.from(val) - if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]]) as unknown as T[] - return [] -} - -export function toArray (val: any) { - if (isNil(val)) return [] - if (isArray(val)) return val - return [ val ] -} diff --git a/src/util/index.ts b/src/util/index.ts index 5a6152c3d9..8b57690b66 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -6,7 +6,6 @@ export * from './underscore' export * from './operator-trie' export * from './type-guards' export * from './async' -export * from './collection' export * from './strftime' export * from './liquid-date' export * from './timezone-date' diff --git a/src/util/underscore.ts b/src/util/underscore.ts index 08168df7bf..c851ef6d0e 100644 --- a/src/util/underscore.ts +++ b/src/util/underscore.ts @@ -46,6 +46,22 @@ export function stringify (value: any): string { return String(value) } +export function toEnumerable (val: any): T[] { + val = toValue(val) + if (isArray(val)) return val + if (isString(val) && val.length > 0) return [val] as unknown as T[] + if (isIterable(val)) return Array.from(val) + if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]]) as unknown as T[] + return [] +} + +export function toArray (val: any) { + val = toValue(val) + if (isNil(val)) return [] + if (isArray(val)) return val + return [ val ] +} + export function toValue (value: any): any { return (value instanceof Drop && isFunction(value.valueOf)) ? value.valueOf() : value } diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts index b1d50a6d24..ed2c7bd45c 100644 --- a/test/integration/filters/array.spec.ts +++ b/test/integration/filters/array.spec.ts @@ -158,10 +158,53 @@ describe('filters/array', function () { await test('{{ undefinedValue | push: arg | join: "," }}', scope, 'foo') await test('{{ nullValue | push: arg | join: "," }}', scope, 'foo') }) - it('should ignore nil right value', async () => { - const scope = { nullValue: null } - await test('{{ nullValue | push | join: "," }}', scope, '') - await test('{{ nullValue | push: nil | join: "," }}', scope, '') + it('should support nil right value', async () => { + const scope = { nullValue: [] } + await test('{{ nullValue | push | size }}', scope, '1') + await test('{{ nullValue | push: nil | size }}', scope, '1') + }) + }) + + describe('pop', () => { + it('should support pop', async () => { + const scope = { val: ['hey', 'you'] } + await test('{{ val | pop | join: "," }}', scope, 'hey') + }) + it('should not change original array', async () => { + const scope = { val: ['hey', 'you'] } + await test('{{ val | pop | join: "," }} {{ val| join: "," }}', scope, 'hey hey,you') + }) + it('should support nil left value', async () => { + await test('{{ notDefined | pop | join: "," }}', {}, '') + }) + }) + + describe('unshift', () => { + it('should support unshift', async () => { + const scope = { val: ['you'] } + await test('{{ val | unshift: "hey" | join: ", " }}', scope, 'hey, you') + }) + it('should not change original array', async () => { + const scope = { val: ['you'] } + await test('{{ val | unshift: "hey" | join: "," }} {{ val | join: "," }}', scope, 'hey,you you') + }) + it('should support nil right value', async () => { + const scope = { val: [] } + await test('{{ val | unshift: nil | size }}', scope, '1') + }) + }) + + describe('shift', () => { + it('should support pop', async () => { + const scope = { val: ['hey', 'you'] } + await test('{{ val | shift }}', scope, 'you') + }) + it('should not change original array', async () => { + const scope = { val: ['hey', 'you'] } + await test('{{ val | shift }} {{ val | join: ","}}', scope, 'you hey,you') + }) + it('should support nil left value', async () => { + await test('{{ notDefined | pop }}', {}, '') }) }) diff --git a/test/integration/tags/for.spec.ts b/test/integration/tags/for.spec.ts index 0d09b8c1a7..0cfcd31a60 100644 --- a/test/integration/tags/for.spec.ts +++ b/test/integration/tags/for.spec.ts @@ -63,7 +63,7 @@ describe('tags/for', function () { const engine = new Liquid({ strictVariables: true }) const src = '{% assign hello = "hello,world" | split: "," | concat: null %}{% for i in hello %}{{ i }},{% endfor %}' const html = await engine.parseAndRender(src, scope) - return expect(html).toBe('hello,world,,') + return expect(html).toBe('hello,world,') }) describe('illegal', function () { it('should reject when for not closed', function () {