Scrollable Tables
in Jekyll Posts
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:
- Early return for documents which do not contain
<table>
elements at all. - 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:
The end result of the plugin. Note the light vertical scroll bar on the bottom of the table.
Happy coding!