nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: harttle/liquidjs
Pull request #612 (2023-05-22 17:01:10)

feat: Add support for the Jekyll sample filter
See https://jekyllrb.com/docs/liquid/filters

I am sorting the array randomly and then picking the first N items or all items if there is no sample limit.


Patch:
diff --git a/src/filters/array.ts b/src/filters/array.ts
index de29f8d06..a82b23164 100644
--- a/src/filters/array.ts
+++ b/src/filters/array.ts
@@ -88,3 +88,14 @@ export function uniq<T> (arr: T[]): T[] {
     return true
   })
 }
+
+export function sample<T> (v: T[] | string, count: number | undefined = undefined): T[] | string {
+  v = toValue(v)
+  if (isNil(v)) return []
+  if (!isArray(v)) {
+    v = stringify(v)
+    return [...v].sort(() => Math.random()).slice(0, count).join('')
+  }
+
+  return [...v].sort(() => Math.random()).slice(0, count)
+}


Test patch:
diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts
index 17ddea177..9df3e7c0b 100644
--- a/test/integration/filters/array.spec.ts
+++ b/test/integration/filters/array.spec.ts
@@ -104,6 +104,20 @@ describe('filters/array', function () {
       expect(html).toBe('abc')
     })
   })
+  describe('sample', function () {
+    it('should return full array sample', () => test(
+      '{{ "hello,world" | split: "," | sample | size }}',
+      '2'
+    ))
+    it('should return full array sample even if excess count', () => test(
+      '{{ "hello,world" | split: "," | sample: 10 | size }}',
+      '2'
+    ))
+    it('should return partial array sample', () => test(
+      '{{ "hello,world" | split: "," | sample: 1 | size }}',
+      '1'
+    ))
+  })
   describe('size', function () {
     it('should return string length', () => test(
       '{{ "Ground control to Major Tom." | size }}',
Repository: harttle/liquidjs
Pull request #637 (2023-08-11 23:51:27)

fix: remove_last was eating an extra character
When the substring to remove was anywhere but the end of the larger string, `remove_last` would eat an extra character. 


Patch:
diff --git a/src/filters/string.ts b/src/filters/string.ts
index 19cae3f83..e7955387b 100644
--- a/src/filters/string.ts
+++ b/src/filters/string.ts
@@ -44,7 +44,7 @@ export function remove_last (v: string, l: string) {
   const pattern = String(l)
   const index = str.lastIndexOf(pattern)
   if (index === -1) return str
-  return str.substring(0, index) + str.substring(index + pattern.length + 1)
+  return str.substring(0, index) + str.substring(index + pattern.length)
 }
 
 export function rstrip (str: string, chars?: string) {


Test patch:
diff --git a/test/integration/filters/string.spec.ts b/test/integration/filters/string.spec.ts
index f052cfd33..d8bdf93f7 100644
--- a/test/integration/filters/string.spec.ts
+++ b/test/integration/filters/string.spec.ts
@@ -203,6 +203,10 @@ describe('filters/string', function () {
       return test('{{ "I strained to see the train through the rain" | remove_last: "rain" }}',
         'I strained to see the train through the ')
     })
+    it('should remove the last occurrence of substring in the middle of a string', function () {
+      return test('{{ "I strained to see the train through the rain and fog" | remove_last: "rain" }}',
+        'I strained to see the train through the  and fog')
+    })
     it('should handle substring not found', function () {
       return test('{{ "I strained to see the train through the rain" | remove_last: "no such thing" }}',
         'I strained to see the train through the rain')
Repository: harttle/liquidjs
Pull request #642 (2023-08-18 16:31:24)

docs: fix options link in setup tutorial
Given in the published version, https://liquidjs.com/tutorials/options.md doesn't exist


Patch:
diff --git a/docs/source/tutorials/setup.md b/docs/source/tutorials/setup.md
index 8969628a2..d92a9b3cc 100644
--- a/docs/source/tutorials/setup.md
+++ b/docs/source/tutorials/setup.md
@@ -100,4 +100,4 @@ Most of the [options available through the JavaScript API][options] are also ava
 A ReactJS demo is also added by [@stevenanthonyrevo](https://github.com/stevenanthonyrevo), see [liquidjs/demo/reactjs/](https://github.com/harttle/liquidjs/blob/master/demo/reactjs/).
 
 [intro]: ./intro-to-liquid.html
-[options]: ./options.md
+[options]: ./options.html
diff --git a/src/tags/case.ts b/src/tags/case.ts
index b55df2465..23564d3a8 100644
--- a/src/tags/case.ts
+++ b/src/tags/case.ts
@@ -17,7 +17,12 @@ export default class extends Tag {
         const values: ValueToken[] = []
         while (!token.tokenizer.end()) {
           values.push(token.tokenizer.readValueOrThrow())
-          token.tokenizer.readTo(',')
+          token.tokenizer.skipBlank()
+          if (token.tokenizer.peek() === ',') {
+            token.tokenizer.readTo(',')
+          } else {
+            token.tokenizer.readTo('or')
+          }
         }
         this.branches.push({
           values,


Test patch:
diff --git a/test/integration/tags/case.spec.ts b/test/integration/tags/case.spec.ts
index d15d452cb..3312eb800 100644
--- a/test/integration/tags/case.spec.ts
+++ b/test/integration/tags/case.spec.ts
@@ -75,4 +75,20 @@ describe('tags/case', function () {
     const html = await liquid.parseAndRender(src)
     return expect(html).toBe('firstsecond')
   })
+  it('should support case with multiple values separated by or', async function () {
+    const src = '{% case 3 %}' +
+            '{% when 1 or 2 or 3 %}1 or 2 or 3' +
+            '{% else %}not 1 or 2 or 3' +
+            '{%endcase%}'
+    const html = await liquid.parseAndRender(src)
+    return expect(html).toBe('1 or 2 or 3')
+  })
+  it('should support case with multiple strings separated by or', async function () {
+    const src = '{% case "or" %}' +
+            '{% when "and" or "or" %}and or or' +
+            '{% else %}not and or or' +
+            '{%endcase%}'
+    const html = await liquid.parseAndRender(src)
+    return expect(html).toBe('and or or')
+  })
 })
Repository: harttle/liquidjs
Pull request #644 (2023-08-18 21:46:59)

fix: case should allow multiple values separated by or
This is supported by Shopify liquid.


Patch:
diff --git a/src/tags/case.ts b/src/tags/case.ts
index b55df2465..23564d3a8 100644
--- a/src/tags/case.ts
+++ b/src/tags/case.ts
@@ -17,7 +17,12 @@ export default class extends Tag {
         const values: ValueToken[] = []
         while (!token.tokenizer.end()) {
           values.push(token.tokenizer.readValueOrThrow())
-          token.tokenizer.readTo(',')
+          token.tokenizer.skipBlank()
+          if (token.tokenizer.peek() === ',') {
+            token.tokenizer.readTo(',')
+          } else {
+            token.tokenizer.readTo('or')
+          }
         }
         this.branches.push({
           values,


Test patch:
diff --git a/test/integration/tags/case.spec.ts b/test/integration/tags/case.spec.ts
index d15d452cb..3312eb800 100644
--- a/test/integration/tags/case.spec.ts
+++ b/test/integration/tags/case.spec.ts
@@ -75,4 +75,20 @@ describe('tags/case', function () {
     const html = await liquid.parseAndRender(src)
     return expect(html).toBe('firstsecond')
   })
+  it('should support case with multiple values separated by or', async function () {
+    const src = '{% case 3 %}' +
+            '{% when 1 or 2 or 3 %}1 or 2 or 3' +
+            '{% else %}not 1 or 2 or 3' +
+            '{%endcase%}'
+    const html = await liquid.parseAndRender(src)
+    return expect(html).toBe('1 or 2 or 3')
+  })
+  it('should support case with multiple strings separated by or', async function () {
+    const src = '{% case "or" %}' +
+            '{% when "and" or "or" %}and or or' +
+            '{% else %}not and or or' +
+            '{%endcase%}'
+    const html = await liquid.parseAndRender(src)
+    return expect(html).toBe('and or or')
+  })
 })
Repository: harttle/liquidjs
Pull request #649 (2023-08-24 19:40:36)

fix: handle windows/dos newlines on `newline_to_br` and `strip_newlines`
Not sure if they are handled by javascript already, but might as well consider the case.


Patch:
diff --git a/src/filters/html.ts b/src/filters/html.ts
index 73be53cb6..b296dc63d 100644
--- a/src/filters/html.ts
+++ b/src/filters/html.ts
@@ -28,7 +28,7 @@ export function escape_once (str: string) {
 }
 
 export function newline_to_br (v: string) {
-  return stringify(v).replace(/\n/g, '<br />\n')
+  return stringify(v).replace(/\r?\n/gm, '<br />\n')
 }
 
 export function strip_html (v: string) {
diff --git a/src/filters/string.ts b/src/filters/string.ts
index e7955387b..2a1013581 100644
--- a/src/filters/string.ts
+++ b/src/filters/string.ts
@@ -74,7 +74,7 @@ export function strip (v: string, chars?: string) {
 }
 
 export function strip_newlines (v: string) {
-  return stringify(v).replace(/\n/g, '')
+  return stringify(v).replace(/\r?\n/gm, '')
 }
 
 export function capitalize (str: string) {


Test patch:
diff --git a/test/integration/filters/html.spec.ts b/test/integration/filters/html.spec.ts
index f40c12d09..460a7aeb1 100644
--- a/test/integration/filters/html.spec.ts
+++ b/test/integration/filters/html.spec.ts
@@ -30,7 +30,7 @@ describe('filters/html', function () {
     it('should support string_with_newlines', function () {
       const src = '{% capture string_with_newlines %}\n' +
               'Hello\n' +
-              'there\n' +
+              'there\r\n' +
               '{% endcapture %}' +
               '{{ string_with_newlines | newline_to_br }}'
       const dst = '<br />\n' +
diff --git a/test/integration/filters/string.spec.ts b/test/integration/filters/string.spec.ts
index d8bdf93f7..bc54e07ce 100644
--- a/test/integration/filters/string.spec.ts
+++ b/test/integration/filters/string.spec.ts
@@ -137,6 +137,12 @@ describe('filters/string', function () {
             '{{ string_with_newlines | strip_newlines }}',
     'Hellothere')
   })
+  it('should support strip_newlines on Windows newlines ', function () {
+    return test('{% capture string_with_newlines %}\n' +
+            'Hello\r\nthere\n{% endcapture %}' +
+            '{{ string_with_newlines | strip_newlines }}',
+    'Hellothere')
+  })
   describe('truncate', function () {
     it('should truncate when string too long', function () {
       return test('{{ "Ground control to Major Tom." | truncate: 20 }}',
Repository: harttle/liquidjs
Pull request #698 (2024-05-18 15:12:55)

fix: allow liquidMethodMissing to return any supported value type
The issue is that the type of `liquidMethodMissing` is too restrictive and only allows returning `string` rather than any supported value type. I ran into this issue when trying to return a `Drop` from `liquidMethodMissing` to enable dynamic lookups along an entire accessor chain.

admtnnr: @harttle I think I addressed your single piece of feedback, let me know if there's anything else you'd like changed!

Patch:
diff --git a/src/drop/drop.ts b/src/drop/drop.ts
index f9ea60890..74b4e152f 100644
--- a/src/drop/drop.ts
+++ b/src/drop/drop.ts
@@ -1,5 +1,5 @@
 export abstract class Drop {
-  public liquidMethodMissing (key: string | number): Promise<string | undefined> | string | undefined {
+  public liquidMethodMissing (key: string | number): Promise<any> | any {
     return undefined
   }
 }


Test patch:
diff --git a/test/integration/drop/drop.spec.ts b/test/integration/drop/drop.spec.ts
index f4f874b48..bf299cefe 100644
--- a/test/integration/drop/drop.spec.ts
+++ b/test/integration/drop/drop.spec.ts
@@ -83,4 +83,20 @@ describe('drop/drop', function () {
     const html = await liquid.parseAndRender(tpl, { address, customer })
     expect(html).toBe('test')
   })
+  it('should support returning supported value types from liquidMethodMissing', async function () {
+    class DynamicTypeDrop extends Drop {
+      liquidMethodMissing (key: string) {
+        switch (key) {
+          case 'number': return 42
+          case 'string': return 'foo'
+          case 'boolean': return true
+          case 'array': return [1, 2, 3]
+          case 'object': return { foo: 'bar' }
+          case 'drop': return new CustomDrop()
+        }
+      }
+    }
+    const html = await liquid.parseAndRender(`{{obj.number}} {{obj.string}} {{obj.boolean}} {{obj.array | first}} {{obj.object.foo}} {{obj.drop.getName}}`, { obj: new DynamicTypeDrop() })
+    expect(html).toBe('42 foo true 1 bar GET NAME')
+  })
 })
Repository: harttle/liquidjs
Pull request #786 (2025-01-04 14:12:35)

fix: break/continue stops whole template, #783


Patch:
diff --git a/src/tags/for.ts b/src/tags/for.ts
index 741f77bb1..779eee207 100644
--- a/src/tags/for.ts
+++ b/src/tags/for.ts
@@ -74,6 +74,7 @@ export default class extends Tag {
       if (ctx.breakCalled) break
       scope.forloop.next()
     }
+    ctx.continueCalled = ctx.breakCalled = false
     ctx.pop()
   }
 


Test patch:
diff --git a/test/integration/tags/for.spec.ts b/test/integration/tags/for.spec.ts
index 53a8678f1..034800d56 100644
--- a/test/integration/tags/for.spec.ts
+++ b/test/integration/tags/for.spec.ts
@@ -178,6 +178,19 @@ describe('tags/for', function () {
       const html = await liquid.parseAndRender(src, scope)
       return expect(html).toBe(' before before')
     })
+    it('should continue current forloop only', async () => {
+      const src = `
+      {%- for i in (1..2) -%}
+        i:{{ i }},
+        {%- for j in (1..2) -%}
+          j:{{ j }},
+          {%- continue -%}
+          after
+        {%- endfor -%}
+      {%- endfor -%}`
+      const html = await liquid.parseAndRender(src, scope)
+      return expect(html).toBe('i:1,j:1,j:2,i:2,j:1,j:2,')
+    })
   })
   describe('break', function () {
     it('should support break', async function () {
@@ -196,6 +209,27 @@ describe('tags/for', function () {
       const html = await liquid.parseAndRender(src, scope)
       return expect(html).toBe('123breaking')
     })
+    it('should not break template outside of forloop', async () => {
+      const src = '{% for i in (1..5) %}' +
+        '{{ i }}' +
+        '{% break %}' +
+        '{% endfor %}' +
+        ' after'
+      const html = await liquid.parseAndRender(src, scope)
+      return expect(html).toBe('1 after')
+    })
+    it('should not break parent forloop', async function () {
+      const src = `
+      {%- for i in (1..3) -%}
+        i:{{ i }},
+        {%- for j in (1..3) -%}
+          j:{{ j }},
+          {%- break -%}
+        {%- endfor -%}
+      {%- endfor -%}`
+      const html = await liquid.parseAndRender(src, scope)
+      return expect(html).toBe('i:1,j:1,i:2,j:1,i:3,j:1,')
+    })
   })
 
   describe('limit', function () {
Repository: harttle/liquidjs
Pull request #793 (2025-01-19 09:20:03)

fix: context for group_by_exp/where_exp/find_exp, #790


Patch:
diff --git a/src/filters/array.ts b/src/filters/array.ts
index 36b50eced..cf967f284 100644
--- a/src/filters/array.ts
+++ b/src/filters/array.ts
@@ -140,7 +140,9 @@ export function * where_exp<T extends object> (this: FilterImpl, arr: T[], itemN
   const array = toArray(arr)
   this.context.memoryLimit.use(array.length)
   for (const item of array) {
-    const value = yield keyTemplate.value(this.context.spawn({ [itemName]: item }))
+    this.context.push({ [itemName]: item })
+    const value = yield keyTemplate.value(this.context)
+    this.context.pop()
     if (value) filtered.push(item)
   }
   return filtered
@@ -165,7 +167,9 @@ export function * group_by_exp<T extends object> (this: FilterImpl, arr: T[], it
   arr = toEnumerable(arr)
   this.context.memoryLimit.use(arr.length)
   for (const item of arr) {
-    const key = yield keyTemplate.value(this.context.spawn({ [itemName]: item }))
+    this.context.push({ [itemName]: item })
+    const key = yield keyTemplate.value(this.context)
+    this.context.pop()
     if (!map.has(key)) map.set(key, [])
     map.get(key).push(item)
   }
@@ -185,7 +189,9 @@ export function * find_exp<T extends object> (this: FilterImpl, arr: T[], itemNa
   const predicate = new Value(stringify(exp), this.liquid)
   const array = toArray(arr)
   for (const item of array) {
-    const value = yield predicate.value(this.context.spawn({ [itemName]: item }))
+    this.context.push({ [itemName]: item })
+    const value = yield predicate.value(this.context)
+    this.context.pop()
     if (value) return item
   }
 }


Test patch:
diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts
index ede56efe1..5c7768053 100644
--- a/test/integration/filters/array.spec.ts
+++ b/test/integration/filters/array.spec.ts
@@ -522,6 +522,20 @@ describe('filters/array', function () {
         - Garlic press
         `)
     })
+    it('should be aware context', function () {
+      const tpl = `{% assign kitchen_products = products | where_exp: "item", "item.type == target" %}
+        Kitchen products:
+        {% for product in kitchen_products -%}
+        - {{ product.title }}
+        {% endfor %}`
+      const scope = { products, target: 'kitchen' }
+      const html = `
+        Kitchen products:
+        - Spatula
+        - Garlic press
+        `
+      return test(tpl, scope, html)
+    })
   })
   describe('group_by', function () {
     const members = [