Repository: harttle/liquidjs
Pull request #771 (2024-11-17 13:30:50)

feat: support Jekyll style `where`, #768

coveralls: ## Pull Request Test Coverage Report for [Build 11879286670](https://coveralls.io/builds/70906294)


### Details

* **9** of **10**   **(90.0%)**  changed or added relevant lines in **4** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.06%**) to **99.914%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/drop/blank-drop.ts](https://coveralls.io/builds/70906294/source?filename=src%2Fdrop%2Fblank-drop.ts#L12) | 0 | 1 | 0.0%
<!-- | **Total:** | **9** | **10** | **90.0%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/70906294/badge)](https://coveralls.io/builds/70906294) |
| :-- | --: |
| Change from base [Build 11878814965](https://coveralls.io/builds/70905976): |  -0.06% |
| Covered Lines: | 2549 |
| Relevant Lines: | 2550 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/docs/package.json b/docs/package.json
index 0e1092519..60b2bcba1 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 dbf75134c..2b98f54f0 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 07b858317..e35a8f0c8 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 79335392f..8ddafed52 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 16d8970eb..d6bec0138 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 b57693e07..4be965486 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<T extends object> (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 e93583d16..a1e062088 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 9e718cd7b..c27aed8a2 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))
+}


Test patch:
diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts
index 092ef449c..ed1e9a44e 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 = [