diff --git a/docs/_docs/templates.md b/docs/_docs/templates.md index a203172e..79b5fdae 100644 --- a/docs/_docs/templates.md +++ b/docs/_docs/templates.md @@ -147,6 +147,22 @@ common tasks easier.

+ + +

Group By Expression

+

Group an array's items using a Liquid expression.

+ + +

+ {% raw %}{{ site.members | group_by_exp:"item", +"item.graduation_year | truncate: 3, \"\"" }}{% endraw %} +

+

+ [{"name"=>"201...", "items"=>[...]}, +{"name"=>"200...", "items"=>[...]}] +

+ +

XML Escape

diff --git a/lib/jekyll/filters.rb b/lib/jekyll/filters.rb index ddd4fea9..43e01df1 100644 --- a/lib/jekyll/filters.rb +++ b/lib/jekyll/filters.rb @@ -8,6 +8,8 @@ require_all "jekyll/filters" module Jekyll module Filters include URLFilters + include GroupingFilters + # Convert a Markdown string into HTML output. # # input - The Markdown String to convert. @@ -205,29 +207,6 @@ module Jekyll as_liquid(input).to_json end - # Group an array of items by a property - # - # input - the inputted Enumerable - # property - the property - # - # Returns an array of Hashes, each looking something like this: - # {"name" => "larry" - # "items" => [...] } # all the items where `property` == "larry" - def group_by(input, property) - if groupable?(input) - input.group_by { |item| item_property(item, property).to_s } - .each_with_object([]) do |item, array| - array << { - "name" => item.first, - "items" => item.last, - "size" => item.last.size - } - end - else - input - end - end - # Filter an array of objects # # input - the object array @@ -381,11 +360,6 @@ module Jekyll end.localtime end - private - def groupable?(element) - element.respond_to?(:group_by) - end - private def item_property(item, property) if item.respond_to?(:to_liquid) @@ -436,6 +410,7 @@ module Jekyll condition end + end end diff --git a/lib/jekyll/filters/grouping_filters.rb b/lib/jekyll/filters/grouping_filters.rb new file mode 100644 index 00000000..a16901d9 --- /dev/null +++ b/lib/jekyll/filters/grouping_filters.rb @@ -0,0 +1,63 @@ +module Jekyll + module Filters + module GroupingFilters + # Group an array of items by a property + # + # input - the inputted Enumerable + # property - the property + # + # Returns an array of Hashes, each looking something like this: + # {"name" => "larry" + # "items" => [...] } # all the items where `property` == "larry" + def group_by(input, property) + if groupable?(input) + groups = input.group_by { |item| item_property(item, property).to_s } + grouped_array(groups) + else + input + end + end + + # Group an array of items by 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 group_by_exp(input, variable, expression) + return input unless groupable?(input) + + parsed_expr = parse_expression(expression) + @context.stack do + groups = input.group_by do |item| + @context[variable] = item + parsed_expr.render(@context) + end + grouped_array(groups) + end + end + + private + def parse_expression(str) + Liquid::Variable.new(str, {}) + end + + private + def groupable?(element) + element.respond_to?(:group_by) + end + + private + def grouped_array(groups) + groups.each_with_object([]) do |item, array| + array << { + "name" => item.first, + "items" => item.last, + "size" => item.last.size + } + end + end + end + end +end diff --git a/test/test_filters.rb b/test/test_filters.rb index 48f90bbb..e5dedfae 100644 --- a/test/test_filters.rb +++ b/test/test_filters.rb @@ -756,6 +756,91 @@ class TestFilters < JekyllUnitTest end end + context "group_by_exp filter" do + should "successfully group array of Jekyll::Page's" do + @filter.site.process + groups = @filter.group_by_exp(@filter.site.pages, "page", "page.layout | upcase") + groups.each do |g| + assert( + ["DEFAULT", "NIL", ""].include?(g["name"]), + "#{g["name"]} isn't a valid grouping." + ) + case g["name"] + when "DEFAULT" + assert( + g["items"].is_a?(Array), + "The list of grouped items for 'default' is not an Array." + ) + # adjust array.size to ignore symlinked page in Windows + qty = Utils::Platforms.really_windows? ? 4 : 5 + assert_equal qty, g["items"].size + when "nil" + assert( + g["items"].is_a?(Array), + "The list of grouped items for 'nil' is not an Array." + ) + assert_equal 2, g["items"].size + when "" + assert( + g["items"].is_a?(Array), + "The list of grouped items for '' is not an Array." + ) + # adjust array.size to ignore symlinked page in Windows + qty = Utils::Platforms.really_windows? ? 14 : 15 + assert_equal qty, g["items"].size + end + end + end + + should "include the size of each grouping" do + groups = @filter.group_by_exp(@filter.site.pages, "page", "page.layout") + groups.each do |g| + assert_equal( + g["items"].size, + g["size"], + "The size property for '#{g["name"]}' doesn't match the size of the Array." + ) + end + end + + should "allow more complex filters" do + items = [ + { "version"=>"1.0", "result"=>"slow" }, + { "version"=>"1.1.5", "result"=>"medium" }, + { "version"=>"2.7.3", "result"=>"fast" } + ] + + result = @filter.group_by_exp(items, "item", "item.version | split: '.' | first") + assert_equal 2, result.size + end + + should "be equivalent of group_by" do + actual = @filter.group_by_exp(@filter.site.pages, "page", "page.layout") + expected = @filter.group_by(@filter.site.pages, "layout") + + assert_equal expected, actual + end + + should "return any input that is not an array" do + assert_equal "some string", @filter.group_by_exp("some string", "la", "le") + end + + should "group by full element (as opposed to a field of the element)" do + items = %w(a b c d) + + result = @filter.group_by_exp(items, "item", "item") + assert_equal 4, result.length + assert_equal ["a"], result.first["items"] + end + + should "accept hashes" do + hash = { 1 => "a", 2 => "b", 3 => "c", 4 => "d" } + + result = @filter.group_by_exp(hash, "item", "item") + assert_equal 4, result.length + end + end + context "sort filter" do should "raise Exception when input is nil" do err = assert_raises ArgumentError do