From 4b01977067c0cf8c7f0c55dc4d6e98df0fe9b5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20S=C3=B8gaard?= Date: Thu, 12 May 2022 16:41:40 +0200 Subject: [PATCH] Add reject filter #1573 --- History.md | 11 ++ lib/liquid/standardfilters.rb | 72 ++++++++++ test/integration/standard_filter_test.rb | 167 +++++++++++++++++++++++ 3 files changed, 250 insertions(+) diff --git a/History.md b/History.md index 7d94db60..5bb7c2ec 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,6 @@ # Liquid Change Log +<<<<<<< HEAD ## 5.5.0 2024-03-21 Please reference the GitHub release for more information. @@ -13,6 +14,7 @@ Please reference the GitHub release for more information. * Allow `#` to be used as an inline comment tag (#1498) [CP Clermont] ### Fixes +<<<<<<< HEAD * `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee] * Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee] * Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont] @@ -34,10 +36,19 @@ Please reference the GitHub release for more information. ### Features * Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard] * Eagerly cache global filters (#1524) [Jean Boussier] +======= +## 5.2.0 (unreleased) + +### Features +* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard] +>>>>>>> 588d407e (Fix new changelog entry so it is under unreleased) ### Fixes * Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith] * Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont] +======= +* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith] +>>>>>>> 50c88fe7 (History.md: Add missing PR number to previous changelog entry) ## 5.1.0 / 2021-09-09 diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index ffaceb32..bbfbc613 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -449,6 +449,7 @@ module Liquid end end +<<<<<<< HEAD # @liquid_public_docs # @liquid_type filter # @liquid_category array @@ -456,6 +457,33 @@ module Liquid # Removes any duplicate items in an array. # @liquid_syntax array | uniq # @liquid_return [array[untyped]] +======= + # Reject the elements of an array to those with a certain property value. + # By default the target is any falsy value. + def reject(input, properties, target_value = nil) + raise_property_error(properties) unless properties.is_a?(String) + + properties = properties.to_s.split('.') + ary = InputIterator.new(input, context) + + ary.reject do |item| + if item.is_a?(Hash) + value = item.dig(*properties) + + if target_value.nil? + !value + else + value == target_value + end + else + true + end + end + end + + # Remove duplicate elements from an array + # provide optional property with which to determine uniqueness +>>>>>>> bfdcfcea (Add reject filter) def uniq(input, property = nil) ary = InputIterator.new(input, context) @@ -557,6 +585,7 @@ module Liquid input.to_s.sub(string.to_s, replacement.to_s) end +<<<<<<< HEAD # @liquid_public_docs # @liquid_type filter # @liquid_category string @@ -585,8 +614,34 @@ module Liquid # Removes any instance of a substring inside a string. # @liquid_syntax string | remove: string # @liquid_return [string] +======= + # Replace the last occurrences of a string with another + def replace_last(input, string, replacement) + input = input.to_s + string = string.to_s + replacement = replacement.to_s + + start_index = input.rindex(string) + + return input unless start_index + + output = input.dup + output[start_index, string.length] = replacement + output + end + + # remove a substring +>>>>>>> e575c1f1 (Add replace_last and remove_last filters) def remove(input, string) +<<<<<<< HEAD +<<<<<<< HEAD replace(input, string, '') +======= + replace(input.to_s, string, '') +>>>>>>> f72cfb13 (Delegate functions to corresponding replace functions) +======= + replace(input, string, '') +>>>>>>> 5187399f (Update lib/liquid/standardfilters.rb) end # @liquid_public_docs @@ -597,9 +652,18 @@ module Liquid # @liquid_syntax string | remove_first: string # @liquid_return [string] def remove_first(input, string) +<<<<<<< HEAD +<<<<<<< HEAD replace_first(input, string, '') +======= + replace_first(input.to_s, string, '') +>>>>>>> f72cfb13 (Delegate functions to corresponding replace functions) +======= + replace_first(input, string, '') +>>>>>>> 45f186b4 (Remove string formatter) end +<<<<<<< HEAD # @liquid_public_docs # @liquid_type filter # @liquid_category string @@ -618,6 +682,14 @@ module Liquid # Adds a given string to the end of a string. # @liquid_syntax string | append: string # @liquid_return [string] +======= + # remove the last occurences of a substring + def remove_last(input, string) + replace_last(input, string, '') + end + + # add one string to another +>>>>>>> e575c1f1 (Add replace_last and remove_last filters) def append(input, string) input.to_s + string.to_s end diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index eae4a1c9..6ac75371 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -603,9 +603,38 @@ class StandardFiltersTest < Minitest::Test def test_replace assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b')) assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2)) +<<<<<<< HEAD +<<<<<<< HEAD +<<<<<<< HEAD assert_equal('1 1 1 1', @filters.replace('1 1 1 1', 2, 3)) assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: '1', 2 }}") +<<<<<<< HEAD + assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b')) +======= +>>>>>>> 38b7364f (Improve tests) + assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)) + assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3)) + assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}") +<<<<<<< HEAD +<<<<<<< HEAD + + assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b')) + assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2)) + assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3)) +======= + assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', '1', 2)) +======= +>>>>>>> 38b7364f (Improve tests) + assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2)) +>>>>>>> 832516fb (Add replace_last and remove_last tests) + assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}") +======= +======= + assert_equal('1 1 1 1', @filters.replace('1 1 1 1', 2, 3)) +>>>>>>> d81f7f04 (Add tests to make sure it returns original string on no replacement) +======= +>>>>>>> 63ae4cc2 (Remove redundant test assertions) assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b')) assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)) assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3)) @@ -614,11 +643,29 @@ class StandardFiltersTest < Minitest::Test assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b')) assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2)) assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3)) +<<<<<<< HEAD + assert_template_result('b b b b', "{{ 'a a a a' | replace: 'a', 'b' }}") + assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: 1, 2 }}") + assert_template_result('1 1 1 1', "{{ '1 1 1 1' | replace: 2, 3 }}") + assert_template_result('b a a a', "{{ 'a a a a' | replace_first: 'a', 'b' }}") + assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: 1, 2 }}") + assert_template_result('a a a a', "{{ 'a a a a' | replace_first: 'b', 'c' }}") + assert_template_result('a a a b', "{{ 'a a a a' | replace_last: 'a', 'b' }}") + assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: 1, 2 }}") +<<<<<<< HEAD +>>>>>>> b3d14e5b (Update tests) +======= + assert_template_result('a a a a', "{{ 'a a a a' | replace_last: 'b', 'c' }}") +>>>>>>> d81f7f04 (Add tests to make sure it returns original string on no replacement) +======= assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}") +>>>>>>> 63ae4cc2 (Remove redundant test assertions) end def test_remove assert_equal(' ', @filters.remove("a a a a", 'a')) +<<<<<<< HEAD +<<<<<<< HEAD assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}") assert_equal('b a a', @filters.remove_first("a b a a", 'a ')) @@ -626,6 +673,31 @@ class StandardFiltersTest < Minitest::Test assert_equal('a a b', @filters.remove_last("a a b a", ' a')) assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}") +======= + assert_equal(' ', @filters.remove("1 1 1 1", 1)) + assert_equal('b a a', @filters.remove_first("a b a a", 'a ')) + assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1)) + assert_equal('a a b', @filters.remove_last("a a b a", ' a')) + assert_equal('1 1 1 ', @filters.remove_last("1 1 1 1", 1)) +<<<<<<< HEAD +<<<<<<< HEAD + assert_template_result('a a a', "{{ 'a a a a' | remove_last: ' a' }}") +>>>>>>> 832516fb (Add replace_last and remove_last tests) +======= + assert_template_result('a a b', "{{ 'a a b a' | remove_last: ' a' }}") +>>>>>>> effee1ed (Update tests) +======= + assert_template_result(' ', "{{ 'a a a a' | remove: 'a' }}") +======= +>>>>>>> 63ae4cc2 (Remove redundant test assertions) + assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}") + + assert_equal('b a a', @filters.remove_first("a b a a", 'a ')) + assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}") + + assert_equal('a a b', @filters.remove_last("a a b a", ' a')) + assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}") +>>>>>>> b3d14e5b (Update tests) end def test_pipes_in_string_arguments @@ -900,6 +972,81 @@ class StandardFiltersTest < Minitest::Test assert_nil(@filters.where([nil], "ok")) end + def test_reject + input = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + expectation = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "delta", "ok" => true }, + ] + + assert_equal(expectation, @filters.reject(input, "ok", false)) + assert_equal(expectation, @filters.reject(input, "ok")) + end + + def test_reject_no_key_set + input = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta" }, + { "handle" => "gamma" }, + { "handle" => "delta", "ok" => true }, + ] + + expectation = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "delta", "ok" => true }, + ] + + assert_equal(expectation, @filters.reject(input, "ok")) + end + + def test_reject_non_array_map_input + assert_equal([], @filters.reject({ "foo" => "bar" }, "foo", "bar")) + assert_equal([{ "foo" => "baz" }], @filters.reject({ "foo" => "baz" }, "foo", "bar")) + end + + def test_reject_indexable_but_non_map_value + assert_equal([], @filters.reject(1, "ok", true)) + assert_equal([], @filters.reject(1, "ok")) + end + + def test_reject_non_boolean_value + input = [ + { "message" => "Bonjour!", "language" => "French" }, + { "message" => "Hello!", "language" => "English" }, + ] + + assert_equal([{ "message" => "Hello!", "language" => "English" }], @filters.reject(input, "language", "French")) + assert_equal([{ "message" => "Bonjour!", "language" => "French" }], @filters.reject(input, "language", "English")) + end + + def test_reject_array_of_only_unindexable_values + assert_equal([], @filters.reject([nil, nil], "ok", true)) + assert_equal([], @filters.reject([nil, nil], "ok")) + end + + def test_reject_deep + input = [ + { "item" => { "handle" => "alpha", "ok" => true } }, + { "item" => { "handle" => "beta", "ok" => false } }, + { "item" => { "handle" => "gamma", "ok" => false } }, + { "item" => { "handle" => "delta", "ok" => true } }, + ] + + expectation = [ + { "item" => { "handle" => "alpha", "ok" => true } }, + { "item" => { "handle" => "delta", "ok" => true } }, + ] + + assert_equal(expectation, @filters.reject(input, "item.ok", false)) + assert_equal(expectation, @filters.reject(input, "item.ok")) + end + def test_all_filters_never_raise_non_liquid_exception test_drop = TestDrop.new(value: "test") test_drop.context = Context.new @@ -926,6 +1073,7 @@ class StandardFiltersTest < Minitest::Test { 1 => "bar" }, ["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }], ] +<<<<<<< HEAD StandardFilters.public_instance_methods(false).each do |method| arg_count = @filters.method(method).arity arg_count *= -1 if arg_count < 0 @@ -934,6 +1082,25 @@ class StandardFiltersTest < Minitest::Test @filters.send(method, *args) rescue Liquid::Error nil +======= + test_types.each do |first| + test_types.each do |second| + test_types.each do |third| + (@filters.methods - Object.methods).each do |method| + arg_count = @filters.method(method).arity + arg_count *= -1 if arg_count < 0 + inputs = [first] + inputs << ([second] * (arg_count - 1)) if arg_count > 1 + inputs << ([third] * (arg_count - 1)) if arg_count > 2 + + begin + @filters.send(method, *inputs) + rescue Liquid::ArgumentError, Liquid::ZeroDivisionError + nil + end + end + end +>>>>>>> f5e77b6d (Update test to support third argument) end end end