about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/docutils/utils/math
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/docutils/utils/math')
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/__init__.py73
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/latex2mathml.py1252
-rwxr-xr-x.venv/lib/python3.12/site-packages/docutils/utils/math/math2html.py3165
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/mathalphabet2unichar.py892
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/mathml_elements.py478
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py261
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/tex2unichar.py730
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/unichar2tex.py808
8 files changed, 7659 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/__init__.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/__init__.py
new file mode 100644
index 00000000..2ad43b42
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/__init__.py
@@ -0,0 +1,73 @@
+# :Id: $Id: __init__.py 9516 2024-01-15 16:11:08Z milde $
+# :Author: Guenter Milde.
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+"""
+This is the Docutils (Python Documentation Utilities) "math" sub-package.
+
+It contains various modules for conversion between different math formats
+(LaTeX, MathML, HTML).
+
+:math2html:    LaTeX math -> HTML conversion from eLyXer
+:latex2mathml: LaTeX math -> presentational MathML
+:unichar2tex:  Unicode character to LaTeX math translation table
+:tex2unichar:  LaTeX math to Unicode character translation dictionaries
+:mathalphabet2unichar:  LaTeX math alphabets to Unicode character translation
+:tex2mathml_extern: Wrapper for 3rd party TeX -> MathML converters
+"""
+
+# helpers for Docutils math support
+# =================================
+
+
+class MathError(ValueError):
+    """Exception for math syntax and math conversion errors.
+
+    The additional attribute `details` may hold a list of Docutils
+    nodes suitable as children for a ``<system_message>``.
+    """
+    def __init__(self, msg, details=[]):
+        super().__init__(msg)
+        self.details = details
+
+
+def toplevel_code(code):
+    """Return string (LaTeX math) `code` with environments stripped out."""
+    chunks = code.split(r'\begin{')
+    return r'\begin{'.join(chunk.split(r'\end{')[-1]
+                           for chunk in chunks)
+
+
+def pick_math_environment(code, numbered=False):
+    """Return the right math environment to display `code`.
+
+    The test simply looks for line-breaks (``\\``) outside environments.
+    Multi-line formulae are set with ``align``, one-liners with
+    ``equation``.
+
+    If `numbered` evaluates to ``False``, the "starred" versions are used
+    to suppress numbering.
+    """
+    if toplevel_code(code).find(r'\\') >= 0:
+        env = 'align'
+    else:
+        env = 'equation'
+    if not numbered:
+        env += '*'
+    return env
+
+
+def wrap_math_code(code, as_block):
+    # Wrap math-code in mode-switching TeX command/environment.
+    # If `as_block` is True, use environment for displayed equation(s).
+    if as_block:
+        env = pick_math_environment(code)
+        return '\\begin{%s}\n%s\n\\end{%s}' % (env, code, env)
+    return '$%s$' % code
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/latex2mathml.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/latex2mathml.py
new file mode 100644
index 00000000..b6ca3934
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/latex2mathml.py
@@ -0,0 +1,1252 @@
+# :Id: $Id: latex2mathml.py 9536 2024-02-01 13:04:22Z milde $
+# :Copyright: © 2005 Jens Jørgen Mortensen [1]_
+#             © 2010, 2021, 2024 Günter Milde.
+#
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+#
+# .. [1] the original `rst2mathml.py` in `sandbox/jensj/latex_math`
+
+"""Convert LaTex maths code into presentational MathML.
+
+This module is provisional:
+the API is not settled and may change with any minor Docutils version.
+"""
+
+# Usage:
+#
+# >>> from latex2mathml import *
+
+import re
+import unicodedata
+
+from docutils.utils.math import (MathError, mathalphabet2unichar,
+                                 tex2unichar, toplevel_code)
+from docutils.utils.math.mathml_elements import (
+    math, mtable, mrow, mtr, mtd, menclose, mphantom, msqrt, mi, mn, mo,
+    mtext, msub, msup, msubsup, munder, mover, munderover, mroot, mfrac,
+    mspace, MathRow)
+
+
+# Character data
+# --------------
+
+# LaTeX math macro to Unicode mappings.
+# Character categories.
+
+# identifiers -> <mi>
+
+letters = {'hbar': 'ℏ'}  # Compatibility mapping: \hbar resembles italic ħ
+#                          "unicode-math" unifies \hbar and \hslash to ℏ.
+letters.update(tex2unichar.mathalpha)
+
+ordinary = tex2unichar.mathord  # Miscellaneous symbols
+
+# special case: Capital Greek letters: (upright in TeX style)
+greek_capitals = {
+    'Phi': '\u03a6', 'Xi': '\u039e', 'Sigma': '\u03a3',
+    'Psi': '\u03a8', 'Delta': '\u0394', 'Theta': '\u0398',
+    'Upsilon': '\u03d2', 'Pi': '\u03a0', 'Omega': '\u03a9',
+    'Gamma': '\u0393', 'Lambda': '\u039b'}
+
+# functions -> <mi>
+functions = {
+    # functions with a space in the name
+    'liminf': 'lim\u202finf',
+    'limsup': 'lim\u202fsup',
+    'injlim': 'inj\u202flim',
+    'projlim': 'proj\u202flim',
+    # embellished function names (see handle_cmd() below)
+    'varlimsup': 'lim',
+    'varliminf': 'lim',
+    'varprojlim': 'lim',
+    'varinjlim': 'lim',
+    # custom function name
+    'operatorname': None,
+}
+functions.update((name, name) for name in
+                 ('arccos', 'arcsin', 'arctan', 'arg',  'cos',
+                  'cosh',   'cot',    'coth',   'csc',  'deg',
+                  'det',    'dim',    'exp',    'gcd',  'hom',
+                  'ker',    'lg',     'ln',     'log',  'Pr',
+                  'sec',    'sin',    'sinh',   'tan',  'tanh'))
+# Function with limits: 'lim', 'sup', 'inf', 'max', 'min':
+# use <mo> to allow "movablelimits" attribute (see below).
+
+# modulo operator/arithmetic
+modulo_functions = {
+    # cmdname: (binary, named, parentheses, padding)
+    'bmod': (True,  True,  False, '0.278em'),  # a mod n
+    'pmod': (False, True,  True,  '0.444em'),  # a  (mod n)
+    'mod':  (False, True,  False, '0.667em'),  # a  mod n
+    'pod':  (False, False, True,  '0.444em'),  # a  (n)
+    }
+
+
+# "mathematical alphabets": map identifiers to the corresponding
+# characters from the "Mathematical Alphanumeric Symbols" block
+math_alphabets = {
+    # 'cmdname':  'mathvariant value'        # package
+    'mathbb':     'double-struck',           # amssymb
+    'mathbf':     'bold',
+    'mathbfit':   'bold-italic',             # isomath
+    'mathcal':    'script',
+    'mathfrak':   'fraktur',                 # amssymb
+    'mathit':     'italic',
+    'mathrm':     'normal',
+    'mathscr':    'script',                  # mathrsfs et al
+    'mathsf':     'sans-serif',
+    'mathbfsfit': 'sans-serif-bold-italic',  # unicode-math
+    'mathsfbfit': 'sans-serif-bold-italic',  # isomath
+    'mathsfit':   'sans-serif-italic',       # isomath
+    'mathtt':     'monospace',
+    # unsupported: bold-fraktur
+    #              bold-script
+    #              bold-sans-serif
+}
+
+# operator, fence, or separator -> <mo>
+
+stretchables = {
+    # extensible delimiters allowed in left/right cmds
+    'backslash':   '\\',
+    'uparrow':     '\u2191',  # ↑ UPWARDS ARROW
+    'downarrow':   '\u2193',  # ↓ DOWNWARDS ARROW
+    'updownarrow': '\u2195',  # ↕ UP DOWN ARROW
+    'Uparrow':     '\u21d1',  # ⇑ UPWARDS DOUBLE ARROW
+    'Downarrow':   '\u21d3',  # ⇓ DOWNWARDS DOUBLE ARROW
+    'Updownarrow': '\u21d5',  # ⇕ UP DOWN DOUBLE ARROW
+    'lmoustache':  '\u23b0',  # ⎰ … CURLY BRACKET SECTION
+    'rmoustache':  '\u23b1',  # ⎱ … LEFT CURLY BRACKET SECTION
+    'arrowvert':   '\u23d0',  # ⏐ VERTICAL LINE EXTENSION
+    'bracevert':   '\u23aa',  # ⎪ CURLY BRACKET EXTENSION
+    'lvert':      '|',        # left  |
+    'lVert':      '\u2016',   # left  ‖
+    'rvert':      '|',        # right |
+    'rVert':      '\u2016',   # right ‖
+    'Arrowvert':  '\u2016',   # ‖
+}
+stretchables.update(tex2unichar.mathfence)
+stretchables.update(tex2unichar.mathopen)   # Braces
+stretchables.update(tex2unichar.mathclose)  # Braces
+
+# >>> print(' '.join(sorted(set(stretchables.values()))))
+# [ \ ] { | } ‖ ↑ ↓ ↕ ⇑ ⇓ ⇕ ⌈ ⌉ ⌊ ⌋ ⌜ ⌝ ⌞ ⌟ ⎪ ⎰ ⎱ ⏐ ⟅ ⟆ ⟦ ⟧ ⟨ ⟩ ⟮ ⟯ ⦇ ⦈
+
+operators = {
+    # negated symbols without pre-composed Unicode character
+    'nleqq':      '\u2266\u0338',  # ≦̸
+    'ngeqq':      '\u2267\u0338',  # ≧̸
+    'nleqslant':  '\u2a7d\u0338',  # ⩽̸
+    'ngeqslant':  '\u2a7e\u0338',  # ⩾̸
+    'ngtrless':   '\u2277\u0338',  # txfonts
+    'nlessgtr':   '\u2276\u0338',  # txfonts
+    'nsubseteqq': '\u2AC5\u0338',  # ⫅̸
+    'nsupseteqq': '\u2AC6\u0338',  # ⫆̸
+    # compatibility definitions:
+    'centerdot': '\u2B1D',  # BLACK VERY SMALL SQUARE | mathbin
+    'varnothing': '\u2300',  # ⌀ DIAMETER SIGN | empty set
+    'varpropto': '\u221d',  # ∝ PROPORTIONAL TO | sans serif
+    'triangle': '\u25B3',  # WHITE UP-POINTING TRIANGLE | mathord
+    'triangledown': '\u25BD',  # WHITE DOWN-POINTING TRIANGLE | mathord
+    # alias commands:
+    'dotsb': '\u22ef',  # ⋯ with binary operators/relations
+    'dotsc': '\u2026',  # … with commas
+    'dotsi': '\u22ef',  # ⋯ with integrals
+    'dotsm': '\u22ef',  # ⋯ multiplication dots
+    'dotso': '\u2026',  # … other dots
+    # functions with movable limits (requires <mo>)
+    'lim': 'lim',
+    'sup': 'sup',
+    'inf': 'inf',
+    'max': 'max',
+    'min': 'min',
+}
+operators.update(tex2unichar.mathbin)    # Binary symbols
+operators.update(tex2unichar.mathrel)    # Relation symbols, arrow symbols
+operators.update(tex2unichar.mathpunct)  # Punctuation
+operators.update(tex2unichar.mathop)     # Variable-sized symbols
+operators.update(stretchables)
+
+
+# special cases
+
+thick_operators = {
+    # style='font-weight: bold;'
+    'thicksim':       '\u223C',  # ∼
+    'thickapprox':    '\u2248',  # ≈
+}
+
+small_operators = {
+    # mathsize='75%'
+    'shortmid':       '\u2223',  # ∣
+    'shortparallel':  '\u2225',  # ∥
+    'nshortmid':      '\u2224',  # ∤
+    'nshortparallel': '\u2226',  # ∦
+    'smallfrown':     '\u2322',  # ⌢ FROWN
+    'smallsmile':     '\u2323',  # ⌣ SMILE
+    'smallint':       '\u222b',  # ∫ INTEGRAL
+}
+
+# Operators and functions with limits above/below in display formulas
+# and in index position inline (movablelimits=True)
+movablelimits = ('bigcap', 'bigcup', 'bigodot', 'bigoplus', 'bigotimes',
+                 'bigsqcup', 'biguplus', 'bigvee', 'bigwedge',
+                 'coprod', 'intop', 'ointop', 'prod', 'sum',
+                 'lim', 'max', 'min', 'sup', 'inf')
+# Depending on settings, integrals may also be in this category.
+# (e.g. if "amsmath" is loaded with option "intlimits", see
+#  http://mirror.ctan.org/macros/latex/required/amsmath/amsldoc.pdf)
+# movablelimits.extend(('fint', 'iiiint', 'iiint', 'iint', 'int', 'oiint',
+#                       'oint', 'ointctrclockwise', 'sqint',
+#                       'varointclockwise',))
+
+# horizontal space -> <mspace>
+
+spaces = {'qquad':         '2em',        # two \quad
+          'quad':          '1em',        # 18 mu
+          'thickspace':    '0.2778em',   # 5mu = 5/18em
+          ';':             '0.2778em',   # 5mu thickspace
+          ' ':             '0.25em',     # inter word space
+          '\n':            '0.25em',     # inter word space
+          'medspace':      '0.2222em',   # 4mu = 2/9em
+          ':':             '0.2222em',   # 4mu medspace
+          'thinspace':     '0.1667em',   # 3mu = 1/6em
+          ',':             '0.1667em',   # 3mu thinspace
+          'negthinspace':  '-0.1667em',  # -3mu = -1/6em
+          '!':             '-0.1667em',  # negthinspace
+          'negmedspace':   '-0.2222em',  # -4mu = -2/9em
+          'negthickspace': '-0.2778em',  # -5mu = -5/18em
+          }
+
+# accents: -> <mo stretchy="false"> in <mover>
+accents = {
+    # TeX:      spacing    combining
+    'acute':    '´',     # '\u0301'
+    'bar':      'ˉ',     # '\u0304'
+    'breve':    '˘',     # '\u0306'
+    'check':    'ˇ',     # '\u030C'
+    'dot':      '˙',     # '\u0307'
+    'ddot':     '¨',     # '\u0308'
+    'dddot':    '˙˙˙',   # '\u20DB'  # or … ?
+    'ddddot':   '˙˙˙˙',  # '\u20DC'  # or ¨¨ ?
+    'grave':    '`',     # '\u0300'
+    'hat':      'ˆ',     # '\u0302'
+    'mathring': '˚',     # '\u030A'
+    'tilde':    '~',     # '\u0303'  # tilde ~ or small tilde ˜?
+    'vec':      '→',     # '\u20d7'  # → too heavy, use scriptlevel="+1"
+}
+
+# limits etc. -> <mo> in <mover> or <munder>
+over = {
+    # TeX:                  (char,     offset-correction/em)
+    'overbrace':            ('\u23DE', -0.2),  # DejaVu Math -0.6
+    'overleftarrow':        ('\u2190', -0.2),
+    'overleftrightarrow':   ('\u2194', -0.2),
+    'overline':             ('_',      -0.2),  # \u2012 does not stretch
+    'overrightarrow':       ('\u2192', -0.2),
+    'widehat':              ('^',      -0.5),
+    'widetilde':            ('~',      -0.3),
+}
+under = {'underbrace':          ('\u23DF',  0.1),  # DejaVu Math -0.7
+         'underleftarrow':      ('\u2190', -0.2),
+         'underleftrightarrow': ('\u2194', -0.2),
+         'underline':           ('_',      -0.8),
+         'underrightarrow':     ('\u2192', -0.2),
+         }
+
+# Character translations
+# ----------------------
+# characters with preferred alternative in mathematical use
+# cf. https://www.w3.org/TR/MathML3/chapter7.html#chars.anomalous
+anomalous_chars = {'-': '\u2212',  # HYPHEN-MINUS -> MINUS SIGN
+                   ':': '\u2236',  # COLON -> RATIO
+                   '~': '\u00a0',  # NO-BREAK SPACE
+                   }
+
+# blackboard bold (Greek characters not working with "mathvariant" (Firefox 78)
+mathbb = {'Γ': '\u213E',    # ℾ
+          'Π': '\u213F',    # ℿ
+          'Σ': '\u2140',    # ⅀
+          'γ': '\u213D',    # ℽ
+          'π': '\u213C',    # ℼ
+          }
+
+# Matrix environments
+matrices = {
+    # name:    fences
+    'matrix':  ('', ''),
+    'smallmatrix':  ('', ''),  # smaller, see begin_environment()!
+    'pmatrix': ('(', ')'),
+    'bmatrix': ('[', ']'),
+    'Bmatrix': ('{', '}'),
+    'vmatrix': ('|', '|'),
+    'Vmatrix': ('\u2016', '\u2016'),  # ‖
+    'aligned': ('', ''),
+    'cases':   ('{', ''),
+}
+
+layout_styles = {
+    'displaystyle':      {'displaystyle': True,  'scriptlevel': 0},
+    'textstyle':         {'displaystyle': False, 'scriptlevel': 0},
+    'scriptstyle':       {'displaystyle': False, 'scriptlevel': 1},
+    'scriptscriptstyle': {'displaystyle': False, 'scriptlevel': 2},
+    }
+# See also https://www.w3.org/TR/MathML3/chapter3.html#presm.scriptlevel
+
+fractions = {
+    # name:   attributes
+    'frac':   {},
+    'cfrac':  {'displaystyle': True,  'scriptlevel': 0,
+               'class': 'cfrac'},  # in LaTeX with padding
+    'dfrac':  layout_styles['displaystyle'],
+    'tfrac':  layout_styles['textstyle'],
+    'binom':  {'linethickness': 0},
+    'dbinom': layout_styles['displaystyle'] | {'linethickness': 0},
+    'tbinom': layout_styles['textstyle'] | {'linethickness': 0},
+}
+
+delimiter_sizes = ['', '1.2em', '1.623em', '2.047em', '2.470em']
+bigdelimiters = {'left':  0,
+                 'right': 0,
+                 'bigl':  1,
+                 'bigr':  1,
+                 'Bigl':  2,
+                 'Bigr':  2,
+                 'biggl': 3,
+                 'biggr': 3,
+                 'Biggl': 4,
+                 'Biggr': 4,
+                 }
+
+
+# LaTeX to MathML translation
+# ---------------------------
+
+# auxiliary functions
+# ~~~~~~~~~~~~~~~~~~~
+
+def tex_cmdname(string):
+    """Return leading TeX command name and remainder of `string`.
+
+    >>> tex_cmdname('mymacro2') # up to first non-letter
+    ('mymacro', '2')
+    >>> tex_cmdname('name 2') # strip trailing whitespace
+    ('name', '2')
+    >>> tex_cmdname('_2') # single non-letter character
+    ('_', '2')
+
+    """
+    m = re.match(r'([a-zA-Z]+)[ \n]*(.*)', string, re.DOTALL)
+    if m is None:
+        m = re.match(r'(.?)(.*)', string, re.DOTALL)
+    return m.group(1), m.group(2)
+
+
+# Test:
+#
+# >>> tex_cmdname('name\nnext') # strip trailing whitespace, also newlines
+# ('name', 'next')
+# >>> tex_cmdname('name_2') # first non-letter terminates
+# ('name', '_2')
+# >>> tex_cmdname('name_2\nnext line') # line-break allowed
+# ('name', '_2\nnext line')
+# >>> tex_cmdname(' next') # leading whitespace is returned
+# (' ', 'next')
+# >>> tex_cmdname('1 2') # whitespace after non-letter is kept
+# ('1', ' 2')
+# >>> tex_cmdname('1\n2\t3') # whitespace after non-letter is kept
+# ('1', '\n2\t3')
+# >>> tex_cmdname('') # empty string
+# ('', '')
+
+
+def tex_number(string):
+    """Return leading number literal and remainder of `string`.
+
+    >>> tex_number('123.4')
+    ('123.4', '')
+
+    """
+    m = re.match(r'([0-9.,]*[0-9]+)(.*)', string, re.DOTALL)
+    if m is None:
+        return '', string
+    return m.group(1), m.group(2)
+
+
+# Test:
+#
+# >>> tex_number(' 23.4b') # leading whitespace -> no number
+# ('', ' 23.4b')
+# >>> tex_number('23,400/2') # comma separator included
+# ('23,400', '/2')
+# >>> tex_number('23. 4/2') # trailing separator not included
+# ('23', '. 4/2')
+# >>> tex_number('4, 2') # trailing separator not included
+# ('4', ', 2')
+# >>> tex_number('1 000.4')
+# ('1', ' 000.4')
+
+
+def tex_token(string):
+    """Return first simple TeX token and remainder of `string`.
+
+    >>> tex_token('\\command{without argument}')
+    ('\\command', '{without argument}')
+    >>> tex_token('or first character')
+    ('o', 'r first character')
+
+    """
+    m = re.match(r"""((?P<cmd>\\[a-zA-Z]+)\s* # TeX command, skip whitespace
+                      |(?P<chcmd>\\.)          # one-character TeX command
+                      |(?P<ch>.?))            # first character (or empty)
+                     (?P<remainder>.*$)    # remaining part of string
+                 """, string, re.VERBOSE | re.DOTALL)
+    cmd, chcmd, ch, remainder = m.group('cmd', 'chcmd', 'ch', 'remainder')
+    return cmd or chcmd or ch, remainder
+
+# Test:
+#
+# >>> tex_token('{opening bracket of group}')
+# ('{', 'opening bracket of group}')
+# >>> tex_token('\\skip whitespace after macro name')
+# ('\\skip', 'whitespace after macro name')
+# >>> tex_token('. but not after single char')
+# ('.', ' but not after single char')
+# >>> tex_token('') # empty string.
+# ('', '')
+# >>> tex_token('\{escaped bracket')
+# ('\\{', 'escaped bracket')
+
+
+def tex_group(string):
+    """Return first TeX group or token and remainder of `string`.
+
+    >>> tex_group('{first group} returned without brackets')
+    ('first group', ' returned without brackets')
+
+    """
+    split_index = 0
+    nest_level = 0   # level of {{nested} groups}
+    escape = False   # the next character is escaped (\)
+
+    if not string.startswith('{'):
+        # special case: there is no group, return first token and remainder
+        return string[:1], string[1:]
+    for c in string:
+        split_index += 1
+        if escape:
+            escape = False
+        elif c == '\\':
+            escape = True
+        elif c == '{':
+            nest_level += 1
+        elif c == '}':
+            nest_level -= 1
+        if nest_level == 0:
+            break
+    else:
+        raise MathError('Group without closing bracket!')
+    return string[1:split_index-1], string[split_index:]
+
+
+# >>> tex_group('{} empty group')
+# ('', ' empty group')
+# >>> tex_group('{group with {nested} group} ')
+# ('group with {nested} group', ' ')
+# >>> tex_group('{group with {nested group}} at the end')
+# ('group with {nested group}', ' at the end')
+# >>> tex_group('{{group} {with {{complex }nesting}} constructs}')
+# ('{group} {with {{complex }nesting}} constructs', '')
+# >>> tex_group('{group with \\{escaped\\} brackets}')
+# ('group with \\{escaped\\} brackets', '')
+# >>> tex_group('{group followed by closing bracket}} from outer group')
+# ('group followed by closing bracket', '} from outer group')
+# >>> tex_group('No group? Return first character.')
+# ('N', 'o group? Return first character.')
+# >>> tex_group(' {also whitespace}')
+# (' ', '{also whitespace}')
+
+
+def tex_token_or_group(string):
+    """Return first TeX group or token and remainder of `string`.
+
+    >>> tex_token_or_group('\\command{without argument}')
+    ('\\command', '{without argument}')
+    >>> tex_token_or_group('first character')
+    ('f', 'irst character')
+    >>> tex_token_or_group(' also whitespace')
+    (' ', 'also whitespace')
+    >>> tex_token_or_group('{first group} keep rest')
+    ('first group', ' keep rest')
+
+    """
+    arg, remainder = tex_token(string)
+    if arg == '{':
+        arg, remainder = tex_group(string.lstrip())
+    return arg, remainder
+
+# >>> tex_token_or_group('\{no group but left bracket')
+# ('\\{', 'no group but left bracket')
+
+
+def tex_optarg(string):
+    """Return optional argument and remainder.
+
+    >>> tex_optarg('[optional argument] returned without brackets')
+    ('optional argument', ' returned without brackets')
+    >>> tex_optarg('{empty string, if there is no optional arg}')
+    ('', '{empty string, if there is no optional arg}')
+
+    """
+    m = re.match(r"""\s*                            # leading whitespace
+                 \[(?P<optarg>(\\]|[^\[\]]|\\])*)\] # [group] without nested groups
+                 (?P<remainder>.*$)
+                 """, string, re.VERBOSE | re.DOTALL)
+    if m is None and not string.startswith('['):
+        return '', string
+    try:
+        return m.group('optarg'), m.group('remainder')
+    except AttributeError:
+        raise MathError(f'Could not extract optional argument from "{string}"!')
+
+# Test:
+# >>> tex_optarg(' [optional argument] after whitespace')
+# ('optional argument', ' after whitespace')
+# >>> tex_optarg('[missing right bracket')
+# Traceback (most recent call last):
+#     ...
+# docutils.utils.math.MathError: Could not extract optional argument from "[missing right bracket"!
+# >>> tex_optarg('[group with [nested group]]')
+# Traceback (most recent call last):
+#     ...
+# docutils.utils.math.MathError: Could not extract optional argument from "[group with [nested group]]"!
+
+
+def parse_latex_math(root, source):
+    """Append MathML conversion of `string` to `node` and return it.
+
+    >>> parse_latex_math(math(), r'\alpha')
+    math(mi('α'))
+    >>> parse_latex_math(mrow(), r'x_{n}')
+    mrow(msub(mi('x'), mi('n')))
+
+    """
+    # Normalize white-space:
+    string = source  # not-yet handled part of source
+    node = root  # the current "insertion point"
+
+    # Loop over `string` while changing it.
+    while len(string) > 0:
+        # Take off first character:
+        c, string = string[0], string[1:]
+
+        if c in ' \n':
+            continue  # whitespace is ignored in LaTeX math mode
+        if c == '\\':  # start of a LaTeX macro
+            cmdname, string = tex_cmdname(string)
+            node, string = handle_cmd(cmdname, node, string)
+        elif c in "_^":
+            node = handle_script_or_limit(node, c)
+        elif c == '{':
+            if isinstance(node, MathRow) and node.nchildren == 1:
+                # LaTeX takes one arg, MathML node accepts a group
+                node.nchildren = None  # allow appending until closed by '}'
+            else:  # wrap group in an <mrow>
+                new_node = mrow()
+                node.append(new_node)
+                node = new_node
+        elif c == '}':
+            node = node.close()
+        elif c == '&':
+            new_node = mtd()
+            node.close().append(new_node)
+            node = new_node
+        elif c.isalpha():
+            node = node.append(mi(c))
+        elif c.isdigit():
+            number, string = tex_number(string)
+            node = node.append(mn(c+number))
+        elif c in anomalous_chars:
+            # characters with a special meaning in LaTeX math mode
+            # fix spacing before "unary" minus.
+            attributes = {}
+            if c == '-' and len(node):
+                previous_node = node[-1]
+                if (previous_node.text and previous_node.text in '([='
+                    or previous_node.get('class') == 'mathopen'):
+                    attributes['form'] = 'prefix'
+            node = node.append(mo(anomalous_chars[c], **attributes))
+        elif c in "/()[]|":
+            node = node.append(mo(c, stretchy=False))
+        elif c in "+*=<>,.!?`';@":
+            node = node.append(mo(c))
+        else:
+            raise MathError(f'Unsupported character: "{c}"!')
+            # TODO: append as <mi>?
+        if node is None:
+            if not string:
+                return root  # ignore unbalanced braces
+            raise MathError(f'No insertion point for "{string}". '
+                            f'Unbalanced braces in "{source[:-len(string)]}"?')
+    if node.nchildren and len(node) < node.nchildren:
+        raise MathError('Last node missing children. Source incomplete?')
+    return root
+
+# Test:
+
+# >>> parse_latex_math(math(), '')
+# math()
+# >>> parse_latex_math(math(), ' \\sqrt{ \\alpha}')
+# math(msqrt(mi('α')))
+# >>> parse_latex_math(math(), '23.4x')
+# math(mn('23.4'), mi('x'))
+# >>> parse_latex_math(math(), '\\sqrt 2 \\ne 3')
+# math(msqrt(mn('2')), mo('≠'), mn('3'))
+# >>> parse_latex_math(math(), '\\sqrt{2 + 3} < 10')
+# math(msqrt(mn('2'), mo('+'), mn('3'), nchildren=3), mo('<'), mn('10'))
+# >>> parse_latex_math(math(), '\\sqrt[3]{2 + 3}')
+# math(mroot(mrow(mn('2'), mo('+'), mn('3'), nchildren=3), mn('3')))
+# >>> parse_latex_math(math(), '\max_x') # function takes limits
+# math(munder(mo('max', movablelimits='true'), mi('x')))
+# >>> parse_latex_math(math(), 'x^j_i') # ensure correct order: base, sub, sup
+# math(msubsup(mi('x'), mi('i'), mi('j')))
+# >>> parse_latex_math(math(), '\int^j_i') # ensure correct order
+# math(msubsup(mo('∫'), mi('i'), mi('j')))
+# >>> parse_latex_math(math(), 'x_{\\alpha}')
+# math(msub(mi('x'), mi('α')))
+# >>> parse_latex_math(math(), 'x_\\text{in}')
+# math(msub(mi('x'), mtext('in')))
+# >>> parse_latex_math(math(), '2⌘')
+# Traceback (most recent call last):
+# docutils.utils.math.MathError: Unsupported character: "⌘"!
+# >>> parse_latex_math(math(), '23}x')  # doctest: +ELLIPSIS
+# Traceback (most recent call last):
+# ...
+# docutils.utils.math.MathError: ... Unbalanced braces in "23}"?
+# >>> parse_latex_math(math(), '\\frac{2}')
+# Traceback (most recent call last):
+# ...
+# docutils.utils.math.MathError: Last node missing children. Source incomplete?
+
+
+def handle_cmd(name, node, string):  # noqa: C901 TODO make this less complex
+    """Process LaTeX command `name` followed by `string`.
+
+    Append result to `node`.
+    If needed, parse `string` for command argument.
+    Return new current node and remainder of `string`:
+
+    >>> handle_cmd('hbar', math(), r' \frac')
+    (math(mi('ℏ')), ' \\frac')
+    >>> handle_cmd('hspace', math(), r'{1ex} (x)')
+    (math(mspace(width='1ex')), ' (x)')
+
+    """
+
+    # Token elements
+    # ==============
+
+    # identifier  ->  <mi>
+
+    if name in letters:
+        new_node = mi(letters[name])
+        if name in greek_capitals:
+            # upright in "TeX style" but MathML sets them italic ("ISO style").
+            # CSS styling does not change the font style in Firefox 78.
+            # Use 'mathvariant="normal"'?
+            new_node.set('class', 'capital-greek')
+        node = node.append(new_node)
+        return node, string
+
+    if name in ordinary:
+        # <mi mathvariant="normal"> well supported by Chromium but
+        # Firefox 115.5.0 puts additional space around the symbol, e.g.
+        # <mi mathvariant="normal">∂</mi><mi>t</mi> looks like ∂ t, not ∂t
+        # return node.append(mi(ordinary[name], mathvariant='normal')), string
+        return node.append(mi(ordinary[name])), string
+
+    if name in functions:
+        # use <mi> followed by invisible function applicator character
+        # (see https://www.w3.org/TR/MathML3/chapter3.html#presm.mi)
+        if name == 'operatorname':
+            # custom function name, e.g. ``\operatorname{abs}(x)``
+            # TODO: \operatorname* -> with limits
+            arg, string = tex_token_or_group(string)
+            new_node = mi(arg, mathvariant='normal')
+        else:
+            new_node = mi(functions[name])
+        # embellished function names:
+        if name == 'varliminf':    # \underline\lim
+            new_node = munder(new_node, mo('_'))
+        elif name == 'varlimsup':  # \overline\lim
+            new_node = mover(new_node, mo('¯'), accent=False)
+        elif name == 'varprojlim':  # \underleftarrow\lim
+            new_node = munder(new_node, mo('\u2190'))
+        elif name == 'varinjlim':  # \underrightarrow\lim
+            new_node = munder(new_node, mo('\u2192'))
+
+        node = node.append(new_node)
+        # add ApplyFunction when appropriate (not \sin^2(x), say)
+        # cf. https://www.w3.org/TR/MathML3/chapter3.html#presm.mi
+        if string and string[0] not in ('^', '_'):
+            node = node.append(mo('\u2061'))  # &ApplyFunction;
+        return node, string
+
+    if name in modulo_functions:
+        (binary, named, parentheses, padding) = modulo_functions[name]
+        if binary:
+            node = node.append(mo('mod', lspace=padding, rspace=padding))
+            return node, string
+        # left padding
+        if node.in_block():
+            padding = '1em'
+        node = node.append(mspace(width=padding))
+        if parentheses:
+            node = node.append(mo('(', stretchy=False))
+        if named:
+            node = node.append(mi('mod'))
+            node = node.append(mspace(width='0.333em'))
+        arg, string = tex_token_or_group(string)
+        node = parse_latex_math(node, arg)
+        if parentheses:
+            node = node.append(mo(')', stretchy=False))
+        return node, string
+
+    # font changes or mathematical alphanumeric characters
+
+    if name in ('boldsymbol', 'pmb'):  # \pmb is "poor mans bold"
+        new_node = mrow(CLASS='boldsymbol')
+        node.append(new_node)
+        return new_node, string
+
+    if name in math_alphabets:
+        return handle_math_alphabet(name, node, string)
+
+    # operator, fence, or separator  ->  <mo>
+
+    if name == 'colon':  # trailing punctuation, not binary relation
+        node = node.append(mo(':', form='postfix', lspace='0', rspace='0.28em'))
+        return node, string
+
+    if name == 'idotsint':  # AMS shortcut for ∫︀···∫︀
+        node = parse_latex_math(node, r'\int\dotsi\int')
+        return node, string
+
+    if name in thick_operators:
+        node = node.append(mo(thick_operators[name], style='font-weight: bold'))
+        return node, string
+
+    if name in small_operators:
+        node = node.append(mo(small_operators[name], mathsize='75%'))
+        return node, string
+
+    if name in operators:
+        attributes = {}
+        if name in movablelimits and string and string[0] in ' _^':
+            attributes['movablelimits'] = True
+        elif name in ('lvert', 'lVert'):
+            attributes['class'] = 'mathopen'
+        node = node.append(mo(operators[name], **attributes))
+        return node, string
+
+    if name in bigdelimiters:
+        delimiter_attributes = {}
+        size = delimiter_sizes[bigdelimiters[name]]
+        delimiter, string = tex_token_or_group(string)
+        if delimiter not in '()[]/|.':
+            try:
+                delimiter = stretchables[delimiter.lstrip('\\')]
+            except KeyError:
+                raise MathError(f'Unsupported "\\{name}" delimiter '
+                                f'"{delimiter}"!')
+        if size:
+            delimiter_attributes['maxsize'] = size
+            delimiter_attributes['minsize'] = size
+            delimiter_attributes['symmetric'] = True
+        if name == 'left' or name.endswith('l'):
+            row = mrow()
+            node.append(row)
+            node = row
+        if delimiter != '.':  # '.' stands for "empty delimiter"
+            node.append(mo(delimiter, **delimiter_attributes))
+        if name == 'right' or name.endswith('r'):
+            node = node.close()
+        return node, string
+
+    if name == 'not':
+        # negation: LaTeX just overlays next symbol with "/".
+        arg, string = tex_token(string)
+        if arg == '{':
+            return node, '{\\not ' + string
+        if arg.startswith('\\'):  # LaTeX macro
+            try:
+                arg = operators[arg[1:]]
+            except KeyError:
+                raise MathError(rf'"\not" cannot negate: "{arg}"!')
+        arg = unicodedata.normalize('NFC', arg+'\u0338')
+        node = node.append(mo(arg))
+        return node, string
+
+    # arbitrary text (usually comments)  ->  <mtext>
+    if name in ('text', 'mbox', 'textrm'):
+        arg, string = tex_token_or_group(string)
+        parts = arg.split('$')  # extract inline math
+        for i, part in enumerate(parts):
+            if i % 2 == 0:  # i is even
+                # LaTeX keeps whitespace in, e.g., ``\text{ foo }``,
+                # <mtext> displays only internal whitespace.
+                # → replace marginal whitespace with NBSP
+                part = re.sub('(^[ \n]|[ \n]$)', '\u00a0', part)
+                node = node.append(mtext(part))
+            else:
+                parse_latex_math(node, part)
+        return node, string
+
+    # horizontal space -> <mspace>
+    if name in spaces:
+        node = node.append(mspace(width='%s'%spaces[name]))
+        return node, string
+
+    if name in ('hspace', 'mspace'):
+        arg, string = tex_group(string)
+        if arg.endswith('mu'):
+            # unit "mu" (1mu=1/18em) not supported by MathML
+            arg = '%sem' % (float(arg[:-2])/18)
+        node = node.append(mspace(width='%s'%arg))
+        return node, string
+
+    if name == 'phantom':
+        new_node = mphantom()
+        node.append(new_node)
+        return new_node, string
+
+    if name == 'boxed':
+        # CSS padding is broken in Firefox 115.6.0esr
+        # therefore we still need the deprecated <menclose> element
+        new_node = menclose(notation='box', CLASS='boxed')
+        node.append(new_node)
+        return new_node, string
+
+    # Complex elements (Layout schemata)
+    # ==================================
+
+    if name == 'sqrt':
+        radix, string = tex_optarg(string)
+        if radix:
+            indexnode = mrow()
+            new_node = mroot(indexnode, switch=True)
+            parse_latex_math(indexnode, radix)
+            indexnode.close()
+        else:
+            new_node = msqrt()
+        node.append(new_node)
+        return new_node, string
+
+    if name in fractions:
+        attributes = fractions[name]
+        if name == 'cfrac':
+            optarg, string = tex_optarg(string)
+            optargs = {'l': 'left', 'r': 'right'}
+            if optarg in optargs:
+                attributes = attributes.copy()
+                attributes['numalign'] = optargs[optarg]  # "numalign" is deprecated
+                attributes['class'] += ' numalign-' + optargs[optarg]
+        new_node = frac = mfrac(**attributes)
+        if name.endswith('binom'):
+            new_node = mrow(mo('('), new_node, mo(')'), CLASS='binom')
+            new_node.nchildren = 3
+        node.append(new_node)
+        return frac, string
+
+    if name == '\\':  # end of a row
+        entry = mtd()
+        new_node = mtr(entry)
+        node.close().close().append(new_node)
+        return entry, string
+
+    if name in accents:
+        accent_node = mo(accents[name], stretchy=False)
+        # mi() would be simpler, but semantically wrong
+        # --- https://w3c.github.io/mathml-core/#operator-fence-separator-or-accent-mo
+        if name == 'vec':
+            accent_node.set('scriptlevel', '+1')  # scale down arrow
+        new_node = mover(accent_node, accent=True, switch=True)
+        node.append(new_node)
+        return new_node, string
+
+    if name in over:
+        # set "accent" to False (otherwise dots on i and j are dropped)
+        # but to True on accent node get "textstyle" (full size) symbols on top
+        new_node = mover(mo(over[name][0], accent=True),
+                         switch=True, accent=False)
+        node.append(new_node)
+        return new_node, string
+
+    if name == 'overset':
+        new_node = mover(switch=True)
+        node.append(new_node)
+        return new_node, string
+
+    if name in under:
+        new_node = munder(mo(under[name][0]), switch=True)
+        node.append(new_node)
+        return new_node, string
+
+    if name == 'underset':
+        new_node = munder(switch=True)
+        node.append(new_node)
+        return new_node, string
+
+    if name in ('xleftarrow', 'xrightarrow'):
+        subscript, string = tex_optarg(string)
+        base = mo(operators['long'+name[1:]])
+        if subscript:
+            new_node = munderover(base)
+            sub_node = parse_latex_math(mrow(), subscript)
+            if len(sub_node) == 1:
+                sub_node = sub_node[0]
+            new_node.append(sub_node)
+        else:
+            new_node = mover(base)
+        node.append(new_node)
+        return new_node, string
+
+    if name in layout_styles:  # 'displaystyle', 'textstyle', ...
+        if len(node) > 0:
+            raise MathError(rf'Declaration "\{name}" must be first command '
+                            'in a group!')
+        for k, v in layout_styles[name].items():
+            node.set(k, v)
+        return node, string
+
+    if name.endswith('limits'):
+        arg, remainder = tex_token(string)
+        if arg in '_^':  # else ignore
+            string = remainder
+            node = handle_script_or_limit(node, arg, limits=name)
+        return node, string
+
+    # Environments
+
+    if name == 'begin':
+        return begin_environment(node, string)
+
+    if name == 'end':
+        return end_environment(node, string)
+
+    raise MathError(rf'Unknown LaTeX command "\{name}".')
+
+# >>> handle_cmd('left', math(), '[a\\right]')
+# (mrow(mo('[')), 'a\\right]')
+# >>> handle_cmd('left', math(), '. a)') # empty \left
+# (mrow(), ' a)')
+# >>> handle_cmd('left', math(), '\\uparrow a)') # cmd
+# (mrow(mo('↑')), 'a)')
+# >>> handle_cmd('not', math(), '\\equiv \\alpha)') # cmd
+# (math(mo('≢')), '\\alpha)')
+# >>> handle_cmd('text', math(), '{ for } i>0') # group
+# (math(mtext('\xa0for\xa0')), ' i>0')
+# >>> handle_cmd('text', math(), '{B}T') # group
+# (math(mtext('B')), 'T')
+# >>> handle_cmd('text', math(), '{number of apples}}') # group
+# (math(mtext('number of apples')), '}')
+# >>> handle_cmd('text', math(), 'i \\sin(x)') # single char
+# (math(mtext('i')), ' \\sin(x)')
+# >>> handle_cmd(' ', math(), '  next') # inter word space
+# (math(mspace(width='0.25em')), '  next')
+# >>> handle_cmd('\n', math(), '\nnext') # inter word space
+# (math(mspace(width='0.25em')), '\nnext')
+# >>> handle_cmd('sin', math(), '(\\alpha)')
+# (math(mi('sin'), mo('\u2061')), '(\\alpha)')
+# >>> handle_cmd('sin', math(), ' \\alpha')
+# (math(mi('sin'), mo('\u2061')), ' \\alpha')
+# >>> handle_cmd('operatorname', math(), '{abs}(x)')
+# (math(mi('abs', mathvariant='normal'), mo('\u2061')), '(x)')
+# >>> handle_cmd('overline', math(), '{981}')
+# (mover(mo('_', accent='true'), switch=True, accent='false'), '{981}')
+# >>> handle_cmd('bar', math(), '{x}')
+# (mover(mo('ˉ', stretchy='false'), switch=True, accent='true'), '{x}')
+# >>> handle_cmd('xleftarrow', math(), r'[\alpha]{10}')
+# (munderover(mo('⟵'), mi('α')), '{10}')
+# >>> handle_cmd('xleftarrow', math(), r'[\alpha=5]{10}')
+# (munderover(mo('⟵'), mrow(mi('α'), mo('='), mn('5'))), '{10}')
+# >>> handle_cmd('left', math(), '< a)')
+# Traceback (most recent call last):
+# docutils.utils.math.MathError: Unsupported "\left" delimiter "<"!
+# >>> handle_cmd('not', math(), '{< b} c') #  LaTeX ignores the braces, too.
+# (math(), '{\\not < b} c')
+
+
+def handle_math_alphabet(name, node, string):
+    attributes = {}
+    if name == 'mathscr':
+        attributes['class'] = 'mathscr'
+    arg, string = tex_token_or_group(string)
+    # Shortcut for text arg like \mathrm{out} with more than one letter:
+    if name == 'mathrm' and arg.isalpha() and len(arg) > 1:
+        node = node.append(mi(arg))  # <mi> defaults to "normal" font
+        return node, string
+    # Parse into an <mrow>
+    container = mrow(**attributes)
+    node.append(container)
+    parse_latex_math(container, arg)
+    key = name.replace('mathscr', 'mathcal').replace('mathbfsfit', 'mathsfbfit')
+    a2ch = getattr(mathalphabet2unichar, key, {})
+    for subnode in container.iter():
+        if isinstance(subnode, mn):
+            # a number may consist of more than one digit
+            subnode.text = ''.join(a2ch.get(ch, ch) for ch in subnode.text)
+        elif isinstance(subnode, mi):
+            # don't convert multi-letter identifiers (functions)
+            subnode.text = a2ch.get(subnode.text, subnode.text)
+            if name == 'mathrm' and subnode.text.isalpha():
+                subnode.set('mathvariant', 'normal')
+    return container.close(), string
+
+# >>> handle_math_alphabet('mathrm', math(), '\\alpha')
+# (math(mi('α', mathvariant='normal')), '')
+# >>> handle_math_alphabet('mathbb', math(), '{R} = 3')
+# (math(mi('ℝ')), ' = 3')
+# >>> handle_math_alphabet('mathcal', math(), '{F = 3}')
+# (math(mrow(mi('ℱ'), mo('='), mn('3'), nchildren=3)), '')
+# >>> handle_math_alphabet('mathrm', math(), '{out} = 3')  # drop <mrow>
+# (math(mi('out')), ' = 3')
+#
+# Single letters in \mathrm require "mathvariant='normal'":
+# >>> handle_math_alphabet('mathrm', math(), '{V = 3}')  # doctest: +ELLIPSIS
+# (math(mrow(mi('V', mathvariant='normal'), mo('='), mn('3'), ...)), '')
+
+
+def handle_script_or_limit(node, c, limits=''):
+    """Append script or limit element to `node`."""
+    child = node.pop()
+    if limits == 'limits':
+        child.set('movablelimits', 'false')
+    elif (limits == 'movablelimits'
+          or getattr(child, 'text', '') in movablelimits):
+        child.set('movablelimits', 'true')
+
+    if c == '_':
+        if isinstance(child, mover):
+            new_node = munderover(*child, switch=True)
+        elif isinstance(child, msup):
+            new_node = msubsup(*child, switch=True)
+        elif (limits in ('limits', 'movablelimits')
+              or limits == '' and child.get('movablelimits', None)):
+            new_node = munder(child)
+        else:
+            new_node = msub(child)
+    elif c == '^':
+        if isinstance(child, munder):
+            new_node = munderover(*child)
+        elif isinstance(child, msub):
+            new_node = msubsup(*child)
+        elif (limits in ('limits', 'movablelimits')
+              or limits == '' and child.get('movablelimits', None)):
+            new_node = mover(child)
+        else:
+            new_node = msup(child)
+    node.append(new_node)
+    return new_node
+
+
+def begin_environment(node, string):
+    name, string = tex_group(string)
+    if name in matrices:
+        left_delimiter = matrices[name][0]
+        attributes = {}
+        if left_delimiter:
+            wrapper = mrow(mo(left_delimiter))
+            if name == 'cases':
+                wrapper = mrow(mo(left_delimiter, rspace='0.17em'))
+                attributes['columnalign'] = 'left'
+                attributes['class'] = 'cases'
+            node.append(wrapper)
+            node = wrapper
+        elif name == 'smallmatrix':
+            attributes['rowspacing'] = '0.02em'
+            attributes['columnspacing'] = '0.333em'
+            attributes['scriptlevel'] = '1'
+        elif name == 'aligned':
+            attributes['class'] = 'ams-align'
+        # TODO: array, aligned & alignedat take an optional [t], [b], or [c].
+        entry = mtd()
+        node.append(mtable(mtr(entry), **attributes))
+        node = entry
+    else:
+        raise MathError(f'Environment "{name}" not supported!')
+    return node, string
+
+
+def end_environment(node, string):
+    name, string = tex_group(string)
+    if name in matrices:
+        node = node.close().close().close()  # close: mtd, mdr, mtable
+        right_delimiter = matrices[name][1]
+        if right_delimiter:
+            node = node.append(mo(right_delimiter))
+            node = node.close()
+        elif name == 'cases':
+            node = node.close()
+    else:
+        raise MathError(f'Environment "{name}" not supported!')
+    return node, string
+
+
+# Return the number of "equation_columns" in `code_lines`. cf. "alignat"
+# in http://mirror.ctan.org/macros/latex/required/amsmath/amsldoc.pdf
+def tex_equation_columns(rows):
+    tabs = max(row.count('&') - row.count(r'\&') for row in rows)
+    if tabs == 0:
+        return 0
+    return int(tabs/2 + 1)
+
+# >>> tex_equation_columns(['a = b'])
+# 0
+# >>> tex_equation_columns(['a &= b'])
+# 1
+# >>> tex_equation_columns(['a &= b & a \in S'])
+# 2
+# >>> tex_equation_columns(['a &= b & c &= d'])
+# 2
+
+
+# Return dictionary with attributes to style an <mtable> as align environment:
+# Not used with HTML. Replaced by CSS rule for "mtable.ams-align" in
+# "minimal.css" as "columnalign" is disregarded by Chromium and webkit.
+def align_attributes(rows):
+    atts = {'class': 'ams-align',
+            'displaystyle': True}
+    # get maximal number of non-escaped "next column" markup characters:
+    tabs = max(row.count('&') - row.count(r'\&') for row in rows)
+    if tabs:
+        aligns = ['right', 'left'] * tabs
+        spacing = ['0', '2em'] * tabs
+        atts['columnalign'] = ' '.join(aligns[:tabs+1])
+        atts['columnspacing'] = ' '.join(spacing[:tabs])
+    return atts
+
+# >>> align_attributes(['a = b'])
+# {'class': 'ams-align', 'displaystyle': True}
+# >>> align_attributes(['a &= b'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left', 'columnspacing': '0'}
+# >>> align_attributes(['a &= b & a \in S'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left right', 'columnspacing': '0 2em'}
+# >>> align_attributes(['a &= b & c &= d'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left right left', 'columnspacing': '0 2em 0'}
+# >>> align_attributes([r'a &= b & c &= d \& e'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left right left', 'columnspacing': '0 2em 0'}
+# >>> align_attributes([r'a &= b & c &= d & e'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left right left right', 'columnspacing': '0 2em 0 2em'}
+
+
+def tex2mathml(tex_math, as_block=False):
+    """Return string with MathML code corresponding to `tex_math`.
+
+    Set `as_block` to ``True`` for displayed formulas.
+    """
+    # Set up tree
+    math_tree = math(xmlns='http://www.w3.org/1998/Math/MathML')
+    node = math_tree
+    if as_block:
+        math_tree.set('display', 'block')
+        rows = toplevel_code(tex_math).split(r'\\')
+        if len(rows) > 1:
+            # emulate "align*" environment with a math table
+            node = mtd()
+            math_tree.append(mtable(mtr(node), CLASS='ams-align',
+                                    displaystyle=True))
+    parse_latex_math(node, tex_math)
+    math_tree.indent_xml()
+    return math_tree.toxml()
+
+# >>> print(tex2mathml('3'))
+# <math xmlns="http://www.w3.org/1998/Math/MathML">
+#   <mn>3</mn>
+# </math>
+# >>> print(tex2mathml('3', as_block=True))
+# <math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
+#   <mn>3</mn>
+# </math>
+# >>> print(tex2mathml(r'a & b \\ c & d', as_block=True))
+# <math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
+#   <mtable class="ams-align" displaystyle="true">
+#     <mtr>
+#       <mtd>
+#         <mi>a</mi>
+#       </mtd>
+#       <mtd>
+#         <mi>b</mi>
+#       </mtd>
+#     </mtr>
+#     <mtr>
+#       <mtd>
+#         <mi>c</mi>
+#       </mtd>
+#       <mtd>
+#         <mi>d</mi>
+#       </mtd>
+#     </mtr>
+#   </mtable>
+# </math>
+# >>> print(tex2mathml(r'a \\ b', as_block=True))
+# <math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
+#   <mtable class="ams-align" displaystyle="true">
+#     <mtr>
+#       <mtd>
+#         <mi>a</mi>
+#       </mtd>
+#     </mtr>
+#     <mtr>
+#       <mtd>
+#         <mi>b</mi>
+#       </mtd>
+#     </mtr>
+#   </mtable>
+# </math>
+
+
+# TODO: look up more symbols from tr25, e.g.
+#
+#
+# Table 2.8 Using Vertical Line or Solidus Overlay
+#   some of the negated forms of mathematical relations that can only be
+#   encoded by using either U+0338 COMBINING LONG SOLIDUS OVERLAY or U+20D2
+#   COMBINING LONG VERTICAL LINE OVERLAY . (For issues with using 0338 in
+#   MathML, see Section 3.2.7, Combining Marks.
+#
+# Table 2.9 Variants of Mathematical Symbols using VS1?
+#
+# Sequence      Description
+# 0030 + VS1    DIGIT ZERO - short diagonal stroke form
+# 2205 + VS1    EMPTY SET - zero with long diagonal stroke overlay form
+# 2229 + VS1    INTERSECTION - with serifs
+# 222A + VS1    UNION - with serifs
+# 2268 + VS1    LESS-THAN BUT NOT EQUAL TO - with vertical stroke
+# 2269 + VS1    GREATER-THAN BUT NOT EQUAL TO - with vertical stroke
+# 2272 + VS1    LESS-THAN OR EQUIVALENT TO - following the slant of the lower leg
+# 2273 + VS1    GREATER-THAN OR EQUIVALENT TO - following the slant of the lower leg
+# 228A + VS1    SUBSET OF WITH NOT EQUAL TO - variant with stroke through bottom members
+# 228B + VS1    SUPERSET OF WITH NOT EQUAL TO - variant with stroke through bottom members
+# 2293 + VS1    SQUARE CAP - with serifs
+# 2294 + VS1    SQUARE CUP - with serifs
+# 2295 + VS1    CIRCLED PLUS - with white rim
+# 2297 + VS1    CIRCLED TIMES - with white rim
+# 229C + VS1    CIRCLED EQUALS - equal sign inside and touching the circle
+# 22DA + VS1    LESS-THAN slanted EQUAL TO OR GREATER-THAN
+# 22DB + VS1    GREATER-THAN slanted EQUAL TO OR LESS-THAN
+# 2A3C + VS1    INTERIOR PRODUCT - tall variant with narrow foot
+# 2A3D + VS1    RIGHTHAND INTERIOR PRODUCT - tall variant with narrow foot
+# 2A9D + VS1    SIMILAR OR LESS-THAN - following the slant of the upper leg
+# 2A9E + VS1    SIMILAR OR GREATER-THAN - following the slant of the upper leg
+# 2AAC + VS1    SMALLER THAN OR slanted EQUAL
+# 2AAD + VS1    LARGER THAN OR slanted EQUAL
+# 2ACB + VS1    SUBSET OF ABOVE NOT EQUAL TO - variant with stroke through bottom members
+# 2ACC + VS1    SUPERSET OF ABOVE NOT EQUAL TO - variant with stroke through bottom members
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/math2html.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/math2html.py
new file mode 100755
index 00000000..dc94cff7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/math2html.py
@@ -0,0 +1,3165 @@
+#! /usr/bin/env python3
+#   math2html: convert LaTeX equations to HTML output.
+#
+#   Copyright (C) 2009-2011 Alex Fernández, 2021 Günter Milde
+#
+#   Released under the terms of the `2-Clause BSD license'_, in short:
+#   Copying and distribution of this file, with or without modification,
+#   are permitted in any medium without royalty provided the copyright
+#   notice and this notice are preserved.
+#   This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+#   Based on eLyXer: convert LyX source files to HTML output.
+#   http://alexfernandez.github.io/elyxer/
+
+# Versions:
+# 1.2.5  2015-02-26  eLyXer standalone formula conversion to HTML.
+# 1.3    2021-06-02  Removed code for conversion of LyX files not
+#                    required for LaTeX math.
+#                    Support for more math commands from the AMS "math-guide".
+# 2.0    2021-12-31  Drop 2.7 compatibility code.
+
+import pathlib
+import sys
+import unicodedata
+
+from docutils.utils.math import tex2unichar
+
+
+__version__ = '1.3 (2021-06-02)'
+
+
+class Trace:
+    "A tracing class"
+
+    debugmode = False
+    quietmode = False
+    showlinesmode = False
+
+    prefix = None
+
+    def debug(cls, message):
+        "Show a debug message"
+        if not Trace.debugmode or Trace.quietmode:
+            return
+        Trace.show(message, sys.stdout)
+
+    def message(cls, message):
+        "Show a trace message"
+        if Trace.quietmode:
+            return
+        if Trace.prefix and Trace.showlinesmode:
+            message = Trace.prefix + message
+        Trace.show(message, sys.stdout)
+
+    def error(cls, message):
+        "Show an error message"
+        message = '* ' + message
+        if Trace.prefix and Trace.showlinesmode:
+            message = Trace.prefix + message
+        Trace.show(message, sys.stderr)
+
+    def show(cls, message, channel):
+        "Show a message out of a channel"
+        channel.write(message + '\n')
+
+    debug = classmethod(debug)
+    message = classmethod(message)
+    error = classmethod(error)
+    show = classmethod(show)
+
+
+class ContainerConfig:
+    "Configuration class from elyxer.config file"
+
+    extracttext = {
+        'allowed': ['FormulaConstant'],
+        'extracted': ['AlphaCommand',
+                      'Bracket',
+                      'BracketCommand',
+                      'CombiningFunction',
+                      'EmptyCommand',
+                      'FontFunction',
+                      'Formula',
+                      'FormulaNumber',
+                      'FormulaSymbol',
+                      'OneParamFunction',
+                      'OversetFunction',
+                      'RawText',
+                      'SpacedCommand',
+                      'SymbolFunction',
+                      'TextFunction',
+                      'UndersetFunction',
+                      ],
+    }
+
+
+class EscapeConfig:
+    "Configuration class from elyxer.config file"
+
+    chars = {
+        '\n': '',
+        "'": '’',
+        '`': '‘',
+    }
+
+    entities = {
+        '&': '&amp;',
+        '<': '&lt;',
+        '>': '&gt;',
+    }
+
+
+class FormulaConfig:
+    "Configuration class from elyxer.config file"
+
+    alphacommands = {
+        '\\AmS': '<span class="textsc">AmS</span>',
+        '\\AA':        'Å',
+        '\\AE':        'Æ',
+        '\\DH':        'Ð',
+        '\\L':         'Ł',
+        '\\O':         'Ø',
+        '\\OE':        'Œ',
+        '\\TH':        'Þ',
+        '\\aa':        'å',
+        '\\ae':        'æ',
+        '\\dh':        'ð',
+        '\\i':         'ı',
+        '\\j':         'ȷ',
+        '\\l':         'ł',
+        '\\o':         'ø',
+        '\\oe':        'œ',
+        '\\ss':        'ß',
+        '\\th':        'þ',
+        '\\hbar':      'ħ',  # cf. \hslash: ℏ in tex2unichar
+    }
+    for key, value in tex2unichar.mathalpha.items():
+        alphacommands['\\'+key] = value
+
+    array = {
+        'begin': r'\begin',
+        'cellseparator': '&',
+        'end': r'\end',
+        'rowseparator': r'\\',
+    }
+
+    bigbrackets = {'(': ['⎛', '⎜', '⎝'],
+                   ')': ['⎞', '⎟', '⎠'],
+                   '[': ['⎡', '⎢', '⎣'],
+                   ']': ['⎤', '⎥', '⎦'],
+                   '{': ['⎧', '⎪', '⎨', '⎩'],
+                   '}': ['⎫', '⎪', '⎬', '⎭'],
+                   # TODO: 2-row brackets with ⎰⎱ (\lmoustache \rmoustache)
+                   '|': ['|'],  # 007C VERTICAL LINE
+                   # '|': ['⎮'],  # 23AE INTEGRAL EXTENSION
+                   # '|': ['⎪'],  # 23AA CURLY BRACKET EXTENSION
+                   '‖': ['‖'],  # 2016 DOUBLE VERTICAL LINE
+                   # '∥': ['∥'],  # 2225 PARALLEL TO
+                   }
+
+    bracketcommands = {
+        '\\left': 'span class="stretchy"',
+        '\\left.': '<span class="leftdot"></span>',
+        '\\middle': 'span class="stretchy"',
+        '\\right': 'span class="stretchy"',
+        '\\right.': '<span class="rightdot"></span>',
+    }
+
+    combiningfunctions = {
+        "\\'":           '\u0301',  # x́
+        '\\"':           '\u0308',  # ẍ
+        '\\^':           '\u0302',  # x̂
+        '\\`':           '\u0300',  # x̀
+        '\\~':           '\u0303',  # x̃
+        '\\c':           '\u0327',  # x̧
+        '\\r':           '\u030a',  # x̊
+        '\\s':           '\u0329',  # x̩
+        '\\textcircled': '\u20dd',  # x⃝
+        '\\textsubring': '\u0325',  # x̥
+        '\\v':           '\u030c',  # x̌
+    }
+    for key, value in tex2unichar.mathaccent.items():
+        combiningfunctions['\\'+key] = value
+
+    commands = {
+        '\\\\': '<br/>',
+        '\\\n': ' ',  # escaped whitespace
+        '\\\t': ' ',  # escaped whitespace
+        '\\centerdot': '\u2B1D',  # BLACK VERY SMALL SQUARE, mathbin
+        '\\colon': ': ',
+        '\\copyright': '©',
+        '\\dotminus': '∸',
+        '\\dots': '…',
+        '\\dotsb': '⋯',
+        '\\dotsc': '…',
+        '\\dotsi': '⋯',
+        '\\dotsm': '⋯',
+        '\\dotso': '…',
+        '\\euro': '€',
+        '\\guillemotleft': '«',
+        '\\guillemotright': '»',
+        '\\lVert': '‖',
+        '\\Arrowvert':  '‖',
+        '\\lvert': '|',
+        '\\newline': '<br/>',
+        '\\nobreakspace': ' ',
+        '\\nolimits': '',
+        '\\nonumber': '',
+        '\\qquad': '  ',
+        '\\rVert': '‖',
+        '\\rvert': '|',
+        '\\textasciicircum': '^',
+        '\\textasciitilde': '~',
+        '\\textbackslash': '\\',
+        '\\textcopyright': '©',
+        '\\textdegree': '°',
+        '\\textellipsis': '…',
+        '\\textemdash': '—',
+        '\\textendash': '—',
+        '\\texteuro': '€',
+        '\\textgreater': '>',
+        '\\textless': '<',
+        '\\textordfeminine': 'ª',
+        '\\textordmasculine': 'º',
+        '\\textquotedblleft': '“',
+        '\\textquotedblright': '”',
+        '\\textquoteright': '’',
+        '\\textregistered': '®',
+        '\\textrightarrow': '→',
+        '\\textsection': '§',
+        '\\texttrademark': '™',
+        '\\texttwosuperior': '²',
+        '\\textvisiblespace': ' ',
+        '\\thickspace': '<span class="thickspace"> </span>',  # 5/13 em
+        '\\;': '<span class="thickspace"> </span>',  # 5/13 em
+        '\\triangle': '\u25B3',  # WHITE UP-POINTING TRIANGLE, mathord
+        '\\triangledown': '\u25BD',  # WHITE DOWN-POINTING TRIANGLE, mathord
+        '\\varnothing': '\u2300',  # ⌀ DIAMETER SIGN
+        # functions
+        '\\Pr': 'Pr',
+        '\\arccos': 'arccos',
+        '\\arcsin': 'arcsin',
+        '\\arctan': 'arctan',
+        '\\arg': 'arg',
+        '\\cos': 'cos',
+        '\\cosh': 'cosh',
+        '\\cot': 'cot',
+        '\\coth': 'coth',
+        '\\csc': 'csc',
+        '\\deg': 'deg',
+        '\\det': 'det',
+        '\\dim': 'dim',
+        '\\exp': 'exp',
+        '\\gcd': 'gcd',
+        '\\hom': 'hom',
+        '\\injlim': 'inj lim',
+        '\\ker': 'ker',
+        '\\lg': 'lg',
+        '\\liminf': 'lim inf',
+        '\\limsup': 'lim sup',
+        '\\ln': 'ln',
+        '\\log': 'log',
+        '\\projlim': 'proj lim',
+        '\\sec': 'sec',
+        '\\sin': 'sin',
+        '\\sinh': 'sinh',
+        '\\tan': 'tan',
+        '\\tanh': 'tanh',
+    }
+    cmddict = {}
+    cmddict.update(tex2unichar.mathbin)  # TODO: spacing around binary operators
+    cmddict.update(tex2unichar.mathopen)
+    cmddict.update(tex2unichar.mathclose)
+    cmddict.update(tex2unichar.mathfence)
+    cmddict.update(tex2unichar.mathord)
+    cmddict.update(tex2unichar.mathpunct)
+    cmddict.update(tex2unichar.space)
+    commands.update(('\\' + key, value) for key, value in cmddict.items())
+
+    oversetfunctions = {
+        # math accents (cf. combiningfunctions)
+        # '\\acute':    '´',
+        '\\bar':      '‒',  # FIGURE DASH
+        # '\\breve':    '˘',
+        # '\\check':    'ˇ',
+        '\\dddot':    '<span class="smallsymbol">⋯</span>',
+        # '\\ddot':     '··', # ¨ too high
+        # '\\dot':      '·',
+        # '\\grave':    '`',
+        # '\\hat':      '^',
+        # '\\mathring': '˚',
+        # '\\tilde':    '~',
+        '\\vec':      '<span class="smallsymbol">→</span>',
+        # embellishments
+        '\\overleftarrow': '⟵',
+        '\\overleftrightarrow': '⟷',
+        '\\overrightarrow': '⟶',
+        '\\widehat': '^',
+        '\\widetilde': '~',
+    }
+
+    undersetfunctions = {
+        '\\underleftarrow': '⟵',
+        '\\underleftrightarrow': '⟷',
+        '\\underrightarrow': '⟶',
+    }
+
+    endings = {
+        'bracket': '}',
+        'complex': '\\]',
+        'endafter': '}',
+        'endbefore': '\\end{',
+        'squarebracket': ']',
+    }
+
+    environments = {
+        'align': ['r', 'l'],
+        'eqnarray': ['r', 'c', 'l'],
+        'gathered': ['l', 'l'],
+        'smallmatrix': ['c', 'c'],
+    }
+
+    fontfunctions = {
+        '\\boldsymbol': 'b', '\\mathbb': 'span class="blackboard"',
+        '\\mathbb{A}': '𝔸', '\\mathbb{B}': '𝔹', '\\mathbb{C}': 'ℂ',
+        '\\mathbb{D}': '𝔻', '\\mathbb{E}': '𝔼', '\\mathbb{F}': '𝔽',
+        '\\mathbb{G}': '𝔾', '\\mathbb{H}': 'ℍ', '\\mathbb{J}': '𝕁',
+        '\\mathbb{K}': '𝕂', '\\mathbb{L}': '𝕃', '\\mathbb{N}': 'ℕ',
+        '\\mathbb{O}': '𝕆', '\\mathbb{P}': 'ℙ', '\\mathbb{Q}': 'ℚ',
+        '\\mathbb{R}': 'ℝ', '\\mathbb{S}': '𝕊', '\\mathbb{T}': '𝕋',
+        '\\mathbb{W}': '𝕎', '\\mathbb{Z}': 'ℤ', '\\mathbf': 'b',
+        '\\mathcal': 'span class="scriptfont"',
+        '\\mathcal{B}': 'ℬ', '\\mathcal{E}': 'ℰ', '\\mathcal{F}':
+        'ℱ', '\\mathcal{H}': 'ℋ', '\\mathcal{I}': 'ℐ',
+        '\\mathcal{L}': 'ℒ', '\\mathcal{M}': 'ℳ', '\\mathcal{R}': 'ℛ',
+        '\\mathfrak': 'span class="fraktur"',
+        '\\mathfrak{C}': 'ℭ', '\\mathfrak{F}': '𝔉', '\\mathfrak{H}': 'ℌ',
+        '\\mathfrak{I}': 'ℑ', '\\mathfrak{R}': 'ℜ', '\\mathfrak{Z}': 'ℨ',
+        '\\mathit': 'i',
+        '\\mathring{A}': 'Å', '\\mathring{U}': 'Ů',
+        '\\mathring{a}': 'å', '\\mathring{u}': 'ů', '\\mathring{w}': 'ẘ',
+        '\\mathring{y}': 'ẙ',
+        '\\mathrm': 'span class="mathrm"',
+        '\\mathscr': 'span class="mathscr"',
+        '\\mathscr{B}': 'ℬ', '\\mathscr{E}': 'ℰ', '\\mathscr{F}': 'ℱ',
+        '\\mathscr{H}': 'ℋ', '\\mathscr{I}': 'ℐ', '\\mathscr{L}': 'ℒ',
+        '\\mathscr{M}': 'ℳ', '\\mathscr{R}': 'ℛ',
+        '\\mathsf': 'span class="mathsf"',
+        '\\mathtt': 'span class="mathtt"',
+        '\\operatorname': 'span class="mathrm"',
+    }
+
+    hybridfunctions = {
+        '\\addcontentsline': ['{$p!}{$q!}{$r!}', 'f0{}', 'ignored'],
+        '\\addtocontents': ['{$p!}{$q!}', 'f0{}', 'ignored'],
+        '\\backmatter': ['', 'f0{}', 'ignored'],
+        '\\binom': ['{$1}{$2}', 'f2{(}f0{f1{$1}f1{$2}}f2{)}', 'span class="binom"', 'span class="binomstack"', 'span class="bigdelimiter size2"'],
+        '\\boxed': ['{$1}', 'f0{$1}', 'span class="boxed"'],
+        '\\cfrac': ['[$p!]{$1}{$2}', 'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}', 'span class="fullfraction"', 'span class="numerator align-$p"', 'span class="denominator"', 'span class="ignored"'],
+        '\\color': ['{$p!}{$1}', 'f0{$1}', 'span style="color: $p;"'],
+        '\\colorbox': ['{$p!}{$1}', 'f0{$1}', 'span class="colorbox" style="background: $p;"'],
+        '\\dbinom': ['{$1}{$2}', '(f0{f1{f2{$1}}f1{f2{ }}f1{f2{$2}}})', 'span class="binomial"', 'span class="binomrow"', 'span class="binomcell"'],
+        '\\dfrac': ['{$1}{$2}', 'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}', 'span class="fullfraction"', 'span class="numerator"', 'span class="denominator"', 'span class="ignored"'],
+        '\\displaystyle': ['{$1}', 'f0{$1}', 'span class="displaystyle"'],
+        '\\fancyfoot': ['[$p!]{$q!}', 'f0{}', 'ignored'],
+        '\\fancyhead': ['[$p!]{$q!}', 'f0{}', 'ignored'],
+        '\\fbox': ['{$1}', 'f0{$1}', 'span class="fbox"'],
+        '\\fboxrule': ['{$p!}', 'f0{}', 'ignored'],
+        '\\fboxsep': ['{$p!}', 'f0{}', 'ignored'],
+        '\\fcolorbox': ['{$p!}{$q!}{$1}', 'f0{$1}', 'span class="boxed" style="border-color: $p; background: $q;"'],
+        '\\frac': ['{$1}{$2}', 'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}', 'span class="fraction"', 'span class="numerator"', 'span class="denominator"', 'span class="ignored"'],
+        '\\framebox': ['[$p!][$q!]{$1}', 'f0{$1}', 'span class="framebox align-$q" style="width: $p;"'],
+        '\\frontmatter': ['', 'f0{}', 'ignored'],
+        '\\href': ['[$o]{$u!}{$t!}', 'f0{$t}', 'a href="$u"'],
+        '\\hspace': ['{$p!}', 'f0{ }', 'span class="hspace" style="width: $p;"'],
+        '\\leftroot': ['{$p!}', 'f0{ }', 'span class="leftroot" style="width: $p;px"'],
+        # TODO: convert 1 mu to 1/18 em
+        # '\\mspace': ['{$p!}', 'f0{ }', 'span class="hspace" style="width: $p;"'],
+        '\\nicefrac': ['{$1}{$2}', 'f0{f1{$1}⁄f2{$2}}', 'span class="fraction"', 'sup class="numerator"', 'sub class="denominator"', 'span class="ignored"'],
+        '\\parbox': ['[$p!]{$w!}{$1}', 'f0{1}', 'div class="Boxed" style="width: $w;"'],
+        '\\raisebox': ['{$p!}{$1}', 'f0{$1.font}', 'span class="raisebox" style="vertical-align: $p;"'],
+        '\\renewenvironment': ['{$1!}{$2!}{$3!}', ''],
+        '\\rule': ['[$v!]{$w!}{$h!}', 'f0/', 'hr class="line" style="width: $w; height: $h;"'],
+        '\\scriptscriptstyle': ['{$1}', 'f0{$1}', 'span class="scriptscriptstyle"'],
+        '\\scriptstyle': ['{$1}', 'f0{$1}', 'span class="scriptstyle"'],
+        # TODO: increase √-size with argument (\frac in display mode, ...)
+        '\\sqrt': ['[$0]{$1}', 'f0{f1{$0}f2{√}f4{(}f3{$1}f4{)}}', 'span class="sqrt"', 'sup class="root"', 'span class="radical"', 'span class="root"', 'span class="ignored"'],
+        '\\stackrel': ['{$1}{$2}', 'f0{f1{$1}f2{$2}}', 'span class="stackrel"', 'span class="upstackrel"', 'span class="downstackrel"'],
+        '\\tbinom': ['{$1}{$2}', '(f0{f1{f2{$1}}f1{f2{ }}f1{f2{$2}}})', 'span class="binomial"', 'span class="binomrow"', 'span class="binomcell"'],
+        '\\tfrac':  ['{$1}{$2}', 'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}', 'span class="textfraction"', 'span class="numerator"', 'span class="denominator"', 'span class="ignored"'],
+        '\\textcolor': ['{$p!}{$1}', 'f0{$1}', 'span style="color: $p;"'],
+        '\\textstyle': ['{$1}', 'f0{$1}', 'span class="textstyle"'],
+        '\\thispagestyle': ['{$p!}', 'f0{}', 'ignored'],
+        '\\unit': ['[$0]{$1}', '$0f0{$1.font}', 'span class="unit"'],
+        '\\unitfrac': ['[$0]{$1}{$2}', '$0f0{f1{$1.font}⁄f2{$2.font}}', 'span class="fraction"', 'sup class="unit"', 'sub class="unit"'],
+        '\\uproot': ['{$p!}', 'f0{ }', 'span class="uproot" style="width: $p;px"'],
+        '\\url': ['{$u!}', 'f0{$u}', 'a href="$u"'],
+        '\\vspace': ['{$p!}', 'f0{ }', 'span class="vspace" style="height: $p;"'],
+    }
+
+    hybridsizes = {
+        '\\binom': '$1+$2', '\\cfrac': '$1+$2', '\\dbinom': '$1+$2+1',
+        '\\dfrac': '$1+$2', '\\frac': '$1+$2', '\\tbinom': '$1+$2+1',
+    }
+
+    labelfunctions = {
+        '\\label': 'a name="#"',
+    }
+
+    limitcommands = {
+        '\\biginterleave': '⫼',
+        '\\inf': 'inf',
+        '\\lim': 'lim',
+        '\\max': 'max',
+        '\\min': 'min',
+        '\\sup': 'sup',
+        '\\ointop':    '<span class="bigoperator integral">∮</span>',
+        '\\bigcap':    '<span class="bigoperator">⋂</span>',
+        '\\bigcup':    '<span class="bigoperator">⋃</span>',
+        '\\bigodot':   '<span class="bigoperator">⨀</span>',
+        '\\bigoplus':  '<span class="bigoperator">⨁</span>',
+        '\\bigotimes': '<span class="bigoperator">⨂</span>',
+        '\\bigsqcap':  '<span class="bigoperator">⨅</span>',
+        '\\bigsqcup':  '<span class="bigoperator">⨆</span>',
+        '\\biguplus':  '<span class="bigoperator">⨄</span>',
+        '\\bigvee':    '<span class="bigoperator">⋁</span>',
+        '\\bigwedge':  '<span class="bigoperator">⋀</span>',
+        '\\coprod':    '<span class="bigoperator">∐</span>',
+        '\\intop':     '<span class="bigoperator integral">∫</span>',
+        '\\prod':      '<span class="bigoperator">∏</span>',
+        '\\sum':       '<span class="bigoperator">∑</span>',
+        '\\varprod':   '<span class="bigoperator">⨉</span>',
+        '\\zcmp': '⨟', '\\zhide': '⧹', '\\zpipe': '⨠', '\\zproject': '⨡',
+        # integrals have limits in index position with LaTeX default settings
+        # TODO: move to commands?
+        '\\int': '<span class="bigoperator integral">∫</span>',
+        '\\iint': '<span class="bigoperator integral">∬</span>',
+        '\\iiint': '<span class="bigoperator integral">∭</span>',
+        '\\iiiint': '<span class="bigoperator integral">⨌</span>',
+        '\\fint': '<span class="bigoperator integral">⨏</span>',
+        '\\idotsint': '<span class="bigoperator integral">∫⋯∫</span>',
+        '\\oint': '<span class="bigoperator integral">∮</span>',
+        '\\oiint': '<span class="bigoperator integral">∯</span>',
+        '\\oiiint': '<span class="bigoperator integral">∰</span>',
+        '\\ointclockwise': '<span class="bigoperator integral">∲</span>',
+        '\\ointctrclockwise': '<span class="bigoperator integral">∳</span>',
+        '\\smallint': '<span class="smallsymbol integral">∫</span>',
+        '\\sqint': '<span class="bigoperator integral">⨖</span>',
+        '\\varointclockwise': '<span class="bigoperator integral">∲</span>',
+    }
+
+    modified = {
+        '\n': '', ' ': '', '$': '', '&': '	', '\'': '’', '+': '\u2009+\u2009',
+        ',': ',\u2009', '-': '\u2009−\u2009', '/': '\u2009⁄\u2009', ':': ' : ', '<': '\u2009&lt;\u2009',
+        '=': '\u2009=\u2009', '>': '\u2009&gt;\u2009', '@': '', '~': '\u00a0',
+    }
+
+    onefunctions = {
+        '\\big': 'span class="bigdelimiter size1"',
+        '\\bigl': 'span class="bigdelimiter size1"',
+        '\\bigr': 'span class="bigdelimiter size1"',
+        '\\Big': 'span class="bigdelimiter size2"',
+        '\\Bigl': 'span class="bigdelimiter size2"',
+        '\\Bigr': 'span class="bigdelimiter size2"',
+        '\\bigg': 'span class="bigdelimiter size3"',
+        '\\biggl': 'span class="bigdelimiter size3"',
+        '\\biggr': 'span class="bigdelimiter size3"',
+        '\\Bigg': 'span class="bigdelimiter size4"',
+        '\\Biggl': 'span class="bigdelimiter size4"',
+        '\\Biggr': 'span class="bigdelimiter size4"',
+        # '\\bar': 'span class="bar"',
+        '\\begin{array}': 'span class="arraydef"',
+        '\\centering': 'span class="align-center"',
+        '\\ensuremath': 'span class="ensuremath"',
+        '\\hphantom': 'span class="phantom"',
+        '\\noindent': 'span class="noindent"',
+        '\\overbrace': 'span class="overbrace"',
+        '\\overline': 'span class="overline"',
+        '\\phantom': 'span class="phantom"',
+        '\\underbrace': 'span class="underbrace"',
+        '\\underline': '',
+        '\\vphantom': 'span class="phantom"',
+    }
+
+    # relations (put additional space before and after the symbol)
+    spacedcommands = {
+        # negated symbols without pre-composed Unicode character
+        '\\nleqq':      '\u2266\u0338',  # ≦̸
+        '\\ngeqq':      '\u2267\u0338',  # ≧̸
+        '\\nleqslant':  '\u2a7d\u0338',  # ⩽̸
+        '\\ngeqslant':  '\u2a7e\u0338',  # ⩾̸
+        '\\nsubseteqq': '\u2AC5\u0338',  # ⫅̸
+        '\\nsupseteqq': '\u2AC6\u0338',  # ⫆̸
+        '\\nsqsubset':  '\u2276\u228F',  # ⊏̸
+        # modified glyphs
+        '\\shortmid': '<span class="smallsymbol">∣</span>',
+        '\\shortparallel': '<span class="smallsymbol">∥</span>',
+        '\\nshortmid': '<span class="smallsymbol">∤</span>',
+        '\\nshortparallel': '<span class="smallsymbol">∦</span>',
+        '\\smallfrown': '<span class="smallsymbol">⌢</span>',
+        '\\smallsmile': '<span class="smallsymbol">⌣</span>',
+        '\\thickapprox': '<span class="boldsymbol">≈</span>',
+        '\\thicksim': '<span class="boldsymbol">∼</span>',
+        '\\varpropto': '<span class="mathsf">\u221d</span>',  # ∝ PROPORTIONAL TO
+    }
+    for key, value in tex2unichar.mathrel.items():
+        spacedcommands['\\'+key] = value
+    starts = {
+        'beginafter': '}', 'beginbefore': '\\begin{', 'bracket': '{',
+        'command': '\\', 'comment': '%', 'complex': '\\[', 'simple': '$',
+        'squarebracket': '[', 'unnumbered': '*',
+    }
+
+    symbolfunctions = {
+        '^': 'sup', '_': 'sub',
+    }
+
+    textfunctions = {
+        '\\mbox': 'span class="mbox"',
+        '\\text': 'span class="text"',
+        '\\textbf': 'span class="textbf"',
+        '\\textit': 'span class="textit"',
+        '\\textnormal': 'span class="textnormal"',
+        '\\textrm': 'span class="textrm"',
+        '\\textsc': 'span class="textsc"',
+        '\\textsf': 'span class="textsf"',
+        '\\textsl': 'span class="textsl"',
+        '\\texttt': 'span class="texttt"',
+        '\\textup': 'span class="normal"',
+    }
+
+    unmodified = {
+        'characters': ['.', '*', '€', '(', ')', '[', ']',
+                       '·', '!', ';', '|', '§', '"', '?'],
+        }
+
+
+class CommandLineParser:
+    "A parser for runtime options"
+
+    def __init__(self, options):
+        self.options = options
+
+    def parseoptions(self, args):
+        "Parse command line options"
+        if len(args) == 0:
+            return None
+        while len(args) > 0 and args[0].startswith('--'):
+            key, value = self.readoption(args)
+            if not key:
+                return 'Option ' + value + ' not recognized'
+            if not value:
+                return 'Option ' + key + ' needs a value'
+            setattr(self.options, key, value)
+        return None
+
+    def readoption(self, args):
+        "Read the key and value for an option"
+        arg = args[0][2:]
+        del args[0]
+        if '=' in arg:
+            key = self.readequalskey(arg, args)
+        else:
+            key = arg.replace('-', '')
+        if not hasattr(self.options, key):
+            return None, key
+        current = getattr(self.options, key)
+        if isinstance(current, bool):
+            return key, True
+        # read value
+        if len(args) == 0:
+            return key, None
+        if args[0].startswith('"'):
+            initial = args[0]
+            del args[0]
+            return key, self.readquoted(args, initial)
+        value = args[0].decode('utf-8')
+        del args[0]
+        if isinstance(current, list):
+            current.append(value)
+            return key, current
+        return key, value
+
+    def readquoted(self, args, initial):
+        "Read a value between quotes"
+        Trace.error('Oops')
+        value = initial[1:]
+        while len(args) > 0 and not args[0].endswith('"') and not args[0].startswith('--'):
+            Trace.error('Appending ' + args[0])
+            value += ' ' + args[0]
+            del args[0]
+        if len(args) == 0 or args[0].startswith('--'):
+            return None
+        value += ' ' + args[0:-1]
+        return value
+
+    def readequalskey(self, arg, args):
+        "Read a key using equals"
+        split = arg.split('=', 1)
+        key = split[0]
+        value = split[1]
+        args.insert(0, value)
+        return key
+
+
+class Options:
+    "A set of runtime options"
+
+    location = None
+
+    debug = False
+    quiet = False
+    version = False
+    help = False
+    simplemath = False
+    showlines = True
+
+    branches = {}
+
+    def parseoptions(self, args):
+        "Parse command line options"
+        Options.location = args[0]
+        del args[0]
+        parser = CommandLineParser(Options)
+        result = parser.parseoptions(args)
+        if result:
+            Trace.error(result)
+            self.usage()
+        self.processoptions()
+
+    def processoptions(self):
+        "Process all options parsed."
+        if Options.help:
+            self.usage()
+        if Options.version:
+            self.showversion()
+        # set in Trace if necessary
+        for param in dir(Trace):
+            if param.endswith('mode'):
+                setattr(Trace, param, getattr(self, param[:-4]))
+
+    def usage(self):
+        "Show correct usage"
+        Trace.error(f'Usage: {pathlib.Path(Options.location).parent}'
+                    ' [options] "input string"')
+        Trace.error('Convert input string with LaTeX math to MathML')
+        self.showoptions()
+
+    def showoptions(self):
+        "Show all possible options"
+        Trace.error('    --help:                 show this online help')
+        Trace.error('    --quiet:                disables all runtime messages')
+        Trace.error('    --debug:                enable debugging messages (for developers)')
+        Trace.error('    --version:              show version number and release date')
+        Trace.error('    --simplemath:           do not generate fancy math constructions')
+        sys.exit()
+
+    def showversion(self):
+        "Return the current eLyXer version string"
+        Trace.error('math2html '+__version__)
+        sys.exit()
+
+
+class Cloner:
+    "An object used to clone other objects."
+
+    def clone(cls, original):
+        "Return an exact copy of an object."
+        "The original object must have an empty constructor."
+        return cls.create(original.__class__)
+
+    def create(cls, type):
+        "Create an object of a given class."
+        clone = type.__new__(type)
+        clone.__init__()
+        return clone
+
+    clone = classmethod(clone)
+    create = classmethod(create)
+
+
+class ContainerExtractor:
+    """A class to extract certain containers.
+
+    The config parameter is a map containing three lists:
+    allowed, copied and extracted.
+    Each of the three is a list of class names for containers.
+    Allowed containers are included as is into the result.
+    Cloned containers are cloned and placed into the result.
+    Extracted containers are looked into.
+    All other containers are silently ignored.
+    """
+
+    def __init__(self, config):
+        self.allowed = config['allowed']
+        self.extracted = config['extracted']
+
+    def extract(self, container):
+        "Extract a group of selected containers from a container."
+        list = []
+        locate = lambda c: c.__class__.__name__ in self.allowed
+        recursive = lambda c: c.__class__.__name__ in self.extracted
+        process = lambda c: self.process(c, list)
+        container.recursivesearch(locate, recursive, process)
+        return list
+
+    def process(self, container, list):
+        "Add allowed containers."
+        name = container.__class__.__name__
+        if name in self.allowed:
+            list.append(container)
+        else:
+            Trace.error('Unknown container class ' + name)
+
+    def safeclone(self, container):
+        "Return a new container with contents only in a safe list, recursively."
+        clone = Cloner.clone(container)
+        clone.output = container.output
+        clone.contents = self.extract(container)
+        return clone
+
+
+class Parser:
+    "A generic parser"
+
+    def __init__(self):
+        self.begin = 0
+        self.parameters = {}
+
+    def parseheader(self, reader):
+        "Parse the header"
+        header = reader.currentline().split()
+        reader.nextline()
+        self.begin = reader.linenumber
+        return header
+
+    def parseparameter(self, reader):
+        "Parse a parameter"
+        split = reader.currentline().strip().split(' ', 1)
+        reader.nextline()
+        if len(split) == 0:
+            return
+        key = split[0]
+        if len(split) == 1:
+            self.parameters[key] = True
+            return
+        if '"' not in split[1]:
+            self.parameters[key] = split[1].strip()
+            return
+        doublesplit = split[1].split('"')
+        self.parameters[key] = doublesplit[1]
+
+    def parseending(self, reader, process):
+        "Parse until the current ending is found"
+        if not self.ending:
+            Trace.error('No ending for ' + str(self))
+            return
+        while not reader.currentline().startswith(self.ending):
+            process()
+
+    def parsecontainer(self, reader, contents):
+        container = self.factory.createcontainer(reader)
+        if container:
+            container.parent = self.parent
+            contents.append(container)
+
+    def __str__(self):
+        "Return a description"
+        return self.__class__.__name__ + ' (' + str(self.begin) + ')'
+
+
+class LoneCommand(Parser):
+    "A parser for just one command line"
+
+    def parse(self, reader):
+        "Read nothing"
+        return []
+
+
+class TextParser(Parser):
+    "A parser for a command and a bit of text"
+
+    stack = []
+
+    def __init__(self, container):
+        Parser.__init__(self)
+        self.ending = None
+        if container.__class__.__name__ in ContainerConfig.endings:
+            self.ending = ContainerConfig.endings[container.__class__.__name__]
+        self.endings = []
+
+    def parse(self, reader):
+        "Parse lines as long as they are text"
+        TextParser.stack.append(self.ending)
+        self.endings = TextParser.stack + [ContainerConfig.endings['Layout'],
+                                           ContainerConfig.endings['Inset'],
+                                           self.ending]
+        contents = []
+        while not self.isending(reader):
+            self.parsecontainer(reader, contents)
+        return contents
+
+    def isending(self, reader):
+        "Check if text is ending"
+        current = reader.currentline().split()
+        if len(current) == 0:
+            return False
+        if current[0] in self.endings:
+            if current[0] in TextParser.stack:
+                TextParser.stack.remove(current[0])
+            else:
+                TextParser.stack = []
+            return True
+        return False
+
+
+class ExcludingParser(Parser):
+    "A parser that excludes the final line"
+
+    def parse(self, reader):
+        "Parse everything up to (and excluding) the final line"
+        contents = []
+        self.parseending(reader, lambda: self.parsecontainer(reader, contents))
+        return contents
+
+
+class BoundedParser(ExcludingParser):
+    "A parser bound by a final line"
+
+    def parse(self, reader):
+        "Parse everything, including the final line"
+        contents = ExcludingParser.parse(self, reader)
+        # skip last line
+        reader.nextline()
+        return contents
+
+
+class BoundedDummy(Parser):
+    "A bound parser that ignores everything"
+
+    def parse(self, reader):
+        "Parse the contents of the container"
+        self.parseending(reader, lambda: reader.nextline())
+        # skip last line
+        reader.nextline()
+        return []
+
+
+class StringParser(Parser):
+    "Parses just a string"
+
+    def parseheader(self, reader):
+        "Do nothing, just take note"
+        self.begin = reader.linenumber + 1
+        return []
+
+    def parse(self, reader):
+        "Parse a single line"
+        contents = reader.currentline()
+        reader.nextline()
+        return contents
+
+
+class ContainerOutput:
+    "The generic HTML output for a container."
+
+    def gethtml(self, container):
+        "Show an error."
+        Trace.error('gethtml() not implemented for ' + str(self))
+
+    def isempty(self):
+        "Decide if the output is empty: by default, not empty."
+        return False
+
+
+class EmptyOutput(ContainerOutput):
+
+    def gethtml(self, container):
+        "Return empty HTML code."
+        return []
+
+    def isempty(self):
+        "This output is particularly empty."
+        return True
+
+
+class FixedOutput(ContainerOutput):
+    "Fixed output"
+
+    def gethtml(self, container):
+        "Return constant HTML code"
+        return container.html
+
+
+class ContentsOutput(ContainerOutput):
+    "Outputs the contents converted to HTML"
+
+    def gethtml(self, container):
+        "Return the HTML code"
+        html = []
+        if container.contents is None:
+            return html
+        for element in container.contents:
+            if not hasattr(element, 'gethtml'):
+                Trace.error('No html in ' + element.__class__.__name__ + ': ' + str(element))
+                return html
+            html += element.gethtml()
+        return html
+
+
+class TaggedOutput(ContentsOutput):
+    "Outputs an HTML tag surrounding the contents."
+
+    tag = None
+    breaklines = False
+    empty = False
+
+    def settag(self, tag, breaklines=False, empty=False):
+        "Set the value for the tag and other attributes."
+        self.tag = tag
+        if breaklines:
+            self.breaklines = breaklines
+        if empty:
+            self.empty = empty
+        return self
+
+    def setbreaklines(self, breaklines):
+        "Set the value for breaklines."
+        self.breaklines = breaklines
+        return self
+
+    def gethtml(self, container):
+        "Return the HTML code."
+        if self.empty:
+            return [self.selfclosing(container)]
+        html = [self.open(container)]
+        html += ContentsOutput.gethtml(self, container)
+        html.append(self.close(container))
+        return html
+
+    def open(self, container):
+        "Get opening line."
+        if not self.checktag(container):
+            return ''
+        open = '<' + self.tag + '>'
+        if self.breaklines:
+            return open + '\n'
+        return open
+
+    def close(self, container):
+        "Get closing line."
+        if not self.checktag(container):
+            return ''
+        close = '</' + self.tag.split()[0] + '>'
+        if self.breaklines:
+            return '\n' + close + '\n'
+        return close
+
+    def selfclosing(self, container):
+        "Get self-closing line."
+        if not self.checktag(container):
+            return ''
+        selfclosing = '<' + self.tag + '/>'
+        if self.breaklines:
+            return selfclosing + '\n'
+        return selfclosing
+
+    def checktag(self, container):
+        "Check that the tag is valid."
+        if not self.tag:
+            Trace.error('No tag in ' + str(container))
+            return False
+        if self.tag == '':
+            return False
+        return True
+
+
+class FilteredOutput(ContentsOutput):
+    "Returns the output in the contents, but filtered:"
+    "some strings are replaced by others."
+
+    def __init__(self):
+        "Initialize the filters."
+        self.filters = []
+
+    def addfilter(self, original, replacement):
+        "Add a new filter: replace the original by the replacement."
+        self.filters.append((original, replacement))
+
+    def gethtml(self, container):
+        "Return the HTML code"
+        result = []
+        html = ContentsOutput.gethtml(self, container)
+        for line in html:
+            result.append(self.filter(line))
+        return result
+
+    def filter(self, line):
+        "Filter a single line with all available filters."
+        for original, replacement in self.filters:
+            if original in line:
+                line = line.replace(original, replacement)
+        return line
+
+
+class StringOutput(ContainerOutput):
+    "Returns a bare string as output"
+
+    def gethtml(self, container):
+        "Return a bare string"
+        return [container.string]
+
+
+class Globable:
+    """A bit of text which can be globbed (lumped together in bits).
+    Methods current(), skipcurrent(), checkfor() and isout() have to be
+    implemented by subclasses."""
+
+    leavepending = False
+
+    def __init__(self):
+        self.endinglist = EndingList()
+
+    def checkbytemark(self):
+        "Check for a Unicode byte mark and skip it."
+        if self.finished():
+            return
+        if ord(self.current()) == 0xfeff:
+            self.skipcurrent()
+
+    def isout(self):
+        "Find out if we are out of the position yet."
+        Trace.error('Unimplemented isout()')
+        return True
+
+    def current(self):
+        "Return the current character."
+        Trace.error('Unimplemented current()')
+        return ''
+
+    def checkfor(self, string):
+        "Check for the given string in the current position."
+        Trace.error('Unimplemented checkfor()')
+        return False
+
+    def finished(self):
+        "Find out if the current text has finished."
+        if self.isout():
+            if not self.leavepending:
+                self.endinglist.checkpending()
+            return True
+        return self.endinglist.checkin(self)
+
+    def skipcurrent(self):
+        "Return the current character and skip it."
+        Trace.error('Unimplemented skipcurrent()')
+        return ''
+
+    def glob(self, currentcheck):
+        "Glob a bit of text that satisfies a check on the current char."
+        glob = ''
+        while not self.finished() and currentcheck():
+            glob += self.skipcurrent()
+        return glob
+
+    def globalpha(self):
+        "Glob a bit of alpha text"
+        return self.glob(lambda: self.current().isalpha())
+
+    def globnumber(self):
+        "Glob a row of digits."
+        return self.glob(lambda: self.current().isdigit())
+
+    def isidentifier(self):
+        "Return if the current character is alphanumeric or _."
+        if self.current().isalnum() or self.current() == '_':
+            return True
+        return False
+
+    def globidentifier(self):
+        "Glob alphanumeric and _ symbols."
+        return self.glob(self.isidentifier)
+
+    def isvalue(self):
+        "Return if the current character is a value character:"
+        "not a bracket or a space."
+        if self.current().isspace():
+            return False
+        if self.current() in '{}()':
+            return False
+        return True
+
+    def globvalue(self):
+        "Glob a value: any symbols but brackets."
+        return self.glob(self.isvalue)
+
+    def skipspace(self):
+        "Skip all whitespace at current position."
+        return self.glob(lambda: self.current().isspace())
+
+    def globincluding(self, magicchar):
+        "Glob a bit of text up to (including) the magic char."
+        glob = self.glob(lambda: self.current() != magicchar) + magicchar
+        self.skip(magicchar)
+        return glob
+
+    def globexcluding(self, excluded):
+        "Glob a bit of text up until (excluding) any excluded character."
+        return self.glob(lambda: self.current() not in excluded)
+
+    def pushending(self, ending, optional=False):
+        "Push a new ending to the bottom"
+        self.endinglist.add(ending, optional)
+
+    def popending(self, expected=None):
+        "Pop the ending found at the current position"
+        if self.isout() and self.leavepending:
+            return expected
+        ending = self.endinglist.pop(self)
+        if expected and expected != ending:
+            Trace.error('Expected ending ' + expected + ', got ' + ending)
+        self.skip(ending)
+        return ending
+
+    def nextending(self):
+        "Return the next ending in the queue."
+        nextending = self.endinglist.findending(self)
+        if not nextending:
+            return None
+        return nextending.ending
+
+
+class EndingList:
+    "A list of position endings"
+
+    def __init__(self):
+        self.endings = []
+
+    def add(self, ending, optional=False):
+        "Add a new ending to the list"
+        self.endings.append(PositionEnding(ending, optional))
+
+    def pickpending(self, pos):
+        "Pick any pending endings from a parse position."
+        self.endings += pos.endinglist.endings
+
+    def checkin(self, pos):
+        "Search for an ending"
+        if self.findending(pos):
+            return True
+        return False
+
+    def pop(self, pos):
+        "Remove the ending at the current position"
+        if pos.isout():
+            Trace.error('No ending out of bounds')
+            return ''
+        ending = self.findending(pos)
+        if not ending:
+            Trace.error('No ending at ' + pos.current())
+            return ''
+        for each in reversed(self.endings):
+            self.endings.remove(each)
+            if each == ending:
+                return each.ending
+            elif not each.optional:
+                Trace.error('Removed non-optional ending ' + each)
+        Trace.error('No endings left')
+        return ''
+
+    def findending(self, pos):
+        "Find the ending at the current position"
+        if len(self.endings) == 0:
+            return None
+        for index, ending in enumerate(reversed(self.endings)):
+            if ending.checkin(pos):
+                return ending
+            if not ending.optional:
+                return None
+        return None
+
+    def checkpending(self):
+        "Check if there are any pending endings"
+        if len(self.endings) != 0:
+            Trace.error('Pending ' + str(self) + ' left open')
+
+    def __str__(self):
+        "Printable representation"
+        string = 'endings ['
+        for ending in self.endings:
+            string += str(ending) + ','
+        if len(self.endings) > 0:
+            string = string[:-1]
+        return string + ']'
+
+
+class PositionEnding:
+    "An ending for a parsing position"
+
+    def __init__(self, ending, optional):
+        self.ending = ending
+        self.optional = optional
+
+    def checkin(self, pos):
+        "Check for the ending"
+        return pos.checkfor(self.ending)
+
+    def __str__(self):
+        "Printable representation"
+        string = 'Ending ' + self.ending
+        if self.optional:
+            string += ' (optional)'
+        return string
+
+
+class Position(Globable):
+    """A position in a text to parse.
+    Including those in Globable, functions to implement by subclasses are:
+    skip(), identifier(), extract(), isout() and current()."""
+
+    def __init__(self):
+        Globable.__init__(self)
+
+    def skip(self, string):
+        "Skip a string"
+        Trace.error('Unimplemented skip()')
+
+    def identifier(self):
+        "Return an identifier for the current position."
+        Trace.error('Unimplemented identifier()')
+        return 'Error'
+
+    def extract(self, length):
+        "Extract the next string of the given length, or None if not enough text,"
+        "without advancing the parse position."
+        Trace.error('Unimplemented extract()')
+        return None
+
+    def checkfor(self, string):
+        "Check for a string at the given position."
+        return string == self.extract(len(string))
+
+    def checkforlower(self, string):
+        "Check for a string in lower case."
+        extracted = self.extract(len(string))
+        if not extracted:
+            return False
+        return string.lower() == self.extract(len(string)).lower()
+
+    def skipcurrent(self):
+        "Return the current character and skip it."
+        current = self.current()
+        self.skip(current)
+        return current
+
+    def __next__(self):
+        "Advance the position and return the next character."
+        self.skipcurrent()
+        return self.current()
+
+    def checkskip(self, string):
+        "Check for a string at the given position; if there, skip it"
+        if not self.checkfor(string):
+            return False
+        self.skip(string)
+        return True
+
+    def error(self, message):
+        "Show an error message and the position identifier."
+        Trace.error(message + ': ' + self.identifier())
+
+
+class TextPosition(Position):
+    "A parse position based on a raw text."
+
+    def __init__(self, text):
+        "Create the position from some text."
+        Position.__init__(self)
+        self.pos = 0
+        self.text = text
+        self.checkbytemark()
+
+    def skip(self, string):
+        "Skip a string of characters."
+        self.pos += len(string)
+
+    def identifier(self):
+        "Return a sample of the remaining text."
+        length = 30
+        if self.pos + length > len(self.text):
+            length = len(self.text) - self.pos
+        return '*' + self.text[self.pos:self.pos + length] + '*'
+
+    def isout(self):
+        "Find out if we are out of the text yet."
+        return self.pos >= len(self.text)
+
+    def current(self):
+        "Return the current character, assuming we are not out."
+        return self.text[self.pos]
+
+    def extract(self, length):
+        "Extract the next string of the given length, or None if not enough text."
+        if self.pos + length > len(self.text):
+            return None
+        return self.text[self.pos : self.pos + length]                 # noqa: E203
+
+
+class Container:
+    "A container for text and objects in a lyx file"
+
+    partkey = None
+    parent = None
+    begin = None
+
+    def __init__(self):
+        self.contents = list()
+
+    def process(self):
+        "Process contents"
+        pass
+
+    def gethtml(self):
+        "Get the resulting HTML"
+        html = self.output.gethtml(self)
+        if isinstance(html, str):
+            Trace.error('Raw string ' + html)
+            html = [html]
+        return html
+
+    def escape(self, line, replacements=EscapeConfig.entities):
+        "Escape a line with replacements from a map"
+        pieces = sorted(replacements.keys())
+        # do them in order
+        for piece in pieces:
+            if piece in line:
+                line = line.replace(piece, replacements[piece])
+        return line
+
+    def escapeentities(self, line):
+        "Escape all Unicode characters to HTML entities."
+        result = ''
+        pos = TextPosition(line)
+        while not pos.finished():
+            if ord(pos.current()) > 128:
+                codepoint = hex(ord(pos.current()))
+                if codepoint == '0xd835':
+                    codepoint = hex(ord(next(pos)) + 0xf800)
+                result += '&#' + codepoint[1:] + ';'
+            else:
+                result += pos.current()
+            pos.skipcurrent()
+        return result
+
+    def searchall(self, type):
+        "Search for all embedded containers of a given type"
+        list = []
+        self.searchprocess(type, lambda container: list.append(container))
+        return list
+
+    def searchremove(self, type):
+        "Search for all containers of a type and remove them"
+        list = self.searchall(type)
+        for container in list:
+            container.parent.contents.remove(container)
+        return list
+
+    def searchprocess(self, type, process):
+        "Search for elements of a given type and process them"
+        self.locateprocess(lambda container: isinstance(container, type), process)
+
+    def locateprocess(self, locate, process):
+        "Search for all embedded containers and process them"
+        for container in self.contents:
+            container.locateprocess(locate, process)
+            if locate(container):
+                process(container)
+
+    def recursivesearch(self, locate, recursive, process):
+        "Perform a recursive search in the container."
+        for container in self.contents:
+            if recursive(container):
+                container.recursivesearch(locate, recursive, process)
+            if locate(container):
+                process(container)
+
+    def extracttext(self):
+        "Extract all text from allowed containers."
+        constants = ContainerExtractor(ContainerConfig.extracttext).extract(self)
+        return ''.join(constant.string for constant in constants)
+
+    def group(self, index, group, isingroup):
+        "Group some adjoining elements into a group"
+        if index >= len(self.contents):
+            return
+        if hasattr(self.contents[index], 'grouped'):
+            return
+        while index < len(self.contents) and isingroup(self.contents[index]):
+            self.contents[index].grouped = True
+            group.contents.append(self.contents[index])
+            self.contents.pop(index)
+        self.contents.insert(index, group)
+
+    def remove(self, index):
+        "Remove a container but leave its contents"
+        container = self.contents[index]
+        self.contents.pop(index)
+        while len(container.contents) > 0:
+            self.contents.insert(index, container.contents.pop())
+
+    def tree(self, level=0):
+        "Show in a tree"
+        Trace.debug("  " * level + str(self))
+        for container in self.contents:
+            container.tree(level + 1)
+
+    def getparameter(self, name):
+        "Get the value of a parameter, if present."
+        if name not in self.parameters:
+            return None
+        return self.parameters[name]
+
+    def getparameterlist(self, name):
+        "Get the value of a comma-separated parameter as a list."
+        paramtext = self.getparameter(name)
+        if not paramtext:
+            return []
+        return paramtext.split(',')
+
+    def hasemptyoutput(self):
+        "Check if the parent's output is empty."
+        current = self.parent
+        while current:
+            if current.output.isempty():
+                return True
+            current = current.parent
+        return False
+
+    def __str__(self):
+        "Get a description"
+        if not self.begin:
+            return self.__class__.__name__
+        return self.__class__.__name__ + '@' + str(self.begin)
+
+
+class BlackBox(Container):
+    "A container that does not output anything"
+
+    def __init__(self):
+        self.parser = LoneCommand()
+        self.output = EmptyOutput()
+        self.contents = []
+
+
+class StringContainer(Container):
+    "A container for a single string"
+
+    parsed = None
+
+    def __init__(self):
+        self.parser = StringParser()
+        self.output = StringOutput()
+        self.string = ''
+
+    def process(self):
+        "Replace special chars from the contents."
+        if self.parsed:
+            self.string = self.replacespecial(self.parsed)
+            self.parsed = None
+
+    def replacespecial(self, line):
+        "Replace all special chars from a line"
+        replaced = self.escape(line, EscapeConfig.entities)
+        replaced = self.changeline(replaced)
+        if ContainerConfig.string['startcommand'] in replaced and len(replaced) > 1:
+            # unprocessed commands
+            if self.begin:
+                message = 'Unknown command at ' + str(self.begin) + ': '
+            else:
+                message = 'Unknown command: '
+            Trace.error(message + replaced.strip())
+        return replaced
+
+    def changeline(self, line):
+        return self.escape(line, EscapeConfig.chars)
+
+    def extracttext(self):
+        "Return all text."
+        return self.string
+
+    def __str__(self):
+        "Return a printable representation."
+        result = 'StringContainer'
+        if self.begin:
+            result += '@' + str(self.begin)
+        ellipsis = '...'
+        if len(self.string.strip()) <= 15:
+            ellipsis = ''
+        return result + ' (' + self.string.strip()[:15] + ellipsis + ')'
+
+
+class Constant(StringContainer):
+    "A constant string"
+
+    def __init__(self, text):
+        self.contents = []
+        self.string = text
+        self.output = StringOutput()
+
+    def __str__(self):
+        return 'Constant: ' + self.string
+
+
+class DocumentParameters:
+    "Global parameters for the document."
+
+    displaymode = False
+
+
+class FormulaParser(Parser):
+    "Parses a formula"
+
+    def parseheader(self, reader):
+        "See if the formula is inlined"
+        self.begin = reader.linenumber + 1
+        type = self.parsetype(reader)
+        if not type:
+            reader.nextline()
+            type = self.parsetype(reader)
+            if not type:
+                Trace.error('Unknown formula type in ' + reader.currentline().strip())
+                return ['unknown']
+        return [type]
+
+    def parsetype(self, reader):
+        "Get the formula type from the first line."
+        if reader.currentline().find(FormulaConfig.starts['simple']) >= 0:
+            return 'inline'
+        if reader.currentline().find(FormulaConfig.starts['complex']) >= 0:
+            return 'block'
+        if reader.currentline().find(FormulaConfig.starts['unnumbered']) >= 0:
+            return 'block'
+        if reader.currentline().find(FormulaConfig.starts['beginbefore']) >= 0:
+            return 'numbered'
+        return None
+
+    def parse(self, reader):
+        "Parse the formula until the end"
+        formula = self.parseformula(reader)
+        while not reader.currentline().startswith(self.ending):
+            stripped = reader.currentline().strip()
+            if len(stripped) > 0:
+                Trace.error('Unparsed formula line ' + stripped)
+            reader.nextline()
+        reader.nextline()
+        return formula
+
+    def parseformula(self, reader):
+        "Parse the formula contents"
+        simple = FormulaConfig.starts['simple']
+        if simple in reader.currentline():
+            rest = reader.currentline().split(simple, 1)[1]
+            if simple in rest:
+                # formula is $...$
+                return self.parsesingleliner(reader, simple, simple)
+            # formula is multiline $...$
+            return self.parsemultiliner(reader, simple, simple)
+        if FormulaConfig.starts['complex'] in reader.currentline():
+            # formula of the form \[...\]
+            return self.parsemultiliner(reader, FormulaConfig.starts['complex'],
+                                        FormulaConfig.endings['complex'])
+        beginbefore = FormulaConfig.starts['beginbefore']
+        beginafter = FormulaConfig.starts['beginafter']
+        if beginbefore in reader.currentline():
+            if reader.currentline().strip().endswith(beginafter):
+                current = reader.currentline().strip()
+                endsplit = current.split(beginbefore)[1].split(beginafter)
+                startpiece = beginbefore + endsplit[0] + beginafter
+                endbefore = FormulaConfig.endings['endbefore']
+                endafter = FormulaConfig.endings['endafter']
+                endpiece = endbefore + endsplit[0] + endafter
+                return startpiece + self.parsemultiliner(reader, startpiece, endpiece) + endpiece
+            Trace.error('Missing ' + beginafter + ' in ' + reader.currentline())
+            return ''
+        begincommand = FormulaConfig.starts['command']
+        beginbracket = FormulaConfig.starts['bracket']
+        if begincommand in reader.currentline() and beginbracket in reader.currentline():
+            endbracket = FormulaConfig.endings['bracket']
+            return self.parsemultiliner(reader, beginbracket, endbracket)
+        Trace.error('Formula beginning ' + reader.currentline() + ' is unknown')
+        return ''
+
+    def parsesingleliner(self, reader, start, ending):
+        "Parse a formula in one line"
+        line = reader.currentline().strip()
+        if start not in line:
+            Trace.error('Line ' + line + ' does not contain formula start ' + start)
+            return ''
+        if not line.endswith(ending):
+            Trace.error('Formula ' + line + ' does not end with ' + ending)
+            return ''
+        index = line.index(start)
+        rest = line[index + len(start):-len(ending)]
+        reader.nextline()
+        return rest
+
+    def parsemultiliner(self, reader, start, ending):
+        "Parse a formula in multiple lines"
+        formula = ''
+        line = reader.currentline()
+        if start not in line:
+            Trace.error('Line ' + line.strip() + ' does not contain formula start ' + start)
+            return ''
+        index = line.index(start)
+        line = line[index + len(start):].strip()
+        while not line.endswith(ending):
+            formula += line + '\n'
+            reader.nextline()
+            line = reader.currentline()
+        formula += line[:-len(ending)]
+        reader.nextline()
+        return formula
+
+
+class FormulaBit(Container):
+    "A bit of a formula"
+
+    type = None
+    size = 1
+    original = ''
+
+    def __init__(self):
+        "The formula bit type can be 'alpha', 'number', 'font'."
+        self.contents = []
+        self.output = ContentsOutput()
+
+    def setfactory(self, factory):
+        "Set the internal formula factory."
+        self.factory = factory
+        return self
+
+    def add(self, bit):
+        "Add any kind of formula bit already processed"
+        self.contents.append(bit)
+        self.original += bit.original
+        bit.parent = self
+
+    def skiporiginal(self, string, pos):
+        "Skip a string and add it to the original formula"
+        self.original += string
+        if not pos.checkskip(string):
+            Trace.error('String ' + string + ' not at ' + pos.identifier())
+
+    def computesize(self):
+        "Compute the size of the bit as the max of the sizes of all contents."
+        if len(self.contents) == 0:
+            return 1
+        self.size = max(element.size for element in self.contents)
+        return self.size
+
+    def clone(self):
+        "Return a copy of itself."
+        return self.factory.parseformula(self.original)
+
+    def __str__(self):
+        "Get a string representation"
+        return self.__class__.__name__ + ' read in ' + self.original
+
+
+class TaggedBit(FormulaBit):
+    "A tagged string in a formula"
+
+    def constant(self, constant, tag):
+        "Set the constant and the tag"
+        self.output = TaggedOutput().settag(tag)
+        self.add(FormulaConstant(constant))
+        return self
+
+    def complete(self, contents, tag, breaklines=False):
+        "Set the constant and the tag"
+        self.contents = contents
+        self.output = TaggedOutput().settag(tag, breaklines)
+        return self
+
+    def selfcomplete(self, tag):
+        "Set the self-closing tag, no contents (as in <hr/>)."
+        self.output = TaggedOutput().settag(tag, empty=True)
+        return self
+
+
+class FormulaConstant(Constant):
+    "A constant string in a formula"
+
+    def __init__(self, string):
+        "Set the constant string"
+        Constant.__init__(self, string)
+        self.original = string
+        self.size = 1
+        self.type = None
+
+    def computesize(self):
+        "Compute the size of the constant: always 1."
+        return self.size
+
+    def clone(self):
+        "Return a copy of itself."
+        return FormulaConstant(self.original)
+
+    def __str__(self):
+        "Return a printable representation."
+        return 'Formula constant: ' + self.string
+
+
+class RawText(FormulaBit):
+    "A bit of text inside a formula"
+
+    def detect(self, pos):
+        "Detect a bit of raw text"
+        return pos.current().isalpha()
+
+    def parsebit(self, pos):
+        "Parse alphabetic text"
+        alpha = pos.globalpha()
+        self.add(FormulaConstant(alpha))
+        self.type = 'alpha'
+
+
+class FormulaSymbol(FormulaBit):
+    "A symbol inside a formula"
+
+    modified = FormulaConfig.modified
+    unmodified = FormulaConfig.unmodified['characters']
+
+    def detect(self, pos):
+        "Detect a symbol"
+        if pos.current() in FormulaSymbol.unmodified:
+            return True
+        if pos.current() in FormulaSymbol.modified:
+            return True
+        return False
+
+    def parsebit(self, pos):
+        "Parse the symbol"
+        if pos.current() in FormulaSymbol.unmodified:
+            self.addsymbol(pos.current(), pos)
+            return
+        if pos.current() in FormulaSymbol.modified:
+            self.addsymbol(FormulaSymbol.modified[pos.current()], pos)
+            return
+        Trace.error('Symbol ' + pos.current() + ' not found')
+
+    def addsymbol(self, symbol, pos):
+        "Add a symbol"
+        self.skiporiginal(pos.current(), pos)
+        self.contents.append(FormulaConstant(symbol))
+
+
+class FormulaNumber(FormulaBit):
+    "A string of digits in a formula"
+
+    def detect(self, pos):
+        "Detect a digit"
+        return pos.current().isdigit()
+
+    def parsebit(self, pos):
+        "Parse a bunch of digits"
+        digits = pos.glob(lambda: pos.current().isdigit())
+        self.add(FormulaConstant(digits))
+        self.type = 'number'
+
+
+class Comment(FormulaBit):
+    "A LaTeX comment: % to the end of the line."
+
+    start = FormulaConfig.starts['comment']
+
+    def detect(self, pos):
+        "Detect the %."
+        return pos.current() == self.start
+
+    def parsebit(self, pos):
+        "Parse to the end of the line."
+        self.original += pos.globincluding('\n')
+
+
+class WhiteSpace(FormulaBit):
+    "Some white space inside a formula."
+
+    def detect(self, pos):
+        "Detect the white space."
+        return pos.current().isspace()
+
+    def parsebit(self, pos):
+        "Parse all whitespace."
+        self.original += pos.skipspace()
+
+    def __str__(self):
+        "Return a printable representation."
+        return 'Whitespace: *' + self.original + '*'
+
+
+class Bracket(FormulaBit):
+    "A {} bracket inside a formula"
+
+    start = FormulaConfig.starts['bracket']
+    ending = FormulaConfig.endings['bracket']
+
+    def __init__(self):
+        "Create a (possibly literal) new bracket"
+        FormulaBit.__init__(self)
+        self.inner = None
+
+    def detect(self, pos):
+        "Detect the start of a bracket"
+        return pos.checkfor(self.start)
+
+    def parsebit(self, pos):
+        "Parse the bracket"
+        self.parsecomplete(pos, self.innerformula)
+        return self
+
+    def parsetext(self, pos):
+        "Parse a text bracket"
+        self.parsecomplete(pos, self.innertext)
+        return self
+
+    def parseliteral(self, pos):
+        "Parse a literal bracket"
+        self.parsecomplete(pos, self.innerliteral)
+        return self
+
+    def parsecomplete(self, pos, innerparser):
+        "Parse the start and end marks"
+        if not pos.checkfor(self.start):
+            Trace.error('Bracket should start with ' + self.start + ' at ' + pos.identifier())
+            return None
+        self.skiporiginal(self.start, pos)
+        pos.pushending(self.ending)
+        innerparser(pos)
+        self.original += pos.popending(self.ending)
+        self.computesize()
+
+    def innerformula(self, pos):
+        "Parse a whole formula inside the bracket"
+        while not pos.finished():
+            self.add(self.factory.parseany(pos))
+
+    def innertext(self, pos):
+        "Parse some text inside the bracket, following textual rules."
+        specialchars = list(FormulaConfig.symbolfunctions.keys())
+        specialchars.append(FormulaConfig.starts['command'])
+        specialchars.append(FormulaConfig.starts['bracket'])
+        specialchars.append(Comment.start)
+        while not pos.finished():
+            if pos.current() in specialchars:
+                self.add(self.factory.parseany(pos))
+                if pos.checkskip(' '):
+                    self.original += ' '
+            else:
+                self.add(FormulaConstant(pos.skipcurrent()))
+
+    def innerliteral(self, pos):
+        "Parse a literal inside the bracket, which does not generate HTML."
+        self.literal = ''
+        while not pos.finished() and not pos.current() == self.ending:
+            if pos.current() == self.start:
+                self.parseliteral(pos)
+            else:
+                self.literal += pos.skipcurrent()
+        self.original += self.literal
+
+
+class SquareBracket(Bracket):
+    "A [] bracket inside a formula"
+
+    start = FormulaConfig.starts['squarebracket']
+    ending = FormulaConfig.endings['squarebracket']
+
+    def clone(self):
+        "Return a new square bracket with the same contents."
+        bracket = SquareBracket()
+        bracket.contents = self.contents
+        return bracket
+
+
+class MathsProcessor:
+    "A processor for a maths construction inside the FormulaProcessor."
+
+    def process(self, contents, index):
+        "Process an element inside a formula."
+        Trace.error('Unimplemented process() in ' + str(self))
+
+    def __str__(self):
+        "Return a printable description."
+        return 'Maths processor ' + self.__class__.__name__
+
+
+class FormulaProcessor:
+    "A processor specifically for formulas."
+
+    processors = []
+
+    def process(self, bit):
+        "Process the contents of every formula bit, recursively."
+        self.processcontents(bit)
+        self.processinsides(bit)
+        self.traversewhole(bit)
+
+    def processcontents(self, bit):
+        "Process the contents of a formula bit."
+        if not isinstance(bit, FormulaBit):
+            return
+        bit.process()
+        for element in bit.contents:
+            self.processcontents(element)
+
+    def processinsides(self, bit):
+        "Process the insides (limits, brackets) in a formula bit."
+        if not isinstance(bit, FormulaBit):
+            return
+        for index, element in enumerate(bit.contents):
+            for processor in self.processors:
+                processor.process(bit.contents, index)
+            # continue with recursive processing
+            self.processinsides(element)
+
+    def traversewhole(self, formula):
+        "Traverse over the contents to alter variables and space units."
+        last = None
+        for bit, contents in self.traverse(formula):
+            if bit.type == 'alpha':
+                self.italicize(bit, contents)
+            elif bit.type == 'font' and last and last.type == 'number':
+                bit.contents.insert(0, FormulaConstant('\u2009'))
+            last = bit
+
+    def traverse(self, bit):
+        "Traverse a formula and yield a flattened structure of (bit, list) pairs."
+        for element in bit.contents:
+            if hasattr(element, 'type') and element.type:
+                yield element, bit.contents
+            elif isinstance(element, FormulaBit):
+                yield from self.traverse(element)
+
+    def italicize(self, bit, contents):
+        "Italicize the given bit of text."
+        index = contents.index(bit)
+        contents[index] = TaggedBit().complete([bit], 'i')
+
+
+class Formula(Container):
+    "A LaTeX formula"
+
+    def __init__(self):
+        self.parser = FormulaParser()
+        self.output = TaggedOutput().settag('span class="formula"')
+
+    def process(self):
+        "Convert the formula to tags"
+        if self.header[0] == 'inline':
+            DocumentParameters.displaymode = False
+        else:
+            DocumentParameters.displaymode = True
+            self.output.settag('div class="formula"', True)
+        self.classic()
+
+    def classic(self):
+        "Make the contents using classic output generation with XHTML and CSS."
+        whole = FormulaFactory().parseformula(self.parsed)
+        FormulaProcessor().process(whole)
+        whole.parent = self
+        self.contents = [whole]
+
+    def parse(self, pos):
+        "Parse using a parse position instead of self.parser."
+        if pos.checkskip('$$'):
+            self.parsedollarblock(pos)
+        elif pos.checkskip('$'):
+            self.parsedollarinline(pos)
+        elif pos.checkskip('\\('):
+            self.parseinlineto(pos, '\\)')
+        elif pos.checkskip('\\['):
+            self.parseblockto(pos, '\\]')
+        else:
+            pos.error('Unparseable formula')
+        self.process()
+        return self
+
+    def parsedollarinline(self, pos):
+        "Parse a $...$ formula."
+        self.header = ['inline']
+        self.parsedollar(pos)
+
+    def parsedollarblock(self, pos):
+        "Parse a $$...$$ formula."
+        self.header = ['block']
+        self.parsedollar(pos)
+        if not pos.checkskip('$'):
+            pos.error('Formula should be $$...$$, but last $ is missing.')
+
+    def parsedollar(self, pos):
+        "Parse to the next $."
+        pos.pushending('$')
+        self.parsed = pos.globexcluding('$')
+        pos.popending('$')
+
+    def parseinlineto(self, pos, limit):
+        "Parse a \\(...\\) formula."
+        self.header = ['inline']
+        self.parseupto(pos, limit)
+
+    def parseblockto(self, pos, limit):
+        "Parse a \\[...\\] formula."
+        self.header = ['block']
+        self.parseupto(pos, limit)
+
+    def parseupto(self, pos, limit):
+        "Parse a formula that ends with the given command."
+        pos.pushending(limit)
+        self.parsed = pos.glob(lambda: True)
+        pos.popending(limit)
+
+    def __str__(self):
+        "Return a printable representation."
+        if self.partkey and self.partkey.number:
+            return 'Formula (' + self.partkey.number + ')'
+        return 'Unnumbered formula'
+
+
+class WholeFormula(FormulaBit):
+    "Parse a whole formula"
+
+    def detect(self, pos):
+        "Not outside the formula is enough."
+        return not pos.finished()
+
+    def parsebit(self, pos):
+        "Parse with any formula bit"
+        while not pos.finished():
+            self.add(self.factory.parseany(pos))
+
+
+class FormulaFactory:
+    "Construct bits of formula"
+
+    # bit types will be appended later
+    types = [FormulaSymbol, RawText, FormulaNumber, Bracket, Comment, WhiteSpace]
+    skippedtypes = [Comment, WhiteSpace]
+    defining = False
+
+    def __init__(self):
+        "Initialize the map of instances."
+        self.instances = {}
+
+    def detecttype(self, type, pos):
+        "Detect a bit of a given type."
+        if pos.finished():
+            return False
+        return self.instance(type).detect(pos)
+
+    def instance(self, type):
+        "Get an instance of the given type."
+        if type not in self.instances or not self.instances[type]:
+            self.instances[type] = self.create(type)
+        return self.instances[type]
+
+    def create(self, type):
+        "Create a new formula bit of the given type."
+        return Cloner.create(type).setfactory(self)
+
+    def clearskipped(self, pos):
+        "Clear any skipped types."
+        while not pos.finished():
+            if not self.skipany(pos):
+                return
+        return
+
+    def skipany(self, pos):
+        "Skip any skipped types."
+        for type in self.skippedtypes:
+            if self.instance(type).detect(pos):
+                return self.parsetype(type, pos)
+        return None
+
+    def parseany(self, pos):
+        "Parse any formula bit at the current location."
+        for type in self.types + self.skippedtypes:
+            if self.detecttype(type, pos):
+                return self.parsetype(type, pos)
+        Trace.error('Unrecognized formula at ' + pos.identifier())
+        return FormulaConstant(pos.skipcurrent())
+
+    def parsetype(self, type, pos):
+        "Parse the given type and return it."
+        bit = self.instance(type)
+        self.instances[type] = None
+        returnedbit = bit.parsebit(pos)
+        if returnedbit:
+            return returnedbit.setfactory(self)
+        return bit
+
+    def parseformula(self, formula):
+        "Parse a string of text that contains a whole formula."
+        pos = TextPosition(formula)
+        whole = self.create(WholeFormula)
+        if whole.detect(pos):
+            whole.parsebit(pos)
+            return whole
+        # no formula found
+        if not pos.finished():
+            Trace.error('Unknown formula at: ' + pos.identifier())
+            whole.add(TaggedBit().constant(formula, 'span class="unknown"'))
+        return whole
+
+
+class FormulaCommand(FormulaBit):
+    "A LaTeX command inside a formula"
+
+    types = []
+    start = FormulaConfig.starts['command']
+    commandmap = None
+
+    def detect(self, pos):
+        "Find the current command."
+        return pos.checkfor(FormulaCommand.start)
+
+    def parsebit(self, pos):
+        "Parse the command."
+        command = self.extractcommand(pos)
+        bit = self.parsewithcommand(command, pos)
+        if bit:
+            return bit
+        if command.startswith('\\up') or command.startswith('\\Up'):
+            upgreek = self.parseupgreek(command, pos)
+            if upgreek:
+                return upgreek
+        if not self.factory.defining:
+            Trace.error('Unknown command ' + command)
+        self.output = TaggedOutput().settag('span class="unknown"')
+        self.add(FormulaConstant(command))
+        return None
+
+    def parsewithcommand(self, command, pos):
+        "Parse the command type once we have the command."
+        for type in FormulaCommand.types:
+            if command in type.commandmap:
+                return self.parsecommandtype(command, type, pos)
+        return None
+
+    def parsecommandtype(self, command, type, pos):
+        "Parse a given command type."
+        bit = self.factory.create(type)
+        bit.setcommand(command)
+        returned = bit.parsebit(pos)
+        if returned:
+            return returned
+        return bit
+
+    def extractcommand(self, pos):
+        "Extract the command from the current position."
+        if not pos.checkskip(FormulaCommand.start):
+            pos.error('Missing command start ' + FormulaCommand.start)
+            return
+        if pos.finished():
+            return self.emptycommand(pos)
+        if pos.current().isalpha():
+            # alpha command
+            command = FormulaCommand.start + pos.globalpha()
+            # skip mark of short command
+            pos.checkskip('*')
+            return command
+        # symbol command
+        return FormulaCommand.start + pos.skipcurrent()
+
+    def emptycommand(self, pos):
+        """Check for an empty command: look for command disguised as ending.
+        Special case against '{ \\{ \\} }' situation."""
+        command = ''
+        if not pos.isout():
+            ending = pos.nextending()
+            if ending and pos.checkskip(ending):
+                command = ending
+        return FormulaCommand.start + command
+
+    def parseupgreek(self, command, pos):
+        "Parse the Greek \\up command.."
+        if len(command) < 4:
+            return None
+        if command.startswith('\\up'):
+            upcommand = '\\' + command[3:]
+        elif pos.checkskip('\\Up'):
+            upcommand = '\\' + command[3:4].upper() + command[4:]
+        else:
+            Trace.error('Impossible upgreek command: ' + command)
+            return
+        upgreek = self.parsewithcommand(upcommand, pos)
+        if upgreek:
+            upgreek.type = 'font'
+        return upgreek
+
+
+class CommandBit(FormulaCommand):
+    "A formula bit that includes a command"
+
+    def setcommand(self, command):
+        "Set the command in the bit"
+        self.command = command
+        if self.commandmap:
+            self.original += command
+            self.translated = self.commandmap[self.command]
+
+    def parseparameter(self, pos):
+        "Parse a parameter at the current position"
+        self.factory.clearskipped(pos)
+        if pos.finished():
+            return None
+        parameter = self.factory.parseany(pos)
+        self.add(parameter)
+        return parameter
+
+    def parsesquare(self, pos):
+        "Parse a square bracket"
+        self.factory.clearskipped(pos)
+        if not self.factory.detecttype(SquareBracket, pos):
+            return None
+        bracket = self.factory.parsetype(SquareBracket, pos)
+        self.add(bracket)
+        return bracket
+
+    def parseliteral(self, pos):
+        "Parse a literal bracket."
+        self.factory.clearskipped(pos)
+        if not self.factory.detecttype(Bracket, pos):
+            if not pos.isvalue():
+                Trace.error('No literal parameter found at: ' + pos.identifier())
+                return None
+            return pos.globvalue()
+        bracket = Bracket().setfactory(self.factory)
+        self.add(bracket.parseliteral(pos))
+        return bracket.literal
+
+    def parsesquareliteral(self, pos):
+        "Parse a square bracket literally."
+        self.factory.clearskipped(pos)
+        if not self.factory.detecttype(SquareBracket, pos):
+            return None
+        bracket = SquareBracket().setfactory(self.factory)
+        self.add(bracket.parseliteral(pos))
+        return bracket.literal
+
+    def parsetext(self, pos):
+        "Parse a text parameter."
+        self.factory.clearskipped(pos)
+        if not self.factory.detecttype(Bracket, pos):
+            Trace.error('No text parameter for ' + self.command)
+            return None
+        bracket = Bracket().setfactory(self.factory).parsetext(pos)
+        self.add(bracket)
+        return bracket
+
+
+class EmptyCommand(CommandBit):
+    "An empty command (without parameters)"
+
+    commandmap = FormulaConfig.commands
+
+    def parsebit(self, pos):
+        "Parse a command without parameters"
+        self.contents = [FormulaConstant(self.translated)]
+
+
+class SpacedCommand(CommandBit):
+    """An empty command which should have math spacing in formulas."""
+
+    commandmap = FormulaConfig.spacedcommands
+
+    def parsebit(self, pos):
+        "Place as contents the command translated and spaced."
+        # pad with MEDIUM MATHEMATICAL SPACE (4/18 em): too wide in STIX fonts :(
+        # self.contents = [FormulaConstant('\u205f' + self.translated + '\u205f')]
+        # pad with THIN SPACE (1/5 em)
+        self.contents = [FormulaConstant('\u2009' + self.translated + '\u2009')]
+
+
+class AlphaCommand(EmptyCommand):
+    """A command without parameters whose result is alphabetical."""
+
+    commandmap = FormulaConfig.alphacommands
+    greek_capitals = ('\\Xi', '\\Theta', '\\Pi', '\\Sigma', '\\Gamma',
+                      '\\Lambda', '\\Phi', '\\Psi', '\\Delta',
+                      '\\Upsilon', '\\Omega')
+
+    def parsebit(self, pos):
+        "Parse the command and set type to alpha"
+        EmptyCommand.parsebit(self, pos)
+        if self.command not in self.greek_capitals:
+            # Greek Capital letters are upright in LaTeX default math-style.
+            # TODO: use italic, like in MathML and "iso" math-style?
+            self.type = 'alpha'
+
+
+class OneParamFunction(CommandBit):
+    "A function of one parameter"
+
+    commandmap = FormulaConfig.onefunctions
+    simplified = False
+
+    def parsebit(self, pos):
+        "Parse a function with one parameter"
+        self.output = TaggedOutput().settag(self.translated)
+        self.parseparameter(pos)
+        self.simplifyifpossible()
+
+    def simplifyifpossible(self):
+        "Try to simplify to a single character."
+        if self.original in self.commandmap:
+            self.output = FixedOutput()
+            self.html = [self.commandmap[self.original]]
+            self.simplified = True
+
+
+class SymbolFunction(CommandBit):
+    "Find a function which is represented by a symbol (like _ or ^)"
+
+    commandmap = FormulaConfig.symbolfunctions
+
+    def detect(self, pos):
+        "Find the symbol"
+        return pos.current() in SymbolFunction.commandmap
+
+    def parsebit(self, pos):
+        "Parse the symbol"
+        self.setcommand(pos.current())
+        pos.skip(self.command)
+        self.output = TaggedOutput().settag(self.translated)
+        self.parseparameter(pos)
+
+
+class TextFunction(CommandBit):
+    "A function where parameters are read as text."
+
+    commandmap = FormulaConfig.textfunctions
+
+    def parsebit(self, pos):
+        "Parse a text parameter"
+        self.output = TaggedOutput().settag(self.translated)
+        self.parsetext(pos)
+
+    def process(self):
+        "Set the type to font"
+        self.type = 'font'
+
+
+class FontFunction(OneParamFunction):
+    """A function of one parameter that changes the font."""
+    # TODO: keep letters italic with \boldsymbol.
+
+    commandmap = FormulaConfig.fontfunctions
+
+    def process(self):
+        "Simplify if possible using a single character."
+        self.type = 'font'
+        self.simplifyifpossible()
+
+
+FormulaFactory.types += [FormulaCommand, SymbolFunction]
+FormulaCommand.types = [
+    AlphaCommand, EmptyCommand, OneParamFunction, FontFunction,
+    TextFunction, SpacedCommand]
+
+
+class BigBracket:
+    "A big bracket generator."
+
+    def __init__(self, size, bracket, alignment='l'):
+        "Set the size and symbol for the bracket."
+        self.size = size
+        self.original = bracket
+        self.alignment = alignment
+        self.pieces = None
+        if bracket in FormulaConfig.bigbrackets:
+            self.pieces = FormulaConfig.bigbrackets[bracket]
+
+    def getpiece(self, index):
+        "Return the nth piece for the bracket."
+        function = getattr(self, 'getpiece' + str(len(self.pieces)))
+        return function(index)
+
+    def getpiece1(self, index):
+        "Return the only piece for a single-piece bracket."
+        return self.pieces[0]
+
+    def getpiece3(self, index):
+        "Get the nth piece for a 3-piece bracket: parenthesis or square bracket."
+        if index == 0:
+            return self.pieces[0]
+        if index == self.size - 1:
+            return self.pieces[-1]
+        return self.pieces[1]
+
+    def getpiece4(self, index):
+        "Get the nth piece for a 4-piece bracket: curly bracket."
+        if index == 0:
+            return self.pieces[0]
+        if index == self.size - 1:
+            return self.pieces[3]
+        if index == (self.size - 1)/2:
+            return self.pieces[2]
+        return self.pieces[1]
+
+    def getcell(self, index):
+        "Get the bracket piece as an array cell."
+        piece = self.getpiece(index)
+        span = 'span class="bracket align-' + self.alignment + '"'
+        return TaggedBit().constant(piece, span)
+
+    def getcontents(self):
+        "Get the bracket as an array or as a single bracket."
+        if self.size == 1 or not self.pieces:
+            return self.getsinglebracket()
+        rows = []
+        for index in range(self.size):
+            cell = self.getcell(index)
+            rows.append(TaggedBit().complete([cell], 'span class="arrayrow"'))
+        return [TaggedBit().complete(rows, 'span class="array"')]
+
+    def getsinglebracket(self):
+        "Return the bracket as a single sign."
+        if self.original == '.':
+            return [TaggedBit().constant('', 'span class="emptydot"')]
+        return [TaggedBit().constant(self.original, 'span class="stretchy"')]
+
+
+class FormulaEquation(CommandBit):
+    "A simple numbered equation."
+
+    piece = 'equation'
+
+    def parsebit(self, pos):
+        "Parse the array"
+        self.output = ContentsOutput()
+        self.add(self.factory.parsetype(WholeFormula, pos))
+
+
+class FormulaCell(FormulaCommand):
+    "An array cell inside a row"
+
+    def setalignment(self, alignment):
+        self.alignment = alignment
+        self.output = TaggedOutput().settag('span class="arraycell align-'
+                                            + alignment + '"', True)
+        return self
+
+    def parsebit(self, pos):
+        self.factory.clearskipped(pos)
+        if pos.finished():
+            return
+        self.add(self.factory.parsetype(WholeFormula, pos))
+
+
+class FormulaRow(FormulaCommand):
+    "An array row inside an array"
+
+    cellseparator = FormulaConfig.array['cellseparator']
+
+    def setalignments(self, alignments):
+        self.alignments = alignments
+        self.output = TaggedOutput().settag('span class="arrayrow"', True)
+        return self
+
+    def parsebit(self, pos):
+        "Parse a whole row"
+        index = 0
+        pos.pushending(self.cellseparator, optional=True)
+        while not pos.finished():
+            cell = self.createcell(index)
+            cell.parsebit(pos)
+            self.add(cell)
+            index += 1
+            pos.checkskip(self.cellseparator)
+        if len(self.contents) == 0:
+            self.output = EmptyOutput()
+
+    def createcell(self, index):
+        "Create the cell that corresponds to the given index."
+        alignment = self.alignments[index % len(self.alignments)]
+        return self.factory.create(FormulaCell).setalignment(alignment)
+
+
+class MultiRowFormula(CommandBit):
+    "A formula with multiple rows."
+
+    def parserows(self, pos):
+        "Parse all rows, finish when no more row ends"
+        self.rows = []
+        first = True
+        for row in self.iteraterows(pos):
+            if first:
+                first = False
+            else:
+                # intersparse empty rows
+                self.addempty()
+            row.parsebit(pos)
+            self.addrow(row)
+        self.size = len(self.rows)
+
+    def iteraterows(self, pos):
+        "Iterate over all rows, end when no more row ends"
+        rowseparator = FormulaConfig.array['rowseparator']
+        while True:
+            pos.pushending(rowseparator, True)
+            row = self.factory.create(FormulaRow)
+            yield row.setalignments(self.alignments)
+            if pos.checkfor(rowseparator):
+                self.original += pos.popending(rowseparator)
+            else:
+                return
+
+    def addempty(self):
+        "Add an empty row."
+        row = self.factory.create(FormulaRow).setalignments(self.alignments)
+        for index, originalcell in enumerate(self.rows[-1].contents):
+            cell = row.createcell(index)
+            cell.add(FormulaConstant(' '))
+            row.add(cell)
+        self.addrow(row)
+
+    def addrow(self, row):
+        "Add a row to the contents and to the list of rows."
+        self.rows.append(row)
+        self.add(row)
+
+
+class FormulaArray(MultiRowFormula):
+    "An array within a formula"
+
+    piece = 'array'
+
+    def parsebit(self, pos):
+        "Parse the array"
+        self.output = TaggedOutput().settag('span class="array"', False)
+        self.parsealignments(pos)
+        self.parserows(pos)
+
+    def parsealignments(self, pos):
+        "Parse the different alignments"
+        # vertical
+        self.valign = 'c'
+        literal = self.parsesquareliteral(pos)
+        if literal:
+            self.valign = literal
+        # horizontal
+        literal = self.parseliteral(pos)
+        self.alignments = []
+        for s in literal:
+            self.alignments.append(s)
+
+
+class FormulaMatrix(MultiRowFormula):
+    "A matrix (array with center alignment)."
+
+    piece = 'matrix'
+
+    def parsebit(self, pos):
+        "Parse the matrix, set alignments to 'c'."
+        self.output = TaggedOutput().settag('span class="array"', False)
+        self.valign = 'c'
+        self.alignments = ['c']
+        self.parserows(pos)
+
+
+class FormulaCases(MultiRowFormula):
+    "A cases statement"
+
+    piece = 'cases'
+
+    def parsebit(self, pos):
+        "Parse the cases"
+        self.output = ContentsOutput()
+        self.alignments = ['l', 'l']
+        self.parserows(pos)
+        for row in self.contents:
+            for cell in row.contents:
+                cell.output.settag('span class="case align-l"', True)
+                cell.contents.append(FormulaConstant(' '))
+        array = TaggedBit().complete(self.contents, 'span class="bracketcases"', True)
+        brace = BigBracket(len(self.contents), '{', 'l')
+        self.contents = brace.getcontents() + [array]
+
+
+class EquationEnvironment(MultiRowFormula):
+    "A \\begin{}...\\end equation environment with rows and cells."
+
+    def parsebit(self, pos):
+        "Parse the whole environment."
+        environment = self.piece.replace('*', '')
+        self.output = TaggedOutput().settag(
+                        'span class="environment %s"'%environment, False)
+        if environment in FormulaConfig.environments:
+            self.alignments = FormulaConfig.environments[environment]
+        else:
+            Trace.error('Unknown equation environment ' + self.piece)
+            # print in red
+            self.output = TaggedOutput().settag('span class="unknown"')
+            self.add(FormulaConstant('\\begin{%s} '%environment))
+
+            self.alignments = ['l']
+        self.parserows(pos)
+
+
+class BeginCommand(CommandBit):
+    "A \\begin{}...\\end command and what it entails (array, cases, aligned)"
+
+    commandmap = {FormulaConfig.array['begin']: ''}
+
+    types = [FormulaEquation, FormulaArray, FormulaCases, FormulaMatrix]
+
+    def parsebit(self, pos):
+        "Parse the begin command"
+        command = self.parseliteral(pos)
+        bit = self.findbit(command)
+        ending = FormulaConfig.array['end'] + '{' + command + '}'
+        pos.pushending(ending)
+        bit.parsebit(pos)
+        self.add(bit)
+        self.original += pos.popending(ending)
+        self.size = bit.size
+
+    def findbit(self, piece):
+        "Find the command bit corresponding to the \\begin{piece}"
+        for type in BeginCommand.types:
+            if piece.replace('*', '') == type.piece:
+                return self.factory.create(type)
+        bit = self.factory.create(EquationEnvironment)
+        bit.piece = piece
+        return bit
+
+
+FormulaCommand.types += [BeginCommand]
+
+
+class CombiningFunction(OneParamFunction):
+
+    commandmap = FormulaConfig.combiningfunctions
+
+    def parsebit(self, pos):
+        "Parse a combining function."
+        combining = self.translated
+        parameter = self.parsesingleparameter(pos)
+        if not parameter:
+            Trace.error('Missing parameter for combining function ' + self.command)
+            return
+        # Trace.message('apply %s to %r'%(self.command, parameter.extracttext()))
+        # parameter.tree()
+        if not isinstance(parameter, FormulaConstant):
+            try:
+                extractor = ContainerExtractor(ContainerConfig.extracttext)
+                parameter = extractor.extract(parameter)[0]
+            except IndexError:
+                Trace.error('No base character found for "%s".' % self.command)
+                return
+        # Trace.message('  basechar: %r' % parameter.string)
+        # Insert combining character after the first character:
+        if parameter.string.startswith('\u2009'):
+            i = 2  # skip padding by SpacedCommand and FormulaConfig.modified
+        else:
+            i = 1
+        parameter.string = parameter.string[:i] + combining + parameter.string[i:]
+        # Use pre-composed characters if possible: \not{=} -> ≠, say.
+        parameter.string = unicodedata.normalize('NFC', parameter.string)
+
+    def parsesingleparameter(self, pos):
+        "Parse a parameter, or a single letter."
+        self.factory.clearskipped(pos)
+        if pos.finished():
+            return None
+        return self.parseparameter(pos)
+
+
+class OversetFunction(OneParamFunction):
+    "A function that decorates some bit of text with an overset."
+
+    commandmap = FormulaConfig.oversetfunctions
+
+    def parsebit(self, pos):
+        "Parse an overset-function"
+        symbol = self.translated
+        self.symbol = TaggedBit().constant(symbol, 'sup')
+        self.parameter = self.parseparameter(pos)
+        self.output = TaggedOutput().settag('span class="embellished"')
+        self.contents.insert(0, self.symbol)
+        self.parameter.output = TaggedOutput().settag('span class="base"')
+        self.simplifyifpossible()
+
+
+class UndersetFunction(OneParamFunction):
+    "A function that decorates some bit of text with an underset."
+
+    commandmap = FormulaConfig.undersetfunctions
+
+    def parsebit(self, pos):
+        "Parse an underset-function"
+        symbol = self.translated
+        self.symbol = TaggedBit().constant(symbol, 'sub')
+        self.parameter = self.parseparameter(pos)
+        self.output = TaggedOutput().settag('span class="embellished"')
+        self.contents.insert(0, self.symbol)
+        self.parameter.output = TaggedOutput().settag('span class="base"')
+        self.simplifyifpossible()
+
+
+class LimitCommand(EmptyCommand):
+    "A command which accepts limits above and below, in display mode."
+
+    commandmap = FormulaConfig.limitcommands
+
+    def parsebit(self, pos):
+        "Parse a limit command."
+        self.output = TaggedOutput().settag('span class="limits"')
+        symbol = self.translated
+        self.contents.append(TaggedBit().constant(symbol, 'span class="limit"'))
+
+
+class LimitPreviousCommand(LimitCommand):
+    "A command to limit the previous command."
+
+    commandmap = None
+
+    def parsebit(self, pos):
+        "Do nothing."
+        self.output = TaggedOutput().settag('span class="limits"')
+        self.factory.clearskipped(pos)
+
+    def __str__(self):
+        "Return a printable representation."
+        return 'Limit previous command'
+
+
+class LimitsProcessor(MathsProcessor):
+    "A processor for limits inside an element."
+
+    def process(self, contents, index):
+        "Process the limits for an element."
+        if Options.simplemath:
+            return
+        if self.checklimits(contents, index):
+            self.modifylimits(contents, index)
+        if self.checkscript(contents, index) and self.checkscript(contents, index + 1):
+            self.modifyscripts(contents, index)
+
+    def checklimits(self, contents, index):
+        "Check if the current position has a limits command."
+        # TODO: check for \limits macro
+        if not DocumentParameters.displaymode:
+            return False
+        if self.checkcommand(contents, index + 1, LimitPreviousCommand):
+            self.limitsahead(contents, index)
+            return False
+        if not isinstance(contents[index], LimitCommand):
+            return False
+        return self.checkscript(contents, index + 1)
+
+    def limitsahead(self, contents, index):
+        "Limit the current element based on the next."
+        contents[index + 1].add(contents[index].clone())
+        contents[index].output = EmptyOutput()
+
+    def modifylimits(self, contents, index):
+        "Modify a limits commands so that the limits appear above and below."
+        limited = contents[index]
+        subscript = self.getlimit(contents, index + 1)
+        if self.checkscript(contents, index + 1):
+            superscript = self.getlimit(contents, index + 1)
+        else:
+            superscript = TaggedBit().constant('\u2009', 'sup class="limit"')
+        # fix order if source is x^i
+        if subscript.command == '^':
+            superscript, subscript = subscript, superscript
+        limited.contents.append(subscript)
+        limited.contents.insert(0, superscript)
+
+    def getlimit(self, contents, index):
+        "Get the limit for a limits command."
+        limit = self.getscript(contents, index)
+        limit.output.tag = limit.output.tag.replace('script', 'limit')
+        return limit
+
+    def modifyscripts(self, contents, index):
+        "Modify the super- and subscript to appear vertically aligned."
+        subscript = self.getscript(contents, index)
+        # subscript removed so instead of index + 1 we get index again
+        superscript = self.getscript(contents, index)
+        # super-/subscript are reversed if source is x^i_j
+        if subscript.command == '^':
+            superscript, subscript = subscript, superscript
+        scripts = TaggedBit().complete([superscript, subscript], 'span class="scripts"')
+        contents.insert(index, scripts)
+
+    def checkscript(self, contents, index):
+        "Check if the current element is a sub- or superscript."
+        return self.checkcommand(contents, index, SymbolFunction)
+
+    def checkcommand(self, contents, index, type):
+        "Check for the given type as the current element."
+        if len(contents) <= index:
+            return False
+        return isinstance(contents[index], type)
+
+    def getscript(self, contents, index):
+        "Get the sub- or superscript."
+        bit = contents[index]
+        bit.output.tag += ' class="script"'
+        del contents[index]
+        return bit
+
+
+class BracketCommand(OneParamFunction):
+    "A command which defines a bracket."
+
+    commandmap = FormulaConfig.bracketcommands
+
+    def parsebit(self, pos):
+        "Parse the bracket."
+        OneParamFunction.parsebit(self, pos)
+
+    def create(self, direction, character):
+        "Create the bracket for the given character."
+        self.original = character
+        self.command = '\\' + direction
+        self.contents = [FormulaConstant(character)]
+        return self
+
+
+class BracketProcessor(MathsProcessor):
+    "A processor for bracket commands."
+
+    def process(self, contents, index):
+        "Convert the bracket using Unicode pieces, if possible."
+        if Options.simplemath:
+            return
+        if self.checkleft(contents, index):
+            return self.processleft(contents, index)
+
+    def processleft(self, contents, index):
+        "Process a left bracket."
+        rightindex = self.findright(contents, index + 1)
+        if not rightindex:
+            return
+        size = self.findmax(contents, index, rightindex)
+        self.resize(contents[index], size)
+        self.resize(contents[rightindex], size)
+
+    def checkleft(self, contents, index):
+        "Check if the command at the given index is left."
+        return self.checkdirection(contents[index], '\\left')
+
+    def checkright(self, contents, index):
+        "Check if the command at the given index is right."
+        return self.checkdirection(contents[index], '\\right')
+
+    def checkdirection(self, bit, command):
+        "Check if the given bit is the desired bracket command."
+        if not isinstance(bit, BracketCommand):
+            return False
+        return bit.command == command
+
+    def findright(self, contents, index):
+        "Find the right bracket starting at the given index, or 0."
+        depth = 1
+        while index < len(contents):
+            if self.checkleft(contents, index):
+                depth += 1
+            if self.checkright(contents, index):
+                depth -= 1
+            if depth == 0:
+                return index
+            index += 1
+        return None
+
+    def findmax(self, contents, leftindex, rightindex):
+        "Find the max size of the contents between the two given indices."
+        sliced = contents[leftindex:rightindex]
+        return max(element.size for element in sliced)
+
+    def resize(self, command, size):
+        "Resize a bracket command to the given size."
+        character = command.extracttext()
+        alignment = command.command.replace('\\', '')
+        bracket = BigBracket(size, character, alignment)
+        command.output = ContentsOutput()
+        command.contents = bracket.getcontents()
+
+
+FormulaCommand.types += [OversetFunction, UndersetFunction,
+                         CombiningFunction, LimitCommand, BracketCommand]
+
+FormulaProcessor.processors += [
+    LimitsProcessor(), BracketProcessor(),
+]
+
+
+class ParameterDefinition:
+    "The definition of a parameter in a hybrid function."
+    "[] parameters are optional, {} parameters are mandatory."
+    "Each parameter has a one-character name, like {$1} or {$p}."
+    "A parameter that ends in ! like {$p!} is a literal."
+    "Example: [$1]{$p!} reads an optional parameter $1 and a literal mandatory parameter p."
+
+    parambrackets = [('[', ']'), ('{', '}')]
+
+    def __init__(self):
+        self.name = None
+        self.literal = False
+        self.optional = False
+        self.value = None
+        self.literalvalue = None
+
+    def parse(self, pos):
+        "Parse a parameter definition: [$0], {$x}, {$1!}..."
+        for (opening, closing) in ParameterDefinition.parambrackets:
+            if pos.checkskip(opening):
+                if opening == '[':
+                    self.optional = True
+                if not pos.checkskip('$'):
+                    Trace.error('Wrong parameter name, did you mean $' + pos.current() + '?')
+                    return None
+                self.name = pos.skipcurrent()
+                if pos.checkskip('!'):
+                    self.literal = True
+                if not pos.checkskip(closing):
+                    Trace.error('Wrong parameter closing ' + pos.skipcurrent())
+                    return None
+                return self
+        Trace.error('Wrong character in parameter template: ' + pos.skipcurrent())
+        return None
+
+    def read(self, pos, function):
+        "Read the parameter itself using the definition."
+        if self.literal:
+            if self.optional:
+                self.literalvalue = function.parsesquareliteral(pos)
+            else:
+                self.literalvalue = function.parseliteral(pos)
+            if self.literalvalue:
+                self.value = FormulaConstant(self.literalvalue)
+        elif self.optional:
+            self.value = function.parsesquare(pos)
+        else:
+            self.value = function.parseparameter(pos)
+
+    def __str__(self):
+        "Return a printable representation."
+        result = 'param ' + self.name
+        if self.value:
+            result += ': ' + str(self.value)
+        else:
+            result += ' (empty)'
+        return result
+
+
+class ParameterFunction(CommandBit):
+    "A function with a variable number of parameters defined in a template."
+    "The parameters are defined as a parameter definition."
+
+    def readparams(self, readtemplate, pos):
+        "Read the params according to the template."
+        self.params = {}
+        for paramdef in self.paramdefs(readtemplate):
+            paramdef.read(pos, self)
+            self.params['$' + paramdef.name] = paramdef
+
+    def paramdefs(self, readtemplate):
+        "Read each param definition in the template"
+        pos = TextPosition(readtemplate)
+        while not pos.finished():
+            paramdef = ParameterDefinition().parse(pos)
+            if paramdef:
+                yield paramdef
+
+    def getparam(self, name):
+        "Get a parameter as parsed."
+        if name not in self.params:
+            return None
+        return self.params[name]
+
+    def getvalue(self, name):
+        "Get the value of a parameter."
+        return self.getparam(name).value
+
+    def getliteralvalue(self, name):
+        "Get the literal value of a parameter."
+        param = self.getparam(name)
+        if not param or not param.literalvalue:
+            return None
+        return param.literalvalue
+
+
+class HybridFunction(ParameterFunction):
+    """
+    A parameter function where the output is also defined using a template.
+    The template can use a number of functions; each function has an associated
+    tag.
+    Example: [f0{$1},span class="fbox"] defines a function f0 which corresponds
+    to a span of class fbox, yielding <span class="fbox">$1</span>.
+    Literal parameters can be used in tags definitions:
+      [f0{$1},span style="color: $p;"]
+    yields <span style="color: $p;">$1</span>, where $p is a literal parameter.
+    Sizes can be specified in hybridsizes, e.g. adding parameter sizes. By
+    default the resulting size is the max of all arguments. Sizes are used
+    to generate the right parameters.
+    A function followed by a single / is output as a self-closing XHTML tag:
+      [f0/,hr]
+    will generate <hr/>.
+    """
+
+    commandmap = FormulaConfig.hybridfunctions
+
+    def parsebit(self, pos):
+        "Parse a function with [] and {} parameters"
+        readtemplate = self.translated[0]
+        writetemplate = self.translated[1]
+        self.readparams(readtemplate, pos)
+        self.contents = self.writeparams(writetemplate)
+        self.computehybridsize()
+
+    def writeparams(self, writetemplate):
+        "Write all params according to the template"
+        return self.writepos(TextPosition(writetemplate))
+
+    def writepos(self, pos):
+        "Write all params as read in the parse position."
+        result = []
+        while not pos.finished():
+            if pos.checkskip('$'):
+                param = self.writeparam(pos)
+                if param:
+                    result.append(param)
+            elif pos.checkskip('f'):
+                function = self.writefunction(pos)
+                if function:
+                    function.type = None
+                    result.append(function)
+            elif pos.checkskip('('):
+                result.append(self.writebracket('left', '('))
+            elif pos.checkskip(')'):
+                result.append(self.writebracket('right', ')'))
+            else:
+                result.append(FormulaConstant(pos.skipcurrent()))
+        return result
+
+    def writeparam(self, pos):
+        "Write a single param of the form $0, $x..."
+        name = '$' + pos.skipcurrent()
+        if name not in self.params:
+            Trace.error('Unknown parameter ' + name)
+            return None
+        if not self.params[name]:
+            return None
+        if pos.checkskip('.'):
+            self.params[name].value.type = pos.globalpha()
+        return self.params[name].value
+
+    def writefunction(self, pos):
+        "Write a single function f0,...,fn."
+        tag = self.readtag(pos)
+        if not tag:
+            return None
+        if pos.checkskip('/'):
+            # self-closing XHTML tag, such as <hr/>
+            return TaggedBit().selfcomplete(tag)
+        if not pos.checkskip('{'):
+            Trace.error('Function should be defined in {}')
+            return None
+        pos.pushending('}')
+        contents = self.writepos(pos)
+        pos.popending()
+        if len(contents) == 0:
+            return None
+        return TaggedBit().complete(contents, tag)
+
+    def readtag(self, pos):
+        "Get the tag corresponding to the given index. Does parameter substitution."
+        if not pos.current().isdigit():
+            Trace.error('Function should be f0,...,f9: f' + pos.current())
+            return None
+        index = int(pos.skipcurrent())
+        if 2 + index > len(self.translated):
+            Trace.error('Function f' + str(index) + ' is not defined')
+            return None
+        tag = self.translated[2 + index]
+        if '$' not in tag:
+            return tag
+        for variable in self.params:
+            if variable in tag:
+                param = self.params[variable]
+                if not param.literal:
+                    Trace.error('Parameters in tag ' + tag + ' should be literal: {' + variable + '!}')
+                    continue
+                if param.literalvalue:
+                    value = param.literalvalue
+                else:
+                    value = ''
+                tag = tag.replace(variable, value)
+        return tag
+
+    def writebracket(self, direction, character):
+        "Return a new bracket looking at the given direction."
+        return self.factory.create(BracketCommand).create(direction, character)
+
+    def computehybridsize(self):
+        "Compute the size of the hybrid function."
+        if self.command not in HybridSize.configsizes:
+            self.computesize()
+            return
+        self.size = HybridSize().getsize(self)
+        # set the size in all elements at first level
+        for element in self.contents:
+            element.size = self.size
+
+
+class HybridSize:
+    "The size associated with a hybrid function."
+
+    configsizes = FormulaConfig.hybridsizes
+
+    def getsize(self, function):
+        "Read the size for a function and parse it."
+        sizestring = self.configsizes[function.command]
+        for name in function.params:
+            if name in sizestring:
+                size = function.params[name].value.computesize()
+                sizestring = sizestring.replace(name, str(size))
+        if '$' in sizestring:
+            Trace.error('Unconverted variable in hybrid size: ' + sizestring)
+            return 1
+        return eval(sizestring)
+
+
+FormulaCommand.types += [HybridFunction]
+
+
+def math2html(formula):
+    "Convert some TeX math to HTML."
+    factory = FormulaFactory()
+    whole = factory.parseformula(formula)
+    FormulaProcessor().process(whole)
+    whole.process()
+    return ''.join(whole.gethtml())
+
+
+def main():
+    "Main function, called if invoked from the command line"
+    args = sys.argv
+    Options().parseoptions(args)
+    if len(args) != 1:
+        Trace.error('Usage: math2html.py escaped_string')
+        exit()
+    result = math2html(args[0])
+    Trace.message(result)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/mathalphabet2unichar.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/mathalphabet2unichar.py
new file mode 100644
index 00000000..876cea47
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/mathalphabet2unichar.py
@@ -0,0 +1,892 @@
+#!/usr/bin/env python3
+#
+# LaTeX math to Unicode symbols translation dictionaries for
+# the content of math alphabet commands (\mathtt, \mathbf, ...).
+# Generated with ``write_mathalphabet2unichar.py`` from the data in
+# http://milde.users.sourceforge.net/LUCR/Math/
+#
+# :Copyright: © 2024 Günter Milde.
+# :License: Released under the terms of the `2-Clause BSD license`__, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# __ https://opensource.org/licenses/BSD-2-Clause
+
+mathbb = {
+    '0': '\U0001d7d8',  # 𝟘 MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO
+    '1': '\U0001d7d9',  # 𝟙 MATHEMATICAL DOUBLE-STRUCK DIGIT ONE
+    '2': '\U0001d7da',  # 𝟚 MATHEMATICAL DOUBLE-STRUCK DIGIT TWO
+    '3': '\U0001d7db',  # 𝟛 MATHEMATICAL DOUBLE-STRUCK DIGIT THREE
+    '4': '\U0001d7dc',  # 𝟜 MATHEMATICAL DOUBLE-STRUCK DIGIT FOUR
+    '5': '\U0001d7dd',  # 𝟝 MATHEMATICAL DOUBLE-STRUCK DIGIT FIVE
+    '6': '\U0001d7de',  # 𝟞 MATHEMATICAL DOUBLE-STRUCK DIGIT SIX
+    '7': '\U0001d7df',  # 𝟟 MATHEMATICAL DOUBLE-STRUCK DIGIT SEVEN
+    '8': '\U0001d7e0',  # 𝟠 MATHEMATICAL DOUBLE-STRUCK DIGIT EIGHT
+    '9': '\U0001d7e1',  # 𝟡 MATHEMATICAL DOUBLE-STRUCK DIGIT NINE
+    'A': '\U0001d538',  # 𝔸 MATHEMATICAL DOUBLE-STRUCK CAPITAL A
+    'B': '\U0001d539',  # 𝔹 MATHEMATICAL DOUBLE-STRUCK CAPITAL B
+    'C': '\u2102',  # ℂ DOUBLE-STRUCK CAPITAL C
+    'D': '\U0001d53b',  # 𝔻 MATHEMATICAL DOUBLE-STRUCK CAPITAL D
+    'E': '\U0001d53c',  # 𝔼 MATHEMATICAL DOUBLE-STRUCK CAPITAL E
+    'F': '\U0001d53d',  # 𝔽 MATHEMATICAL DOUBLE-STRUCK CAPITAL F
+    'G': '\U0001d53e',  # 𝔾 MATHEMATICAL DOUBLE-STRUCK CAPITAL G
+    'H': '\u210d',  # ℍ DOUBLE-STRUCK CAPITAL H
+    'I': '\U0001d540',  # 𝕀 MATHEMATICAL DOUBLE-STRUCK CAPITAL I
+    'J': '\U0001d541',  # 𝕁 MATHEMATICAL DOUBLE-STRUCK CAPITAL J
+    'K': '\U0001d542',  # 𝕂 MATHEMATICAL DOUBLE-STRUCK CAPITAL K
+    'L': '\U0001d543',  # 𝕃 MATHEMATICAL DOUBLE-STRUCK CAPITAL L
+    'M': '\U0001d544',  # 𝕄 MATHEMATICAL DOUBLE-STRUCK CAPITAL M
+    'N': '\u2115',  # ℕ DOUBLE-STRUCK CAPITAL N
+    'O': '\U0001d546',  # 𝕆 MATHEMATICAL DOUBLE-STRUCK CAPITAL O
+    'P': '\u2119',  # ℙ DOUBLE-STRUCK CAPITAL P
+    'Q': '\u211a',  # ℚ DOUBLE-STRUCK CAPITAL Q
+    'R': '\u211d',  # ℝ DOUBLE-STRUCK CAPITAL R
+    'S': '\U0001d54a',  # 𝕊 MATHEMATICAL DOUBLE-STRUCK CAPITAL S
+    'T': '\U0001d54b',  # 𝕋 MATHEMATICAL DOUBLE-STRUCK CAPITAL T
+    'U': '\U0001d54c',  # 𝕌 MATHEMATICAL DOUBLE-STRUCK CAPITAL U
+    'V': '\U0001d54d',  # 𝕍 MATHEMATICAL DOUBLE-STRUCK CAPITAL V
+    'W': '\U0001d54e',  # 𝕎 MATHEMATICAL DOUBLE-STRUCK CAPITAL W
+    'X': '\U0001d54f',  # 𝕏 MATHEMATICAL DOUBLE-STRUCK CAPITAL X
+    'Y': '\U0001d550',  # 𝕐 MATHEMATICAL DOUBLE-STRUCK CAPITAL Y
+    'Z': '\u2124',  # ℤ DOUBLE-STRUCK CAPITAL Z
+    'a': '\U0001d552',  # 𝕒 MATHEMATICAL DOUBLE-STRUCK SMALL A
+    'b': '\U0001d553',  # 𝕓 MATHEMATICAL DOUBLE-STRUCK SMALL B
+    'c': '\U0001d554',  # 𝕔 MATHEMATICAL DOUBLE-STRUCK SMALL C
+    'd': '\U0001d555',  # 𝕕 MATHEMATICAL DOUBLE-STRUCK SMALL D
+    'e': '\U0001d556',  # 𝕖 MATHEMATICAL DOUBLE-STRUCK SMALL E
+    'f': '\U0001d557',  # 𝕗 MATHEMATICAL DOUBLE-STRUCK SMALL F
+    'g': '\U0001d558',  # 𝕘 MATHEMATICAL DOUBLE-STRUCK SMALL G
+    'h': '\U0001d559',  # 𝕙 MATHEMATICAL DOUBLE-STRUCK SMALL H
+    'i': '\U0001d55a',  # 𝕚 MATHEMATICAL DOUBLE-STRUCK SMALL I
+    'j': '\U0001d55b',  # 𝕛 MATHEMATICAL DOUBLE-STRUCK SMALL J
+    'k': '\U0001d55c',  # 𝕜 MATHEMATICAL DOUBLE-STRUCK SMALL K
+    'l': '\U0001d55d',  # 𝕝 MATHEMATICAL DOUBLE-STRUCK SMALL L
+    'm': '\U0001d55e',  # 𝕞 MATHEMATICAL DOUBLE-STRUCK SMALL M
+    'n': '\U0001d55f',  # 𝕟 MATHEMATICAL DOUBLE-STRUCK SMALL N
+    'o': '\U0001d560',  # 𝕠 MATHEMATICAL DOUBLE-STRUCK SMALL O
+    'p': '\U0001d561',  # 𝕡 MATHEMATICAL DOUBLE-STRUCK SMALL P
+    'q': '\U0001d562',  # 𝕢 MATHEMATICAL DOUBLE-STRUCK SMALL Q
+    'r': '\U0001d563',  # 𝕣 MATHEMATICAL DOUBLE-STRUCK SMALL R
+    's': '\U0001d564',  # 𝕤 MATHEMATICAL DOUBLE-STRUCK SMALL S
+    't': '\U0001d565',  # 𝕥 MATHEMATICAL DOUBLE-STRUCK SMALL T
+    'u': '\U0001d566',  # 𝕦 MATHEMATICAL DOUBLE-STRUCK SMALL U
+    'v': '\U0001d567',  # 𝕧 MATHEMATICAL DOUBLE-STRUCK SMALL V
+    'w': '\U0001d568',  # 𝕨 MATHEMATICAL DOUBLE-STRUCK SMALL W
+    'x': '\U0001d569',  # 𝕩 MATHEMATICAL DOUBLE-STRUCK SMALL X
+    'y': '\U0001d56a',  # 𝕪 MATHEMATICAL DOUBLE-STRUCK SMALL Y
+    'z': '\U0001d56b',  # 𝕫 MATHEMATICAL DOUBLE-STRUCK SMALL Z
+    'Γ': '\u213e',  # ℾ DOUBLE-STRUCK CAPITAL GAMMA
+    'Π': '\u213f',  # ℿ DOUBLE-STRUCK CAPITAL PI
+    'Σ': '\u2140',  # ⅀ DOUBLE-STRUCK N-ARY SUMMATION
+    'γ': '\u213d',  # ℽ DOUBLE-STRUCK SMALL GAMMA
+    'π': '\u213c',  # ℼ DOUBLE-STRUCK SMALL PI
+    }
+
+mathbf = {
+    '0': '\U0001d7ce',  # 𝟎 MATHEMATICAL BOLD DIGIT ZERO
+    '1': '\U0001d7cf',  # 𝟏 MATHEMATICAL BOLD DIGIT ONE
+    '2': '\U0001d7d0',  # 𝟐 MATHEMATICAL BOLD DIGIT TWO
+    '3': '\U0001d7d1',  # 𝟑 MATHEMATICAL BOLD DIGIT THREE
+    '4': '\U0001d7d2',  # 𝟒 MATHEMATICAL BOLD DIGIT FOUR
+    '5': '\U0001d7d3',  # 𝟓 MATHEMATICAL BOLD DIGIT FIVE
+    '6': '\U0001d7d4',  # 𝟔 MATHEMATICAL BOLD DIGIT SIX
+    '7': '\U0001d7d5',  # 𝟕 MATHEMATICAL BOLD DIGIT SEVEN
+    '8': '\U0001d7d6',  # 𝟖 MATHEMATICAL BOLD DIGIT EIGHT
+    '9': '\U0001d7d7',  # 𝟗 MATHEMATICAL BOLD DIGIT NINE
+    'A': '\U0001d400',  # 𝐀 MATHEMATICAL BOLD CAPITAL A
+    'B': '\U0001d401',  # 𝐁 MATHEMATICAL BOLD CAPITAL B
+    'C': '\U0001d402',  # 𝐂 MATHEMATICAL BOLD CAPITAL C
+    'D': '\U0001d403',  # 𝐃 MATHEMATICAL BOLD CAPITAL D
+    'E': '\U0001d404',  # 𝐄 MATHEMATICAL BOLD CAPITAL E
+    'F': '\U0001d405',  # 𝐅 MATHEMATICAL BOLD CAPITAL F
+    'G': '\U0001d406',  # 𝐆 MATHEMATICAL BOLD CAPITAL G
+    'H': '\U0001d407',  # 𝐇 MATHEMATICAL BOLD CAPITAL H
+    'I': '\U0001d408',  # 𝐈 MATHEMATICAL BOLD CAPITAL I
+    'J': '\U0001d409',  # 𝐉 MATHEMATICAL BOLD CAPITAL J
+    'K': '\U0001d40a',  # 𝐊 MATHEMATICAL BOLD CAPITAL K
+    'L': '\U0001d40b',  # 𝐋 MATHEMATICAL BOLD CAPITAL L
+    'M': '\U0001d40c',  # 𝐌 MATHEMATICAL BOLD CAPITAL M
+    'N': '\U0001d40d',  # 𝐍 MATHEMATICAL BOLD CAPITAL N
+    'O': '\U0001d40e',  # 𝐎 MATHEMATICAL BOLD CAPITAL O
+    'P': '\U0001d40f',  # 𝐏 MATHEMATICAL BOLD CAPITAL P
+    'Q': '\U0001d410',  # 𝐐 MATHEMATICAL BOLD CAPITAL Q
+    'R': '\U0001d411',  # 𝐑 MATHEMATICAL BOLD CAPITAL R
+    'S': '\U0001d412',  # 𝐒 MATHEMATICAL BOLD CAPITAL S
+    'T': '\U0001d413',  # 𝐓 MATHEMATICAL BOLD CAPITAL T
+    'U': '\U0001d414',  # 𝐔 MATHEMATICAL BOLD CAPITAL U
+    'V': '\U0001d415',  # 𝐕 MATHEMATICAL BOLD CAPITAL V
+    'W': '\U0001d416',  # 𝐖 MATHEMATICAL BOLD CAPITAL W
+    'X': '\U0001d417',  # 𝐗 MATHEMATICAL BOLD CAPITAL X
+    'Y': '\U0001d418',  # 𝐘 MATHEMATICAL BOLD CAPITAL Y
+    'Z': '\U0001d419',  # 𝐙 MATHEMATICAL BOLD CAPITAL Z
+    'a': '\U0001d41a',  # 𝐚 MATHEMATICAL BOLD SMALL A
+    'b': '\U0001d41b',  # 𝐛 MATHEMATICAL BOLD SMALL B
+    'c': '\U0001d41c',  # 𝐜 MATHEMATICAL BOLD SMALL C
+    'd': '\U0001d41d',  # 𝐝 MATHEMATICAL BOLD SMALL D
+    'e': '\U0001d41e',  # 𝐞 MATHEMATICAL BOLD SMALL E
+    'f': '\U0001d41f',  # 𝐟 MATHEMATICAL BOLD SMALL F
+    'g': '\U0001d420',  # 𝐠 MATHEMATICAL BOLD SMALL G
+    'h': '\U0001d421',  # 𝐡 MATHEMATICAL BOLD SMALL H
+    'i': '\U0001d422',  # 𝐢 MATHEMATICAL BOLD SMALL I
+    'j': '\U0001d423',  # 𝐣 MATHEMATICAL BOLD SMALL J
+    'k': '\U0001d424',  # 𝐤 MATHEMATICAL BOLD SMALL K
+    'l': '\U0001d425',  # 𝐥 MATHEMATICAL BOLD SMALL L
+    'm': '\U0001d426',  # 𝐦 MATHEMATICAL BOLD SMALL M
+    'n': '\U0001d427',  # 𝐧 MATHEMATICAL BOLD SMALL N
+    'o': '\U0001d428',  # 𝐨 MATHEMATICAL BOLD SMALL O
+    'p': '\U0001d429',  # 𝐩 MATHEMATICAL BOLD SMALL P
+    'q': '\U0001d42a',  # 𝐪 MATHEMATICAL BOLD SMALL Q
+    'r': '\U0001d42b',  # 𝐫 MATHEMATICAL BOLD SMALL R
+    's': '\U0001d42c',  # 𝐬 MATHEMATICAL BOLD SMALL S
+    't': '\U0001d42d',  # 𝐭 MATHEMATICAL BOLD SMALL T
+    'u': '\U0001d42e',  # 𝐮 MATHEMATICAL BOLD SMALL U
+    'v': '\U0001d42f',  # 𝐯 MATHEMATICAL BOLD SMALL V
+    'w': '\U0001d430',  # 𝐰 MATHEMATICAL BOLD SMALL W
+    'x': '\U0001d431',  # 𝐱 MATHEMATICAL BOLD SMALL X
+    'y': '\U0001d432',  # 𝐲 MATHEMATICAL BOLD SMALL Y
+    'z': '\U0001d433',  # 𝐳 MATHEMATICAL BOLD SMALL Z
+    'Γ': '\U0001d6aa',  # 𝚪 MATHEMATICAL BOLD CAPITAL GAMMA
+    'Δ': '\U0001d6ab',  # 𝚫 MATHEMATICAL BOLD CAPITAL DELTA
+    'Θ': '\U0001d6af',  # 𝚯 MATHEMATICAL BOLD CAPITAL THETA
+    'Λ': '\U0001d6b2',  # 𝚲 MATHEMATICAL BOLD CAPITAL LAMDA
+    'Ξ': '\U0001d6b5',  # 𝚵 MATHEMATICAL BOLD CAPITAL XI
+    'Π': '\U0001d6b7',  # 𝚷 MATHEMATICAL BOLD CAPITAL PI
+    'Σ': '\U0001d6ba',  # 𝚺 MATHEMATICAL BOLD CAPITAL SIGMA
+    'Υ': '\U0001d6bc',  # 𝚼 MATHEMATICAL BOLD CAPITAL UPSILON
+    'Φ': '\U0001d6bd',  # 𝚽 MATHEMATICAL BOLD CAPITAL PHI
+    'Ψ': '\U0001d6bf',  # 𝚿 MATHEMATICAL BOLD CAPITAL PSI
+    'Ω': '\U0001d6c0',  # 𝛀 MATHEMATICAL BOLD CAPITAL OMEGA
+    'α': '\U0001d6c2',  # 𝛂 MATHEMATICAL BOLD SMALL ALPHA
+    'β': '\U0001d6c3',  # 𝛃 MATHEMATICAL BOLD SMALL BETA
+    'γ': '\U0001d6c4',  # 𝛄 MATHEMATICAL BOLD SMALL GAMMA
+    'δ': '\U0001d6c5',  # 𝛅 MATHEMATICAL BOLD SMALL DELTA
+    'ε': '\U0001d6c6',  # 𝛆 MATHEMATICAL BOLD SMALL EPSILON
+    'ζ': '\U0001d6c7',  # 𝛇 MATHEMATICAL BOLD SMALL ZETA
+    'η': '\U0001d6c8',  # 𝛈 MATHEMATICAL BOLD SMALL ETA
+    'θ': '\U0001d6c9',  # 𝛉 MATHEMATICAL BOLD SMALL THETA
+    'ι': '\U0001d6ca',  # 𝛊 MATHEMATICAL BOLD SMALL IOTA
+    'κ': '\U0001d6cb',  # 𝛋 MATHEMATICAL BOLD SMALL KAPPA
+    'λ': '\U0001d6cc',  # 𝛌 MATHEMATICAL BOLD SMALL LAMDA
+    'μ': '\U0001d6cd',  # 𝛍 MATHEMATICAL BOLD SMALL MU
+    'ν': '\U0001d6ce',  # 𝛎 MATHEMATICAL BOLD SMALL NU
+    'ξ': '\U0001d6cf',  # 𝛏 MATHEMATICAL BOLD SMALL XI
+    'π': '\U0001d6d1',  # 𝛑 MATHEMATICAL BOLD SMALL PI
+    'ρ': '\U0001d6d2',  # 𝛒 MATHEMATICAL BOLD SMALL RHO
+    'ς': '\U0001d6d3',  # 𝛓 MATHEMATICAL BOLD SMALL FINAL SIGMA
+    'σ': '\U0001d6d4',  # 𝛔 MATHEMATICAL BOLD SMALL SIGMA
+    'τ': '\U0001d6d5',  # 𝛕 MATHEMATICAL BOLD SMALL TAU
+    'υ': '\U0001d6d6',  # 𝛖 MATHEMATICAL BOLD SMALL UPSILON
+    'φ': '\U0001d6d7',  # 𝛗 MATHEMATICAL BOLD SMALL PHI
+    'χ': '\U0001d6d8',  # 𝛘 MATHEMATICAL BOLD SMALL CHI
+    'ψ': '\U0001d6d9',  # 𝛙 MATHEMATICAL BOLD SMALL PSI
+    'ω': '\U0001d6da',  # 𝛚 MATHEMATICAL BOLD SMALL OMEGA
+    'ϑ': '\U0001d6dd',  # 𝛝 MATHEMATICAL BOLD THETA SYMBOL
+    'ϕ': '\U0001d6df',  # 𝛟 MATHEMATICAL BOLD PHI SYMBOL
+    'ϖ': '\U0001d6e1',  # 𝛡 MATHEMATICAL BOLD PI SYMBOL
+    'Ϝ': '\U0001d7ca',  # 𝟊 MATHEMATICAL BOLD CAPITAL DIGAMMA
+    'ϝ': '\U0001d7cb',  # 𝟋 MATHEMATICAL BOLD SMALL DIGAMMA
+    'ϰ': '\U0001d6de',  # 𝛞 MATHEMATICAL BOLD KAPPA SYMBOL
+    'ϱ': '\U0001d6e0',  # 𝛠 MATHEMATICAL BOLD RHO SYMBOL
+    'ϵ': '\U0001d6dc',  # 𝛜 MATHEMATICAL BOLD EPSILON SYMBOL
+    '∂': '\U0001d6db',  # 𝛛 MATHEMATICAL BOLD PARTIAL DIFFERENTIAL
+    '∇': '\U0001d6c1',  # 𝛁 MATHEMATICAL BOLD NABLA
+    }
+
+mathbfit = {
+    'A': '\U0001d468',  # 𝑨 MATHEMATICAL BOLD ITALIC CAPITAL A
+    'B': '\U0001d469',  # 𝑩 MATHEMATICAL BOLD ITALIC CAPITAL B
+    'C': '\U0001d46a',  # 𝑪 MATHEMATICAL BOLD ITALIC CAPITAL C
+    'D': '\U0001d46b',  # 𝑫 MATHEMATICAL BOLD ITALIC CAPITAL D
+    'E': '\U0001d46c',  # 𝑬 MATHEMATICAL BOLD ITALIC CAPITAL E
+    'F': '\U0001d46d',  # 𝑭 MATHEMATICAL BOLD ITALIC CAPITAL F
+    'G': '\U0001d46e',  # 𝑮 MATHEMATICAL BOLD ITALIC CAPITAL G
+    'H': '\U0001d46f',  # 𝑯 MATHEMATICAL BOLD ITALIC CAPITAL H
+    'I': '\U0001d470',  # 𝑰 MATHEMATICAL BOLD ITALIC CAPITAL I
+    'J': '\U0001d471',  # 𝑱 MATHEMATICAL BOLD ITALIC CAPITAL J
+    'K': '\U0001d472',  # 𝑲 MATHEMATICAL BOLD ITALIC CAPITAL K
+    'L': '\U0001d473',  # 𝑳 MATHEMATICAL BOLD ITALIC CAPITAL L
+    'M': '\U0001d474',  # 𝑴 MATHEMATICAL BOLD ITALIC CAPITAL M
+    'N': '\U0001d475',  # 𝑵 MATHEMATICAL BOLD ITALIC CAPITAL N
+    'O': '\U0001d476',  # 𝑶 MATHEMATICAL BOLD ITALIC CAPITAL O
+    'P': '\U0001d477',  # 𝑷 MATHEMATICAL BOLD ITALIC CAPITAL P
+    'Q': '\U0001d478',  # 𝑸 MATHEMATICAL BOLD ITALIC CAPITAL Q
+    'R': '\U0001d479',  # 𝑹 MATHEMATICAL BOLD ITALIC CAPITAL R
+    'S': '\U0001d47a',  # 𝑺 MATHEMATICAL BOLD ITALIC CAPITAL S
+    'T': '\U0001d47b',  # 𝑻 MATHEMATICAL BOLD ITALIC CAPITAL T
+    'U': '\U0001d47c',  # 𝑼 MATHEMATICAL BOLD ITALIC CAPITAL U
+    'V': '\U0001d47d',  # 𝑽 MATHEMATICAL BOLD ITALIC CAPITAL V
+    'W': '\U0001d47e',  # 𝑾 MATHEMATICAL BOLD ITALIC CAPITAL W
+    'X': '\U0001d47f',  # 𝑿 MATHEMATICAL BOLD ITALIC CAPITAL X
+    'Y': '\U0001d480',  # 𝒀 MATHEMATICAL BOLD ITALIC CAPITAL Y
+    'Z': '\U0001d481',  # 𝒁 MATHEMATICAL BOLD ITALIC CAPITAL Z
+    'a': '\U0001d482',  # 𝒂 MATHEMATICAL BOLD ITALIC SMALL A
+    'b': '\U0001d483',  # 𝒃 MATHEMATICAL BOLD ITALIC SMALL B
+    'c': '\U0001d484',  # 𝒄 MATHEMATICAL BOLD ITALIC SMALL C
+    'd': '\U0001d485',  # 𝒅 MATHEMATICAL BOLD ITALIC SMALL D
+    'e': '\U0001d486',  # 𝒆 MATHEMATICAL BOLD ITALIC SMALL E
+    'f': '\U0001d487',  # 𝒇 MATHEMATICAL BOLD ITALIC SMALL F
+    'g': '\U0001d488',  # 𝒈 MATHEMATICAL BOLD ITALIC SMALL G
+    'h': '\U0001d489',  # 𝒉 MATHEMATICAL BOLD ITALIC SMALL H
+    'i': '\U0001d48a',  # 𝒊 MATHEMATICAL BOLD ITALIC SMALL I
+    'j': '\U0001d48b',  # 𝒋 MATHEMATICAL BOLD ITALIC SMALL J
+    'k': '\U0001d48c',  # 𝒌 MATHEMATICAL BOLD ITALIC SMALL K
+    'l': '\U0001d48d',  # 𝒍 MATHEMATICAL BOLD ITALIC SMALL L
+    'm': '\U0001d48e',  # 𝒎 MATHEMATICAL BOLD ITALIC SMALL M
+    'n': '\U0001d48f',  # 𝒏 MATHEMATICAL BOLD ITALIC SMALL N
+    'o': '\U0001d490',  # 𝒐 MATHEMATICAL BOLD ITALIC SMALL O
+    'p': '\U0001d491',  # 𝒑 MATHEMATICAL BOLD ITALIC SMALL P
+    'q': '\U0001d492',  # 𝒒 MATHEMATICAL BOLD ITALIC SMALL Q
+    'r': '\U0001d493',  # 𝒓 MATHEMATICAL BOLD ITALIC SMALL R
+    's': '\U0001d494',  # 𝒔 MATHEMATICAL BOLD ITALIC SMALL S
+    't': '\U0001d495',  # 𝒕 MATHEMATICAL BOLD ITALIC SMALL T
+    'u': '\U0001d496',  # 𝒖 MATHEMATICAL BOLD ITALIC SMALL U
+    'v': '\U0001d497',  # 𝒗 MATHEMATICAL BOLD ITALIC SMALL V
+    'w': '\U0001d498',  # 𝒘 MATHEMATICAL BOLD ITALIC SMALL W
+    'x': '\U0001d499',  # 𝒙 MATHEMATICAL BOLD ITALIC SMALL X
+    'y': '\U0001d49a',  # 𝒚 MATHEMATICAL BOLD ITALIC SMALL Y
+    'z': '\U0001d49b',  # 𝒛 MATHEMATICAL BOLD ITALIC SMALL Z
+    'Γ': '\U0001d71e',  # 𝜞 MATHEMATICAL BOLD ITALIC CAPITAL GAMMA
+    'Δ': '\U0001d71f',  # 𝜟 MATHEMATICAL BOLD ITALIC CAPITAL DELTA
+    'Θ': '\U0001d723',  # 𝜣 MATHEMATICAL BOLD ITALIC CAPITAL THETA
+    'Λ': '\U0001d726',  # 𝜦 MATHEMATICAL BOLD ITALIC CAPITAL LAMDA
+    'Ξ': '\U0001d729',  # 𝜩 MATHEMATICAL BOLD ITALIC CAPITAL XI
+    'Π': '\U0001d72b',  # 𝜫 MATHEMATICAL BOLD ITALIC CAPITAL PI
+    'Σ': '\U0001d72e',  # 𝜮 MATHEMATICAL BOLD ITALIC CAPITAL SIGMA
+    'Υ': '\U0001d730',  # 𝜰 MATHEMATICAL BOLD ITALIC CAPITAL UPSILON
+    'Φ': '\U0001d731',  # 𝜱 MATHEMATICAL BOLD ITALIC CAPITAL PHI
+    'Ψ': '\U0001d733',  # 𝜳 MATHEMATICAL BOLD ITALIC CAPITAL PSI
+    'Ω': '\U0001d734',  # 𝜴 MATHEMATICAL BOLD ITALIC CAPITAL OMEGA
+    'α': '\U0001d736',  # 𝜶 MATHEMATICAL BOLD ITALIC SMALL ALPHA
+    'β': '\U0001d737',  # 𝜷 MATHEMATICAL BOLD ITALIC SMALL BETA
+    'γ': '\U0001d738',  # 𝜸 MATHEMATICAL BOLD ITALIC SMALL GAMMA
+    'δ': '\U0001d739',  # 𝜹 MATHEMATICAL BOLD ITALIC SMALL DELTA
+    'ε': '\U0001d73a',  # 𝜺 MATHEMATICAL BOLD ITALIC SMALL EPSILON
+    'ζ': '\U0001d73b',  # 𝜻 MATHEMATICAL BOLD ITALIC SMALL ZETA
+    'η': '\U0001d73c',  # 𝜼 MATHEMATICAL BOLD ITALIC SMALL ETA
+    'θ': '\U0001d73d',  # 𝜽 MATHEMATICAL BOLD ITALIC SMALL THETA
+    'ι': '\U0001d73e',  # 𝜾 MATHEMATICAL BOLD ITALIC SMALL IOTA
+    'κ': '\U0001d73f',  # 𝜿 MATHEMATICAL BOLD ITALIC SMALL KAPPA
+    'λ': '\U0001d740',  # 𝝀 MATHEMATICAL BOLD ITALIC SMALL LAMDA
+    'μ': '\U0001d741',  # 𝝁 MATHEMATICAL BOLD ITALIC SMALL MU
+    'ν': '\U0001d742',  # 𝝂 MATHEMATICAL BOLD ITALIC SMALL NU
+    'ξ': '\U0001d743',  # 𝝃 MATHEMATICAL BOLD ITALIC SMALL XI
+    'π': '\U0001d745',  # 𝝅 MATHEMATICAL BOLD ITALIC SMALL PI
+    'ρ': '\U0001d746',  # 𝝆 MATHEMATICAL BOLD ITALIC SMALL RHO
+    'ς': '\U0001d747',  # 𝝇 MATHEMATICAL BOLD ITALIC SMALL FINAL SIGMA
+    'σ': '\U0001d748',  # 𝝈 MATHEMATICAL BOLD ITALIC SMALL SIGMA
+    'τ': '\U0001d749',  # 𝝉 MATHEMATICAL BOLD ITALIC SMALL TAU
+    'υ': '\U0001d74a',  # 𝝊 MATHEMATICAL BOLD ITALIC SMALL UPSILON
+    'φ': '\U0001d74b',  # 𝝋 MATHEMATICAL BOLD ITALIC SMALL PHI
+    'χ': '\U0001d74c',  # 𝝌 MATHEMATICAL BOLD ITALIC SMALL CHI
+    'ψ': '\U0001d74d',  # 𝝍 MATHEMATICAL BOLD ITALIC SMALL PSI
+    'ω': '\U0001d74e',  # 𝝎 MATHEMATICAL BOLD ITALIC SMALL OMEGA
+    'ϑ': '\U0001d751',  # 𝝑 MATHEMATICAL BOLD ITALIC THETA SYMBOL
+    'ϕ': '\U0001d753',  # 𝝓 MATHEMATICAL BOLD ITALIC PHI SYMBOL
+    'ϖ': '\U0001d755',  # 𝝕 MATHEMATICAL BOLD ITALIC PI SYMBOL
+    'ϰ': '\U0001d752',  # 𝝒 MATHEMATICAL BOLD ITALIC KAPPA SYMBOL
+    'ϱ': '\U0001d754',  # 𝝔 MATHEMATICAL BOLD ITALIC RHO SYMBOL
+    'ϵ': '\U0001d750',  # 𝝐 MATHEMATICAL BOLD ITALIC EPSILON SYMBOL
+    '∂': '\U0001d74f',  # 𝝏 MATHEMATICAL BOLD ITALIC PARTIAL DIFFERENTIAL
+    '∇': '\U0001d735',  # 𝜵 MATHEMATICAL BOLD ITALIC NABLA
+    }
+
+mathcal = {
+    'A': '\U0001d49c',  # 𝒜 MATHEMATICAL SCRIPT CAPITAL A
+    'B': '\u212c',  # ℬ SCRIPT CAPITAL B
+    'C': '\U0001d49e',  # 𝒞 MATHEMATICAL SCRIPT CAPITAL C
+    'D': '\U0001d49f',  # 𝒟 MATHEMATICAL SCRIPT CAPITAL D
+    'E': '\u2130',  # ℰ SCRIPT CAPITAL E
+    'F': '\u2131',  # ℱ SCRIPT CAPITAL F
+    'G': '\U0001d4a2',  # 𝒢 MATHEMATICAL SCRIPT CAPITAL G
+    'H': '\u210b',  # ℋ SCRIPT CAPITAL H
+    'I': '\u2110',  # ℐ SCRIPT CAPITAL I
+    'J': '\U0001d4a5',  # 𝒥 MATHEMATICAL SCRIPT CAPITAL J
+    'K': '\U0001d4a6',  # 𝒦 MATHEMATICAL SCRIPT CAPITAL K
+    'L': '\u2112',  # ℒ SCRIPT CAPITAL L
+    'M': '\u2133',  # ℳ SCRIPT CAPITAL M
+    'N': '\U0001d4a9',  # 𝒩 MATHEMATICAL SCRIPT CAPITAL N
+    'O': '\U0001d4aa',  # 𝒪 MATHEMATICAL SCRIPT CAPITAL O
+    'P': '\U0001d4ab',  # 𝒫 MATHEMATICAL SCRIPT CAPITAL P
+    'Q': '\U0001d4ac',  # 𝒬 MATHEMATICAL SCRIPT CAPITAL Q
+    'R': '\u211b',  # ℛ SCRIPT CAPITAL R
+    'S': '\U0001d4ae',  # 𝒮 MATHEMATICAL SCRIPT CAPITAL S
+    'T': '\U0001d4af',  # 𝒯 MATHEMATICAL SCRIPT CAPITAL T
+    'U': '\U0001d4b0',  # 𝒰 MATHEMATICAL SCRIPT CAPITAL U
+    'V': '\U0001d4b1',  # 𝒱 MATHEMATICAL SCRIPT CAPITAL V
+    'W': '\U0001d4b2',  # 𝒲 MATHEMATICAL SCRIPT CAPITAL W
+    'X': '\U0001d4b3',  # 𝒳 MATHEMATICAL SCRIPT CAPITAL X
+    'Y': '\U0001d4b4',  # 𝒴 MATHEMATICAL SCRIPT CAPITAL Y
+    'Z': '\U0001d4b5',  # 𝒵 MATHEMATICAL SCRIPT CAPITAL Z
+    'a': '\U0001d4b6',  # 𝒶 MATHEMATICAL SCRIPT SMALL A
+    'b': '\U0001d4b7',  # 𝒷 MATHEMATICAL SCRIPT SMALL B
+    'c': '\U0001d4b8',  # 𝒸 MATHEMATICAL SCRIPT SMALL C
+    'd': '\U0001d4b9',  # 𝒹 MATHEMATICAL SCRIPT SMALL D
+    'e': '\u212f',  # ℯ SCRIPT SMALL E
+    'f': '\U0001d4bb',  # 𝒻 MATHEMATICAL SCRIPT SMALL F
+    'g': '\u210a',  # ℊ SCRIPT SMALL G
+    'h': '\U0001d4bd',  # 𝒽 MATHEMATICAL SCRIPT SMALL H
+    'i': '\U0001d4be',  # 𝒾 MATHEMATICAL SCRIPT SMALL I
+    'j': '\U0001d4bf',  # 𝒿 MATHEMATICAL SCRIPT SMALL J
+    'k': '\U0001d4c0',  # 𝓀 MATHEMATICAL SCRIPT SMALL K
+    'l': '\U0001d4c1',  # 𝓁 MATHEMATICAL SCRIPT SMALL L
+    'm': '\U0001d4c2',  # 𝓂 MATHEMATICAL SCRIPT SMALL M
+    'n': '\U0001d4c3',  # 𝓃 MATHEMATICAL SCRIPT SMALL N
+    'o': '\u2134',  # ℴ SCRIPT SMALL O
+    'p': '\U0001d4c5',  # 𝓅 MATHEMATICAL SCRIPT SMALL P
+    'q': '\U0001d4c6',  # 𝓆 MATHEMATICAL SCRIPT SMALL Q
+    'r': '\U0001d4c7',  # 𝓇 MATHEMATICAL SCRIPT SMALL R
+    's': '\U0001d4c8',  # 𝓈 MATHEMATICAL SCRIPT SMALL S
+    't': '\U0001d4c9',  # 𝓉 MATHEMATICAL SCRIPT SMALL T
+    'u': '\U0001d4ca',  # 𝓊 MATHEMATICAL SCRIPT SMALL U
+    'v': '\U0001d4cb',  # 𝓋 MATHEMATICAL SCRIPT SMALL V
+    'w': '\U0001d4cc',  # 𝓌 MATHEMATICAL SCRIPT SMALL W
+    'x': '\U0001d4cd',  # 𝓍 MATHEMATICAL SCRIPT SMALL X
+    'y': '\U0001d4ce',  # 𝓎 MATHEMATICAL SCRIPT SMALL Y
+    'z': '\U0001d4cf',  # 𝓏 MATHEMATICAL SCRIPT SMALL Z
+    }
+
+mathfrak = {
+    'A': '\U0001d504',  # 𝔄 MATHEMATICAL FRAKTUR CAPITAL A
+    'B': '\U0001d505',  # 𝔅 MATHEMATICAL FRAKTUR CAPITAL B
+    'C': '\u212d',  # ℭ BLACK-LETTER CAPITAL C
+    'D': '\U0001d507',  # 𝔇 MATHEMATICAL FRAKTUR CAPITAL D
+    'E': '\U0001d508',  # 𝔈 MATHEMATICAL FRAKTUR CAPITAL E
+    'F': '\U0001d509',  # 𝔉 MATHEMATICAL FRAKTUR CAPITAL F
+    'G': '\U0001d50a',  # 𝔊 MATHEMATICAL FRAKTUR CAPITAL G
+    'H': '\u210c',  # ℌ BLACK-LETTER CAPITAL H
+    'I': '\u2111',  # ℑ BLACK-LETTER CAPITAL I
+    'J': '\U0001d50d',  # 𝔍 MATHEMATICAL FRAKTUR CAPITAL J
+    'K': '\U0001d50e',  # 𝔎 MATHEMATICAL FRAKTUR CAPITAL K
+    'L': '\U0001d50f',  # 𝔏 MATHEMATICAL FRAKTUR CAPITAL L
+    'M': '\U0001d510',  # 𝔐 MATHEMATICAL FRAKTUR CAPITAL M
+    'N': '\U0001d511',  # 𝔑 MATHEMATICAL FRAKTUR CAPITAL N
+    'O': '\U0001d512',  # 𝔒 MATHEMATICAL FRAKTUR CAPITAL O
+    'P': '\U0001d513',  # 𝔓 MATHEMATICAL FRAKTUR CAPITAL P
+    'Q': '\U0001d514',  # 𝔔 MATHEMATICAL FRAKTUR CAPITAL Q
+    'R': '\u211c',  # ℜ BLACK-LETTER CAPITAL R
+    'S': '\U0001d516',  # 𝔖 MATHEMATICAL FRAKTUR CAPITAL S
+    'T': '\U0001d517',  # 𝔗 MATHEMATICAL FRAKTUR CAPITAL T
+    'U': '\U0001d518',  # 𝔘 MATHEMATICAL FRAKTUR CAPITAL U
+    'V': '\U0001d519',  # 𝔙 MATHEMATICAL FRAKTUR CAPITAL V
+    'W': '\U0001d51a',  # 𝔚 MATHEMATICAL FRAKTUR CAPITAL W
+    'X': '\U0001d51b',  # 𝔛 MATHEMATICAL FRAKTUR CAPITAL X
+    'Y': '\U0001d51c',  # 𝔜 MATHEMATICAL FRAKTUR CAPITAL Y
+    'Z': '\u2128',  # ℨ BLACK-LETTER CAPITAL Z
+    'a': '\U0001d51e',  # 𝔞 MATHEMATICAL FRAKTUR SMALL A
+    'b': '\U0001d51f',  # 𝔟 MATHEMATICAL FRAKTUR SMALL B
+    'c': '\U0001d520',  # 𝔠 MATHEMATICAL FRAKTUR SMALL C
+    'd': '\U0001d521',  # 𝔡 MATHEMATICAL FRAKTUR SMALL D
+    'e': '\U0001d522',  # 𝔢 MATHEMATICAL FRAKTUR SMALL E
+    'f': '\U0001d523',  # 𝔣 MATHEMATICAL FRAKTUR SMALL F
+    'g': '\U0001d524',  # 𝔤 MATHEMATICAL FRAKTUR SMALL G
+    'h': '\U0001d525',  # 𝔥 MATHEMATICAL FRAKTUR SMALL H
+    'i': '\U0001d526',  # 𝔦 MATHEMATICAL FRAKTUR SMALL I
+    'j': '\U0001d527',  # 𝔧 MATHEMATICAL FRAKTUR SMALL J
+    'k': '\U0001d528',  # 𝔨 MATHEMATICAL FRAKTUR SMALL K
+    'l': '\U0001d529',  # 𝔩 MATHEMATICAL FRAKTUR SMALL L
+    'm': '\U0001d52a',  # 𝔪 MATHEMATICAL FRAKTUR SMALL M
+    'n': '\U0001d52b',  # 𝔫 MATHEMATICAL FRAKTUR SMALL N
+    'o': '\U0001d52c',  # 𝔬 MATHEMATICAL FRAKTUR SMALL O
+    'p': '\U0001d52d',  # 𝔭 MATHEMATICAL FRAKTUR SMALL P
+    'q': '\U0001d52e',  # 𝔮 MATHEMATICAL FRAKTUR SMALL Q
+    'r': '\U0001d52f',  # 𝔯 MATHEMATICAL FRAKTUR SMALL R
+    's': '\U0001d530',  # 𝔰 MATHEMATICAL FRAKTUR SMALL S
+    't': '\U0001d531',  # 𝔱 MATHEMATICAL FRAKTUR SMALL T
+    'u': '\U0001d532',  # 𝔲 MATHEMATICAL FRAKTUR SMALL U
+    'v': '\U0001d533',  # 𝔳 MATHEMATICAL FRAKTUR SMALL V
+    'w': '\U0001d534',  # 𝔴 MATHEMATICAL FRAKTUR SMALL W
+    'x': '\U0001d535',  # 𝔵 MATHEMATICAL FRAKTUR SMALL X
+    'y': '\U0001d536',  # 𝔶 MATHEMATICAL FRAKTUR SMALL Y
+    'z': '\U0001d537',  # 𝔷 MATHEMATICAL FRAKTUR SMALL Z
+    }
+
+mathit = {
+    'A': '\U0001d434',  # 𝐴 MATHEMATICAL ITALIC CAPITAL A
+    'B': '\U0001d435',  # 𝐵 MATHEMATICAL ITALIC CAPITAL B
+    'C': '\U0001d436',  # 𝐶 MATHEMATICAL ITALIC CAPITAL C
+    'D': '\U0001d437',  # 𝐷 MATHEMATICAL ITALIC CAPITAL D
+    'E': '\U0001d438',  # 𝐸 MATHEMATICAL ITALIC CAPITAL E
+    'F': '\U0001d439',  # 𝐹 MATHEMATICAL ITALIC CAPITAL F
+    'G': '\U0001d43a',  # 𝐺 MATHEMATICAL ITALIC CAPITAL G
+    'H': '\U0001d43b',  # 𝐻 MATHEMATICAL ITALIC CAPITAL H
+    'I': '\U0001d43c',  # 𝐼 MATHEMATICAL ITALIC CAPITAL I
+    'J': '\U0001d43d',  # 𝐽 MATHEMATICAL ITALIC CAPITAL J
+    'K': '\U0001d43e',  # 𝐾 MATHEMATICAL ITALIC CAPITAL K
+    'L': '\U0001d43f',  # 𝐿 MATHEMATICAL ITALIC CAPITAL L
+    'M': '\U0001d440',  # 𝑀 MATHEMATICAL ITALIC CAPITAL M
+    'N': '\U0001d441',  # 𝑁 MATHEMATICAL ITALIC CAPITAL N
+    'O': '\U0001d442',  # 𝑂 MATHEMATICAL ITALIC CAPITAL O
+    'P': '\U0001d443',  # 𝑃 MATHEMATICAL ITALIC CAPITAL P
+    'Q': '\U0001d444',  # 𝑄 MATHEMATICAL ITALIC CAPITAL Q
+    'R': '\U0001d445',  # 𝑅 MATHEMATICAL ITALIC CAPITAL R
+    'S': '\U0001d446',  # 𝑆 MATHEMATICAL ITALIC CAPITAL S
+    'T': '\U0001d447',  # 𝑇 MATHEMATICAL ITALIC CAPITAL T
+    'U': '\U0001d448',  # 𝑈 MATHEMATICAL ITALIC CAPITAL U
+    'V': '\U0001d449',  # 𝑉 MATHEMATICAL ITALIC CAPITAL V
+    'W': '\U0001d44a',  # 𝑊 MATHEMATICAL ITALIC CAPITAL W
+    'X': '\U0001d44b',  # 𝑋 MATHEMATICAL ITALIC CAPITAL X
+    'Y': '\U0001d44c',  # 𝑌 MATHEMATICAL ITALIC CAPITAL Y
+    'Z': '\U0001d44d',  # 𝑍 MATHEMATICAL ITALIC CAPITAL Z
+    'a': '\U0001d44e',  # 𝑎 MATHEMATICAL ITALIC SMALL A
+    'b': '\U0001d44f',  # 𝑏 MATHEMATICAL ITALIC SMALL B
+    'c': '\U0001d450',  # 𝑐 MATHEMATICAL ITALIC SMALL C
+    'd': '\U0001d451',  # 𝑑 MATHEMATICAL ITALIC SMALL D
+    'e': '\U0001d452',  # 𝑒 MATHEMATICAL ITALIC SMALL E
+    'f': '\U0001d453',  # 𝑓 MATHEMATICAL ITALIC SMALL F
+    'g': '\U0001d454',  # 𝑔 MATHEMATICAL ITALIC SMALL G
+    'h': '\u210e',  # ℎ PLANCK CONSTANT
+    'i': '\U0001d456',  # 𝑖 MATHEMATICAL ITALIC SMALL I
+    'j': '\U0001d457',  # 𝑗 MATHEMATICAL ITALIC SMALL J
+    'k': '\U0001d458',  # 𝑘 MATHEMATICAL ITALIC SMALL K
+    'l': '\U0001d459',  # 𝑙 MATHEMATICAL ITALIC SMALL L
+    'm': '\U0001d45a',  # 𝑚 MATHEMATICAL ITALIC SMALL M
+    'n': '\U0001d45b',  # 𝑛 MATHEMATICAL ITALIC SMALL N
+    'o': '\U0001d45c',  # 𝑜 MATHEMATICAL ITALIC SMALL O
+    'p': '\U0001d45d',  # 𝑝 MATHEMATICAL ITALIC SMALL P
+    'q': '\U0001d45e',  # 𝑞 MATHEMATICAL ITALIC SMALL Q
+    'r': '\U0001d45f',  # 𝑟 MATHEMATICAL ITALIC SMALL R
+    's': '\U0001d460',  # 𝑠 MATHEMATICAL ITALIC SMALL S
+    't': '\U0001d461',  # 𝑡 MATHEMATICAL ITALIC SMALL T
+    'u': '\U0001d462',  # 𝑢 MATHEMATICAL ITALIC SMALL U
+    'v': '\U0001d463',  # 𝑣 MATHEMATICAL ITALIC SMALL V
+    'w': '\U0001d464',  # 𝑤 MATHEMATICAL ITALIC SMALL W
+    'x': '\U0001d465',  # 𝑥 MATHEMATICAL ITALIC SMALL X
+    'y': '\U0001d466',  # 𝑦 MATHEMATICAL ITALIC SMALL Y
+    'z': '\U0001d467',  # 𝑧 MATHEMATICAL ITALIC SMALL Z
+    'ı': '\U0001d6a4',  # 𝚤 MATHEMATICAL ITALIC SMALL DOTLESS I
+    'ȷ': '\U0001d6a5',  # 𝚥 MATHEMATICAL ITALIC SMALL DOTLESS J
+    'Γ': '\U0001d6e4',  # 𝛤 MATHEMATICAL ITALIC CAPITAL GAMMA
+    'Δ': '\U0001d6e5',  # 𝛥 MATHEMATICAL ITALIC CAPITAL DELTA
+    'Θ': '\U0001d6e9',  # 𝛩 MATHEMATICAL ITALIC CAPITAL THETA
+    'Λ': '\U0001d6ec',  # 𝛬 MATHEMATICAL ITALIC CAPITAL LAMDA
+    'Ξ': '\U0001d6ef',  # 𝛯 MATHEMATICAL ITALIC CAPITAL XI
+    'Π': '\U0001d6f1',  # 𝛱 MATHEMATICAL ITALIC CAPITAL PI
+    'Σ': '\U0001d6f4',  # 𝛴 MATHEMATICAL ITALIC CAPITAL SIGMA
+    'Υ': '\U0001d6f6',  # 𝛶 MATHEMATICAL ITALIC CAPITAL UPSILON
+    'Φ': '\U0001d6f7',  # 𝛷 MATHEMATICAL ITALIC CAPITAL PHI
+    'Ψ': '\U0001d6f9',  # 𝛹 MATHEMATICAL ITALIC CAPITAL PSI
+    'Ω': '\U0001d6fa',  # 𝛺 MATHEMATICAL ITALIC CAPITAL OMEGA
+    'α': '\U0001d6fc',  # 𝛼 MATHEMATICAL ITALIC SMALL ALPHA
+    'β': '\U0001d6fd',  # 𝛽 MATHEMATICAL ITALIC SMALL BETA
+    'γ': '\U0001d6fe',  # 𝛾 MATHEMATICAL ITALIC SMALL GAMMA
+    'δ': '\U0001d6ff',  # 𝛿 MATHEMATICAL ITALIC SMALL DELTA
+    'ε': '\U0001d700',  # 𝜀 MATHEMATICAL ITALIC SMALL EPSILON
+    'ζ': '\U0001d701',  # 𝜁 MATHEMATICAL ITALIC SMALL ZETA
+    'η': '\U0001d702',  # 𝜂 MATHEMATICAL ITALIC SMALL ETA
+    'θ': '\U0001d703',  # 𝜃 MATHEMATICAL ITALIC SMALL THETA
+    'ι': '\U0001d704',  # 𝜄 MATHEMATICAL ITALIC SMALL IOTA
+    'κ': '\U0001d705',  # 𝜅 MATHEMATICAL ITALIC SMALL KAPPA
+    'λ': '\U0001d706',  # 𝜆 MATHEMATICAL ITALIC SMALL LAMDA
+    'μ': '\U0001d707',  # 𝜇 MATHEMATICAL ITALIC SMALL MU
+    'ν': '\U0001d708',  # 𝜈 MATHEMATICAL ITALIC SMALL NU
+    'ξ': '\U0001d709',  # 𝜉 MATHEMATICAL ITALIC SMALL XI
+    'π': '\U0001d70b',  # 𝜋 MATHEMATICAL ITALIC SMALL PI
+    'ρ': '\U0001d70c',  # 𝜌 MATHEMATICAL ITALIC SMALL RHO
+    'ς': '\U0001d70d',  # 𝜍 MATHEMATICAL ITALIC SMALL FINAL SIGMA
+    'σ': '\U0001d70e',  # 𝜎 MATHEMATICAL ITALIC SMALL SIGMA
+    'τ': '\U0001d70f',  # 𝜏 MATHEMATICAL ITALIC SMALL TAU
+    'υ': '\U0001d710',  # 𝜐 MATHEMATICAL ITALIC SMALL UPSILON
+    'φ': '\U0001d711',  # 𝜑 MATHEMATICAL ITALIC SMALL PHI
+    'χ': '\U0001d712',  # 𝜒 MATHEMATICAL ITALIC SMALL CHI
+    'ψ': '\U0001d713',  # 𝜓 MATHEMATICAL ITALIC SMALL PSI
+    'ω': '\U0001d714',  # 𝜔 MATHEMATICAL ITALIC SMALL OMEGA
+    'ϑ': '\U0001d717',  # 𝜗 MATHEMATICAL ITALIC THETA SYMBOL
+    'ϕ': '\U0001d719',  # 𝜙 MATHEMATICAL ITALIC PHI SYMBOL
+    'ϖ': '\U0001d71b',  # 𝜛 MATHEMATICAL ITALIC PI SYMBOL
+    'ϱ': '\U0001d71a',  # 𝜚 MATHEMATICAL ITALIC RHO SYMBOL
+    'ϵ': '\U0001d716',  # 𝜖 MATHEMATICAL ITALIC EPSILON SYMBOL
+    '∂': '\U0001d715',  # 𝜕 MATHEMATICAL ITALIC PARTIAL DIFFERENTIAL
+    '∇': '\U0001d6fb',  # 𝛻 MATHEMATICAL ITALIC NABLA
+    }
+
+mathsf = {
+    '0': '\U0001d7e2',  # 𝟢 MATHEMATICAL SANS-SERIF DIGIT ZERO
+    '1': '\U0001d7e3',  # 𝟣 MATHEMATICAL SANS-SERIF DIGIT ONE
+    '2': '\U0001d7e4',  # 𝟤 MATHEMATICAL SANS-SERIF DIGIT TWO
+    '3': '\U0001d7e5',  # 𝟥 MATHEMATICAL SANS-SERIF DIGIT THREE
+    '4': '\U0001d7e6',  # 𝟦 MATHEMATICAL SANS-SERIF DIGIT FOUR
+    '5': '\U0001d7e7',  # 𝟧 MATHEMATICAL SANS-SERIF DIGIT FIVE
+    '6': '\U0001d7e8',  # 𝟨 MATHEMATICAL SANS-SERIF DIGIT SIX
+    '7': '\U0001d7e9',  # 𝟩 MATHEMATICAL SANS-SERIF DIGIT SEVEN
+    '8': '\U0001d7ea',  # 𝟪 MATHEMATICAL SANS-SERIF DIGIT EIGHT
+    '9': '\U0001d7eb',  # 𝟫 MATHEMATICAL SANS-SERIF DIGIT NINE
+    'A': '\U0001d5a0',  # 𝖠 MATHEMATICAL SANS-SERIF CAPITAL A
+    'B': '\U0001d5a1',  # 𝖡 MATHEMATICAL SANS-SERIF CAPITAL B
+    'C': '\U0001d5a2',  # 𝖢 MATHEMATICAL SANS-SERIF CAPITAL C
+    'D': '\U0001d5a3',  # 𝖣 MATHEMATICAL SANS-SERIF CAPITAL D
+    'E': '\U0001d5a4',  # 𝖤 MATHEMATICAL SANS-SERIF CAPITAL E
+    'F': '\U0001d5a5',  # 𝖥 MATHEMATICAL SANS-SERIF CAPITAL F
+    'G': '\U0001d5a6',  # 𝖦 MATHEMATICAL SANS-SERIF CAPITAL G
+    'H': '\U0001d5a7',  # 𝖧 MATHEMATICAL SANS-SERIF CAPITAL H
+    'I': '\U0001d5a8',  # 𝖨 MATHEMATICAL SANS-SERIF CAPITAL I
+    'J': '\U0001d5a9',  # 𝖩 MATHEMATICAL SANS-SERIF CAPITAL J
+    'K': '\U0001d5aa',  # 𝖪 MATHEMATICAL SANS-SERIF CAPITAL K
+    'L': '\U0001d5ab',  # 𝖫 MATHEMATICAL SANS-SERIF CAPITAL L
+    'M': '\U0001d5ac',  # 𝖬 MATHEMATICAL SANS-SERIF CAPITAL M
+    'N': '\U0001d5ad',  # 𝖭 MATHEMATICAL SANS-SERIF CAPITAL N
+    'O': '\U0001d5ae',  # 𝖮 MATHEMATICAL SANS-SERIF CAPITAL O
+    'P': '\U0001d5af',  # 𝖯 MATHEMATICAL SANS-SERIF CAPITAL P
+    'Q': '\U0001d5b0',  # 𝖰 MATHEMATICAL SANS-SERIF CAPITAL Q
+    'R': '\U0001d5b1',  # 𝖱 MATHEMATICAL SANS-SERIF CAPITAL R
+    'S': '\U0001d5b2',  # 𝖲 MATHEMATICAL SANS-SERIF CAPITAL S
+    'T': '\U0001d5b3',  # 𝖳 MATHEMATICAL SANS-SERIF CAPITAL T
+    'U': '\U0001d5b4',  # 𝖴 MATHEMATICAL SANS-SERIF CAPITAL U
+    'V': '\U0001d5b5',  # 𝖵 MATHEMATICAL SANS-SERIF CAPITAL V
+    'W': '\U0001d5b6',  # 𝖶 MATHEMATICAL SANS-SERIF CAPITAL W
+    'X': '\U0001d5b7',  # 𝖷 MATHEMATICAL SANS-SERIF CAPITAL X
+    'Y': '\U0001d5b8',  # 𝖸 MATHEMATICAL SANS-SERIF CAPITAL Y
+    'Z': '\U0001d5b9',  # 𝖹 MATHEMATICAL SANS-SERIF CAPITAL Z
+    'a': '\U0001d5ba',  # 𝖺 MATHEMATICAL SANS-SERIF SMALL A
+    'b': '\U0001d5bb',  # 𝖻 MATHEMATICAL SANS-SERIF SMALL B
+    'c': '\U0001d5bc',  # 𝖼 MATHEMATICAL SANS-SERIF SMALL C
+    'd': '\U0001d5bd',  # 𝖽 MATHEMATICAL SANS-SERIF SMALL D
+    'e': '\U0001d5be',  # 𝖾 MATHEMATICAL SANS-SERIF SMALL E
+    'f': '\U0001d5bf',  # 𝖿 MATHEMATICAL SANS-SERIF SMALL F
+    'g': '\U0001d5c0',  # 𝗀 MATHEMATICAL SANS-SERIF SMALL G
+    'h': '\U0001d5c1',  # 𝗁 MATHEMATICAL SANS-SERIF SMALL H
+    'i': '\U0001d5c2',  # 𝗂 MATHEMATICAL SANS-SERIF SMALL I
+    'j': '\U0001d5c3',  # 𝗃 MATHEMATICAL SANS-SERIF SMALL J
+    'k': '\U0001d5c4',  # 𝗄 MATHEMATICAL SANS-SERIF SMALL K
+    'l': '\U0001d5c5',  # 𝗅 MATHEMATICAL SANS-SERIF SMALL L
+    'm': '\U0001d5c6',  # 𝗆 MATHEMATICAL SANS-SERIF SMALL M
+    'n': '\U0001d5c7',  # 𝗇 MATHEMATICAL SANS-SERIF SMALL N
+    'o': '\U0001d5c8',  # 𝗈 MATHEMATICAL SANS-SERIF SMALL O
+    'p': '\U0001d5c9',  # 𝗉 MATHEMATICAL SANS-SERIF SMALL P
+    'q': '\U0001d5ca',  # 𝗊 MATHEMATICAL SANS-SERIF SMALL Q
+    'r': '\U0001d5cb',  # 𝗋 MATHEMATICAL SANS-SERIF SMALL R
+    's': '\U0001d5cc',  # 𝗌 MATHEMATICAL SANS-SERIF SMALL S
+    't': '\U0001d5cd',  # 𝗍 MATHEMATICAL SANS-SERIF SMALL T
+    'u': '\U0001d5ce',  # 𝗎 MATHEMATICAL SANS-SERIF SMALL U
+    'v': '\U0001d5cf',  # 𝗏 MATHEMATICAL SANS-SERIF SMALL V
+    'w': '\U0001d5d0',  # 𝗐 MATHEMATICAL SANS-SERIF SMALL W
+    'x': '\U0001d5d1',  # 𝗑 MATHEMATICAL SANS-SERIF SMALL X
+    'y': '\U0001d5d2',  # 𝗒 MATHEMATICAL SANS-SERIF SMALL Y
+    'z': '\U0001d5d3',  # 𝗓 MATHEMATICAL SANS-SERIF SMALL Z
+    }
+
+mathsfbf = {
+    '0': '\U0001d7ec',  # 𝟬 MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO
+    '1': '\U0001d7ed',  # 𝟭 MATHEMATICAL SANS-SERIF BOLD DIGIT ONE
+    '2': '\U0001d7ee',  # 𝟮 MATHEMATICAL SANS-SERIF BOLD DIGIT TWO
+    '3': '\U0001d7ef',  # 𝟯 MATHEMATICAL SANS-SERIF BOLD DIGIT THREE
+    '4': '\U0001d7f0',  # 𝟰 MATHEMATICAL SANS-SERIF BOLD DIGIT FOUR
+    '5': '\U0001d7f1',  # 𝟱 MATHEMATICAL SANS-SERIF BOLD DIGIT FIVE
+    '6': '\U0001d7f2',  # 𝟲 MATHEMATICAL SANS-SERIF BOLD DIGIT SIX
+    '7': '\U0001d7f3',  # 𝟳 MATHEMATICAL SANS-SERIF BOLD DIGIT SEVEN
+    '8': '\U0001d7f4',  # 𝟴 MATHEMATICAL SANS-SERIF BOLD DIGIT EIGHT
+    '9': '\U0001d7f5',  # 𝟵 MATHEMATICAL SANS-SERIF BOLD DIGIT NINE
+    'A': '\U0001d5d4',  # 𝗔 MATHEMATICAL SANS-SERIF BOLD CAPITAL A
+    'B': '\U0001d5d5',  # 𝗕 MATHEMATICAL SANS-SERIF BOLD CAPITAL B
+    'C': '\U0001d5d6',  # 𝗖 MATHEMATICAL SANS-SERIF BOLD CAPITAL C
+    'D': '\U0001d5d7',  # 𝗗 MATHEMATICAL SANS-SERIF BOLD CAPITAL D
+    'E': '\U0001d5d8',  # 𝗘 MATHEMATICAL SANS-SERIF BOLD CAPITAL E
+    'F': '\U0001d5d9',  # 𝗙 MATHEMATICAL SANS-SERIF BOLD CAPITAL F
+    'G': '\U0001d5da',  # 𝗚 MATHEMATICAL SANS-SERIF BOLD CAPITAL G
+    'H': '\U0001d5db',  # 𝗛 MATHEMATICAL SANS-SERIF BOLD CAPITAL H
+    'I': '\U0001d5dc',  # 𝗜 MATHEMATICAL SANS-SERIF BOLD CAPITAL I
+    'J': '\U0001d5dd',  # 𝗝 MATHEMATICAL SANS-SERIF BOLD CAPITAL J
+    'K': '\U0001d5de',  # 𝗞 MATHEMATICAL SANS-SERIF BOLD CAPITAL K
+    'L': '\U0001d5df',  # 𝗟 MATHEMATICAL SANS-SERIF BOLD CAPITAL L
+    'M': '\U0001d5e0',  # 𝗠 MATHEMATICAL SANS-SERIF BOLD CAPITAL M
+    'N': '\U0001d5e1',  # 𝗡 MATHEMATICAL SANS-SERIF BOLD CAPITAL N
+    'O': '\U0001d5e2',  # 𝗢 MATHEMATICAL SANS-SERIF BOLD CAPITAL O
+    'P': '\U0001d5e3',  # 𝗣 MATHEMATICAL SANS-SERIF BOLD CAPITAL P
+    'Q': '\U0001d5e4',  # 𝗤 MATHEMATICAL SANS-SERIF BOLD CAPITAL Q
+    'R': '\U0001d5e5',  # 𝗥 MATHEMATICAL SANS-SERIF BOLD CAPITAL R
+    'S': '\U0001d5e6',  # 𝗦 MATHEMATICAL SANS-SERIF BOLD CAPITAL S
+    'T': '\U0001d5e7',  # 𝗧 MATHEMATICAL SANS-SERIF BOLD CAPITAL T
+    'U': '\U0001d5e8',  # 𝗨 MATHEMATICAL SANS-SERIF BOLD CAPITAL U
+    'V': '\U0001d5e9',  # 𝗩 MATHEMATICAL SANS-SERIF BOLD CAPITAL V
+    'W': '\U0001d5ea',  # 𝗪 MATHEMATICAL SANS-SERIF BOLD CAPITAL W
+    'X': '\U0001d5eb',  # 𝗫 MATHEMATICAL SANS-SERIF BOLD CAPITAL X
+    'Y': '\U0001d5ec',  # 𝗬 MATHEMATICAL SANS-SERIF BOLD CAPITAL Y
+    'Z': '\U0001d5ed',  # 𝗭 MATHEMATICAL SANS-SERIF BOLD CAPITAL Z
+    'a': '\U0001d5ee',  # 𝗮 MATHEMATICAL SANS-SERIF BOLD SMALL A
+    'b': '\U0001d5ef',  # 𝗯 MATHEMATICAL SANS-SERIF BOLD SMALL B
+    'c': '\U0001d5f0',  # 𝗰 MATHEMATICAL SANS-SERIF BOLD SMALL C
+    'd': '\U0001d5f1',  # 𝗱 MATHEMATICAL SANS-SERIF BOLD SMALL D
+    'e': '\U0001d5f2',  # 𝗲 MATHEMATICAL SANS-SERIF BOLD SMALL E
+    'f': '\U0001d5f3',  # 𝗳 MATHEMATICAL SANS-SERIF BOLD SMALL F
+    'g': '\U0001d5f4',  # 𝗴 MATHEMATICAL SANS-SERIF BOLD SMALL G
+    'h': '\U0001d5f5',  # 𝗵 MATHEMATICAL SANS-SERIF BOLD SMALL H
+    'i': '\U0001d5f6',  # 𝗶 MATHEMATICAL SANS-SERIF BOLD SMALL I
+    'j': '\U0001d5f7',  # 𝗷 MATHEMATICAL SANS-SERIF BOLD SMALL J
+    'k': '\U0001d5f8',  # 𝗸 MATHEMATICAL SANS-SERIF BOLD SMALL K
+    'l': '\U0001d5f9',  # 𝗹 MATHEMATICAL SANS-SERIF BOLD SMALL L
+    'm': '\U0001d5fa',  # 𝗺 MATHEMATICAL SANS-SERIF BOLD SMALL M
+    'n': '\U0001d5fb',  # 𝗻 MATHEMATICAL SANS-SERIF BOLD SMALL N
+    'o': '\U0001d5fc',  # 𝗼 MATHEMATICAL SANS-SERIF BOLD SMALL O
+    'p': '\U0001d5fd',  # 𝗽 MATHEMATICAL SANS-SERIF BOLD SMALL P
+    'q': '\U0001d5fe',  # 𝗾 MATHEMATICAL SANS-SERIF BOLD SMALL Q
+    'r': '\U0001d5ff',  # 𝗿 MATHEMATICAL SANS-SERIF BOLD SMALL R
+    's': '\U0001d600',  # 𝘀 MATHEMATICAL SANS-SERIF BOLD SMALL S
+    't': '\U0001d601',  # 𝘁 MATHEMATICAL SANS-SERIF BOLD SMALL T
+    'u': '\U0001d602',  # 𝘂 MATHEMATICAL SANS-SERIF BOLD SMALL U
+    'v': '\U0001d603',  # 𝘃 MATHEMATICAL SANS-SERIF BOLD SMALL V
+    'w': '\U0001d604',  # 𝘄 MATHEMATICAL SANS-SERIF BOLD SMALL W
+    'x': '\U0001d605',  # 𝘅 MATHEMATICAL SANS-SERIF BOLD SMALL X
+    'y': '\U0001d606',  # 𝘆 MATHEMATICAL SANS-SERIF BOLD SMALL Y
+    'z': '\U0001d607',  # 𝘇 MATHEMATICAL SANS-SERIF BOLD SMALL Z
+    'Γ': '\U0001d758',  # 𝝘 MATHEMATICAL SANS-SERIF BOLD CAPITAL GAMMA
+    'Δ': '\U0001d759',  # 𝝙 MATHEMATICAL SANS-SERIF BOLD CAPITAL DELTA
+    'Θ': '\U0001d75d',  # 𝝝 MATHEMATICAL SANS-SERIF BOLD CAPITAL THETA
+    'Λ': '\U0001d760',  # 𝝠 MATHEMATICAL SANS-SERIF BOLD CAPITAL LAMDA
+    'Ξ': '\U0001d763',  # 𝝣 MATHEMATICAL SANS-SERIF BOLD CAPITAL XI
+    'Π': '\U0001d765',  # 𝝥 MATHEMATICAL SANS-SERIF BOLD CAPITAL PI
+    'Σ': '\U0001d768',  # 𝝨 MATHEMATICAL SANS-SERIF BOLD CAPITAL SIGMA
+    'Υ': '\U0001d76a',  # 𝝪 MATHEMATICAL SANS-SERIF BOLD CAPITAL UPSILON
+    'Φ': '\U0001d76b',  # 𝝫 MATHEMATICAL SANS-SERIF BOLD CAPITAL PHI
+    'Ψ': '\U0001d76d',  # 𝝭 MATHEMATICAL SANS-SERIF BOLD CAPITAL PSI
+    'Ω': '\U0001d76e',  # 𝝮 MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA
+    'α': '\U0001d770',  # 𝝰 MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA
+    'β': '\U0001d771',  # 𝝱 MATHEMATICAL SANS-SERIF BOLD SMALL BETA
+    'γ': '\U0001d772',  # 𝝲 MATHEMATICAL SANS-SERIF BOLD SMALL GAMMA
+    'δ': '\U0001d773',  # 𝝳 MATHEMATICAL SANS-SERIF BOLD SMALL DELTA
+    'ε': '\U0001d774',  # 𝝴 MATHEMATICAL SANS-SERIF BOLD SMALL EPSILON
+    'ζ': '\U0001d775',  # 𝝵 MATHEMATICAL SANS-SERIF BOLD SMALL ZETA
+    'η': '\U0001d776',  # 𝝶 MATHEMATICAL SANS-SERIF BOLD SMALL ETA
+    'θ': '\U0001d777',  # 𝝷 MATHEMATICAL SANS-SERIF BOLD SMALL THETA
+    'ι': '\U0001d778',  # 𝝸 MATHEMATICAL SANS-SERIF BOLD SMALL IOTA
+    'κ': '\U0001d779',  # 𝝹 MATHEMATICAL SANS-SERIF BOLD SMALL KAPPA
+    'λ': '\U0001d77a',  # 𝝺 MATHEMATICAL SANS-SERIF BOLD SMALL LAMDA
+    'μ': '\U0001d77b',  # 𝝻 MATHEMATICAL SANS-SERIF BOLD SMALL MU
+    'ν': '\U0001d77c',  # 𝝼 MATHEMATICAL SANS-SERIF BOLD SMALL NU
+    'ξ': '\U0001d77d',  # 𝝽 MATHEMATICAL SANS-SERIF BOLD SMALL XI
+    'π': '\U0001d77f',  # 𝝿 MATHEMATICAL SANS-SERIF BOLD SMALL PI
+    'ρ': '\U0001d780',  # 𝞀 MATHEMATICAL SANS-SERIF BOLD SMALL RHO
+    'ς': '\U0001d781',  # 𝞁 MATHEMATICAL SANS-SERIF BOLD SMALL FINAL SIGMA
+    'σ': '\U0001d782',  # 𝞂 MATHEMATICAL SANS-SERIF BOLD SMALL SIGMA
+    'τ': '\U0001d783',  # 𝞃 MATHEMATICAL SANS-SERIF BOLD SMALL TAU
+    'υ': '\U0001d784',  # 𝞄 MATHEMATICAL SANS-SERIF BOLD SMALL UPSILON
+    'φ': '\U0001d785',  # 𝞅 MATHEMATICAL SANS-SERIF BOLD SMALL PHI
+    'χ': '\U0001d786',  # 𝞆 MATHEMATICAL SANS-SERIF BOLD SMALL CHI
+    'ψ': '\U0001d787',  # 𝞇 MATHEMATICAL SANS-SERIF BOLD SMALL PSI
+    'ω': '\U0001d788',  # 𝞈 MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA
+    'ϑ': '\U0001d78b',  # 𝞋 MATHEMATICAL SANS-SERIF BOLD THETA SYMBOL
+    'ϕ': '\U0001d78d',  # 𝞍 MATHEMATICAL SANS-SERIF BOLD PHI SYMBOL
+    'ϖ': '\U0001d78f',  # 𝞏 MATHEMATICAL SANS-SERIF BOLD PI SYMBOL
+    'ϱ': '\U0001d78e',  # 𝞎 MATHEMATICAL SANS-SERIF BOLD RHO SYMBOL
+    'ϵ': '\U0001d78a',  # 𝞊 MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL
+    '∇': '\U0001d76f',  # 𝝯 MATHEMATICAL SANS-SERIF BOLD NABLA
+    }
+
+mathsfbfit = {
+    'A': '\U0001d63c',  # 𝘼 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL A
+    'B': '\U0001d63d',  # 𝘽 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL B
+    'C': '\U0001d63e',  # 𝘾 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL C
+    'D': '\U0001d63f',  # 𝘿 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL D
+    'E': '\U0001d640',  # 𝙀 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL E
+    'F': '\U0001d641',  # 𝙁 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL F
+    'G': '\U0001d642',  # 𝙂 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL G
+    'H': '\U0001d643',  # 𝙃 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL H
+    'I': '\U0001d644',  # 𝙄 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL I
+    'J': '\U0001d645',  # 𝙅 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL J
+    'K': '\U0001d646',  # 𝙆 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL K
+    'L': '\U0001d647',  # 𝙇 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL L
+    'M': '\U0001d648',  # 𝙈 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL M
+    'N': '\U0001d649',  # 𝙉 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL N
+    'O': '\U0001d64a',  # 𝙊 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL O
+    'P': '\U0001d64b',  # 𝙋 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL P
+    'Q': '\U0001d64c',  # 𝙌 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Q
+    'R': '\U0001d64d',  # 𝙍 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL R
+    'S': '\U0001d64e',  # 𝙎 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL S
+    'T': '\U0001d64f',  # 𝙏 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL T
+    'U': '\U0001d650',  # 𝙐 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL U
+    'V': '\U0001d651',  # 𝙑 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL V
+    'W': '\U0001d652',  # 𝙒 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL W
+    'X': '\U0001d653',  # 𝙓 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL X
+    'Y': '\U0001d654',  # 𝙔 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Y
+    'Z': '\U0001d655',  # 𝙕 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Z
+    'a': '\U0001d656',  # 𝙖 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL A
+    'b': '\U0001d657',  # 𝙗 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL B
+    'c': '\U0001d658',  # 𝙘 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL C
+    'd': '\U0001d659',  # 𝙙 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL D
+    'e': '\U0001d65a',  # 𝙚 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL E
+    'f': '\U0001d65b',  # 𝙛 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL F
+    'g': '\U0001d65c',  # 𝙜 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL G
+    'h': '\U0001d65d',  # 𝙝 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL H
+    'i': '\U0001d65e',  # 𝙞 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL I
+    'j': '\U0001d65f',  # 𝙟 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL J
+    'k': '\U0001d660',  # 𝙠 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL K
+    'l': '\U0001d661',  # 𝙡 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL L
+    'm': '\U0001d662',  # 𝙢 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL M
+    'n': '\U0001d663',  # 𝙣 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL N
+    'o': '\U0001d664',  # 𝙤 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL O
+    'p': '\U0001d665',  # 𝙥 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL P
+    'q': '\U0001d666',  # 𝙦 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Q
+    'r': '\U0001d667',  # 𝙧 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL R
+    's': '\U0001d668',  # 𝙨 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL S
+    't': '\U0001d669',  # 𝙩 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL T
+    'u': '\U0001d66a',  # 𝙪 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL U
+    'v': '\U0001d66b',  # 𝙫 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL V
+    'w': '\U0001d66c',  # 𝙬 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL W
+    'x': '\U0001d66d',  # 𝙭 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL X
+    'y': '\U0001d66e',  # 𝙮 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Y
+    'z': '\U0001d66f',  # 𝙯 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Z
+    'Γ': '\U0001d792',  # 𝞒 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL GAMMA
+    'Δ': '\U0001d793',  # 𝞓 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL DELTA
+    'Θ': '\U0001d797',  # 𝞗 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL THETA
+    'Λ': '\U0001d79a',  # 𝞚 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL LAMDA
+    'Ξ': '\U0001d79d',  # 𝞝 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL XI
+    'Π': '\U0001d79f',  # 𝞟 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PI
+    'Σ': '\U0001d7a2',  # 𝞢 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL SIGMA
+    'Υ': '\U0001d7a4',  # 𝞤 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL UPSILON
+    'Φ': '\U0001d7a5',  # 𝞥 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PHI
+    'Ψ': '\U0001d7a7',  # 𝞧 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PSI
+    'Ω': '\U0001d7a8',  # 𝞨 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA
+    'α': '\U0001d7aa',  # 𝞪 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA
+    'β': '\U0001d7ab',  # 𝞫 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL BETA
+    'γ': '\U0001d7ac',  # 𝞬 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL GAMMA
+    'δ': '\U0001d7ad',  # 𝞭 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL DELTA
+    'ε': '\U0001d7ae',  # 𝞮 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL EPSILON
+    'ζ': '\U0001d7af',  # 𝞯 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ZETA
+    'η': '\U0001d7b0',  # 𝞰 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ETA
+    'θ': '\U0001d7b1',  # 𝞱 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL THETA
+    'ι': '\U0001d7b2',  # 𝞲 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL IOTA
+    'κ': '\U0001d7b3',  # 𝞳 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL KAPPA
+    'λ': '\U0001d7b4',  # 𝞴 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL LAMDA
+    'μ': '\U0001d7b5',  # 𝞵 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL MU
+    'ν': '\U0001d7b6',  # 𝞶 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL NU
+    'ξ': '\U0001d7b7',  # 𝞷 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL XI
+    'π': '\U0001d7b9',  # 𝞹 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PI
+    'ρ': '\U0001d7ba',  # 𝞺 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL RHO
+    'ς': '\U0001d7bb',  # 𝞻 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL FINAL SIGMA
+    'σ': '\U0001d7bc',  # 𝞼 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL SIGMA
+    'τ': '\U0001d7bd',  # 𝞽 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL TAU
+    'υ': '\U0001d7be',  # 𝞾 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL UPSILON
+    'φ': '\U0001d7bf',  # 𝞿 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PHI
+    'χ': '\U0001d7c0',  # 𝟀 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL CHI
+    'ψ': '\U0001d7c1',  # 𝟁 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PSI
+    'ω': '\U0001d7c2',  # 𝟂 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA
+    'ϑ': '\U0001d7c5',  # 𝟅 MATHEMATICAL SANS-SERIF BOLD ITALIC THETA SYMBOL
+    'ϕ': '\U0001d7c7',  # 𝟇 MATHEMATICAL SANS-SERIF BOLD ITALIC PHI SYMBOL
+    'ϖ': '\U0001d7c9',  # 𝟉 MATHEMATICAL SANS-SERIF BOLD ITALIC PI SYMBOL
+    'ϰ': '\U0001d7c6',  # 𝟆 MATHEMATICAL SANS-SERIF BOLD ITALIC KAPPA SYMBOL
+    'ϱ': '\U0001d7c8',  # 𝟈 MATHEMATICAL SANS-SERIF BOLD ITALIC RHO SYMBOL
+    'ϵ': '\U0001d7c4',  # 𝟄 MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL
+    '∂': '\U0001d7c3',  # 𝟃 MATHEMATICAL SANS-SERIF BOLD ITALIC PARTIAL DIFFERENTIAL
+    '∇': '\U0001d7a9',  # 𝞩 MATHEMATICAL SANS-SERIF BOLD ITALIC NABLA
+    }
+
+mathsfit = {
+    'A': '\U0001d608',  # 𝘈 MATHEMATICAL SANS-SERIF ITALIC CAPITAL A
+    'B': '\U0001d609',  # 𝘉 MATHEMATICAL SANS-SERIF ITALIC CAPITAL B
+    'C': '\U0001d60a',  # 𝘊 MATHEMATICAL SANS-SERIF ITALIC CAPITAL C
+    'D': '\U0001d60b',  # 𝘋 MATHEMATICAL SANS-SERIF ITALIC CAPITAL D
+    'E': '\U0001d60c',  # 𝘌 MATHEMATICAL SANS-SERIF ITALIC CAPITAL E
+    'F': '\U0001d60d',  # 𝘍 MATHEMATICAL SANS-SERIF ITALIC CAPITAL F
+    'G': '\U0001d60e',  # 𝘎 MATHEMATICAL SANS-SERIF ITALIC CAPITAL G
+    'H': '\U0001d60f',  # 𝘏 MATHEMATICAL SANS-SERIF ITALIC CAPITAL H
+    'I': '\U0001d610',  # 𝘐 MATHEMATICAL SANS-SERIF ITALIC CAPITAL I
+    'J': '\U0001d611',  # 𝘑 MATHEMATICAL SANS-SERIF ITALIC CAPITAL J
+    'K': '\U0001d612',  # 𝘒 MATHEMATICAL SANS-SERIF ITALIC CAPITAL K
+    'L': '\U0001d613',  # 𝘓 MATHEMATICAL SANS-SERIF ITALIC CAPITAL L
+    'M': '\U0001d614',  # 𝘔 MATHEMATICAL SANS-SERIF ITALIC CAPITAL M
+    'N': '\U0001d615',  # 𝘕 MATHEMATICAL SANS-SERIF ITALIC CAPITAL N
+    'O': '\U0001d616',  # 𝘖 MATHEMATICAL SANS-SERIF ITALIC CAPITAL O
+    'P': '\U0001d617',  # 𝘗 MATHEMATICAL SANS-SERIF ITALIC CAPITAL P
+    'Q': '\U0001d618',  # 𝘘 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Q
+    'R': '\U0001d619',  # 𝘙 MATHEMATICAL SANS-SERIF ITALIC CAPITAL R
+    'S': '\U0001d61a',  # 𝘚 MATHEMATICAL SANS-SERIF ITALIC CAPITAL S
+    'T': '\U0001d61b',  # 𝘛 MATHEMATICAL SANS-SERIF ITALIC CAPITAL T
+    'U': '\U0001d61c',  # 𝘜 MATHEMATICAL SANS-SERIF ITALIC CAPITAL U
+    'V': '\U0001d61d',  # 𝘝 MATHEMATICAL SANS-SERIF ITALIC CAPITAL V
+    'W': '\U0001d61e',  # 𝘞 MATHEMATICAL SANS-SERIF ITALIC CAPITAL W
+    'X': '\U0001d61f',  # 𝘟 MATHEMATICAL SANS-SERIF ITALIC CAPITAL X
+    'Y': '\U0001d620',  # 𝘠 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Y
+    'Z': '\U0001d621',  # 𝘡 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Z
+    'a': '\U0001d622',  # 𝘢 MATHEMATICAL SANS-SERIF ITALIC SMALL A
+    'b': '\U0001d623',  # 𝘣 MATHEMATICAL SANS-SERIF ITALIC SMALL B
+    'c': '\U0001d624',  # 𝘤 MATHEMATICAL SANS-SERIF ITALIC SMALL C
+    'd': '\U0001d625',  # 𝘥 MATHEMATICAL SANS-SERIF ITALIC SMALL D
+    'e': '\U0001d626',  # 𝘦 MATHEMATICAL SANS-SERIF ITALIC SMALL E
+    'f': '\U0001d627',  # 𝘧 MATHEMATICAL SANS-SERIF ITALIC SMALL F
+    'g': '\U0001d628',  # 𝘨 MATHEMATICAL SANS-SERIF ITALIC SMALL G
+    'h': '\U0001d629',  # 𝘩 MATHEMATICAL SANS-SERIF ITALIC SMALL H
+    'i': '\U0001d62a',  # 𝘪 MATHEMATICAL SANS-SERIF ITALIC SMALL I
+    'j': '\U0001d62b',  # 𝘫 MATHEMATICAL SANS-SERIF ITALIC SMALL J
+    'k': '\U0001d62c',  # 𝘬 MATHEMATICAL SANS-SERIF ITALIC SMALL K
+    'l': '\U0001d62d',  # 𝘭 MATHEMATICAL SANS-SERIF ITALIC SMALL L
+    'm': '\U0001d62e',  # 𝘮 MATHEMATICAL SANS-SERIF ITALIC SMALL M
+    'n': '\U0001d62f',  # 𝘯 MATHEMATICAL SANS-SERIF ITALIC SMALL N
+    'o': '\U0001d630',  # 𝘰 MATHEMATICAL SANS-SERIF ITALIC SMALL O
+    'p': '\U0001d631',  # 𝘱 MATHEMATICAL SANS-SERIF ITALIC SMALL P
+    'q': '\U0001d632',  # 𝘲 MATHEMATICAL SANS-SERIF ITALIC SMALL Q
+    'r': '\U0001d633',  # 𝘳 MATHEMATICAL SANS-SERIF ITALIC SMALL R
+    's': '\U0001d634',  # 𝘴 MATHEMATICAL SANS-SERIF ITALIC SMALL S
+    't': '\U0001d635',  # 𝘵 MATHEMATICAL SANS-SERIF ITALIC SMALL T
+    'u': '\U0001d636',  # 𝘶 MATHEMATICAL SANS-SERIF ITALIC SMALL U
+    'v': '\U0001d637',  # 𝘷 MATHEMATICAL SANS-SERIF ITALIC SMALL V
+    'w': '\U0001d638',  # 𝘸 MATHEMATICAL SANS-SERIF ITALIC SMALL W
+    'x': '\U0001d639',  # 𝘹 MATHEMATICAL SANS-SERIF ITALIC SMALL X
+    'y': '\U0001d63a',  # 𝘺 MATHEMATICAL SANS-SERIF ITALIC SMALL Y
+    'z': '\U0001d63b',  # 𝘻 MATHEMATICAL SANS-SERIF ITALIC SMALL Z
+    }
+
+mathtt = {
+    '0': '\U0001d7f6',  # 𝟶 MATHEMATICAL MONOSPACE DIGIT ZERO
+    '1': '\U0001d7f7',  # 𝟷 MATHEMATICAL MONOSPACE DIGIT ONE
+    '2': '\U0001d7f8',  # 𝟸 MATHEMATICAL MONOSPACE DIGIT TWO
+    '3': '\U0001d7f9',  # 𝟹 MATHEMATICAL MONOSPACE DIGIT THREE
+    '4': '\U0001d7fa',  # 𝟺 MATHEMATICAL MONOSPACE DIGIT FOUR
+    '5': '\U0001d7fb',  # 𝟻 MATHEMATICAL MONOSPACE DIGIT FIVE
+    '6': '\U0001d7fc',  # 𝟼 MATHEMATICAL MONOSPACE DIGIT SIX
+    '7': '\U0001d7fd',  # 𝟽 MATHEMATICAL MONOSPACE DIGIT SEVEN
+    '8': '\U0001d7fe',  # 𝟾 MATHEMATICAL MONOSPACE DIGIT EIGHT
+    '9': '\U0001d7ff',  # 𝟿 MATHEMATICAL MONOSPACE DIGIT NINE
+    'A': '\U0001d670',  # 𝙰 MATHEMATICAL MONOSPACE CAPITAL A
+    'B': '\U0001d671',  # 𝙱 MATHEMATICAL MONOSPACE CAPITAL B
+    'C': '\U0001d672',  # 𝙲 MATHEMATICAL MONOSPACE CAPITAL C
+    'D': '\U0001d673',  # 𝙳 MATHEMATICAL MONOSPACE CAPITAL D
+    'E': '\U0001d674',  # 𝙴 MATHEMATICAL MONOSPACE CAPITAL E
+    'F': '\U0001d675',  # 𝙵 MATHEMATICAL MONOSPACE CAPITAL F
+    'G': '\U0001d676',  # 𝙶 MATHEMATICAL MONOSPACE CAPITAL G
+    'H': '\U0001d677',  # 𝙷 MATHEMATICAL MONOSPACE CAPITAL H
+    'I': '\U0001d678',  # 𝙸 MATHEMATICAL MONOSPACE CAPITAL I
+    'J': '\U0001d679',  # 𝙹 MATHEMATICAL MONOSPACE CAPITAL J
+    'K': '\U0001d67a',  # 𝙺 MATHEMATICAL MONOSPACE CAPITAL K
+    'L': '\U0001d67b',  # 𝙻 MATHEMATICAL MONOSPACE CAPITAL L
+    'M': '\U0001d67c',  # 𝙼 MATHEMATICAL MONOSPACE CAPITAL M
+    'N': '\U0001d67d',  # 𝙽 MATHEMATICAL MONOSPACE CAPITAL N
+    'O': '\U0001d67e',  # 𝙾 MATHEMATICAL MONOSPACE CAPITAL O
+    'P': '\U0001d67f',  # 𝙿 MATHEMATICAL MONOSPACE CAPITAL P
+    'Q': '\U0001d680',  # 𝚀 MATHEMATICAL MONOSPACE CAPITAL Q
+    'R': '\U0001d681',  # 𝚁 MATHEMATICAL MONOSPACE CAPITAL R
+    'S': '\U0001d682',  # 𝚂 MATHEMATICAL MONOSPACE CAPITAL S
+    'T': '\U0001d683',  # 𝚃 MATHEMATICAL MONOSPACE CAPITAL T
+    'U': '\U0001d684',  # 𝚄 MATHEMATICAL MONOSPACE CAPITAL U
+    'V': '\U0001d685',  # 𝚅 MATHEMATICAL MONOSPACE CAPITAL V
+    'W': '\U0001d686',  # 𝚆 MATHEMATICAL MONOSPACE CAPITAL W
+    'X': '\U0001d687',  # 𝚇 MATHEMATICAL MONOSPACE CAPITAL X
+    'Y': '\U0001d688',  # 𝚈 MATHEMATICAL MONOSPACE CAPITAL Y
+    'Z': '\U0001d689',  # 𝚉 MATHEMATICAL MONOSPACE CAPITAL Z
+    'a': '\U0001d68a',  # 𝚊 MATHEMATICAL MONOSPACE SMALL A
+    'b': '\U0001d68b',  # 𝚋 MATHEMATICAL MONOSPACE SMALL B
+    'c': '\U0001d68c',  # 𝚌 MATHEMATICAL MONOSPACE SMALL C
+    'd': '\U0001d68d',  # 𝚍 MATHEMATICAL MONOSPACE SMALL D
+    'e': '\U0001d68e',  # 𝚎 MATHEMATICAL MONOSPACE SMALL E
+    'f': '\U0001d68f',  # 𝚏 MATHEMATICAL MONOSPACE SMALL F
+    'g': '\U0001d690',  # 𝚐 MATHEMATICAL MONOSPACE SMALL G
+    'h': '\U0001d691',  # 𝚑 MATHEMATICAL MONOSPACE SMALL H
+    'i': '\U0001d692',  # 𝚒 MATHEMATICAL MONOSPACE SMALL I
+    'j': '\U0001d693',  # 𝚓 MATHEMATICAL MONOSPACE SMALL J
+    'k': '\U0001d694',  # 𝚔 MATHEMATICAL MONOSPACE SMALL K
+    'l': '\U0001d695',  # 𝚕 MATHEMATICAL MONOSPACE SMALL L
+    'm': '\U0001d696',  # 𝚖 MATHEMATICAL MONOSPACE SMALL M
+    'n': '\U0001d697',  # 𝚗 MATHEMATICAL MONOSPACE SMALL N
+    'o': '\U0001d698',  # 𝚘 MATHEMATICAL MONOSPACE SMALL O
+    'p': '\U0001d699',  # 𝚙 MATHEMATICAL MONOSPACE SMALL P
+    'q': '\U0001d69a',  # 𝚚 MATHEMATICAL MONOSPACE SMALL Q
+    'r': '\U0001d69b',  # 𝚛 MATHEMATICAL MONOSPACE SMALL R
+    's': '\U0001d69c',  # 𝚜 MATHEMATICAL MONOSPACE SMALL S
+    't': '\U0001d69d',  # 𝚝 MATHEMATICAL MONOSPACE SMALL T
+    'u': '\U0001d69e',  # 𝚞 MATHEMATICAL MONOSPACE SMALL U
+    'v': '\U0001d69f',  # 𝚟 MATHEMATICAL MONOSPACE SMALL V
+    'w': '\U0001d6a0',  # 𝚠 MATHEMATICAL MONOSPACE SMALL W
+    'x': '\U0001d6a1',  # 𝚡 MATHEMATICAL MONOSPACE SMALL X
+    'y': '\U0001d6a2',  # 𝚢 MATHEMATICAL MONOSPACE SMALL Y
+    'z': '\U0001d6a3',  # 𝚣 MATHEMATICAL MONOSPACE SMALL Z
+    }
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/mathml_elements.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/mathml_elements.py
new file mode 100644
index 00000000..f2059c9f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/mathml_elements.py
@@ -0,0 +1,478 @@
+# :Id: $Id: mathml_elements.py 9561 2024-03-14 16:34:48Z milde $
+# :Copyright: 2024 Günter Milde.
+#
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+"""MathML element classes based on `xml.etree`.
+
+The module is intended for programmatic generation of MathML
+and covers the part of `MathML Core`_ that is required by
+Docutil's *TeX math to MathML* converter.
+
+This module is PROVISIONAL:
+the API is not settled and may change with any minor Docutils version.
+
+.. _MathML Core: https://www.w3.org/TR/mathml-core/
+"""
+
+# Usage:
+#
+# >>> from mathml_elements import *
+
+import numbers
+import xml.etree.ElementTree as ET
+
+
+GLOBAL_ATTRIBUTES = (
+    'class',  # space-separated list of element classes
+    # 'data-*',  # custom data attributes (see HTML)
+    'dir',  # directionality ('ltr', 'rtl')
+    'displaystyle',  # True: normal, False: compact
+    'id',  # unique identifier
+    # 'mathbackground',  # color definition, deprecated
+    # 'mathcolor',  # color definition, deprecated
+    # 'mathsize',  # font-size, deprecated
+    'nonce',  # cryptographic nonce ("number used once")
+    'scriptlevel',  # math-depth for the element
+    'style',  # CSS styling declarations
+    'tabindex',  # indicate if the element takes input focus
+    )
+"""Global MathML attributes
+
+https://w3c.github.io/mathml-core/#global-attributes
+"""
+
+
+# Base classes
+# ------------
+
+class MathElement(ET.Element):
+    """Base class for MathML elements."""
+
+    nchildren = None
+    """Expected number of children or None"""
+    # cf. https://www.w3.org/TR/MathML3/chapter3.html#id.3.1.3.2
+    parent = None
+    """Parent node in MathML element tree."""
+
+    def __init__(self, *children, **attributes):
+        """Set up node with `children` and `attributes`.
+
+        Attribute names are normalised to lowercase.
+        You may use "CLASS" to set a "class" attribute.
+        Attribute values are converted to strings
+        (with True -> "true" and False -> "false").
+
+        >>> math(CLASS='test', level=3, split=True)
+        math(class='test', level='3', split='true')
+        >>> math(CLASS='test', level=3, split=True).toxml()
+        '<math class="test" level="3" split="true"></math>'
+
+        """
+        attrib = {k.lower(): self.a_str(v) for k, v in attributes.items()}
+        super().__init__(self.__class__.__name__, **attrib)
+        self.extend(children)
+
+    @staticmethod
+    def a_str(v):
+        # Return string representation for attribute value `v`.
+        if isinstance(v, bool):
+            return str(v).lower()
+        return str(v)
+
+    def __repr__(self):
+        """Return full string representation."""
+        args = [repr(child) for child in self]
+        if self.text:
+            args.append(repr(self.text))
+        if self.nchildren != self.__class__.nchildren:
+            args.append(f'nchildren={self.nchildren}')
+        if getattr(self, 'switch', None):
+            args.append('switch=True')
+        args += [f'{k}={v!r}' for k, v in self.items() if v is not None]
+        return f'{self.tag}({", ".join(args)})'
+
+    def __str__(self):
+        """Return concise, informal string representation."""
+        if self.text:
+            args = repr(self.text)
+        else:
+            args = ', '.join(f'{child}' for child in self)
+        return f'{self.tag}({args})'
+
+    def set(self, key, value):
+        super().set(key, self.a_str(value))
+
+    def __setitem__(self, key, value):
+        if self.nchildren == 0:
+            raise TypeError(f'Element "{self}" does not take children.')
+        if isinstance(value, MathElement):
+            value.parent = self
+        else:  # value may be an iterable
+            if self.nchildren and len(self) + len(value) > self.nchildren:
+                raise TypeError(f'Element "{self}" takes only {self.nchildren}'
+                                ' children')
+            for e in value:
+                e.parent = self
+        super().__setitem__(key, value)
+
+    def is_full(self):
+        """Return boolean indicating whether children may be appended."""
+        return self.nchildren is not None and len(self) >= self.nchildren
+
+    def close(self):
+        """Close element and return first non-full anchestor or None."""
+        self.nchildren = len(self)  # mark node as full
+        parent = self.parent
+        while parent is not None and parent.is_full():
+            parent = parent.parent
+        return parent
+
+    def append(self, element):
+        """Append `element` and return new "current node" (insertion point).
+
+        Append as child element and set the internal `parent` attribute.
+
+        If self is already full, raise TypeError.
+
+        If self is full after appending, call `self.close()`
+        (returns first non-full anchestor or None) else return `self`.
+        """
+        if self.is_full():
+            if self.nchildren:
+                status = f'takes only {self.nchildren} children'
+            else:
+                status = 'does not take children'
+            raise TypeError(f'Element "{self}" {status}.')
+        super().append(element)
+        element.parent = self
+        if self.is_full():
+            return self.close()
+        return self
+
+    def extend(self, elements):
+        """Sequentially append `elements`. Return new "current node".
+
+        Raise TypeError if overfull.
+        """
+        current_node = self
+        for element in elements:
+            current_node = self.append(element)
+        return current_node
+
+    def pop(self, index=-1):
+        element = self[index]
+        del self[index]
+        return element
+
+    def in_block(self):
+        """Return True, if `self` or an ancestor has ``display='block'``.
+
+        Used to find out whether we are in inline vs. displayed maths.
+        """
+        if self.get('display') is None:
+            try:
+                return self.parent.in_block()
+            except AttributeError:
+                return False
+        return self.get('display') == 'block'
+
+    # XML output:
+
+    def indent_xml(self, space='  ', level=0):
+        """Format XML output with indents.
+
+        Use with care:
+          Formatting whitespace is permanently added to the
+          `text` and `tail` attributes of `self` and anchestors!
+        """
+        ET.indent(self, space, level)
+
+    def unindent_xml(self):
+        """Strip whitespace at the end of `text` and `tail` attributes...
+
+        to revert changes made by the `indent_xml()` method.
+        Use with care, trailing whitespace from the original may be lost.
+        """
+        for e in self.iter():
+            if not isinstance(e, MathToken) and e.text:
+                e.text = e.text.rstrip()
+            if e.tail:
+                e.tail = e.tail.rstrip()
+
+    def toxml(self, encoding=None):
+        """Return an XML representation of the element.
+
+        By default, the return value is a `str` instance. With an explicit
+        `encoding` argument, the result is a `bytes` instance in the
+        specified encoding. The XML default encoding is UTF-8, any other
+        encoding must be specified in an XML document header.
+
+        Name and encoding handling match `xml.dom.minidom.Node.toxml()`
+        while `etree.Element.tostring()` returns `bytes` by default.
+        """
+        xml = ET.tostring(self, encoding or 'unicode',
+                          short_empty_elements=False)
+        # Visible representation for "Apply Function" character:
+        try:
+            xml = xml.replace('\u2061', '&ApplyFunction;')
+        except TypeError:
+            xml = xml.replace('\u2061'.encode(encoding), b'&ApplyFunction;')
+        return xml
+
+
+# Group sub-expressions in a horizontal row
+#
+# The elements <msqrt>, <mstyle>, <merror>, <mpadded>, <mphantom>,
+# <menclose>, <mtd>, <mscarry>, and <math> treat their contents
+# as a single inferred mrow formed from all their children.
+# (https://www.w3.org/TR/mathml4/#presm_inferredmrow)
+#
+# MathML Core uses the term "anonymous mrow element".
+
+class MathRow(MathElement):
+    """Base class for elements treating content as a single mrow."""
+
+
+# 2d Schemata
+
+class MathSchema(MathElement):
+    """Base class for schemata expecting 2 or more children.
+
+    The special attribute `switch` indicates that the last two child
+    elements are in reversed order and must be switched before XML-export.
+    See `msub` for an example.
+    """
+    nchildren = 2
+
+    def __init__(self, *children, **kwargs):
+        self.switch = kwargs.pop('switch', False)
+        super().__init__(*children, **kwargs)
+
+    def append(self, element):
+        """Append element. Normalize order and close if full."""
+        current_node = super().append(element)
+        if self.switch and self.is_full():
+            self[-1], self[-2] = self[-2], self[-1]
+            self.switch = False
+        return current_node
+
+
+# Token elements represent the smallest units of mathematical notation which
+# carry meaning.
+
+class MathToken(MathElement):
+    """Token Element: contains textual data instead of children.
+
+    Expect text data on initialisation.
+    """
+    nchildren = 0
+
+    def __init__(self, text, **attributes):
+        super().__init__(**attributes)
+        if not isinstance(text, (str, numbers.Number)):
+            raise ValueError('MathToken element expects `str` or number,'
+                             f' not "{text}".')
+        self.text = str(text)
+
+
+# MathML element classes
+# ----------------------
+
+class math(MathRow):
+    """Top-level MathML element, a single mathematical formula."""
+
+
+# Token elements
+# ~~~~~~~~~~~~~~
+
+class mtext(MathToken):
+    """Arbitrary text with no notational meaning."""
+
+
+class mi(MathToken):
+    """Identifier, such as a function name, variable or symbolic constant."""
+
+
+class mn(MathToken):
+    """Numeric literal.
+
+    >>> mn(3.41).toxml()
+    '<mn>3.41</mn>'
+
+    Normally a sequence of digits with a possible separator (a dot or a comma).
+    (Values with comma must be specified as `str`.)
+    """
+
+
+class mo(MathToken):
+    """Operator, Fence, Separator, or Accent.
+
+    >>> mo('<').toxml()
+    '<mo>&lt;</mo>'
+
+    Besides operators in strict mathematical meaning, this element also
+    includes "operators" like parentheses, separators like comma and
+    semicolon, or "absolute value" bars.
+    """
+
+
+class mspace(MathElement):
+    """Blank space, whose size is set by its attributes.
+
+    Takes additional attributes `depth`, `height`, `width`.
+    Takes no children and no text.
+
+    See also `mphantom`.
+    """
+    nchildren = 0
+
+
+# General Layout Schemata
+# ~~~~~~~~~~~~~~~~~~~~~~~
+
+class mrow(MathRow):
+    """Generic element to group children as a horizontal row.
+
+    Removed on closing if not required (see `mrow.close()`).
+    """
+
+    def transfer_attributes(self, other):
+        """Transfer attributes from self to other.
+
+        "List values" (class, style) are appended to existing values,
+        other values replace existing values.
+        """
+        delimiters = {'class': ' ', 'style': '; '}
+        for k, v in self.items():
+            if k in ('class', 'style') and v:
+                if other.get(k):
+                    v = delimiters[k].join(
+                        (other.get(k).rstrip(delimiters[k]), v))
+            other.set(k, v)
+
+    def close(self):
+        """Close element and return first non-full anchestor or None.
+
+        Remove <mrow> if it has only one child element.
+        """
+        parent = self.parent
+        # replace `self` with single child
+        if parent is not None and len(self) == 1:
+            child = self[0]
+            try:
+                parent[list(parent).index(self)] = child
+                child.parent = parent
+            except (AttributeError, ValueError):
+                return None
+            self.transfer_attributes(child)
+        return super().close()
+
+
+class mfrac(MathSchema):
+    """Fractions or fraction-like objects such as binomial coefficients."""
+
+
+class msqrt(MathRow):
+    """Square root. See also `mroot`."""
+    nchildren = 1  # \sqrt expects one argument or a group
+
+
+class mroot(MathSchema):
+    """Roots with an explicit index. See also `msqrt`."""
+
+
+class mstyle(MathRow):
+    """Style Change.
+
+    In modern browsers, <mstyle> is equivalent to an <mrow> element.
+    However, <mstyle> may still be relevant for compatibility with
+    MathML implementations outside browsers.
+    """
+
+
+class merror(MathRow):
+    """Display contents as error messages."""
+
+
+class menclose(MathRow):
+    """Renders content inside an enclosing notation...
+
+    ... specified by the notation attribute.
+
+    Non-standard but still required by Firefox for boxed expressions.
+    """
+    nchildren = 1  # \boxed expects one argument or a group
+
+
+class mpadded(MathRow):
+    """Adjust space around content."""
+    # nchildren = 1  # currently not used by latex2mathml
+
+
+class mphantom(MathRow):
+    """Placeholder: Rendered invisibly but dimensions are kept."""
+    nchildren = 1  # \phantom expects one argument or a group
+
+
+# Script and Limit Schemata
+# ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+class msub(MathSchema):
+    """Attach a subscript to an expression."""
+
+
+class msup(MathSchema):
+    """Attach a superscript to an expression."""
+
+
+class msubsup(MathSchema):
+    """Attach both a subscript and a superscript to an expression."""
+    nchildren = 3
+
+# Examples:
+#
+# The `switch` attribute reverses the order of the last two children:
+# >>> msub(mn(1), mn(2)).toxml()
+# '<msub><mn>1</mn><mn>2</mn></msub>'
+# >>> msub(mn(1), mn(2), switch=True).toxml()
+# '<msub><mn>2</mn><mn>1</mn></msub>'
+#
+# >>> msubsup(mi('base'), mn(1), mn(2)).toxml()
+# '<msubsup><mi>base</mi><mn>1</mn><mn>2</mn></msubsup>'
+# >>> msubsup(mi('base'), mn(1), mn(2), switch=True).toxml()
+# '<msubsup><mi>base</mi><mn>2</mn><mn>1</mn></msubsup>'
+
+
+class munder(msub):
+    """Attach an accent or a limit under an expression."""
+
+
+class mover(msup):
+    """Attach an accent or a limit over an expression."""
+
+
+class munderover(msubsup):
+    """Attach accents or limits both under and over an expression."""
+
+
+# Tabular Math
+# ~~~~~~~~~~~~
+
+class mtable(MathElement):
+    """Table or matrix element."""
+
+
+class mtr(MathRow):
+    """Row in a table or a matrix."""
+
+
+class mtd(MathRow):
+    """Cell in a table or a matrix"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py
new file mode 100644
index 00000000..11f9ab3e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py
@@ -0,0 +1,261 @@
+# :Id: $Id: tex2mathml_extern.py 9536 2024-02-01 13:04:22Z milde $
+# :Copyright: © 2015 Günter Milde.
+# :License: Released under the terms of the `2-Clause BSD license`__, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# __ https://opensource.org/licenses/BSD-2-Clause
+
+"""Wrappers for TeX->MathML conversion by external tools
+
+This module is provisional:
+the API is not settled and may change with any minor Docutils version.
+"""
+
+import subprocess
+
+from docutils import nodes
+from docutils.utils.math import MathError, wrap_math_code
+
+# `latexml` expects a complete document:
+document_template = r"""\documentclass{article}
+\begin{document}
+%s
+\end{document}
+"""
+
+
+def _check_result(result, details=[]):
+    # raise MathError if the conversion went wrong
+    # :details: list of doctree nodes with additional info
+    msg = ''
+    if not details and result.stderr:
+        details = [nodes.paragraph('', result.stderr, classes=['pre-wrap'])]
+    if details:
+        msg = f'TeX to MathML converter `{result.args[0]}` failed:'
+    elif result.returncode:
+        msg = (f'TeX to MathMl converter `{result.args[0]}` '
+               f'exited with Errno {result.returncode}.')
+    elif not result.stdout:
+        msg = f'TeX to MathML converter `{result.args[0]}` returned no MathML.'
+    if msg:
+        raise MathError(msg, details=details)
+
+
+def blahtexml(math_code, as_block=False):
+    """Convert LaTeX math code to MathML with blahtexml__.
+
+    __ http://gva.noekeon.org/blahtexml/
+    """
+    args = ['blahtexml',
+            '--mathml',
+            '--indented',
+            '--spacing', 'moderate',
+            '--mathml-encoding', 'raw',
+            '--other-encoding', 'raw',
+            '--doctype-xhtml+mathml',
+            '--annotate-TeX',
+            ]
+    # "blahtexml" expects LaTeX code without math-mode-switch.
+    # We still need to tell it about displayed equation(s).
+    mathml_args = ' display="block"' if as_block else ''
+    _wrapped = wrap_math_code(math_code, as_block)
+    if '{align*}' in _wrapped:
+        math_code = _wrapped.replace('{align*}', '{aligned}')
+
+    result = subprocess.run(args, input=math_code,
+                            capture_output=True, text=True)
+
+    # blahtexml writes <error> messages to stdout
+    if '<error>' in result.stdout:
+        result.stderr = result.stdout[result.stdout.find('<message>')+9:
+                                      result.stdout.find('</message>')]
+    else:
+        result.stdout = result.stdout[result.stdout.find('<markup>')+9:
+                                      result.stdout.find('</markup>')]
+    _check_result(result)
+    return (f'<math xmlns="http://www.w3.org/1998/Math/MathML"{mathml_args}>'
+            f'\n{result.stdout}</math>')
+
+
+def latexml(math_code, as_block=False):
+    """Convert LaTeX math code to MathML with LaTeXML__.
+
+    Comprehensive macro support but **very** slow.
+
+    __ http://dlmf.nist.gov/LaTeXML/
+    """
+
+    # LaTeXML works in 2 stages, expects complete documents.
+    #
+    # The `latexmlmath`__ convenience wrapper does not support block-level
+    # (displayed) equations.
+    #
+    # __ https://metacpan.org/dist/LaTeXML/view/bin/latexmlmath
+    args1 = ['latexml',
+             '-',  # read from stdin
+             '--preload=amsmath',
+             '--preload=amssymb',  # also loads amsfonts
+             '--inputencoding=utf8',
+             '--',
+             ]
+    math_code = document_template % wrap_math_code(math_code, as_block)
+
+    result1 = subprocess.run(args1, input=math_code,
+                             capture_output=True, text=True)
+    if result1.stderr:
+        result1.stderr = '\n'.join(line for line in result1.stderr.splitlines()
+                                   if line.startswith('Error:')
+                                   or line.startswith('Warning:')
+                                   or line.startswith('Fatal:'))
+    _check_result(result1)
+
+    args2 = ['latexmlpost',
+             '-',
+             '--nonumbersections',
+             '--format=html5',  # maths included as MathML
+             '--omitdoctype',   # Make it simple, we only need the maths.
+             '--noscan',        # ...
+             '--nocrossref',
+             '--nographicimages',
+             '--nopictureimages',
+             '--nodefaultresources',  # do not copy *.css files to output dir
+             '--'
+             ]
+    result2 = subprocess.run(args2, input=result1.stdout,
+                             capture_output=True, text=True)
+    # Extract MathML from HTML document:
+    # <table> with <math> in cells for "align", <math> element else.
+    start = result2.stdout.find('<table class="ltx_equationgroup')
+    if start != -1:
+        stop = result2.stdout.find('</table>', start)+8
+        result2.stdout = result2.stdout[start:stop].replace(
+            'ltx_equationgroup', 'borderless align-center')
+    else:
+        result2.stdout = result2.stdout[result2.stdout.find('<math'):
+                                        result2.stdout.find('</math>')+7]
+    # Search for error messages
+    if result2.stdout:
+        _msg_source = result2.stdout  # latexmlpost reports errors in output
+    else:
+        _msg_source = result2.stderr  # just in case
+    result2.stderr = '\n'.join(line for line in _msg_source.splitlines()
+                               if line.startswith('Error:')
+                               or line.startswith('Warning:')
+                               or line.startswith('Fatal:'))
+    _check_result(result2)
+    return result2.stdout
+
+
+def pandoc(math_code, as_block=False):
+    """Convert LaTeX math code to MathML with pandoc__.
+
+    __ https://pandoc.org/
+    """
+    args = ['pandoc',
+            '--mathml',
+            '--from=latex',
+            ]
+    result = subprocess.run(args, input=wrap_math_code(math_code, as_block),
+                            capture_output=True, text=True)
+
+    result.stdout = result.stdout[result.stdout.find('<math'):
+                                  result.stdout.find('</math>')+7]
+    # Pandoc (2.9.2.1) messages are pre-formatted for the terminal:
+    #   1. summary
+    #   2. math source (part)
+    #   3. error spot indicator '^' (works only in a literal block)
+    #   4. assumed problem
+    #   5. assumed solution (may be wrong or confusing)
+    # Construct a "details" list:
+    details = []
+    if result.stderr:
+        lines = result.stderr.splitlines()
+        details.append(nodes.paragraph('', lines[0]))
+        details.append(nodes.literal_block('', '\n'.join(lines[1:3])))
+        details.append(nodes.paragraph('', '\n'.join(lines[3:]),
+                                       classes=['pre-wrap']))
+    _check_result(result, details=details)
+    return result.stdout
+
+
+def ttm(math_code, as_block=False):
+    """Convert LaTeX math code to MathML with TtM__.
+
+    Aged, limited, but fast.
+
+    __ http://silas.psfc.mit.edu/tth/mml/
+    """
+    args = ['ttm',
+            '-L',  # source is LaTeX snippet
+            '-r']  # output MathML snippet
+    math_code = wrap_math_code(math_code, as_block)
+
+    # "ttm" does not support UTF-8 input. (Docutils converts most math
+    # characters to LaTeX commands before calling this function.)
+    try:
+        result = subprocess.run(args, input=math_code,
+                                capture_output=True, text=True,
+                                encoding='ISO-8859-1')
+    except UnicodeEncodeError as err:
+        raise MathError(err)
+
+    result.stdout = result.stdout[result.stdout.find('<math'):
+                                  result.stdout.find('</math>')+7]
+    if as_block:
+        result.stdout = result.stdout.replace('<math xmlns=',
+                                              '<math display="block" xmlns=')
+    result.stderr = '\n'.join(line[5:] + '.'
+                              for line in result.stderr.splitlines()
+                              if line.startswith('**** '))
+    _check_result(result)
+    return result.stdout
+
+
+# self-test
+
+if __name__ == "__main__":
+    example = (r'\frac{\partial \sin^2(\alpha)}{\partial \vec r}'
+               r'\varpi \mathbb{R} \, \text{Grüße}')
+
+    print("""<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<title>test external mathml converters</title>
+</head>
+<body>
+<p>Test external converters</p>
+<p>
+""")
+    print(f'latexml: {latexml(example)},')
+    print(f'ttm: {ttm(example.replace("mathbb", "mathbf"))},')
+    print(f'blahtexml: {blahtexml(example)},')
+    print(f'pandoc: {pandoc(example)}.')
+    print('</p>')
+
+    print('<p>latexml:</p>')
+    print(latexml(example, as_block=True))
+    print('<p>ttm:</p>')
+    print(ttm(example.replace('mathbb', 'mathbf'), as_block=True))
+    print('<p>blahtexml:</p>')
+    print(blahtexml(example, as_block=True))
+    print('<p>pandoc:</p>')
+    print(pandoc(example, as_block=True))
+
+    print('</main>\n</body>\n</html>')
+
+    buggy = r'\sinc \phy'
+    # buggy = '\sqrt[e]'
+    try:
+        # print(blahtexml(buggy))
+        # print(latexml(f'${buggy}$'))
+        print(pandoc(f'${buggy}$'))
+        # print(ttm(f'${buggy}$'))
+    except MathError as err:
+        print(err)
+        print(err.details)
+        for node in err.details:
+            print(node.astext())
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2unichar.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2unichar.py
new file mode 100644
index 00000000..c84e8a6f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2unichar.py
@@ -0,0 +1,730 @@
+#!/usr/bin/env python3
+
+# LaTeX math to Unicode symbols translation dictionaries.
+# Generated with ``write_tex2unichar.py`` from the data in
+# http://milde.users.sourceforge.net/LUCR/Math/
+
+# Includes commands from:
+#   standard LaTeX
+#   amssymb
+#   amsmath
+#   amsxtra
+#   bbold
+#   esint
+#   mathabx
+#   mathdots
+#   txfonts
+#   stmaryrd
+#   wasysym
+
+mathaccent = {
+    'acute': '\u0301',  #  ́ COMBINING ACUTE ACCENT
+    'bar': '\u0304',  #  ̄ COMBINING MACRON
+    'breve': '\u0306',  #  ̆ COMBINING BREVE
+    'check': '\u030c',  #  ̌ COMBINING CARON
+    'ddddot': '\u20dc',  #  ⃜ COMBINING FOUR DOTS ABOVE
+    'dddot': '\u20db',  #  ⃛ COMBINING THREE DOTS ABOVE
+    'ddot': '\u0308',  #  ̈ COMBINING DIAERESIS
+    'dot': '\u0307',  #  ̇ COMBINING DOT ABOVE
+    'grave': '\u0300',  #  ̀ COMBINING GRAVE ACCENT
+    'hat': '\u0302',  #  ̂ COMBINING CIRCUMFLEX ACCENT
+    'mathring': '\u030a',  #  ̊ COMBINING RING ABOVE
+    'not': '\u0338',  #  ̸ COMBINING LONG SOLIDUS OVERLAY
+    'overleftrightarrow': '\u20e1',  #  ⃡ COMBINING LEFT RIGHT ARROW ABOVE
+    'overline': '\u0305',  #  ̅ COMBINING OVERLINE
+    'tilde': '\u0303',  #  ̃ COMBINING TILDE
+    'underbar': '\u0331',  #  ̱ COMBINING MACRON BELOW
+    'underleftarrow': '\u20ee',  #  ⃮ COMBINING LEFT ARROW BELOW
+    'underline': '\u0332',  #  ̲ COMBINING LOW LINE
+    'underrightarrow': '\u20ef',  #  ⃯ COMBINING RIGHT ARROW BELOW
+    'vec': '\u20d7',  #  ⃗ COMBINING RIGHT ARROW ABOVE
+    }
+
+mathalpha = {
+    'Bbbk': '\U0001d55c',  # 𝕜 MATHEMATICAL DOUBLE-STRUCK SMALL K
+    'Delta': '\u0394',  # Δ GREEK CAPITAL LETTER DELTA
+    'Gamma': '\u0393',  # Γ GREEK CAPITAL LETTER GAMMA
+    'Im': '\u2111',  # ℑ BLACK-LETTER CAPITAL I
+    'Lambda': '\u039b',  # Λ GREEK CAPITAL LETTER LAMDA
+    'Omega': '\u03a9',  # Ω GREEK CAPITAL LETTER OMEGA
+    'Phi': '\u03a6',  # Φ GREEK CAPITAL LETTER PHI
+    'Pi': '\u03a0',  # Π GREEK CAPITAL LETTER PI
+    'Psi': '\u03a8',  # Ψ GREEK CAPITAL LETTER PSI
+    'Re': '\u211c',  # ℜ BLACK-LETTER CAPITAL R
+    'Sigma': '\u03a3',  # Σ GREEK CAPITAL LETTER SIGMA
+    'Theta': '\u0398',  # Θ GREEK CAPITAL LETTER THETA
+    'Upsilon': '\u03a5',  # Υ GREEK CAPITAL LETTER UPSILON
+    'Xi': '\u039e',  # Ξ GREEK CAPITAL LETTER XI
+    'aleph': '\u2135',  # ℵ ALEF SYMBOL
+    'alpha': '\u03b1',  # α GREEK SMALL LETTER ALPHA
+    'beta': '\u03b2',  # β GREEK SMALL LETTER BETA
+    'beth': '\u2136',  # ℶ BET SYMBOL
+    'chi': '\u03c7',  # χ GREEK SMALL LETTER CHI
+    'daleth': '\u2138',  # ℸ DALET SYMBOL
+    'delta': '\u03b4',  # δ GREEK SMALL LETTER DELTA
+    'digamma': '\u03dd',  # ϝ GREEK SMALL LETTER DIGAMMA
+    'ell': '\u2113',  # ℓ SCRIPT SMALL L
+    'epsilon': '\u03f5',  # ϵ GREEK LUNATE EPSILON SYMBOL
+    'eta': '\u03b7',  # η GREEK SMALL LETTER ETA
+    'eth': '\xf0',  # ð LATIN SMALL LETTER ETH
+    'gamma': '\u03b3',  # γ GREEK SMALL LETTER GAMMA
+    'gimel': '\u2137',  # ℷ GIMEL SYMBOL
+    'imath': '\u0131',  # ı LATIN SMALL LETTER DOTLESS I
+    'iota': '\u03b9',  # ι GREEK SMALL LETTER IOTA
+    'jmath': '\u0237',  # ȷ LATIN SMALL LETTER DOTLESS J
+    'kappa': '\u03ba',  # κ GREEK SMALL LETTER KAPPA
+    'lambda': '\u03bb',  # λ GREEK SMALL LETTER LAMDA
+    'mu': '\u03bc',  # μ GREEK SMALL LETTER MU
+    'nu': '\u03bd',  # ν GREEK SMALL LETTER NU
+    'omega': '\u03c9',  # ω GREEK SMALL LETTER OMEGA
+    'phi': '\u03d5',  # ϕ GREEK PHI SYMBOL
+    'pi': '\u03c0',  # π GREEK SMALL LETTER PI
+    'psi': '\u03c8',  # ψ GREEK SMALL LETTER PSI
+    'rho': '\u03c1',  # ρ GREEK SMALL LETTER RHO
+    'sigma': '\u03c3',  # σ GREEK SMALL LETTER SIGMA
+    'tau': '\u03c4',  # τ GREEK SMALL LETTER TAU
+    'theta': '\u03b8',  # θ GREEK SMALL LETTER THETA
+    'upsilon': '\u03c5',  # υ GREEK SMALL LETTER UPSILON
+    'varDelta': '\U0001d6e5',  # 𝛥 MATHEMATICAL ITALIC CAPITAL DELTA
+    'varGamma': '\U0001d6e4',  # 𝛤 MATHEMATICAL ITALIC CAPITAL GAMMA
+    'varLambda': '\U0001d6ec',  # 𝛬 MATHEMATICAL ITALIC CAPITAL LAMDA
+    'varOmega': '\U0001d6fa',  # 𝛺 MATHEMATICAL ITALIC CAPITAL OMEGA
+    'varPhi': '\U0001d6f7',  # 𝛷 MATHEMATICAL ITALIC CAPITAL PHI
+    'varPi': '\U0001d6f1',  # 𝛱 MATHEMATICAL ITALIC CAPITAL PI
+    'varPsi': '\U0001d6f9',  # 𝛹 MATHEMATICAL ITALIC CAPITAL PSI
+    'varSigma': '\U0001d6f4',  # 𝛴 MATHEMATICAL ITALIC CAPITAL SIGMA
+    'varTheta': '\U0001d6e9',  # 𝛩 MATHEMATICAL ITALIC CAPITAL THETA
+    'varUpsilon': '\U0001d6f6',  # 𝛶 MATHEMATICAL ITALIC CAPITAL UPSILON
+    'varXi': '\U0001d6ef',  # 𝛯 MATHEMATICAL ITALIC CAPITAL XI
+    'varepsilon': '\u03b5',  # ε GREEK SMALL LETTER EPSILON
+    'varkappa': '\u03f0',  # ϰ GREEK KAPPA SYMBOL
+    'varphi': '\u03c6',  # φ GREEK SMALL LETTER PHI
+    'varpi': '\u03d6',  # ϖ GREEK PI SYMBOL
+    'varrho': '\u03f1',  # ϱ GREEK RHO SYMBOL
+    'varsigma': '\u03c2',  # ς GREEK SMALL LETTER FINAL SIGMA
+    'vartheta': '\u03d1',  # ϑ GREEK THETA SYMBOL
+    'wp': '\u2118',  # ℘ SCRIPT CAPITAL P
+    'xi': '\u03be',  # ξ GREEK SMALL LETTER XI
+    'zeta': '\u03b6',  # ζ GREEK SMALL LETTER ZETA
+    }
+
+mathbin = {
+    'Cap': '\u22d2',  # ⋒ DOUBLE INTERSECTION
+    'Circle': '\u25cb',  # ○ WHITE CIRCLE
+    'Cup': '\u22d3',  # ⋓ DOUBLE UNION
+    'LHD': '\u25c0',  # ◀ BLACK LEFT-POINTING TRIANGLE
+    'RHD': '\u25b6',  # ▶ BLACK RIGHT-POINTING TRIANGLE
+    'amalg': '\u2a3f',  # ⨿ AMALGAMATION OR COPRODUCT
+    'ast': '\u2217',  # ∗ ASTERISK OPERATOR
+    'barwedge': '\u22bc',  # ⊼ NAND
+    'bigcirc': '\u25ef',  # ◯ LARGE CIRCLE
+    'bigtriangledown': '\u25bd',  # ▽ WHITE DOWN-POINTING TRIANGLE
+    'bigtriangleup': '\u25b3',  # △ WHITE UP-POINTING TRIANGLE
+    'bindnasrepma': '\u214b',  # ⅋ TURNED AMPERSAND
+    'blacklozenge': '\u29eb',  # ⧫ BLACK LOZENGE
+    'boxast': '\u29c6',  # ⧆ SQUARED ASTERISK
+    'boxbar': '\u25eb',  # ◫ WHITE SQUARE WITH VERTICAL BISECTING LINE
+    'boxbox': '\u29c8',  # ⧈ SQUARED SQUARE
+    'boxbslash': '\u29c5',  # ⧅ SQUARED FALLING DIAGONAL SLASH
+    'boxcircle': '\u29c7',  # ⧇ SQUARED SMALL CIRCLE
+    'boxdot': '\u22a1',  # ⊡ SQUARED DOT OPERATOR
+    'boxminus': '\u229f',  # ⊟ SQUARED MINUS
+    'boxplus': '\u229e',  # ⊞ SQUARED PLUS
+    'boxslash': '\u29c4',  # ⧄ SQUARED RISING DIAGONAL SLASH
+    'boxtimes': '\u22a0',  # ⊠ SQUARED TIMES
+    'bullet': '\u2022',  # • BULLET
+    'cap': '\u2229',  # ∩ INTERSECTION
+    'cdot': '\u22c5',  # ⋅ DOT OPERATOR
+    'circ': '\u2218',  # ∘ RING OPERATOR
+    'circledast': '\u229b',  # ⊛ CIRCLED ASTERISK OPERATOR
+    'circledbslash': '\u29b8',  # ⦸ CIRCLED REVERSE SOLIDUS
+    'circledcirc': '\u229a',  # ⊚ CIRCLED RING OPERATOR
+    'circleddash': '\u229d',  # ⊝ CIRCLED DASH
+    'circledgtr': '\u29c1',  # ⧁ CIRCLED GREATER-THAN
+    'circledless': '\u29c0',  # ⧀ CIRCLED LESS-THAN
+    'cup': '\u222a',  # ∪ UNION
+    'curlyvee': '\u22ce',  # ⋎ CURLY LOGICAL OR
+    'curlywedge': '\u22cf',  # ⋏ CURLY LOGICAL AND
+    'dagger': '\u2020',  # † DAGGER
+    'ddagger': '\u2021',  # ‡ DOUBLE DAGGER
+    'diamond': '\u22c4',  # ⋄ DIAMOND OPERATOR
+    'div': '\xf7',  # ÷ DIVISION SIGN
+    'divideontimes': '\u22c7',  # ⋇ DIVISION TIMES
+    'dotplus': '\u2214',  # ∔ DOT PLUS
+    'doublebarwedge': '\u2a5e',  # ⩞ LOGICAL AND WITH DOUBLE OVERBAR
+    'gtrdot': '\u22d7',  # ⋗ GREATER-THAN WITH DOT
+    'intercal': '\u22ba',  # ⊺ INTERCALATE
+    'interleave': '\u2af4',  # ⫴ TRIPLE VERTICAL BAR BINARY RELATION
+    'invamp': '\u214b',  # ⅋ TURNED AMPERSAND
+    'land': '\u2227',  # ∧ LOGICAL AND
+    'leftthreetimes': '\u22cb',  # ⋋ LEFT SEMIDIRECT PRODUCT
+    'lessdot': '\u22d6',  # ⋖ LESS-THAN WITH DOT
+    'lor': '\u2228',  # ∨ LOGICAL OR
+    'ltimes': '\u22c9',  # ⋉ LEFT NORMAL FACTOR SEMIDIRECT PRODUCT
+    'mp': '\u2213',  # ∓ MINUS-OR-PLUS SIGN
+    'odot': '\u2299',  # ⊙ CIRCLED DOT OPERATOR
+    'ominus': '\u2296',  # ⊖ CIRCLED MINUS
+    'oplus': '\u2295',  # ⊕ CIRCLED PLUS
+    'oslash': '\u2298',  # ⊘ CIRCLED DIVISION SLASH
+    'otimes': '\u2297',  # ⊗ CIRCLED TIMES
+    'pm': '\xb1',  # ± PLUS-MINUS SIGN
+    'rightthreetimes': '\u22cc',  # ⋌ RIGHT SEMIDIRECT PRODUCT
+    'rtimes': '\u22ca',  # ⋊ RIGHT NORMAL FACTOR SEMIDIRECT PRODUCT
+    'setminus': '\u29f5',  # ⧵ REVERSE SOLIDUS OPERATOR
+    'slash': '\u2215',  # ∕ DIVISION SLASH
+    'smallsetminus': '\u2216',  # ∖ SET MINUS
+    'smalltriangledown': '\u25bf',  # ▿ WHITE DOWN-POINTING SMALL TRIANGLE
+    'smalltriangleleft': '\u25c3',  # ◃ WHITE LEFT-POINTING SMALL TRIANGLE
+    'smalltriangleright': '\u25b9',  # ▹ WHITE RIGHT-POINTING SMALL TRIANGLE
+    'sqcap': '\u2293',  # ⊓ SQUARE CAP
+    'sqcup': '\u2294',  # ⊔ SQUARE CUP
+    'sslash': '\u2afd',  # ⫽ DOUBLE SOLIDUS OPERATOR
+    'star': '\u22c6',  # ⋆ STAR OPERATOR
+    'talloblong': '\u2afe',  # ⫾ WHITE VERTICAL BAR
+    'times': '\xd7',  # × MULTIPLICATION SIGN
+    'triangleleft': '\u25c3',  # ◃ WHITE LEFT-POINTING SMALL TRIANGLE
+    'triangleright': '\u25b9',  # ▹ WHITE RIGHT-POINTING SMALL TRIANGLE
+    'uplus': '\u228e',  # ⊎ MULTISET UNION
+    'vee': '\u2228',  # ∨ LOGICAL OR
+    'veebar': '\u22bb',  # ⊻ XOR
+    'wedge': '\u2227',  # ∧ LOGICAL AND
+    'wr': '\u2240',  # ≀ WREATH PRODUCT
+    }
+
+mathclose = {
+    'Rbag': '\u27c6',  # ⟆ RIGHT S-SHAPED BAG DELIMITER
+    'lrcorner': '\u231f',  # ⌟ BOTTOM RIGHT CORNER
+    'rangle': '\u27e9',  # ⟩ MATHEMATICAL RIGHT ANGLE BRACKET
+    'rbag': '\u27c6',  # ⟆ RIGHT S-SHAPED BAG DELIMITER
+    'rbrace': '}',  # } RIGHT CURLY BRACKET
+    'rbrack': ']',  # ] RIGHT SQUARE BRACKET
+    'rceil': '\u2309',  # ⌉ RIGHT CEILING
+    'rfloor': '\u230b',  # ⌋ RIGHT FLOOR
+    'rgroup': '\u27ef',  # ⟯ MATHEMATICAL RIGHT FLATTENED PARENTHESIS
+    'rrbracket': '\u27e7',  # ⟧ MATHEMATICAL RIGHT WHITE SQUARE BRACKET
+    'rrparenthesis': '\u2988',  # ⦈ Z NOTATION RIGHT IMAGE BRACKET
+    'urcorner': '\u231d',  # ⌝ TOP RIGHT CORNER
+    '}': '}',  # } RIGHT CURLY BRACKET
+    }
+
+mathfence = {
+    'Vert': '\u2016',  # ‖ DOUBLE VERTICAL LINE
+    'vert': '|',  # | VERTICAL LINE
+    '|': '\u2016',  # ‖ DOUBLE VERTICAL LINE
+    }
+
+mathop = {
+    'bigcap': '\u22c2',  # ⋂ N-ARY INTERSECTION
+    'bigcup': '\u22c3',  # ⋃ N-ARY UNION
+    'biginterleave': '\u2afc',  # ⫼ LARGE TRIPLE VERTICAL BAR OPERATOR
+    'bigodot': '\u2a00',  # ⨀ N-ARY CIRCLED DOT OPERATOR
+    'bigoplus': '\u2a01',  # ⨁ N-ARY CIRCLED PLUS OPERATOR
+    'bigotimes': '\u2a02',  # ⨂ N-ARY CIRCLED TIMES OPERATOR
+    'bigsqcap': '\u2a05',  # ⨅ N-ARY SQUARE INTERSECTION OPERATOR
+    'bigsqcup': '\u2a06',  # ⨆ N-ARY SQUARE UNION OPERATOR
+    'biguplus': '\u2a04',  # ⨄ N-ARY UNION OPERATOR WITH PLUS
+    'bigvee': '\u22c1',  # ⋁ N-ARY LOGICAL OR
+    'bigwedge': '\u22c0',  # ⋀ N-ARY LOGICAL AND
+    'coprod': '\u2210',  # ∐ N-ARY COPRODUCT
+    'fatsemi': '\u2a1f',  # ⨟ Z NOTATION SCHEMA COMPOSITION
+    'fint': '\u2a0f',  # ⨏ INTEGRAL AVERAGE WITH SLASH
+    'iiiint': '\u2a0c',  # ⨌ QUADRUPLE INTEGRAL OPERATOR
+    'iiint': '\u222d',  # ∭ TRIPLE INTEGRAL
+    'iint': '\u222c',  # ∬ DOUBLE INTEGRAL
+    'int': '\u222b',  # ∫ INTEGRAL
+    'intop': '\u222b',  # ∫ INTEGRAL
+    'oiiint': '\u2230',  # ∰ VOLUME INTEGRAL
+    'oiint': '\u222f',  # ∯ SURFACE INTEGRAL
+    'oint': '\u222e',  # ∮ CONTOUR INTEGRAL
+    'ointctrclockwise': '\u2233',  # ∳ ANTICLOCKWISE CONTOUR INTEGRAL
+    'ointop': '\u222e',  # ∮ CONTOUR INTEGRAL
+    'prod': '\u220f',  # ∏ N-ARY PRODUCT
+    'sqint': '\u2a16',  # ⨖ QUATERNION INTEGRAL OPERATOR
+    'sum': '\u2211',  # ∑ N-ARY SUMMATION
+    'varointclockwise': '\u2232',  # ∲ CLOCKWISE CONTOUR INTEGRAL
+    'varprod': '\u2a09',  # ⨉ N-ARY TIMES OPERATOR
+    }
+
+mathopen = {
+    'Lbag': '\u27c5',  # ⟅ LEFT S-SHAPED BAG DELIMITER
+    'langle': '\u27e8',  # ⟨ MATHEMATICAL LEFT ANGLE BRACKET
+    'lbag': '\u27c5',  # ⟅ LEFT S-SHAPED BAG DELIMITER
+    'lbrace': '{',  # { LEFT CURLY BRACKET
+    'lbrack': '[',  # [ LEFT SQUARE BRACKET
+    'lceil': '\u2308',  # ⌈ LEFT CEILING
+    'lfloor': '\u230a',  # ⌊ LEFT FLOOR
+    'lgroup': '\u27ee',  # ⟮ MATHEMATICAL LEFT FLATTENED PARENTHESIS
+    'llbracket': '\u27e6',  # ⟦ MATHEMATICAL LEFT WHITE SQUARE BRACKET
+    'llcorner': '\u231e',  # ⌞ BOTTOM LEFT CORNER
+    'llparenthesis': '\u2987',  # ⦇ Z NOTATION LEFT IMAGE BRACKET
+    'ulcorner': '\u231c',  # ⌜ TOP LEFT CORNER
+    '{': '{',  # { LEFT CURLY BRACKET
+    }
+
+mathord = {
+    '#': '#',  # # NUMBER SIGN
+    '$': '$',  # $ DOLLAR SIGN
+    '%': '%',  # % PERCENT SIGN
+    '&': '&',  # & AMPERSAND
+    'AC': '\u223f',  # ∿ SINE WAVE
+    'APLcomment': '\u235d',  # ⍝ APL FUNCTIONAL SYMBOL UP SHOE JOT
+    'APLdownarrowbox': '\u2357',  # ⍗ APL FUNCTIONAL SYMBOL QUAD DOWNWARDS ARROW
+    'APLinput': '\u235e',  # ⍞ APL FUNCTIONAL SYMBOL QUOTE QUAD
+    'APLinv': '\u2339',  # ⌹ APL FUNCTIONAL SYMBOL QUAD DIVIDE
+    'APLleftarrowbox': '\u2347',  # ⍇ APL FUNCTIONAL SYMBOL QUAD LEFTWARDS ARROW
+    'APLlog': '\u235f',  # ⍟ APL FUNCTIONAL SYMBOL CIRCLE STAR
+    'APLrightarrowbox': '\u2348',  # ⍈ APL FUNCTIONAL SYMBOL QUAD RIGHTWARDS ARROW
+    'APLuparrowbox': '\u2350',  # ⍐ APL FUNCTIONAL SYMBOL QUAD UPWARDS ARROW
+    'Aries': '\u2648',  # ♈ ARIES
+    'Box': '\u2b1c',  # ⬜ WHITE LARGE SQUARE
+    'CIRCLE': '\u25cf',  # ● BLACK CIRCLE
+    'CheckedBox': '\u2611',  # ☑ BALLOT BOX WITH CHECK
+    'Diamond': '\u25c7',  # ◇ WHITE DIAMOND
+    'Diamondblack': '\u25c6',  # ◆ BLACK DIAMOND
+    'Diamonddot': '\u27d0',  # ⟐ WHITE DIAMOND WITH CENTRED DOT
+    'Finv': '\u2132',  # Ⅎ TURNED CAPITAL F
+    'Game': '\u2141',  # ⅁ TURNED SANS-SERIF CAPITAL G
+    'Gemini': '\u264a',  # ♊ GEMINI
+    'Jupiter': '\u2643',  # ♃ JUPITER
+    'LEFTCIRCLE': '\u25d6',  # ◖ LEFT HALF BLACK CIRCLE
+    'LEFTcircle': '\u25d0',  # ◐ CIRCLE WITH LEFT HALF BLACK
+    'Leo': '\u264c',  # ♌ LEO
+    'Libra': '\u264e',  # ♎ LIBRA
+    'Mars': '\u2642',  # ♂ MALE SIGN
+    'Mercury': '\u263f',  # ☿ MERCURY
+    'Neptune': '\u2646',  # ♆ NEPTUNE
+    'P': '\xb6',  # ¶ PILCROW SIGN
+    'Pluto': '\u2647',  # ♇ PLUTO
+    'RIGHTCIRCLE': '\u25d7',  # ◗ RIGHT HALF BLACK CIRCLE
+    'RIGHTcircle': '\u25d1',  # ◑ CIRCLE WITH RIGHT HALF BLACK
+    'S': '\xa7',  # § SECTION SIGN
+    'Saturn': '\u2644',  # ♄ SATURN
+    'Scorpio': '\u264f',  # ♏ SCORPIUS
+    'Square': '\u2610',  # ☐ BALLOT BOX
+    'Sun': '\u2609',  # ☉ SUN
+    'Taurus': '\u2649',  # ♉ TAURUS
+    'Uranus': '\u2645',  # ♅ URANUS
+    'Venus': '\u2640',  # ♀ FEMALE SIGN
+    'XBox': '\u2612',  # ☒ BALLOT BOX WITH X
+    'Yup': '\u2144',  # ⅄ TURNED SANS-SERIF CAPITAL Y
+    '_': '_',  # _ LOW LINE
+    'angle': '\u2220',  # ∠ ANGLE
+    'aquarius': '\u2652',  # ♒ AQUARIUS
+    'aries': '\u2648',  # ♈ ARIES
+    'arrowvert': '\u23d0',  # ⏐ VERTICAL LINE EXTENSION
+    'backprime': '\u2035',  # ‵ REVERSED PRIME
+    'backslash': '\\',  # \ REVERSE SOLIDUS
+    'bigstar': '\u2605',  # ★ BLACK STAR
+    'blacksmiley': '\u263b',  # ☻ BLACK SMILING FACE
+    'blacksquare': '\u25fc',  # ◼ BLACK MEDIUM SQUARE
+    'blacktriangle': '\u25b4',  # ▴ BLACK UP-POINTING SMALL TRIANGLE
+    'blacktriangledown': '\u25be',  # ▾ BLACK DOWN-POINTING SMALL TRIANGLE
+    'blacktriangleup': '\u25b4',  # ▴ BLACK UP-POINTING SMALL TRIANGLE
+    'bot': '\u22a5',  # ⊥ UP TACK
+    'boy': '\u2642',  # ♂ MALE SIGN
+    'bracevert': '\u23aa',  # ⎪ CURLY BRACKET EXTENSION
+    'cancer': '\u264b',  # ♋ CANCER
+    'capricornus': '\u2651',  # ♑ CAPRICORN
+    'cdots': '\u22ef',  # ⋯ MIDLINE HORIZONTAL ELLIPSIS
+    'cent': '\xa2',  # ¢ CENT SIGN
+    'checkmark': '\u2713',  # ✓ CHECK MARK
+    'circledR': '\u24c7',  # Ⓡ CIRCLED LATIN CAPITAL LETTER R
+    'circledS': '\u24c8',  # Ⓢ CIRCLED LATIN CAPITAL LETTER S
+    'clubsuit': '\u2663',  # ♣ BLACK CLUB SUIT
+    'complement': '\u2201',  # ∁ COMPLEMENT
+    'diagdown': '\u27cd',  # ⟍ MATHEMATICAL FALLING DIAGONAL
+    'diagup': '\u27cb',  # ⟋ MATHEMATICAL RISING DIAGONAL
+    'diameter': '\u2300',  # ⌀ DIAMETER SIGN
+    'diamondsuit': '\u2662',  # ♢ WHITE DIAMOND SUIT
+    'earth': '\u2641',  # ♁ EARTH
+    'emptyset': '\u2205',  # ∅ EMPTY SET
+    'exists': '\u2203',  # ∃ THERE EXISTS
+    'female': '\u2640',  # ♀ FEMALE SIGN
+    'flat': '\u266d',  # ♭ MUSIC FLAT SIGN
+    'forall': '\u2200',  # ∀ FOR ALL
+    'fourth': '\u2057',  # ⁗ QUADRUPLE PRIME
+    'frownie': '\u2639',  # ☹ WHITE FROWNING FACE
+    'gemini': '\u264a',  # ♊ GEMINI
+    'girl': '\u2640',  # ♀ FEMALE SIGN
+    'heartsuit': '\u2661',  # ♡ WHITE HEART SUIT
+    'hslash': '\u210f',  # ℏ PLANCK CONSTANT OVER TWO PI
+    'infty': '\u221e',  # ∞ INFINITY
+    'invdiameter': '\u2349',  # ⍉ APL FUNCTIONAL SYMBOL CIRCLE BACKSLASH
+    'invneg': '\u2310',  # ⌐ REVERSED NOT SIGN
+    'jupiter': '\u2643',  # ♃ JUPITER
+    'ldots': '\u2026',  # … HORIZONTAL ELLIPSIS
+    'leftmoon': '\u263e',  # ☾ LAST QUARTER MOON
+    'leo': '\u264c',  # ♌ LEO
+    'libra': '\u264e',  # ♎ LIBRA
+    'lmoustache': '\u23b0',  # ⎰ UPPER LEFT OR LOWER RIGHT CURLY BRACKET SECTION
+    'lnot': '\xac',  # ¬ NOT SIGN
+    'lozenge': '\u25ca',  # ◊ LOZENGE
+    'male': '\u2642',  # ♂ MALE SIGN
+    'maltese': '\u2720',  # ✠ MALTESE CROSS
+    'mathcent': '\xa2',  # ¢ CENT SIGN
+    'mathdollar': '$',  # $ DOLLAR SIGN
+    'mathsterling': '\xa3',  # £ POUND SIGN
+    'measuredangle': '\u2221',  # ∡ MEASURED ANGLE
+    'medbullet': '\u26ab',  # ⚫ MEDIUM BLACK CIRCLE
+    'medcirc': '\u26aa',  # ⚪ MEDIUM WHITE CIRCLE
+    'mercury': '\u263f',  # ☿ MERCURY
+    'mho': '\u2127',  # ℧ INVERTED OHM SIGN
+    'nabla': '\u2207',  # ∇ NABLA
+    'natural': '\u266e',  # ♮ MUSIC NATURAL SIGN
+    'neg': '\xac',  # ¬ NOT SIGN
+    'neptune': '\u2646',  # ♆ NEPTUNE
+    'nexists': '\u2204',  # ∄ THERE DOES NOT EXIST
+    'notbackslash': '\u2340',  # ⍀ APL FUNCTIONAL SYMBOL BACKSLASH BAR
+    'partial': '\u2202',  # ∂ PARTIAL DIFFERENTIAL
+    'pisces': '\u2653',  # ♓ PISCES
+    'pluto': '\u2647',  # ♇ PLUTO
+    'pounds': '\xa3',  # £ POUND SIGN
+    'prime': '\u2032',  # ′ PRIME
+    'quarternote': '\u2669',  # ♩ QUARTER NOTE
+    'rightmoon': '\u263d',  # ☽ FIRST QUARTER MOON
+    'rmoustache': '\u23b1',  # ⎱ UPPER RIGHT OR LOWER LEFT CURLY BRACKET SECTION
+    'sagittarius': '\u2650',  # ♐ SAGITTARIUS
+    'saturn': '\u2644',  # ♄ SATURN
+    'scorpio': '\u264f',  # ♏ SCORPIUS
+    'second': '\u2033',  # ″ DOUBLE PRIME
+    'sharp': '\u266f',  # ♯ MUSIC SHARP SIGN
+    'smiley': '\u263a',  # ☺ WHITE SMILING FACE
+    'spadesuit': '\u2660',  # ♠ BLACK SPADE SUIT
+    'spddot': '\xa8',  # ¨ DIAERESIS
+    'sphat': '^',  # ^ CIRCUMFLEX ACCENT
+    'sphericalangle': '\u2222',  # ∢ SPHERICAL ANGLE
+    'sptilde': '~',  # ~ TILDE
+    'square': '\u25fb',  # ◻ WHITE MEDIUM SQUARE
+    'sun': '\u263c',  # ☼ WHITE SUN WITH RAYS
+    'surd': '\u221a',  # √ SQUARE ROOT
+    'taurus': '\u2649',  # ♉ TAURUS
+    'third': '\u2034',  # ‴ TRIPLE PRIME
+    'top': '\u22a4',  # ⊤ DOWN TACK
+    'twonotes': '\u266b',  # ♫ BEAMED EIGHTH NOTES
+    'uranus': '\u2645',  # ♅ URANUS
+    'varEarth': '\u2641',  # ♁ EARTH
+    'varclubsuit': '\u2667',  # ♧ WHITE CLUB SUIT
+    'vardiamondsuit': '\u2666',  # ♦ BLACK DIAMOND SUIT
+    'varheartsuit': '\u2665',  # ♥ BLACK HEART SUIT
+    'varspadesuit': '\u2664',  # ♤ WHITE SPADE SUIT
+    'virgo': '\u264d',  # ♍ VIRGO
+    'wasylozenge': '\u2311',  # ⌑ SQUARE LOZENGE
+    'yen': '\xa5',  # ¥ YEN SIGN
+    }
+
+mathover = {
+    'overbrace': '\u23de',  # ⏞ TOP CURLY BRACKET
+    'wideparen': '\u23dc',  # ⏜ TOP PARENTHESIS
+    }
+
+mathpunct = {
+    'ddots': '\u22f1',  # ⋱ DOWN RIGHT DIAGONAL ELLIPSIS
+    'vdots': '\u22ee',  # ⋮ VERTICAL ELLIPSIS
+    }
+
+mathradical = {
+    'sqrt[3]': '\u221b',  # ∛ CUBE ROOT
+    'sqrt[4]': '\u221c',  # ∜ FOURTH ROOT
+    }
+
+mathrel = {
+    'Bot': '\u2aeb',  # ⫫ DOUBLE UP TACK
+    'Bumpeq': '\u224e',  # ≎ GEOMETRICALLY EQUIVALENT TO
+    'Coloneqq': '\u2a74',  # ⩴ DOUBLE COLON EQUAL
+    'Doteq': '\u2251',  # ≑ GEOMETRICALLY EQUAL TO
+    'Downarrow': '\u21d3',  # ⇓ DOWNWARDS DOUBLE ARROW
+    'Leftarrow': '\u21d0',  # ⇐ LEFTWARDS DOUBLE ARROW
+    'Leftrightarrow': '\u21d4',  # ⇔ LEFT RIGHT DOUBLE ARROW
+    'Lleftarrow': '\u21da',  # ⇚ LEFTWARDS TRIPLE ARROW
+    'Longleftarrow': '\u27f8',  # ⟸ LONG LEFTWARDS DOUBLE ARROW
+    'Longleftrightarrow': '\u27fa',  # ⟺ LONG LEFT RIGHT DOUBLE ARROW
+    'Longmapsfrom': '\u27fd',  # ⟽ LONG LEFTWARDS DOUBLE ARROW FROM BAR
+    'Longmapsto': '\u27fe',  # ⟾ LONG RIGHTWARDS DOUBLE ARROW FROM BAR
+    'Longrightarrow': '\u27f9',  # ⟹ LONG RIGHTWARDS DOUBLE ARROW
+    'Lsh': '\u21b0',  # ↰ UPWARDS ARROW WITH TIP LEFTWARDS
+    'Mapsfrom': '\u2906',  # ⤆ LEFTWARDS DOUBLE ARROW FROM BAR
+    'Mapsto': '\u2907',  # ⤇ RIGHTWARDS DOUBLE ARROW FROM BAR
+    'Nearrow': '\u21d7',  # ⇗ NORTH EAST DOUBLE ARROW
+    'Nwarrow': '\u21d6',  # ⇖ NORTH WEST DOUBLE ARROW
+    'Perp': '\u2aeb',  # ⫫ DOUBLE UP TACK
+    'Rightarrow': '\u21d2',  # ⇒ RIGHTWARDS DOUBLE ARROW
+    'Rrightarrow': '\u21db',  # ⇛ RIGHTWARDS TRIPLE ARROW
+    'Rsh': '\u21b1',  # ↱ UPWARDS ARROW WITH TIP RIGHTWARDS
+    'Searrow': '\u21d8',  # ⇘ SOUTH EAST DOUBLE ARROW
+    'Subset': '\u22d0',  # ⋐ DOUBLE SUBSET
+    'Supset': '\u22d1',  # ⋑ DOUBLE SUPERSET
+    'Swarrow': '\u21d9',  # ⇙ SOUTH WEST DOUBLE ARROW
+    'Top': '\u2aea',  # ⫪ DOUBLE DOWN TACK
+    'Uparrow': '\u21d1',  # ⇑ UPWARDS DOUBLE ARROW
+    'Updownarrow': '\u21d5',  # ⇕ UP DOWN DOUBLE ARROW
+    'VDash': '\u22ab',  # ⊫ DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE
+    'Vdash': '\u22a9',  # ⊩ FORCES
+    'Vvdash': '\u22aa',  # ⊪ TRIPLE VERTICAL BAR RIGHT TURNSTILE
+    'apprge': '\u2273',  # ≳ GREATER-THAN OR EQUIVALENT TO
+    'apprle': '\u2272',  # ≲ LESS-THAN OR EQUIVALENT TO
+    'approx': '\u2248',  # ≈ ALMOST EQUAL TO
+    'approxeq': '\u224a',  # ≊ ALMOST EQUAL OR EQUAL TO
+    'asymp': '\u224d',  # ≍ EQUIVALENT TO
+    'backepsilon': '\u220d',  # ∍ SMALL CONTAINS AS MEMBER
+    'backsim': '\u223d',  # ∽ REVERSED TILDE
+    'backsimeq': '\u22cd',  # ⋍ REVERSED TILDE EQUALS
+    'barin': '\u22f6',  # ⋶ ELEMENT OF WITH OVERBAR
+    'barleftharpoon': '\u296b',  # ⥫ LEFTWARDS HARPOON WITH BARB DOWN BELOW LONG DASH
+    'barrightharpoon': '\u296d',  # ⥭ RIGHTWARDS HARPOON WITH BARB DOWN BELOW LONG DASH
+    'because': '\u2235',  # ∵ BECAUSE
+    'between': '\u226c',  # ≬ BETWEEN
+    'blacktriangleleft': '\u25c2',  # ◂ BLACK LEFT-POINTING SMALL TRIANGLE
+    'blacktriangleright': '\u25b8',  # ▸ BLACK RIGHT-POINTING SMALL TRIANGLE
+    'bowtie': '\u22c8',  # ⋈ BOWTIE
+    'bumpeq': '\u224f',  # ≏ DIFFERENCE BETWEEN
+    'circeq': '\u2257',  # ≗ RING EQUAL TO
+    'circlearrowleft': '\u21ba',  # ↺ ANTICLOCKWISE OPEN CIRCLE ARROW
+    'circlearrowright': '\u21bb',  # ↻ CLOCKWISE OPEN CIRCLE ARROW
+    'coloneq': '\u2254',  # ≔ COLON EQUALS
+    'coloneqq': '\u2254',  # ≔ COLON EQUALS
+    'cong': '\u2245',  # ≅ APPROXIMATELY EQUAL TO
+    'corresponds': '\u2259',  # ≙ ESTIMATES
+    'curlyeqprec': '\u22de',  # ⋞ EQUAL TO OR PRECEDES
+    'curlyeqsucc': '\u22df',  # ⋟ EQUAL TO OR SUCCEEDS
+    'curvearrowleft': '\u21b6',  # ↶ ANTICLOCKWISE TOP SEMICIRCLE ARROW
+    'curvearrowright': '\u21b7',  # ↷ CLOCKWISE TOP SEMICIRCLE ARROW
+    'dasharrow': '\u21e2',  # ⇢ RIGHTWARDS DASHED ARROW
+    'dashleftarrow': '\u21e0',  # ⇠ LEFTWARDS DASHED ARROW
+    'dashrightarrow': '\u21e2',  # ⇢ RIGHTWARDS DASHED ARROW
+    'dashv': '\u22a3',  # ⊣ LEFT TACK
+    'dlsh': '\u21b2',  # ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
+    'doteq': '\u2250',  # ≐ APPROACHES THE LIMIT
+    'doteqdot': '\u2251',  # ≑ GEOMETRICALLY EQUAL TO
+    'downarrow': '\u2193',  # ↓ DOWNWARDS ARROW
+    'downdownarrows': '\u21ca',  # ⇊ DOWNWARDS PAIRED ARROWS
+    'downdownharpoons': '\u2965',  # ⥥ DOWNWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT
+    'downharpoonleft': '\u21c3',  # ⇃ DOWNWARDS HARPOON WITH BARB LEFTWARDS
+    'downharpoonright': '\u21c2',  # ⇂ DOWNWARDS HARPOON WITH BARB RIGHTWARDS
+    'downuparrows': '\u21f5',  # ⇵ DOWNWARDS ARROW LEFTWARDS OF UPWARDS ARROW
+    'downupharpoons': '\u296f',  # ⥯ DOWNWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT
+    'drsh': '\u21b3',  # ↳ DOWNWARDS ARROW WITH TIP RIGHTWARDS
+    'eqcirc': '\u2256',  # ≖ RING IN EQUAL TO
+    'eqcolon': '\u2255',  # ≕ EQUALS COLON
+    'eqqcolon': '\u2255',  # ≕ EQUALS COLON
+    'eqsim': '\u2242',  # ≂ MINUS TILDE
+    'eqslantgtr': '\u2a96',  # ⪖ SLANTED EQUAL TO OR GREATER-THAN
+    'eqslantless': '\u2a95',  # ⪕ SLANTED EQUAL TO OR LESS-THAN
+    'equiv': '\u2261',  # ≡ IDENTICAL TO
+    'fallingdotseq': '\u2252',  # ≒ APPROXIMATELY EQUAL TO OR THE IMAGE OF
+    'frown': '\u2322',  # ⌢ FROWN
+    'ge': '\u2265',  # ≥ GREATER-THAN OR EQUAL TO
+    'geq': '\u2265',  # ≥ GREATER-THAN OR EQUAL TO
+    'geqq': '\u2267',  # ≧ GREATER-THAN OVER EQUAL TO
+    'geqslant': '\u2a7e',  # ⩾ GREATER-THAN OR SLANTED EQUAL TO
+    'gets': '\u2190',  # ← LEFTWARDS ARROW
+    'gg': '\u226b',  # ≫ MUCH GREATER-THAN
+    'ggcurly': '\u2abc',  # ⪼ DOUBLE SUCCEEDS
+    'ggg': '\u22d9',  # ⋙ VERY MUCH GREATER-THAN
+    'gggtr': '\u22d9',  # ⋙ VERY MUCH GREATER-THAN
+    'gnapprox': '\u2a8a',  # ⪊ GREATER-THAN AND NOT APPROXIMATE
+    'gneq': '\u2a88',  # ⪈ GREATER-THAN AND SINGLE-LINE NOT EQUAL TO
+    'gneqq': '\u2269',  # ≩ GREATER-THAN BUT NOT EQUAL TO
+    'gnsim': '\u22e7',  # ⋧ GREATER-THAN BUT NOT EQUIVALENT TO
+    'gtrapprox': '\u2a86',  # ⪆ GREATER-THAN OR APPROXIMATE
+    'gtreqless': '\u22db',  # ⋛ GREATER-THAN EQUAL TO OR LESS-THAN
+    'gtreqqless': '\u2a8c',  # ⪌ GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN
+    'gtrless': '\u2277',  # ≷ GREATER-THAN OR LESS-THAN
+    'gtrsim': '\u2273',  # ≳ GREATER-THAN OR EQUIVALENT TO
+    'hash': '\u22d5',  # ⋕ EQUAL AND PARALLEL TO
+    'hookleftarrow': '\u21a9',  # ↩ LEFTWARDS ARROW WITH HOOK
+    'hookrightarrow': '\u21aa',  # ↪ RIGHTWARDS ARROW WITH HOOK
+    'iddots': '\u22f0',  # ⋰ UP RIGHT DIAGONAL ELLIPSIS
+    'impliedby': '\u27f8',  # ⟸ LONG LEFTWARDS DOUBLE ARROW
+    'implies': '\u27f9',  # ⟹ LONG RIGHTWARDS DOUBLE ARROW
+    'in': '\u2208',  # ∈ ELEMENT OF
+    'le': '\u2264',  # ≤ LESS-THAN OR EQUAL TO
+    'leadsto': '\u2933',  # ⤳ WAVE ARROW POINTING DIRECTLY RIGHT
+    'leftarrow': '\u2190',  # ← LEFTWARDS ARROW
+    'leftarrowtail': '\u21a2',  # ↢ LEFTWARDS ARROW WITH TAIL
+    'leftarrowtriangle': '\u21fd',  # ⇽ LEFTWARDS OPEN-HEADED ARROW
+    'leftbarharpoon': '\u296a',  # ⥪ LEFTWARDS HARPOON WITH BARB UP ABOVE LONG DASH
+    'leftharpoondown': '\u21bd',  # ↽ LEFTWARDS HARPOON WITH BARB DOWNWARDS
+    'leftharpoonup': '\u21bc',  # ↼ LEFTWARDS HARPOON WITH BARB UPWARDS
+    'leftleftarrows': '\u21c7',  # ⇇ LEFTWARDS PAIRED ARROWS
+    'leftleftharpoons': '\u2962',  # ⥢ LEFTWARDS HARPOON WITH BARB UP ABOVE LEFTWARDS HARPOON WITH BARB DOWN
+    'leftrightarrow': '\u2194',  # ↔ LEFT RIGHT ARROW
+    'leftrightarrows': '\u21c6',  # ⇆ LEFTWARDS ARROW OVER RIGHTWARDS ARROW
+    'leftrightarrowtriangle': '\u21ff',  # ⇿ LEFT RIGHT OPEN-HEADED ARROW
+    'leftrightharpoon': '\u294a',  # ⥊ LEFT BARB UP RIGHT BARB DOWN HARPOON
+    'leftrightharpoons': '\u21cb',  # ⇋ LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON
+    'leftrightsquigarrow': '\u21ad',  # ↭ LEFT RIGHT WAVE ARROW
+    'leftslice': '\u2aa6',  # ⪦ LESS-THAN CLOSED BY CURVE
+    'leftsquigarrow': '\u21dc',  # ⇜ LEFTWARDS SQUIGGLE ARROW
+    'leftturn': '\u21ba',  # ↺ ANTICLOCKWISE OPEN CIRCLE ARROW
+    'leq': '\u2264',  # ≤ LESS-THAN OR EQUAL TO
+    'leqq': '\u2266',  # ≦ LESS-THAN OVER EQUAL TO
+    'leqslant': '\u2a7d',  # ⩽ LESS-THAN OR SLANTED EQUAL TO
+    'lessapprox': '\u2a85',  # ⪅ LESS-THAN OR APPROXIMATE
+    'lesseqgtr': '\u22da',  # ⋚ LESS-THAN EQUAL TO OR GREATER-THAN
+    'lesseqqgtr': '\u2a8b',  # ⪋ LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN
+    'lessgtr': '\u2276',  # ≶ LESS-THAN OR GREATER-THAN
+    'lesssim': '\u2272',  # ≲ LESS-THAN OR EQUIVALENT TO
+    'lhd': '\u22b2',  # ⊲ NORMAL SUBGROUP OF
+    'lightning': '\u21af',  # ↯ DOWNWARDS ZIGZAG ARROW
+    'll': '\u226a',  # ≪ MUCH LESS-THAN
+    'llcurly': '\u2abb',  # ⪻ DOUBLE PRECEDES
+    'lll': '\u22d8',  # ⋘ VERY MUCH LESS-THAN
+    'llless': '\u22d8',  # ⋘ VERY MUCH LESS-THAN
+    'lnapprox': '\u2a89',  # ⪉ LESS-THAN AND NOT APPROXIMATE
+    'lneq': '\u2a87',  # ⪇ LESS-THAN AND SINGLE-LINE NOT EQUAL TO
+    'lneqq': '\u2268',  # ≨ LESS-THAN BUT NOT EQUAL TO
+    'lnsim': '\u22e6',  # ⋦ LESS-THAN BUT NOT EQUIVALENT TO
+    'longleftarrow': '\u27f5',  # ⟵ LONG LEFTWARDS ARROW
+    'longleftrightarrow': '\u27f7',  # ⟷ LONG LEFT RIGHT ARROW
+    'longmapsfrom': '\u27fb',  # ⟻ LONG LEFTWARDS ARROW FROM BAR
+    'longmapsto': '\u27fc',  # ⟼ LONG RIGHTWARDS ARROW FROM BAR
+    'longrightarrow': '\u27f6',  # ⟶ LONG RIGHTWARDS ARROW
+    'looparrowleft': '\u21ab',  # ↫ LEFTWARDS ARROW WITH LOOP
+    'looparrowright': '\u21ac',  # ↬ RIGHTWARDS ARROW WITH LOOP
+    'lrtimes': '\u22c8',  # ⋈ BOWTIE
+    'mapsfrom': '\u21a4',  # ↤ LEFTWARDS ARROW FROM BAR
+    'mapsto': '\u21a6',  # ↦ RIGHTWARDS ARROW FROM BAR
+    'mid': '\u2223',  # ∣ DIVIDES
+    'models': '\u22a7',  # ⊧ MODELS
+    'multimap': '\u22b8',  # ⊸ MULTIMAP
+    'multimapboth': '\u29df',  # ⧟ DOUBLE-ENDED MULTIMAP
+    'multimapdotbothA': '\u22b6',  # ⊶ ORIGINAL OF
+    'multimapdotbothB': '\u22b7',  # ⊷ IMAGE OF
+    'multimapinv': '\u27dc',  # ⟜ LEFT MULTIMAP
+    'nLeftarrow': '\u21cd',  # ⇍ LEFTWARDS DOUBLE ARROW WITH STROKE
+    'nLeftrightarrow': '\u21ce',  # ⇎ LEFT RIGHT DOUBLE ARROW WITH STROKE
+    'nRightarrow': '\u21cf',  # ⇏ RIGHTWARDS DOUBLE ARROW WITH STROKE
+    'nVDash': '\u22af',  # ⊯ NEGATED DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE
+    'nVdash': '\u22ae',  # ⊮ DOES NOT FORCE
+    'ncong': '\u2247',  # ≇ NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO
+    'ne': '\u2260',  # ≠ NOT EQUAL TO
+    'nearrow': '\u2197',  # ↗ NORTH EAST ARROW
+    'neq': '\u2260',  # ≠ NOT EQUAL TO
+    'ngeq': '\u2271',  # ≱ NEITHER GREATER-THAN NOR EQUAL TO
+    'ngtr': '\u226f',  # ≯ NOT GREATER-THAN
+    'ngtrless': '\u2279',  # ≹ NEITHER GREATER-THAN NOR LESS-THAN
+    'ni': '\u220b',  # ∋ CONTAINS AS MEMBER
+    'nleftarrow': '\u219a',  # ↚ LEFTWARDS ARROW WITH STROKE
+    'nleftrightarrow': '\u21ae',  # ↮ LEFT RIGHT ARROW WITH STROKE
+    'nleq': '\u2270',  # ≰ NEITHER LESS-THAN NOR EQUAL TO
+    'nless': '\u226e',  # ≮ NOT LESS-THAN
+    'nlessgtr': '\u2278',  # ≸ NEITHER LESS-THAN NOR GREATER-THAN
+    'nmid': '\u2224',  # ∤ DOES NOT DIVIDE
+    'notasymp': '\u226d',  # ≭ NOT EQUIVALENT TO
+    'notin': '\u2209',  # ∉ NOT AN ELEMENT OF
+    'notni': '\u220c',  # ∌ DOES NOT CONTAIN AS MEMBER
+    'notowner': '\u220c',  # ∌ DOES NOT CONTAIN AS MEMBER
+    'notslash': '\u233f',  # ⌿ APL FUNCTIONAL SYMBOL SLASH BAR
+    'nparallel': '\u2226',  # ∦ NOT PARALLEL TO
+    'nprec': '\u2280',  # ⊀ DOES NOT PRECEDE
+    'npreceq': '\u22e0',  # ⋠ DOES NOT PRECEDE OR EQUAL
+    'nrightarrow': '\u219b',  # ↛ RIGHTWARDS ARROW WITH STROKE
+    'nsim': '\u2241',  # ≁ NOT TILDE
+    'nsimeq': '\u2244',  # ≄ NOT ASYMPTOTICALLY EQUAL TO
+    'nsubseteq': '\u2288',  # ⊈ NEITHER A SUBSET OF NOR EQUAL TO
+    'nsucc': '\u2281',  # ⊁ DOES NOT SUCCEED
+    'nsucceq': '\u22e1',  # ⋡ DOES NOT SUCCEED OR EQUAL
+    'nsupseteq': '\u2289',  # ⊉ NEITHER A SUPERSET OF NOR EQUAL TO
+    'ntriangleleft': '\u22ea',  # ⋪ NOT NORMAL SUBGROUP OF
+    'ntrianglelefteq': '\u22ec',  # ⋬ NOT NORMAL SUBGROUP OF OR EQUAL TO
+    'ntriangleright': '\u22eb',  # ⋫ DOES NOT CONTAIN AS NORMAL SUBGROUP
+    'ntrianglerighteq': '\u22ed',  # ⋭ DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL
+    'nvDash': '\u22ad',  # ⊭ NOT TRUE
+    'nvdash': '\u22ac',  # ⊬ DOES NOT PROVE
+    'nwarrow': '\u2196',  # ↖ NORTH WEST ARROW
+    'owns': '\u220b',  # ∋ CONTAINS AS MEMBER
+    'parallel': '\u2225',  # ∥ PARALLEL TO
+    'perp': '\u27c2',  # ⟂ PERPENDICULAR
+    'pitchfork': '\u22d4',  # ⋔ PITCHFORK
+    'prec': '\u227a',  # ≺ PRECEDES
+    'precapprox': '\u2ab7',  # ⪷ PRECEDES ABOVE ALMOST EQUAL TO
+    'preccurlyeq': '\u227c',  # ≼ PRECEDES OR EQUAL TO
+    'preceq': '\u2aaf',  # ⪯ PRECEDES ABOVE SINGLE-LINE EQUALS SIGN
+    'preceqq': '\u2ab3',  # ⪳ PRECEDES ABOVE EQUALS SIGN
+    'precnapprox': '\u2ab9',  # ⪹ PRECEDES ABOVE NOT ALMOST EQUAL TO
+    'precneqq': '\u2ab5',  # ⪵ PRECEDES ABOVE NOT EQUAL TO
+    'precnsim': '\u22e8',  # ⋨ PRECEDES BUT NOT EQUIVALENT TO
+    'precsim': '\u227e',  # ≾ PRECEDES OR EQUIVALENT TO
+    'propto': '\u221d',  # ∝ PROPORTIONAL TO
+    'restriction': '\u21be',  # ↾ UPWARDS HARPOON WITH BARB RIGHTWARDS
+    'rhd': '\u22b3',  # ⊳ CONTAINS AS NORMAL SUBGROUP
+    'rightarrow': '\u2192',  # → RIGHTWARDS ARROW
+    'rightarrowtail': '\u21a3',  # ↣ RIGHTWARDS ARROW WITH TAIL
+    'rightarrowtriangle': '\u21fe',  # ⇾ RIGHTWARDS OPEN-HEADED ARROW
+    'rightbarharpoon': '\u296c',  # ⥬ RIGHTWARDS HARPOON WITH BARB UP ABOVE LONG DASH
+    'rightharpoondown': '\u21c1',  # ⇁ RIGHTWARDS HARPOON WITH BARB DOWNWARDS
+    'rightharpoonup': '\u21c0',  # ⇀ RIGHTWARDS HARPOON WITH BARB UPWARDS
+    'rightleftarrows': '\u21c4',  # ⇄ RIGHTWARDS ARROW OVER LEFTWARDS ARROW
+    'rightleftharpoon': '\u294b',  # ⥋ LEFT BARB DOWN RIGHT BARB UP HARPOON
+    'rightleftharpoons': '\u21cc',  # ⇌ RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON
+    'rightrightarrows': '\u21c9',  # ⇉ RIGHTWARDS PAIRED ARROWS
+    'rightrightharpoons': '\u2964',  # ⥤ RIGHTWARDS HARPOON WITH BARB UP ABOVE RIGHTWARDS HARPOON WITH BARB DOWN
+    'rightslice': '\u2aa7',  # ⪧ GREATER-THAN CLOSED BY CURVE
+    'rightsquigarrow': '\u21dd',  # ⇝ RIGHTWARDS SQUIGGLE ARROW
+    'rightturn': '\u21bb',  # ↻ CLOCKWISE OPEN CIRCLE ARROW
+    'risingdotseq': '\u2253',  # ≓ IMAGE OF OR APPROXIMATELY EQUAL TO
+    'searrow': '\u2198',  # ↘ SOUTH EAST ARROW
+    'sim': '\u223c',  # ∼ TILDE OPERATOR
+    'simeq': '\u2243',  # ≃ ASYMPTOTICALLY EQUAL TO
+    'smile': '\u2323',  # ⌣ SMILE
+    'sqsubset': '\u228f',  # ⊏ SQUARE IMAGE OF
+    'sqsubseteq': '\u2291',  # ⊑ SQUARE IMAGE OF OR EQUAL TO
+    'sqsupset': '\u2290',  # ⊐ SQUARE ORIGINAL OF
+    'sqsupseteq': '\u2292',  # ⊒ SQUARE ORIGINAL OF OR EQUAL TO
+    'strictfi': '\u297c',  # ⥼ LEFT FISH TAIL
+    'strictif': '\u297d',  # ⥽ RIGHT FISH TAIL
+    'subset': '\u2282',  # ⊂ SUBSET OF
+    'subseteq': '\u2286',  # ⊆ SUBSET OF OR EQUAL TO
+    'subseteqq': '\u2ac5',  # ⫅ SUBSET OF ABOVE EQUALS SIGN
+    'subsetneq': '\u228a',  # ⊊ SUBSET OF WITH NOT EQUAL TO
+    'subsetneqq': '\u2acb',  # ⫋ SUBSET OF ABOVE NOT EQUAL TO
+    'succ': '\u227b',  # ≻ SUCCEEDS
+    'succapprox': '\u2ab8',  # ⪸ SUCCEEDS ABOVE ALMOST EQUAL TO
+    'succcurlyeq': '\u227d',  # ≽ SUCCEEDS OR EQUAL TO
+    'succeq': '\u2ab0',  # ⪰ SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN
+    'succeqq': '\u2ab4',  # ⪴ SUCCEEDS ABOVE EQUALS SIGN
+    'succnapprox': '\u2aba',  # ⪺ SUCCEEDS ABOVE NOT ALMOST EQUAL TO
+    'succneqq': '\u2ab6',  # ⪶ SUCCEEDS ABOVE NOT EQUAL TO
+    'succnsim': '\u22e9',  # ⋩ SUCCEEDS BUT NOT EQUIVALENT TO
+    'succsim': '\u227f',  # ≿ SUCCEEDS OR EQUIVALENT TO
+    'supset': '\u2283',  # ⊃ SUPERSET OF
+    'supseteq': '\u2287',  # ⊇ SUPERSET OF OR EQUAL TO
+    'supseteqq': '\u2ac6',  # ⫆ SUPERSET OF ABOVE EQUALS SIGN
+    'supsetneq': '\u228b',  # ⊋ SUPERSET OF WITH NOT EQUAL TO
+    'supsetneqq': '\u2acc',  # ⫌ SUPERSET OF ABOVE NOT EQUAL TO
+    'swarrow': '\u2199',  # ↙ SOUTH WEST ARROW
+    'therefore': '\u2234',  # ∴ THEREFORE
+    'to': '\u2192',  # → RIGHTWARDS ARROW
+    'trianglelefteq': '\u22b4',  # ⊴ NORMAL SUBGROUP OF OR EQUAL TO
+    'triangleq': '\u225c',  # ≜ DELTA EQUAL TO
+    'trianglerighteq': '\u22b5',  # ⊵ CONTAINS AS NORMAL SUBGROUP OR EQUAL TO
+    'twoheadleftarrow': '\u219e',  # ↞ LEFTWARDS TWO HEADED ARROW
+    'twoheadrightarrow': '\u21a0',  # ↠ RIGHTWARDS TWO HEADED ARROW
+    'uparrow': '\u2191',  # ↑ UPWARDS ARROW
+    'updownarrow': '\u2195',  # ↕ UP DOWN ARROW
+    'updownarrows': '\u21c5',  # ⇅ UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW
+    'updownharpoons': '\u296e',  # ⥮ UPWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT
+    'upharpoonleft': '\u21bf',  # ↿ UPWARDS HARPOON WITH BARB LEFTWARDS
+    'upharpoonright': '\u21be',  # ↾ UPWARDS HARPOON WITH BARB RIGHTWARDS
+    'upuparrows': '\u21c8',  # ⇈ UPWARDS PAIRED ARROWS
+    'upupharpoons': '\u2963',  # ⥣ UPWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT
+    'vDash': '\u22a8',  # ⊨ TRUE
+    'vartriangle': '\u25b5',  # ▵ WHITE UP-POINTING SMALL TRIANGLE
+    'vartriangleleft': '\u22b2',  # ⊲ NORMAL SUBGROUP OF
+    'vartriangleright': '\u22b3',  # ⊳ CONTAINS AS NORMAL SUBGROUP
+    'vdash': '\u22a2',  # ⊢ RIGHT TACK
+    'wasytherefore': '\u2234',  # ∴ THEREFORE
+    }
+
+mathunder = {
+    'underbrace': '\u23df',  # ⏟ BOTTOM CURLY BRACKET
+    }
+
+space = {
+    ' ': ' ',  #   SPACE
+    ',': '\u2006',  #   SIX-PER-EM SPACE
+    ':': '\u205f',  #   MEDIUM MATHEMATICAL SPACE
+    'medspace': '\u205f',  #   MEDIUM MATHEMATICAL SPACE
+    'quad': '\u2001',  #   EM QUAD
+    'thinspace': '\u2006',  #   SIX-PER-EM SPACE
+    }
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/unichar2tex.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/unichar2tex.py
new file mode 100644
index 00000000..da1f828a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/unichar2tex.py
@@ -0,0 +1,808 @@
+# LaTeX math to Unicode symbols translation table
+# for use with the translate() method of unicode objects.
+# Generated with ``write_unichar2tex.py`` from the data in
+# http://milde.users.sourceforge.net/LUCR/Math/
+
+# Includes commands from: standard LaTeX, amssymb, amsmath
+
+uni2tex_table = {
+0xa0: '~',
+0xa3: '\\pounds ',
+0xa5: '\\yen ',
+0xa7: '\\S ',
+0xac: '\\neg ',
+0xb1: '\\pm ',
+0xb6: '\\P ',
+0xd7: '\\times ',
+0xf0: '\\eth ',
+0xf7: '\\div ',
+0x131: '\\imath ',
+0x237: '\\jmath ',
+0x393: '\\Gamma ',
+0x394: '\\Delta ',
+0x398: '\\Theta ',
+0x39b: '\\Lambda ',
+0x39e: '\\Xi ',
+0x3a0: '\\Pi ',
+0x3a3: '\\Sigma ',
+0x3a5: '\\Upsilon ',
+0x3a6: '\\Phi ',
+0x3a8: '\\Psi ',
+0x3a9: '\\Omega ',
+0x3b1: '\\alpha ',
+0x3b2: '\\beta ',
+0x3b3: '\\gamma ',
+0x3b4: '\\delta ',
+0x3b5: '\\varepsilon ',
+0x3b6: '\\zeta ',
+0x3b7: '\\eta ',
+0x3b8: '\\theta ',
+0x3b9: '\\iota ',
+0x3ba: '\\kappa ',
+0x3bb: '\\lambda ',
+0x3bc: '\\mu ',
+0x3bd: '\\nu ',
+0x3be: '\\xi ',
+0x3c0: '\\pi ',
+0x3c1: '\\rho ',
+0x3c2: '\\varsigma ',
+0x3c3: '\\sigma ',
+0x3c4: '\\tau ',
+0x3c5: '\\upsilon ',
+0x3c6: '\\varphi ',
+0x3c7: '\\chi ',
+0x3c8: '\\psi ',
+0x3c9: '\\omega ',
+0x3d1: '\\vartheta ',
+0x3d5: '\\phi ',
+0x3d6: '\\varpi ',
+0x3dd: '\\digamma ',
+0x3f0: '\\varkappa ',
+0x3f1: '\\varrho ',
+0x3f5: '\\epsilon ',
+0x3f6: '\\backepsilon ',
+0x2001: '\\quad ',
+0x2003: '\\quad ',
+0x2006: '\\, ',
+0x2016: '\\| ',
+0x2020: '\\dagger ',
+0x2021: '\\ddagger ',
+0x2022: '\\bullet ',
+0x2026: '\\ldots ',
+0x2032: '\\prime ',
+0x2035: '\\backprime ',
+0x205f: '\\: ',
+0x2102: '\\mathbb{C}',
+0x210b: '\\mathcal{H}',
+0x210c: '\\mathfrak{H}',
+0x210d: '\\mathbb{H}',
+0x210f: '\\hslash ',
+0x2110: '\\mathcal{I}',
+0x2111: '\\Im ',
+0x2112: '\\mathcal{L}',
+0x2113: '\\ell ',
+0x2115: '\\mathbb{N}',
+0x2118: '\\wp ',
+0x2119: '\\mathbb{P}',
+0x211a: '\\mathbb{Q}',
+0x211b: '\\mathcal{R}',
+0x211c: '\\Re ',
+0x211d: '\\mathbb{R}',
+0x2124: '\\mathbb{Z}',
+0x2127: '\\mho ',
+0x2128: '\\mathfrak{Z}',
+0x212c: '\\mathcal{B}',
+0x212d: '\\mathfrak{C}',
+0x2130: '\\mathcal{E}',
+0x2131: '\\mathcal{F}',
+0x2132: '\\Finv ',
+0x2133: '\\mathcal{M}',
+0x2135: '\\aleph ',
+0x2136: '\\beth ',
+0x2137: '\\gimel ',
+0x2138: '\\daleth ',
+0x2190: '\\leftarrow ',
+0x2191: '\\uparrow ',
+0x2192: '\\rightarrow ',
+0x2193: '\\downarrow ',
+0x2194: '\\leftrightarrow ',
+0x2195: '\\updownarrow ',
+0x2196: '\\nwarrow ',
+0x2197: '\\nearrow ',
+0x2198: '\\searrow ',
+0x2199: '\\swarrow ',
+0x219a: '\\nleftarrow ',
+0x219b: '\\nrightarrow ',
+0x219e: '\\twoheadleftarrow ',
+0x21a0: '\\twoheadrightarrow ',
+0x21a2: '\\leftarrowtail ',
+0x21a3: '\\rightarrowtail ',
+0x21a6: '\\mapsto ',
+0x21a9: '\\hookleftarrow ',
+0x21aa: '\\hookrightarrow ',
+0x21ab: '\\looparrowleft ',
+0x21ac: '\\looparrowright ',
+0x21ad: '\\leftrightsquigarrow ',
+0x21ae: '\\nleftrightarrow ',
+0x21b0: '\\Lsh ',
+0x21b1: '\\Rsh ',
+0x21b6: '\\curvearrowleft ',
+0x21b7: '\\curvearrowright ',
+0x21ba: '\\circlearrowleft ',
+0x21bb: '\\circlearrowright ',
+0x21bc: '\\leftharpoonup ',
+0x21bd: '\\leftharpoondown ',
+0x21be: '\\upharpoonright ',
+0x21bf: '\\upharpoonleft ',
+0x21c0: '\\rightharpoonup ',
+0x21c1: '\\rightharpoondown ',
+0x21c2: '\\downharpoonright ',
+0x21c3: '\\downharpoonleft ',
+0x21c4: '\\rightleftarrows ',
+0x21c6: '\\leftrightarrows ',
+0x21c7: '\\leftleftarrows ',
+0x21c8: '\\upuparrows ',
+0x21c9: '\\rightrightarrows ',
+0x21ca: '\\downdownarrows ',
+0x21cb: '\\leftrightharpoons ',
+0x21cc: '\\rightleftharpoons ',
+0x21cd: '\\nLeftarrow ',
+0x21ce: '\\nLeftrightarrow ',
+0x21cf: '\\nRightarrow ',
+0x21d0: '\\Leftarrow ',
+0x21d1: '\\Uparrow ',
+0x21d2: '\\Rightarrow ',
+0x21d3: '\\Downarrow ',
+0x21d4: '\\Leftrightarrow ',
+0x21d5: '\\Updownarrow ',
+0x21da: '\\Lleftarrow ',
+0x21db: '\\Rrightarrow ',
+0x21dd: '\\rightsquigarrow ',
+0x21e0: '\\dashleftarrow ',
+0x21e2: '\\dashrightarrow ',
+0x2200: '\\forall ',
+0x2201: '\\complement ',
+0x2202: '\\partial ',
+0x2203: '\\exists ',
+0x2204: '\\nexists ',
+0x2205: '\\emptyset ',
+0x2207: '\\nabla ',
+0x2208: '\\in ',
+0x2209: '\\notin ',
+0x220b: '\\ni ',
+0x220f: '\\prod ',
+0x2210: '\\coprod ',
+0x2211: '\\sum ',
+0x2212: '-',
+0x2213: '\\mp ',
+0x2214: '\\dotplus ',
+0x2215: '\\slash ',
+0x2216: '\\smallsetminus ',
+0x2217: '\\ast ',
+0x2218: '\\circ ',
+0x2219: '\\bullet ',
+0x221a: '\\surd ',
+0x221b: '\\sqrt[3] ',
+0x221c: '\\sqrt[4] ',
+0x221d: '\\propto ',
+0x221e: '\\infty ',
+0x2220: '\\angle ',
+0x2221: '\\measuredangle ',
+0x2222: '\\sphericalangle ',
+0x2223: '\\mid ',
+0x2224: '\\nmid ',
+0x2225: '\\parallel ',
+0x2226: '\\nparallel ',
+0x2227: '\\wedge ',
+0x2228: '\\vee ',
+0x2229: '\\cap ',
+0x222a: '\\cup ',
+0x222b: '\\int ',
+0x222c: '\\iint ',
+0x222d: '\\iiint ',
+0x222e: '\\oint ',
+0x2234: '\\therefore ',
+0x2235: '\\because ',
+0x2236: ':',
+0x223c: '\\sim ',
+0x223d: '\\backsim ',
+0x2240: '\\wr ',
+0x2241: '\\nsim ',
+0x2242: '\\eqsim ',
+0x2243: '\\simeq ',
+0x2245: '\\cong ',
+0x2247: '\\ncong ',
+0x2248: '\\approx ',
+0x224a: '\\approxeq ',
+0x224d: '\\asymp ',
+0x224e: '\\Bumpeq ',
+0x224f: '\\bumpeq ',
+0x2250: '\\doteq ',
+0x2251: '\\Doteq ',
+0x2252: '\\fallingdotseq ',
+0x2253: '\\risingdotseq ',
+0x2256: '\\eqcirc ',
+0x2257: '\\circeq ',
+0x225c: '\\triangleq ',
+0x2260: '\\neq ',
+0x2261: '\\equiv ',
+0x2264: '\\leq ',
+0x2265: '\\geq ',
+0x2266: '\\leqq ',
+0x2267: '\\geqq ',
+0x2268: '\\lneqq ',
+0x2269: '\\gneqq ',
+0x226a: '\\ll ',
+0x226b: '\\gg ',
+0x226c: '\\between ',
+0x226e: '\\nless ',
+0x226f: '\\ngtr ',
+0x2270: '\\nleq ',
+0x2271: '\\ngeq ',
+0x2272: '\\lesssim ',
+0x2273: '\\gtrsim ',
+0x2276: '\\lessgtr ',
+0x2277: '\\gtrless ',
+0x227a: '\\prec ',
+0x227b: '\\succ ',
+0x227c: '\\preccurlyeq ',
+0x227d: '\\succcurlyeq ',
+0x227e: '\\precsim ',
+0x227f: '\\succsim ',
+0x2280: '\\nprec ',
+0x2281: '\\nsucc ',
+0x2282: '\\subset ',
+0x2283: '\\supset ',
+0x2286: '\\subseteq ',
+0x2287: '\\supseteq ',
+0x2288: '\\nsubseteq ',
+0x2289: '\\nsupseteq ',
+0x228a: '\\subsetneq ',
+0x228b: '\\supsetneq ',
+0x228e: '\\uplus ',
+0x228f: '\\sqsubset ',
+0x2290: '\\sqsupset ',
+0x2291: '\\sqsubseteq ',
+0x2292: '\\sqsupseteq ',
+0x2293: '\\sqcap ',
+0x2294: '\\sqcup ',
+0x2295: '\\oplus ',
+0x2296: '\\ominus ',
+0x2297: '\\otimes ',
+0x2298: '\\oslash ',
+0x2299: '\\odot ',
+0x229a: '\\circledcirc ',
+0x229b: '\\circledast ',
+0x229d: '\\circleddash ',
+0x229e: '\\boxplus ',
+0x229f: '\\boxminus ',
+0x22a0: '\\boxtimes ',
+0x22a1: '\\boxdot ',
+0x22a2: '\\vdash ',
+0x22a3: '\\dashv ',
+0x22a4: '\\top ',
+0x22a5: '\\bot ',
+0x22a7: '\\models ',
+0x22a8: '\\vDash ',
+0x22a9: '\\Vdash ',
+0x22aa: '\\Vvdash ',
+0x22ac: '\\nvdash ',
+0x22ad: '\\nvDash ',
+0x22ae: '\\nVdash ',
+0x22af: '\\nVDash ',
+0x22b2: '\\vartriangleleft ',
+0x22b3: '\\vartriangleright ',
+0x22b4: '\\trianglelefteq ',
+0x22b5: '\\trianglerighteq ',
+0x22b8: '\\multimap ',
+0x22ba: '\\intercal ',
+0x22bb: '\\veebar ',
+0x22bc: '\\barwedge ',
+0x22c0: '\\bigwedge ',
+0x22c1: '\\bigvee ',
+0x22c2: '\\bigcap ',
+0x22c3: '\\bigcup ',
+0x22c4: '\\diamond ',
+0x22c5: '\\cdot ',
+0x22c6: '\\star ',
+0x22c7: '\\divideontimes ',
+0x22c8: '\\bowtie ',
+0x22c9: '\\ltimes ',
+0x22ca: '\\rtimes ',
+0x22cb: '\\leftthreetimes ',
+0x22cc: '\\rightthreetimes ',
+0x22cd: '\\backsimeq ',
+0x22ce: '\\curlyvee ',
+0x22cf: '\\curlywedge ',
+0x22d0: '\\Subset ',
+0x22d1: '\\Supset ',
+0x22d2: '\\Cap ',
+0x22d3: '\\Cup ',
+0x22d4: '\\pitchfork ',
+0x22d6: '\\lessdot ',
+0x22d7: '\\gtrdot ',
+0x22d8: '\\lll ',
+0x22d9: '\\ggg ',
+0x22da: '\\lesseqgtr ',
+0x22db: '\\gtreqless ',
+0x22de: '\\curlyeqprec ',
+0x22df: '\\curlyeqsucc ',
+0x22e0: '\\npreceq ',
+0x22e1: '\\nsucceq ',
+0x22e6: '\\lnsim ',
+0x22e7: '\\gnsim ',
+0x22e8: '\\precnsim ',
+0x22e9: '\\succnsim ',
+0x22ea: '\\ntriangleleft ',
+0x22eb: '\\ntriangleright ',
+0x22ec: '\\ntrianglelefteq ',
+0x22ed: '\\ntrianglerighteq ',
+0x22ee: '\\vdots ',
+0x22ef: '\\cdots ',
+0x22f1: '\\ddots ',
+0x2308: '\\lceil ',
+0x2309: '\\rceil ',
+0x230a: '\\lfloor ',
+0x230b: '\\rfloor ',
+0x231c: '\\ulcorner ',
+0x231d: '\\urcorner ',
+0x231e: '\\llcorner ',
+0x231f: '\\lrcorner ',
+0x2322: '\\frown ',
+0x2323: '\\smile ',
+0x23aa: '\\bracevert ',
+0x23b0: '\\lmoustache ',
+0x23b1: '\\rmoustache ',
+0x23d0: '\\arrowvert ',
+0x23de: '\\overbrace ',
+0x23df: '\\underbrace ',
+0x24c7: '\\circledR ',
+0x24c8: '\\circledS ',
+0x25b2: '\\blacktriangle ',
+0x25b3: '\\bigtriangleup ',
+0x25b7: '\\triangleright ',
+0x25bc: '\\blacktriangledown ',
+0x25bd: '\\bigtriangledown ',
+0x25c1: '\\triangleleft ',
+0x25c7: '\\Diamond ',
+0x25ca: '\\lozenge ',
+0x25ef: '\\bigcirc ',
+0x25fb: '\\square ',
+0x25fc: '\\blacksquare ',
+0x2605: '\\bigstar ',
+0x2660: '\\spadesuit ',
+0x2661: '\\heartsuit ',
+0x2662: '\\diamondsuit ',
+0x2663: '\\clubsuit ',
+0x266d: '\\flat ',
+0x266e: '\\natural ',
+0x266f: '\\sharp ',
+0x2713: '\\checkmark ',
+0x2720: '\\maltese ',
+0x27c2: '\\perp ',
+0x27cb: '\\diagup ',
+0x27cd: '\\diagdown ',
+0x27e8: '\\langle ',
+0x27e9: '\\rangle ',
+0x27ee: '\\lgroup ',
+0x27ef: '\\rgroup ',
+0x27f5: '\\longleftarrow ',
+0x27f6: '\\longrightarrow ',
+0x27f7: '\\longleftrightarrow ',
+0x27f8: '\\Longleftarrow ',
+0x27f9: '\\Longrightarrow ',
+0x27fa: '\\Longleftrightarrow ',
+0x27fc: '\\longmapsto ',
+0x29eb: '\\blacklozenge ',
+0x29f5: '\\setminus ',
+0x2a00: '\\bigodot ',
+0x2a01: '\\bigoplus ',
+0x2a02: '\\bigotimes ',
+0x2a04: '\\biguplus ',
+0x2a06: '\\bigsqcup ',
+0x2a0c: '\\iiiint ',
+0x2a3f: '\\amalg ',
+0x2a5e: '\\doublebarwedge ',
+0x2a7d: '\\leqslant ',
+0x2a7e: '\\geqslant ',
+0x2a85: '\\lessapprox ',
+0x2a86: '\\gtrapprox ',
+0x2a87: '\\lneq ',
+0x2a88: '\\gneq ',
+0x2a89: '\\lnapprox ',
+0x2a8a: '\\gnapprox ',
+0x2a8b: '\\lesseqqgtr ',
+0x2a8c: '\\gtreqqless ',
+0x2a95: '\\eqslantless ',
+0x2a96: '\\eqslantgtr ',
+0x2aaf: '\\preceq ',
+0x2ab0: '\\succeq ',
+0x2ab5: '\\precneqq ',
+0x2ab6: '\\succneqq ',
+0x2ab7: '\\precapprox ',
+0x2ab8: '\\succapprox ',
+0x2ab9: '\\precnapprox ',
+0x2aba: '\\succnapprox ',
+0x2ac5: '\\subseteqq ',
+0x2ac6: '\\supseteqq ',
+0x2acb: '\\subsetneqq ',
+0x2acc: '\\supsetneqq ',
+0x2b1c: '\\Box ',
+0x1d400: '\\mathbf{A}',
+0x1d401: '\\mathbf{B}',
+0x1d402: '\\mathbf{C}',
+0x1d403: '\\mathbf{D}',
+0x1d404: '\\mathbf{E}',
+0x1d405: '\\mathbf{F}',
+0x1d406: '\\mathbf{G}',
+0x1d407: '\\mathbf{H}',
+0x1d408: '\\mathbf{I}',
+0x1d409: '\\mathbf{J}',
+0x1d40a: '\\mathbf{K}',
+0x1d40b: '\\mathbf{L}',
+0x1d40c: '\\mathbf{M}',
+0x1d40d: '\\mathbf{N}',
+0x1d40e: '\\mathbf{O}',
+0x1d40f: '\\mathbf{P}',
+0x1d410: '\\mathbf{Q}',
+0x1d411: '\\mathbf{R}',
+0x1d412: '\\mathbf{S}',
+0x1d413: '\\mathbf{T}',
+0x1d414: '\\mathbf{U}',
+0x1d415: '\\mathbf{V}',
+0x1d416: '\\mathbf{W}',
+0x1d417: '\\mathbf{X}',
+0x1d418: '\\mathbf{Y}',
+0x1d419: '\\mathbf{Z}',
+0x1d41a: '\\mathbf{a}',
+0x1d41b: '\\mathbf{b}',
+0x1d41c: '\\mathbf{c}',
+0x1d41d: '\\mathbf{d}',
+0x1d41e: '\\mathbf{e}',
+0x1d41f: '\\mathbf{f}',
+0x1d420: '\\mathbf{g}',
+0x1d421: '\\mathbf{h}',
+0x1d422: '\\mathbf{i}',
+0x1d423: '\\mathbf{j}',
+0x1d424: '\\mathbf{k}',
+0x1d425: '\\mathbf{l}',
+0x1d426: '\\mathbf{m}',
+0x1d427: '\\mathbf{n}',
+0x1d428: '\\mathbf{o}',
+0x1d429: '\\mathbf{p}',
+0x1d42a: '\\mathbf{q}',
+0x1d42b: '\\mathbf{r}',
+0x1d42c: '\\mathbf{s}',
+0x1d42d: '\\mathbf{t}',
+0x1d42e: '\\mathbf{u}',
+0x1d42f: '\\mathbf{v}',
+0x1d430: '\\mathbf{w}',
+0x1d431: '\\mathbf{x}',
+0x1d432: '\\mathbf{y}',
+0x1d433: '\\mathbf{z}',
+0x1d434: 'A',
+0x1d435: 'B',
+0x1d436: 'C',
+0x1d437: 'D',
+0x1d438: 'E',
+0x1d439: 'F',
+0x1d43a: 'G',
+0x1d43b: 'H',
+0x1d43c: 'I',
+0x1d43d: 'J',
+0x1d43e: 'K',
+0x1d43f: 'L',
+0x1d440: 'M',
+0x1d441: 'N',
+0x1d442: 'O',
+0x1d443: 'P',
+0x1d444: 'Q',
+0x1d445: 'R',
+0x1d446: 'S',
+0x1d447: 'T',
+0x1d448: 'U',
+0x1d449: 'V',
+0x1d44a: 'W',
+0x1d44b: 'X',
+0x1d44c: 'Y',
+0x1d44d: 'Z',
+0x1d44e: 'a',
+0x1d44f: 'b',
+0x1d450: 'c',
+0x1d451: 'd',
+0x1d452: 'e',
+0x1d453: 'f',
+0x1d454: 'g',
+0x1d456: 'i',
+0x1d457: 'j',
+0x1d458: 'k',
+0x1d459: 'l',
+0x1d45a: 'm',
+0x1d45b: 'n',
+0x1d45c: 'o',
+0x1d45d: 'p',
+0x1d45e: 'q',
+0x1d45f: 'r',
+0x1d460: 's',
+0x1d461: 't',
+0x1d462: 'u',
+0x1d463: 'v',
+0x1d464: 'w',
+0x1d465: 'x',
+0x1d466: 'y',
+0x1d467: 'z',
+0x1d49c: '\\mathcal{A}',
+0x1d49e: '\\mathcal{C}',
+0x1d49f: '\\mathcal{D}',
+0x1d4a2: '\\mathcal{G}',
+0x1d4a5: '\\mathcal{J}',
+0x1d4a6: '\\mathcal{K}',
+0x1d4a9: '\\mathcal{N}',
+0x1d4aa: '\\mathcal{O}',
+0x1d4ab: '\\mathcal{P}',
+0x1d4ac: '\\mathcal{Q}',
+0x1d4ae: '\\mathcal{S}',
+0x1d4af: '\\mathcal{T}',
+0x1d4b0: '\\mathcal{U}',
+0x1d4b1: '\\mathcal{V}',
+0x1d4b2: '\\mathcal{W}',
+0x1d4b3: '\\mathcal{X}',
+0x1d4b4: '\\mathcal{Y}',
+0x1d4b5: '\\mathcal{Z}',
+0x1d504: '\\mathfrak{A}',
+0x1d505: '\\mathfrak{B}',
+0x1d507: '\\mathfrak{D}',
+0x1d508: '\\mathfrak{E}',
+0x1d509: '\\mathfrak{F}',
+0x1d50a: '\\mathfrak{G}',
+0x1d50d: '\\mathfrak{J}',
+0x1d50e: '\\mathfrak{K}',
+0x1d50f: '\\mathfrak{L}',
+0x1d510: '\\mathfrak{M}',
+0x1d511: '\\mathfrak{N}',
+0x1d512: '\\mathfrak{O}',
+0x1d513: '\\mathfrak{P}',
+0x1d514: '\\mathfrak{Q}',
+0x1d516: '\\mathfrak{S}',
+0x1d517: '\\mathfrak{T}',
+0x1d518: '\\mathfrak{U}',
+0x1d519: '\\mathfrak{V}',
+0x1d51a: '\\mathfrak{W}',
+0x1d51b: '\\mathfrak{X}',
+0x1d51c: '\\mathfrak{Y}',
+0x1d51e: '\\mathfrak{a}',
+0x1d51f: '\\mathfrak{b}',
+0x1d520: '\\mathfrak{c}',
+0x1d521: '\\mathfrak{d}',
+0x1d522: '\\mathfrak{e}',
+0x1d523: '\\mathfrak{f}',
+0x1d524: '\\mathfrak{g}',
+0x1d525: '\\mathfrak{h}',
+0x1d526: '\\mathfrak{i}',
+0x1d527: '\\mathfrak{j}',
+0x1d528: '\\mathfrak{k}',
+0x1d529: '\\mathfrak{l}',
+0x1d52a: '\\mathfrak{m}',
+0x1d52b: '\\mathfrak{n}',
+0x1d52c: '\\mathfrak{o}',
+0x1d52d: '\\mathfrak{p}',
+0x1d52e: '\\mathfrak{q}',
+0x1d52f: '\\mathfrak{r}',
+0x1d530: '\\mathfrak{s}',
+0x1d531: '\\mathfrak{t}',
+0x1d532: '\\mathfrak{u}',
+0x1d533: '\\mathfrak{v}',
+0x1d534: '\\mathfrak{w}',
+0x1d535: '\\mathfrak{x}',
+0x1d536: '\\mathfrak{y}',
+0x1d537: '\\mathfrak{z}',
+0x1d538: '\\mathbb{A}',
+0x1d539: '\\mathbb{B}',
+0x1d53b: '\\mathbb{D}',
+0x1d53c: '\\mathbb{E}',
+0x1d53d: '\\mathbb{F}',
+0x1d53e: '\\mathbb{G}',
+0x1d540: '\\mathbb{I}',
+0x1d541: '\\mathbb{J}',
+0x1d542: '\\mathbb{K}',
+0x1d543: '\\mathbb{L}',
+0x1d544: '\\mathbb{M}',
+0x1d546: '\\mathbb{O}',
+0x1d54a: '\\mathbb{S}',
+0x1d54b: '\\mathbb{T}',
+0x1d54c: '\\mathbb{U}',
+0x1d54d: '\\mathbb{V}',
+0x1d54e: '\\mathbb{W}',
+0x1d54f: '\\mathbb{X}',
+0x1d550: '\\mathbb{Y}',
+0x1d55c: '\\Bbbk ',
+0x1d5a0: '\\mathsf{A}',
+0x1d5a1: '\\mathsf{B}',
+0x1d5a2: '\\mathsf{C}',
+0x1d5a3: '\\mathsf{D}',
+0x1d5a4: '\\mathsf{E}',
+0x1d5a5: '\\mathsf{F}',
+0x1d5a6: '\\mathsf{G}',
+0x1d5a7: '\\mathsf{H}',
+0x1d5a8: '\\mathsf{I}',
+0x1d5a9: '\\mathsf{J}',
+0x1d5aa: '\\mathsf{K}',
+0x1d5ab: '\\mathsf{L}',
+0x1d5ac: '\\mathsf{M}',
+0x1d5ad: '\\mathsf{N}',
+0x1d5ae: '\\mathsf{O}',
+0x1d5af: '\\mathsf{P}',
+0x1d5b0: '\\mathsf{Q}',
+0x1d5b1: '\\mathsf{R}',
+0x1d5b2: '\\mathsf{S}',
+0x1d5b3: '\\mathsf{T}',
+0x1d5b4: '\\mathsf{U}',
+0x1d5b5: '\\mathsf{V}',
+0x1d5b6: '\\mathsf{W}',
+0x1d5b7: '\\mathsf{X}',
+0x1d5b8: '\\mathsf{Y}',
+0x1d5b9: '\\mathsf{Z}',
+0x1d5ba: '\\mathsf{a}',
+0x1d5bb: '\\mathsf{b}',
+0x1d5bc: '\\mathsf{c}',
+0x1d5bd: '\\mathsf{d}',
+0x1d5be: '\\mathsf{e}',
+0x1d5bf: '\\mathsf{f}',
+0x1d5c0: '\\mathsf{g}',
+0x1d5c1: '\\mathsf{h}',
+0x1d5c2: '\\mathsf{i}',
+0x1d5c3: '\\mathsf{j}',
+0x1d5c4: '\\mathsf{k}',
+0x1d5c5: '\\mathsf{l}',
+0x1d5c6: '\\mathsf{m}',
+0x1d5c7: '\\mathsf{n}',
+0x1d5c8: '\\mathsf{o}',
+0x1d5c9: '\\mathsf{p}',
+0x1d5ca: '\\mathsf{q}',
+0x1d5cb: '\\mathsf{r}',
+0x1d5cc: '\\mathsf{s}',
+0x1d5cd: '\\mathsf{t}',
+0x1d5ce: '\\mathsf{u}',
+0x1d5cf: '\\mathsf{v}',
+0x1d5d0: '\\mathsf{w}',
+0x1d5d1: '\\mathsf{x}',
+0x1d5d2: '\\mathsf{y}',
+0x1d5d3: '\\mathsf{z}',
+0x1d670: '\\mathtt{A}',
+0x1d671: '\\mathtt{B}',
+0x1d672: '\\mathtt{C}',
+0x1d673: '\\mathtt{D}',
+0x1d674: '\\mathtt{E}',
+0x1d675: '\\mathtt{F}',
+0x1d676: '\\mathtt{G}',
+0x1d677: '\\mathtt{H}',
+0x1d678: '\\mathtt{I}',
+0x1d679: '\\mathtt{J}',
+0x1d67a: '\\mathtt{K}',
+0x1d67b: '\\mathtt{L}',
+0x1d67c: '\\mathtt{M}',
+0x1d67d: '\\mathtt{N}',
+0x1d67e: '\\mathtt{O}',
+0x1d67f: '\\mathtt{P}',
+0x1d680: '\\mathtt{Q}',
+0x1d681: '\\mathtt{R}',
+0x1d682: '\\mathtt{S}',
+0x1d683: '\\mathtt{T}',
+0x1d684: '\\mathtt{U}',
+0x1d685: '\\mathtt{V}',
+0x1d686: '\\mathtt{W}',
+0x1d687: '\\mathtt{X}',
+0x1d688: '\\mathtt{Y}',
+0x1d689: '\\mathtt{Z}',
+0x1d68a: '\\mathtt{a}',
+0x1d68b: '\\mathtt{b}',
+0x1d68c: '\\mathtt{c}',
+0x1d68d: '\\mathtt{d}',
+0x1d68e: '\\mathtt{e}',
+0x1d68f: '\\mathtt{f}',
+0x1d690: '\\mathtt{g}',
+0x1d691: '\\mathtt{h}',
+0x1d692: '\\mathtt{i}',
+0x1d693: '\\mathtt{j}',
+0x1d694: '\\mathtt{k}',
+0x1d695: '\\mathtt{l}',
+0x1d696: '\\mathtt{m}',
+0x1d697: '\\mathtt{n}',
+0x1d698: '\\mathtt{o}',
+0x1d699: '\\mathtt{p}',
+0x1d69a: '\\mathtt{q}',
+0x1d69b: '\\mathtt{r}',
+0x1d69c: '\\mathtt{s}',
+0x1d69d: '\\mathtt{t}',
+0x1d69e: '\\mathtt{u}',
+0x1d69f: '\\mathtt{v}',
+0x1d6a0: '\\mathtt{w}',
+0x1d6a1: '\\mathtt{x}',
+0x1d6a2: '\\mathtt{y}',
+0x1d6a3: '\\mathtt{z}',
+0x1d6a4: '\\imath ',
+0x1d6a5: '\\jmath ',
+0x1d6aa: '\\mathbf{\\Gamma}',
+0x1d6ab: '\\mathbf{\\Delta}',
+0x1d6af: '\\mathbf{\\Theta}',
+0x1d6b2: '\\mathbf{\\Lambda}',
+0x1d6b5: '\\mathbf{\\Xi}',
+0x1d6b7: '\\mathbf{\\Pi}',
+0x1d6ba: '\\mathbf{\\Sigma}',
+0x1d6bc: '\\mathbf{\\Upsilon}',
+0x1d6bd: '\\mathbf{\\Phi}',
+0x1d6bf: '\\mathbf{\\Psi}',
+0x1d6c0: '\\mathbf{\\Omega}',
+0x1d6e4: '\\mathit{\\Gamma}',
+0x1d6e5: '\\mathit{\\Delta}',
+0x1d6e9: '\\mathit{\\Theta}',
+0x1d6ec: '\\mathit{\\Lambda}',
+0x1d6ef: '\\mathit{\\Xi}',
+0x1d6f1: '\\mathit{\\Pi}',
+0x1d6f4: '\\mathit{\\Sigma}',
+0x1d6f6: '\\mathit{\\Upsilon}',
+0x1d6f7: '\\mathit{\\Phi}',
+0x1d6f9: '\\mathit{\\Psi}',
+0x1d6fa: '\\mathit{\\Omega}',
+0x1d6fc: '\\alpha ',
+0x1d6fd: '\\beta ',
+0x1d6fe: '\\gamma ',
+0x1d6ff: '\\delta ',
+0x1d700: '\\varepsilon ',
+0x1d701: '\\zeta ',
+0x1d702: '\\eta ',
+0x1d703: '\\theta ',
+0x1d704: '\\iota ',
+0x1d705: '\\kappa ',
+0x1d706: '\\lambda ',
+0x1d707: '\\mu ',
+0x1d708: '\\nu ',
+0x1d709: '\\xi ',
+0x1d70b: '\\pi ',
+0x1d70c: '\\rho ',
+0x1d70d: '\\varsigma ',
+0x1d70e: '\\sigma ',
+0x1d70f: '\\tau ',
+0x1d710: '\\upsilon ',
+0x1d711: '\\varphi ',
+0x1d712: '\\chi ',
+0x1d713: '\\psi ',
+0x1d714: '\\omega ',
+0x1d715: '\\partial ',
+0x1d716: '\\epsilon ',
+0x1d717: '\\vartheta ',
+0x1d718: '\\varkappa ',
+0x1d719: '\\phi ',
+0x1d71a: '\\varrho ',
+0x1d71b: '\\varpi ',
+0x1d7ce: '\\mathbf{0}',
+0x1d7cf: '\\mathbf{1}',
+0x1d7d0: '\\mathbf{2}',
+0x1d7d1: '\\mathbf{3}',
+0x1d7d2: '\\mathbf{4}',
+0x1d7d3: '\\mathbf{5}',
+0x1d7d4: '\\mathbf{6}',
+0x1d7d5: '\\mathbf{7}',
+0x1d7d6: '\\mathbf{8}',
+0x1d7d7: '\\mathbf{9}',
+0x1d7e2: '\\mathsf{0}',
+0x1d7e3: '\\mathsf{1}',
+0x1d7e4: '\\mathsf{2}',
+0x1d7e5: '\\mathsf{3}',
+0x1d7e6: '\\mathsf{4}',
+0x1d7e7: '\\mathsf{5}',
+0x1d7e8: '\\mathsf{6}',
+0x1d7e9: '\\mathsf{7}',
+0x1d7ea: '\\mathsf{8}',
+0x1d7eb: '\\mathsf{9}',
+0x1d7f6: '\\mathtt{0}',
+0x1d7f7: '\\mathtt{1}',
+0x1d7f8: '\\mathtt{2}',
+0x1d7f9: '\\mathtt{3}',
+0x1d7fa: '\\mathtt{4}',
+0x1d7fb: '\\mathtt{5}',
+0x1d7fc: '\\mathtt{6}',
+0x1d7fd: '\\mathtt{7}',
+0x1d7fe: '\\mathtt{8}',
+0x1d7ff: '\\mathtt{9}',
+}