Graphviz plugin for Jekyll
Problem statement
I’m rather fond of graphviz for my everyday graphing needs.
But I’m also tired of re-using the clunky old Makefile:
all: $(shell find . -iname \*.dot |sed 's/dot$\/svg/')
watch:
onfilechange \*.dot 'make'
.SUFFIXES: .dot .svg
.dot.svg:
dot -Tsvg -o $@ $<
Because in order to have a reasonable refresh-after-save when developing,
I have to switch to the assets dir, and run the make watch
. Not the end
of the world, but doesn’t spark joy either.
Desired end-state
Therefore, I would like to have a Jekyll plugin1. One that parses a Liquid
block and spits out the SVG as inline img
tag (with an optional title).
Example:
{% graphviz_captioned some title %}
digraph G {
node [shape=box]
edge [color = "#aaaaaa"]
overlap = "false"
nodesep = 1
sda2 [label="sda2", color="#ffd44f"]
sdb2 [label="sdb2", color="#ffd44f"]
md1 [label="mdraid mirror (md1)", color="#ffd44f"]
sda2, sdb2 -> md1
}
{% endgraphviz_captioned %}
should produce:
some title
and errors should be clearly visible (in page).
And while one could make a case that I’m suffering from NIH, I think the few lines of Ruby don’t justify the additional dependency.
Not to mention that the support for the image captions (see the example above) isn’t there.
Solution
The solution is straightforward, one file in _plugins
directory
does the trick:
# frozen_string_literal: true
=begin
Allows to use the following in templates:
{% graphviz some title %}
digraph G {
node [shape=box]
edge [color = "#aaaaaa"]
overlap = "false"
nodesep = 1
sda2 [label="sda2", color="#ffd44f"]
sdb2 [label="sdb2", color="#ffd44f"]
md1 [label="mdraid mirror (md1)", color="#ffd44f"]
sda2, sdb2 -> md1
}
{% endgraphviz %}
Which will generate the graph using `dot` command,
and output the result as `img` tag with proper title.
Alternatively, you can leave out the title out,
in which case it will be set to `inlined graphviz graph`.
And if you use `graphviz_captioned` block instead of `graphviz`,
the title will also end up as a caption below the image.
=end
require "jekyll"
require 'open3'
require 'cgi'
require 'base64'
require 'digest/sha2'
require 'fileutils'
require 'tempfile'
$LOAD_PATH.unshift(File.dirname(__FILE__))
module Jekyll
class RenderGraphvizBlock < Liquid::Block
CACHE = '_gvcache'
def initialize(tag_name, text, tokens)
super
@caption = !!(tag_name =~ /captioned/)
@title = text && text.strip
end
def gen_svg(input)
FileUtils.mkdir_p(CACHE)
sha = Digest::SHA256.hexdigest(input)
cache = File.join(CACHE, sha)
if FileTest.exists?(cache)
output = File.read(cache)
return [output, nil]
end
output, status = Open3.capture2e("dot -Tsvg", stdin_data: input, binmode: true)
output.force_encoding 'UTF-8'
if status.success?
# cache it
t = Tempfile.new(cache)
t.write(output)
t.close
File.rename(t.path, cache)
[output, nil]
else
[nil, output]
end
end
def render(context)
text = super
output, error = gen_svg(text.strip)
if error
"<pre><code>" + CGI.escapeHTML(error) + "</code></pre>"
else
title = @title || "inlined graphviz graph"
b64 = Base64.strict_encode64(output)
out = []
out << "<p>"
out << "<img alt=\"#{CGI.escapeHTML(title)}\" "
out << "src=\"data:image/svg+xml;base64,#{b64}\" />"
if @caption
out << "\n<em>#{CGI.escapeHTML(title)}</em>"
end
out << "</p>"
out.join
end
end
end
end
Liquid::Template.register_tag('graphviz', Jekyll::RenderGraphvizBlock)
Liquid::Template.register_tag('graphviz_captioned', Jekyll::RenderGraphvizBlock)
Closing words
To eat my own dog food, I’ve converted the last post to use this plugin.
Works as expected. Yay.
-
The same way I have a plugin for the stl viewer, but I haven’t talked about that at all, have I? ↩