diff --git a/docs/package.json b/docs/package.json index 0e10925190..60b2bcba1b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "hexo": { - "version": "7.3.0" + "version": "5.4.0" }, "scripts": { "build": "hexo generate", @@ -34,4 +34,4 @@ "engines": { "node": ">=8.10.0" } -} +} \ No newline at end of file diff --git a/docs/source/filters/where.md b/docs/source/filters/where.md index dbf75134cf..2b98f54f05 100644 --- a/docs/source/filters/where.md +++ b/docs/source/filters/where.md @@ -103,4 +103,30 @@ Output - 3 ``` +## Jekyll style + +{% since %}v10.19.0{% endsince %} + +For Liquid users migrating from Jekyll, there's a `jekyllWhere` option to mimic the behavior of Jekyll's `where` filter. This option is set to `false` by default. When enabled, if `property` is an array, the target value is matched using `Array.includes` instead of `==`, which is particularly useful for filtering tags. + +```javascript +const pages = [ + { tags: ["cat", "food"], title: 'Cat Food' }, + { tags: ["dog", "food"], title: 'Dog Food' }, +] +``` + +Input +```liquid +{% assign selected = pages | where: 'tags', "cat" %} +{% for item in selected -%} +- {{ item.title }} +{% endfor %} +``` + +Output +```text +Cat Food +``` + [truthy]: ../tutorials/truthy-and-falsy.html diff --git a/docs/source/zh-cn/filters/where.md b/docs/source/zh-cn/filters/where.md index 07b8583174..e35a8f0c8f 100644 --- a/docs/source/zh-cn/filters/where.md +++ b/docs/source/zh-cn/filters/where.md @@ -103,5 +103,32 @@ const products = [ - 3 ``` +## Jekyll 风格 + +{% since %}v10.19.0{% endsince %} + +对于从 Jekyll 迁移到 Liquid 的用户,有一个 `jekyllWhere` 选项可以模拟 Jekyll 的 `where` 过滤器的行为。该选项默认设置为 `false`。启用后,如果 `property` 是一个数组,目标值将使用 `Array.includes` 而不是 `==` 进行匹配,这在过滤标签时特别有用。 + +例如,以下代码: + +```javascript +const pages = [ + { tags: ["cat", "food"], title: 'Cat Food' }, + { tags: ["dog", "food"], title: 'Dog Food' }, +] +``` + +输入 +```liquid +{% assign selected = pages | where: 'tags', "cat" %} +{% for item in selected -%} +- {{ item.title }} +{% endfor %} +``` + +输出 +```text +Cat Food +``` [truthy]: ../tutorials/truthy-and-falsy.html diff --git a/src/drop/blank-drop.ts b/src/drop/blank-drop.ts index 79335392fd..8ddafed52b 100644 --- a/src/drop/blank-drop.ts +++ b/src/drop/blank-drop.ts @@ -8,4 +8,7 @@ export class BlankDrop extends EmptyDrop { if (isString(value)) return /^\s*$/.test(value) return super.equals(value) } + static is (value: unknown) { + return value instanceof BlankDrop + } } diff --git a/src/drop/empty-drop.ts b/src/drop/empty-drop.ts index 16d8970ebc..d6bec0138e 100644 --- a/src/drop/empty-drop.ts +++ b/src/drop/empty-drop.ts @@ -25,4 +25,7 @@ export class EmptyDrop extends Drop implements Comparable { public valueOf () { return '' } + static is (value: unknown) { + return value instanceof EmptyDrop + } } diff --git a/src/filters/array.ts b/src/filters/array.ts index b57693e074..4be9654866 100644 --- a/src/filters/array.ts +++ b/src/filters/array.ts @@ -1,8 +1,9 @@ import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast } from '../util' -import { equals, evalToken, isTruthy } from '../render' +import { arrayIncludes, equals, evalToken, isTruthy } from '../render' import { Value, FilterImpl } from '../template' import { Tokenizer } from '../parser' import type { Scope } from '../context' +import { EmptyDrop } from '../drop' export const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) { const array = toArray(v) @@ -124,9 +125,12 @@ export function * where (this: FilterImpl, arr: T[], property: for (const item of arr) { values.push(yield evalToken(token, this.context.spawn(item))) } + const matcher = this.context.opts.jekyllWhere + ? (v: any) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected)) + : (v: any) => equals(v, expected) return arr.filter((_, i) => { if (expected === undefined) return isTruthy(values[i], this.context) - return equals(values[i], expected) + return matcher(values[i]) }) } diff --git a/src/liquid-options.ts b/src/liquid-options.ts index e93583d168..a1e0620887 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -22,6 +22,8 @@ export interface LiquidOptions { relativeReference?: boolean; /** Use jekyll style include, pass parameters to `include` variable of current scope. Defaults to `false`. */ jekyllInclude?: boolean; + /** Use jekyll style where filter, enables array item match. Defaults to `false`. */ + jekyllWhere?: boolean; /** Add a extname (if filepath doesn't include one) before template file lookup. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */ extname?: string; /** Whether or not to cache resolved templates. Defaults to `false`. */ diff --git a/src/render/operator.ts b/src/render/operator.ts index 9e718cd7b7..c27aed8a2c 100644 --- a/src/render/operator.ts +++ b/src/render/operator.ts @@ -58,3 +58,7 @@ function arrayEquals (lhs: any[], rhs: any[]): boolean { if (lhs.length !== rhs.length) return false return !lhs.some((value, i) => !equals(value, rhs[i])) } + +export function arrayIncludes (arr: any[], item: any): boolean { + return arr.some(value => equals(value, item)) +} diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts index 092ef449cd..ed1e9a44e1 100644 --- a/test/integration/filters/array.spec.ts +++ b/test/integration/filters/array.spec.ts @@ -484,6 +484,28 @@ describe('filters/array', function () { Kitchen products: `) }) + it('should support nil as target', () => { + const scope = { list: [{ foo: 'FOO' }, { bar: 'BAR', type: 2 }] } + return test('{{list | where: "type", nil | json}}', scope, '[{"foo":"FOO"}]') + }) + it('should support empty as target', async () => { + const scope = { pages: [{ tags: ['FOO'] }, { tags: [] }, { title: 'foo' }] } + await test('{{pages | where: "tags", empty | json}}', scope, '[{"tags":[]}]') + }) + it('should not match string with array', async () => { + const scope = { objs: [{ foo: ['FOO', 'bar'] }] } + await test('{{objs | where: "foo", "FOO" | json}}', scope, '[]') + }) + describe('jekyll style', () => { + it('should not match string with array', async () => { + const scope = { objs: [{ foo: ['FOO', 'bar'] }] } + await test('{{objs | where: "foo", "FOO" | json}}', scope, '[{"foo":["FOO","bar"]}]', { jekyllWhere: true }) + }) + it('should support empty as target', async () => { + const scope = { pages: [{ tags: ['FOO'] }, { tags: [] }, { title: 'foo' }] } + await test('{{pages | where: "tags", empty | json}}', scope, '[{"tags":[]}]', { jekyllWhere: true }) + }) + }) }) describe('where_exp', function () { const products = [