Custom markup in nanoc

Published
4 July 2014
Tagged

If you're maintaining a blog, you will probably want to put images in your posts at some point. If you have some fancy server-side blogging software to do this for you, you're set. If, however, you're using a set of files on your local drive, which somehow get turned into a website, you have to maintain your post images yourself.

This is the current setup, assuming I wrote a blog post in May 2014 which contained two images, "image.jpg" and "image2.jpg":

It's not the most efficient setup, but it's mine.

It's not the most efficient setup, but it's mine.

Note that all posts from the same month share one folder for images. They have to double up on the namespace (i.e. array of possible image names), which means if I have two posts where I use an image called image.jpg I'll need to rename one of these files to image2.jpg or something similarly creative.

There's one problem with such a setup: when writing my blog post, I can't just assume that the image "foo" that I want will be located at /assets/images/posts/foo.png: I need to work out the year and month of the blog entry, then enter all that manually, e.g.:

![Sample caption](/assets/images/posts/2014-05/my-files.png)

This gets boring and repetitive quickly. I'd much prefer that my blogging engine (which is, after all, generating the whole site) work out where the image should be stored, so I only have to enter, say:

 !![Sample caption](my-files.png)

Would you believe me if I said this were true?

Bastardising markdown

All my blogging is done in Markdown. Markdown has seen a number of different variations as people get it to do different things that John Gruber never intended when he made it. There's a number of very good ruby markdown libraries for ruby too: my favourite is currently redcarpet, because of the vast array of different bits of markdown it can include. Things like my sample image syntax above can be included by adding new filters, but it's easier to do this by adding a filter to nanoc itself.

Nanoc's filters are surprisingly easy to create and maintain. All they need is an identifier and some code to run on pages. Here's an "identity" filter (i.e. a filter that just returns the post as-is):

class IdentityFilter < Nanoc::Filter
  identifier :identity

  def run(content)
    content
  end
end

If you put this in your site's lib folder (or some subfolder thereof), it'll get loaded at runtime. Then all you need to do is include it in your rules like you would normally:

compile "/stuff/*" do
  filter :identity
end

Of course, this doesn't do that much. But the nice thing about these filters is that they can be chained:

compile "/stuff/*" di
  filter :identity
  filter :redcarpet
end

Since our !![]() syntax is really a very specific subset of the usual markdown ![]() image syntax, we can probably just turn it into markdown, to be properly parsed by our markdown engine of choice:

The filter process

The filter process

Here's the skeleton of our plugin:

class ImageFilter < Nanoc::Filter
  identifier :images

  def run(content)
    content
  end
end

But what now? How do we get our !![]()-style images to autofill year and month? The trick here is that the item we're processing right now is available inside the filter as @item. That item's identifier contains date information (and, indeed, you might arbitrarily include date information in the header of your item, which means it's sitting in the hash ready for you to use). Given my post entry has an identifier like /blog/2014-05/2014-05-02-blog-entry/, we can perform a quick regex to retrieve the relevant data[1]:

if @item.identifier =~ %r<\A/blog/(\d{4}-\d{2})>
  yearmonth = $1
end

Here's how we can modify our articles, removing the "custom" image tags and replacing them with standard markdown-style image tags:

class ImageFilter < Nanoc::Filter
  identifier :images

  def run(content)
    if @item.identifier =~ %r<\A/blog/(\d{4}-\d{2})>
      yearmonth = $1

      content.gsub(/^!!\[(.*)]\((.*)\)$/) do
        alt = $1
        href = File.join("/assets/images/posts", yearmonth, $2)
        "![#{alt}](#{href})"
      end
    else
      content
    end
  end
end

As a bonus, this filter won't modify the item if it doesn't match the identifier string.

Expanding further

Obviously this kind of setup is perfectly designed for many small filters, each doing one focussed task. This means that you've got plenty of space to expand your own custom filters as you see fit. Want to use a <figure> tag around your images, or maybe implement some sort of custom syntax for maximum width or captions? It's all doable with a dedicated filter. Don't forget that each filter can be enabled or disabled on a specific subset of files, giving you ultimate control over which pages use your custom syntax.

Incidentally, this is what my custom filter looks like. Decoding everything is left as an exercise to the reader:

class ImageFilter < Nanoc::Filter
  identifier :images

  @images = {}
  class << self
    attr_reader :images

    def register_image(str, &blck)
      regexp = Regexp.compile("^" + str + '\[([^\]]*)\]\(([^)]*)\)$')
      @images[regexp] = blck
    end
  end

  def run(content, params={})
    ImageFilter.images.each do |regexp, blck|
      content = content.gsub(regexp) do
        caption = $1
        href = $2

        # Allow optional syntax: "href|width"
        if href.include?("|")
          href,width = href.split("|",2)
        end

        href = File.join(blck[@item], href)

        image_tag =   %|<img src="#{href}" title="#{caption}"| +
                      (width ? %| width="#{width}px"| : "") +
                      "/>"
        
        figure = "<figure>#{image_tag}"
        unless caption.empty?
          caption = Redcarpet::Markdown.new(Redcarpet::Render::HTML).render(caption)
          figure << %|<div class="caption">#{caption}</div>|
        end
        figure << "</figure>"
        figure
      end
    end
    content
  end
end

ImageFilter.register_image("!!"){ |item| item[:date].strftime("/assets/images/posts/%Y-%m") }
ImageFilter.register_image("!!p"){ "/assets/images/projects" }
ImageFilter.register_image("!!s"){ "/assets/images/site" }

Bonus step: quick image filing with Automator

The friendliest-looking robot-with-a-giant-pipe you ever saw.

The friendliest-looking robot-with-a-giant-pipe you ever saw.

You still have to get your images to your image directory, and if you're like me you can't be bothered shifting the images into the right place. Thankfully, you can automate this using Automator's suite of services and similar tricks.

I'm shifting my images using a little ruby script, which basically works out the right month-year combination and slots images into the right place. It looks something like this:

require "fileutils"

#This is where you store your images
ROOT_LOCATION = File.join(ENV['HOME'], "blog/content/assets/image/posts")

#This is the subfolder to store them in
yearmonth = Time.now.strftime("%Y-%m")
subfolder = File.join(ROOT_LOCATION, yearmonth)
FileUtils::mkdir(subfolder) unless File.exists?(subfolder)

ARGV.each do |file|
  FileUtils::mv file, subfolder
end

While you could run this from the terminal, it makes much more sense to create a service in Automator that deals with the whole running-the-script thing for you:

Replace the path in this example with your own.

Replace the path in this example with your own.

Save this as a service, and give it an appropriate name (mine is named "→1klb"). Now all you need to do is select your image, right click and hit "→1klb" (or equivalent for your blog), and boom, the image is placed in the right place in your file system. Between this and the !![]() syntax I introduced earlier, you'll be dropping images into your blog posts like nobody's business.


  1. Note: this code will break after the year 9999 ↩ī¸Ž