diff options
author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
---|---|---|
committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/docutils/utils | |
parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
download | gn-ai-master.tar.gz |
Diffstat (limited to '.venv/lib/python3.12/site-packages/docutils/utils')
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')) # ⁡ + 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 = { + '&': '&', + '<': '<', + '>': '>', + } + + +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<\u2009', + '=': '\u2009=\u2009', '>': '\u2009>\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', '⁡') + except TypeError: + xml = xml.replace('\u2061'.encode(encoding), b'⁡') + 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><</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 ``"`` 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 ``"`` 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 " entities to " for Dreamweaver users + + convert_quot = False # translate " 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('"', '"') + + 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 + ------ ----- + \\ \ + \" " + \' ' + \. . + \- - + \` ` + """ + replacements = ((r'\\', r'\'), + (r'\"', r'"'), + (r"\'", r'''), + (r'\.', r'.'), + (r'\-', r'-'), + (r'\`', r'`')) + 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', + } |