Symbols and functions#

Every built-in renderer translates names, operators and function calls as it renders them: alpha becomes α, sqrt a radical. You can add your own translations.

Display symbols for names#

Most notation needs no code: name the variable as described in naming variables (N_pl_Rd, DeltaH, V_dot, M_star), or give it a display spec with the symbols option, which every built-in renderer reads:

import handcalcs
handcalcs.set_option("symbols", {"N_plRd": "N_{pl,Rd}", "V_flow": "dot(V)"})

The same option works for one calculation as a comment command:

# hc: symbols = {"N_plRd": "N_{pl,Rd}", "V_flow": "dot(V)"}
N_plRd = 1250
V_flow = 2.5
Output (MarkdownRenderer)
\[\begin{aligned} N_{\mathrm{pl,Rd}} &= 1250 \\ \dot{V} &= 2.5 \end{aligned}\]
$$\begin{aligned} N_{\mathrm{pl,Rd}} &= 1250 \\ \dot{V} &= 2.5 \end{aligned}$$

Subscript style and Greek prefixes#

Two render-context fields change how every renderer reads names: subscripts ("comma" or v1’s "nested") and greek_prefixes (False keeps DeltaH as written). Set them for a section with a comment command:

# hc: subscripts = "nested"; greek_prefixes = False
N_pl_Rd = 1250
DeltaH = 4.2
Output (MarkdownRenderer)
\[\begin{aligned} N_{\mathrm{pl}_{\mathrm{Rd}}} &= 1250 \\ \mathrm{DeltaH} &= 4.2 \end{aligned}\]
$$\begin{aligned} N_{\mathrm{pl}_{\mathrm{Rd}}} &= 1250 \\ \mathrm{DeltaH} &= 4.2 \end{aligned}$$

or for the session with handcalcs.set_option("subscripts", "nested") (see settings and the render context).

Custom symbols with a rule#

For markup the spec syntax can’t express, a rule registered as name:pre runs on every variable name before the renderer’s own translation. Replace the identifier with the symbol you want:

"""Custom symbols and function names for the LaTeX family of renderers."""
from handcalcs.renderers import MarkdownRenderer

# Markup the ``symbols`` option can't express: a tilde, a calligraphic letter.
SYMBOLS = {
    "u_t": "\\tilde{u}",
    "F_cal": "\\mathcal{F}",
}


class SymbolMarkdownRenderer(MarkdownRenderer):
    name = "symbol_markdown"
    MATH_FUNCS = MarkdownRenderer.MATH_FUNCS | {
        "ceil_to": "\\operatorname{ceil}",
    }


@SymbolMarkdownRenderer.register("name:pre")
def swap_custom_symbols(renderer, node, base_context):
    """Runs before the built-in 'name:sym' rule, which leaves LaTeX untouched."""
    if node.identifier in SYMBOLS:
        node.identifier = SYMBOLS[node.identifier]
    return node
u_t = 2.5
F_cal = u_t * 3
M_r = ceil_to(F_cal)
Output (SymbolMarkdownRenderer)
\[\begin{aligned} \tilde{u} &= 2.5 \\ \mathcal{F} &= \tilde{u} \cdot 3 = 2.5 \cdot 3 = 7.5 \\ M_{r} &= \operatorname{ceil}\left(\mathcal{F}\right) = \operatorname{ceil}\left(7.5\right) = 8 \end{aligned}\]
$$\begin{aligned} \tilde{u} &= 2.5 \\ \mathcal{F} &= \tilde{u} \cdot 3 = 2.5 \cdot 3 = 7.5 \\ M_{r} &= \operatorname{ceil}\left(\mathcal{F}\right) = \operatorname{ceil}\left(7.5\right) = 8 \end{aligned}$$

Why pre and not sym? The LaTeX family translates names in a name:sym rule (latex_identifier), and a subclass’s rules run after the inherited ones. A pre rule runs first, and latex_identifier leaves anything that is no longer a Python identifier (it contains a backslash, a brace, …) alone. See rule order.

The same rule works for the HTML and ASCII renderers, using HTML markup or Unicode in the replacement: {"u_t": "ũ"}. The HTML renderer also leaves markup such as ũ alone.

Function names#

The LaTeX and Markdown renderers look function names up in MATH_FUNCS. Extend it in a subclass, as SymbolMarkdownRenderer above does for ceil_to. A function that is not in the table is shown by name in \operatorname{...}.

For other renderers, register a function_call:pre rule and change the function name’s identifier (shown in the symbolic step) and value (shown in the substituted step):

        node.function_name.identifier = "ln"
        node.function_name.value = "ln"
    return node


class TimesHTMLRenderer(HTMLRenderer):
    """× instead of · for multiplication."""


@TimesHTMLRenderer.register("mult_op:pre")
x = 10
y = log(x)
Output (LnHTMLRenderer)
x = 10
y = ln(x) = ln(10) = 2.3026
<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;}</style><div class="hc-line">x = 10</div><div class="hc-line">y = ln(x) = ln(10) = <span class="hc-result">2.3026</span></div></div>

Indexes in brackets#

The LaTeX, Markdown, HTML and Word renderers show xs[0] as xs with subscript 0. To keep the brackets as written in the code, clear the node’s subscript labels in a subscript:pre rule:

from handcalcs.renderers import MarkdownRenderer


class BracketMarkdownRenderer(MarkdownRenderer):
    """xs[0] in brackets rather than as xs with subscript 0."""


@BracketMarkdownRenderer.register("subscript:pre")
def brackets(renderer, node, base_context):
    node.labels = None  # without subscript labels, the lookup is shown in brackets
    return node
M_Ed = [90, 120, 140]
M_max = M_Ed[2]
Output (BracketMarkdownRenderer)
\[\begin{aligned} M_{\mathrm{Ed}} &= [90, 120, 140] \\ M_{\mathrm{max}} &= M_{\mathrm{Ed}}[2] = 140 \end{aligned}\]
$$\begin{aligned} M_{\mathrm{Ed}} &= [90, 120, 140] \\ M_{\mathrm{max}} &= M_{\mathrm{Ed}}[2] = 140 \end{aligned}$$

Operator symbols#

The LaTeX and Markdown renderers look operator symbols up in OPERATOR_SYMBOLS, keyed by node type:

class TimesMarkdownRenderer(MarkdownRenderer):
    OPERATOR_SYMBOLS = MarkdownRenderer.OPERATOR_SYMBOLS | {"mult_op": "\\times"}

The node types are add_op, sub_op, mult_op, div_op, floor_op, modulo_op, pow_op, the comparisons lt_op, gt_op, lte_op, gte_op, eq_op, neq_op, in_op, not_in_op, is_op, is_not_op, and the boolean and_op, or_op. In the HTML and ASCII renderers, set node.symbol in a {type}:pre rule (as the built-in swap_py_operators rules do). A subclass’s rule runs after the inherited one, so it has the last word:

    return node


class NoGreekTextRenderer(ASCIIRenderer):
    """ASCII output with Greek names left as written."""


NoGreekTextRenderer.rule_handlers["name"]["sym"].remove(swap_greeks)
a = 2
b = 3
c = a * b
Output (TimesHTMLRenderer)
a = 2
b = 3
c = a × b = 2 × 3 = 6
<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;}</style><div class="hc-line">a = 2</div><div class="hc-line">b = 3</div><div class="hc-line">c = a × b = 2 × 3 = <span class="hc-result">6</span></div></div>

Turning off a built-in translation#

A subclass inherits its parent’s rules. To stop, for example, Greek letter translation in the ASCII renderer, remove the rule from the subclass’s rule list:

from handcalcs.renderers import ASCIIRenderer
from handcalcs.renderers.ascii import swap_greeks

class NoGreekTextRenderer(ASCIIRenderer):
    pass

NoGreekTextRenderer.rule_handlers["name"]["sym"].remove(swap_greeks)
alpha = 2
beta = alpha * 3
Output (NoGreekTextRenderer)
alpha = 2
 beta = alpha * 3 = 2 * 3 = 6

Each subclass gets its own copy of the rule lists, so this doesn’t affect ASCIIRenderer itself. In the HTML renderer, one rule, swap_identifier in handcalcs.renderers.html, does all the name typesetting (Greek letters, subscripts, accents); the LaTeX family’s is swap_identifier in handcalcs.renderers.latex.

Next: Customizing one instance.