Convert Pandoc Markdown-style footnotes into sidenotes
This is a simple Pandoc filter to convert footnotes into a format that can be consumed by Tufte CSS and Pandoc Markdown CSS Theme.
This project provides two, equivalent filters:
-
pandoc-sidenote.lua, a Lua filter, which is run invoked internal to the pandoc binary. See Lua filters in the Pandoc docs for more.This is the preferred way to use
pandoc-sidenote.The Lua filter is newer than the JSON filter, but I have tested it on a wide range of Markdown files and it produces byte-wise identical results.
-
pandoc-sidenote, a JSON filter which reads a JSON representation of the pandoc AST on stdin and produces JSON on stdout.This filter is written in Haskell and compiled to a native executable. It uses the upstream
pandoc-typeslibrary to deserialize the JSON it receives on stdin, which means thatpandoc-sidenotecan only be used with a version ofpandocbuilt against the samepandoc-typesversion. See JSON filter dependencies below.This is cumbersome, as it means that
pandoc-sidenoteneeds to be rebuilt and republished for any newpandocversions which makes a change to the core AST, even if it's a part of the AST that this filter doesn't care about.
The Lua filter has no dependencies. Skip ahead to Installation to install the Lua filter.
The JSON filter is built against a specific version of Pandoc. This table maps
pandoc versions to pandoc-sidenote versions:
| pandoc | pandoc-sidenote |
|---|---|
| 3.0 | 0.23.0 |
| 2.11 | 0.22.0, 0.22.1, 0.22.2 |
| 2.9 | 0.20.0 |
| 2.1, 1.19 | 0.19.0 |
| 1.18 | 0.9.0 |
If a newer version of pandoc has been released, the Stack build manifest
will need to be adjusted for that version, and the project then rebuilt.
The Lua filter (pandoc-sidenote.lua) is the preferred way to use this project.
You can install it globally or local to a project. The Lua filter is a single
Lua file that works on all platforms where pandoc works.
Alternatively, you can install the pandoc-sidenote native executable, which
works as a JSON filter. JSON filters are slower and more tedious to use with
cutting edge Pandoc versions.
Consult the docs for how Pandoc searches for Lua filters.
To install globally:
-
Download
pandoc-sidenote.lua -
Put the file in the Pandoc user data directory. See the Pandoc docs for what the data directory is on your system. Specify a data directory by passing
--data-dirwhen invokingpandoc. -
Invoke
pandoclike this:pandoc --lua-filter pandoc-sidenote.lua [...]
As an example, on macOS or Linux, this shell snippet will download and install the filter globally:
curl --remote-name --no-clobber --create-dirs \
--output-dir "${XDG_DATA_DIR:-$HOME/.local/share/}/pandoc/filters" \
-sSL "https://github.com/jez/pandoc-sidenote/raw/refs/heads/master/pandoc-sidenote.lua"Consult the docs for how Pandoc searches for Lua filters.
To install locally:
-
Download
pandoc-sidenote.lua -
Put the file in the Pandoc user data directory. See the Pandoc docs for what the data directory is on your system. Specify a data directory by passing
--data-dirwhen invokingpandoc. -
Invoke
pandoclike this:pandoc --lua-filter ./pandoc-sidenote.lua [...]
As an example, on macOS or Linux, this shell snippet will download and install the filter into the current directory:
curl --remote-name --no-clobber \
-sSL "https://github.com/jez/pandoc-sidenote/raw/refs/heads/master/pandoc-sidenote.lua"pandoc-sidenote is on Hackage and can thus be installed using cabal:
cabal install pandoc-sidenoteIf you're on OS X, you can install the pandoc-sidenote binary from my Homebrew
tap:
brew install jez/formulae/pandoc-sidenoteOtherwise, you'll have to install from source. This project is written in Haskell and built using Stack. If you're new to Haskell, now's a perfect time to wet your toes! Go install Stack first, then run these commands:
git clone https://github.com/jez/pandoc-sidenote
cd pandoc-sidenote
# this is going to be reaaally long the first time
stack build
# copy the compiled binary onto your PATH
stack install-
By default, all markdown foot notes will be converted into numbered side notes.
This will be a side note.[^1] [^1]: Example side note text. -
To get a margin note (which has no number), make the footnote text start with
{-}. This is akin to the# Heading {-}syntax that Pandoc supports for eliding numbers in documents that would otherwise have numbered headings.This will be a margin note.[^2] It will not have numbers [^2]: {-} Example margin note text. -
To leave a footnote untouched so that it remains in the document as a normal footnote, prefix the text with
{.}.This text will remain a footnote.[^3] [^3]: {.} Example footnote text.Be careful mixing side notes with footnotes in the same document, because they will be numbered independently. Documents that make heavy use of footnotes may want to use margin notes (
{-}) instead of side notes. -
By default, side notes and margin notes cannot contain block elements, like code blocks, lists, or tables. If they do, they are silently dropped.
To avoid having block elements dropped, use
{^}or{^-}for block-based side notes and margin notes, respectively.This side note[^4] and this margin note[^5] can contain block elements. [^4]: {^} There are block elements in this side note: - Hello, world! [^5]: {^} There are block elements in this margin note: ```python print("Hello, world!") ```Note: these two features produce an HTML structure that is currently only supported by Pandoc Markdown CSS Theme. Notably, Tufte CSS does not support this structure yet.
Note: these two features are only supported by the Lua filter, not the JSON filter.
The core functionality is also exposed as a Haskell library, which can be called by Haskell applications such as Hakyll. It comes in two different flavours:
-
SideNote.hs: An implementation making use of pandoc's native
Spanconstructors. This is what's used in thepandoc-sidenoteexecutable. -
SideNoteHTML.hs: An implementation that converts the footnote directly into HTML, enabling the embedding of arbitrary blocks inside of side and marginnotes.
On the whole, each file weighs in at just about 100 lines of code—check them out if you're curious how they work.
Side note: I run this command to generate the zip files attached to releases that are downloaded by the Homebrew formula:
makeIt would be nice to get GitHub Actions set up to build and publish releases for each tagged commit automatically.
I run this command to publish packages to Hackage:
# First, edit `package.yaml` to remove `-Werror`, then:
stack upload .