Handlers
A handler is the engine code behind a div, span or heading class. When you write ::: dialog, the dialog shortcut
expands to the handler's own class, ks-dialog, and the dispatcher routes it to the dialog handler, which emits the
right output for the current format. Every class a handler answers is reached that way, through the system shortcut that
fronts it.
Underneath, Pandoc offers a primitive but powerful model — a document tree you reshape with Lua. The handler layer is where Keystone turns that model into something authors never touch.
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 filters/divs/<name>/ handles the class ks-<name>, which
shortcut expansion writes, at most one per element. That handler owns the element, and the element's other classes reach
it untouched.
There is deliberately no handler aggregation — Keystone never runs two handlers on one element. Stacked handlers
would compose in class order: {.font .align} and {.align .font} would differ silently, and nobody reads class order
as meaningful. An element naming two shortcuts stops the build.
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.
What a handler sees
A handler sees the document as it stands at its own level: Pandoc reaches spans before divs, and inner divs before outer ones, so everything inside an element has been through its own handler first.
A handler therefore decides by what each element is — its type, how many, and where — and owns its output. Pandoc's writers decide what renders: a paragraph is a paragraph whatever it holds, and an HTML comment is a block like any other.
Anatomy of a handler
Each handler is a directory under filters/divs/:
filters/divs/<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
└── README.md (optional) — usage notes
The build reads macros.tex (PDF) and style.css (EPUB) directly, so these two files style PDF and EPUB. DOCX/ODT
styling works through a reference document instead — see Customizing.
handler.lua returns a table keyed by element kind — div, span and header for fenced divs, bracketed spans and
headings, plus 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, report) --[[ emit LaTeX ]] end,
epub = function(el, report) --[[ emit CSS-classed HTML ]] end,
default = function(el, report) --[[ everything else ]] end,
},
}
Each function receives the element and a report handle — call report.warn(…) / report.fatal(…) to surface a
diagnostic about the author's input (never io.stderr or error() directly). See
Validation & diagnostics.
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) still has a hook per form its fragment declares,
one that changes nothing, as container's does: div = { default = function() end }.
Beside the hooks, the table may declare:
required_attributes— the attributes the handler can't run without.domain— what each attribute takes: a list of values; a rule with its options, one an author'sacceptstakes ({ integer = { min = 2, max = 4 } },"css-length"); or a family the font registry holds, one of its sizes, or a mark the book declares ("font-family","font-size","mark"). A"font-family"attribute's fonts are embedded in EPUB and given their CSS, asfont's are. An attribute with no domain takes any text.takes_content = false— for a handler that renders from attributes alone, such aspagebreakandvspace: content written into its element is then refused rather than dropped.
Validation & diagnostics says how the shortcut layer holds each to the handler.
For a step-by-step build of a real handler — filling in these function bodies, wiring the style files, and declaring its attributes — see Writing a handler.
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.