From 08a8169586cc11bc645d97ecbbd4f8dbee0e65a8 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Thu, 30 Jul 2026 11:22:11 +0200 Subject: [PATCH] Math mode for left/right text added --- k8s/deployment.yaml | 2 +- web/app.py | 334 ++++++++++++++++++++++++--------------- web/templates/index.html | 2 + 3 files changed, 206 insertions(+), 132 deletions(-) diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index d74d175..90db510 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: e440-generator - image: git.baumann.gr/adebaumann/e440-generator:1.0 + image: git.baumann.gr/adebaumann/e440-generator:1.01 imagePullPolicy: Always ports: - containerPort: 5000 diff --git a/web/app.py b/web/app.py index 676860d..1e80ab7 100644 --- a/web/app.py +++ b/web/app.py @@ -11,62 +11,67 @@ app = Flask(__name__) VERSION = "1.0" -_LOGO_PATH = pathlib.Path(__file__).parent / 'static' / 'logo.png' +_LOGO_PATH = pathlib.Path(__file__).parent / "static" / "logo.png" _LOGO_B64 = base64.b64encode(_LOGO_PATH.read_bytes()).decode() -_FontData = namedtuple('_FontData', ['key', 'label', 'b64', 'glyph_set', 'cmap', 'hmtx', 'upm', 'glyf']) +_FontData = namedtuple( + "_FontData", ["key", "label", "b64", "glyph_set", "cmap", "hmtx", "upm", "glyf"] +) _FONT_LABELS = { - 'GOST_2.30481_type_A': 'GOST Type A', - 'GOST_2.30481_type_A_Bold': 'GOST Type A Bold', - 'Gost2.30481TypeB': 'GOST Type B', - 'ISOCPEUR': 'ISOCPEUR', - 'SIEMENS_GOST_Type_A': 'Siemens GOST Type A', - 'TGL_31034-1': 'TGL 31 034', - 'alte-din-1451-mittelschrift.regular': 'Alte DIN 1451 Mittelschrift', - 'alte-din-1451-mittelschrift.gepraegt': 'Alte DIN 1451 Mittelschrift (Gepraegt)', + "GOST_2.30481_type_A": "GOST Type A", + "GOST_2.30481_type_A_Bold": "GOST Type A Bold", + "Gost2.30481TypeB": "GOST Type B", + "ISOCPEUR": "ISOCPEUR", + "SIEMENS_GOST_Type_A": "Siemens GOST Type A", + "TGL_31034-1": "TGL 31 034", + "alte-din-1451-mittelschrift.regular": "Alte DIN 1451 Mittelschrift", + "alte-din-1451-mittelschrift.gepraegt": "Alte DIN 1451 Mittelschrift (Gepraegt)", } def _load_fonts(): - font_dir = pathlib.Path(__file__).parent / 'fonts' + font_dir = pathlib.Path(__file__).parent / "fonts" if not font_dir.exists(): - font_dir = pathlib.Path(__file__).parent.parent / 'fonts' + font_dir = pathlib.Path(__file__).parent.parent / "fonts" fonts = {} - for path in sorted(font_dir.glob('*.ttf')): + for path in sorted(font_dir.glob("*.ttf")): key = path.stem obj = TTFont(str(path)) fonts[key] = _FontData( key=key, - label=_FONT_LABELS.get(key, key.replace('_', ' ')), + label=_FONT_LABELS.get(key, key.replace("_", " ")), b64=base64.b64encode(path.read_bytes()).decode(), glyph_set=obj.getGlyphSet(), cmap=obj.getBestCmap(), - hmtx=obj['hmtx'].metrics, - upm=obj['head'].unitsPerEm, - glyf=obj['glyf'], + hmtx=obj["hmtx"].metrics, + upm=obj["head"].unitsPerEm, + glyf=obj["glyf"], ) return fonts _FONTS = _load_fonts() -_DEFAULT_FONT = 'alte-din-1451-mittelschrift.regular' +_DEFAULT_FONT = "alte-din-1451-mittelschrift.regular" # SVG absolute coordinate constants (1 user unit = 1 mm), pre-layer-translate # local frame — the wrapper applies translate(-60.717741,-107.14126). PIVOT_X, PIVOT_Y = 107.42845, 162.62819 -R0 = 43.842751 # rim / inner-tick radius -ANGLE_MIN, ANGLE_MAX = -45.667193, 45.667193 # degrees from vertical, clockwise-positive +R0 = 43.842751 # rim / inner-tick radius +ANGLE_MIN, ANGLE_MAX = ( + -45.667193, + 45.667193, +) # degrees from vertical, clockwise-positive -MAJOR_LEN, MAJOR_W = 4.7839203, 0.79658329 +MAJOR_LEN, MAJOR_W = 4.7839203, 0.79658329 MEDIUM_LEN, MEDIUM_W = 3.1215858, 0.37985462 -SMALL_LEN, SMALL_W = 1.8121802, 0.27478597 +SMALL_LEN, SMALL_W = 1.8121802, 0.27478597 LABEL_RADIUS = 49.862 UNIT_X, UNIT_Y = 107.03512, 136.64594 -LEFT_X, LEFT_Y0 = 73.432251, 145.16051 +LEFT_X, LEFT_Y0 = 73.432251, 145.16051 RIGHT_X, RIGHT_Y0 = 142.4485, 145.32773 CORNER_LINE_SPACING = 4.4586 CORNER_FONT_SIZE = 3.56685 @@ -89,12 +94,12 @@ BODY_D = ( # --- Inline markup for the unit label ------------------------------------- _SUP_SCALE = 0.62 -_SUP_RISE = 0.36 -_SUB_DROP = 0.14 +_SUP_RISE = 0.36 +_SUB_DROP = 0.14 _FRAC_SCALE = 0.62 -_FRAC_AXIS = 0.32 -_FRAC_GAP = 0.14 -_FRAC_SIDE = 0.12 +_FRAC_AXIS = 0.32 +_FRAC_GAP = 0.14 +_FRAC_SIDE = 0.12 _UNIT_BOX_X0, _UNIT_BOX_X1 = UNIT_X - 15.0, UNIT_X + 15.0 _UNIT_BOX_Y0, _UNIT_BOX_Y1 = UNIT_Y - 6.0, UNIT_Y + 6.0 @@ -117,7 +122,7 @@ def _glyph_svg(ch: str, x: float, y: float, fs: float, fill: str, fd: _FontData) d = pen.getCommands() if not d: return None, adv - t = f'translate({x:.5f},{y:.5f}) scale({s:.8f},{-s:.8f})' + t = f"translate({x:.5f},{y:.5f}) scale({s:.8f},{-s:.8f})" return f'', adv @@ -135,50 +140,50 @@ def _glyph_ybounds(ch: str, fd: _FontData): def _extract_braced(s: str, i: int): depth, j = 1, i + 1 while j < len(s) and depth: - if s[j] == '{': + if s[j] == "{": depth += 1 - elif s[j] == '}': + elif s[j] == "}": depth -= 1 if depth == 0: break j += 1 - return s[i + 1:j], j + 1 + return s[i + 1 : j], j + 1 def _parse_braced(inner: str): depth = 0 for k, ch in enumerate(inner): - if ch == '{': + if ch == "{": depth += 1 - elif ch == '}': + elif ch == "}": depth -= 1 - elif ch == '/' and depth == 0: - return ('frac', _parse_markup(inner[:k]), _parse_markup(inner[k + 1:])) - return ('group', _parse_markup(inner)) + elif ch == "/" and depth == 0: + return ("frac", _parse_markup(inner[:k]), _parse_markup(inner[k + 1 :])) + return ("group", _parse_markup(inner)) def _parse_atom(s: str, i: int): if i >= len(s): return [], i - if s[i] == '{': + if s[i] == "{": inner, j = _extract_braced(s, i) return [_parse_braced(inner)], j - return [('lit', s[i])], i + 1 + return [("lit", s[i])], i + 1 def _parse_markup(s: str): nodes, i = [], 0 while i < len(s): c = s[i] - if c in '^_': + if c in "^_": atom, i = _parse_atom(s, i + 1) - nodes.append(('sup' if c == '^' else 'sub', atom)) - elif c == '{': + nodes.append(("sup" if c == "^" else "sub", atom)) + elif c == "{": inner, j = _extract_braced(s, i) nodes.append(_parse_braced(inner)) i = j else: - nodes.append(('lit', c)) + nodes.append(("lit", c)) i += 1 return nodes @@ -186,17 +191,20 @@ def _parse_markup(s: str): def _measure(nodes, fs: float, fd: _FontData) -> float: w = 0.0 for n in nodes: - if n[0] == 'lit': + if n[0] == "lit": gn = fd.cmap.get(ord(n[1])) if gn: w += fd.hmtx.get(gn, (0, 0))[0] / fd.upm * fs - elif n[0] == 'group': + elif n[0] == "group": w += _measure(n[1], fs, fd) - elif n[0] in ('sup', 'sub'): + elif n[0] in ("sup", "sub"): w += _measure(n[1], fs * _SUP_SCALE, fd) - elif n[0] == 'frac': + elif n[0] == "frac": fs2 = fs * _FRAC_SCALE - w += max(_measure(n[1], fs2, fd), _measure(n[2], fs2, fd)) + 2 * _FRAC_SIDE * fs2 + w += ( + max(_measure(n[1], fs2, fd), _measure(n[2], fs2, fd)) + + 2 * _FRAC_SIDE * fs2 + ) return w @@ -213,18 +221,18 @@ def _frac_layout(n, fs: float, fd: _FontData): def _vbounds(nodes, fs: float, fd: _FontData): lo, hi = math.inf, -math.inf for n in nodes: - if n[0] == 'lit': + if n[0] == "lit": ymin, ymax = _glyph_ybounds(n[1], fd) a, b = -ymax * fs, -ymin * fs - elif n[0] == 'group': + elif n[0] == "group": a, b = _vbounds(n[1], fs, fd) - elif n[0] == 'sup': + elif n[0] == "sup": a, b = _vbounds(n[1], fs * _SUP_SCALE, fd) a, b = a - _SUP_RISE * fs, b - _SUP_RISE * fs - elif n[0] == 'sub': + elif n[0] == "sub": a, b = _vbounds(n[1], fs * _SUP_SCALE, fd) a, b = a + _SUB_DROP * fs, b + _SUB_DROP * fs - elif n[0] == 'frac': + elif n[0] == "frac": fs2, axis, num_base, den_base = _frac_layout(n, fs, fd) nt, nb = _vbounds(n[1], fs2, fd) dt, db = _vbounds(n[2], fs2, fd) @@ -239,27 +247,31 @@ def _vbounds(nodes, fs: float, fd: _FontData): def _draw_nodes(nodes, x: float, y: float, fs: float, fill: str, fd: _FontData): parts, cx = [], x for n in nodes: - if n[0] == 'lit': + if n[0] == "lit": p, adv = _glyph_svg(n[1], cx, y, fs, fill, fd) if p: parts.append(p) cx += adv - elif n[0] == 'group': + elif n[0] == "group": p, adv = _draw_nodes(n[1], cx, y, fs, fill, fd) parts += p cx += adv - elif n[0] in ('sup', 'sub'): + elif n[0] in ("sup", "sub"): fs2 = fs * _SUP_SCALE - y2 = y - _SUP_RISE * fs if n[0] == 'sup' else y + _SUB_DROP * fs + y2 = y - _SUP_RISE * fs if n[0] == "sup" else y + _SUB_DROP * fs p, adv = _draw_nodes(n[1], cx, y2, fs2, fill, fd) parts += p cx += adv - elif n[0] == 'frac': + elif n[0] == "frac": fs2, axis, num_base, den_base = _frac_layout(n, fs, fd) wn, wd = _measure(n[1], fs2, fd), _measure(n[2], fs2, fd) bar_w = max(wn, wd) + 2 * _FRAC_SIDE * fs2 - pn, _ = _draw_nodes(n[1], cx + (bar_w - wn) / 2, y + num_base, fs2, fill, fd) - pd, _ = _draw_nodes(n[2], cx + (bar_w - wd) / 2, y + den_base, fs2, fill, fd) + pn, _ = _draw_nodes( + n[1], cx + (bar_w - wn) / 2, y + num_base, fs2, fill, fd + ) + pd, _ = _draw_nodes( + n[2], cx + (bar_w - wd) / 2, y + den_base, fs2, fill, fd + ) parts += pn + pd ay = y + axis parts.append( @@ -270,7 +282,7 @@ def _draw_nodes(nodes, x: float, y: float, fs: float, fill: str, fd: _FontData): return parts, cx - x -def _markup_unit_svg(unit: str, fd: _FontData, fill='#1a1a1a') -> str: +def _markup_unit_svg(unit: str, fd: _FontData, fill="#1a1a1a") -> str: nodes = _parse_markup(unit) x0, x1 = _UNIT_BOX_X0, _UNIT_BOX_X1 y0, y1 = _UNIT_BOX_Y0, _UNIT_BOX_Y1 @@ -286,18 +298,40 @@ def _markup_unit_svg(unit: str, fd: _FontData, fill='#1a1a1a') -> str: w = _measure(nodes, fs, fd) parts, _ = _draw_nodes(nodes, xc - w / 2, UNIT_Y, fs, fill, fd) - return '\n'.join(parts) + return "\n".join(parts) -def _text_to_paths(text, x, y, font_size, fd: _FontData, anchor='start', fill='#1a1a1a', wrap_transform=None): +def _markup_corner_svg( + text: str, x: float, y: float, fd: _FontData, fill="#1a1a1a", anchor="start" +) -> str: + nodes = _parse_markup(text) + fs = CORNER_FONT_SIZE + w = _measure(nodes, fs, fd) + if anchor == "end": + x -= w + parts, _ = _draw_nodes(nodes, x, y, fs, fill, fd) + return "\n".join(parts) + + +def _text_to_paths( + text, + x, + y, + font_size, + fd: _FontData, + anchor="start", + fill="#1a1a1a", + wrap_transform=None, +): s = font_size / fd.upm - if anchor in ('middle', 'end'): + if anchor in ("middle", "end"): total_w = sum( fd.hmtx.get(fd.cmap.get(ord(ch)), (0, 0))[0] * s - for ch in text if fd.cmap.get(ord(ch)) + for ch in text + if fd.cmap.get(ord(ch)) ) - x -= total_w / 2 if anchor == 'middle' else total_w + x -= total_w / 2 if anchor == "middle" else total_w parts = [] cx = x @@ -309,11 +343,11 @@ def _text_to_paths(text, x, y, font_size, fd: _FontData, anchor='start', fill='# fd.glyph_set[gn].draw(pen) d = pen.getCommands() if d: - t = f'translate({cx:.5f},{y:.5f}) scale({s:.8f},{-s:.8f})' + t = f"translate({cx:.5f},{y:.5f}) scale({s:.8f},{-s:.8f})" parts.append(f'') cx += fd.hmtx.get(gn, (0, 0))[0] * s - inner = '\n'.join(parts) + inner = "\n".join(parts) if wrap_transform: return f'{inner}' return inner @@ -321,24 +355,40 @@ def _text_to_paths(text, x, y, font_size, fd: _FontData, anchor='start', fill='# def _fmt(v: float) -> str: if math.isinf(v): - return '∞' + return "∞" r = round(v) if abs(v - r) < 1e-9 * max(1.0, abs(v)): return str(int(r)) exp = math.floor(math.log10(abs(v))) decimals = max(0, 4 - 1 - exp) s = f"{v:.{decimals}f}" - return s.rstrip('0').rstrip('.') if '.' in s else s + return s.rstrip("0").rstrip(".") if "." in s else s -def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, small_ticks, - label_count, use_paths=True, scale_type='linear', custom_labels=None, - label_size=5.1126, font_key=None): - fd = _FONTS.get(font_key) or _FONTS.get(_DEFAULT_FONT) or next(iter(_FONTS.values())) +def generate_svg( + unit, + min_val, + max_val, + left_label, + right_label, + big_ticks, + small_ticks, + label_count, + use_paths=True, + scale_type="linear", + custom_labels=None, + label_size=5.1126, + font_key=None, +): + fd = ( + _FONTS.get(font_key) or _FONTS.get(_DEFAULT_FONT) or next(iter(_FONTS.values())) + ) - if scale_type == 'log': + if scale_type == "log": if min_val <= 0 or max_val <= 0: - raise ValueError("Logarithmic scale requires min and max values greater than 0.") + raise ValueError( + "Logarithmic scale requires min and max values greater than 0." + ) lmin, lmax = math.log(min_val), math.log(max_val) lspan = lmax - lmin if lspan == 0: @@ -350,7 +400,7 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma def major_value(i): return min_val * (max_val / min_val) ** (i / big_ticks) - elif scale_type == 'sqrt': + elif scale_type == "sqrt": if min_val < 0: raise ValueError("Square-root scale requires min value ≥ 0.") span = max_val - min_val @@ -363,12 +413,15 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma def major_value(i): return min_val + span * (i / big_ticks) ** 2 - elif scale_type == 'reciprocal': + elif scale_type == "reciprocal": if min_val <= 0: - raise ValueError("Reciprocal scale requires min and max values greater than 0.") + raise ValueError( + "Reciprocal scale requires min and max values greater than 0." + ) if not math.isinf(max_val) and max_val <= min_val: raise ValueError("Reciprocal scale requires max value greater than min.") if math.isinf(max_val): + def value_to_angle(v): if math.isinf(v): return ANGLE_MIN @@ -382,7 +435,9 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma k = 1.0 / min_val - 1.0 / max_val def value_to_angle(v): - return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (1.0 / v - 1.0 / max_val) / k + return ( + ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (1.0 / v - 1.0 / max_val) / k + ) def major_value(i): return 1.0 / (1.0 / max_val + i / big_ticks * k) @@ -411,7 +466,9 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma if label_count == 1: label_indices = [0] else: - label_indices = [round(k * big_ticks / (label_count - 1)) for k in range(label_count)] + label_indices = [ + round(k * big_ticks / (label_count - 1)) for k in range(label_count) + ] def _tick_line(angle_deg, length, width): x = PIVOT_X @@ -424,10 +481,10 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma ) font_face = ( - ' ' + "" ) parts = [ @@ -471,29 +528,37 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma medium_j = sub // 2 if small_ticks % 2 == 1 else None for i in range(big_ticks): for j in range(1, small_ticks + 1): - if scale_type == 'reciprocal': - angle = big_angles[i] + (j / sub) * (big_angles[i + 1] - big_angles[i]) + if scale_type == "reciprocal": + angle = big_angles[i] + (j / sub) * ( + big_angles[i + 1] - big_angles[i] + ) else: v = big_vals[i] + j * (big_vals[i + 1] - big_vals[i]) / sub angle = value_to_angle(v) - length, width = (MEDIUM_LEN, MEDIUM_W) if j == medium_j else (SMALL_LEN, SMALL_W) + length, width = ( + (MEDIUM_LEN, MEDIUM_W) if j == medium_j else (SMALL_LEN, SMALL_W) + ) parts.append(_tick_line(angle, length, width)) lx, ly = PIVOT_X, PIVOT_Y - LABEL_RADIUS for k, idx in enumerate(label_indices): angle = big_angles[idx] label = custom_labels[k] if custom_labels else _fmt(big_vals[idx]) - wrap = f'rotate({angle:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})' + wrap = f"rotate({angle:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})" if use_paths: - parts.append(' ' + _text_to_paths(label, lx, ly, label_size, fd, - anchor='middle', wrap_transform=wrap)) + parts.append( + " " + + _text_to_paths( + label, lx, ly, label_size, fd, anchor="middle", wrap_transform=wrap + ) + ) else: parts.append( - f' {escape(label)}' ) - parts.append(' ' + _markup_unit_svg(unit, fd)) + parts.append(" " + _markup_unit_svg(unit, fd)) left_lines = left_label.splitlines()[:2] right_lines = right_label.splitlines()[:2] @@ -502,64 +567,71 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma if not line: continue y = LEFT_Y0 + i * CORNER_LINE_SPACING - if use_paths: - parts.append(' ' + _text_to_paths(line, LEFT_X, y, CORNER_FONT_SIZE, fd, anchor='start')) - else: - parts.append( - f' {escape(line)}' - ) + parts.append(" " + _markup_corner_svg(line, LEFT_X, y, fd, anchor="start")) for i, line in enumerate(right_lines): if not line: continue y = RIGHT_Y0 + i * CORNER_LINE_SPACING - if use_paths: - parts.append(' ' + _text_to_paths(line, RIGHT_X, y, CORNER_FONT_SIZE, fd, anchor='end')) - else: - parts.append( - f' {escape(line)}' - ) + parts.append(" " + _markup_corner_svg(line, RIGHT_X, y, fd, anchor="end")) - parts += [' ', ''] - return '\n'.join(parts) + parts += [" ", ""] + return "\n".join(parts) -@app.route('/') +@app.route("/") def index(): fonts = list(_FONTS.values()) - return render_template('index.html', logo_b64=_LOGO_B64, version=VERSION, - fonts=fonts, default_font=_DEFAULT_FONT) + return render_template( + "index.html", + logo_b64=_LOGO_B64, + version=VERSION, + fonts=fonts, + default_font=_DEFAULT_FONT, + ) -@app.route('/generate', methods=['POST']) +@app.route("/generate", methods=["POST"]) def generate(): - unit = request.form.get('unit', '%') - left_label = request.form.get('left_label', '') - right_label = request.form.get('right_label', '') - use_paths = request.form.get('output_mode', 'paths') == 'paths' - scale_type = request.form.get('scale_type', 'linear') - font_key = request.form.get('font', _DEFAULT_FONT) - custom_labels = [s.strip() for s in request.form.get('labels', '').split(',') if s.strip()] or None + unit = request.form.get("unit", "%") + left_label = request.form.get("left_label", "") + right_label = request.form.get("right_label", "") + use_paths = request.form.get("output_mode", "paths") == "paths" + scale_type = request.form.get("scale_type", "linear") + font_key = request.form.get("font", _DEFAULT_FONT) + custom_labels = [ + s.strip() for s in request.form.get("labels", "").split(",") if s.strip() + ] or None try: - min_val = float(request.form.get('min', 0)) - max_val = float(request.form.get('max', 100)) - big_ticks = max(1, min(50, int(request.form.get('big_ticks', 10)))) - small_ticks = max(0, min(20, int(request.form.get('small_ticks', 4)))) - label_count = max(2, int(request.form.get('label_count', 6))) - label_size = max(0.5, min(25.0, float(request.form.get('label_size', 5.1126)))) - if request.form.get('max_is_inf') == '1': + min_val = float(request.form.get("min", 0)) + max_val = float(request.form.get("max", 100)) + big_ticks = max(1, min(50, int(request.form.get("big_ticks", 10)))) + small_ticks = max(0, min(20, int(request.form.get("small_ticks", 4)))) + label_count = max(2, int(request.form.get("label_count", 6))) + label_size = max(0.5, min(25.0, float(request.form.get("label_size", 5.1126)))) + if request.form.get("max_is_inf") == "1": max_val = math.inf - svg = generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, - small_ticks, label_count, use_paths, scale_type, custom_labels, - label_size, font_key) + svg = generate_svg( + unit, + min_val, + max_val, + left_label, + right_label, + big_ticks, + small_ticks, + label_count, + use_paths, + scale_type, + custom_labels, + label_size, + font_key, + ) except ValueError as err: - return Response(str(err), status=400, mimetype='text/plain') - return Response(svg, mimetype='image/svg+xml') + return Response(str(err), status=400, mimetype="text/plain") + return Response(svg, mimetype="image/svg+xml") -if __name__ == '__main__': +if __name__ == "__main__": app.run(debug=True) diff --git a/web/templates/index.html b/web/templates/index.html index 98a77f5..e52fef4 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -309,11 +309,13 @@ + Markup: ^ superscript · _ subscript · {fraction} · nests, e.g. V_{in}
+ Markup: ^ superscript · _ subscript · {fraction} · nests, e.g. V_{in}