“A nearly impenetrable thicket of geekitude…”

Nanoc Filters as Markdown Extensions

Posted on April 27, 2018 at 08:34

In one of the static web site projects I have been working on, the main text is composed using Markdown but a number of common constructs are used which Markdown can’t easily express. That’s not Markdown’s fault in any sense; I’m using it well outside its originally intended scope.

Here’s how I made things a bit simpler by using a Nanoc filter as a pseudo-extension for Markdown.

One of Nanoc’s great strengths is its flexible configuration, which is a domain-specific language based on Ruby. Here’s the configuration used for Markdown sources in the project in question:

compile '/**/*.md' do
  filter :admonitions
  filter :kramdown, input: 'GFM', enable_coderay: false,
                    hard_wrap: false
  filter :colorize_syntax, default_colorizer: :rouge
  layout '/default.*'
  filter :relativize_paths, type: :html
end

All of the above is stock Nanoc configuration except for the filter :admonitions at the top, which invokes my “admonitions” filter. The three named filters (:admonitions for the “extension”, :kramdown to perform Markdown to HTML processing and :colorize_syntax to decorate code blocks) are processed as a pipeline, so the output of my filter needs to be acceptable kramdown-flavour Markdown input.

Here’s the complete source of admonitions.rb:

# frozen_string_literal: true

#
# :admonitions filter
#
# Replaces this:
#
#    %foo{whatever}
#
# With a "foo" admonition. The content can be spread across multiple lines.
#
Nanoc::Filter.define(:admonitions) do |content, _params|
  content.gsub(/^%([a-z]+)\{([^\}]+)\}/m) do |_match|
    m = Regexp.last_match
    stripped = m[2].strip
    "<div class=\"admonition-wrapper admonition-#{m[1]}\">\n" \
      "<div class=\"admonition\" markdown=\"1\">\n" \
      "#{stripped}\n</div>\n</div>\n"
  end
end

For each occurrence of a given regular expression, this substitutes a more complex result. In this case, the subsitution relies on kramdown’s ability to process embedded HTML containing nested Markdown content to good effect.

An example of the sort of input this handles might be as follows:

This is a paragraph of normal _Markdown_ text.

%foo{
Here we have some more **Markdown**.

A second paragraph of Markdown input within the `foo` body.
}

The :admonitions filter would turn that into the following:

This is a paragraph of normal _Markdown_ text.

<div class="admonition-wrapper admonition-foo">
<div class="admonition" markdown="1">
Here we have some more **Markdown**.

A second paragraph of Markdown input within the `foo` body.
</div>
</div>

After being processed by kramdown, the result might be something like this:

<p>This is a paragraph of normal <em>Markdown</em> text.</p>

<div class="admonition-wrapper admonition-foo">
<div class="admonition">
<p>Here we have some more <strong>Markdown</strong>.</p>

<p>A second paragraph of Markdown input within the <code>foo</code> body.</p>
</div>
</div>

Note that the markdown="1" attribute is the signal to kramdown to interpret the body of the <div> as Markdown as opposed to HTML, and does not appear in the final output.

When rendered with appropriate CSS, the results look like this (this example is from a development version of the site):

example admonitions

Nanoc has a lot of built-in filters, and I am sure that I could have got the same kind of effect using one of the others — :mustache, perhaps. In this case, though, Nanoc made it really easy to get exactly what I wanted with the minimum of fuss.

Tags: