Skip to content

Customizing

The core template puts the engine in your project under .pandoc/. You change it by editing those files and rebuilding the image:

make image      # rebuild the engine with your changes
make publish    # build your book against it

This is the harder end of Keystone, and the principle here is hard is hard, not impossible. Keystone makes the easy things easy; shipping the whole engine in your project is how it keeps the hard things possible. The work is real — but nothing is hidden or off-limits.

Before changing the engine, check whether you need to. Most new constructs are shortcuts, not new code — a shortcut composes existing handlers in shortcuts.yaml and needs no rebuild. Reach into the engine only for genuinely new behavior. See Writing your own shortcuts.

Adding a handler

When a behavior can't be composed from existing handlers, add one:

  1. Create a directory under .pandoc/filters/divs/<class-name>/. The directory name becomes the class — divs/sidenote/ handles ::: sidenote.
  2. Add handler.lua returning a per-format hook table (see Handlers for the contract). Implement the formats you need; unhandled formats pass through.
  3. Add optional style files — macros.tex for PDF, style.css for EPUB. If a macro needs a LaTeX package the image doesn't ship, add it when you build the image — see The runtime image.
  4. make image, then build your book to confirm it works.
  5. Front it with a shortcut in shortcuts.yaml — the author-facing name your manuscript uses, and where any required attributes get enforced.

For a step-by-step build of a real handler — concrete function bodies, wiring the style files, validating attributes, and fronting it with a shortcut — follow Writing a handler.

The new class is a shortcut target the moment it exists — no registration needed.

The hook table reaches further than div/span: a handler can also route Header elements by their class, or intercept whole element types (like Figure) through global slots. The authoritative contract is the dispatcher itself — read .pandoc/filters/divs.lua and the existing handlers alongside it for the complete surface.

Keep each handler to a single concern and let it pass content through in formats it can't serve — the format integration and handler pages explain why this is what keeps output correct everywhere. If you edit filter code, construct and inspect the document through the engine's AST library (KAST) rather than calling Pandoc's constructors directly, and read build-time settings through the env-io library rather than os.getenv; the existing handlers and filters show the pattern.

DOCX/ODT styling is limited here

A docx/odt hook can do plenty on its own — emit raw word-processor markup, restructure content, or attach a custom-style that reuses a paragraph or character style already in the reference document. What it can't do from the template is add a new named style: the reference doc is prebuilt, and the tooling that regenerates it isn't shipped. So for genuinely new word-processor styling a custom handler realistically covers PDF and EPUB, while its DOCX/ODT output emits markup directly or falls back to the default style.

Adding that new style is possible — regenerating the reference documents to carry it is the fork path — but rarely worth it. DOCX and ODT are editable handoff formats for getting a draft to an editor, not publication targets; PDF and EPUB are where styling effort pays off.

Fonts

You don't need to touch the engine to add a font — drop the .otf files in fonts/ and register them in fonts/fonts-registry.yaml, which works in both templates. See Typography & fonts. Editing the engine's built-in font set is a deeper change that belongs to a fork.

The fork boundary

There's a line between using Keystone and forking it. Adding shortcuts, adding a handler, adjusting a filter for your own book — that's customization, and core is built for it. Rebuilding the reference documents, changing the built-in font registry, or reworking the pipeline means you've taken ownership of the engine itself. That's a legitimate path and core is where it starts — just know that a fork no longer receives Keystone updates automatically.