I have been very satisfied with Jekyll. It is very comfortable for me to write my blog posts in Markdown, and the maintenance of a static site generator based project needs a lot less time than anything else. But when I was writing my post about Claro’s screenshot automation I found a very interesting challenge: the table I put into the article caused vertical overflow on narrow screen width devices.

The solution for this kind of issue is pretty easy: we only have to wrap the table into a (block level) HTML element; let the wrapper element inherit the available width but make it overflow vertically. But how can I achieve this with Jekyll?

I was sure I am not the only person having this issue. But people at this issue were suggesting to add the needed HTML markup to the Markdown document – what I don’t really like to do: On one hand, I want to keep my Markdown files as “clean” as possible. On the other hand, it is cleaner and much more maintainable to process the HTML output at a very late phase somehow, and then automatically add the wrapper HTML tag where it is needed.

Luckily, I found some good news: Jekyll provides a helpful infrastructure called “Hooks” that we can use for this! We only have to implement a Jekyll plugin, register its (static) method as a hook, which then gets invoked on the desired event and can modify the generated output.

The simplest and easiest approach imho is to create a file in the <project root>/_plugins directory named table-wrapper.rb. This file contains the class of the table wrapper Jekyll plugin, and also registers the appropriate method:

1
2
3
4
5
6
7
8
9
10
11
12
require 'nokogiri';

module Jekyll
  class TableWrapper
    def self.process(document)
    end
  end
end

Jekyll::Hooks.register [:pages, :documents], :post_render do |document|
  Jekyll::TableWrapper.process(document) if document.write?
end

To parse and modify the actual file’s output, I chose Nokogiri, mostly because it seems to be pretty common in the Ruby community. My strategy is pretty simple: if the current output contains a <table>, I wrap it into a <div class="vertical-scroll-wrapper"> HTML element – unless the table’s parent is an overflow container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'nokogiri';

module Jekyll
  class TableWrapper
    def self.process(document)
      wrapper_tag = 'div';
      wrapper_css_class = 'vertical-scroll-wrapper';
      wrapper = "<#{wrapper_tag} class=\"#{wrapper_css_class}\"></#{wrapper_tag}>"

      xpath_selector = '//body//table'
      xpath_selector << "[not(parent::#{wrapper_tag}[contains(concat(' ', normalize-space(@class), ' '), ' #{wrapper_css_class} ')])]"

      parsed_document = Nokogiri::HTML(document.output)
      parsed_document.search(xpath_selector).each do |table_node|
        table_node.wrap(wrapper)
      end

      document.output = parsed_document.to_html
    end
  end
end

Jekyll::Hooks.register [:pages, :documents], :post_render do |output|
  Jekyll::TableWrapper.process(output) if document.write?
end

The version above worked well in case of HTML files… But it turned out that in Jekyll, :documents also means CSS files! So every time I’ve called to_html on the HTML-parsed version of my CSS files, Nokogiri prepended <!DOCTYPE html> to them.

At this point I’ve stopped for a minute. I was pretty sure that the Jekyll Target Blank plugin is also using Jekyll’s plugin system. I checked how it solves this situation – well, it also checks the extension of the generated file!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'nokogiri';

module Jekyll
  class TableWrapper
    def self.process(document)
      wrapper_tag = 'div';
      wrapper_css_class = 'vertical-scroll-wrapper';
      wrapper = "<#{wrapper_tag} class=\"#{wrapper_css_class}\"></#{wrapper_tag}>"

      xpath_selector = '//body//table'
      xpath_selector << "[not(parent::#{wrapper_tag}[contains(concat(' ', normalize-space(@class), ' '), ' #{wrapper_css_class} ')])]"

      parsed_document = Nokogiri::HTML(document.output)
      parsed_document.search(xpath_selector).each do |table_node|
        table_node.wrap(wrapper)
      end

      document.output = parsed_document.to_html
    end
  end
end

Jekyll::Hooks.register [:pages, :documents], :post_render do |output|
  Jekyll::TableWrapper.process(output) if document.write? and document.output_ext == '.html'
end

The plugin above is “almost” perfect. The missing pieces were:

  1. Early return for documents which do not contain <table> elements at all.
  2. Exclude those tables which are added by the default Kramdown code highlighter when it is configured to show code line numbers.

This is the “perfect” plugin file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
require 'nokogiri';

module Jekyll
  class TableWrapper
    def self.process(document)
      # Early return if no tables are present.
      if document.output.index(/<table\b/).nil?
        return
      end

      wrapper_tag = 'div';
      wrapper_css_class = 'vertical-scroll-wrapper';
      wrapper = "<#{wrapper_tag} class=\"#{wrapper_css_class}\"></#{wrapper_tag}>"

      xpath_selector = '//body//table'
      xpath_selector << "[not(parent::#{wrapper_tag}[contains(concat(' ', normalize-space(@class), ' '), ' #{wrapper_css_class} ')])]"
      # Exclude tables added to syntax highlighted code blocks.
      xpath_selector << '[not(ancestor::pre)][not(ancestor::code)]'

      parsed_document = Nokogiri::HTML(document.output)
      parsed_document.search(xpath_selector).each do |table_node|
        table_node.wrap(wrapper)
      end

      document.output = parsed_document.to_html
    end
  end
end

Jekyll::Hooks.register [:pages, :documents], :post_render do |document|
  Jekyll::TableWrapper.process(document) if document.write? and document.output_ext == '.html'
end

The CSS which makes my tables vertically scrollable is pretty obvious:

1
2
3
4
.vertical-scroll-wrapper {
  overflow: hidden;
  overflow-x: auto;
}

And finally, the end result:

A photo about a mobile screen showing a table with a horizontal scroll bar on the bottom. The end result of the plugin. Note the light vertical scroll bar on the bottom of the table.

Happy coding!