Example: a Unicode renderer#

Technique: rules only. This renderer improves the plain-text output with Unicode subscripts, a centred dot for multiplication and proper comparison signs. It changes no handlers and no layout; it inherits everything from ASCIIRenderer and adds four rules.

The code#

"""
A plain-text renderer with nicer Unicode math: Unicode subscripts, a centred dot
for multiplication and proper comparison signs. Built from rules only.
"""
from handcalcs.renderers import ASCIIRenderer

SUBSCRIPTS = str.maketrans(
    "0123456789aehijklmnoprstuvx+-=()",
    "₀₁₂₃₄₅₆₇₈₉ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ₊₋₌₍₎",
)
COMPARISONS = {"lte_op": " ≤ ", "gte_op": " ≥ ", "neq_op": " ≠ ", "lt_op": " < ", "gt_op": " > "}


class UnicodeRenderer(ASCIIRenderer):
    name = "unicode"


@UnicodeRenderer.register("name:sym")
def unicode_subscripts(renderer, node, base_context):
    """``I_y`` -> ``Iᵧ`` when every character has a subscript form, else unchanged."""
    head, *subscripts = node.identifier.split("_")
    if not subscripts:
        return node
    tail = "".join(subscripts)
    if all(char.translate(SUBSCRIPTS) != char for char in tail):
        node.identifier = head + tail.translate(SUBSCRIPTS)
    return node


@UnicodeRenderer.register("mult_op:pre")
def centred_dot(renderer, node, base_context):
    node.symbol = "·"
    return node


@UnicodeRenderer.register("lte_op:pre")
@UnicodeRenderer.register("gte_op:pre")
@UnicodeRenderer.register("neq_op:pre")
@UnicodeRenderer.register("lt_op:pre")
@UnicodeRenderer.register("gt_op:pre")
def comparison_signs(renderer, node, base_context):
    node.symbol = COMPARISONS[node.type]
    return node

The result#

## Web check
x_1 = 51.7
alpha_max = x_1 * 2
F_y = 350
if x_1 <= 1100 / sqrt(F_y):
    web_class = 1
Output (UnicodeRenderer)
=========
Web check
=========

  x₁ = 51.7
αₘₐₓ = x₁ · 2 = 51.7 · 2 = 103.4
 F_y = 350
Since (x₁ ≤ 1100 / √(F_y)) -> (51.7 ≤ 1100 / √(350)) is True:
    web_class = 1

How it works#

unicode_subscripts is a name:sym rule, because a name’s identifier only appears in the symbolic step. It runs after the inherited swap_greeks rule (a subclass’s rules run after its parent’s), so alpha_max has already become α_max when it sees it. Unicode has no subscript form for some letters (y, w, b, …), so the rule leaves a name alone unless every character can be converted, which is why F_y keeps its underscore.

centred_dot and comparison_signs are pre rules, because operators appear in both the symbolic and the numeric step. They set node.symbol from fixed values, so running twice on the same node (once per step) is harmless. See Rules see each node twice.

The comparison signs include their own spaces because the compare handler joins its parts with no separator (a<b).

Try it#

  • Register a pow_op:pre rule that turns ** with a non-number exponent into ^.

  • Add a name:sym rule that turns a trailing _prime into ′.

Next: Example: a reStructuredText renderer.