Adding section links to Hakyll articles with Text.Pandoc.Walk
It is handy to be able to link to section headings in long articles. Pandoc and Hakyll don’t do that out of the box. But they give you all the power you need to implement it yourself. In this post I’ll show you how.
Objective §
The main objective is to provide links (HTML <a>
element) to
section headings. They should be located near or within the heading
element. Having them in the document will make it easy for readers
to grab a link to a specific section of the article. (I myself
often want to do this!)
You could make the whole heading a link, but I like the approach that reveals a link when the pointer hovers over the heading. Some sites use a pilcrow (¶), pound (#) or a link symbol (🔗) as the link text. I will use a section sign (§).
Building blocks §
Pandoc does set the id
attribute of HTML heading elements to a
value derived from the heading text. For example, one of my
previous posts had a section headed Probabilities. The
resulting HTML for the heading is:
<h2 id="probabilities">Probabilities</h2>
The value of the id
attribute will be the href
target of the
<a>
element we create (with #
prepended to make it a URI
fragment).
Hakyll provides the pandocCompilerWithTransform
function for
compiling documents using Pandoc and applying an arbitrary
transformation to them. It has the type:
pandocCompilerWithTransform :: ReaderOptions
-> WriterOptions
-> (Pandoc -> Pandoc)
-> Compiler (Item String)
Note the (Pandoc -> Pandoc)
argument. This is the tranformation
function. It works with the Pandoc
native AST data type,
rather than HTML or the input type (e.g. Markdown).
For constructing such a transformation, Pandoc provides the
Text.Pandoc.Walk
module and the walk
function:
walk :: (Walkable a b) => (a -> a) -> b -> b
walk f x
walks the structure x :: b
(bottom up) and replaces
every occurrence of a value of type a
with the result of applying
f :: (a -> a)
to it.
There are many instances of Walkable
. We are interested in the
one that visits all the Block
elements (that’s what headings are)
in the Pandoc
:
instance Walkable Block Pandoc
Putting it together §
I needed a handful of Pandoc constructors to implement the
transformation. The Header
constructor (of the Block
data type)
represents a document (sub)heading with Int
depth, attributes, and
the list of Inline
elements that constitute the header content.
data Block
...
| Header Int Attr [Inline]
...
I also had to construct a Link
(one of the cases of the Inline
data type). A Link
has attributes, content ([Inline]
) and a
target. I also used the Str
and Space
constructors.
data Inline
= Str Text -- ^ Literal text
...
| Space -- ^ Inter-word space
...
| Link Attr [Inline] Target
By the way, Attr
and Target
are defined as:
-- id, classes and key-value pairs
type Attr = (Text, [Text], [(Text, Text)])
-- URI, title
type Target = (Text, Text)
With these constructors in hand, here is the whole transformation function:
addSectionLinks :: Pandoc -> Pandoc
= walk f where
addSectionLinks Header n attr@(idAttr, _, _) inlines) | n > 1 =
f (let link = Link nullAttr [Str "§"] ("#" <> idAttr, "")
in Header n attr (inlines <> [Space, link])
= x f x
Note that we only apply this change to headings of depth greater
than one. I do not need to provide a link for the article title,
which is at the top of the page. For all other headers, we add a
Link
to its inline content, where the target is the fragment
pointing at the idAddr
of the header itself.
To apply the transformation, I had to replace a single occurrence of:
pandocCompiler :: Compiler (Item String)
with:
pandocCompilerWithTransform
defaultHakyllReaderOptions
defaultHakyllWriterOptions addSectionLinks
Style §
I want to hide the heading link unless the cursor is hovering over the heading. I would like to accomplish it with this small dose of CSS:
:is(h2, h3, h4, h5, h6) a {
text-decoration: none;
color: grey;
visibility: hidden;
}
:is(h2, h3, h4, h5, h6):hover a {
visibility: visible;
}
The is()
pseudo-class function matches anything that matches the
selector arguments, avoiding tedious repetition. It is part of CSS
Selectors Level 4, which is still a draft. Firefox and Safari
fully support it but unfortunately other browsers are lagging
behind. So I am stuck with the tedious repetition for now:
, h3 a, h4 a, h5 a, h6 a {
h2 atext-decoration: none;
color: grey;
visibility: hidden;
}
:hover a, h3:hover a, h4:hover a, h5:hover a, h6:hover a {
h2visibility: visible;
}
I also used text-decoration
and color
to make the link
appearance clean and understated.
Conclusion §
As a result of this change, the HTML emitted for section headers (of depth > 1) looks like:
<h2 id="probabilities">
<a href="#probabilities">§</a>
Probabilities </h2>
You can experience the results for yourself, right here on this page (and in my other posts). You can also view the commit that implements this feature.
There’s not much else to say, really. Pandoc is still awesome. Hakyll is still awesome. And I am very happy with the results of this little enhancement. Go forth and pilcrow-ise your Hakyll sites!