Remy Schrader Remy Schrader - 4 months ago 14
Ruby Question

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?:


  • Apple


    • basket


  • apricot


    • plastic


  • Banana


    • basket


  • bowtie


    • closet


  • Cat


    • outside



Answer

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>