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/<name>/. The handler's class is ks-<name> — divs/sidenote/ handles ks-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. Front it with a system shortcut — the name your manuscript uses, and where required attributes and accepted values get enforced. It's a fragment, .pandoc/system-shortcuts/sidenote.yaml, whose class: ks-sidenote names the handler, whose forms: declares the forms it renders, and whose entries accept each attribute's domain.
  5. make image, which compiles the fragments, then build your book to confirm it works.

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

The hook table reaches further than divs and spans: a header hook renders a heading, which the fragment declares as the heading form, and global slots intercept whole element types (like Figure). The dispatcher, .pandoc/filters/divs.lua, owns the hooks' contract; read it alongside the existing handlers. Handlers lists the rest of a handler's table.

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.

Reaching outside the engine

Some capabilities don't belong in a filter at all — rendering a diagram needs a renderer, and putting one in the image would charge every book for it. A hook is how a project gains one: a separate container it names in its own compose file, which the engine talks to over a socket. Nothing in the engine changes, and a project that wires none pays nothing. Available hooks lists what is published, and Wiring a published hook is the procedure.

That is the customization that does not cost you a fork, so reach for it before the section below.

The fork boundary

There's a line between using Keystone and forking it. Adding shortcuts, adding a handler, wiring a hook, 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.