Handlers and rules#
A renderer’s behaviour is a set of functions stored in three tables, filled by
register. This page covers what you can register, the function signatures, and the
order things run in.
Registering#
@MyRenderer.register("calc_line") # a handler
def render_calc_line(renderer, node, base_context): ...
@MyRenderer.register("name:sym") # a rule
def swap_names(renderer, node, base_context): ...
register is a class method. A function can be registered several times by stacking
decorators. To change a single instance instead, use renderer.register_handler(classifier, function) (see Customizing one instance).
Source: BaseRenderer.register and
_route_registration.
Classifiers#
The string you register under is a classifier. Its form picks the table:
Classifier |
Registers |
Signature |
Returns |
|---|---|---|---|
|
the handler for that node type (replaces any inherited one) |
|
the rendered piece |
|
a rule run before the handler, in any mode |
|
the node (changed or replaced) |
|
a rule run before the handler when |
|
the node |
|
a rule run before the handler when |
|
the node |
|
a rule run on the handler’s result |
|
the rendered piece |
|
the header line of a block node |
|
a string ( |
|
an item added at the start or end of the render tree |
|
an item for the tree |
{type} is a node’s type string (calc_line, name, div_op, … see Nodes).
Anything else raises NotImplementedError when you register it.
Important
Rules must return the node (or, for post, the rendered piece). A rule that forgets
to return node replaces the node with None.
Handlers#
A handler renders one node and returns its piece of the render tree.
For a node with children, it renders them by calling renderer.render(child, base_context), which goes back through the rules and handlers for the child’s type:
@MyRenderer.register("div_op")
def render_div(renderer, node, base_context):
top = renderer.render(node.left, base_context)
bottom = renderer.render(node.right, base_context)
return f"{top} ÷ {bottom}"
Every type the parser can produce has a handler on BaseRenderer, so a subclass only
registers handlers for the types it renders differently. If a node reaches
render_node with no handler, it raises NotImplementedError naming the type.
Rules#
Rules adjust rendering without rewriting a handler. They are the right tool when the handler’s logic is fine but a detail differs:
prechanges the node before rendering in both modes: operator symbols, function names, delimiters (node.pre,node.post).symandnumchange the node for one mode only. The built-in renderers translate variable names inname:symrules, because a name’s identifier only shows in the symbolic step (the numeric step shows its value).postchanges the result: wrap it in markup, add a class, color it.
A rule can also read or set context fields, as the built-in
calc_line:pre rule does to decide whether a line is a parameter
(toggle_param_line).
Rules see each node twice#
A calculation renders its right-hand side once in sym mode and once in num mode, so
pre and post rules on the nodes inside it run twice on the same node object.
Make rules idempotent: set a value (node.symbol = "×") rather than build on the current
one (node.symbol += "×"), and look things up by node.type rather than by the current
symbol. The built-in rules follow this pattern; see
swap_operator_symbols.
Order of execution#
For each node, render_node runs:
{type}:prerules{type}:symor{type}:numrules (bycurrent_mode; neither when it is unset)the
{type}handler{type}:postrules
Within a category, rules run in registration order, and a subclass’s rule list starts
as a copy of its parent’s. So a subclass’s rules run after its parent’s for the same
classifier. To run before an inherited sym rule, register a pre rule instead (the
technique used in Symbols and functions).
Inheritance#
Each subclass gets its own copies of the three tables when it is defined
(__init_subclass__), so
registering on a subclass never changes its parent. Each instance copies its class’s
tables when it is created.
This means:
Register on your class before creating instances.
To remove an inherited rule, remove it from your class’s table:
MyRenderer.rule_handlers["name"]["sym"].remove(parent_rule).To call the parent’s handler from yours, look it up on the parent class:
@MyRenderer.register("constant") def render_constant(renderer, node, base_context): text = LatexRenderer.node_handlers["constant"](renderer, node, base_context) return f"\\mathbf{{{text}}}"
(A
postrule does the same job more simply; use this when you need to choose whether to call the parent at all. The Markdown renderer does this for strings,render_constant.)
The tables#
Class attribute |
Instance attribute |
Holds |
|---|---|---|
|
|
|
|
|
|
|
|
|
Next: The render context.