Building on the list of brews by style page I built for the brewlog, I also wanted a page to summarize all of the ingredients I’ve used, with total weights.

Not just fun trivia, but this is actually useful. It could help me understand which speciality grains would be worth buying in bulk, since I’d use them enough to justify it.

Unfortunately, this isn’t quite as easy as the styles page. In addition to creating a list of ingredients, I also need to tally up the total amount for each. And, unlike the styles, each recipe has lists of ingredients so some nested loops are needed.

This is a bit more than what can be done in markdown and Liquid.

Jekyll Plugins

A custom plugin can be included in a Jekyll site by placing it in the _plugins directory. My plugin is a simple Ruby script: ingredients.rb. No other changes are needed to load the file, when Jekyll generates the site, it will automatically find the file and load it as a plugin.

A Ruby Plugin

The structure of a Jekyll plugin written in Ruby is simple. I want to add a couple of filters that can be used in markdown with Liquid:

module Jekyll
  module IngredientsFilters
    # functions to define new filters
    # the function name will be the name of the new filter
  end
end

Liquid::Template.register_filter(Jekyll:IngredientsFilters)

I decided the easiest approach was to break the task apart into a few steps:

  • Get a list of ingredients across all recipes
  • Get a total weight for a given ingredient
  • Parse weight strings from the formatted value in the recipe
  • Format a raw weight back to a nice formatted string

Ingredients List

First, generating a list of all ingredients. I actually split everything between ‘grains’ (mash) and ‘boil.’ Not only is that a logical split (between grain and hops), but the recipe definied in the front matter of each posts defines them the same way; two lists.

So, this is the filter that returns a table of grain names and corresponding total amounts. It takes a list of posts as an argument:

def grain_weights(posts)
  grains = {}
  posts.each do |post|
    post['recipe']['fermentables'].each do |grain|
      if not grains.include?(grain['name'])
        grains[grain['name']] = 0
      end
      grains[grain['name']] += parse_weight(grain['amount'])
    end
  end
  return (grains.sort_by { |name, amount| amount }).reverse
end

In my new page, ingredients.markdown, I use this like a normal filter:

{% assign grains = site.posts | grain_weights %}

Creating a list in markdown is now super simple:

{% for grain in grains %}
 - {{ grain[0] }}: {{ grain[1] | format_weight -}}
{% endfor %}

The table can be accessed like an array, the ingredient name ends up in index ‘0’ and the amount in ‘1.’

Parsing and Formatting Weight Strings

The grain_weights filter uses another function, parse_weight to convert the string defined in the recipe to an actual number. This is done with a few simple regular expressions:

def parse_weight(amount)
  lb = amount.match /(?<lbs>[0-9\.]+) lb/
  oz = amount.match /(?<oz>[0-9\.]+) oz/
  lbs = 0
  if lb
    lbs += lb[:lbs].to_f
  end
  if oz
    lbs += oz[:oz].to_f / 16.0
  end
  return lbs
end

Then a simple filter to format the totals back when displaying them:

def format_weight(amount)
  lbs = amount.floor
  oz = (amount - amount.floor) * 16.0
  w = ""
  if lbs > 0
    w += "%0.0f lbs" % [lbs]
  end
  if oz > 0
    if lbs > 0
      w += " "
    end
    w += "%0.1f oz" % [oz]
  end
  return w
end