Nanoc Filters as Markdown Extensions
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):
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.