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 |
Configuration, |
|
2. Class attributes |
A renderer’s fixed tokens: its stylesheet, math delimiters, symbol tables |
A subclass that overrides the attribute |
|
3. Rules and handlers |
How a particular kind of node is rendered |
A function registered with |
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
Beam check
Simply supported beam with a uniform load.
<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#
Every setting a renderer reads, and the four ways to set one.
CSS classes and custom properties, colors, dark mode, highlighting.
Math delimiters, headings, indentation, colors in math.
Add your own symbols, function names and operator symbols.
Change a single renderer object without a subclass.
Complete, tested recipes for common requests.
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.