Compare commits

..
7 changed files with 16 additions and 250 deletions
+1 -3
View File
@@ -29,8 +29,6 @@
## Releasing
* Bump the version in `lib/liquid/version.rb`
* Update the `History.md` file
* Open a PR like [this one](https://github.com/Shopify/liquid/pull/1894) and merge it to `main`
* Bump the version in `lib/liquid/version.rb` and merge it on `main`
* Create a new release using the [GitHub UI](https://github.com/Shopify/liquid/releases/new)
-4
View File
@@ -20,10 +20,6 @@ group :benchmark, :test do
end
end
group :development do
gem "webrick"
end
group :test do
gem 'rubocop', '~> 1.61.0'
gem 'rubocop-shopify', '~> 2.12.0', require: false
+2 -54
View File
@@ -1,64 +1,12 @@
# Liquid Change Log
## 5.8.0 (unreleased)
## 5.7.2 2025-01-30
- Fix the `sort` filter to handle nested properties gracefully when their types don't match
## 5.7.1 2025-01-24
* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays
* Fix the `has` filter to return `false` when filtering empty arrays
## 5.7.0 2025-01-16
### Features
* Add `find`, `find_index`, `has`, and `reject` filters to arrays
* Compatibility with Ruby 3.4
## 5.6.4 2025-01-14
## 5.6.0 (unreleased)
### Fixes
* Add a default `string_scanner` to avoid errors with `Liquid::VariableLookup.parse("foo.bar")` [Ian Ker-Seymer]
## 5.6.3 2025-01-13
* Remove `lru_redux` dependency [Michael Go]
## 5.6.2 2025-01-13
### Fixes
* Preserve the old behavior of requiring floats to start with a digit [Michael Go]
## 5.6.1 2025-01-13
### Performance improvements
* Faster Expression parser / Tokenizer with StringScanner [Michael Go]
## 5.6.0 2024-12-19
### Architectural changes
* Added new `Environment` class to manage configuration and state that was previously stored in `Template` [Ian Ker-Seymer]
* Moved tag registration from `Template` to `Environment` [Ian Ker-Seymer]
* Removed `StrainerFactory` in favor of `Environment`-based strainer creation [Ian Ker-Seymer]
* Consolidated standard tags into a new `Tags` module with `STANDARD_TAGS` constant [Ian Ker-Seymer]
### Performance improvements
* Optimized `Lexer` with a new `Lexer2` implementation using jump tables for faster tokenization, requires Ruby 3.4 [Ian Ker-Seymer]
* Improved variable rendering with specialized handling for different types [Michael Go]
* Reduced array allocations by using frozen empty constants [Michael Go]
### API changes
* Deprecated several `Template` class methods in favor of `Environment` methods [Ian Ker-Seymer]
* Added deprecation warnings system [Ian Ker-Seymer]
* Changed how filters and tags are registered to use Environment [Ian Ker-Seymer]
### Fixes
* Fixed table row handling of break interrupts [Alex Coco]
* Improved variable output handling for arrays [Ian Ker-Seymer]
* Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
## 5.5.0 2024-03-21
Please reference the GitHub release for more information.
+8 -38
View File
@@ -387,23 +387,7 @@ module Liquid
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort do |a, b|
a = fetch_property(a, property)
b = fetch_property(b, property)
##
# We handle nested properties gracefully to avoid breaking backward
# compatibility.
#
# However, we raise errors for incompatible types when no nested
# properties are used to maintain strict type checking in simple
# cases.
if has_nested_property?(property)
type_safe_compare(a, b) { |a, b| nil_safe_compare(a, b) }
else
nil_safe_compare(a, b)
end
end
ary.sort { |a, b| nil_safe_compare(fetch_property(a, property), fetch_property(b, property)) }
rescue TypeError
raise_property_error(property)
end
@@ -475,7 +459,7 @@ module Liquid
# @liquid_syntax array | some: string, string
# @liquid_return [boolean]
def has(input, property, target_value = nil)
filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
filter_array(input, property, target_value) { |ary, &block| ary.any?(&block) }
end
# @liquid_public_docs
@@ -488,7 +472,7 @@ module Liquid
# @liquid_syntax array | find: string, string
# @liquid_return [untyped]
def find(input, property, target_value = nil)
filter_array(input, property, target_value, nil) { |ary, &block| ary.find(&block) }
filter_array(input, property, target_value) { |ary, &block| ary.find(&block) }
end
# @liquid_public_docs
@@ -501,7 +485,7 @@ module Liquid
# @liquid_syntax array | find_index: string, string
# @liquid_return [number]
def find_index(input, property, target_value = nil)
filter_array(input, property, target_value, nil) { |ary, &block| ary.find_index(&block) }
filter_array(input, property, target_value) { |ary, &block| ary.find_index(&block) }
end
# @liquid_public_docs
@@ -985,10 +969,10 @@ module Liquid
attr_reader :context
def filter_array(input, property, target_value, default_value = [], &block)
def filter_array(input, property, target_value, &block)
ary = InputIterator.new(input, context)
return default_value if ary.empty?
return [] if ary.empty?
block.call(ary) do |item|
if target_value.nil?
@@ -1021,7 +1005,7 @@ module Liquid
# ```
value = drop[property_or_keys]
return value if !value.nil? || !has_nested_property?(property_or_keys)
return value if !value.nil? || !property_or_keys.is_a?(String)
keys = property_or_keys.split('.')
keys.reduce(drop) do |drop, key|
@@ -1029,10 +1013,6 @@ module Liquid
end
end
def has_nested_property?(property)
property.is_a?(String) && property.include?('.')
end
def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end
@@ -1056,16 +1036,6 @@ module Liquid
end
end
def type_safe_compare(a, b)
klass_a = a.class
klass_b = b.class
# Converting classes to string to have a deterministic comparison.
return nil_safe_casecmp(klass_a, klass_b) if klass_a != klass_b
yield(a, b)
end
def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
@@ -1095,7 +1065,7 @@ module Liquid
def join(glue)
first = true
output = +""
each do |item|
@input.each do |item|
if first
first = false
else
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.7.2"
VERSION = "5.6.5"
end
+4 -4
View File
@@ -77,16 +77,16 @@ class HashRenderingTest < Minitest::Test
assert_template_result("{\"numbers\"=>[{:foo=>42}]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [{ foo: 42 }] } })
end
def test_render_hash_with_hash_key
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
end
def test_join_filter_with_hash
array = [{ "key1" => "value1" }, { "key2" => "value2" }]
glue = { "lol" => "wut" }
assert_template_result("{\"key1\"=>\"value1\"}{\"lol\"=>\"wut\"}{\"key2\"=>\"value2\"}", "{{ my_array | join: glue }}", { "my_array" => array, "glue" => glue })
end
def test_render_hash_with_hash_key
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
end
def test_rendering_hash_with_custom_to_s_method_uses_custom_to_s
my_hash = Class.new(Hash) do
def to_s
-146
View File
@@ -157,18 +157,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal([], @filters.slice(input, -(1 << 63), 6))
end
def test_find_on_empty_array
assert_nil(@filters.find([], 'foo', 'bar'))
end
def test_find_index_on_empty_array
assert_nil(@filters.find_index([], 'foo', 'bar'))
end
def test_has_on_empty_array
refute(@filters.has([], 'foo', 'bar'))
end
def test_truncate
assert_equal('1234...', @filters.truncate('1234567890', 7))
assert_equal('1234567890', @filters.truncate('1234567890', 20))
@@ -317,16 +305,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1121314', @filters.join([1, 2, 3, 4], 1))
end
def test_join_calls_to_liquid_on_each_element
drop = Class.new(Liquid::Drop) do
def to_liquid
'i did it'
end
end
assert_equal('i did it, i did it', @filters.join([drop.new, drop.new], ", "))
end
def test_sort
assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
@@ -988,18 +966,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
end
def test_has_with_empty_arrays
template = <<~LIQUID
{%- assign has_product = products | has: 'title.content', 'Not found' -%}
{%- unless has_product -%}
Product not found.
{%- endunless -%}
LIQUID
expected_output = "Product not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_has_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
@@ -1066,18 +1032,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_find_with_empty_arrays
template = <<~LIQUID
{%- assign product = products | find: 'title.content', 'Not found' -%}
{%- unless product -%}
Product not found.
{%- endunless -%}
LIQUID
expected_output = "Product not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_find_index_with_value
products = [
{ "title" => "Pro goggles", "price" => 1299 },
@@ -1106,18 +1060,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_find_index_with_empty_arrays
template = <<~LIQUID
{%- assign index = products | find_index: 'title.content', 'Not found' -%}
{%- unless index -%}
Index not found.
{%- endunless -%}
LIQUID
expected_output = "Index not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_where
array = [
{ "handle" => "alpha", "ok" => true },
@@ -1385,94 +1327,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_sort_with_different_types
input = [
{ "price" => 1000 },
{ "price" => :none },
{ "price" => 3000 }
]
assert_raises(Liquid::ArgumentError) do
@filters.sort(input, "price")
end
end
def test_sort_with_nested_different_types
input = [
{ "price" => { "value" => 1000, "unit" => "BRL" } },
{ "price" => { "value" => 2000, "unit" => nil } },
{ "price" => { "value" => 3000, "unit" => :none } }
]
expected_output = "2000, 1000, 3000"
template = <<~LIQUID
{{- input | sort: 'price.unit' | map: 'price.value' | join: ', ' -}}
LIQUID
assert_template_result(expected_output, template, { "input" => input })
end
def test_sort_natural_with_nested_different_types
input = [
{ "price" => { "value" => 1000, "unit" => "brl" } },
{ "price" => { "value" => 2000, "unit" => "BRL" } },
{ "price" => { "value" => 3000, "unit" => nil } },
{ "price" => { "value" => 4000, "unit" => :brl } }
]
expected_output = "1000, 2000, 4000, 3000"
template = <<~LIQUID
{{- input | sort_natural: 'price.unit' | map: 'price.value' | join: ', ' -}}
LIQUID
assert_template_result(expected_output, template, { "input" => input })
end
def test_uniq_with_nested_different_types
input = [
{ "price" => { "value" => 1000, "unit" => "BRL" } },
{ "price" => { "value" => 2000, "unit" => "BRL" } },
{ "price" => { "value" => 3000, "unit" => :USD } },
{ "price" => { "value" => 4000, "unit" => :BRL } },
{ "price" => { "value" => 5000, "unit" => nil } }
]
expected_output = "BRL, USD, BRL, " # Uniq handles different types uniqueness
template = <<~LIQUID
{{- input | uniq: 'price.unit' | map: 'price.unit' | join: ', ' -}}
LIQUID
assert_template_result(expected_output, template, { "input" => input })
end
def test_map_with_nested_different_types
input = [
{ "price" => { "value" => 1000, "unit" => "brl" } },
{ "price" => { "value" => 2000, "unit" => "BRL" } },
{ "price" => { "value" => 3000, "unit" => nil } },
{ "price" => { "value" => 4000, "unit" => :brl } }
]
expected_output = "brl, BRL, , brl"
template = <<~LIQUID
{{- input | map: 'price.unit'| join: ', ' -}}
LIQUID
assert_template_result(expected_output, template, { "input" => input })
end
def test_sum_with_nested_different_types
input = [
{ "price" => { "value" => 1000 } },
{ "price" => { "value" => nil } },
{ "price" => { "value" => :none } },
{ "price" => { "value" => 3000 } }
]
expected_output = "4000"
template = <<~LIQUID
{{- input | sum: 'price.value' -}}
LIQUID
assert_template_result(expected_output, template, { "input" => input })
end
private
def with_timezone(tz)