Customizing a renderer#

The built-in renderers are designed to be adjusted. Most changes take a few lines and fall into one of three levers, from lightest to most powerful.

Lever

What it changes

How

Example

1. Settings

Values the renderer reads while rendering: number format, the = symbol, indentation, which steps are shown

Configuration, # hc: commands, render_context=

# hc: format_code = ".2f"

2. Class attributes

A renderer’s fixed tokens: its stylesheet, math delimiters, symbol tables

A subclass that overrides the attribute

STYLE = HTMLRenderer.STYLE + "..."

3. Rules and handlers

How a particular kind of node is rendered

A function registered with @Renderer.register(...)

color every result

A first customization#

Subclass a renderer, override an attribute, register a rule. This HTML renderer colors headings and highlights each calculated result:

"""An HTMLRenderer with a color theme and highlighted results."""
from handcalcs.renderers import HTMLRenderer


class ThemedHTMLRenderer(HTMLRenderer):
    name = "themed_html"

    # Extra rules appended to the built-in stylesheet. They use :where() too, so
    # a host page can still override them.
    STYLE = HTMLRenderer.STYLE + (
        ".handcalcs{--hc-accent:#0f766e;--hc-muted:#6b7280;--hc-result-bg:#ccfbf1;}"
        "@media (prefers-color-scheme:dark){.handcalcs{"
        "--hc-accent:#5eead4;--hc-muted:#9ca3af;--hc-result-bg:#134e4a;}}"
        ".handcalcs :where(h1,h2,h3,h4,h5,h6){color:var(--hc-accent);}"
        ".handcalcs :where(.hc-comment){color:var(--hc-muted);font-style:italic;}"
        ".handcalcs :where(.hc-header){color:var(--hc-accent);font-weight:600;}"
        ".handcalcs :where(.hc-result){background:var(--hc-result-bg);"
        "padding:0 0.25em;border-radius:3px;font-weight:600;}"
    )


@ThemedHTMLRenderer.register("calc_line:post")
def highlight_result(renderer, rendered, node, base_context):
    """Wrap the result of a one-line calculation in <span class="hc-result">."""
    if not isinstance(rendered, list):
        return rendered  # a multi-line (# hc: -m) grid is already one string
    assign = base_context.current.assign
    positions = [i for i, part in enumerate(rendered) if part == assign]
    if len(positions) < 2:
        return rendered  # a parameter (target = value): leave inputs plain
    result = positions[-1] + 1
    rendered[result] = f'<span class="hc-result">{rendered[result]}</span>'
    return rendered
## Beam check
# Simply supported beam with a uniform load.
w = 5  # kN/m
L = 6  # m
M = w * L**2 / 8  # kN m
Output (ThemedHTMLRenderer)

Beam check

Simply supported beam with a uniform load.

w = 5 (kN/m)
L = 6 (m)
M = w · L2 / 8 = 5 · 62 / 8 = 22.5 (kN m)
<div class="handcalcs"><style>.handcalcs{--hc-accent:#0B5F66;--hc-accent-dark:#91CFD4;--hc-guide-1:#85B8BD;--hc-guide-2:#D5B386;--hc-guide-3:#A8B0C7;}:is([data-jp-theme-light="false"],.vscode-dark,.vscode-high-contrast,[data-theme="dark"]) .handcalcs{--hc-accent-text:var(--hc-accent-dark);--hc-tint:color-mix(in srgb,var(--hc-accent) 30%,transparent);--hc-highlight:color-mix(in srgb,var(--hc-accent) 38%,transparent);}.handcalcs{--hc-gap:0.35em;--hc-indent:1.5em;--hc-param-gap:2em;--hc-eq-gap:0.3em;--hc-accent-text:var(--hc-accent);--hc-tint:color-mix(in srgb,var(--hc-accent) 9%,transparent);--hc-highlight:color-mix(in srgb,var(--hc-accent) 12%,transparent);--hc-muted:color-mix(in srgb,currentColor 60%,transparent);--hc-chip:color-mix(in srgb,currentColor 6%,transparent);display:flex;flex-direction:column;gap:var(--hc-gap);line-height:1.6;font-variant-numeric:tabular-nums;}.handcalcs :where(.hc-block,.hc-body){display:flex;flex-direction:column;gap:var(--hc-gap);}.handcalcs :where(.hc-body){margin-inline-start:0.55em;padding-inline-start:calc(var(--hc-indent) - 0.55em);border-inline-start:2px solid var(--hc-guide);--hc-guide:var(--hc-guide-1);}.handcalcs :where(.hc-body .hc-body){--hc-guide:var(--hc-guide-2);}.handcalcs :where(.hc-body .hc-body .hc-body){--hc-guide:var(--hc-guide-3);}.handcalcs :where(.hc-body .hc-body .hc-body .hc-body){--hc-guide:var(--hc-guide-1);}.handcalcs :where(.hc-body .hc-body .hc-body .hc-body .hc-body){--hc-guide:var(--hc-guide-2);}.handcalcs :where(.hc-line,.hc-header,.hc-comment){margin-block:0;}.handcalcs :where(.hc-header){background:var(--hc-tint);border-radius:4px;padding:0.1em 0.5em;}.handcalcs :where(.hc-header)::before{content:"\25B8";color:var(--hc-accent-text);margin-inline-end:0.4em;}.handcalcs :where(.hc-kw){color:var(--hc-accent-text);}.handcalcs :where(.hc-result){color:var(--hc-accent-text);background:var(--hc-highlight);font-weight:600;padding:0 0.25em;border-radius:3px;-webkit-box-decoration-break:clone;box-decoration-break:clone;}.handcalcs :where(.hc-note){color:var(--hc-muted);}.handcalcs :where(.hc-check){font-weight:700;}.handcalcs :where(.hc-pass){color:#1A7F37;}.handcalcs :where(.hc-fail){color:#CF222E;}.handcalcs :where(.hc-import){align-self:start;font-family:var(--jp-code-font-family,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);font-size:0.85em;color:var(--hc-muted);background:var(--hc-chip);padding:0.05em 0.5em;border-radius:3px;}.handcalcs :where(.hc-comment,.hc-note) :where(code){font-size:0.9em;background:var(--hc-chip);padding:0 0.25em;border-radius:3px;}.handcalcs table.hc-params{margin:0;border:0;border-collapse:separate;border-spacing:0;table-layout:auto;width:auto;align-self:center;font-size:inherit;color:inherit;font-variant-numeric:tabular-nums;background:var(--hc-tint);border-radius:4px;padding:0.1em 0.6em;}.handcalcs table.hc-params.hc-multiline{align-self:start;background:none;border-radius:0;padding:0 0 0 0.5em;border-inline-start:2px solid var(--hc-accent-text);}.handcalcs table.hc-params tr:nth-child(n){background:none;}.handcalcs table.hc-params td{padding:0;border:0;vertical-align:baseline;line-height:inherit;white-space:normal;max-width:none;}.handcalcs table.hc-params td.hc-param-id{text-align:right;padding-inline-end:var(--hc-eq-gap);}.handcalcs table.hc-params td.hc-param-eq{text-align:center;}.handcalcs table.hc-params td.hc-param-val{text-align:left;padding-inline-start:var(--hc-eq-gap);}.handcalcs table.hc-params td.hc-param-gap{width:var(--hc-param-gap);}.handcalcs table.hc-params td.hc-param-comment{text-align:left;padding-inline-start:var(--hc-eq-gap);}.handcalcs :where(.hc-pre){white-space:pre;}.handcalcs :where(.hc-matrix){display:inline-table;vertical-align:middle;border-inline:1px solid currentColor;border-radius:0.2em;padding:0 0.15em;}.handcalcs :where(.hc-matrix-row){display:table-row;}.handcalcs :where(.hc-matrix-cell){display:table-cell;text-align:right;padding:0 0.3em;line-height:1.2;}.handcalcs{--hc-accent:#0f766e;--hc-muted:#6b7280;--hc-result-bg:#ccfbf1;}@media (prefers-color-scheme:dark){.handcalcs{--hc-accent:#5eead4;--hc-muted:#9ca3af;--hc-result-bg:#134e4a;}}.handcalcs :where(h1,h2,h3,h4,h5,h6){color:var(--hc-accent);}.handcalcs :where(.hc-comment){color:var(--hc-muted);font-style:italic;}.handcalcs :where(.hc-header){color:var(--hc-accent);font-weight:600;}.handcalcs :where(.hc-result){background:var(--hc-result-bg);padding:0 0.25em;border-radius:3px;font-weight:600;}</style><h1>Beam check</h1><p class="hc-comment">Simply supported beam with a uniform load.</p><div class="hc-line">w = 5 <span class="hc-note">(kN/m)</span></div><div class="hc-line">L = 6 <span class="hc-note">(m)</span></div><div class="hc-line">M = w · L<sup>2</sup> / 8 = 5 · 6<sup>2</sup> / 8 = <span class="hc-result"><span class="hc-result">22.5</span></span> <span class="hc-note">(kN m)</span></div></div>

Use it like any renderer: HandCalcs(ThemedHTMLRenderer()). Because it declares a name, it can also be chosen in Jupyter with %hc_default_renderer themed_html, and it is displayed as HTML because it inherits from HTMLRenderer.

Go deeper#

Settings and the render context

Every setting a renderer reads, and the four ways to set one.

Settings and the render context
Styling the HTML output

CSS classes and custom properties, colors, dark mode, highlighting.

Styling the HTML output
LaTeX and Markdown tokens

Math delimiters, headings, indentation, colors in math.

LaTeX and Markdown tokens
Symbols and functions

Add your own symbols, function names and operator symbols.

Symbols and functions
Customizing one instance

Change a single renderer object without a subclass.

Customizing one instance
Cookbook

Complete, tested recipes for common requests.

Cookbook

When a change goes beyond adjusting (a new output format, or a different structure), build a renderer instead. The same three levers are the whole of the renderer API, so nothing here is wasted.