# pandoc-crossref: numbering and cross references for Pandoc documents

> pandoc-crossref is a Haskell filter for Pandoc that numbers figures, equations and tables and resolves the cross references to them, with per chapter numbering and optional LaTeX integration through cleveref and the listings package. It suits anyone writing a technical document in Markdown and publishing it to PDF, and the sharpest operational detail is that its binaries must match the Pandoc version they were built against.

**lierdakil/pandoc-crossref** — Pandoc filter for cross-references

- Repository: https://github.com/lierdakil/pandoc-crossref
- Website: https://lierdakil.github.io/pandoc-crossref/
- Stars: 1,073 · Forks: 80
- Language: Haskell
- License: GPL-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/lierdakil-pandoc-crossref

## What the filter does

pandoc-crossref is a Pandoc filter for numbering figures, equations, tables and cross references to them, written in Haskell and licensed GPL-2.0.

The problem it solves appears the moment a technical document has more than one figure. Pandoc's own markdown has no figure numbering, so a figure caption is just text, and a sentence that refers to Figure 3 is text you have to keep in step by hand. Reorder a section, insert a figure in the middle, and every later reference is wrong.

The filter takes an input markdown file and produces output in a range of formats, with the project shipping demonstrations of the same document converted to HTML, LaTeX, PDF and Markdown, so you can see the behaviour in each target rather than guessing.

The generated document is where the value shows. Figures get numbers, equations get numbers, tables get numbers, and the cross references to them are resolved to whatever the final number turned out to be. The numbering is per document, and the references are updated when the document changes shape.

The project is inspired by two earlier filters, pandoc-fignos and pandoc-eqnos by Tom Duck, and that origin is worth noting because it explains the scope. pandoc-crossref covers figures, equations and tables together, along with the cross references to them, which is the part that turns a numbering tool into a complete referencing system.

## How it hooks into LaTeX

The LaTeX output path is where a Pandoc filter like this has to be careful, because LaTeX has its own numbering and referencing machinery and a filter that duplicates it produces conflicts.

The package tries to use LaTeX labels and references if the output type is LaTeX, and it also tries to supplement rudimentary LaTeX configuration that should mimic metadata configuration by setting the header-includes variable. That last part is the mechanism: the filter writes LaTeX preamble fragments into the document metadata, so the packages the output needs are loaded without the author editing the template.

On top of that, the project documents two optional integrations for LaTeX and PDF output. The first is cleveref, which produces cross references that read as Figure 1 rather than as a bare number, and the second is the listings package, which handles code listings. Both are demonstrated with paired PDF and LaTeX outputs in the project's demo pages, so you can compare the plain build with the enhanced one.

The distinction to understand is between the two layers. The filter resolves references itself, using its own numbering, and for LaTeX output it expresses those references using LaTeX's label and reference mechanism so that LaTeX's own counters stay authoritative. cleveref then improves how the resulting reference reads.

So the filter is not replacing LaTeX's cross referencing, it is feeding it.

## Per chapter numbering, and the requirement it brings with it

Chapter numbering is supported, and it comes with a condition that is easy to miss.

You can enable per-chapter numbering, in the same spirit as the chapters option for LaTeX output. But for non-LaTeX and non-PDF output you have to specify the chapters metadata variable explicitly, using the -M chapters flag.

That asymmetry exists because of how the two paths work. In LaTeX, the chapter structure is real, since the document is broken into chapters and the counters are scoped accordingly. In HTML or Markdown there is no chapter concept, so the filter needs to be told where the chapters begin, and the metadata variable is how you tell it.

The project ships demo outputs for that mode as well, in HTML, Markdown, LaTeX and PDF, so you can see what per chapter numbering looks like in each target before committing to it.

This is the kind of feature that is worth checking early in a project, because the flag is a pipeline option and it has to be set consistently across every build that produces the same document, otherwise two builds of the same source disagree about the numbering.

## Installing the prebuilt binaries, and the version warning

The easiest route is a prebuilt executable, and the README is refreshingly honest about what that means.

The releases page carries prebuilt executables for Windows, macOS and Linux, and the instruction is to unpack them and place them somewhere Pandoc can find them, somewhere on the PATH, in the current working directory, or next to pandoc.exe on Windows.

The caveat is stated twice. The binaries are a product of automated build scripts and are provided as-is, with zero guarantees, and then there is the warning in capitals, which is that you must make sure your Pandoc version matches the version pandoc-crossref was built against, otherwise weird stuff will likely happen.

That warning is the important one. A Pandoc filter is a program that Pandoc runs and exchanges a structured document with, and the interface between them is versioned with Pandoc itself. A filter built against one Pandoc version and run with another can fail in confusing ways, because the document model has changed underneath it.

If you are on a newer Pandoc than the latest release has been built for, the README invites you to open an issue, and there is a nightlies tag with automatic builds for the latest commits. The tag name is a misnomer, and the README says so, those are built on each push rather than nightly.

For anyone who does not trust downloaded binaries, which the README calls completely reasonable, the fallbacks are building from source or using a third party package.

## Building from source, and the third party packages

There are two preferred source builds, and the README is direct about which is easier for a newcomer.

The cabal route needs GHC and cabal-install, and the easiest way to get those is ghcup, with the two install commands given. Then the update and the install.

```bash
cabal v2-update
cabal v2-install --install-method=copy pandoc-cli pandoc-crossref
```

That installs both the filter and Pandoc itself into the cabal bin directory, which you add to the PATH. To pin a version you add a constraint for pandoc-crossref to the command.

The stack route is described as the easier option if you are completely new to Haskell, and it needs stack and maybe git.

```bash
git clone https://github.com/lierdakil/pandoc-crossref.git
cd pandoc-crossref
git checkout <commit/tag/branch>
stack install
```

That installs the executable into the local bin directory, and you can run stack install pandoc-cli in the same directory if you also want Pandoc built.

There is one gotcha for the cabal route, and it is the kind of detail that only appears when you build from a checkout rather than from Hackage. You need to either match the compiler version specified in the workflow file, or remove the frozen project file from the root of the repository, otherwise cabal complains about a version mismatch of the boot packages.

Third party packages exist for Arch Linux, NixOS and the Nix package manager, macOS through Homebrew, FreeBSD as a binary package, any Linux distribution through Linuxbrew, Gentoo through the gentoo-haskell overlay, and Windows through scoop.

## Nix, the binary cache, and how the project tests itself

The repository is a Nix flake, and the Makefile shows a level of build engineering that is unusual for a Pandoc filter.

Installing as a flake is one command, and it takes the latest commit from the master branch, or you can specify a commit, branch or tag.

```bash
nix profile install github:lierdakil/pandoc-crossref
```

The Makefile builds several outputs at once, the default, a static build and a pandoc one, and it pushes them to a binary cache with cachix. The pin target is the interesting part. It pins each build by its tag and architecture with the revision count kept at one, so the cache holds the current release for each platform rather than growing without bound.

The test targets run two suites, a unit test and an integration test, and there is a target that regenerates the test fixtures by running two shell scripts in a Nix develop shell. The presence of generated fixtures that are checked in and regenerated on demand is the sign of a project where the output of the filter is compared against expected files.

The frozen dependency file is generated too. The Makefile reads the compiler version and the Pandoc version out of the continuous integration workflow with a YAML tool and regenerates the freeze file from them, and the stack file is generated from a template plus the same workflow file. So the dependency pins are derived from the CI matrix rather than maintained by hand, which is how they stay in step with the versions the binaries are built against.

## Conclusion

Adopt pandoc-crossref if you write a document in Markdown with figures, equations and tables and need the numbering and the references to be consistent and to survive a change in the order of the document. Do not adopt it if you only need one of the two things it does, because a single purpose filter is simpler for that. Before you install a prebuilt binary, check the Pandoc version it was built against, since the README warns that a mismatch produces undefined behaviour, and build from source if your Pandoc is newer than the latest release.

## FAQ

### What does pandoc-crossref do?

It is a Pandoc filter for numbering figures, equations and tables and for cross references to them. The same input can be converted to HTML, LaTeX, PDF or Markdown, and the filter also supports per-chapter numbering, which for non-LaTeX output needs the chapters metadata variable to be specified.

### Why does pandoc-crossref break when my Pandoc version is different?

A Pandoc filter exchanges a structured document with Pandoc, and that interface is versioned with Pandoc itself, so a filter built against one version can misbehave with another. The README warns in capitals to make sure your Pandoc version matches the one pandoc-crossref was built against, and the prebuilt binaries are provided as-is with zero guarantees.

### How do I install pandoc-crossref without downloading a binary?

Build it from source with cabal-install, installing GHC and cabal through ghcup and then running cabal v2-install --install-method=copy pandoc-cli pandoc-crossref, or with stack by cloning the repository and running stack install. The repository is also a Nix flake, installable with nix profile install github:lierdakil/pandoc-crossref.

### Does pandoc-crossref work with cleveref and code listings?

Yes, optionally for LaTeX and PDF output. The package tries to use LaTeX labels and references when the output type is LaTeX and supplements the LaTeX configuration through the header-includes variable, and the project demonstrates outputs with cleveref and with the listings package.

## Sources

- [License: GPL-2.0](https://github.com/lierdakil/pandoc-crossref/blob/master/LICENSE)
- [lierdakil/pandoc-crossref on GitHub](https://github.com/lierdakil/pandoc-crossref)
- [Project website](https://lierdakil.github.io/pandoc-crossref/)
- [README](https://github.com/lierdakil/pandoc-crossref/blob/master/README.md)
- [Releases](https://github.com/lierdakil/pandoc-crossref/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lierdakil-pandoc-crossref
