hakyll-diagrams
Contents
Overview
Compiles any diagrams code found within markdown source files
and inserts the resulting figures into the generated HTML output. The figures can be embedded
as inline SVG code or referenced via external image files using <img> tags.
For example, when a block like this is found in a markdown file:
``` diagram { svg:width=300 }
let
target = mconcat
[ circle 1 # lw 0 # fc red
, circle 2 # lw 0 # fc white
, circle 3 # lw 0 # fc red
]
background = roundedRect 8 8 0.1
# lw 0
# fc (sRGB24read "#808080")
# opacity 0.15
in target <> background
```
That code will be compiled, and replaced by the rendered SVG figure:

If we add a few attributes to the rendered SVG figure:
``` diagram {
figcaption="An interactive figure. It reacts to mouse hover and each part is a link."
svg:width=400
}
let
hydrogen =
mconcat
[ proton
, electron # moveTo p0
, orbit
]
where
orbit =
circle 2
# svgAttr "pointer-events" "stroke"
# href "https://en.wikipedia.org/wiki/Atomic_orbital"
# svgClass "svg_orbit"
electron =
circle 0.1
# fc blue
# lw 0
# href "https://en.wikipedia.org/wiki/Electron"
# svgClass "svg_electron"
proton =
circle 0.3
# fc red
# lw 0
# href "https://en.wikipedia.org/wiki/Proton"
# svgClass "svg_proton"
p0 = fromJust $ maxTraceP origin (angleV $ 3/8 @@ turn) orbit
background =
roundedRect 5 5 0.1
# lw 0
# fc (sRGB24read "#808080")
# opacity 0.15
in hydrogen <> background
```
We will generate this figure:

And with the help of some additional CSS or JavaScript code, we can make it an interactive
figure. That will not be possible to be shown here, because GitHub has restrictions on
adding inline SVG and JavaScript code to the Markdown files, however that same figure with
interactive features is available
here.
Usage
In a source file, any Haskell code within a code block having diagram as one of its
classes will be rendered to a diagram figure. In a markdown file, it means any code block
like this:
``` diagram {
<options>
}
<code>
```
Where <options> is a list of library and HTML tag attributes, fully described in the
Options section, and <code> must consist of a single Haskell expression
whose resulting value has type Diagram SVG.
Code
The code block must contain a single Haskell expression of type
Diagram SVG.
The interpreter environment is configurable via the Options record, which serves as an
argument to the drawDiagramsWith function. All exports from modules listed in
globalModules and localModules will be available in the context.
Options
The options configure what the diagram rendering will produce as results, and specify any
additional attributes for the resulting HTML and SVG elements.
A valid options list must be in the format:
options := {option}
option := id | class | figcaption | attribute
id := "#" name
class := "." name
figcaption := "figcaption=" value
attritube := tag ":" name "=" value
tag := "figure" | "img" | "svg"
name := <any valid HTML attribute name>
value := <any valid HTML attribute value>
An input like this:
``` diagram { img:src="external_figure.svg" figcaption="foo"
.my_class_a .my_class_b #my_id
img:alt="a circle" figure:myattr=foo img:width=10
svg:class="my_svg_class" svg:width=50
}
circle 10
```
Will generate an external SVG file named external_figure.svg, written under the Hakyll's
configured
destinationDirectory.
The <svg> tag will have the following attributes:
<svg width="50.0000" class="my_svg_class" ... >
Besides generating an external file, that input will be replaced in the resulting HTML
file by a code like:
<figure id="my_id" class="my_class_a my_class_b" data-myattr="foo">
<img src="external_figure.svg" alt="a circle" width="10" />
<figcaption>foo</figcaption>
</figure>
Regardless of any options, a <svg> element will always be generated. If we have set an
img:src="path" attribute, the rendered SVG will be written to an external file at
path, and an <img> element will link to it. If we don't have an img:src="path"
attribute, the rendered SVG will be inlined in the HTML code.
If we have set a figcaption="caption" attribute, the <svg> or <img> element will be
nested in a <figure> element. Otherwise they will be inserted without a parent.
The attributes must be in the format: tag:name=value, and tag must be one of:
figure, img, and svg. This means that the pair name=value will be assigned to the
tag element. If that element would not be generated, such as when we don't have an
<img> tag because of an inlined SVG, or if the tag has any other value than those valid
three, it will be ignored without an error.
The #id and .class values will be assigned to the outermost element, whether it is
<figure>, <img>, or <svg>.
We can use the Hakyll's metadata
block to create
an Options record to be used as argument to the drawDiagramsWith functions that
will render that page diagrams.
This works starting from a base Options record, whose fields will be modified by the
values at the metadata block. Each one of the record fields can be assigned a value
through a metadata key named the same as that field prefixed with dg..
- Each field must be formatted as a list where each element is separated by a comma
,.
- Any occurrence of an ellipsis
... will be replaced by the current value of that field
as in the base Options.
- When setting either
globalModules or localModules, a value like Utils will
result in an unqualified import of Utils module. A value like Commons as Cm will
result in a qualified import of Commons module under Cm name.
- Any field not present will retain the same value as in the base
Options. An empty
string "" will set the field to an empty list.
When starting with base:
Options
{ globalModules =
[ ("Prelude", Nothing)
, ("Diagrams.Prelude", Nothing)
, ("Diagrams.Backend.SVG", Nothing)
]
, localModules = []
, searchPaths = ["app", "posts"]
, languageExtensions = ["NoMonomorphismRestriction"]
}
the metadata block:
---
dg.localModules: Utils, Commons as Cm
dg.searchPaths: lib, ..., foo
dg.languageExtensions: ""
---
will result in:
Options
{ globalModules =
[ ("Prelude", Nothing)
, ("Diagrams.Prelude", Nothing)
, ("Diagrams.Backend.SVG", Nothing)
]
, localModules = [("Utils", Nothing), ("Commons", Just "Cm")]
, searchPaths = ["lib", "app", "posts", "foo"]
, languageExtensions = []
}
Haskell API
All the API documentation is available in the Haddock generated project
page.
The main functions are drawDiagrams and drawDiagramsWith. Both functions accept a
Pandoc
data structure and transform it by replacing
CodeBlock's
having a diagram class with their rendered diagram figures, as explained in the Input
format section.
The simplest way to integrate these functions into a Hakyll's
rule is
with code like this:
match "posts/*.md" $ do
route $ setExtension "html"
compile $
pandocCompilerWithTransformM
defaultHakyllReaderOptions
defaultHakyllWriterOptions
drawDiagrams
We can use either readOptionsFromMetadata or readOptionsFromMetadataWith function
to control at the input source level what Options will be used to configure the
interpreter environment when rendering the diagrams on that page. The Metadata
options section explains the metadata block syntax.
One simple way to add this metadata reading functionality to the example code above is to
replace the drawDiagrams function by a drawDiagrams' defined as:
drawDiagrams' :: Pandoc -> Compiler Pandoc
drawDiagrams' pandoc = readOptionsFromMetadataWith opts >>= flip drawDiagramsWith pandoc
where
opts = defaultOptions { searchPaths = ["app", "posts"] }
Package databases
The hint library is used to
interpret the Haskell code, and therefore all of its
limitations will
apply here as well. In particular, if there isn't a .ghc.environment.\<something>
package environment
file
(as generated by cabal
--write-ghc-environment-files=always
option), setting either HAKYLL_DIAGRAMS_PACKAGE_ENV (a single path) or
HAKYLL_DIAGRAMS_PACKAGE_DB (N comma-separated paths) environment variables may be
required. If these environment variables are defined, their values will be used to set the
GHC
-package-env
and
-package-db
options respectively.
Similar projects
There are two diagrams projects,
diagrams-pandoc and
diagrams-builder, that implement
a portion of the functionality of this hakyll-diagrams library. They are not used
as dependencies because they would not allow some useful features. For example, with
diagrams-builder it is not possible to import a local module qualified, and it has
some potential issues
when combining them. With diagrams-pandoc we cannot add arbitrary attributes to a
specific HTML element, or generate an inlined SVG instead of an externally linked file.
There is also a homonymous hakyll-diagrams
library that integrates the two diagrams libraries with hakyll, running them inside
the
Compiler
monad. Since it uses those two diagrams libraries, it has the same limitations
discussed above. Because of this, I have decided to create a new hakyll-diagrams library
with all the desired features.