HTML renderer#

HTMLRenderer produces a self-contained HTML fragment: a <div class="handcalcs"> with its own small, scoped stylesheet. It needs no math engine, so it works in any web page, email or app.

from handcalcs.renderers import HTMLRenderer
## Bolt group
# Shear is shared equally between the bolts.
n = 4  # bolts
V_f = 120  # kN
V_b = V_f / n
f = 1; g = 2; h = 4
if V_b <= 40:
    status = "OK"
Output (HTMLRenderer)

Bolt group

Shear is shared equally between the bolts.

n = 4 (bolts)
Vf = 120 (kN)
Vb = Vf / n = 120 / 4 = 30
f=1g=2h=4
Since (Vb ≤ 40) → (30 ≤ 40) is True:
status = “OK”
<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><h1>Bolt group</h1><p class="hc-comment">Shear is shared equally between the bolts.</p><div class="hc-line">n = 4 <span class="hc-note">(bolts)</span></div><div class="hc-line">V<sub>f</sub> = 120 <span class="hc-note">(kN)</span></div><div class="hc-line">V<sub>b</sub> = V<sub>f</sub> / n = 120 / 4 = <span class="hc-result">30</span></div><table class="hc-params"><tr><td class="hc-param-id">f</td><td class="hc-param-eq">=</td><td class="hc-param-val">1</td><td class="hc-param-comment"></td><td class="hc-param-gap"></td><td class="hc-param-id">g</td><td class="hc-param-eq">=</td><td class="hc-param-val">2</td><td class="hc-param-comment"></td><td class="hc-param-gap"></td><td class="hc-param-id">h</td><td class="hc-param-eq">=</td><td class="hc-param-val">4</td><td class="hc-param-comment"></td></tr></table><div class="hc-block"><div class="hc-header"><span class="hc-kw">Since</span> (V<sub>b</sub> &le; 40) → (30 &le; 40) <span class="hc-kw">is True:</span></div><div class="hc-body"><div class="hc-line">status = “OK”</div></div></div></div>

A fuller example#

Headings and prose, a parameter grid, a long equation broken with -m, a governing branch, and a pass/fail check, all in one calculation:

## Steel beam
# Lateral-torsional buckling resistance of a simply supported span.
# hc: -h -c 3
E = 200000     # MPa
G = 77000      # MPa
L = 6000       # mm
I_y = 160e6    # mm⁴
J = 133e3      # mm⁴
C_w = 200e9    # mm⁶
### Elastic critical moment
M_u = pi / L * sqrt(E * I_y * G * J + (pi * E / L)**2 * I_y * C_w)  # hc: -m
M_p = 285e6    # N mm
if M_u > 0.67 * M_p:
    M_r = 1.15 * 0.9 * M_p * (1 - 0.28 * M_p / M_u)  # hc: -m
else:
    M_r = 0.9 * M_u
M_f = 210e6    # N mm
M_f <= M_r     # hc: --check
Output (HTMLRenderer)

Steel beam

Lateral-torsional buckling resistance of a simply supported span.

E=200000(MPa)G=77000(MPa)L=6000(mm)
Iy=1.6e+08(mm⁴)J=1.33e+05(mm⁴)Cw=2e+11(mm⁶)

Elastic critical moment

Mu = π / L · √(E · Iy · G · J + (π · E / L)2 · Iy · Cw) = 3.1416 / 6000 · √(200000 · 1.6e+08 · 77000 · 1.33e+05 + (3.1416 · 200000 / 6000)2 · 1.6e+08 · 2e+11) = 4.3134e+08
Mp = 2.85e+08 (N mm)
Since (Mu > 0.67 · Mp) → (4.3134e+08 > 0.67 · 2.85e+08) is True:
Mr = 1.15 · 0.9 · Mp · (1 - 0.28 · Mp / Mu) = 1.15 · 0.9 · 2.85e+08 · (1 - 0.28 · 2.85e+08 / 4.3134e+08) = 2.404e+08
Mf = 2.1e+08 (N mm)
Mf ≤ Mr = 2.1e+08 ≤ 2.404e+08 ✓
<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><h1>Steel beam</h1><p class="hc-comment">Lateral-torsional buckling resistance of a simply supported span.</p><table class="hc-params"><tr><td class="hc-param-id">E</td><td class="hc-param-eq">=</td><td class="hc-param-val">200000</td><td class="hc-param-comment"><span class="hc-note">(MPa)</span></td><td class="hc-param-gap"></td><td class="hc-param-id">G</td><td class="hc-param-eq">=</td><td class="hc-param-val">77000</td><td class="hc-param-comment"><span class="hc-note">(MPa)</span></td><td class="hc-param-gap"></td><td class="hc-param-id">L</td><td class="hc-param-eq">=</td><td class="hc-param-val">6000</td><td class="hc-param-comment"><span class="hc-note">(mm)</span></td></tr><tr><td class="hc-param-id">I<sub>y</sub></td><td class="hc-param-eq">=</td><td class="hc-param-val">1.6e+08</td><td class="hc-param-comment"><span class="hc-note">(mm⁴)</span></td><td class="hc-param-gap"></td><td class="hc-param-id">J</td><td class="hc-param-eq">=</td><td class="hc-param-val">1.33e+05</td><td class="hc-param-comment"><span class="hc-note">(mm⁴)</span></td><td class="hc-param-gap"></td><td class="hc-param-id">C<sub>w</sub></td><td class="hc-param-eq">=</td><td class="hc-param-val">2e+11</td><td class="hc-param-comment"><span class="hc-note">(mm⁶)</span></td></tr></table><h2>Elastic critical moment</h2><div class="hc-line">M<sub>u</sub> = π / L · √(E · I<sub>y</sub> · G · J + (π · E / L)<sup>2</sup> · I<sub>y</sub> · C<sub>w</sub>) = 3.1416 / 6000 · √(200000 · 1.6e+08 · 77000 · 1.33e+05 + (3.1416 · 200000 / 6000)<sup>2</sup> · 1.6e+08 · 2e+11) = <span class="hc-result">4.3134e+08</span></div><div class="hc-line">M<sub>p</sub> = 2.85e+08 <span class="hc-note">(N mm)</span></div><div class="hc-block"><div class="hc-header"><span class="hc-kw">Since</span> (M<sub>u</sub> &gt; 0.67 · M<sub>p</sub>) → (4.3134e+08 &gt; 0.67 · 2.85e+08) <span class="hc-kw">is True:</span></div><div class="hc-body"><div class="hc-line">M<sub>r</sub> = 1.15 · 0.9 · M<sub>p</sub> · (1 - 0.28 · M<sub>p</sub> / M<sub>u</sub>) = 1.15 · 0.9 · 2.85e+08 · (1 - 0.28 · 2.85e+08 / 4.3134e+08) = <span class="hc-result">2.404e+08</span></div></div></div><div class="hc-line">M<sub>f</sub> = 2.1e+08 <span class="hc-note">(N mm)</span></div><div class="hc-line">M<sub>f</sub> &le; M<sub>r</sub> = 2.1e+08 &le; 2.404e+08 <span class="hc-check hc-pass">✓</span></div></div>

Using the output#

Insert the string anywhere in a page. It inherits the page’s font and color, and its styles never override yours (see Styling the HTML output).

from handcalcs import HandCalcs
from handcalcs.renderers import HTMLRenderer

fragment = HandCalcs(HTMLRenderer())(source)
page = f"<html><body><main>{fragment}</main></body></html>"

For a page of its own, give a destination. The file is a complete HTML page (<!DOCTYPE html>, a <head> with the character set and a title, the calculation in the <body>), from the renderer’s preamble and postamble:

HandCalcs(HTMLRenderer())(source, destination="beam.html")

In Jupyter, %hc_default_renderer html displays it with IPython.display.HTML.

Output structure#

<div class="handcalcs">
  <style>/* scoped rules */</style>
  <h1>Bolt group</h1>
  <p class="hc-comment">Shear is shared equally between the bolts.</p>
  <div class="hc-line">n = 4 (bolts)</div>
  <table class="hc-params">...</table>
  <div class="hc-block">
    <div class="hc-header">Since ... is True:</div>
    <div class="hc-body">
      <div class="hc-line">status = "OK"</div>
    </div>
  </div>
</div>

Class

Element

handcalcs

The container. Holds the CSS custom properties.

hc-line

One rendered line (a calculation, parameter or import).

hc-comment

A paragraph of comment lines.

hc-block, hc-header, hc-body

An if/for block: its header line and its indented body.

hc-params

A parameter grid (a <table>); cells are hc-param-id, hc-param-eq, hc-param-val, hc-param-comment, with hc-param-gap spacers.

hc-multiline

Added to the table of a # hc: -m calculation.

hc-pre

Pre-formatted text (whitespace kept).

Headings are <h1> (for ##) to <h6>. The headings and text sizes come from the host page.

Conventions#

  • Names: Greek letters as Unicode characters; _ makes a <sub> subscript, and further subscripts follow after a comma (N<sub>pl,Rd</sub>; nested with the subscripts option); __ is a space, ___ a literal underscore. hat, bar, dot, ddot, prime and star add a hat, an overbar, a dot, two dots, a prime or a superscript * to the part before them. All of this is one name:sym rule, swap_identifier. See naming variables.

  • Operators: * → ·, ** → <sup>, sqrt → √, <= → ≤.

  • Numbers use the format code as-is (1.2e+05 stays as written). Strings are shown in curly quotes and HTML-escaped.

Source#

src/handcalcs/renderers/html.py