Multi-target publishing with Markdown

I have for a long time been a fan of writing everything as plain text. This started with me using LaTeX, which I still do when I want a perfectly formatted result. However, the last few years I have taken to using some of the many Markdown variants available. This has several advantages: First and foremost, the source is perfectly readable as-is, which makes it ideal for readme files and the like. And you can generate multiple output formats, depending of course on what markdown variant you use.

I initially started using Asciidoc, which is really good, and can generate output in HTML, PDF, ODF, and several different slideshow formats. However, it is not based on standard Markdown, but uses it’s own slightly different syntax. This is annoying if you sometimes also need to use normal Markdown. Recently I have switched to Pandoc, which is based on MultiMarkdown syntax with some extensions. It can generate oodles of different output formats, several of them text based, which for instance makes it easy to write MediaWiki pages. I always have problems remembering MediaWiki’s fiddly syntax, and having to write things like '''''bla''''' just makes me cringe.

Each format can take many configuration parameters. If I want multiple output targets, I usually use a make file, something like this:

SOURCE = example_file.md
HTML = $(SOURCE:.md=.html)
PDF = $(SOURCE:.md=.pdf)
WIKI = $(SOURCE:.md=.wiki)
all: $(HTML) $(PDF) $(WIKI)
clean:
rm -f *.aux *.tex *.out *.log *.html *.pdf *.wiki
$(HTML): $(SOURCE)
pandoc -NSs $(SOURCE) -o $(HTML)
$(PDF): $(SOURCE)
pandoc -NSs --variable=fontsize:11pt \
--variable=geometry:a4paper,margin=2cm \
$(SOURCE) -o $(PDF)
$(WIKI): $(SOURCE)
pandoc -S $(SOURCE) -t mediawiki -o $(WIKI)

Another format supported by Pandoc is Json, which can be used for both output and input. The Json structure essentially contains the internal parse tree of the document, which makes it useful for custom filters and such. Pandoc is written in Haskell, and as a good opportunity to write my very first Haskell code snippet, I thought I’d try to make a filter for Takeshi Komiya’s neat Blockdiag. Here it is:

-- Pandoc filter for Takeshi Komiya's Blockdiag (http://blockdiag.com)
--
-- Put your blockdiag source inside a code block and tag it with one of
-- the available diagram classes. You can pass extra options to the processor
-- by using an "options" attribute. If you want, you can set the name of the
-- generated image with the "name" attribute, else the name will be auto
-- generated. The images are saved in an "img" subdirectory which must exist.
--
-- Example:
--
-- ~~~~ {.blockdiag}
-- blockdiag {
-- A -> B -> C -> D;
-- A -> E -> F -> G;
-- }
-- ~~~~
import Text.Pandoc
import Data.ByteString.Lazy.UTF8 (fromString)
import Data.Digest.Pure.SHA (sha1, showDigest)
import Data.List
import System.Directory(removeFile)
import System.Environment(getArgs)
import System.Exit(ExitCode(ExitSuccess))
import System.FilePath ((</>))
import System.IO
import System.Process(readProcessWithExitCode)
-- Available diagram classes, mapping directly to an executable.
diagrams = ["actdiag", "blockdiag", "nwdiag", "packetdiag", "rackdiag", "seqdiag"]
handleBlockdiag :: String -> Block -> IO Block
handleBlockdiag format (CodeBlock (_, classes, namevals) contents)
| any (`elem` classes) diagrams = do
let
diag =
case find (`elem` classes) diagrams of
Just diag -> diag
Nothing -> error $ "blockdiag called with bad type. (This can't happen)"
options =
case lookup "options" namevals of
Just opts -> opts
Nothing -> ""
imagesuffix =
case format of
-- Supposed to support eps too, but has unresolved dependencies
"svg" -> ".svg"
_ -> ".png"
(name, outfile) =
case lookup "name" namevals of
Just name -> ([Str name], "img" </> name ++ imagesuffix)
Nothing -> ([], "img" </> uniqueName contents ++ imagesuffix)
(tempfile, temphd) <- openTempFile "." "diag"
hPutStr temphd contents
hClose temphd
(ec, _out, err) <- readProcessWithExitCode
diag
(["-T", format, "-o", outfile] ++ words options ++ [tempfile])
""
removeFile tempfile
if ec == ExitSuccess
then
return $ Para [Image name (outfile, "")]
else
error $ "blockdiag returned an error status: " ++ err
handleBlockdiag _ x = return x
main :: IO ()
main = do
args <- getArgs
toJsonFilter
$ handleBlockdiag
$ case args of
[f] -> f
_ -> "" -- default
uniqueName :: String -> String
uniqueName = showDigest . sha1 . fromString

You just put your diagram source in a standard Pandoc code block, tagged like so:

~~~~ {.blockdiag} 
blockdiag { 
  // branching edges to multiple children 
  A -> B, C;

  // branching edges from multiple parents
  D, E -> F;
}
~~~~

You then call it like this:

pandoc -t json test.md | handleBlockdiag png | pandoc -f json -o test.pdf

And get this result. Works great!