From 9240addcf05d6e92cb661d6d51fc3b8c436298b7 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Fri, 22 Mar 2019 20:23:34 +0530 Subject: [PATCH] Detect `nil` and empty values in objects with `where` filter (#7580) Merge pull request 7580 --- docs/_docs/liquid/filters.md | 15 ++++++++++ features/embed_filters.feature | 52 ++++++++++++++++++++++++++++++++++ features/step_definitions.rb | 11 +++++++ lib/jekyll/filters.rb | 30 ++++++++++++++++---- test/test_filters.rb | 35 +++++++++++++++++++++++ 5 files changed, 137 insertions(+), 6 deletions(-) diff --git a/docs/_docs/liquid/filters.md b/docs/_docs/liquid/filters.md index 84aad19e..bd33c07c 100644 --- a/docs/_docs/liquid/filters.md +++ b/docs/_docs/liquid/filters.md @@ -104,6 +104,21 @@ The default is `default`. They are as follows (with what they filter): - `ascii`: spaces, non-alphanumeric, and non-ASCII characters - `latin`: like `default`, except Latin characters are first transliterated (e.g. `àèïòü` to `aeiou`) {%- include docs_version_badge.html version="3.7.0" -%}. +### Detecting `nil` values with `where` filter {%- include docs_version_badge.html version="4.0.0" -%} + +You can use the `where` filter to detect documents and pages with properties that are `nil` or `""`. For example, + +```liquid +// Using `nil` to select posts that either do not have `my_prop` defined or `my_prop` has been set to `nil` explicitly. +{% raw %}{% assign filtered_posts = site.posts | where: 'my_prop', nil %}{% endraw %} +``` + +```liquid +// Using Liquid's special literal `empty` or `blank` to select posts that have `my_prop` set to an empty value. +{% raw %}{% assign filtered_posts = site.posts | where: 'my_prop', empty %}{% endraw %} +``` + + ### Standard Liquid Filters For your convenience, here is the list of all [Liquid filters]({{ page.shopify_filter_url }}) with links to examples in the official Liquid documentation. diff --git a/features/embed_filters.feature b/features/embed_filters.feature index e3802fe5..32f0fe7e 100644 --- a/features/embed_filters.feature +++ b/features/embed_filters.feature @@ -107,3 +107,55 @@ Feature: Embed filters Then I should get a zero exit status And the _site directory should exist And I should see exactly "The rule of 3: Fly, Run, Jump," in "_site/bird.html" + + Scenario: Filter posts by given property and value + Given I have a _posts directory + And I have the following posts: + | title | date | content | property | + | Bird | 2019-03-13 | Chirp | [nature, sounds] | + | Cat | 2019-03-14 | Meow | [sounds] | + | Dog | 2019-03-15 | Bark | | + | Elephant | 2019-03-16 | Asiatic | wildlife | + | Goat | 2019-03-17 | Mountains | "" | + | Horse | 2019-03-18 | Mustang | [] | + | Iguana | 2019-03-19 | Reptile | {} | + | Jaguar | 2019-03-20 | Reptile | {foo: lorem, bar: nature} | + And I have a "string-value.md" page with content: + """ + {% assign pool = site.posts | reverse | where: 'property', 'wildlife' %} + {{ pool | map: 'title' | join: ', ' }} + """ + And I have a "string-value-array.md" page with content: + """ + {% assign pool = site.posts | reverse | where: 'property', 'sounds' %} + {{ pool | map: 'title' | join: ', ' }} + """ + And I have a "string-value-hash.md" page with content: + """ + {% assign pool = site.posts | reverse | where: 'property', 'nature' %} + {{ pool | map: 'title' | join: ', ' }} + """ + And I have a "nil-value.md" page with content: + """ + {% assign pool = site.posts | reverse | where: 'property', nil %} + {{ pool | map: 'title' | join: ', ' }} + """ + And I have an "empty-liquid-literal.md" page with content: + """ + {% assign pool = site.posts | reverse | where: 'property', empty %} + {{ pool | map: 'title' | join: ', ' }} + """ + And I have a "blank-liquid-literal.md" page with content: + """ + {% assign pool = site.posts | reverse | where: 'property', blank %} + {{ pool | map: 'title' | join: ', ' }} + """ + When I run jekyll build + Then I should get a zero exit status + And the _site directory should exist + And I should see exactly "

Elephant

" in "_site/string-value.html" + And I should see exactly "

Bird, Cat

" in "_site/string-value-array.html" + And I should see exactly "

Bird

" in "_site/string-value-hash.html" + And I should see exactly "

Dog

" in "_site/nil-value.html" + And I should see exactly "

Dog, Goat, Horse, Iguana

" in "_site/empty-liquid-literal.html" + And I should see exactly "

Dog, Goat, Horse, Iguana

" in "_site/blank-liquid-literal.html" diff --git a/features/step_definitions.rb b/features/step_definitions.rb index fe5ade94..fb2b75bb 100644 --- a/features/step_definitions.rb +++ b/features/step_definitions.rb @@ -67,6 +67,17 @@ end # +Given(%r!^I have an? "(.*)" page with content:$!) do |file, text| + File.write(file, <<~DATA) + --- + --- + + #{text} + DATA +end + +# + Given(%r!^I have an? (.*) directory$!) do |dir| unless File.directory?(dir) then FileUtils.mkdir_p(dir) diff --git a/lib/jekyll/filters.rb b/lib/jekyll/filters.rb index 01fd0372..055a3e5e 100644 --- a/lib/jekyll/filters.rb +++ b/lib/jekyll/filters.rb @@ -161,13 +161,15 @@ module Jekyll # Filter an array of objects # - # input - the object array - # property - property within each object to filter by - # value - desired value + # input - the object array. + # property - the property within each object to filter by. + # value - the desired value. + # Cannot be an instance of Array nor Hash since calling #to_s on them returns + # their `#inspect` string object. # # Returns the filtered array of objects def where(input, property, value) - return input if property.nil? || value.nil? + return input if !property || value.is_a?(Array) || value.is_a?(Hash) return input unless input.respond_to?(:select) input = input.values if input.is_a?(Hash) @@ -182,8 +184,8 @@ module Jekyll # stash or retrive results to return @where_filter_cache[input_id][property][value] ||= begin input.select do |object| - Array(item_property(object, property)).map!(&:to_s).include?(value.to_s) - end || [] + compare_property_vs_target(item_property(object, property), value) + end.to_a end end @@ -323,6 +325,22 @@ module Jekyll .map!(&:last) end + # `where` filter helper + def compare_property_vs_target(property, target) + case target + when NilClass + return true if property.nil? + when Liquid::Expression::MethodLiteral # `empty` or `blank` + return true if Array(property).join == target.to_s + else + Array(property).each do |prop| + return true if prop.to_s == target.to_s + end + end + + false + end + def item_property(item, property) @item_property_cache ||= {} @item_property_cache[property] ||= {} diff --git a/test/test_filters.rb b/test/test_filters.rb index fcdc0208..39ec8565 100644 --- a/test/test_filters.rb +++ b/test/test_filters.rb @@ -844,6 +844,11 @@ class TestFilters < JekyllUnitTest assert_equal 2, @filter.where(@array_of_objects, "color", "red").length end + should "filter objects with null properties appropriately" do + array = [{}, { "color" => nil }, { "color" => "" }, { "color" => "text" }] + assert_equal 2, @filter.where(array, "color", nil).length + end + should "filter array properties appropriately" do hash = { "a" => { "tags"=>%w(x y) }, @@ -862,6 +867,36 @@ class TestFilters < JekyllUnitTest assert_equal 2, @filter.where(hash, "tags", "x").length end + should "filter hash properties with null and empty values" do + hash = { + "a" => { "tags" => {} }, + "b" => { "tags" => "" }, + "c" => { "tags" => nil }, + "d" => { "tags" => ["x", nil] }, + "e" => { "tags" => [] }, + "f" => { "tags" => "xtra" }, + } + + assert_equal [{ "tags" => nil }], @filter.where(hash, "tags", nil) + + assert_equal( + [{ "tags" => "" }, { "tags" => ["x", nil] }], + @filter.where(hash, "tags", "") + ) + + # `{{ hash | where: 'tags', empty }}` + assert_equal( + [{ "tags" => {} }, { "tags" => "" }, { "tags" => nil }, { "tags" => [] }], + @filter.where(hash, "tags", Liquid::Expression::LITERALS["empty"]) + ) + + # `{{ `hash | where: 'tags', blank }}` + assert_equal( + [{ "tags" => {} }, { "tags" => "" }, { "tags" => nil }, { "tags" => [] }], + @filter.where(hash, "tags", Liquid::Expression::LITERALS["blank"]) + ) + end + should "not match substrings" do hash = { "a" => { "category"=>"bear" },