aboutsummaryrefslogtreecommitdiff
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}',
+}