Building a renderer#

A renderer is a Python class. It inherits a complete, working rendering pipeline from BaseRenderer (or from one of the built-in renderers) and changes only what is different about its output format. This page gives you the mental model and a working renderer; the pages after it go into each part.

The mental model#

 your code ──run──▶ values
     │                │
     └──parse──▶ tree of nodes, each variable paired with its value
                      │
          renderer.render()   one handler per node type, plus rules around it
                      │
                      ▼
                render tree      nested lists of strings (or anything you return)
                      │
          renderer.complete()   join into the final output
                      │
                      ▼
                   output        str, bytes, a workbook, a list of dicts...

You customize the two middle steps:

  1. Handlers and rules decide what each node becomes. A handler renders one node type (calc_line, name, div_op, …). A rule runs before or after a handler to adjust the node or its result: swapping alpha for α, wrapping a result in a tag. You register both with a decorator.

  2. complete() turns the tree of rendered pieces into the final output. For text formats it calls join(), whose per-item method join_item() is where layout lives: newlines, indentation, wrapping blocks.

A first renderer#

This renderer produces plain text and changes one thing, how multiplication is shown:

from handcalcs.renderers import BaseRenderer


class TimesRenderer(BaseRenderer):
    name = "times"  # registers it, so %hc_default_renderer times works

    def complete(self, tree, base_context):
        return self.join(tree, base_context)  # nested lists -> text


@TimesRenderer.register("mult_op:pre")
def times_sign(renderer, node, base_context):
    node.symbol = "×"
    return node
>>> from handcalcs import HandCalcs
>>> print(HandCalcs(TimesRenderer())("a = 2\nb = a * 3\n"))
a = 2
b = a × 3 = 2 × 3 = 6

Everything else (calculation lines, parameter grids, conditionals, comment commands, number formats) comes from BaseRenderer.

Choosing a parent class#

Start from

When your format is

You get

BaseRenderer

something new

the full pipeline; Python-style symbols; plain text from join()

ASCIIRenderer

text with Unicode math

Greek letters, √, superscripts

LatexRenderer

anything that embeds LaTeX math

Greek, subscripts, fractions, operators; math groups

MarkdownRenderer

a Markdown dialect

the above, with Markdown structure

HTMLRenderer

HTML

<sub>/<sup>, entities, a CSS layout

Inheriting from the closest built-in renderer means less code, and a renderer that is shown correctly in Jupyter (see Publishing).

Go deeper#

Architecture

The full pipeline, step by step, with links to the source.

Architecture
Nodes

Every node type a renderer receives, and its attributes.

Nodes
Handlers and rules

register, classifiers, signatures and order of execution.

Handlers and rules
The render context

Global and line settings, current_mode, and the pitfalls.

The render context
Overridable methods

complete, join_item, create_context, the grid formatters, and when to override each.

Overridable methods
The render tree

The nested-list shapes join() understands.

The render tree

Worked examples#

Unicode text

Rules only: Unicode subscripts and math signs on top of ASCIIRenderer.

Example: a Unicode renderer
reStructuredText

Tokens, handlers and join_item on top of LatexRenderer.

Example: a reStructuredText renderer
Structured data

Output that is not text: new handlers and a new complete().

Example: structured data

Then: testing your renderer and publishing it.