From 66c4ff8800f11858a1d4073ae83c4ea30519efbc Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Fri, 5 Feb 2016 20:27:33 +0000 Subject: [PATCH] Add a where_exp filter for filtering by expression This commit introduces a where_exp filter, which can be used as follows: `{{ array | where_exp: "item", "item == 10" }}` `{{ array | where_exp: "item", "item.field > 10" }}` `{{ site.posts | where_exp: "post", "post contains 'field'" }}` `{{ site.posts | where_exp: "post", "post.array contains 'giraffes'" }}` This permits a variety of use cases, such as reported in: jekyll#4467, jekyll#4385, jekyll#2787. --- lib/jekyll/filters.rb | 36 +++++++++++++++++++++++++ site/_docs/templates.md | 16 ++++++++++++ test/test_filters.rb | 58 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/lib/jekyll/filters.rb b/lib/jekyll/filters.rb index 02523d9c..613726fd 100644 --- a/lib/jekyll/filters.rb +++ b/lib/jekyll/filters.rb @@ -1,6 +1,7 @@ require 'uri' require 'json' require 'date' +require 'liquid' module Jekyll module Filters @@ -225,6 +226,26 @@ module Jekyll input.select { |object| Array(item_property(object, property)).map(&:to_s).include?(value.to_s) } end + # Filters an array of objects against an expression + # + # input - the object array + # variable - the variable to assign each item to in the expression + # expression - a Liquid comparison expression passed in as a string + # + # Returns the filtered array of objects + def where_exp(input, variable, expression) + return input unless input.is_a?(Enumerable) + input = input.values if input.is_a?(Hash) + + c = parse_condition(expression) + @context.stack do + input.select do |object| + @context[variable] = object + c.evaluate(@context) + end + end + end + # Sort an array of objects # # input - the object array @@ -363,5 +384,20 @@ module Jekyll end end end + + # Parse a string to a Liquid Condition + def parse_condition(exp) + parser = Liquid::Parser.new(exp) + a = parser.expression + if op = parser.consume?(:comparison) + b = parser.expression + condition = Liquid::Condition.new(a, op, b) + else + condition = Liquid::Condition.new(a) + end + parser.consume(:end_of_string) + + condition + end end end diff --git a/site/_docs/templates.md b/site/_docs/templates.md index 60892505..cb6a145b 100644 --- a/site/_docs/templates.md +++ b/site/_docs/templates.md @@ -88,6 +88,22 @@ common tasks easier.

+ + +

Where Expression

+

Select all the objects in an array where the expression is true.

+ + +

+ {% raw %}{{ site.members | where_exp:"item", +"item.graduation_year == 2014" }}{% endraw %} + {% raw %}{{ site.members | where_exp:"item", +"item.graduation_year < 2014" }}{% endraw %} + {% raw %}{{ site.members | where_exp:"item", +"item.projects includes 'foo'" }}{% endraw %} +

+ +

Group By

diff --git a/test/test_filters.rb b/test/test_filters.rb index ae685872..565e8208 100644 --- a/test/test_filters.rb +++ b/test/test_filters.rb @@ -354,6 +354,64 @@ class TestFilters < JekyllUnitTest end end + context "where_exp filter" do + should "return any input that is not an array" do + assert_equal "some string", @filter.where_exp("some string", "la", "le") + end + + should "filter objects in a hash appropriately" do + hash = {"a"=>{"color"=>"red"}, "b"=>{"color"=>"blue"}} + assert_equal 1, @filter.where_exp(hash, "item", "item.color == 'red'").length + assert_equal [{"color"=>"red"}], @filter.where_exp(hash, "item", "item.color == 'red'") + end + + should "filter objects appropriately" do + assert_equal 2, @filter.where_exp(@array_of_objects, "item", "item.color == 'red'").length + end + + should "stringify during comparison for compatibility with liquid parsing" do + hash = { + "The Words" => {"rating" => 1.2, "featured" => false}, + "Limitless" => {"rating" => 9.2, "featured" => true}, + "Hustle" => {"rating" => 4.7, "featured" => true}, + } + + results = @filter.where_exp(hash, "item", "item.featured == true") + assert_equal 2, results.length + assert_equal 9.2, results[0]["rating"] + assert_equal 4.7, results[1]["rating"] + + results = @filter.where_exp(hash, "item", "item.rating == 4.7") + assert_equal 1, results.length + assert_equal 4.7, results[0]["rating"] + end + + should "filter with other operators" do + assert_equal [3, 4, 5], @filter.where_exp([ 1, 2, 3, 4, 5 ], "n", "n >= 3") + end + + objects = [ + { "id" => "a", "groups" => [1, 2] }, + { "id" => "b", "groups" => [2, 3] }, + { "id" => "c" }, + { "id" => "d", "groups" => [1, 3] } + ] + should "filter with the contains operator over arrays" do + results = @filter.where_exp(objects, "obj", "obj.groups contains 1") + assert_equal 2, results.length + assert_equal "a", results[0]["id"] + assert_equal "d", results[1]["id"] + end + + should "filter with the contains operator over hash keys" do + results = @filter.where_exp(objects, "obj", "obj contains 'groups'") + assert_equal 3, results.length + assert_equal "a", results[0]["id"] + assert_equal "b", results[1]["id"] + assert_equal "d", results[2]["id"] + end + end + context "sort filter" do should "raise Exception when input is nil" do err = assert_raises ArgumentError do