aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/docutils/utils
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/docutils/utils
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/docutils/utils')
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/__init__.py861
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/code_analyzer.py136
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/error_reporting.py222
-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
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/punctuation_chars.py123
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/roman.py154
-rwxr-xr-x.venv/lib/python3.12/site-packages/docutils/utils/smartquotes.py1004
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/urischemes.py138
15 files changed, 10297 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/__init__.py b/.venv/lib/python3.12/site-packages/docutils/utils/__init__.py
new file mode 100644
index 00000000..777f8013
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/__init__.py
@@ -0,0 +1,861 @@
+# $Id: __init__.py 9544 2024-02-17 10:37:45Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Miscellaneous utilities for the documentation utilities.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import sys
+import os
+import os.path
+from pathlib import PurePath, Path
+import re
+import itertools
+import warnings
+import unicodedata
+
+from docutils import ApplicationError, DataError, __version_info__
+from docutils import io, nodes
+# for backwards compatibility
+from docutils.nodes import unescape # noqa: F401
+
+
+class SystemMessage(ApplicationError):
+
+ def __init__(self, system_message, level):
+ Exception.__init__(self, system_message.astext())
+ self.level = level
+
+
+class SystemMessagePropagation(ApplicationError):
+ pass
+
+
+class Reporter:
+
+ """
+ Info/warning/error reporter and ``system_message`` element generator.
+
+ Five levels of system messages are defined, along with corresponding
+ methods: `debug()`, `info()`, `warning()`, `error()`, and `severe()`.
+
+ There is typically one Reporter object per process. A Reporter object is
+ instantiated with thresholds for reporting (generating warnings) and
+ halting processing (raising exceptions), a switch to turn debug output on
+ or off, and an I/O stream for warnings. These are stored as instance
+ attributes.
+
+ When a system message is generated, its level is compared to the stored
+ thresholds, and a warning or error is generated as appropriate. Debug
+ messages are produced if the stored debug switch is on, independently of
+ other thresholds. Message output is sent to the stored warning stream if
+ not set to ''.
+
+ The Reporter class also employs a modified form of the "Observer" pattern
+ [GoF95]_ to track system messages generated. The `attach_observer` method
+ should be called before parsing, with a bound method or function which
+ accepts system messages. The observer can be removed with
+ `detach_observer`, and another added in its place.
+
+ .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of
+ Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA,
+ 1995.
+ """
+
+ levels = 'DEBUG INFO WARNING ERROR SEVERE'.split()
+ """List of names for system message levels, indexed by level."""
+
+ # system message level constants:
+ (DEBUG_LEVEL,
+ INFO_LEVEL,
+ WARNING_LEVEL,
+ ERROR_LEVEL,
+ SEVERE_LEVEL) = range(5)
+
+ def __init__(self, source, report_level, halt_level, stream=None,
+ debug=False, encoding=None, error_handler='backslashreplace'):
+ """
+ :Parameters:
+ - `source`: The path to or description of the source data.
+ - `report_level`: The level at or above which warning output will
+ be sent to `stream`.
+ - `halt_level`: The level at or above which `SystemMessage`
+ exceptions will be raised, halting execution.
+ - `debug`: Show debug (level=0) system messages?
+ - `stream`: Where warning output is sent. Can be file-like (has a
+ ``.write`` method), a string (file name, opened for writing),
+ '' (empty string) or `False` (for discarding all stream messages)
+ or `None` (implies `sys.stderr`; default).
+ - `encoding`: The output encoding.
+ - `error_handler`: The error handler for stderr output encoding.
+ """
+
+ self.source = source
+ """The path to or description of the source data."""
+
+ self.error_handler = error_handler
+ """The character encoding error handler."""
+
+ self.debug_flag = debug
+ """Show debug (level=0) system messages?"""
+
+ self.report_level = report_level
+ """The level at or above which warning output will be sent
+ to `self.stream`."""
+
+ self.halt_level = halt_level
+ """The level at or above which `SystemMessage` exceptions
+ will be raised, halting execution."""
+
+ if not isinstance(stream, io.ErrorOutput):
+ stream = io.ErrorOutput(stream, encoding, error_handler)
+
+ self.stream = stream
+ """Where warning output is sent."""
+
+ self.encoding = encoding or getattr(stream, 'encoding', 'ascii')
+ """The output character encoding."""
+
+ self.observers = []
+ """List of bound methods or functions to call with each system_message
+ created."""
+
+ self.max_level = -1
+ """The highest level system message generated so far."""
+
+ def set_conditions(self, category, report_level, halt_level,
+ stream=None, debug=False):
+ warnings.warn('docutils.utils.Reporter.set_conditions() deprecated; '
+ 'Will be removed in Docutils 0.21 or later. '
+ 'Set attributes via configuration settings or directly.',
+ DeprecationWarning, stacklevel=2)
+ self.report_level = report_level
+ self.halt_level = halt_level
+ if not isinstance(stream, io.ErrorOutput):
+ stream = io.ErrorOutput(stream, self.encoding, self.error_handler)
+ self.stream = stream
+ self.debug_flag = debug
+
+ def attach_observer(self, observer):
+ """
+ The `observer` parameter is a function or bound method which takes one
+ argument, a `nodes.system_message` instance.
+ """
+ self.observers.append(observer)
+
+ def detach_observer(self, observer):
+ self.observers.remove(observer)
+
+ def notify_observers(self, message):
+ for observer in self.observers:
+ observer(message)
+
+ def system_message(self, level, message, *children, **kwargs):
+ """
+ Return a system_message object.
+
+ Raise an exception or generate a warning if appropriate.
+ """
+ # `message` can be a `str` or `Exception` instance.
+ if isinstance(message, Exception):
+ message = str(message)
+
+ attributes = kwargs.copy()
+ if 'base_node' in kwargs:
+ source, line = get_source_line(kwargs['base_node'])
+ del attributes['base_node']
+ if source is not None:
+ attributes.setdefault('source', source)
+ if line is not None:
+ attributes.setdefault('line', line)
+ # assert source is not None, "line- but no source-argument"
+ if 'source' not in attributes:
+ # 'line' is absolute line number
+ try:
+ source, line = self.get_source_and_line(attributes.get('line'))
+ except AttributeError:
+ source, line = None, None
+ if source is not None:
+ attributes['source'] = source
+ if line is not None:
+ attributes['line'] = line
+ # assert attributes['line'] is not None, (message, kwargs)
+ # assert attributes['source'] is not None, (message, kwargs)
+ attributes.setdefault('source', self.source)
+
+ msg = nodes.system_message(message, level=level,
+ type=self.levels[level],
+ *children, **attributes)
+ if self.stream and (level >= self.report_level
+ or self.debug_flag and level == self.DEBUG_LEVEL
+ or level >= self.halt_level):
+ self.stream.write(msg.astext() + '\n')
+ if level >= self.halt_level:
+ raise SystemMessage(msg, level)
+ if level > self.DEBUG_LEVEL or self.debug_flag:
+ self.notify_observers(msg)
+ self.max_level = max(level, self.max_level)
+ return msg
+
+ def debug(self, *args, **kwargs):
+ """
+ Level-0, "DEBUG": an internal reporting issue. Typically, there is no
+ effect on the processing. Level-0 system messages are handled
+ separately from the others.
+ """
+ if self.debug_flag:
+ return self.system_message(self.DEBUG_LEVEL, *args, **kwargs)
+
+ def info(self, *args, **kwargs):
+ """
+ Level-1, "INFO": a minor issue that can be ignored. Typically there is
+ no effect on processing, and level-1 system messages are not reported.
+ """
+ return self.system_message(self.INFO_LEVEL, *args, **kwargs)
+
+ def warning(self, *args, **kwargs):
+ """
+ Level-2, "WARNING": an issue that should be addressed. If ignored,
+ there may be unpredictable problems with the output.
+ """
+ return self.system_message(self.WARNING_LEVEL, *args, **kwargs)
+
+ def error(self, *args, **kwargs):
+ """
+ Level-3, "ERROR": an error that should be addressed. If ignored, the
+ output will contain errors.
+ """
+ return self.system_message(self.ERROR_LEVEL, *args, **kwargs)
+
+ def severe(self, *args, **kwargs):
+ """
+ Level-4, "SEVERE": a severe error that must be addressed. If ignored,
+ the output will contain severe errors. Typically level-4 system
+ messages are turned into exceptions which halt processing.
+ """
+ return self.system_message(self.SEVERE_LEVEL, *args, **kwargs)
+
+
+class ExtensionOptionError(DataError): pass
+class BadOptionError(ExtensionOptionError): pass
+class BadOptionDataError(ExtensionOptionError): pass
+class DuplicateOptionError(ExtensionOptionError): pass
+
+
+def extract_extension_options(field_list, options_spec):
+ """
+ Return a dictionary mapping extension option names to converted values.
+
+ :Parameters:
+ - `field_list`: A flat field list without field arguments, where each
+ field body consists of a single paragraph only.
+ - `options_spec`: Dictionary mapping known option names to a
+ conversion function such as `int` or `float`.
+
+ :Exceptions:
+ - `KeyError` for unknown option names.
+ - `ValueError` for invalid option values (raised by the conversion
+ function).
+ - `TypeError` for invalid option value types (raised by conversion
+ function).
+ - `DuplicateOptionError` for duplicate options.
+ - `BadOptionError` for invalid fields.
+ - `BadOptionDataError` for invalid option data (missing name,
+ missing data, bad quotes, etc.).
+ """
+ option_list = extract_options(field_list)
+ return assemble_option_dict(option_list, options_spec)
+
+
+def extract_options(field_list):
+ """
+ Return a list of option (name, value) pairs from field names & bodies.
+
+ :Parameter:
+ `field_list`: A flat field list, where each field name is a single
+ word and each field body consists of a single paragraph only.
+
+ :Exceptions:
+ - `BadOptionError` for invalid fields.
+ - `BadOptionDataError` for invalid option data (missing name,
+ missing data, bad quotes, etc.).
+ """
+ option_list = []
+ for field in field_list:
+ if len(field[0].astext().split()) != 1:
+ raise BadOptionError(
+ 'extension option field name may not contain multiple words')
+ name = str(field[0].astext().lower())
+ body = field[1]
+ if len(body) == 0:
+ data = None
+ elif (len(body) > 1
+ or not isinstance(body[0], nodes.paragraph)
+ or len(body[0]) != 1
+ or not isinstance(body[0][0], nodes.Text)):
+ raise BadOptionDataError(
+ 'extension option field body may contain\n'
+ 'a single paragraph only (option "%s")' % name)
+ else:
+ data = body[0][0].astext()
+ option_list.append((name, data))
+ return option_list
+
+
+def assemble_option_dict(option_list, options_spec):
+ """
+ Return a mapping of option names to values.
+
+ :Parameters:
+ - `option_list`: A list of (name, value) pairs (the output of
+ `extract_options()`).
+ - `options_spec`: Dictionary mapping known option names to a
+ conversion function such as `int` or `float`.
+
+ :Exceptions:
+ - `KeyError` for unknown option names.
+ - `DuplicateOptionError` for duplicate options.
+ - `ValueError` for invalid option values (raised by conversion
+ function).
+ - `TypeError` for invalid option value types (raised by conversion
+ function).
+ """
+ options = {}
+ for name, value in option_list:
+ convertor = options_spec[name] # raises KeyError if unknown
+ if convertor is None:
+ raise KeyError(name) # or if explicitly disabled
+ if name in options:
+ raise DuplicateOptionError('duplicate option "%s"' % name)
+ try:
+ options[name] = convertor(value)
+ except (ValueError, TypeError) as detail:
+ raise detail.__class__('(option: "%s"; value: %r)\n%s'
+ % (name, value, ' '.join(detail.args)))
+ return options
+
+
+class NameValueError(DataError): pass
+
+
+def decode_path(path):
+ """
+ Ensure `path` is Unicode. Return `str` instance.
+
+ Decode file/path string in a failsafe manner if not already done.
+ """
+ # TODO: is this still required with Python 3?
+ if isinstance(path, str):
+ return path
+ try:
+ path = path.decode(sys.getfilesystemencoding(), 'strict')
+ except AttributeError: # default value None has no decode method
+ if not path:
+ return ''
+ raise ValueError('`path` value must be a String or ``None``, '
+ f'not {path!r}')
+ except UnicodeDecodeError:
+ try:
+ path = path.decode('utf-8', 'strict')
+ except UnicodeDecodeError:
+ path = path.decode('ascii', 'replace')
+ return path
+
+
+def extract_name_value(line):
+ """
+ Return a list of (name, value) from a line of the form "name=value ...".
+
+ :Exception:
+ `NameValueError` for invalid input (missing name, missing data, bad
+ quotes, etc.).
+ """
+ attlist = []
+ while line:
+ equals = line.find('=')
+ if equals == -1:
+ raise NameValueError('missing "="')
+ attname = line[:equals].strip()
+ if equals == 0 or not attname:
+ raise NameValueError(
+ 'missing attribute name before "="')
+ line = line[equals+1:].lstrip()
+ if not line:
+ raise NameValueError(
+ 'missing value after "%s="' % attname)
+ if line[0] in '\'"':
+ endquote = line.find(line[0], 1)
+ if endquote == -1:
+ raise NameValueError(
+ 'attribute "%s" missing end quote (%s)'
+ % (attname, line[0]))
+ if len(line) > endquote + 1 and line[endquote + 1].strip():
+ raise NameValueError(
+ 'attribute "%s" end quote (%s) not followed by '
+ 'whitespace' % (attname, line[0]))
+ data = line[1:endquote]
+ line = line[endquote+1:].lstrip()
+ else:
+ space = line.find(' ')
+ if space == -1:
+ data = line
+ line = ''
+ else:
+ data = line[:space]
+ line = line[space+1:].lstrip()
+ attlist.append((attname.lower(), data))
+ return attlist
+
+
+def new_reporter(source_path, settings):
+ """
+ Return a new Reporter object.
+
+ :Parameters:
+ `source` : string
+ The path to or description of the source text of the document.
+ `settings` : optparse.Values object
+ Runtime settings.
+ """
+ reporter = Reporter(
+ source_path, settings.report_level, settings.halt_level,
+ stream=settings.warning_stream, debug=settings.debug,
+ encoding=settings.error_encoding,
+ error_handler=settings.error_encoding_error_handler)
+ return reporter
+
+
+def new_document(source_path, settings=None):
+ """
+ Return a new empty document object.
+
+ :Parameters:
+ `source_path` : string
+ The path to or description of the source text of the document.
+ `settings` : optparse.Values object
+ Runtime settings. If none are provided, a default core set will
+ be used. If you will use the document object with any Docutils
+ components, you must provide their default settings as well.
+
+ For example, if parsing rST, at least provide the rst-parser
+ settings, obtainable as follows:
+
+ Defaults for parser component::
+
+ settings = docutils.frontend.get_default_settings(
+ docutils.parsers.rst.Parser)
+
+ Defaults and configuration file customizations::
+
+ settings = docutils.core.Publisher(
+ parser=docutils.parsers.rst.Parser).get_settings()
+
+ """
+ # Import at top of module would lead to circular dependency!
+ from docutils import frontend
+ if settings is None:
+ settings = frontend.get_default_settings()
+ source_path = decode_path(source_path)
+ reporter = new_reporter(source_path, settings)
+ document = nodes.document(settings, reporter, source=source_path)
+ document.note_source(source_path, -1)
+ return document
+
+
+def clean_rcs_keywords(paragraph, keyword_substitutions):
+ if len(paragraph) == 1 and isinstance(paragraph[0], nodes.Text):
+ textnode = paragraph[0]
+ for pattern, substitution in keyword_substitutions:
+ match = pattern.search(textnode)
+ if match:
+ paragraph[0] = nodes.Text(pattern.sub(substitution, textnode))
+ return
+
+
+def relative_path(source, target):
+ """
+ Build and return a path to `target`, relative to `source` (both files).
+
+ The return value is a `str` suitable to be included in `source`
+ as a reference to `target`.
+
+ :Parameters:
+ `source` : path-like object or None
+ Path of a file in the start directory for the relative path
+ (the file does not need to exist).
+ The value ``None`` is replaced with "<cwd>/dummy_file".
+ `target` : path-like object
+ End point of the returned relative path.
+
+ Differences to `os.path.relpath()`:
+
+ * Inverse argument order.
+ * `source` is assumed to be a FILE in the start directory (add a "dummy"
+ file name to obtain the path relative from a directory)
+ while `os.path.relpath()` expects a DIRECTORY as `start` argument.
+ * Always use Posix path separator ("/") for the output.
+ * Use `os.sep` for parsing the input
+ (changing the value of `os.sep` is ignored by `os.relpath()`).
+ * If there is no common prefix, return the absolute path to `target`.
+
+ Differences to `pathlib.PurePath.relative_to(other)`:
+
+ * pathlib offers an object oriented interface.
+ * `source` expects path to a FILE while `other` expects a DIRECTORY.
+ * `target` defaults to the cwd, no default value for `other`.
+ * `relative_path()` always returns a path (relative or absolute),
+ while `PurePath.relative_to()` raises a ValueError
+ if `target` is not a subpath of `other` (no ".." inserted).
+ """
+ source_parts = os.path.abspath(source or type(target)('dummy_file')
+ ).split(os.sep)
+ target_parts = os.path.abspath(target).split(os.sep)
+ # Check first 2 parts because '/dir'.split('/') == ['', 'dir']:
+ if source_parts[:2] != target_parts[:2]:
+ # Nothing in common between paths.
+ # Return absolute path, using '/' for URLs:
+ return '/'.join(target_parts)
+ source_parts.reverse()
+ target_parts.reverse()
+ while (source_parts and target_parts
+ and source_parts[-1] == target_parts[-1]):
+ # Remove path components in common:
+ source_parts.pop()
+ target_parts.pop()
+ target_parts.reverse()
+ parts = ['..'] * (len(source_parts) - 1) + target_parts
+ return '/'.join(parts)
+
+
+def get_stylesheet_reference(settings, relative_to=None):
+ """
+ Retrieve a stylesheet reference from the settings object.
+
+ Deprecated. Use get_stylesheet_list() instead to
+ enable specification of multiple stylesheets as a comma-separated
+ list.
+ """
+ warnings.warn('utils.get_stylesheet_reference()'
+ ' is obsoleted by utils.get_stylesheet_list()'
+ ' and will be removed in Docutils 2.0.',
+ DeprecationWarning, stacklevel=2)
+ if settings.stylesheet_path:
+ assert not settings.stylesheet, (
+ 'stylesheet and stylesheet_path are mutually exclusive.')
+ if relative_to is None:
+ relative_to = settings._destination
+ return relative_path(relative_to, settings.stylesheet_path)
+ else:
+ return settings.stylesheet
+
+
+# Return 'stylesheet' or 'stylesheet_path' arguments as list.
+#
+# The original settings arguments are kept unchanged: you can test
+# with e.g. ``if settings.stylesheet_path: ...``.
+#
+# Differences to the depracated `get_stylesheet_reference()`:
+# * return value is a list
+# * no re-writing of the path (and therefore no optional argument)
+# (if required, use ``utils.relative_path(source, target)``
+# in the calling script)
+def get_stylesheet_list(settings):
+ """
+ Retrieve list of stylesheet references from the settings object.
+ """
+ assert not (settings.stylesheet and settings.stylesheet_path), (
+ 'stylesheet and stylesheet_path are mutually exclusive.')
+ stylesheets = settings.stylesheet_path or settings.stylesheet or []
+ # programmatically set default may be string with comma separated list:
+ if not isinstance(stylesheets, list):
+ stylesheets = [path.strip() for path in stylesheets.split(',')]
+ if settings.stylesheet_path:
+ # expand relative paths if found in stylesheet-dirs:
+ stylesheets = [find_file_in_dirs(path, settings.stylesheet_dirs)
+ for path in stylesheets]
+ return stylesheets
+
+
+def find_file_in_dirs(path, dirs):
+ """
+ Search for `path` in the list of directories `dirs`.
+
+ Return the first expansion that matches an existing file.
+ """
+ path = Path(path)
+ if path.is_absolute():
+ return path.as_posix()
+ for d in dirs:
+ f = Path(d).expanduser() / path
+ if f.exists():
+ return f.as_posix()
+ return path.as_posix()
+
+
+def get_trim_footnote_ref_space(settings):
+ """
+ Return whether or not to trim footnote space.
+
+ If trim_footnote_reference_space is not None, return it.
+
+ If trim_footnote_reference_space is None, return False unless the
+ footnote reference style is 'superscript'.
+ """
+ if settings.setdefault('trim_footnote_reference_space', None) is None:
+ return getattr(settings, 'footnote_references', None) == 'superscript'
+ else:
+ return settings.trim_footnote_reference_space
+
+
+def get_source_line(node):
+ """
+ Return the "source" and "line" attributes from the `node` given or from
+ its closest ancestor.
+ """
+ while node:
+ if node.source or node.line:
+ return node.source, node.line
+ node = node.parent
+ return None, None
+
+
+def escape2null(text):
+ """Return a string with escape-backslashes converted to nulls."""
+ parts = []
+ start = 0
+ while True:
+ found = text.find('\\', start)
+ if found == -1:
+ parts.append(text[start:])
+ return ''.join(parts)
+ parts.append(text[start:found])
+ parts.append('\x00' + text[found+1:found+2])
+ start = found + 2 # skip character after escape
+
+
+def split_escaped_whitespace(text):
+ """
+ Split `text` on escaped whitespace (null+space or null+newline).
+ Return a list of strings.
+ """
+ strings = text.split('\x00 ')
+ strings = [string.split('\x00\n') for string in strings]
+ # flatten list of lists of strings to list of strings:
+ return list(itertools.chain(*strings))
+
+
+def strip_combining_chars(text):
+ return ''.join(c for c in text if not unicodedata.combining(c))
+
+
+def find_combining_chars(text):
+ """Return indices of all combining chars in Unicode string `text`.
+
+ >>> from docutils.utils import find_combining_chars
+ >>> find_combining_chars('A t̆ab̆lĕ')
+ [3, 6, 9]
+
+ """
+ return [i for i, c in enumerate(text) if unicodedata.combining(c)]
+
+
+def column_indices(text):
+ """Indices of Unicode string `text` when skipping combining characters.
+
+ >>> from docutils.utils import column_indices
+ >>> column_indices('A t̆ab̆lĕ')
+ [0, 1, 2, 4, 5, 7, 8]
+
+ """
+ # TODO: account for asian wide chars here instead of using dummy
+ # replacements in the tableparser?
+ string_indices = list(range(len(text)))
+ for index in find_combining_chars(text):
+ string_indices[index] = None
+ return [i for i in string_indices if i is not None]
+
+
+east_asian_widths = {'W': 2, # Wide
+ 'F': 2, # Full-width (wide)
+ 'Na': 1, # Narrow
+ 'H': 1, # Half-width (narrow)
+ 'N': 1, # Neutral (not East Asian, treated as narrow)
+ 'A': 1, # Ambiguous (s/b wide in East Asian context,
+ } # narrow otherwise, but that doesn't work)
+"""Mapping of result codes from `unicodedata.east_asian_widt()` to character
+column widths."""
+
+
+def column_width(text):
+ """Return the column width of text.
+
+ Correct ``len(text)`` for wide East Asian and combining Unicode chars.
+ """
+ width = sum(east_asian_widths[unicodedata.east_asian_width(c)]
+ for c in text)
+ # correction for combining chars:
+ width -= len(find_combining_chars(text))
+ return width
+
+
+def uniq(L):
+ r = []
+ for item in L:
+ if item not in r:
+ r.append(item)
+ return r
+
+
+def normalize_language_tag(tag):
+ """Return a list of normalized combinations for a `BCP 47` language tag.
+
+ Example:
+
+ >>> from docutils.utils import normalize_language_tag
+ >>> normalize_language_tag('de_AT-1901')
+ ['de-at-1901', 'de-at', 'de-1901', 'de']
+ >>> normalize_language_tag('de-CH-x_altquot')
+ ['de-ch-x-altquot', 'de-ch', 'de-x-altquot', 'de']
+
+ """
+ # normalize:
+ tag = tag.lower().replace('-', '_')
+ # split (except singletons, which mark the following tag as non-standard):
+ tag = re.sub(r'_([a-zA-Z0-9])_', r'_\1-', tag)
+ subtags = [subtag for subtag in tag.split('_')]
+ base_tag = (subtags.pop(0),)
+ # find all combinations of subtags
+ taglist = []
+ for n in range(len(subtags), 0, -1):
+ for tags in itertools.combinations(subtags, n):
+ taglist.append('-'.join(base_tag+tags))
+ taglist += base_tag
+ return taglist
+
+
+def xml_declaration(encoding=None):
+ """Return an XML text declaration.
+
+ Include an encoding declaration, if `encoding`
+ is not 'unicode', '', or None.
+ """
+ if encoding and encoding.lower() != 'unicode':
+ encoding_declaration = f' encoding="{encoding}"'
+ else:
+ encoding_declaration = ''
+ return f'<?xml version="1.0"{encoding_declaration}?>\n'
+
+
+class DependencyList:
+
+ """
+ List of dependencies, with file recording support.
+
+ Note that the output file is not automatically closed. You have
+ to explicitly call the close() method.
+ """
+
+ def __init__(self, output_file=None, dependencies=()):
+ """
+ Initialize the dependency list, automatically setting the
+ output file to `output_file` (see `set_output()`) and adding
+ all supplied dependencies.
+
+ If output_file is None, no file output is done when calling add().
+ """
+ self.list = []
+ self.file = None
+ if output_file:
+ self.set_output(output_file)
+ self.add(*dependencies)
+
+ def set_output(self, output_file):
+ """
+ Set the output file and clear the list of already added
+ dependencies.
+
+ `output_file` must be a string. The specified file is
+ immediately overwritten.
+
+ If output_file is '-', the output will be written to stdout.
+ """
+ if output_file:
+ if output_file == '-':
+ self.file = sys.stdout
+ else:
+ self.file = open(output_file, 'w', encoding='utf-8')
+
+ def add(self, *paths):
+ """
+ Append `path` to `self.list` unless it is already there.
+
+ Also append to `self.file` unless it is already there
+ or `self.file is `None`.
+ """
+ for path in paths:
+ if isinstance(path, PurePath):
+ path = path.as_posix() # use '/' as separator
+ if path not in self.list:
+ self.list.append(path)
+ if self.file is not None:
+ self.file.write(path+'\n')
+
+ def close(self):
+ """
+ Close the output file.
+ """
+ if self.file is not sys.stdout:
+ self.file.close()
+ self.file = None
+
+ def __repr__(self):
+ try:
+ output_file = self.file.name
+ except AttributeError:
+ output_file = None
+ return '%s(%r, %s)' % (self.__class__.__name__, output_file, self.list)
+
+
+release_level_abbreviations = {
+ 'alpha': 'a',
+ 'beta': 'b',
+ 'candidate': 'rc',
+ 'final': ''}
+
+
+def version_identifier(version_info=None):
+ """
+ Return a version identifier string built from `version_info`, a
+ `docutils.VersionInfo` namedtuple instance or compatible tuple. If
+ `version_info` is not provided, by default return a version identifier
+ string based on `docutils.__version_info__` (i.e. the current Docutils
+ version).
+ """
+ if version_info is None:
+ version_info = __version_info__
+ if version_info.micro:
+ micro = '.%s' % version_info.micro
+ else:
+ # 0 is omitted:
+ micro = ''
+ releaselevel = release_level_abbreviations[version_info.releaselevel]
+ if version_info.serial:
+ serial = version_info.serial
+ else:
+ # 0 is omitted:
+ serial = ''
+ if version_info.release:
+ dev = ''
+ else:
+ dev = '.dev'
+ version = '%s.%s%s%s%s%s' % (
+ version_info.major,
+ version_info.minor,
+ micro,
+ releaselevel,
+ serial,
+ dev)
+ return version
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/code_analyzer.py b/.venv/lib/python3.12/site-packages/docutils/utils/code_analyzer.py
new file mode 100644
index 00000000..cd020bb1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/code_analyzer.py
@@ -0,0 +1,136 @@
+# :Author: Georg Brandl; Lea Wiemann; Günter Milde
+# :Date: $Date: 2022-11-16 15:01:31 +0100 (Mi, 16. Nov 2022) $
+# :Copyright: This module has been placed in the public domain.
+
+"""Lexical analysis of formal languages (i.e. code) using Pygments."""
+
+from docutils import ApplicationError
+try:
+ import pygments
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters.html import _get_ttype_class
+ with_pygments = True
+except ImportError:
+ with_pygments = False
+
+# Filter the following token types from the list of class arguments:
+unstyled_tokens = ['token', # Token (base token type)
+ 'text', # Token.Text
+ ''] # short name for Token and Text
+# (Add, e.g., Token.Punctuation with ``unstyled_tokens += 'punctuation'``.)
+
+
+class LexerError(ApplicationError):
+ pass
+
+
+class Lexer:
+ """Parse `code` lines and yield "classified" tokens.
+
+ Arguments
+
+ code -- string of source code to parse,
+ language -- formal language the code is written in,
+ tokennames -- either 'long', 'short', or 'none' (see below).
+
+ Merge subsequent tokens of the same token-type.
+
+ Iterating over an instance yields the tokens as ``(tokentype, value)``
+ tuples. The value of `tokennames` configures the naming of the tokentype:
+
+ 'long': downcased full token type name,
+ 'short': short name defined by pygments.token.STANDARD_TYPES
+ (= class argument used in pygments html output),
+ 'none': skip lexical analysis.
+ """
+
+ def __init__(self, code, language, tokennames='short'):
+ """
+ Set up a lexical analyzer for `code` in `language`.
+ """
+ self.code = code
+ self.language = language
+ self.tokennames = tokennames
+ self.lexer = None
+ # get lexical analyzer for `language`:
+ if language in ('', 'text') or tokennames == 'none':
+ return
+ if not with_pygments:
+ raise LexerError('Cannot analyze code. '
+ 'Pygments package not found.')
+ try:
+ self.lexer = get_lexer_by_name(self.language)
+ except pygments.util.ClassNotFound:
+ raise LexerError('Cannot analyze code. '
+ 'No Pygments lexer found for "%s".' % language)
+ # self.lexer.add_filter('tokenmerge')
+ # Since version 1.2. (released Jan 01, 2010) Pygments has a
+ # TokenMergeFilter. # ``self.merge(tokens)`` in __iter__ could
+ # be replaced by ``self.lexer.add_filter('tokenmerge')`` in __init__.
+ # However, `merge` below also strips a final newline added by pygments.
+ #
+ # self.lexer.add_filter('tokenmerge')
+
+ def merge(self, tokens):
+ """Merge subsequent tokens of same token-type.
+
+ Also strip the final newline (added by pygments).
+ """
+ tokens = iter(tokens)
+ (lasttype, lastval) = next(tokens)
+ for ttype, value in tokens:
+ if ttype is lasttype:
+ lastval += value
+ else:
+ yield lasttype, lastval
+ (lasttype, lastval) = (ttype, value)
+ if lastval.endswith('\n'):
+ lastval = lastval[:-1]
+ if lastval:
+ yield lasttype, lastval
+
+ def __iter__(self):
+ """Parse self.code and yield "classified" tokens.
+ """
+ if self.lexer is None:
+ yield [], self.code
+ return
+ tokens = pygments.lex(self.code, self.lexer)
+ for tokentype, value in self.merge(tokens):
+ if self.tokennames == 'long': # long CSS class args
+ classes = str(tokentype).lower().split('.')
+ else: # short CSS class args
+ classes = [_get_ttype_class(tokentype)]
+ classes = [cls for cls in classes if cls not in unstyled_tokens]
+ yield classes, value
+
+
+class NumberLines:
+ """Insert linenumber-tokens at the start of every code line.
+
+ Arguments
+
+ tokens -- iterable of ``(classes, value)`` tuples
+ startline -- first line number
+ endline -- last line number
+
+ Iterating over an instance yields the tokens with a
+ ``(['ln'], '<the line number>')`` token added for every code line.
+ Multi-line tokens are split."""
+
+ def __init__(self, tokens, startline, endline):
+ self.tokens = tokens
+ self.startline = startline
+ # pad linenumbers, e.g. endline == 100 -> fmt_str = '%3d '
+ self.fmt_str = '%%%dd ' % len(str(endline))
+
+ def __iter__(self):
+ lineno = self.startline
+ yield ['ln'], self.fmt_str % lineno
+ for ttype, value in self.tokens:
+ lines = value.split('\n')
+ for line in lines[:-1]:
+ yield ttype, line + '\n'
+ lineno += 1
+ yield ['ln'], self.fmt_str % lineno
+ yield ttype, lines[-1]
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/error_reporting.py b/.venv/lib/python3.12/site-packages/docutils/utils/error_reporting.py
new file mode 100644
index 00000000..805c95bb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/error_reporting.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+# :Id: $Id: error_reporting.py 9078 2022-06-17 11:31:40Z milde $
+# :Copyright: © 2011 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
+
+"""
+Deprecated module to handle Exceptions across Python versions.
+
+.. warning::
+ This module is deprecated with the end of support for Python 2.7
+ and will be removed in Docutils 0.21 or later.
+
+ Replacements:
+ | SafeString -> str
+ | ErrorString -> docutils.io.error_string()
+ | ErrorOutput -> docutils.io.ErrorOutput
+
+Error reporting should be safe from encoding/decoding errors.
+However, implicit conversions of strings and exceptions like
+
+>>> u'%s world: %s' % ('H\xe4llo', Exception(u'H\xe4llo'))
+
+fail in some Python versions:
+
+* In Python <= 2.6, ``unicode(<exception instance>)`` uses
+ `__str__` and fails with non-ASCII chars in`unicode` arguments.
+ (work around http://bugs.python.org/issue2517):
+
+* In Python 2, unicode(<exception instance>) fails, with non-ASCII
+ chars in arguments. (Use case: in some locales, the errstr
+ argument of IOError contains non-ASCII chars.)
+
+* In Python 2, str(<exception instance>) fails, with non-ASCII chars
+ in `unicode` arguments.
+
+The `SafeString`, `ErrorString` and `ErrorOutput` classes handle
+common exceptions.
+"""
+
+import sys
+import warnings
+
+from docutils.io import _locale_encoding as locale_encoding # noqa
+
+warnings.warn('The `docutils.utils.error_reporting` module is deprecated '
+ 'and will be removed in Docutils 0.21 or later.\n'
+ 'Details with help("docutils.utils.error_reporting").',
+ DeprecationWarning, stacklevel=2)
+
+
+if sys.version_info >= (3, 0):
+ unicode = str # noqa
+
+
+class SafeString:
+ """
+ A wrapper providing robust conversion to `str` and `unicode`.
+ """
+
+ def __init__(self, data, encoding=None, encoding_errors='backslashreplace',
+ decoding_errors='replace'):
+ self.data = data
+ self.encoding = (encoding or getattr(data, 'encoding', None)
+ or locale_encoding or 'ascii')
+ self.encoding_errors = encoding_errors
+ self.decoding_errors = decoding_errors
+
+ def __str__(self):
+ try:
+ return str(self.data)
+ except UnicodeEncodeError:
+ if isinstance(self.data, Exception):
+ args = [str(SafeString(arg, self.encoding,
+ self.encoding_errors))
+ for arg in self.data.args]
+ return ', '.join(args)
+ if isinstance(self.data, unicode):
+ if sys.version_info > (3, 0):
+ return self.data
+ else:
+ return self.data.encode(self.encoding,
+ self.encoding_errors)
+ raise
+
+ def __unicode__(self):
+ """
+ Return unicode representation of `self.data`.
+
+ Try ``unicode(self.data)``, catch `UnicodeError` and
+
+ * if `self.data` is an Exception instance, work around
+ http://bugs.python.org/issue2517 with an emulation of
+ Exception.__unicode__,
+
+ * else decode with `self.encoding` and `self.decoding_errors`.
+ """
+ try:
+ u = unicode(self.data)
+ if isinstance(self.data, EnvironmentError):
+ u = u.replace(": u'", ": '") # normalize filename quoting
+ return u
+ except UnicodeError as error: # catch ..Encode.. and ..Decode.. errors
+ if isinstance(self.data, EnvironmentError):
+ return "[Errno %s] %s: '%s'" % (
+ self.data.errno,
+ SafeString(self.data.strerror, self.encoding,
+ self.decoding_errors),
+ SafeString(self.data.filename, self.encoding,
+ self.decoding_errors))
+ if isinstance(self.data, Exception):
+ args = [unicode(SafeString(
+ arg, self.encoding,
+ decoding_errors=self.decoding_errors))
+ for arg in self.data.args]
+ return u', '.join(args)
+ if isinstance(error, UnicodeDecodeError):
+ return unicode(self.data, self.encoding, self.decoding_errors)
+ raise
+
+
+class ErrorString(SafeString):
+ """
+ Safely report exception type and message.
+ """
+ def __str__(self):
+ return '%s: %s' % (self.data.__class__.__name__,
+ super(ErrorString, self).__str__())
+
+ def __unicode__(self):
+ return u'%s: %s' % (self.data.__class__.__name__,
+ super(ErrorString, self).__unicode__())
+
+
+class ErrorOutput:
+ """
+ Wrapper class for file-like error streams with
+ failsafe de- and encoding of `str`, `bytes`, `unicode` and
+ `Exception` instances.
+ """
+
+ def __init__(self, stream=None, encoding=None,
+ encoding_errors='backslashreplace',
+ decoding_errors='replace'):
+ """
+ :Parameters:
+ - `stream`: a file-like object,
+ a string (path to a file),
+ `None` (write to `sys.stderr`, default), or
+ evaluating to `False` (write() requests are ignored).
+ - `encoding`: `stream` text encoding. Guessed if None.
+ - `encoding_errors`: how to treat encoding errors.
+ """
+ if stream is None:
+ stream = sys.stderr
+ elif not stream:
+ stream = False
+ # if `stream` is a file name, open it
+ elif isinstance(stream, str):
+ stream = open(stream, 'w')
+ elif isinstance(stream, unicode):
+ stream = open(stream.encode(sys.getfilesystemencoding()), 'w')
+
+ self.stream = stream
+ """Where warning output is sent."""
+
+ self.encoding = (encoding or getattr(stream, 'encoding', None)
+ or locale_encoding or 'ascii')
+ """The output character encoding."""
+
+ self.encoding_errors = encoding_errors
+ """Encoding error handler."""
+
+ self.decoding_errors = decoding_errors
+ """Decoding error handler."""
+
+ def write(self, data):
+ """
+ Write `data` to self.stream. Ignore, if self.stream is False.
+
+ `data` can be a `string`, `unicode`, or `Exception` instance.
+ """
+ if self.stream is False:
+ return
+ if isinstance(data, Exception):
+ data = unicode(SafeString(data, self.encoding,
+ self.encoding_errors,
+ self.decoding_errors))
+ try:
+ self.stream.write(data)
+ except UnicodeEncodeError:
+ self.stream.write(data.encode(self.encoding, self.encoding_errors))
+ except TypeError:
+ if isinstance(data, unicode): # passed stream may expect bytes
+ self.stream.write(data.encode(self.encoding,
+ self.encoding_errors))
+ return
+ if self.stream in (sys.stderr, sys.stdout):
+ self.stream.buffer.write(data) # write bytes to raw stream
+ else:
+ self.stream.write(unicode(data, self.encoding,
+ self.decoding_errors))
+
+ def close(self):
+ """
+ Close the error-output stream.
+
+ Ignored if the stream is` sys.stderr` or `sys.stdout` or has no
+ close() method.
+ """
+ if self.stream in (sys.stdout, sys.stderr):
+ return
+ try:
+ self.stream.close()
+ except AttributeError:
+ pass
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}',
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/punctuation_chars.py b/.venv/lib/python3.12/site-packages/docutils/utils/punctuation_chars.py
new file mode 100644
index 00000000..9dd21404
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/punctuation_chars.py
@@ -0,0 +1,123 @@
+# :Id: $Id: punctuation_chars.py 9270 2022-11-24 20:28:03Z milde $
+# :Copyright: © 2011, 2017 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
+#
+# This file is generated by
+# ``docutils/tools/dev/generate_punctuation_chars.py``.
+# ::
+
+"""Docutils character category patterns.
+
+ Patterns for the implementation of the `inline markup recognition rules`_
+ in the reStructuredText parser `docutils.parsers.rst.states.py` based
+ on Unicode character categories.
+ The patterns are used inside ``[ ]`` in regular expressions.
+
+ Rule (5) requires determination of matching open/close pairs. However, the
+ pairing of open/close quotes is ambiguous due to different typographic
+ conventions in different languages. The ``quote_pairs`` function tests
+ whether two characters form an open/close pair.
+
+ The patterns are generated by
+ ``docutils/tools/dev/generate_punctuation_chars.py`` to prevent dependence
+ on the Python version and avoid the time-consuming generation with every
+ Docutils run. See there for motives and implementation details.
+
+ The category of some characters changed with the development of the
+ Unicode standard. The current lists are generated with the help of the
+ "unicodedata" module of Python 2.7.13 (based on Unicode version 5.2.0).
+
+ .. _inline markup recognition rules:
+ https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html
+ #inline-markup-recognition-rules
+"""
+
+openers = (
+ '"\'(<\\[{\u0f3a\u0f3c\u169b\u2045\u207d\u208d\u2329\u2768'
+ '\u276a\u276c\u276e\u2770\u2772\u2774\u27c5\u27e6\u27e8\u27ea'
+ '\u27ec\u27ee\u2983\u2985\u2987\u2989\u298b\u298d\u298f\u2991'
+ '\u2993\u2995\u2997\u29d8\u29da\u29fc\u2e22\u2e24\u2e26\u2e28'
+ '\u3008\u300a\u300c\u300e\u3010\u3014\u3016\u3018\u301a\u301d'
+ '\u301d\ufd3e\ufe17\ufe35\ufe37\ufe39\ufe3b\ufe3d\ufe3f\ufe41'
+ '\ufe43\ufe47\ufe59\ufe5b\ufe5d\uff08\uff3b\uff5b\uff5f\uff62'
+ '\xab\u2018\u201c\u2039\u2e02\u2e04\u2e09\u2e0c\u2e1c\u2e20'
+ '\u201a\u201e\xbb\u2019\u201d\u203a\u2e03\u2e05\u2e0a\u2e0d'
+ '\u2e1d\u2e21\u201b\u201f'
+ )
+closers = (
+ '"\')>\\]}\u0f3b\u0f3d\u169c\u2046\u207e\u208e\u232a\u2769'
+ '\u276b\u276d\u276f\u2771\u2773\u2775\u27c6\u27e7\u27e9\u27eb'
+ '\u27ed\u27ef\u2984\u2986\u2988\u298a\u298c\u298e\u2990\u2992'
+ '\u2994\u2996\u2998\u29d9\u29db\u29fd\u2e23\u2e25\u2e27\u2e29'
+ '\u3009\u300b\u300d\u300f\u3011\u3015\u3017\u3019\u301b\u301e'
+ '\u301f\ufd3f\ufe18\ufe36\ufe38\ufe3a\ufe3c\ufe3e\ufe40\ufe42'
+ '\ufe44\ufe48\ufe5a\ufe5c\ufe5e\uff09\uff3d\uff5d\uff60\uff63'
+ '\xbb\u2019\u201d\u203a\u2e03\u2e05\u2e0a\u2e0d\u2e1d\u2e21'
+ '\u201b\u201f\xab\u2018\u201c\u2039\u2e02\u2e04\u2e09\u2e0c'
+ '\u2e1c\u2e20\u201a\u201e'
+ )
+delimiters = (
+ '\\-/:\u058a\xa1\xb7\xbf\u037e\u0387\u055a-\u055f\u0589'
+ '\u05be\u05c0\u05c3\u05c6\u05f3\u05f4\u0609\u060a\u060c'
+ '\u060d\u061b\u061e\u061f\u066a-\u066d\u06d4\u0700-\u070d'
+ '\u07f7-\u07f9\u0830-\u083e\u0964\u0965\u0970\u0df4\u0e4f'
+ '\u0e5a\u0e5b\u0f04-\u0f12\u0f85\u0fd0-\u0fd4\u104a-\u104f'
+ '\u10fb\u1361-\u1368\u1400\u166d\u166e\u16eb-\u16ed\u1735'
+ '\u1736\u17d4-\u17d6\u17d8-\u17da\u1800-\u180a\u1944\u1945'
+ '\u19de\u19df\u1a1e\u1a1f\u1aa0-\u1aa6\u1aa8-\u1aad\u1b5a-'
+ '\u1b60\u1c3b-\u1c3f\u1c7e\u1c7f\u1cd3\u2010-\u2017\u2020-'
+ '\u2027\u2030-\u2038\u203b-\u203e\u2041-\u2043\u2047-'
+ '\u2051\u2053\u2055-\u205e\u2cf9-\u2cfc\u2cfe\u2cff\u2e00'
+ '\u2e01\u2e06-\u2e08\u2e0b\u2e0e-\u2e1b\u2e1e\u2e1f\u2e2a-'
+ '\u2e2e\u2e30\u2e31\u3001-\u3003\u301c\u3030\u303d\u30a0'
+ '\u30fb\ua4fe\ua4ff\ua60d-\ua60f\ua673\ua67e\ua6f2-\ua6f7'
+ '\ua874-\ua877\ua8ce\ua8cf\ua8f8-\ua8fa\ua92e\ua92f\ua95f'
+ '\ua9c1-\ua9cd\ua9de\ua9df\uaa5c-\uaa5f\uaade\uaadf\uabeb'
+ '\ufe10-\ufe16\ufe19\ufe30-\ufe32\ufe45\ufe46\ufe49-\ufe4c'
+ '\ufe50-\ufe52\ufe54-\ufe58\ufe5f-\ufe61\ufe63\ufe68\ufe6a'
+ '\ufe6b\uff01-\uff03\uff05-\uff07\uff0a\uff0c-\uff0f\uff1a'
+ '\uff1b\uff1f\uff20\uff3c\uff61\uff64\uff65'
+ '\U00010100\U00010101\U0001039f\U000103d0\U00010857'
+ '\U0001091f\U0001093f\U00010a50-\U00010a58\U00010a7f'
+ '\U00010b39-\U00010b3f\U000110bb\U000110bc\U000110be-'
+ '\U000110c1\U00012470-\U00012473'
+ )
+closing_delimiters = r'\\.,;!?'
+
+
+# Matching open/close quotes
+# --------------------------
+
+# Matching open/close pairs are at the same position in
+# `punctuation_chars.openers` and `punctuation_chars.closers`.
+# Additional matches (due to different typographic conventions
+# in different languages) are stored in `quote_pairs`.
+
+quote_pairs = {
+ # open char: matching closing characters # use case
+ '\xbb': '\xbb', # » » Swedish
+ '\u2018': '\u201a', # ‘ ‚ Albanian/Greek/Turkish
+ '\u2019': '\u2019', # ’ ’ Swedish
+ '\u201a': '\u2018\u2019', # ‚ ‘ German, ‚ ’ Polish
+ '\u201c': '\u201e', # “ „ Albanian/Greek/Turkish
+ '\u201e': '\u201c\u201d', # „ “ German, „ ” Polish
+ '\u201d': '\u201d', # ” ” Swedish
+ '\u203a': '\u203a', # › › Swedish
+ }
+"""Additional open/close quote pairs."""
+
+
+def match_chars(c1, c2):
+ """Test whether `c1` and `c2` are a matching open/close character pair."""
+ try:
+ i = openers.index(c1)
+ except ValueError: # c1 not in openers
+ return False
+ return c2 == closers[i] or c2 in quote_pairs.get(c1, '')
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/roman.py b/.venv/lib/python3.12/site-packages/docutils/utils/roman.py
new file mode 100644
index 00000000..df0c5b33
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/roman.py
@@ -0,0 +1,154 @@
+##############################################################################
+#
+# Copyright (c) 2001 Mark Pilgrim and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Convert to and from Roman numerals"""
+
+__author__ = "Mark Pilgrim (f8dy@diveintopython.org)"
+__version__ = "1.4"
+__date__ = "8 August 2001"
+__copyright__ = """Copyright (c) 2001 Mark Pilgrim
+
+This program is part of "Dive Into Python", a free Python tutorial for
+experienced programmers. Visit http://diveintopython.org/ for the
+latest version.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the Python 2.1.1 license, available at
+http://www.python.org/2.1.1/license.html
+"""
+
+import argparse
+import re
+import sys
+
+
+# Define exceptions
+class RomanError(Exception):
+ pass
+
+
+class OutOfRangeError(RomanError):
+ pass
+
+
+class NotIntegerError(RomanError):
+ pass
+
+
+class InvalidRomanNumeralError(RomanError):
+ pass
+
+
+# Define digit mapping
+romanNumeralMap = (('M', 1000),
+ ('CM', 900),
+ ('D', 500),
+ ('CD', 400),
+ ('C', 100),
+ ('XC', 90),
+ ('L', 50),
+ ('XL', 40),
+ ('X', 10),
+ ('IX', 9),
+ ('V', 5),
+ ('IV', 4),
+ ('I', 1))
+
+
+def toRoman(n):
+ """convert integer to Roman numeral"""
+ if not isinstance(n, int):
+ raise NotIntegerError("decimals can not be converted")
+ if not (-1 < n < 5000):
+ raise OutOfRangeError("number out of range (must be 0..4999)")
+
+ # special case
+ if n == 0:
+ return 'N'
+
+ result = ""
+ for numeral, integer in romanNumeralMap:
+ while n >= integer:
+ result += numeral
+ n -= integer
+ return result
+
+
+# Define pattern to detect valid Roman numerals
+romanNumeralPattern = re.compile("""
+ ^ # beginning of string
+ M{0,4} # thousands - 0 to 4 M's
+ (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
+ # or 500-800 (D, followed by 0 to 3 C's)
+ (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
+ # or 50-80 (L, followed by 0 to 3 X's)
+ (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
+ # or 5-8 (V, followed by 0 to 3 I's)
+ $ # end of string
+ """, re.VERBOSE)
+
+
+def fromRoman(s):
+ """convert Roman numeral to integer"""
+ if not s:
+ raise InvalidRomanNumeralError('Input can not be blank')
+
+ # special case
+ if s == 'N':
+ return 0
+
+ if not romanNumeralPattern.search(s):
+ raise InvalidRomanNumeralError('Invalid Roman numeral: %s' % s)
+
+ result = 0
+ index = 0
+ for numeral, integer in romanNumeralMap:
+ while s[index:index + len(numeral)] == numeral:
+ result += integer
+ index += len(numeral)
+ return result
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ prog='roman',
+ description='convert between roman and arabic numerals'
+ )
+ parser.add_argument('number', help='the value to convert')
+ parser.add_argument(
+ '-r', '--reverse',
+ action='store_true',
+ default=False,
+ help='convert roman to numeral (case insensitive) [default: False]')
+
+ args = parser.parse_args()
+ args.number = args.number
+ return args
+
+
+def main():
+ args = parse_args()
+ if args.reverse:
+ u = args.number.upper()
+ r = fromRoman(u)
+ print(r)
+ else:
+ i = int(args.number)
+ n = toRoman(i)
+ print(n)
+
+ return 0
+
+
+if __name__ == "__main__": # pragma: no cover
+ sys.exit(main()) # pragma: no cover
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/smartquotes.py b/.venv/lib/python3.12/site-packages/docutils/utils/smartquotes.py
new file mode 100755
index 00000000..b8766db2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/smartquotes.py
@@ -0,0 +1,1004 @@
+#!/usr/bin/python3
+# :Id: $Id: smartquotes.py 9481 2023-11-19 21:19:20Z milde $
+# :Copyright: © 2010-2023 Günter Milde,
+# original `SmartyPants`_: © 2003 John Gruber
+# smartypants.py: © 2004, 2007 Chad Miller
+# :Maintainer: docutils-develop@lists.sourceforge.net
+# :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
+# notices 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
+
+
+r"""
+=========================
+Smart Quotes for Docutils
+=========================
+
+Synopsis
+========
+
+"SmartyPants" is a free web publishing plug-in for Movable Type, Blosxom, and
+BBEdit that easily translates plain ASCII punctuation characters into "smart"
+typographic punctuation characters.
+
+``smartquotes.py`` is an adaption of "SmartyPants" to Docutils_.
+
+* Using Unicode instead of HTML entities for typographic punctuation
+ characters, it works for any output format that supports Unicode.
+* Supports `language specific quote characters`__.
+
+__ https://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks
+
+
+Authors
+=======
+
+`John Gruber`_ did all of the hard work of writing this software in Perl for
+`Movable Type`_ and almost all of this useful documentation. `Chad Miller`_
+ported it to Python to use with Pyblosxom_.
+Adapted to Docutils_ by Günter Milde.
+
+Additional Credits
+==================
+
+Portions of the SmartyPants original work are based on Brad Choate's nifty
+MTRegex plug-in. `Brad Choate`_ also contributed a few bits of source code to
+this plug-in. Brad Choate is a fine hacker indeed.
+
+`Jeremy Hedley`_ and `Charles Wiltgen`_ deserve mention for exemplary beta
+testing of the original SmartyPants.
+
+`Rael Dornfest`_ ported SmartyPants to Blosxom.
+
+.. _Brad Choate: http://bradchoate.com/
+.. _Jeremy Hedley: http://antipixel.com/
+.. _Charles Wiltgen: http://playbacktime.com/
+.. _Rael Dornfest: http://raelity.org/
+
+
+Copyright and License
+=====================
+
+SmartyPants_ license (3-Clause BSD license):
+
+ Copyright (c) 2003 John Gruber (http://daringfireball.net/)
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name "SmartyPants" nor the names of its contributors
+ may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ This software is provided by the copyright holders and contributors
+ "as is" and any express or implied warranties, including, but not
+ limited to, the implied warranties of merchantability and fitness for
+ a particular purpose are disclaimed. In no event shall the copyright
+ owner or contributors be liable for any direct, indirect, incidental,
+ special, exemplary, or consequential damages (including, but not
+ limited to, procurement of substitute goods or services; loss of use,
+ data, or profits; or business interruption) however caused and on any
+ theory of liability, whether in contract, strict liability, or tort
+ (including negligence or otherwise) arising in any way out of the use
+ of this software, even if advised of the possibility of such damage.
+
+smartypants.py license (2-Clause BSD license):
+
+ smartypants.py is a derivative work of SmartyPants.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ This software is provided by the copyright holders and contributors
+ "as is" and any express or implied warranties, including, but not
+ limited to, the implied warranties of merchantability and fitness for
+ a particular purpose are disclaimed. In no event shall the copyright
+ owner or contributors be liable for any direct, indirect, incidental,
+ special, exemplary, or consequential damages (including, but not
+ limited to, procurement of substitute goods or services; loss of use,
+ data, or profits; or business interruption) however caused and on any
+ theory of liability, whether in contract, strict liability, or tort
+ (including negligence or otherwise) arising in any way out of the use
+ of this software, even if advised of the possibility of such damage.
+
+.. _John Gruber: http://daringfireball.net/
+.. _Chad Miller: http://web.chad.org/
+
+.. _Pyblosxom: http://pyblosxom.bluesock.org/
+.. _SmartyPants: http://daringfireball.net/projects/smartypants/
+.. _Movable Type: http://www.movabletype.org/
+.. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+.. _Docutils: https://docutils.sourceforge.io/
+
+Description
+===========
+
+SmartyPants can perform the following transformations:
+
+- Straight quotes ( " and ' ) into "curly" quote characters
+- Backticks-style quotes (\`\`like this'') into "curly" quote characters
+- Dashes (``--`` and ``---``) into en- and em-dash entities
+- Three consecutive dots (``...`` or ``. . .``) into an ellipsis ``…``.
+
+This means you can write, edit, and save your posts using plain old
+ASCII straight quotes, plain dashes, and plain dots, but your published
+posts (and final HTML output) will appear with smart quotes, em-dashes,
+and proper ellipses.
+
+Backslash Escapes
+=================
+
+If you need to use literal straight quotes (or plain hyphens and periods),
+`smartquotes` accepts the following backslash escape sequences to force
+ASCII-punctuation. Mind, that you need two backslashes in "docstrings", as
+Python expands them, too.
+
+======== =========
+Escape Character
+======== =========
+``\\`` \\
+``\\"`` \\"
+``\\'`` \\'
+``\\.`` \\.
+``\\-`` \\-
+``\\``` \\`
+======== =========
+
+This is useful, for example, when you want to use straight quotes as
+foot and inch marks: 6\\'2\\" tall; a 17\\" iMac.
+
+
+Caveats
+=======
+
+Why You Might Not Want to Use Smart Quotes in Your Weblog
+---------------------------------------------------------
+
+For one thing, you might not care.
+
+Most normal, mentally stable individuals do not take notice of proper
+typographic punctuation. Many design and typography nerds, however, break
+out in a nasty rash when they encounter, say, a restaurant sign that uses
+a straight apostrophe to spell "Joe's".
+
+If you're the sort of person who just doesn't care, you might well want to
+continue not caring. Using straight quotes -- and sticking to the 7-bit
+ASCII character set in general -- is certainly a simpler way to live.
+
+Even if you *do* care about accurate typography, you still might want to
+think twice before educating the quote characters in your weblog. One side
+effect of publishing curly quote characters is that it makes your
+weblog a bit harder for others to quote from using copy-and-paste. What
+happens is that when someone copies text from your blog, the copied text
+contains the 8-bit curly quote characters (as well as the 8-bit characters
+for em-dashes and ellipses, if you use these options). These characters
+are not standard across different text encoding methods, which is why they
+need to be encoded as characters.
+
+People copying text from your weblog, however, may not notice that you're
+using curly quotes, and they'll go ahead and paste the unencoded 8-bit
+characters copied from their browser into an email message or their own
+weblog. When pasted as raw "smart quotes", these characters are likely to
+get mangled beyond recognition.
+
+That said, my own opinion is that any decent text editor or email client
+makes it easy to stupefy smart quote characters into their 7-bit
+equivalents, and I don't consider it my problem if you're using an
+indecent text editor or email client.
+
+
+Algorithmic Shortcomings
+------------------------
+
+One situation in which quotes will get curled the wrong way is when
+apostrophes are used at the start of leading contractions. For example::
+
+ 'Twas the night before Christmas.
+
+In the case above, SmartyPants will turn the apostrophe into an opening
+secondary quote, when in fact it should be the `RIGHT SINGLE QUOTATION MARK`
+character which is also "the preferred character to use for apostrophe"
+(Unicode). I don't think this problem can be solved in the general case --
+every word processor I've tried gets this wrong as well. In such cases, it's
+best to inset the `RIGHT SINGLE QUOTATION MARK` (’) by hand.
+
+In English, the same character is used for apostrophe and closing secondary
+quote (both plain and "smart" ones). For other locales (French, Italean,
+Swiss, ...) "smart" secondary closing quotes differ from the curly apostrophe.
+
+ .. class:: language-fr
+
+ Il dit : "C'est 'super' !"
+
+If the apostrophe is used at the end of a word, it cannot be distinguished
+from a secondary quote by the algorithm. Therefore, a text like::
+
+ .. class:: language-de-CH
+
+ "Er sagt: 'Ich fass' es nicht.'"
+
+will get a single closing guillemet instead of an apostrophe.
+
+This can be prevented by use use of the `RIGHT SINGLE QUOTATION MARK` in
+the source::
+
+ - "Er sagt: 'Ich fass' es nicht.'"
+ + "Er sagt: 'Ich fass’ es nicht.'"
+
+
+Version History
+===============
+
+1.10 2023-11-18
+ - Pre-compile regexps once, not with every call of `educateQuotes()`
+ (patch #206 by Chris Sewell). Simplify regexps.
+
+1.9 2022-03-04
+ - Code cleanup. Require Python 3.
+
+1.8.1 2017-10-25
+ - Use open quote after Unicode whitespace, ZWSP, and ZWNJ.
+ - Code cleanup.
+
+1.8: 2017-04-24
+ - Command line front-end.
+
+1.7.1: 2017-03-19
+ - Update and extend language-dependent quotes.
+ - Differentiate apostrophe from single quote.
+
+1.7: 2012-11-19
+ - Internationalization: language-dependent quotes.
+
+1.6.1: 2012-11-06
+ - Refactor code, code cleanup,
+ - `educate_tokens()` generator as interface for Docutils.
+
+1.6: 2010-08-26
+ - Adaption to Docutils:
+ - Use Unicode instead of HTML entities,
+ - Remove code special to pyblosxom.
+
+1.5_1.6: Fri, 27 Jul 2007 07:06:40 -0400
+ - Fixed bug where blocks of precious unalterable text was instead
+ interpreted. Thanks to Le Roux and Dirk van Oosterbosch.
+
+1.5_1.5: Sat, 13 Aug 2005 15:50:24 -0400
+ - Fix bogus magical quotation when there is no hint that the
+ user wants it, e.g., in "21st century". Thanks to Nathan Hamblen.
+ - Be smarter about quotes before terminating numbers in an en-dash'ed
+ range.
+
+1.5_1.4: Thu, 10 Feb 2005 20:24:36 -0500
+ - Fix a date-processing bug, as reported by jacob childress.
+ - Begin a test-suite for ensuring correct output.
+ - Removed import of "string", since I didn't really need it.
+ (This was my first every Python program. Sue me!)
+
+1.5_1.3: Wed, 15 Sep 2004 18:25:58 -0400
+ - Abort processing if the flavour is in forbidden-list. Default of
+ [ "rss" ] (Idea of Wolfgang SCHNERRING.)
+ - Remove stray virgules from en-dashes. Patch by Wolfgang SCHNERRING.
+
+1.5_1.2: Mon, 24 May 2004 08:14:54 -0400
+ - Some single quotes weren't replaced properly. Diff-tesuji played
+ by Benjamin GEIGER.
+
+1.5_1.1: Sun, 14 Mar 2004 14:38:28 -0500
+ - Support upcoming pyblosxom 0.9 plugin verification feature.
+
+1.5_1.0: Tue, 09 Mar 2004 08:08:35 -0500
+ - Initial release
+"""
+
+import re
+import sys
+
+
+options = r"""
+Options
+=======
+
+Numeric values are the easiest way to configure SmartyPants' behavior:
+
+:0: Suppress all transformations. (Do nothing.)
+
+:1: Performs default SmartyPants transformations: quotes (including
+ \`\`backticks'' -style), em-dashes, and ellipses. "``--``" (dash dash)
+ is used to signify an em-dash; there is no support for en-dashes
+
+:2: Same as smarty_pants="1", except that it uses the old-school typewriter
+ shorthand for dashes: "``--``" (dash dash) for en-dashes, "``---``"
+ (dash dash dash)
+ for em-dashes.
+
+:3: Same as smarty_pants="2", but inverts the shorthand for dashes:
+ "``--``" (dash dash) for em-dashes, and "``---``" (dash dash dash) for
+ en-dashes.
+
+:-1: Stupefy mode. Reverses the SmartyPants transformation process, turning
+ the characters produced by SmartyPants into their ASCII equivalents.
+ E.g. the LEFT DOUBLE QUOTATION MARK (“) is turned into a simple
+ double-quote (\"), "—" is turned into two dashes, etc.
+
+
+The following single-character attribute values can be combined to toggle
+individual transformations from within the smarty_pants attribute. For
+example, ``"1"`` is equivalent to ``"qBde"``.
+
+:q: Educates normal quote characters: (") and (').
+
+:b: Educates \`\`backticks'' -style double quotes.
+
+:B: Educates \`\`backticks'' -style double quotes and \`single' quotes.
+
+:d: Educates em-dashes.
+
+:D: Educates em-dashes and en-dashes, using old-school typewriter
+ shorthand: (dash dash) for en-dashes, (dash dash dash) for em-dashes.
+
+:i: Educates em-dashes and en-dashes, using inverted old-school typewriter
+ shorthand: (dash dash) for em-dashes, (dash dash dash) for en-dashes.
+
+:e: Educates ellipses.
+
+:w: Translates any instance of ``&quot;`` into a normal double-quote
+ character. This should be of no interest to most people, but
+ of particular interest to anyone who writes their posts using
+ Dreamweaver, as Dreamweaver inexplicably uses this entity to represent
+ a literal double-quote character. SmartyPants only educates normal
+ quotes, not entities (because ordinarily, entities are used for
+ the explicit purpose of representing the specific character they
+ represent). The "w" option must be used in conjunction with one (or
+ both) of the other quote options ("q" or "b"). Thus, if you wish to
+ apply all SmartyPants transformations (quotes, en- and em-dashes, and
+ ellipses) and also translate ``&quot;`` entities into regular quotes
+ so SmartyPants can educate them, you should pass the following to the
+ smarty_pants attribute:
+"""
+
+
+class smartchars:
+ """Smart quotes and dashes"""
+
+ endash = '–' # EN DASH
+ emdash = '—' # EM DASH
+ ellipsis = '…' # HORIZONTAL ELLIPSIS
+ apostrophe = '’' # RIGHT SINGLE QUOTATION MARK
+
+ # quote characters (language-specific, set in __init__())
+ # https://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks
+ # https://de.wikipedia.org/wiki/Anf%C3%BChrungszeichen#Andere_Sprachen
+ # https://fr.wikipedia.org/wiki/Guillemet
+ # https://typographisme.net/post/Les-espaces-typographiques-et-le-web
+ # https://www.btb.termiumplus.gc.ca/tpv2guides/guides/redac/index-fra.html
+ # https://en.wikipedia.org/wiki/Hebrew_punctuation#Quotation_marks
+ # [7] https://www.tustep.uni-tuebingen.de/bi/bi00/bi001t1-anfuehrung.pdf
+ # [8] https://www.korrekturavdelingen.no/anforselstegn.htm
+ # [9] Typografisk håndbok. Oslo: Spartacus. 2000. s. 67. ISBN 8243001530.
+ # [10] https://www.typografi.org/sitat/sitatart.html
+ # [11] https://mk.wikipedia.org/wiki/Правопис_и_правоговор_на_македонскиот_јазик # noqa:E501
+ # [12] https://hrvatska-tipografija.com/polunavodnici/
+ # [13] https://pl.wikipedia.org/wiki/Cudzys%C5%82%C3%B3w
+ #
+ # See also configuration option "smartquote-locales".
+ quotes = {
+ 'af': '“”‘’',
+ 'af-x-altquot': '„”‚’',
+ 'bg': '„“‚‘', # https://bg.wikipedia.org/wiki/Кавички
+ 'ca': '«»“”',
+ 'ca-x-altquot': '“”‘’',
+ 'cs': '„“‚‘',
+ 'cs-x-altquot': '»«›‹',
+ 'da': '»«›‹',
+ 'da-x-altquot': '„“‚‘',
+ # 'da-x-altquot2': '””’’',
+ 'de': '„“‚‘',
+ 'de-x-altquot': '»«›‹',
+ 'de-ch': '«»‹›',
+ 'el': '«»“”', # '«»‟”' https://hal.science/hal-02101618
+ 'en': '“”‘’',
+ 'en-uk-x-altquot': '‘’“”', # Attention: " → ‘ and ' → “ !
+ 'eo': '“”‘’',
+ 'es': '«»“”',
+ 'es-x-altquot': '“”‘’',
+ 'et': '„“‚‘', # no secondary quote listed in
+ 'et-x-altquot': '«»‹›', # the sources above (wikipedia.org)
+ 'eu': '«»‹›',
+ 'fi': '””’’',
+ 'fi-x-altquot': '»»››',
+ 'fr': ('« ', ' »', '“', '”'), # full no-break space
+ 'fr-x-altquot': ('« ', ' »', '“', '”'), # narrow no-break space
+ 'fr-ch': '«»‹›', # https://typoguide.ch/
+ 'fr-ch-x-altquot': ('« ', ' »', '‹ ', ' ›'), # narrow no-break space # noqa:E501
+ 'gl': '«»“”',
+ 'he': '”“»«', # Hebrew is RTL, test position:
+ 'he-x-altquot': '„”‚’', # low quotation marks are opening.
+ # 'he-x-altquot': '“„‘‚', # RTL: low quotation marks opening
+ 'hr': '„”‘’', # Croatian [12]
+ 'hr-x-altquot': '»«›‹',
+ 'hsb': '„“‚‘',
+ 'hsb-x-altquot': '»«›‹',
+ 'hu': '„”«»',
+ 'is': '„“‚‘',
+ 'it': '«»“”',
+ 'it-ch': '«»‹›',
+ 'it-x-altquot': '“”‘’',
+ # 'it-x-altquot2': '“„‘‚', # [7] in headlines
+ 'ja': '「」『』',
+ 'ko': '“”‘’',
+ 'lt': '„“‚‘',
+ 'lv': '„“‚‘',
+ 'mk': '„“‚‘', # Macedonian [11]
+ 'nl': '“”‘’',
+ 'nl-x-altquot': '„”‚’',
+ # 'nl-x-altquot2': '””’’',
+ 'nb': '«»’’', # Norsk bokmål (canonical form 'no')
+ 'nn': '«»’’', # Nynorsk [10]
+ 'nn-x-altquot': '«»‘’', # [8], [10]
+ # 'nn-x-altquot2': '«»«»', # [9], [10]
+ # 'nn-x-altquot3': '„“‚‘', # [10]
+ 'no': '«»’’', # Norsk bokmål [10]
+ 'no-x-altquot': '«»‘’', # [8], [10]
+ # 'no-x-altquot2': '«»«»', # [9], [10
+ # 'no-x-altquot3': '„“‚‘', # [10]
+ 'pl': '„”«»',
+ 'pl-x-altquot': '«»‚’',
+ # 'pl-x-altquot2': '„”‚’', # [13]
+ 'pt': '«»“”',
+ 'pt-br': '“”‘’',
+ 'ro': '„”«»',
+ 'ru': '«»„“',
+ 'sh': '„”‚’', # Serbo-Croatian
+ 'sh-x-altquot': '»«›‹',
+ 'sk': '„“‚‘', # Slovak
+ 'sk-x-altquot': '»«›‹',
+ 'sl': '„“‚‘', # Slovenian
+ 'sl-x-altquot': '»«›‹',
+ 'sq': '«»‹›', # Albanian
+ 'sq-x-altquot': '“„‘‚',
+ 'sr': '„”’’',
+ 'sr-x-altquot': '»«›‹',
+ 'sv': '””’’',
+ 'sv-x-altquot': '»»››',
+ 'tr': '“”‘’',
+ 'tr-x-altquot': '«»‹›',
+ # 'tr-x-altquot2': '“„‘‚', # [7] antiquated?
+ 'uk': '«»„“',
+ 'uk-x-altquot': '„“‚‘',
+ 'zh-cn': '“”‘’',
+ 'zh-tw': '「」『』',
+ }
+
+ def __init__(self, language='en'):
+ self.language = language
+ try:
+ (self.opquote, self.cpquote,
+ self.osquote, self.csquote) = self.quotes[language.lower()]
+ except KeyError:
+ self.opquote, self.cpquote, self.osquote, self.csquote = '""\'\''
+
+
+class RegularExpressions:
+ # character classes:
+ _CH_CLASSES = {'open': '[([{]', # opening braces
+ 'close': r'[^\s]', # everything except whitespace
+ 'punct': r"""[-!" #\$\%'()*+,.\/:;<=>?\@\[\\\]\^_`{|}~]""",
+ 'dash': r'[-–—]',
+ 'sep': '[\\s\u200B\u200C]', # Whitespace, ZWSP, ZWNJ
+ }
+ START_SINGLE = re.compile(r"^'(?=%s\\B)" % _CH_CLASSES['punct'])
+ START_DOUBLE = re.compile(r'^"(?=%s\\B)' % _CH_CLASSES['punct'])
+ ADJACENT_1 = re.compile('"\'(?=\\w)')
+ ADJACENT_2 = re.compile('\'"(?=\\w)')
+ OPEN_SINGLE = re.compile(r"(%(open)s|%(dash)s)'(?=%(punct)s? )"
+ % _CH_CLASSES)
+ OPEN_DOUBLE = re.compile(r'(%(open)s|%(dash)s)"(?=%(punct)s? )'
+ % _CH_CLASSES)
+ DECADE = re.compile(r"'(?=\d{2}s)")
+ APOSTROPHE = re.compile(r"(?<=(\w|\d))'(?=\w)")
+ OPENING_SECONDARY = re.compile("""
+ (# ?<= # look behind fails: requires fixed-width pattern
+ %(sep)s | # a whitespace char, or
+ %(open)s | # opening brace, or
+ %(dash)s # em/en-dash
+ )
+ ' # the quote
+ (?=\\w|%(punct)s) # word character or punctuation
+ """ % _CH_CLASSES, re.VERBOSE)
+ CLOSING_SECONDARY = re.compile(r"(?<!\s)'")
+ OPENING_PRIMARY = re.compile("""
+ (
+ %(sep)s | # a whitespace char, or
+ %(open)s | # zero width separating char, or
+ %(dash)s # em/en-dash
+ )
+ " # the quote, followed by
+ (?=\\w|%(punct)s) # a word character or punctuation
+ """ % _CH_CLASSES, re.VERBOSE)
+ CLOSING_PRIMARY = re.compile(r"""
+ (
+ (?<!\s)" | # no whitespace before
+ "(?=\s) # whitespace behind
+ )
+ """, re.VERBOSE)
+
+
+regexes = RegularExpressions()
+
+
+default_smartypants_attr = '1'
+
+
+def smartyPants(text, attr=default_smartypants_attr, language='en'):
+ """Main function for "traditional" use."""
+
+ return "".join(t for t in educate_tokens(tokenize(text), attr, language))
+
+
+def educate_tokens(text_tokens, attr=default_smartypants_attr, language='en'):
+ """Return iterator that "educates" the items of `text_tokens`."""
+ # Parse attributes:
+ # 0 : do nothing
+ # 1 : set all
+ # 2 : set all, using old school en- and em- dash shortcuts
+ # 3 : set all, using inverted old school en and em- dash shortcuts
+ #
+ # q : quotes
+ # b : backtick quotes (``double'' only)
+ # B : backtick quotes (``double'' and `single')
+ # d : dashes
+ # D : old school dashes
+ # i : inverted old school dashes
+ # e : ellipses
+ # w : convert &quot; entities to " for Dreamweaver users
+
+ convert_quot = False # translate &quot; entities into normal quotes?
+ do_dashes = False
+ do_backticks = False
+ do_quotes = False
+ do_ellipses = False
+ do_stupefy = False
+
+ # if attr == "0": # pass tokens unchanged (see below).
+ if attr == '1': # Do everything, turn all options on.
+ do_quotes = True
+ do_backticks = True
+ do_dashes = 1
+ do_ellipses = True
+ elif attr == '2':
+ # Do everything, turn all options on, use old school dash shorthand.
+ do_quotes = True
+ do_backticks = True
+ do_dashes = 2
+ do_ellipses = True
+ elif attr == '3':
+ # Do everything, use inverted old school dash shorthand.
+ do_quotes = True
+ do_backticks = True
+ do_dashes = 3
+ do_ellipses = True
+ elif attr == '-1': # Special "stupefy" mode.
+ do_stupefy = True
+ else:
+ if 'q' in attr: do_quotes = True # noqa: E701
+ if 'b' in attr: do_backticks = True # noqa: E701
+ if 'B' in attr: do_backticks = 2 # noqa: E701
+ if 'd' in attr: do_dashes = 1 # noqa: E701
+ if 'D' in attr: do_dashes = 2 # noqa: E701
+ if 'i' in attr: do_dashes = 3 # noqa: E701
+ if 'e' in attr: do_ellipses = True # noqa: E701
+ if 'w' in attr: convert_quot = True # noqa: E701
+
+ prev_token_last_char = ' '
+ # Last character of the previous text token. Used as
+ # context to curl leading quote characters correctly.
+
+ for (ttype, text) in text_tokens:
+
+ # skip HTML and/or XML tags as well as empty text tokens
+ # without updating the last character
+ if ttype == 'tag' or not text:
+ yield text
+ continue
+
+ # skip literal text (math, literal, raw, ...)
+ if ttype == 'literal':
+ prev_token_last_char = text[-1:]
+ yield text
+ continue
+
+ last_char = text[-1:] # Remember last char before processing.
+
+ text = processEscapes(text)
+
+ if convert_quot:
+ text = text.replace('&quot;', '"')
+
+ if do_dashes == 1:
+ text = educateDashes(text)
+ elif do_dashes == 2:
+ text = educateDashesOldSchool(text)
+ elif do_dashes == 3:
+ text = educateDashesOldSchoolInverted(text)
+
+ if do_ellipses:
+ text = educateEllipses(text)
+
+ # Note: backticks need to be processed before quotes.
+ if do_backticks:
+ text = educateBackticks(text, language)
+
+ if do_backticks == 2:
+ text = educateSingleBackticks(text, language)
+
+ if do_quotes:
+ # Replace plain quotes in context to prevent conversion to
+ # 2-character sequence in French.
+ context = prev_token_last_char.replace('"', ';').replace("'", ';')
+ text = educateQuotes(context+text, language)[1:]
+
+ if do_stupefy:
+ text = stupefyEntities(text, language)
+
+ # Remember last char as context for the next token
+ prev_token_last_char = last_char
+
+ text = processEscapes(text, restore=True)
+
+ yield text
+
+
+def educateQuotes(text, language='en'):
+ """
+ Parameter: - text string (unicode or bytes).
+ - language (`BCP 47` language tag.)
+ Returns: The `text`, with "educated" curly quote characters.
+
+ Example input: "Isn't this fun?"
+ Example output: “Isn’t this fun?“
+ """
+ smart = smartchars(language)
+
+ if not re.search('[-"\']', text):
+ return text
+
+ # Special case if the very first character is a quote
+ # followed by punctuation at a non-word-break. Use closing quotes.
+ # TODO: example (when does this match?)
+ text = regexes.START_SINGLE.sub(smart.csquote, text)
+ text = regexes.START_DOUBLE.sub(smart.cpquote, text)
+
+ # Special case for adjacent quotes
+ # like "'Quoted' words in a larger quote."
+ text = regexes.ADJACENT_1.sub(smart.opquote+smart.osquote, text)
+ text = regexes.ADJACENT_2.sub(smart.osquote+smart.opquote, text)
+
+ # Special case: "opening character" followed by quote,
+ # optional punctuation and space like "[", '(', or '-'.
+ text = regexes.OPEN_SINGLE.sub(r'\1%s'%smart.csquote, text)
+ text = regexes.OPEN_DOUBLE.sub(r'\1%s'%smart.cpquote, text)
+
+ # Special case for decade abbreviations (the '80s):
+ if language.startswith('en'): # TODO similar cases in other languages?
+ text = regexes.DECADE.sub(smart.apostrophe, text)
+
+ # Get most opening secondary quotes:
+ text = regexes.OPENING_SECONDARY.sub(r'\1'+smart.osquote, text)
+
+ # In many locales, secondary closing quotes are different from apostrophe:
+ if smart.csquote != smart.apostrophe:
+ text = regexes.APOSTROPHE.sub(smart.apostrophe, text)
+ # TODO: keep track of quoting level to recognize apostrophe in, e.g.,
+ # "Ich fass' es nicht."
+
+ text = regexes.CLOSING_SECONDARY.sub(smart.csquote, text)
+
+ # Any remaining secondary quotes should be opening ones:
+ text = text.replace(r"'", smart.osquote)
+
+ # Get most opening primary quotes:
+ text = regexes.OPENING_PRIMARY.sub(r'\1'+smart.opquote, text)
+
+ # primary closing quotes:
+ text = regexes.CLOSING_PRIMARY.sub(smart.cpquote, text)
+
+ # Any remaining quotes should be opening ones.
+ text = text.replace(r'"', smart.opquote)
+
+ return text
+
+
+def educateBackticks(text, language='en'):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with ``backticks'' -style double quotes
+ translated into HTML curly quote entities.
+ Example input: ``Isn't this fun?''
+ Example output: “Isn't this fun?“
+ """
+ smart = smartchars(language)
+
+ text = text.replace(r'``', smart.opquote)
+ text = text.replace(r"''", smart.cpquote)
+ return text
+
+
+def educateSingleBackticks(text, language='en'):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with `backticks' -style single quotes
+ translated into HTML curly quote entities.
+
+ Example input: `Isn't this fun?'
+ Example output: ‘Isn’t this fun?’
+ """
+ smart = smartchars(language)
+
+ text = text.replace(r'`', smart.osquote)
+ text = text.replace(r"'", smart.csquote)
+ return text
+
+
+def educateDashes(text):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each instance of "--" translated to
+ an em-dash character.
+ """
+
+ text = text.replace(r'---', smartchars.endash) # en (yes, backwards)
+ text = text.replace(r'--', smartchars.emdash) # em (yes, backwards)
+ return text
+
+
+def educateDashesOldSchool(text):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each instance of "--" translated to
+ an en-dash character, and each "---" translated to
+ an em-dash character.
+ """
+
+ text = text.replace(r'---', smartchars.emdash)
+ text = text.replace(r'--', smartchars.endash)
+ return text
+
+
+def educateDashesOldSchoolInverted(text):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each instance of "--" translated to
+ an em-dash character, and each "---" translated to
+ an en-dash character. Two reasons why: First, unlike the
+ en- and em-dash syntax supported by
+ EducateDashesOldSchool(), it's compatible with existing
+ entries written before SmartyPants 1.1, back when "--" was
+ only used for em-dashes. Second, em-dashes are more
+ common than en-dashes, and so it sort of makes sense that
+ the shortcut should be shorter to type. (Thanks to Aaron
+ Swartz for the idea.)
+ """
+ text = text.replace(r'---', smartchars.endash) # em
+ text = text.replace(r'--', smartchars.emdash) # en
+ return text
+
+
+def educateEllipses(text):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each instance of "..." translated to
+ an ellipsis character.
+
+ Example input: Huh...?
+ Example output: Huh…?
+ """
+
+ text = text.replace(r'...', smartchars.ellipsis)
+ text = text.replace(r'. . .', smartchars.ellipsis)
+ return text
+
+
+def stupefyEntities(text, language='en'):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each SmartyPants character translated to
+ its ASCII counterpart.
+
+ Example input: “Hello — world.”
+ Example output: "Hello -- world."
+ """
+ smart = smartchars(language)
+
+ text = text.replace(smart.endash, "-")
+ text = text.replace(smart.emdash, "--")
+ text = text.replace(smart.osquote, "'") # open secondary quote
+ text = text.replace(smart.csquote, "'") # close secondary quote
+ text = text.replace(smart.opquote, '"') # open primary quote
+ text = text.replace(smart.cpquote, '"') # close primary quote
+ text = text.replace(smart.ellipsis, '...')
+
+ return text
+
+
+def processEscapes(text, restore=False):
+ r"""
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with after processing the following backslash
+ escape sequences. This is useful if you want to force a "dumb"
+ quote or other character to appear.
+
+ Escape Value
+ ------ -----
+ \\ &#92;
+ \" &#34;
+ \' &#39;
+ \. &#46;
+ \- &#45;
+ \` &#96;
+ """
+ replacements = ((r'\\', r'&#92;'),
+ (r'\"', r'&#34;'),
+ (r"\'", r'&#39;'),
+ (r'\.', r'&#46;'),
+ (r'\-', r'&#45;'),
+ (r'\`', r'&#96;'))
+ if restore:
+ for (ch, rep) in replacements:
+ text = text.replace(rep, ch[1])
+ else:
+ for (ch, rep) in replacements:
+ text = text.replace(ch, rep)
+
+ return text
+
+
+def tokenize(text):
+ """
+ Parameter: String containing HTML markup.
+ Returns: An iterator that yields the tokens comprising the input
+ string. Each token is either a tag (possibly with nested,
+ tags contained therein, such as <a href="<MTFoo>">, or a
+ run of text between tags. Each yielded element is a
+ two-element tuple; the first is either 'tag' or 'text';
+ the second is the actual value.
+
+ Based on the _tokenize() subroutine from Brad Choate's MTRegex plugin.
+ """
+ tag_soup = re.compile(r'([^<]*)(<[^>]*>)')
+ token_match = tag_soup.search(text)
+ previous_end = 0
+
+ while token_match is not None:
+ if token_match.group(1):
+ yield 'text', token_match.group(1)
+ yield 'tag', token_match.group(2)
+ previous_end = token_match.end()
+ token_match = tag_soup.search(text, token_match.end())
+
+ if previous_end < len(text):
+ yield 'text', text[previous_end:]
+
+
+if __name__ == "__main__":
+
+ import itertools
+ import locale
+ try:
+ locale.setlocale(locale.LC_ALL, '') # set to user defaults
+ defaultlanguage = locale.getlocale()[0]
+ except: # noqa catchall
+ defaultlanguage = 'en'
+
+ # Normalize and drop unsupported subtags:
+ defaultlanguage = defaultlanguage.lower().replace('-', '_')
+ # split (except singletons, which mark the following tag as non-standard):
+ defaultlanguage = re.sub(r'_([a-zA-Z0-9])_', r'_\1-', defaultlanguage)
+ _subtags = [subtag for subtag in defaultlanguage.split('_')]
+ _basetag = _subtags.pop(0)
+ # find all combinations of subtags
+ for n in range(len(_subtags), 0, -1):
+ for tags in itertools.combinations(_subtags, n):
+ _tag = '-'.join((_basetag, *tags))
+ if _tag in smartchars.quotes:
+ defaultlanguage = _tag
+ break
+ else:
+ if _basetag in smartchars.quotes:
+ defaultlanguage = _basetag
+ else:
+ defaultlanguage = 'en'
+
+ import argparse
+ parser = argparse.ArgumentParser(
+ description='Filter <input> making ASCII punctuation "smart".')
+ # TODO: require input arg or other means to print USAGE instead of waiting.
+ # parser.add_argument("input", help="Input stream, use '-' for stdin.")
+ parser.add_argument("-a", "--action", default="1",
+ help="what to do with the input (see --actionhelp)")
+ parser.add_argument("-e", "--encoding", default="utf-8",
+ help="text encoding")
+ parser.add_argument("-l", "--language", default=defaultlanguage,
+ help="text language (BCP47 tag), "
+ f"Default: {defaultlanguage}")
+ parser.add_argument("-q", "--alternative-quotes", action="store_true",
+ help="use alternative quote style")
+ parser.add_argument("--doc", action="store_true",
+ help="print documentation")
+ parser.add_argument("--actionhelp", action="store_true",
+ help="list available actions")
+ parser.add_argument("--stylehelp", action="store_true",
+ help="list available quote styles")
+ parser.add_argument("--test", action="store_true",
+ help="perform short self-test")
+ args = parser.parse_args()
+
+ if args.doc:
+ print(__doc__)
+ elif args.actionhelp:
+ print(options)
+ elif args.stylehelp:
+ print()
+ print("Available styles (primary open/close, secondary open/close)")
+ print("language tag quotes")
+ print("============ ======")
+ for key in sorted(smartchars.quotes.keys()):
+ print("%-14s %s" % (key, smartchars.quotes[key]))
+ elif args.test:
+ # Unit test output goes to stderr.
+ import unittest
+
+ class TestSmartypantsAllAttributes(unittest.TestCase):
+ # the default attribute is "1", which means "all".
+ def test_dates(self):
+ self.assertEqual(smartyPants("1440-80's"), "1440-80’s")
+ self.assertEqual(smartyPants("1440-'80s"), "1440-’80s")
+ self.assertEqual(smartyPants("1440---'80s"), "1440–’80s")
+ self.assertEqual(smartyPants("1960's"), "1960’s")
+ self.assertEqual(smartyPants("one two '60s"), "one two ’60s")
+ self.assertEqual(smartyPants("'60s"), "’60s")
+
+ def test_educated_quotes(self):
+ self.assertEqual(smartyPants('"Isn\'t this fun?"'),
+ '“Isn’t this fun?”')
+
+ def test_html_tags(self):
+ text = '<a src="foo">more</a>'
+ self.assertEqual(smartyPants(text), text)
+
+ suite = unittest.TestLoader().loadTestsFromTestCase(
+ TestSmartypantsAllAttributes)
+ unittest.TextTestRunner().run(suite)
+
+ else:
+ if args.alternative_quotes:
+ if '-x-altquot' in args.language:
+ args.language = args.language.replace('-x-altquot', '')
+ else:
+ args.language += '-x-altquot'
+ text = sys.stdin.read()
+ print(smartyPants(text, attr=args.action, language=args.language))
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/urischemes.py b/.venv/lib/python3.12/site-packages/docutils/utils/urischemes.py
new file mode 100644
index 00000000..a0435c02
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/urischemes.py
@@ -0,0 +1,138 @@
+# $Id: urischemes.py 9315 2023-01-15 19:27:55Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+`schemes` is a dictionary with lowercase URI addressing schemes as
+keys and descriptions as values. It was compiled from the index at
+http://www.iana.org/assignments/uri-schemes (revised 2005-11-28)
+and an older list at https://www.w3.org/Addressing/schemes.html.
+"""
+
+# Many values are blank and should be filled in with useful descriptions.
+
+schemes = {
+ 'about': 'provides information on Navigator',
+ 'acap': 'Application Configuration Access Protocol; RFC 2244',
+ 'addbook': "To add vCard entries to Communicator's Address Book",
+ 'afp': 'Apple Filing Protocol',
+ 'afs': 'Andrew File System global file names',
+ 'aim': 'AOL Instant Messenger',
+ 'callto': 'for NetMeeting links',
+ 'castanet': 'Castanet Tuner URLs for Netcaster',
+ 'chttp': 'cached HTTP supported by RealPlayer',
+ 'cid': 'content identifier; RFC 2392',
+ 'crid': 'TV-Anytime Content Reference Identifier; RFC 4078',
+ 'data': 'allows inclusion of small data items as "immediate" data; '
+ 'RFC 2397',
+ 'dav': 'Distributed Authoring and Versioning Protocol; RFC 2518',
+ 'dict': 'dictionary service protocol; RFC 2229',
+ 'dns': 'Domain Name System resources',
+ 'eid': 'External ID; non-URL data; general escape mechanism to allow '
+ 'access to information for applications that are too '
+ 'specialized to justify their own schemes',
+ 'fax': 'a connection to a terminal that can handle telefaxes '
+ '(facsimiles); RFC 2806',
+ 'feed': 'NetNewsWire feed',
+ 'file': 'Host-specific file names; RFC 1738',
+ 'finger': '',
+ 'freenet': '',
+ 'ftp': 'File Transfer Protocol; RFC 1738',
+ 'go': 'go; RFC 3368',
+ 'gopher': 'The Gopher Protocol',
+ 'gsm-sms': 'Global System for Mobile Communications Short Message '
+ 'Service',
+ 'h323': 'video (audiovisual) communication on local area networks; '
+ 'RFC 3508',
+ 'h324': 'video and audio communications over low bitrate connections '
+ 'such as POTS modem connections',
+ 'hdl': 'CNRI handle system',
+ 'hnews': 'an HTTP-tunneling variant of the NNTP news protocol',
+ 'http': 'Hypertext Transfer Protocol; RFC 2616',
+ 'https': 'HTTP over SSL; RFC 2818',
+ 'hydra': 'SubEthaEdit URI. '
+ 'See http://www.codingmonkeys.de/subethaedit.',
+ 'iioploc': 'Internet Inter-ORB Protocol Location?',
+ 'ilu': 'Inter-Language Unification',
+ 'im': 'Instant Messaging; RFC 3860',
+ 'imap': 'Internet Message Access Protocol; RFC 2192',
+ 'info': 'Information Assets with Identifiers in Public Namespaces',
+ 'ior': 'CORBA interoperable object reference',
+ 'ipp': 'Internet Printing Protocol; RFC 3510',
+ 'irc': 'Internet Relay Chat',
+ 'iris.beep': 'iris.beep; RFC 3983',
+ 'iseek': 'See www.ambrosiasw.com; a little util for OS X.',
+ 'jar': 'Java archive',
+ 'javascript': 'JavaScript code; '
+ 'evaluates the expression after the colon',
+ 'jdbc': 'JDBC connection URI.',
+ 'ldap': 'Lightweight Directory Access Protocol',
+ 'lifn': '',
+ 'livescript': '',
+ 'lrq': '',
+ 'mailbox': 'Mail folder access',
+ 'mailserver': 'Access to data available from mail servers',
+ 'mailto': 'Electronic mail address; RFC 2368',
+ 'md5': '',
+ 'mid': 'message identifier; RFC 2392',
+ 'mocha': '',
+ 'modem': 'a connection to a terminal that can handle incoming data '
+ 'calls; RFC 2806',
+ 'mtqp': 'Message Tracking Query Protocol; RFC 3887',
+ 'mupdate': 'Mailbox Update (MUPDATE) Protocol; RFC 3656',
+ 'news': 'USENET news; RFC 1738',
+ 'nfs': 'Network File System protocol; RFC 2224',
+ 'nntp': 'USENET news using NNTP access; RFC 1738',
+ 'opaquelocktoken': 'RFC 2518',
+ 'phone': '',
+ 'pop': 'Post Office Protocol; RFC 2384',
+ 'pop3': 'Post Office Protocol v3',
+ 'pres': 'Presence; RFC 3859',
+ 'printer': '',
+ 'prospero': 'Prospero Directory Service; RFC 4157',
+ 'rdar': 'URLs found in Darwin source '
+ '(http://www.opensource.apple.com/darwinsource/).',
+ 'res': '',
+ 'rtsp': 'real time streaming protocol; RFC 2326',
+ 'rvp': '',
+ 'rwhois': '',
+ 'rx': 'Remote Execution',
+ 'sdp': '',
+ 'service': 'service location; RFC 2609',
+ 'shttp': 'secure hypertext transfer protocol',
+ 'sip': 'Session Initiation Protocol; RFC 3261',
+ 'sips': 'secure session intitiaion protocol; RFC 3261',
+ 'smb': 'SAMBA filesystems.',
+ 'snews': 'For NNTP postings via SSL',
+ 'snmp': 'Simple Network Management Protocol; RFC 4088',
+ 'soap.beep': 'RFC 3288',
+ 'soap.beeps': 'RFC 3288',
+ 'ssh': 'Reference to interactive sessions via ssh.',
+ 't120': 'real time data conferencing (audiographics)',
+ 'tag': 'RFC 4151',
+ 'tcp': '',
+ 'tel': 'a connection to a terminal that handles normal voice '
+ 'telephone calls, a voice mailbox or another voice messaging '
+ 'system or a service that can be operated using DTMF tones; '
+ 'RFC 3966.',
+ 'telephone': 'telephone',
+ 'telnet': 'Reference to interactive sessions; RFC 4248',
+ 'tftp': 'Trivial File Transfer Protocol; RFC 3617',
+ 'tip': 'Transaction Internet Protocol; RFC 2371',
+ 'tn3270': 'Interactive 3270 emulation sessions',
+ 'tv': '',
+ 'urn': 'Uniform Resource Name; RFC 2141',
+ 'uuid': '',
+ 'vemmi': 'versatile multimedia interface; RFC 2122',
+ 'videotex': '',
+ 'view-source': 'displays HTML code that was generated with JavaScript',
+ 'wais': 'Wide Area Information Servers; RFC 4156',
+ 'whodp': '',
+ 'whois++': 'Distributed directory service.',
+ 'x-man-page': 'Opens man page in Terminal.app on OS X '
+ '(see macosxhints.com)',
+ 'xmlrpc.beep': 'RFC 3529',
+ 'xmlrpc.beeps': 'RFC 3529',
+ 'z39.50r': 'Z39.50 Retrieval; RFC 2056',
+ 'z39.50s': 'Z39.50 Session; RFC 2056',
+ }