remy-actual
remy-actual

Reputation: 824

Fetch, manipulate and sort hash object values with Liquid

I'm making a glossary template for a Jekyll site deployed on Github Pages.
Entries are pulled from a _data/glossary.yml file.
I want the template to arrange the entries alphabetically regardless of the order of the data in glossary.yml.

Using {% assign glossary = site.data.glossary | sort 'term' %} does return an alphabetically sorted object that I can iterate over with a for loop.
However the sort filter is case sensitive - lowercase entries are sorted after all of the capitalized or uppercase terms.

Liquid 4.0.0 adds a sort_natural filter that does what I want, but Github Pages currently runs 3.0.6, so I need a workaround.

My question is how can I:

  1. fetch site.data.glossary in a Liquid template?
  2. manipulate the string values of the first map of each entry?
    • (i.e. use the capitalize string filter to get rid of the uppercase/lowercase discrepancies)
  3. sort the whole map using the locally string filtered values?
  4. Bonus: If I can still use the source string values with their original case preserved for final display in the generated html.

For example, given the following data/glossary.yml:

- term: apricot
  loc: plastic

- term: Apple
  loc: basket

- term: Banana
  loc: basket

- term: bowtie
  loc: closet

- term: Cat
  loc: outside

How do I create a local Liquid object variable that sorts and displays the following?:

Upvotes: 3

Views: 930

Answers (1)

David Jacquel
David Jacquel

Reputation: 52809

The only way is to use a filter plugin that will implement liquid 4 natural_sort.

Some cut and past later you have _plugins/natural_sort_filter.rb :

module Jekyll
  module SortNatural
    # Sort elements of an array ignoring case if strings
    # provide optional property with which to sort an array of hashes or drops
    def sort_natural(input, property = nil)
      ary = InputIterator.new(input)

      if property.nil?
        ary.sort { |a, b| a.casecmp(b) }
      elsif ary.empty? # The next two cases assume a non-empty array.
        []
      elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
        ary.sort { |a, b| a[property].casecmp(b[property]) }
      end
    end

    class InputIterator
      include Enumerable

      def initialize(input)
        @input = if input.is_a?(Array)
          input.flatten
        elsif input.is_a?(Hash)
          [input]
        elsif input.is_a?(Enumerable)
          input
        else
          Array(input)
        end
      end

      def join(glue)
        to_a.join(glue)
      end

      def concat(args)
        to_a.concat(args)
      end

      def reverse
        reverse_each.to_a
      end

      def uniq(&block)
        to_a.uniq(&block)
      end

      def compact
        to_a.compact
      end

      def empty?
        @input.each { return false }
        true
      end

      def each
        @input.each do |e|
          yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
        end
      end
    end
  end
end
Liquid::Template.register_filter(Jekyll::SortNatural)

This new filter can be used like this :

{% assign glossary = site.data.glossary | sort_natural: 'term' %}
<ul>
{% for item in glossary %}
  <li>{{ item.term }} - {{ item.loc }}</li>
{% endfor %}
</ul>

Upvotes: 3

Related Questions