This guide is for the people who build, deploy, embed, and extend pandoc rather than simply run it: packagers, integrators who call pandoc from a service, developers writing readers/writers/filters, and operators who run pandoc-server or the WebAssembly build. It documents pandoc's architecture, build system, configuration surface, security posture, and extension points.
For day-to-day command-line usage, see the companion User Guide. This document assumes you are comfortable with the command line, a Haskell toolchain, and document-conversion concepts.
Note
The pandoc library is at version 3.10 (GPL-2.0-or-later). The repository is a multi-package Haskell project: the core library plus three companion packages (
pandoc-cli,pandoc-server,pandoc-lua-engine) and two vendored libraries (citeproc,xml-light).
1. Executive Summary
Pandoc is a Haskell library and command-line tool that converts documents between dozens of markup and word-processing formats. Its defining architectural idea is a single intermediate representation — the Pandoc abstract syntax tree (AST) — that decouples input parsing from output rendering. Every input format has a reader that produces the AST; every output format has a writer that consumes it. As a result, supporting N input and M output formats requires N + M components, not N × M converters.
| Attribute | Value |
|---|---|
| Language | Haskell |
| Library version | 3.10 |
| License | GPL-2.0-or-later |
| Build system | Cabal (cabal-version: 2.4), also Stack |
| Tested compilers | GHC 9.6.7, 9.8.4, 9.10.3, 9.12.2 |
| Packages in repo | pandoc, pandoc-cli, pandoc-server, pandoc-lua-engine (+ vendored citeproc, xml-light) |
| Core library modules | ~200 under Text.Pandoc.* |
| Runtime model | Pure conversion pipeline; no daemon required (optional HTTP server) |
| Scripting | Embedded Lua engine for filters and custom readers/writers |
Important
Pandoc itself does not generate PDFs. PDF output is delegated to an external engine (LaTeX, ConTeXt, groff, typst, or an HTML-to-PDF tool). Any deployment that needs PDF must provision that engine separately.
2. Architecture
2.1 The conversion pipeline
┌──────────┐ ┌──────────┐ ┌─────────────────┐ ┌──────────┐ ┌──────────┐
│ INPUT │→ │ READER │→ │ Pandoc AST │→ │ WRITER │→ │ OUTPUT │
│ (bytes/ │ │ (parse) │ │ Blocks + │ │ (render) │ │ (bytes/ │
│ text) │ │ │ │ Inlines + Meta │ │ │ │ text) │
└──────────┘ └──────────┘ └────────┬────────┘ └──────────┘ └──────────┘
│
┌─────────────┴─────────────┐
│ TRANSFORMS / FILTERS │
│ • Lua filters │
│ • JSON filters (exec) │
│ • citeproc (citations) │
│ • built-in transforms │
└────────────────────────────┘
The AST (Pandoc Meta [Block]) is a list of block-level elements (paragraphs, headings, lists, tables, code blocks) containing inline elements (text runs, emphasis, links, math), plus a metadata map. Readers, writers, and filters all speak this one type. The pipeline is a pure function from input bytes to output bytes, wrapped in pandoc's PandocMonad for controlled IO (file reads, HTTP fetches, logging).
Note
Filters, Lua filters, and citation processing are applied in the exact order they appear on the command line. The order is significant when transforms depend on one another's output.
2.2 Package layout
The repository is split into cooperating packages (declared in cabal.project):
| Package | Directory | Role |
|---|---|---|
pandoc (library) |
src/ |
The core library: readers, writers, the AST, the App driver, templates, highlighting glue |
pandoc-cli |
pandoc-cli/ |
The pandoc executable; wires the library to a command line; can also run as server or REPL |
pandoc-server |
pandoc-server/ |
An HTTP conversion service built on Servant/WAI (version 0.1.3) |
pandoc-lua-engine |
pandoc-lua-engine/ |
The Lua scripting engine for filters and custom readers/writers |
citeproc |
citeproc/ |
Vendored Citation Style Language (CSL) processor |
xml-light |
xml-light/ |
Vendored lightweight XML parser used by XML-based readers/writers |
This separation lets downstream users depend on only what they need — for example, a service that converts in-process can depend on the pandoc library and pandoc-lua-engine without pulling in the CLI or the HTTP server.
2.3 The CLI driver
The command-line entry point lives in Text.Pandoc.App (src/Text/Pandoc/App.hs). Its public surface includes:
| Symbol | Purpose |
|---|---|
convertWithOpts :: Opt -> PandocIO () |
Run a full conversion from a parsed options record |
Opt(..) |
The complete set of conversion options (every CLI flag has a field) |
options |
The option parser specification (GetOpt descriptors) |
parseOptions / parseOptionsFromArgs |
Parse argv into an Opt |
applyFilters |
Apply the configured filter chain to the AST |
defaultOpts |
The default option record |
versionInfo |
The --version banner contents |
The actual flag parsing and the Opt record are factored into the Text.Pandoc.App.Opt and Text.Pandoc.App.CommandLineOptions submodules under src/Text/Pandoc/App/.
3. Repository Structure
pandoc/
├── src/Text/Pandoc/ Core library (~200 modules)
│ ├── App.hs CLI driver: convertWithOpts, options
│ ├── App/ Option record + command-line parsing
│ ├── Readers/ One module per input format (Markdown, LaTeX,
│ │ Docx, ODT, HTML, Org, RST, Typst, …)
│ ├── Writers/ One module per output format (HTML, LaTeX,
│ │ Docx, Powerpoint, Markdown, JATS, …)
│ ├── Parsing/ Shared parser combinators
│ ├── Citeproc/ Bridge to the citeproc library
│ ├── Class/ PandocMonad: controlled IO abstraction
│ └── Data/ Bundled data-file access
├── pandoc-cli/ The pandoc executable
│ ├── src/ Main entry, dispatch to server/lua/repl
│ └── (build variants: server, no-server, lua, no-lua, wasm)
├── pandoc-server/ HTTP server (Servant/WAI)
├── pandoc-lua-engine/ Lua filter & scripting engine
├── citeproc/ Vendored CSL processor + localizations
├── xml-light/ Vendored XML parser
├── data/ Templates, translations, reference docs
│ ├── templates/ Default templates per output format
│ ├── translations/ UI-string localizations (107 locales)
│ ├── reference.docx/.odt/.pptx Style reference documents
│ └── *.lua, abbreviations, init.lua
├── man/ Man-page sources
├── wasm/ WebAssembly build (JS glue, examples, patches)
├── test/ Test suite (command tests, golden files)
├── benchmark/ Performance benchmarks
├── linux/ macos/ windows/ Platform packaging scripts
├── pandoc.cabal Library + test + benchmark definitions
├── cabal.project Multi-package + wasm build configuration
├── stack.yaml Stack build configuration
└── MANUAL.txt The authoritative user manual (source of MANUAL.html)
3.1 The data/ directory
Everything in data/ ships with pandoc and is discoverable at runtime through the user data directory or (when compiled with embed_data_files) baked into the binary:
templates/— the default standalone template for each output format. Inspect any withpandoc -D FORMAT.translations/— localized UI strings (e.g. "Figure", "Table of Contents") for 107 locales, selected via thelangvariable.reference.docx/reference.odt/reference.pptx— the style templates for Office output. Extract withpandoc --print-default-data-file reference.docx.init.lua— the Lua init script run when the scripting engine starts.
4. System Requirements
4.1 Runtime
The released binaries are statically linked and have no dynamic dependencies or external data-file requirements. The only optional runtime dependencies are the external tools pandoc shells out to:
| Capability | External requirement |
|---|---|
| PDF via LaTeX | A TeX distribution: TeX Live, MiKTeX, BasicTeX, or TinyTeX |
| PDF via HTML | weasyprint, prince, wkhtmltopdf, or pagedjs-cli |
| PDF via ConTeXt / ms / typst | context, groff/pdfroff, or typst |
| SVG in formats without SVG support | rsvg-convert (from librsvg) |
| JSON filters | The interpreter for the filter's language (e.g. Python) |
Warning
The statically-linked binaries (and Conda Forge builds) cannot load Lua filters that depend on C-based Lua modules. Deployments needing those must use a dynamically-linked build compiled from source.
4.2 Build toolchain
| Tool | Minimum |
|---|---|
| GHC | 9.6.7 (tested up to 9.12.2) |
| Cabal | 2.0+ (cabal-version: 2.4 in the project) |
| Stack | 1.7.0+ (alternative to Cabal) |
5. Building From Source
Build the executable, not the bare library, by targeting pandoc-cli.
5.1 Stack
git clone https://github.com/jgm/pandoc
cd pandoc
stack setup # downloads the right GHC if needed
stack install pandoc-cli
stack install places the pandoc binary in ~/.local/bin; add it to your PATH.
5.2 Cabal
cabal update
cabal install pandoc-cli # released version from Hackage
To build the local (development) tree instead, run the command from inside the source directory — cabal uses the cabal.project packages.
5.3 Custom build (for packagers)
cabal update
cabal build --only-dependencies
cabal configure --flags=FLAGSPEC --enable-tests
cabal build
cabal test
cabal install pandoc-cli
5.4 Build flags
Flags are toggled with +flag / -flag in cabal configure --flags=..., in cabal.project's package stanzas, or with --flag pandoc:embed_data_files under Stack.
pandoc library flags:
| Flag | Default | Effect |
|---|---|---|
embed_data_files |
off | Bake all data/ files into the binary, producing a relocatable, self-contained executable |
http |
on | Include HTTP support for fetching remote inputs/resources |
pandoc-cli flags:
| Flag | Default | Effect |
|---|---|---|
lua |
on | Compile in Lua filter and custom-writer support (links pandoc-lua-engine) |
server |
on | Compile in HTTP server mode (active when the binary is named/symlinked pandoc-server) |
repl |
on | Compile in the Lua REPL |
nightly |
off | Stamp the build with nightly version metadata |
Tip
For a fully relocatable binary, build with
embed_data_files:cabal build -fembed_data_filesorstack build --flag pandoc:embed_data_files. This is also the recommended build for running under--sandbox, since data-file lookups then succeed without filesystem access.
5.5 Running tests and benchmarks
cabal test # full suite
cabal test --test-options='-p markdown' # only tests matching "markdown"
cabal test --test-options='-j4 --hide-successes'
cabal configure --enable-benchmarks && cabal build
cabal bench
cabal bench --benchmark-options='markdown -s 20'
The test driver is test/test-pandoc.hs. Format conversions are largely tested via command tests (test/command/) — each file pairs an invocation with its expected output — and golden files under format-specific subdirectories (test/docx/, test/odt/, test/pptx/, etc.).
6. Embedding the Library
To convert documents in-process from Haskell, depend on the pandoc library and use the high-level API from the top-level Text.Pandoc module. A conversion runs inside PandocMonad (typically runIO):
import Text.Pandoc
main :: IO ()
main = do
result <- runIO $ do
doc <- readMarkdown def "# Hello *world*"
writeHtml5String def doc
html <- handleError result
putStrLn (show html)
Key library entry points:
| Module | Provides |
|---|---|
Text.Pandoc |
Re-exports readers, writers, options, and the monad |
Text.Pandoc.Readers.* |
readMarkdown, readLaTeX, readDocx, … |
Text.Pandoc.Writers.* |
writeHtml5String, writeLaTeX, writeDocx, … |
Text.Pandoc.Class |
PandocMonad, runIO, runPure (sandboxed, no IO) |
Text.Pandoc.App |
convertWithOpts for a full CLI-equivalent run |
Text.Pandoc.JSON |
toJSONFilter for building JSON filters in Haskell |
Note
runPureruns a conversion with no IO at all — useful for safely converting untrusted input or for testing.runIOpermits controlled IO (file reads, HTTP, logging) mediated byPandocMonad.
7. Deploying pandoc-server
The pandoc-server package (version 0.1.3) exposes pandoc conversion over HTTP using the Servant framework on WAI. The same functionality is built into the pandoc binary when compiled with the server flag and is activated by invoking the binary as pandoc-server.
7.1 Server options
From Text.Pandoc.Server (pandoc-server/src/Text/Pandoc/Server.hs):
| Option | Flag | Default | Meaning |
|---|---|---|---|
| Port | -p / --port |
3030 |
TCP port to listen on |
| Timeout | -t / --timeout |
2 |
Per-request timeout in seconds |
# Run the server (binary built/symlinked as pandoc-server)
pandoc-server --port 3030 --timeout 5
The server defines a Servant API and a WAI app. Requests carry a JSON Params body describing the conversion (input text or base64 Blob, source/target formats, options); the response is the converted document.
7.2 Operational characteristics
- Stateless. Each request is an independent pure conversion. Scale horizontally behind a load balancer; no shared state or database.
- CORS. The server installs WAI CORS middleware, so browser clients can call it cross-origin.
- Short default timeout. The 2-second default protects against pathological inputs; raise it deliberately for large documents.
Caution
A conversion server accepts arbitrary documents from clients. Treat every request as untrusted input. The server performs conversions in a constrained manner, but you remain responsible for resource limits (request size, concurrency, timeout) and for not enabling filters or PDF engines that reach the network or filesystem on a public endpoint. See section 9.
8. WebAssembly Build
Pandoc compiles to WebAssembly (wasm32) for in-browser conversion, powering the online demo. The wasm configuration lives in cabal.project (guarded by if arch(wasm32)) and the wasm/ directory.
Notable wasm build choices:
- Tests are disabled (
tests: False). - The CLI is built with
+lua -repl -serverand the library with+embed_data_files -http— no HTTP, no server, but Lua filters and embedded data are retained. - Several dependencies are pulled from patched Git forks (
wasm/patches/*.patch) to work under the WASI/wasm32 toolchain (conduit-extra, streaming-commons, xml-conduit, splitmix, hslua, and a custom Lua build with stubbedtmpnam/tmpfile/system). wasm/index.jsprovides the JavaScript glue;wasm/examples/contains ready-made conversion demos (e.g. markdown-to-revealjs, docx-with-equations-to-latex).
Note
The wasm build is a specialized target. Most integrators want the native library or
pandoc-serverinstead; reach for wasm only when conversion must run client-side in a browser sandbox.
9. Security Analysis
Pandoc's threat model centers on processing untrusted documents. The relevant controls:
9.1 Sandbox mode
--sandbox (or sandbox: true in a defaults file; runPure in the library) limits the IO that readers and writers may perform to the files named on the command line. This blocks attacks such as exfiltrating local files through include directives in the source document.
pandoc --sandbox untrusted.md -o out.html
Caution
--sandboxdoes not constrain:
- Filters — JSON and Lua filters run with full process privileges. Do not enable user-supplied filters on untrusted input.
- PDF engines — the external engine runs unsandboxed. Generating PDFs from user-controlled HTML can enable server-side request forgery (SSRF) through the engine's resource fetching. If you must do this, isolate the engine (no network, restricted filesystem) at the OS/container level.
9.2 Network access
Pandoc fetches remote inputs and resources over HTTP when given URLs or when --embed-resources downloads assets. On a server or for untrusted input, consider building with -http (disabling HTTP support) or running in --sandbox, and restrict egress at the network layer. --no-check-certificate disables TLS verification — never enable it for untrusted or production fetches.
9.3 Data-file lookups under sandbox
Some readers/writers (notably docx) need bundled data files. Under --sandbox, filesystem lookups fail. Compile with embed_data_files so the data is baked into the binary and the sandbox still works.
9.4 Reporting
Security issues are reported privately per SECURITY.md in the repository, not through public issues.
9.5 Failure modes as signals
Pandoc fails closed with a specific nonzero exit code (see section 12). In automation, treat any nonzero exit as a failed conversion and inspect the code — for example, code 47 (PDF program not found) versus 64 (parse error) call for very different responses.
10. Configuration Surface
Pandoc has no central config file; configuration is layered from three sources, later sources overriding earlier ones:
- The user data directory — bundled and user-overridable resources.
- Defaults files — reusable YAML/JSON option sets (
-d/--defaults). - Command-line options and document metadata — highest precedence.
10.1 The user data directory
Resources placed here override pandoc's built-in defaults. Default locations:
| Platform | Default user data directory |
|---|---|
| *nix / macOS | $XDG_DATA_HOME/pandoc (i.e. $HOME/.local/share/pandoc), falling back to $HOME/.pandoc |
| Windows | %APPDATA%\pandoc |
Override per run with --data-dir. Confirm the active directory with pandoc --version. Recognized subdirectories and files include templates/, defaults/, filters/, metadata/, csl/, and override copies of reference.docx, reference.odt, epub.css, and abbreviations.
Note
Pandoc does not create the user data directory for you. Create it yourself before dropping in overrides.
10.2 Defaults files
A defaults file maps almost every command-line option to a YAML key. Store frequently used option sets and invoke them by name:
# ~/.local/share/pandoc/defaults/report.yaml
from: markdown
to: pdf
pdf-engine: xelatex
standalone: true
number-sections: true
toc: true
toc-depth: 2
variables:
geometry: margin=1in
fontsize: 11pt
verbosity: INFO
pandoc input.md -dreport -o report.pdf
Path fields support ${HOME}, ${USERDATA}, and ${.} (the defaults file's own directory) interpolation. Repeatable options (--css, --metadata-file, --include-in-header, --variable, …) combine with, rather than replace, command-line values.
10.3 Templates and reference documents
- Templates drive standalone output. Print the default with
pandoc -D FORMAT; override by placing a file intemplates/or passing--template. - Reference documents (
reference.docx,reference.odt,reference.pptx) drive the styling of Office output. Extract the defaults with--print-default-data-file, edit the styles, and supply with--reference-doc.
11. Extending Pandoc
11.1 Filters
The lowest-friction extension point. A filter transforms the AST between reading and writing:
- Lua filters (
-L/--lua-filter) run on the built-in engine; no external dependency. Each filter maps AST element names to transforming functions. - JSON filters (
-F/--filter) are external executables reading/writing the AST as JSON. Writable in any language; the target format is passed asargv[1]. In Haskell,Text.Pandoc.JSON.toJSONFilterbuilds one.
Filter lookup order: explicit path → $DATADIR/filters → $PATH (executables only).
11.2 Custom readers and writers
Beyond the built-in formats, pandoc can load a custom reader or writer written in Lua by passing the script path as the -f/-t format. This lets you support a new format without recompiling, by implementing the parse or render logic in Lua against pandoc's AST.
11.3 Adding a built-in format
To add a first-class format, add a module under src/Text/Pandoc/Readers/ or Writers/, wire it into the format registry, and add tests (golden files under test/ and/or command tests under test/command/). Each reader produces the AST; each writer consumes it — the modular design means no other component changes.
12. Exit Codes Reference
Pandoc returns 0 on success and a specific code on failure. Use these in automation to distinguish failure classes:
| Code | Constructor | Code | Constructor |
|---|---|---|---|
| 1 | PandocIOError | 47 | PandocPDFProgramNotFoundError |
| 3 | PandocFailOnWarningError | 61 | PandocHttpError |
| 4 | PandocAppError | 62 | PandocShouldNeverHappenError |
| 5 | PandocTemplateError | 63 | PandocSomeError |
| 6 | PandocOptionError | 64 | PandocParseError |
| 21 | PandocUnknownReaderError | 66 | PandocMakePDFError |
| 22 | PandocUnknownWriterError | 67 | PandocSyntaxMapError |
| 23 | PandocUnsupportedExtensionError | 83 | PandocFilterError |
| 24 | PandocCiteprocError | 84 | PandocLuaError |
| 25 | PandocBibliographyError | 89 | PandocNoScriptingEngine |
| 31 | PandocEpubSubdirectoryError | 91 | PandocMacroLoop |
| 43 | PandocPDFError | 92 | PandocUTF8DecodingError |
| 44 | PandocXMLError | 93 | PandocIpynbDecodingError |
| — | — | 99 | PandocResourceNotFound |
13. Operations Playbook
13.1 Verifying a deployment
pandoc --version # version, features, data dir
pandoc --list-input-formats | wc -l # reader count sanity check
echo '# ok' | pandoc -t html # smoke-test a conversion
13.2 Provisioning for PDF
Confirm the chosen engine is on PATH before enabling PDF jobs. For LaTeX, install missing packages with your TeX manager (e.g. tlmgr install collection-fontsrecommended). For HTML-based PDF, install WeasyPrint/Prince/wkhtmltopdf and select it with --pdf-engine.
13.3 CI integration
- Use
--fail-if-warningsto make conversions fail loudly (exit code 3). - Use
--log=FILEto capture machine-readable JSON logs for build artifacts. - Pin a pandoc version; behavior and default extensions evolve between releases.
- Official Docker images (
pandoc/core,pandoc/latex) and GitHub Actions / GitLab CI examples exist for pipeline use.
13.4 Performance
Released binaries are compiled with -threaded and the RTS option -A8m (8 MB allocation area). For very large documents, the dominant cost is usually parsing and the external PDF engine, not pandoc's rendering. Benchmark with the in-repo cabal bench suite when evaluating regressions.
14. Code Quality & Conventions
- Style. The project enforces formatting with
stylish-haskell(.stylish-haskell.yaml) and lint rules with HLint (.hlint.yaml);.editorconfigstandardizes whitespace. - Dead-code detection.
weeder.tomlconfigures Weeder to find unused exports. - Module discipline. One reader/writer per format keeps the surface area predictable; shared logic lives in
Parsing/,Writers/Shared, andReaders/-level helpers. - Controlled IO. All IO flows through
PandocMonad, enabling both real-IO (runIO) and pure (runPure) execution from the same code — central to sandboxing and testability. - Testing strategy. Heavy use of golden tests and declarative command tests (
test/command/) means most format behavior is pinned to expected output, making regressions easy to catch.
15. Potential Improvements
Drawn from the project's open issue tracker at the documented commit — these are active areas, useful context for contributors and integrators:
| Theme | Representative open issues |
|---|---|
| Typst writer fidelity | lang attribute propagation on spans (#11673); EPUB→PDF via the typst engine (#11626) |
| Org mode reader | Use code (~) vs verbatim (=) markers (#11655) |
| New format support | Atlassian Document Format (ADF) reader/writer for Confluence Cloud (#9898) |
| Markdown lists | Mixing incremental and non-incremental list types (#11653) |
| HTML output | Lazy-loading images (loading="lazy") (#6197) |
| Word/ODT styling | List paragraphs using "List Paragraph" vs "Normal" style (#10974) |
Tip
The modular reader/writer architecture means most of these are localized changes — a single writer module plus tests — rather than cross-cutting work. The format registry and the golden-test layout are the two touch points a contributor must learn first.
16. Quick Reference
Build
| Task | Command |
|---|---|
| Build with Stack | stack install pandoc-cli |
| Build with Cabal | cabal install pandoc-cli |
| Relocatable binary | cabal build -fembed_data_files |
| Run tests | cabal test |
| Run a subset of tests | cabal test --test-options='-p PATTERN' |
| Run benchmarks | cabal bench |
Server
| Task | Command |
|---|---|
| Start server | pandoc-server --port 3030 --timeout 5 |
| Default port / timeout | 3030 / 2s |
Configuration
| Item | Where |
|---|---|
| User data dir (*nix) | $HOME/.local/share/pandoc |
| User data dir (Windows) | %APPDATA%\pandoc |
| Override data dir | --data-dir=DIR |
| Defaults files | <data-dir>/defaults/*.yaml, used via -dNAME |
| Inspect default template | pandoc -D FORMAT |
| Extract a data file | pandoc --print-default-data-file=FILE |
Security
| Goal | Action |
|---|---|
| Process untrusted input | --sandbox (+ embed_data_files build) |
| Disable HTTP support | build with -http |
| Make warnings fatal | --fail-if-warnings |
| Pure, no-IO conversion (library) | runPure |