Skip to content

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.