Drawing a Blank
by Mike Fulcher

Better indentation for injected Jekyll content
Wednesday, July 31, 2013

As much as I love Jekyll, I do have one (minor) gripe: my carefully indented code goes all to hell when the site is compiled. It’s an understandable issue, but I started wondering if it would be easy enough to apply indentation so that the compiled code comes out clean.

Plugins

The solution entails adding a custom plugin, which means that sadly this technique won’t work if your Jekyll site is hosted on GitHub Pages, or if for other reasons you’ve configured Jekyll to use safe mode.

There are two types of content that I want to fix the indentation for: variables which are being printed using double curly-braces (eg {{ content }}) and other files which are being included using the built-in include tag ({% include header.html %}). Tackling each of these requires a slightly different approach.

Printed variables

My approach to deal with printed variables is to add a custom filter which can set the desired level of indentation, effectivey changing this:

{{ content }}

To this:

{{ content | indent:4 }}

Jekyll’s documentation has a section dedicated to custom plugins, so starting from the example filter I put together the following:

module Jekyll
  module IndentFilter
    def indent(content, indent=0)
      output = []
      indentation = ' ' * indent.to_i
      first_line = true
      content.each_line do |line|
        if first_line
          output << line
          first_line = false
        else
          output << (indentation + line)
        end
      end
      output.join('')
    end
  end
end

Liquid::Template.register_filter(Jekyll::IndentFilter)

Hopefully that’s fairly self-explanatory: the filter receives some content with an (optional) argument which is the number of desired indentation spaces. The filter then iterates over the content line by line, building a copy (output) with the indentation prepended to each line. Note that the filter doesn’t apply the intentation to the first line: it assumes that you’ve placed the liquid tag in an area of the markup with indentation already applied.

Included files

So far so good, so what about includes? My intention was to be able to change from this:

{% include header.html %}

To this:

{% indent_include header.html 4 %}

Jekyll’s documentation covers custom tags too, so using that as a base I came up with this:

module Jekyll
  class IndentIncludeTag < Liquid::Tag
    include IndentFilter

    def initialize(tag_name, tag_data, tokens)
      super
      @tokens = tokens
      @file, @indent, *tag_data = tag_data.split(' ')
      @tag_data = tag_data.unshift(@file).join(' ')
      @indent ||= 0
    end

    def render(context)
      # Use the standard include tag to get the file contents.
      content = Jekyll::Tags::IncludeTag.new('include', @tag_data, @tokens).render(context)
      # Apply the indent filter (above).
      indent(content, @indent)
    end
  end
end

Liquid::Template.register_tag('indent_include', Jekyll::IndentIncludeTag)

This one required a little more code, but it’s still fairly straightforward; first the file name and indentation level are extracted from the tag, then Jekyll’s built-in include tag is used to get the contents of the included file which is passed to the new indent_include filter from earlier.

Final code

# _plugins/indentation.rb

module Jekyll
  module IndentFilter
    def indent(content, indent=0)
      output = []
      indentation = ' ' * indent.to_i
      first_line = true
      content.each_line do |line|
        if first_line
          output << line
          first_line = false
        else
          output << (indentation + line)
        end
      end
      output.join('')
    end
  end

  class IndentIncludeTag < Liquid::Tag
    include IndentFilter

    def initialize(tag_name, tag_data, tokens)
      super
      @tokens = tokens
      @file, @indent, *tag_data = tag_data.split(' ')
      @tag_data = tag_data.unshift(@file).join(' ')
      @indent ||= 0
    end

    def render(context)
      # Use the standard include tag to get the file contents.
      content = Jekyll::Tags::IncludeTag.new('include', @tag_data, @tokens).render(context)
      # Apply the indent filter (above).
      indent(content, @indent)
    end
  end
end

Liquid::Template.register_filter(Jekyll::IndentFilter)
Liquid::Template.register_tag('indent_include', Jekyll::IndentIncludeTag)

Caveats

I haven’t thoroughly tested this technique for all scenarios, but one which has already shown some issues is when using indentation on content with code blocks; <pre> and <code> tags will render the indentation along with the code, so that’s not good. I’ll investigate ways to tackle that another day; for now, this still serves it’s purpose whenever I’m not rendering any code blocks.

There’s one other downside, too: you must pass in the indentation level manually. Personally, I don’t mind too much – I care about nicely formatted code, so if this means manually specifying the indentation level then I’m fine with that. Perhaps another day I’ll look into potential ways of inferring the current indentation level.

Happy indenting!