Handlers
A handler is the engine code behind a fenced div or span class. When the
dispatcher sees ::: dialog, it routes the element to the dialog handler,
which emits the right output for the current format. The system shortcuts you
write are names that resolve to these handlers (the ks- classes).
Underneath, Pandoc offers a primitive but powerful model — a document tree you reshape with Lua. The handler layer is where Keystone turns that raw power into something authors never have to touch: the hard things, made to look easy.
The element model
Pandoc gives every div and span three distinct fields — not one flat map. Handlers read them deliberately:
| Field | Markdown | Example |
|---|---|---|
| Identifier | #value or id=value |
::: {.figure #fig-1} |
| Classes | .name |
::: {.figure .wide} |
| Attributes | key=value |
::: {.figure width=50%} |
#fig-1 and id=fig-1 both set the identifier, never an attribute — so a
handler that needs the id reads the identifier field, not the attributes. It's
the same ::: {…} model your markup already uses.
Single dispatch
The dispatcher auto-discovers handlers — each subdirectory of
filters/divs/<name>/ registers <name> as a class, no registration step. At
runtime it walks an element's classes and calls the first one that matches a
handler. That handler owns the element; remaining classes are ignored.
There is deliberately no handler aggregation — Keystone never runs two handlers on one element. The reason is cross-format honesty: in LaTeX each handler returns an opaque block of markup, and in DOCX/ODT a paragraph can carry only one style, so "stacking" handlers can't work the same way everywhere. EPUB could do it, but a feature that works in one format and breaks three is worse than none.
To combine behaviors you nest instead — each level dispatches to one handler,
and the composition is visible in the source. In practice you don't write the
nesting by hand; you define a shortcut that
composes the handlers and use that. This is why composition lives in
shortcuts.yaml, not in the handlers.
Anatomy of a handler
Each handler is a directory under filters/divs/:
filters/divs/<class-name>/
├── handler.lua (required) — returns a table of per-format functions
├── macros.tex (optional) — LaTeX preamble for PDF
├── style.css (optional) — CSS for EPUB/HTML
├── style-docx.xml (optional) — DOCX paragraph styles
├── style-odt.xml (optional) — ODT paragraph styles
└── README.md (optional) — usage notes
handler.lua returns a table keyed by element kind — div and span for
fenced divs and bracketed spans, plus header (route a heading by its class) and
global (intercept a Pandoc element type such as Figure). Each maps format
keys (latex, epub, html, docx, odt, or default) to a function that
produces output for that format:
return {
div = {
latex = function(el) --[[ emit LaTeX ]] end,
epub = function(el) --[[ emit CSS-classed HTML ]] end,
default = function(el) --[[ everything else ]] end,
},
}
A handler need only implement the formats it supports; an unhandled format
passes the element through unchanged. A directory that ships only macros.tex or
style.css (style, no logic) can return an empty table.
The design rule
Each handler owns exactly one concern — alignment, font selection, figure sizing. When a second axis of control shows up, it belongs in its own handler, composed via a shortcut, not bolted on as another attribute. That's what keeps each handler implementable across every format. See Format integration for how the per-format functions actually emit output, and Customizing to add your own.
The set of handlers is deliberately small. Keystone started with more and was
pruned down to the few that genuinely do distinct things; everything
author-facing is a shortcut composed from those. It's a
RISC mindset —
a lean instruction set, with the richness living in how you combine it. Lay the public
shortcut names beside the ks- handlers they resolve to and the leverage is
plain: a small core of primitives, a rich vocabulary built on top.