I’ve made a minimal working example of this code which you may find instructive.
I recently needed to put commuting diagrams in my posts, and the easiest way of doing this, is with TikZ. However, unlike with plain \(\LaTeX\) equations, there’s no built in support in Pandoc for embedding TikZ in markdown. Hakyll doesn’t have support for this either. There is the package latex-formulae-hakyll
, which does a pretty good job, but for various reasons is not exactly what I wanted. There are several other similar tools scattered around github, including cats, but they’re all bloated and don’t work very well. So I decided to make my own. It was a nightmare of a project, but mostly that’s because I didn’t know what I was doing; the end result is small, clean, and pretty.
site.hs
At it’s core, the solution is a single function, which I’ll explain how to use and then how it works. In site.hs
, add the following function.
tikzFilter :: Block -> Compiler Block
tikzFilter (CodeBlock (id, "tikzpicture":extraClasses, namevals) contents) =
(imageBlock . ("data:image/svg+xml;utf8," ++) . URI.encode . filter (/= '\n') . itemBody <$>) $
makeItem contents
>>= loadAndApplyTemplate (fromFilePath "templates/tikz.tex") (bodyField "body")
>>= withItemBody (return . pack
>=> unixFilterLBS "rubber-pipe" ["--pdf"]
>=> unixFilterLBS "pdftocairo" ["-svg", "-", "-"]
>=> return . unpack)
where imageBlock fname = Para [Image (id, "tikzpicture":extraClasses, namevals) [] (fname, "")]
tikzFilter x = return x
First of all, you’re going to need rubber-pipe
and pdftocairo
installed on your system. The former comes from the rubber
package, and the latter from poppler_utils
.
Next you’ll need a few Haskell imports at the top of site.hs
.
import Text.Pandoc.Definition
import Text.Pandoc.Walk (walk, walkM)
import Control.Monad ((>=>))
import Hakyll
import Hakyll.Core.Compiler
import Data.ByteString.Lazy.Char8 (pack, unpack)
import qualified Network.URI.Encode as URI (encode)
Next you’ll want to declare an alias for your pandoc compiler.
=
myPandocCompiler
pandocCompilerWithTransformM pandocReadOpts pandocWriteOpts $ walkM tikzFilter
You’ll need to define pandocReadOpts
and pandocWriteOpts
for yourself; I don’t know what works for you. You can always use defaultHakyllReaderOptions
and defaultHakyllWriterOptions
from Hakyll.
Now you can use myPandocCompiler
in the same way you would use pandocCompiler
from Hakyll. I’m not going to go into that usage here, check out the hakyll tutorials for the details.
templates/tikz.tex
You’ll need to write a template in templates/tikz.tex
. I have
\documentclass{article}
\usepackage[pdftex,active,tightpage]{preview}
\usepackage{amsmath}
\usepackage{tikz}
\usetikzlibrary{matrix}
\usepackage{xcolor}
\definecolor{fg}{HTML}{839496}
\begin{document}
\color{fg}
\begin{preview}
\begin{tikzpicture}[auto]
$body$
\end{tikzpicture}
\end{preview}
\end{document}
Which sets up a standalone tikz environment and sets the color to the foreground color on my website.
css/default.css
The tikz diagrams will be left with the class .tikzpicture
, so you can set up a css rule for that class, for example I center it and give it a height of 200px
. The height is important because the resulting image is an svg which won’t have a standard size.
some/page.md
So now you have some page being compiled with myPandocCompiler
, how do you insert a tikz diagram? It’s simple, write a code block with type tikzpicture
, as follows:
```tikzpicture
\node (X) {$X$};
\node (Y) [below of=X, left of=X] {$Y$};
\node (Y') [below of=X, right of=X] {$Y^\prime$};
\draw[->] (Y) to node {$i$} (X);
\draw[->] (Y') to node [swap] {$i^\prime$} (X);
\draw[transform canvas={yshift=0.5ex}, ->] (Y) to node {$\alpha$} (Y');
\draw[transform canvas={yshift=-0.5ex}, ->] (Y') to node {$\alpha^{-1}$} (Y);
```
And it will be replaced with the output:
If you want to include tikzpicture options, you can put the code in a scope
environment as follows:
```{.tikzpicture style="height: 100px;"}
\begin{scope}[node distance=1.5cm]
\node (X) {$X$};
\node (Y) [below of=X, left of=X] {$Y$};
\node (Y') [below of=X, right of=X] {$Y^\prime$};
\draw[->] (Y) to node {$i$} (X);
\draw[->] (Y') to node [swap] {$i^\prime$} (X);
\draw[transform canvas={yshift=0.5ex}, ->] (Y) to node {$\alpha$} (Y');
\draw[transform canvas={yshift=-0.5ex}, ->] (Y') to node {$\alpha^{-1}$} (Y);
\end{scope}
```
As demonstrated in the above example, you can add other attributes to the tikzpictures and they will apply to the resulting image.
So lets go back and understand what’s happening. Pandoc transforms markdown to html by first passing through an internal Pandoc representation. A Pandoc filter is a function on the Pandoc representation that gets applied in between reading the html and writing the html, and that’s where our function operates.
There are two strange things about the type signature of our function
tikzFilter :: Block -> Compiler Block
We would expect the function to be of type Pandoc -> Pandoc
, so why is it acting on Block
s, and why is it returning something wrapped in the Compiler
monad?
Pandoc has a handy function walk
, which uses typeclass magic to allow us to specify a function that acts on a particular substructure (in this case Block
) of the Pandoc data, and then generalize it to a function on the whole Pandoc structure which just applies it to every instance of that substructure within it.
walkM
and pandocCompilerWithTransformM
allow us to specify such an action monadically. This is helpful because we want to compile the tikz sections before we know what pandoc to output.
Now we get to the function declaration. It has two parts; if we receive a code block with the class “tikzpicture”, which corresponds to blocks starting ```tikzpicture
, we will change it into an image. Otherwise, we will leave it untouched.
CodeBlock (id, "tikzpicture":extraClasses, namevals) contents) =
tikzFilter (...
= return x tikzFilter x
The rest of the code performs this substitution. The key here is that images can be placed inline into markdown and html using data URIs. There is no need to generate a separate image file. The first line expects to get the svg image data in the body of a Hakyll Item
, wrapped in the Compiler
monad. It strips newlines, and encodes the text so that it can be safely placed into an html URI, it adds the header that specifies it as a data URI, and it wraps it in a Pandoc image block, repackaging all of the extra html tags that were attached to the code block.
. ("data:image/svg+xml;utf8," ++) . URI.encode . filter (/= '\n') . itemBody <$>) $
(imageBlock ...
where imageBlock fname = Para [Image (id, "tikzpicture":extraClasses, namevals) [] (fname, "")]
So we make an Item
with the tikz code as it’s body,
makeItem contents
and then we wrap it in our latex template
>>= loadAndApplyTemplate (fromFilePath "templates/tikz.tex") (bodyField "body")
and we take the item body, and pack the data into a ByteString
, and pass it through rubber-pipe
to make a pdf output stream, and pass it through pdftocairo
to turn that pdf into an svg, then we unpack it back into text.
>>= withItemBody (return . pack
>=> unixFilterLBS "rubber-pipe" ["--pdf"]
>=> unixFilterLBS "pdftocairo" ["-svg", "-", "-"]
>=> return . unpack)