You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			2856 lines
		
	
	
		
			105 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			2856 lines
		
	
	
		
			105 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
Implementation details for :mod:`.mathtext`.
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import abc
 | 
						|
import copy
 | 
						|
import enum
 | 
						|
import functools
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import re
 | 
						|
import types
 | 
						|
import unicodedata
 | 
						|
import string
 | 
						|
import typing as T
 | 
						|
from typing import NamedTuple
 | 
						|
 | 
						|
import numpy as np
 | 
						|
from pyparsing import (
 | 
						|
    Empty, Forward, Literal, Group, NotAny, OneOrMore, Optional,
 | 
						|
    ParseBaseException, ParseException, ParseExpression, ParseFatalException,
 | 
						|
    ParserElement, ParseResults, QuotedString, Regex, StringEnd, ZeroOrMore,
 | 
						|
    pyparsing_common, nested_expr, one_of)
 | 
						|
 | 
						|
import matplotlib as mpl
 | 
						|
from . import cbook
 | 
						|
from ._mathtext_data import (
 | 
						|
    latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni)
 | 
						|
from .font_manager import FontProperties, findfont, get_font
 | 
						|
from .ft2font import FT2Font, FT2Image, Kerning, LoadFlags
 | 
						|
 | 
						|
 | 
						|
if T.TYPE_CHECKING:
 | 
						|
    from collections.abc import Iterable
 | 
						|
    from .ft2font import Glyph
 | 
						|
 | 
						|
ParserElement.enable_packrat()
 | 
						|
_log = logging.getLogger("matplotlib.mathtext")
 | 
						|
 | 
						|
 | 
						|
##############################################################################
 | 
						|
# FONTS
 | 
						|
 | 
						|
 | 
						|
def get_unicode_index(symbol: str) -> int:  # Publicly exported.
 | 
						|
    r"""
 | 
						|
    Return the integer index (from the Unicode table) of *symbol*.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    symbol : str
 | 
						|
        A single (Unicode) character, a TeX command (e.g. r'\pi') or a Type1
 | 
						|
        symbol name (e.g. 'phi').
 | 
						|
    """
 | 
						|
    try:  # This will succeed if symbol is a single Unicode char
 | 
						|
        return ord(symbol)
 | 
						|
    except TypeError:
 | 
						|
        pass
 | 
						|
    try:  # Is symbol a TeX symbol (i.e. \alpha)
 | 
						|
        return tex2uni[symbol.strip("\\")]
 | 
						|
    except KeyError as err:
 | 
						|
        raise ValueError(
 | 
						|
            f"{symbol!r} is not a valid Unicode character or TeX/Type1 symbol"
 | 
						|
            ) from err
 | 
						|
 | 
						|
 | 
						|
class VectorParse(NamedTuple):
 | 
						|
    """
 | 
						|
    The namedtuple type returned by ``MathTextParser("path").parse(...)``.
 | 
						|
 | 
						|
    Attributes
 | 
						|
    ----------
 | 
						|
    width, height, depth : float
 | 
						|
        The global metrics.
 | 
						|
    glyphs : list
 | 
						|
        The glyphs including their positions.
 | 
						|
    rect : list
 | 
						|
        The list of rectangles.
 | 
						|
    """
 | 
						|
    width: float
 | 
						|
    height: float
 | 
						|
    depth: float
 | 
						|
    glyphs: list[tuple[FT2Font, float, int, float, float]]
 | 
						|
    rects: list[tuple[float, float, float, float]]
 | 
						|
 | 
						|
VectorParse.__module__ = "matplotlib.mathtext"
 | 
						|
 | 
						|
 | 
						|
class RasterParse(NamedTuple):
 | 
						|
    """
 | 
						|
    The namedtuple type returned by ``MathTextParser("agg").parse(...)``.
 | 
						|
 | 
						|
    Attributes
 | 
						|
    ----------
 | 
						|
    ox, oy : float
 | 
						|
        The offsets are always zero.
 | 
						|
    width, height, depth : float
 | 
						|
        The global metrics.
 | 
						|
    image : FT2Image
 | 
						|
        A raster image.
 | 
						|
    """
 | 
						|
    ox: float
 | 
						|
    oy: float
 | 
						|
    width: float
 | 
						|
    height: float
 | 
						|
    depth: float
 | 
						|
    image: FT2Image
 | 
						|
 | 
						|
RasterParse.__module__ = "matplotlib.mathtext"
 | 
						|
 | 
						|
 | 
						|
class Output:
 | 
						|
    r"""
 | 
						|
    Result of `ship`\ping a box: lists of positioned glyphs and rectangles.
 | 
						|
 | 
						|
    This class is not exposed to end users, but converted to a `VectorParse` or
 | 
						|
    a `RasterParse` by `.MathTextParser.parse`.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, box: Box):
 | 
						|
        self.box = box
 | 
						|
        self.glyphs: list[tuple[float, float, FontInfo]] = []  # (ox, oy, info)
 | 
						|
        self.rects: list[tuple[float, float, float, float]] = []  # (x1, y1, x2, y2)
 | 
						|
 | 
						|
    def to_vector(self) -> VectorParse:
 | 
						|
        w, h, d = map(
 | 
						|
            np.ceil, [self.box.width, self.box.height, self.box.depth])
 | 
						|
        gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset)
 | 
						|
              for ox, oy, info in self.glyphs]
 | 
						|
        rs = [(x1, h - y2, x2 - x1, y2 - y1)
 | 
						|
              for x1, y1, x2, y2 in self.rects]
 | 
						|
        return VectorParse(w, h + d, d, gs, rs)
 | 
						|
 | 
						|
    def to_raster(self, *, antialiased: bool) -> RasterParse:
 | 
						|
        # Metrics y's and mathtext y's are oriented in opposite directions,
 | 
						|
        # hence the switch between ymin and ymax.
 | 
						|
        xmin = min([*[ox + info.metrics.xmin for ox, oy, info in self.glyphs],
 | 
						|
                    *[x1 for x1, y1, x2, y2 in self.rects], 0]) - 1
 | 
						|
        ymin = min([*[oy - info.metrics.ymax for ox, oy, info in self.glyphs],
 | 
						|
                    *[y1 for x1, y1, x2, y2 in self.rects], 0]) - 1
 | 
						|
        xmax = max([*[ox + info.metrics.xmax for ox, oy, info in self.glyphs],
 | 
						|
                    *[x2 for x1, y1, x2, y2 in self.rects], 0]) + 1
 | 
						|
        ymax = max([*[oy - info.metrics.ymin for ox, oy, info in self.glyphs],
 | 
						|
                    *[y2 for x1, y1, x2, y2 in self.rects], 0]) + 1
 | 
						|
        w = xmax - xmin
 | 
						|
        h = ymax - ymin - self.box.depth
 | 
						|
        d = ymax - ymin - self.box.height
 | 
						|
        image = FT2Image(int(np.ceil(w)), int(np.ceil(h + max(d, 0))))
 | 
						|
 | 
						|
        # Ideally, we could just use self.glyphs and self.rects here, shifting
 | 
						|
        # their coordinates by (-xmin, -ymin), but this yields slightly
 | 
						|
        # different results due to floating point slop; shipping twice is the
 | 
						|
        # old approach and keeps baseline images backcompat.
 | 
						|
        shifted = ship(self.box, (-xmin, -ymin))
 | 
						|
 | 
						|
        for ox, oy, info in shifted.glyphs:
 | 
						|
            info.font.draw_glyph_to_bitmap(
 | 
						|
                image, int(ox), int(oy - info.metrics.iceberg), info.glyph,
 | 
						|
                antialiased=antialiased)
 | 
						|
        for x1, y1, x2, y2 in shifted.rects:
 | 
						|
            height = max(int(y2 - y1) - 1, 0)
 | 
						|
            if height == 0:
 | 
						|
                center = (y2 + y1) / 2
 | 
						|
                y = int(center - (height + 1) / 2)
 | 
						|
            else:
 | 
						|
                y = int(y1)
 | 
						|
            image.draw_rect_filled(int(x1), y, int(np.ceil(x2)), y + height)
 | 
						|
        return RasterParse(0, 0, w, h + d, d, image)
 | 
						|
 | 
						|
 | 
						|
class FontMetrics(NamedTuple):
 | 
						|
    """
 | 
						|
    Metrics of a font.
 | 
						|
 | 
						|
    Attributes
 | 
						|
    ----------
 | 
						|
    advance : float
 | 
						|
        The advance distance (in points) of the glyph.
 | 
						|
    height : float
 | 
						|
        The height of the glyph in points.
 | 
						|
    width : float
 | 
						|
        The width of the glyph in points.
 | 
						|
    xmin, xmax, ymin, ymax : float
 | 
						|
        The ink rectangle of the glyph.
 | 
						|
    iceberg : float
 | 
						|
        The distance from the baseline to the top of the glyph. (This corresponds to
 | 
						|
        TeX's definition of "height".)
 | 
						|
    slanted : bool
 | 
						|
        Whether the glyph should be considered as "slanted" (currently used for kerning
 | 
						|
        sub/superscripts).
 | 
						|
    """
 | 
						|
    advance: float
 | 
						|
    height: float
 | 
						|
    width: float
 | 
						|
    xmin: float
 | 
						|
    xmax: float
 | 
						|
    ymin: float
 | 
						|
    ymax: float
 | 
						|
    iceberg: float
 | 
						|
    slanted: bool
 | 
						|
 | 
						|
 | 
						|
class FontInfo(NamedTuple):
 | 
						|
    font: FT2Font
 | 
						|
    fontsize: float
 | 
						|
    postscript_name: str
 | 
						|
    metrics: FontMetrics
 | 
						|
    num: int
 | 
						|
    glyph: Glyph
 | 
						|
    offset: float
 | 
						|
 | 
						|
 | 
						|
class Fonts(abc.ABC):
 | 
						|
    """
 | 
						|
    An abstract base class for a system of fonts to use for mathtext.
 | 
						|
 | 
						|
    The class must be able to take symbol keys and font file names and
 | 
						|
    return the character metrics.  It also delegates to a backend class
 | 
						|
    to do the actual drawing.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
 | 
						|
        """
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        default_font_prop : `~.font_manager.FontProperties`
 | 
						|
            The default non-math font, or the base font for Unicode (generic)
 | 
						|
            font rendering.
 | 
						|
        load_glyph_flags : `.ft2font.LoadFlags`
 | 
						|
            Flags passed to the glyph loader (e.g. ``FT_Load_Glyph`` and
 | 
						|
            ``FT_Load_Char`` for FreeType-based fonts).
 | 
						|
        """
 | 
						|
        self.default_font_prop = default_font_prop
 | 
						|
        self.load_glyph_flags = load_glyph_flags
 | 
						|
 | 
						|
    def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float,
 | 
						|
                 font2: str, fontclass2: str, sym2: str, fontsize2: float,
 | 
						|
                 dpi: float) -> float:
 | 
						|
        """
 | 
						|
        Get the kerning distance for font between *sym1* and *sym2*.
 | 
						|
 | 
						|
        See `~.Fonts.get_metrics` for a detailed description of the parameters.
 | 
						|
        """
 | 
						|
        return 0.
 | 
						|
 | 
						|
    def _get_font(self, font: str) -> FT2Font:
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    def _get_info(self, font: str, font_class: str, sym: str, fontsize: float,
 | 
						|
                  dpi: float) -> FontInfo:
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    def get_metrics(self, font: str, font_class: str, sym: str, fontsize: float,
 | 
						|
                    dpi: float) -> FontMetrics:
 | 
						|
        r"""
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        font : str
 | 
						|
            One of the TeX font names: "tt", "it", "rm", "cal", "sf", "bf",
 | 
						|
            "default", "regular", "bb", "frak", "scr".  "default" and "regular"
 | 
						|
            are synonyms and use the non-math font.
 | 
						|
        font_class : str
 | 
						|
            One of the TeX font names (as for *font*), but **not** "bb",
 | 
						|
            "frak", or "scr".  This is used to combine two font classes.  The
 | 
						|
            only supported combination currently is ``get_metrics("frak", "bf",
 | 
						|
            ...)``.
 | 
						|
        sym : str
 | 
						|
            A symbol in raw TeX form, e.g., "1", "x", or "\sigma".
 | 
						|
        fontsize : float
 | 
						|
            Font size in points.
 | 
						|
        dpi : float
 | 
						|
            Rendering dots-per-inch.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        FontMetrics
 | 
						|
        """
 | 
						|
        info = self._get_info(font, font_class, sym, fontsize, dpi)
 | 
						|
        return info.metrics
 | 
						|
 | 
						|
    def render_glyph(self, output: Output, ox: float, oy: float, font: str,
 | 
						|
                     font_class: str, sym: str, fontsize: float, dpi: float) -> None:
 | 
						|
        """
 | 
						|
        At position (*ox*, *oy*), draw the glyph specified by the remaining
 | 
						|
        parameters (see `get_metrics` for their detailed description).
 | 
						|
        """
 | 
						|
        info = self._get_info(font, font_class, sym, fontsize, dpi)
 | 
						|
        output.glyphs.append((ox, oy, info))
 | 
						|
 | 
						|
    def render_rect_filled(self, output: Output,
 | 
						|
                           x1: float, y1: float, x2: float, y2: float) -> None:
 | 
						|
        """
 | 
						|
        Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*).
 | 
						|
        """
 | 
						|
        output.rects.append((x1, y1, x2, y2))
 | 
						|
 | 
						|
    def get_xheight(self, font: str, fontsize: float, dpi: float) -> float:
 | 
						|
        """
 | 
						|
        Get the xheight for the given *font* and *fontsize*.
 | 
						|
        """
 | 
						|
        raise NotImplementedError()
 | 
						|
 | 
						|
    def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float:
 | 
						|
        """
 | 
						|
        Get the line thickness that matches the given font.  Used as a
 | 
						|
        base unit for drawing lines such as in a fraction or radical.
 | 
						|
        """
 | 
						|
        raise NotImplementedError()
 | 
						|
 | 
						|
    def get_sized_alternatives_for_symbol(self, fontname: str,
 | 
						|
                                          sym: str) -> list[tuple[str, str]]:
 | 
						|
        """
 | 
						|
        Override if your font provides multiple sizes of the same
 | 
						|
        symbol.  Should return a list of symbols matching *sym* in
 | 
						|
        various sizes.  The expression renderer will select the most
 | 
						|
        appropriate size for a given situation from this list.
 | 
						|
        """
 | 
						|
        return [(fontname, sym)]
 | 
						|
 | 
						|
 | 
						|
class TruetypeFonts(Fonts, metaclass=abc.ABCMeta):
 | 
						|
    """
 | 
						|
    A generic base class for all font setups that use Truetype fonts
 | 
						|
    (through FT2Font).
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
 | 
						|
        super().__init__(default_font_prop, load_glyph_flags)
 | 
						|
        # Per-instance cache.
 | 
						|
        self._get_info = functools.cache(self._get_info)  # type: ignore[method-assign]
 | 
						|
        self._fonts = {}
 | 
						|
        self.fontmap: dict[str | int, str] = {}
 | 
						|
 | 
						|
        filename = findfont(self.default_font_prop)
 | 
						|
        default_font = get_font(filename)
 | 
						|
        self._fonts['default'] = default_font
 | 
						|
        self._fonts['regular'] = default_font
 | 
						|
 | 
						|
    def _get_font(self, font: str | int) -> FT2Font:
 | 
						|
        if font in self.fontmap:
 | 
						|
            basename = self.fontmap[font]
 | 
						|
        else:
 | 
						|
            # NOTE: An int is only passed by subclasses which have placed int keys into
 | 
						|
            # `self.fontmap`, so we must cast this to confirm it to typing.
 | 
						|
            basename = T.cast(str, font)
 | 
						|
        cached_font = self._fonts.get(basename)
 | 
						|
        if cached_font is None and os.path.exists(basename):
 | 
						|
            cached_font = get_font(basename)
 | 
						|
            self._fonts[basename] = cached_font
 | 
						|
            self._fonts[cached_font.postscript_name] = cached_font
 | 
						|
            self._fonts[cached_font.postscript_name.lower()] = cached_font
 | 
						|
        return T.cast(FT2Font, cached_font)  # FIXME: Not sure this is guaranteed.
 | 
						|
 | 
						|
    def _get_offset(self, font: FT2Font, glyph: Glyph, fontsize: float,
 | 
						|
                    dpi: float) -> float:
 | 
						|
        if font.postscript_name == 'Cmex10':
 | 
						|
            return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72)
 | 
						|
        return 0.
 | 
						|
 | 
						|
    def _get_glyph(self, fontname: str, font_class: str,
 | 
						|
                   sym: str) -> tuple[FT2Font, int, bool]:
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    # The return value of _get_info is cached per-instance.
 | 
						|
    def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float,
 | 
						|
                  dpi: float) -> FontInfo:
 | 
						|
        font, num, slanted = self._get_glyph(fontname, font_class, sym)
 | 
						|
        font.set_size(fontsize, dpi)
 | 
						|
        glyph = font.load_char(num, flags=self.load_glyph_flags)
 | 
						|
 | 
						|
        xmin, ymin, xmax, ymax = (val / 64 for val in glyph.bbox)
 | 
						|
        offset = self._get_offset(font, glyph, fontsize, dpi)
 | 
						|
        metrics = FontMetrics(
 | 
						|
            advance=glyph.linearHoriAdvance / 65536,
 | 
						|
            height=glyph.height / 64,
 | 
						|
            width=glyph.width / 64,
 | 
						|
            xmin=xmin,
 | 
						|
            xmax=xmax,
 | 
						|
            ymin=ymin + offset,
 | 
						|
            ymax=ymax + offset,
 | 
						|
            # iceberg is the equivalent of TeX's "height"
 | 
						|
            iceberg=glyph.horiBearingY / 64 + offset,
 | 
						|
            slanted=slanted
 | 
						|
        )
 | 
						|
 | 
						|
        return FontInfo(
 | 
						|
            font=font,
 | 
						|
            fontsize=fontsize,
 | 
						|
            postscript_name=font.postscript_name,
 | 
						|
            metrics=metrics,
 | 
						|
            num=num,
 | 
						|
            glyph=glyph,
 | 
						|
            offset=offset
 | 
						|
        )
 | 
						|
 | 
						|
    def get_xheight(self, fontname: str, fontsize: float, dpi: float) -> float:
 | 
						|
        font = self._get_font(fontname)
 | 
						|
        font.set_size(fontsize, dpi)
 | 
						|
        pclt = font.get_sfnt_table('pclt')
 | 
						|
        if pclt is None:
 | 
						|
            # Some fonts don't store the xHeight, so we do a poor man's xHeight
 | 
						|
            metrics = self.get_metrics(
 | 
						|
                fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi)
 | 
						|
            return metrics.iceberg
 | 
						|
        xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
 | 
						|
        return xHeight
 | 
						|
 | 
						|
    def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float:
 | 
						|
        # This function used to grab underline thickness from the font
 | 
						|
        # metrics, but that information is just too un-reliable, so it
 | 
						|
        # is now hardcoded.
 | 
						|
        return ((0.75 / 12.0) * fontsize * dpi) / 72.0
 | 
						|
 | 
						|
    def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float,
 | 
						|
                 font2: str, fontclass2: str, sym2: str, fontsize2: float,
 | 
						|
                 dpi: float) -> float:
 | 
						|
        if font1 == font2 and fontsize1 == fontsize2:
 | 
						|
            info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
 | 
						|
            info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
 | 
						|
            font = info1.font
 | 
						|
            return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64
 | 
						|
        return super().get_kern(font1, fontclass1, sym1, fontsize1,
 | 
						|
                                font2, fontclass2, sym2, fontsize2, dpi)
 | 
						|
 | 
						|
 | 
						|
class BakomaFonts(TruetypeFonts):
 | 
						|
    """
 | 
						|
    Use the Bakoma TrueType fonts for rendering.
 | 
						|
 | 
						|
    Symbols are strewn about a number of font files, each of which has
 | 
						|
    its own proprietary 8-bit encoding.
 | 
						|
    """
 | 
						|
    _fontmap = {
 | 
						|
        'cal': 'cmsy10',
 | 
						|
        'rm':  'cmr10',
 | 
						|
        'tt':  'cmtt10',
 | 
						|
        'it':  'cmmi10',
 | 
						|
        'bf':  'cmb10',
 | 
						|
        'sf':  'cmss10',
 | 
						|
        'ex':  'cmex10',
 | 
						|
    }
 | 
						|
 | 
						|
    def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
 | 
						|
        self._stix_fallback = StixFonts(default_font_prop, load_glyph_flags)
 | 
						|
 | 
						|
        super().__init__(default_font_prop, load_glyph_flags)
 | 
						|
        for key, val in self._fontmap.items():
 | 
						|
            fullpath = findfont(val)
 | 
						|
            self.fontmap[key] = fullpath
 | 
						|
            self.fontmap[val] = fullpath
 | 
						|
 | 
						|
    _slanted_symbols = set(r"\int \oint".split())
 | 
						|
 | 
						|
    def _get_glyph(self, fontname: str, font_class: str,
 | 
						|
                   sym: str) -> tuple[FT2Font, int, bool]:
 | 
						|
        font = None
 | 
						|
        if fontname in self.fontmap and sym in latex_to_bakoma:
 | 
						|
            basename, num = latex_to_bakoma[sym]
 | 
						|
            slanted = (basename == "cmmi10") or sym in self._slanted_symbols
 | 
						|
            font = self._get_font(basename)
 | 
						|
        elif len(sym) == 1:
 | 
						|
            slanted = (fontname == "it")
 | 
						|
            font = self._get_font(fontname)
 | 
						|
            if font is not None:
 | 
						|
                num = ord(sym)
 | 
						|
        if font is not None and font.get_char_index(num) != 0:
 | 
						|
            return font, num, slanted
 | 
						|
        else:
 | 
						|
            return self._stix_fallback._get_glyph(fontname, font_class, sym)
 | 
						|
 | 
						|
    # The Bakoma fonts contain many pre-sized alternatives for the
 | 
						|
    # delimiters.  The AutoSizedChar class will use these alternatives
 | 
						|
    # and select the best (closest sized) glyph.
 | 
						|
    _size_alternatives = {
 | 
						|
        '(':           [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
 | 
						|
                        ('ex', '\xb5'), ('ex', '\xc3')],
 | 
						|
        ')':           [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
 | 
						|
                        ('ex', '\xb6'), ('ex', '\x21')],
 | 
						|
        '{':           [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'),
 | 
						|
                        ('ex', '\xbd'), ('ex', '\x28')],
 | 
						|
        '}':           [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'),
 | 
						|
                        ('ex', '\xbe'), ('ex', '\x29')],
 | 
						|
        # The fourth size of '[' is mysteriously missing from the BaKoMa
 | 
						|
        # font, so I've omitted it for both '[' and ']'
 | 
						|
        '[':           [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'),
 | 
						|
                        ('ex', '\x22')],
 | 
						|
        ']':           [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'),
 | 
						|
                        ('ex', '\x23')],
 | 
						|
        r'\lfloor':    [('ex', '\xa5'), ('ex', '\x6a'),
 | 
						|
                        ('ex', '\xb9'), ('ex', '\x24')],
 | 
						|
        r'\rfloor':    [('ex', '\xa6'), ('ex', '\x6b'),
 | 
						|
                        ('ex', '\xba'), ('ex', '\x25')],
 | 
						|
        r'\lceil':     [('ex', '\xa7'), ('ex', '\x6c'),
 | 
						|
                        ('ex', '\xbb'), ('ex', '\x26')],
 | 
						|
        r'\rceil':     [('ex', '\xa8'), ('ex', '\x6d'),
 | 
						|
                        ('ex', '\xbc'), ('ex', '\x27')],
 | 
						|
        r'\langle':    [('ex', '\xad'), ('ex', '\x44'),
 | 
						|
                        ('ex', '\xbf'), ('ex', '\x2a')],
 | 
						|
        r'\rangle':    [('ex', '\xae'), ('ex', '\x45'),
 | 
						|
                        ('ex', '\xc0'), ('ex', '\x2b')],
 | 
						|
        r'\__sqrt__':  [('ex', '\x70'), ('ex', '\x71'),
 | 
						|
                        ('ex', '\x72'), ('ex', '\x73')],
 | 
						|
        r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'),
 | 
						|
                        ('ex', '\xc2'), ('ex', '\x2d')],
 | 
						|
        r'/':          [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'),
 | 
						|
                        ('ex', '\xcb'), ('ex', '\x2c')],
 | 
						|
        r'\widehat':   [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'),
 | 
						|
                        ('ex', '\x64')],
 | 
						|
        r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'),
 | 
						|
                        ('ex', '\x67')],
 | 
						|
        r'<':          [('cal', 'h'), ('ex', 'D')],
 | 
						|
        r'>':          [('cal', 'i'), ('ex', 'E')]
 | 
						|
        }
 | 
						|
 | 
						|
    for alias, target in [(r'\leftparen', '('),
 | 
						|
                          (r'\rightparen', ')'),
 | 
						|
                          (r'\leftbrace', '{'),
 | 
						|
                          (r'\rightbrace', '}'),
 | 
						|
                          (r'\leftbracket', '['),
 | 
						|
                          (r'\rightbracket', ']'),
 | 
						|
                          (r'\{', '{'),
 | 
						|
                          (r'\}', '}'),
 | 
						|
                          (r'\[', '['),
 | 
						|
                          (r'\]', ']')]:
 | 
						|
        _size_alternatives[alias] = _size_alternatives[target]
 | 
						|
 | 
						|
    def get_sized_alternatives_for_symbol(self, fontname: str,
 | 
						|
                                          sym: str) -> list[tuple[str, str]]:
 | 
						|
        return self._size_alternatives.get(sym, [(fontname, sym)])
 | 
						|
 | 
						|
 | 
						|
class UnicodeFonts(TruetypeFonts):
 | 
						|
    """
 | 
						|
    An abstract base class for handling Unicode fonts.
 | 
						|
 | 
						|
    While some reasonably complete Unicode fonts (such as DejaVu) may
 | 
						|
    work in some situations, the only Unicode font I'm aware of with a
 | 
						|
    complete set of math symbols is STIX.
 | 
						|
 | 
						|
    This class will "fallback" on the Bakoma fonts when a required
 | 
						|
    symbol cannot be found in the font.
 | 
						|
    """
 | 
						|
 | 
						|
    # Some glyphs are not present in the `cmr10` font, and must be brought in
 | 
						|
    # from `cmsy10`. Map the Unicode indices of those glyphs to the indices at
 | 
						|
    # which they are found in `cmsy10`.
 | 
						|
    _cmr10_substitutions = {
 | 
						|
        0x00D7: 0x00A3,  # Multiplication sign.
 | 
						|
        0x2212: 0x00A1,  # Minus sign.
 | 
						|
    }
 | 
						|
 | 
						|
    def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
 | 
						|
        # This must come first so the backend's owner is set correctly
 | 
						|
        fallback_rc = mpl.rcParams['mathtext.fallback']
 | 
						|
        font_cls: type[TruetypeFonts] | None = {
 | 
						|
            'stix': StixFonts,
 | 
						|
            'stixsans': StixSansFonts,
 | 
						|
            'cm': BakomaFonts
 | 
						|
        }.get(fallback_rc)
 | 
						|
        self._fallback_font = (font_cls(default_font_prop, load_glyph_flags)
 | 
						|
                               if font_cls else None)
 | 
						|
 | 
						|
        super().__init__(default_font_prop, load_glyph_flags)
 | 
						|
        for texfont in "cal rm tt it bf sf bfit".split():
 | 
						|
            prop = mpl.rcParams['mathtext.' + texfont]
 | 
						|
            font = findfont(prop)
 | 
						|
            self.fontmap[texfont] = font
 | 
						|
        prop = FontProperties('cmex10')
 | 
						|
        font = findfont(prop)
 | 
						|
        self.fontmap['ex'] = font
 | 
						|
 | 
						|
        # include STIX sized alternatives for glyphs if fallback is STIX
 | 
						|
        if isinstance(self._fallback_font, StixFonts):
 | 
						|
            stixsizedaltfonts = {
 | 
						|
                 0: 'STIXGeneral',
 | 
						|
                 1: 'STIXSizeOneSym',
 | 
						|
                 2: 'STIXSizeTwoSym',
 | 
						|
                 3: 'STIXSizeThreeSym',
 | 
						|
                 4: 'STIXSizeFourSym',
 | 
						|
                 5: 'STIXSizeFiveSym'}
 | 
						|
 | 
						|
            for size, name in stixsizedaltfonts.items():
 | 
						|
                fullpath = findfont(name)
 | 
						|
                self.fontmap[size] = fullpath
 | 
						|
                self.fontmap[name] = fullpath
 | 
						|
 | 
						|
    _slanted_symbols = set(r"\int \oint".split())
 | 
						|
 | 
						|
    def _map_virtual_font(self, fontname: str, font_class: str,
 | 
						|
                          uniindex: int) -> tuple[str, int]:
 | 
						|
        return fontname, uniindex
 | 
						|
 | 
						|
    def _get_glyph(self, fontname: str, font_class: str,
 | 
						|
                   sym: str) -> tuple[FT2Font, int, bool]:
 | 
						|
        try:
 | 
						|
            uniindex = get_unicode_index(sym)
 | 
						|
            found_symbol = True
 | 
						|
        except ValueError:
 | 
						|
            uniindex = ord('?')
 | 
						|
            found_symbol = False
 | 
						|
            _log.warning("No TeX to Unicode mapping for %a.", sym)
 | 
						|
 | 
						|
        fontname, uniindex = self._map_virtual_font(
 | 
						|
            fontname, font_class, uniindex)
 | 
						|
 | 
						|
        new_fontname = fontname
 | 
						|
 | 
						|
        # Only characters in the "Letter" class should be italicized in 'it'
 | 
						|
        # mode.  Greek capital letters should be Roman.
 | 
						|
        if found_symbol:
 | 
						|
            if fontname == 'it' and uniindex < 0x10000:
 | 
						|
                char = chr(uniindex)
 | 
						|
                if (unicodedata.category(char)[0] != "L"
 | 
						|
                        or unicodedata.name(char).startswith("GREEK CAPITAL")):
 | 
						|
                    new_fontname = 'rm'
 | 
						|
 | 
						|
            slanted = (new_fontname == 'it') or sym in self._slanted_symbols
 | 
						|
            found_symbol = False
 | 
						|
            font = self._get_font(new_fontname)
 | 
						|
            if font is not None:
 | 
						|
                if (uniindex in self._cmr10_substitutions
 | 
						|
                        and font.family_name == "cmr10"):
 | 
						|
                    font = get_font(
 | 
						|
                        cbook._get_data_path("fonts/ttf/cmsy10.ttf"))
 | 
						|
                    uniindex = self._cmr10_substitutions[uniindex]
 | 
						|
                glyphindex = font.get_char_index(uniindex)
 | 
						|
                if glyphindex != 0:
 | 
						|
                    found_symbol = True
 | 
						|
 | 
						|
        if not found_symbol:
 | 
						|
            if self._fallback_font:
 | 
						|
                if (fontname in ('it', 'regular')
 | 
						|
                        and isinstance(self._fallback_font, StixFonts)):
 | 
						|
                    fontname = 'rm'
 | 
						|
 | 
						|
                g = self._fallback_font._get_glyph(fontname, font_class, sym)
 | 
						|
                family = g[0].family_name
 | 
						|
                if family in list(BakomaFonts._fontmap.values()):
 | 
						|
                    family = "Computer Modern"
 | 
						|
                _log.info("Substituting symbol %s from %s", sym, family)
 | 
						|
                return g
 | 
						|
 | 
						|
            else:
 | 
						|
                if (fontname in ('it', 'regular')
 | 
						|
                        and isinstance(self, StixFonts)):
 | 
						|
                    return self._get_glyph('rm', font_class, sym)
 | 
						|
                _log.warning("Font %r does not have a glyph for %a [U+%x], "
 | 
						|
                             "substituting with a dummy symbol.",
 | 
						|
                             new_fontname, sym, uniindex)
 | 
						|
                font = self._get_font('rm')
 | 
						|
                uniindex = 0xA4  # currency char, for lack of anything better
 | 
						|
                slanted = False
 | 
						|
 | 
						|
        return font, uniindex, slanted
 | 
						|
 | 
						|
    def get_sized_alternatives_for_symbol(self, fontname: str,
 | 
						|
                                          sym: str) -> list[tuple[str, str]]:
 | 
						|
        if self._fallback_font:
 | 
						|
            return self._fallback_font.get_sized_alternatives_for_symbol(
 | 
						|
                fontname, sym)
 | 
						|
        return [(fontname, sym)]
 | 
						|
 | 
						|
 | 
						|
class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta):
 | 
						|
    _fontmap: dict[str | int, str] = {}
 | 
						|
 | 
						|
    def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
 | 
						|
        # This must come first so the backend's owner is set correctly
 | 
						|
        if isinstance(self, DejaVuSerifFonts):
 | 
						|
            self._fallback_font = StixFonts(default_font_prop, load_glyph_flags)
 | 
						|
        else:
 | 
						|
            self._fallback_font = StixSansFonts(default_font_prop, load_glyph_flags)
 | 
						|
        self.bakoma = BakomaFonts(default_font_prop, load_glyph_flags)
 | 
						|
        TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags)
 | 
						|
        # Include Stix sized alternatives for glyphs
 | 
						|
        self._fontmap.update({
 | 
						|
            1: 'STIXSizeOneSym',
 | 
						|
            2: 'STIXSizeTwoSym',
 | 
						|
            3: 'STIXSizeThreeSym',
 | 
						|
            4: 'STIXSizeFourSym',
 | 
						|
            5: 'STIXSizeFiveSym',
 | 
						|
        })
 | 
						|
        for key, name in self._fontmap.items():
 | 
						|
            fullpath = findfont(name)
 | 
						|
            self.fontmap[key] = fullpath
 | 
						|
            self.fontmap[name] = fullpath
 | 
						|
 | 
						|
    def _get_glyph(self, fontname: str, font_class: str,
 | 
						|
                   sym: str) -> tuple[FT2Font, int, bool]:
 | 
						|
        # Override prime symbol to use Bakoma.
 | 
						|
        if sym == r'\prime':
 | 
						|
            return self.bakoma._get_glyph(fontname, font_class, sym)
 | 
						|
        else:
 | 
						|
            # check whether the glyph is available in the display font
 | 
						|
            uniindex = get_unicode_index(sym)
 | 
						|
            font = self._get_font('ex')
 | 
						|
            if font is not None:
 | 
						|
                glyphindex = font.get_char_index(uniindex)
 | 
						|
                if glyphindex != 0:
 | 
						|
                    return super()._get_glyph('ex', font_class, sym)
 | 
						|
            # otherwise return regular glyph
 | 
						|
            return super()._get_glyph(fontname, font_class, sym)
 | 
						|
 | 
						|
 | 
						|
class DejaVuSerifFonts(DejaVuFonts):
 | 
						|
    """
 | 
						|
    A font handling class for the DejaVu Serif fonts
 | 
						|
 | 
						|
    If a glyph is not found it will fallback to Stix Serif
 | 
						|
    """
 | 
						|
    _fontmap = {
 | 
						|
        'rm': 'DejaVu Serif',
 | 
						|
        'it': 'DejaVu Serif:italic',
 | 
						|
        'bf': 'DejaVu Serif:weight=bold',
 | 
						|
        'bfit': 'DejaVu Serif:italic:bold',
 | 
						|
        'sf': 'DejaVu Sans',
 | 
						|
        'tt': 'DejaVu Sans Mono',
 | 
						|
        'ex': 'DejaVu Serif Display',
 | 
						|
        0:    'DejaVu Serif',
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
class DejaVuSansFonts(DejaVuFonts):
 | 
						|
    """
 | 
						|
    A font handling class for the DejaVu Sans fonts
 | 
						|
 | 
						|
    If a glyph is not found it will fallback to Stix Sans
 | 
						|
    """
 | 
						|
    _fontmap = {
 | 
						|
        'rm': 'DejaVu Sans',
 | 
						|
        'it': 'DejaVu Sans:italic',
 | 
						|
        'bf': 'DejaVu Sans:weight=bold',
 | 
						|
        'bfit': 'DejaVu Sans:italic:bold',
 | 
						|
        'sf': 'DejaVu Sans',
 | 
						|
        'tt': 'DejaVu Sans Mono',
 | 
						|
        'ex': 'DejaVu Sans Display',
 | 
						|
        0:    'DejaVu Sans',
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
class StixFonts(UnicodeFonts):
 | 
						|
    """
 | 
						|
    A font handling class for the STIX fonts.
 | 
						|
 | 
						|
    In addition to what UnicodeFonts provides, this class:
 | 
						|
 | 
						|
    - supports "virtual fonts" which are complete alpha numeric
 | 
						|
      character sets with different font styles at special Unicode
 | 
						|
      code points, such as "Blackboard".
 | 
						|
 | 
						|
    - handles sized alternative characters for the STIXSizeX fonts.
 | 
						|
    """
 | 
						|
    _fontmap: dict[str | int, str] = {
 | 
						|
        'rm': 'STIXGeneral',
 | 
						|
        'it': 'STIXGeneral:italic',
 | 
						|
        'bf': 'STIXGeneral:weight=bold',
 | 
						|
        'bfit': 'STIXGeneral:italic:bold',
 | 
						|
        'nonunirm': 'STIXNonUnicode',
 | 
						|
        'nonuniit': 'STIXNonUnicode:italic',
 | 
						|
        'nonunibf': 'STIXNonUnicode:weight=bold',
 | 
						|
        0: 'STIXGeneral',
 | 
						|
        1: 'STIXSizeOneSym',
 | 
						|
        2: 'STIXSizeTwoSym',
 | 
						|
        3: 'STIXSizeThreeSym',
 | 
						|
        4: 'STIXSizeFourSym',
 | 
						|
        5: 'STIXSizeFiveSym',
 | 
						|
    }
 | 
						|
    _fallback_font = None
 | 
						|
    _sans = False
 | 
						|
 | 
						|
    def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
 | 
						|
        TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags)
 | 
						|
        for key, name in self._fontmap.items():
 | 
						|
            fullpath = findfont(name)
 | 
						|
            self.fontmap[key] = fullpath
 | 
						|
            self.fontmap[name] = fullpath
 | 
						|
 | 
						|
    def _map_virtual_font(self, fontname: str, font_class: str,
 | 
						|
                          uniindex: int) -> tuple[str, int]:
 | 
						|
        # Handle these "fonts" that are actually embedded in
 | 
						|
        # other fonts.
 | 
						|
        font_mapping = stix_virtual_fonts.get(fontname)
 | 
						|
        if (self._sans and font_mapping is None
 | 
						|
                and fontname not in ('regular', 'default')):
 | 
						|
            font_mapping = stix_virtual_fonts['sf']
 | 
						|
            doing_sans_conversion = True
 | 
						|
        else:
 | 
						|
            doing_sans_conversion = False
 | 
						|
 | 
						|
        if isinstance(font_mapping, dict):
 | 
						|
            try:
 | 
						|
                mapping = font_mapping[font_class]
 | 
						|
            except KeyError:
 | 
						|
                mapping = font_mapping['rm']
 | 
						|
        elif isinstance(font_mapping, list):
 | 
						|
            mapping = font_mapping
 | 
						|
        else:
 | 
						|
            mapping = None
 | 
						|
 | 
						|
        if mapping is not None:
 | 
						|
            # Binary search for the source glyph
 | 
						|
            lo = 0
 | 
						|
            hi = len(mapping)
 | 
						|
            while lo < hi:
 | 
						|
                mid = (lo+hi)//2
 | 
						|
                range = mapping[mid]
 | 
						|
                if uniindex < range[0]:
 | 
						|
                    hi = mid
 | 
						|
                elif uniindex <= range[1]:
 | 
						|
                    break
 | 
						|
                else:
 | 
						|
                    lo = mid + 1
 | 
						|
 | 
						|
            if range[0] <= uniindex <= range[1]:
 | 
						|
                uniindex = uniindex - range[0] + range[3]
 | 
						|
                fontname = range[2]
 | 
						|
            elif not doing_sans_conversion:
 | 
						|
                # This will generate a dummy character
 | 
						|
                uniindex = 0x1
 | 
						|
                fontname = mpl.rcParams['mathtext.default']
 | 
						|
 | 
						|
        # Fix some incorrect glyphs.
 | 
						|
        if fontname in ('rm', 'it'):
 | 
						|
            uniindex = stix_glyph_fixes.get(uniindex, uniindex)
 | 
						|
 | 
						|
        # Handle private use area glyphs
 | 
						|
        if fontname in ('it', 'rm', 'bf', 'bfit') and 0xe000 <= uniindex <= 0xf8ff:
 | 
						|
            fontname = 'nonuni' + fontname
 | 
						|
 | 
						|
        return fontname, uniindex
 | 
						|
 | 
						|
    @functools.cache
 | 
						|
    def get_sized_alternatives_for_symbol(  # type: ignore[override]
 | 
						|
            self,
 | 
						|
            fontname: str,
 | 
						|
            sym: str) -> list[tuple[str, str]] | list[tuple[int, str]]:
 | 
						|
        fixes = {
 | 
						|
            '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']',
 | 
						|
            '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}',
 | 
						|
            '>': '\N{MATHEMATICAL RIGHT ANGLE BRACKET}',
 | 
						|
        }
 | 
						|
        sym = fixes.get(sym, sym)
 | 
						|
        try:
 | 
						|
            uniindex = get_unicode_index(sym)
 | 
						|
        except ValueError:
 | 
						|
            return [(fontname, sym)]
 | 
						|
        alternatives = [(i, chr(uniindex)) for i in range(6)
 | 
						|
                        if self._get_font(i).get_char_index(uniindex) != 0]
 | 
						|
        # The largest size of the radical symbol in STIX has incorrect
 | 
						|
        # metrics that cause it to be disconnected from the stem.
 | 
						|
        if sym == r'\__sqrt__':
 | 
						|
            alternatives = alternatives[:-1]
 | 
						|
        return alternatives
 | 
						|
 | 
						|
 | 
						|
class StixSansFonts(StixFonts):
 | 
						|
    """
 | 
						|
    A font handling class for the STIX fonts (that uses sans-serif
 | 
						|
    characters by default).
 | 
						|
    """
 | 
						|
    _sans = True
 | 
						|
 | 
						|
 | 
						|
##############################################################################
 | 
						|
# TeX-LIKE BOX MODEL
 | 
						|
 | 
						|
# The following is based directly on the document 'woven' from the
 | 
						|
# TeX82 source code.  This information is also available in printed
 | 
						|
# form:
 | 
						|
#
 | 
						|
#    Knuth, Donald E.. 1986.  Computers and Typesetting, Volume B:
 | 
						|
#    TeX: The Program.  Addison-Wesley Professional.
 | 
						|
#
 | 
						|
# The most relevant "chapters" are:
 | 
						|
#    Data structures for boxes and their friends
 | 
						|
#    Shipping pages out (ship())
 | 
						|
#    Packaging (hpack() and vpack())
 | 
						|
#    Data structures for math mode
 | 
						|
#    Subroutines for math mode
 | 
						|
#    Typesetting math formulas
 | 
						|
#
 | 
						|
# Many of the docstrings below refer to a numbered "node" in that
 | 
						|
# book, e.g., node123
 | 
						|
#
 | 
						|
# Note that (as TeX) y increases downward, unlike many other parts of
 | 
						|
# matplotlib.
 | 
						|
 | 
						|
# How much text shrinks when going to the next-smallest level.
 | 
						|
SHRINK_FACTOR   = 0.7
 | 
						|
# The number of different sizes of chars to use, beyond which they will not
 | 
						|
# get any smaller
 | 
						|
NUM_SIZE_LEVELS = 6
 | 
						|
 | 
						|
 | 
						|
class FontConstantsBase:
 | 
						|
    """
 | 
						|
    A set of constants that controls how certain things, such as sub-
 | 
						|
    and superscripts are laid out.  These are all metrics that can't
 | 
						|
    be reliably retrieved from the font metrics in the font itself.
 | 
						|
    """
 | 
						|
    # Percentage of x-height of additional horiz. space after sub/superscripts
 | 
						|
    script_space: T.ClassVar[float] = 0.05
 | 
						|
 | 
						|
    # Percentage of x-height that sub/superscripts drop below the baseline
 | 
						|
    subdrop: T.ClassVar[float] = 0.4
 | 
						|
 | 
						|
    # Percentage of x-height that superscripts are raised from the baseline
 | 
						|
    sup1: T.ClassVar[float] = 0.7
 | 
						|
 | 
						|
    # Percentage of x-height that subscripts drop below the baseline
 | 
						|
    sub1: T.ClassVar[float] = 0.3
 | 
						|
 | 
						|
    # Percentage of x-height that subscripts drop below the baseline when a
 | 
						|
    # superscript is present
 | 
						|
    sub2: T.ClassVar[float] = 0.5
 | 
						|
 | 
						|
    # Percentage of x-height that sub/superscripts are offset relative to the
 | 
						|
    # nucleus edge for non-slanted nuclei
 | 
						|
    delta: T.ClassVar[float] = 0.025
 | 
						|
 | 
						|
    # Additional percentage of last character height above 2/3 of the
 | 
						|
    # x-height that superscripts are offset relative to the subscript
 | 
						|
    # for slanted nuclei
 | 
						|
    delta_slanted: T.ClassVar[float] = 0.2
 | 
						|
 | 
						|
    # Percentage of x-height that superscripts and subscripts are offset for
 | 
						|
    # integrals
 | 
						|
    delta_integral: T.ClassVar[float] = 0.1
 | 
						|
 | 
						|
 | 
						|
class ComputerModernFontConstants(FontConstantsBase):
 | 
						|
    script_space = 0.075
 | 
						|
    subdrop = 0.2
 | 
						|
    sup1 = 0.45
 | 
						|
    sub1 = 0.2
 | 
						|
    sub2 = 0.3
 | 
						|
    delta = 0.075
 | 
						|
    delta_slanted = 0.3
 | 
						|
    delta_integral = 0.3
 | 
						|
 | 
						|
 | 
						|
class STIXFontConstants(FontConstantsBase):
 | 
						|
    script_space = 0.1
 | 
						|
    sup1 = 0.8
 | 
						|
    sub2 = 0.6
 | 
						|
    delta = 0.05
 | 
						|
    delta_slanted = 0.3
 | 
						|
    delta_integral = 0.3
 | 
						|
 | 
						|
 | 
						|
class STIXSansFontConstants(FontConstantsBase):
 | 
						|
    script_space = 0.05
 | 
						|
    sup1 = 0.8
 | 
						|
    delta_slanted = 0.6
 | 
						|
    delta_integral = 0.3
 | 
						|
 | 
						|
 | 
						|
class DejaVuSerifFontConstants(FontConstantsBase):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class DejaVuSansFontConstants(FontConstantsBase):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
# Maps font family names to the FontConstantBase subclass to use
 | 
						|
_font_constant_mapping = {
 | 
						|
    'DejaVu Sans': DejaVuSansFontConstants,
 | 
						|
    'DejaVu Sans Mono': DejaVuSansFontConstants,
 | 
						|
    'DejaVu Serif': DejaVuSerifFontConstants,
 | 
						|
    'cmb10': ComputerModernFontConstants,
 | 
						|
    'cmex10': ComputerModernFontConstants,
 | 
						|
    'cmmi10': ComputerModernFontConstants,
 | 
						|
    'cmr10': ComputerModernFontConstants,
 | 
						|
    'cmss10': ComputerModernFontConstants,
 | 
						|
    'cmsy10': ComputerModernFontConstants,
 | 
						|
    'cmtt10': ComputerModernFontConstants,
 | 
						|
    'STIXGeneral': STIXFontConstants,
 | 
						|
    'STIXNonUnicode': STIXFontConstants,
 | 
						|
    'STIXSizeFiveSym': STIXFontConstants,
 | 
						|
    'STIXSizeFourSym': STIXFontConstants,
 | 
						|
    'STIXSizeThreeSym': STIXFontConstants,
 | 
						|
    'STIXSizeTwoSym': STIXFontConstants,
 | 
						|
    'STIXSizeOneSym': STIXFontConstants,
 | 
						|
    # Map the fonts we used to ship, just for good measure
 | 
						|
    'Bitstream Vera Sans': DejaVuSansFontConstants,
 | 
						|
    'Bitstream Vera': DejaVuSansFontConstants,
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
def _get_font_constant_set(state: ParserState) -> type[FontConstantsBase]:
 | 
						|
    constants = _font_constant_mapping.get(
 | 
						|
        state.fontset._get_font(state.font).family_name, FontConstantsBase)
 | 
						|
    # STIX sans isn't really its own fonts, just different code points
 | 
						|
    # in the STIX fonts, so we have to detect this one separately.
 | 
						|
    if (constants is STIXFontConstants and
 | 
						|
            isinstance(state.fontset, StixSansFonts)):
 | 
						|
        return STIXSansFontConstants
 | 
						|
    return constants
 | 
						|
 | 
						|
 | 
						|
class Node:
 | 
						|
    """A node in the TeX box model."""
 | 
						|
 | 
						|
    def __init__(self) -> None:
 | 
						|
        self.size = 0
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return type(self).__name__
 | 
						|
 | 
						|
    def get_kerning(self, next: Node | None) -> float:
 | 
						|
        return 0.0
 | 
						|
 | 
						|
    def shrink(self) -> None:
 | 
						|
        """
 | 
						|
        Shrinks one level smaller.  There are only three levels of
 | 
						|
        sizes, after which things will no longer get smaller.
 | 
						|
        """
 | 
						|
        self.size += 1
 | 
						|
 | 
						|
    def render(self, output: Output, x: float, y: float) -> None:
 | 
						|
        """Render this node."""
 | 
						|
 | 
						|
 | 
						|
class Box(Node):
 | 
						|
    """A node with a physical location."""
 | 
						|
 | 
						|
    def __init__(self, width: float, height: float, depth: float) -> None:
 | 
						|
        super().__init__()
 | 
						|
        self.width  = width
 | 
						|
        self.height = height
 | 
						|
        self.depth  = depth
 | 
						|
 | 
						|
    def shrink(self) -> None:
 | 
						|
        super().shrink()
 | 
						|
        if self.size < NUM_SIZE_LEVELS:
 | 
						|
            self.width  *= SHRINK_FACTOR
 | 
						|
            self.height *= SHRINK_FACTOR
 | 
						|
            self.depth  *= SHRINK_FACTOR
 | 
						|
 | 
						|
    def render(self, output: Output,  # type: ignore[override]
 | 
						|
               x1: float, y1: float, x2: float, y2: float) -> None:
 | 
						|
        pass
 | 
						|
 | 
						|
 | 
						|
class Vbox(Box):
 | 
						|
    """A box with only height (zero width)."""
 | 
						|
 | 
						|
    def __init__(self, height: float, depth: float):
 | 
						|
        super().__init__(0., height, depth)
 | 
						|
 | 
						|
 | 
						|
class Hbox(Box):
 | 
						|
    """A box with only width (zero height and depth)."""
 | 
						|
 | 
						|
    def __init__(self, width: float):
 | 
						|
        super().__init__(width, 0., 0.)
 | 
						|
 | 
						|
 | 
						|
class Char(Node):
 | 
						|
    """
 | 
						|
    A single character.
 | 
						|
 | 
						|
    Unlike TeX, the font information and metrics are stored with each `Char`
 | 
						|
    to make it easier to lookup the font metrics when needed.  Note that TeX
 | 
						|
    boxes have a width, height, and depth, unlike Type1 and TrueType which use
 | 
						|
    a full bounding box and an advance in the x-direction.  The metrics must
 | 
						|
    be converted to the TeX model, and the advance (if different from width)
 | 
						|
    must be converted into a `Kern` node when the `Char` is added to its parent
 | 
						|
    `Hlist`.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, c: str, state: ParserState):
 | 
						|
        super().__init__()
 | 
						|
        self.c = c
 | 
						|
        self.fontset = state.fontset
 | 
						|
        self.font = state.font
 | 
						|
        self.font_class = state.font_class
 | 
						|
        self.fontsize = state.fontsize
 | 
						|
        self.dpi = state.dpi
 | 
						|
        # The real width, height and depth will be set during the
 | 
						|
        # pack phase, after we know the real fontsize
 | 
						|
        self._update_metrics()
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return '`%s`' % self.c
 | 
						|
 | 
						|
    def _update_metrics(self) -> None:
 | 
						|
        metrics = self._metrics = self.fontset.get_metrics(
 | 
						|
            self.font, self.font_class, self.c, self.fontsize, self.dpi)
 | 
						|
        if self.c == ' ':
 | 
						|
            self.width = metrics.advance
 | 
						|
        else:
 | 
						|
            self.width = metrics.width
 | 
						|
        self.height = metrics.iceberg
 | 
						|
        self.depth = -(metrics.iceberg - metrics.height)
 | 
						|
 | 
						|
    def is_slanted(self) -> bool:
 | 
						|
        return self._metrics.slanted
 | 
						|
 | 
						|
    def get_kerning(self, next: Node | None) -> float:
 | 
						|
        """
 | 
						|
        Return the amount of kerning between this and the given character.
 | 
						|
 | 
						|
        This method is called when characters are strung together into `Hlist`
 | 
						|
        to create `Kern` nodes.
 | 
						|
        """
 | 
						|
        advance = self._metrics.advance - self.width
 | 
						|
        kern = 0.
 | 
						|
        if isinstance(next, Char):
 | 
						|
            kern = self.fontset.get_kern(
 | 
						|
                self.font, self.font_class, self.c, self.fontsize,
 | 
						|
                next.font, next.font_class, next.c, next.fontsize,
 | 
						|
                self.dpi)
 | 
						|
        return advance + kern
 | 
						|
 | 
						|
    def render(self, output: Output, x: float, y: float) -> None:
 | 
						|
        self.fontset.render_glyph(
 | 
						|
            output, x, y,
 | 
						|
            self.font, self.font_class, self.c, self.fontsize, self.dpi)
 | 
						|
 | 
						|
    def shrink(self) -> None:
 | 
						|
        super().shrink()
 | 
						|
        if self.size < NUM_SIZE_LEVELS:
 | 
						|
            self.fontsize *= SHRINK_FACTOR
 | 
						|
            self.width    *= SHRINK_FACTOR
 | 
						|
            self.height   *= SHRINK_FACTOR
 | 
						|
            self.depth    *= SHRINK_FACTOR
 | 
						|
 | 
						|
 | 
						|
class Accent(Char):
 | 
						|
    """
 | 
						|
    The font metrics need to be dealt with differently for accents,
 | 
						|
    since they are already offset correctly from the baseline in
 | 
						|
    TrueType fonts.
 | 
						|
    """
 | 
						|
    def _update_metrics(self) -> None:
 | 
						|
        metrics = self._metrics = self.fontset.get_metrics(
 | 
						|
            self.font, self.font_class, self.c, self.fontsize, self.dpi)
 | 
						|
        self.width = metrics.xmax - metrics.xmin
 | 
						|
        self.height = metrics.ymax - metrics.ymin
 | 
						|
        self.depth = 0
 | 
						|
 | 
						|
    def shrink(self) -> None:
 | 
						|
        super().shrink()
 | 
						|
        self._update_metrics()
 | 
						|
 | 
						|
    def render(self, output: Output, x: float, y: float) -> None:
 | 
						|
        self.fontset.render_glyph(
 | 
						|
            output, x - self._metrics.xmin, y + self._metrics.ymin,
 | 
						|
            self.font, self.font_class, self.c, self.fontsize, self.dpi)
 | 
						|
 | 
						|
 | 
						|
class List(Box):
 | 
						|
    """A list of nodes (either horizontal or vertical)."""
 | 
						|
 | 
						|
    def __init__(self, elements: T.Sequence[Node]):
 | 
						|
        super().__init__(0., 0., 0.)
 | 
						|
        self.shift_amount = 0.   # An arbitrary offset
 | 
						|
        self.children = [*elements]  # The child nodes of this list
 | 
						|
        # The following parameters are set in the vpack and hpack functions
 | 
						|
        self.glue_set     = 0.   # The glue setting of this list
 | 
						|
        self.glue_sign    = 0    # 0: normal, -1: shrinking, 1: stretching
 | 
						|
        self.glue_order   = 0    # The order of infinity (0 - 3) for the glue
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return '{}<w={:.02f} h={:.02f} d={:.02f} s={:.02f}>[{}]'.format(
 | 
						|
            super().__repr__(),
 | 
						|
            self.width, self.height,
 | 
						|
            self.depth, self.shift_amount,
 | 
						|
            ', '.join([repr(x) for x in self.children]))
 | 
						|
 | 
						|
    def _set_glue(self, x: float, sign: int, totals: list[float],
 | 
						|
                  error_type: str) -> None:
 | 
						|
        self.glue_order = o = next(
 | 
						|
            # Highest order of glue used by the members of this list.
 | 
						|
            (i for i in range(len(totals))[::-1] if totals[i] != 0), 0)
 | 
						|
        self.glue_sign = sign
 | 
						|
        if totals[o] != 0.:
 | 
						|
            self.glue_set = x / totals[o]
 | 
						|
        else:
 | 
						|
            self.glue_sign = 0
 | 
						|
            self.glue_ratio = 0.
 | 
						|
        if o == 0:
 | 
						|
            if len(self.children):
 | 
						|
                _log.warning("%s %s: %r",
 | 
						|
                             error_type, type(self).__name__, self)
 | 
						|
 | 
						|
    def shrink(self) -> None:
 | 
						|
        for child in self.children:
 | 
						|
            child.shrink()
 | 
						|
        super().shrink()
 | 
						|
        if self.size < NUM_SIZE_LEVELS:
 | 
						|
            self.shift_amount *= SHRINK_FACTOR
 | 
						|
            self.glue_set     *= SHRINK_FACTOR
 | 
						|
 | 
						|
 | 
						|
class Hlist(List):
 | 
						|
    """A horizontal list of boxes."""
 | 
						|
 | 
						|
    def __init__(self, elements: T.Sequence[Node], w: float = 0.0,
 | 
						|
                 m: T.Literal['additional', 'exactly'] = 'additional',
 | 
						|
                 do_kern: bool = True):
 | 
						|
        super().__init__(elements)
 | 
						|
        if do_kern:
 | 
						|
            self.kern()
 | 
						|
        self.hpack(w=w, m=m)
 | 
						|
 | 
						|
    def kern(self) -> None:
 | 
						|
        """
 | 
						|
        Insert `Kern` nodes between `Char` nodes to set kerning.
 | 
						|
 | 
						|
        The `Char` nodes themselves determine the amount of kerning they need
 | 
						|
        (in `~Char.get_kerning`), and this function just creates the correct
 | 
						|
        linked list.
 | 
						|
        """
 | 
						|
        new_children = []
 | 
						|
        num_children = len(self.children)
 | 
						|
        if num_children:
 | 
						|
            for i in range(num_children):
 | 
						|
                elem = self.children[i]
 | 
						|
                if i < num_children - 1:
 | 
						|
                    next = self.children[i + 1]
 | 
						|
                else:
 | 
						|
                    next = None
 | 
						|
 | 
						|
                new_children.append(elem)
 | 
						|
                kerning_distance = elem.get_kerning(next)
 | 
						|
                if kerning_distance != 0.:
 | 
						|
                    kern = Kern(kerning_distance)
 | 
						|
                    new_children.append(kern)
 | 
						|
            self.children = new_children
 | 
						|
 | 
						|
    def hpack(self, w: float = 0.0,
 | 
						|
              m: T.Literal['additional', 'exactly'] = 'additional') -> None:
 | 
						|
        r"""
 | 
						|
        Compute the dimensions of the resulting boxes, and adjust the glue if
 | 
						|
        one of those dimensions is pre-specified.  The computed sizes normally
 | 
						|
        enclose all of the material inside the new box; but some items may
 | 
						|
        stick out if negative glue is used, if the box is overfull, or if a
 | 
						|
        ``\vbox`` includes other boxes that have been shifted left.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        w : float, default: 0
 | 
						|
            A width.
 | 
						|
        m : {'exactly', 'additional'}, default: 'additional'
 | 
						|
            Whether to produce a box whose width is 'exactly' *w*; or a box
 | 
						|
            with the natural width of the contents, plus *w* ('additional').
 | 
						|
 | 
						|
        Notes
 | 
						|
        -----
 | 
						|
        The defaults produce a box with the natural width of the contents.
 | 
						|
        """
 | 
						|
        # I don't know why these get reset in TeX.  Shift_amount is pretty
 | 
						|
        # much useless if we do.
 | 
						|
        # self.shift_amount = 0.
 | 
						|
        h = 0.
 | 
						|
        d = 0.
 | 
						|
        x = 0.
 | 
						|
        total_stretch = [0.] * 4
 | 
						|
        total_shrink = [0.] * 4
 | 
						|
        for p in self.children:
 | 
						|
            if isinstance(p, Char):
 | 
						|
                x += p.width
 | 
						|
                h = max(h, p.height)
 | 
						|
                d = max(d, p.depth)
 | 
						|
            elif isinstance(p, Box):
 | 
						|
                x += p.width
 | 
						|
                if not np.isinf(p.height) and not np.isinf(p.depth):
 | 
						|
                    s = getattr(p, 'shift_amount', 0.)
 | 
						|
                    h = max(h, p.height - s)
 | 
						|
                    d = max(d, p.depth + s)
 | 
						|
            elif isinstance(p, Glue):
 | 
						|
                glue_spec = p.glue_spec
 | 
						|
                x += glue_spec.width
 | 
						|
                total_stretch[glue_spec.stretch_order] += glue_spec.stretch
 | 
						|
                total_shrink[glue_spec.shrink_order] += glue_spec.shrink
 | 
						|
            elif isinstance(p, Kern):
 | 
						|
                x += p.width
 | 
						|
        self.height = h
 | 
						|
        self.depth = d
 | 
						|
 | 
						|
        if m == 'additional':
 | 
						|
            w += x
 | 
						|
        self.width = w
 | 
						|
        x = w - x
 | 
						|
 | 
						|
        if x == 0.:
 | 
						|
            self.glue_sign = 0
 | 
						|
            self.glue_order = 0
 | 
						|
            self.glue_ratio = 0.
 | 
						|
            return
 | 
						|
        if x > 0.:
 | 
						|
            self._set_glue(x, 1, total_stretch, "Overful")
 | 
						|
        else:
 | 
						|
            self._set_glue(x, -1, total_shrink, "Underful")
 | 
						|
 | 
						|
 | 
						|
class Vlist(List):
 | 
						|
    """A vertical list of boxes."""
 | 
						|
 | 
						|
    def __init__(self, elements: T.Sequence[Node], h: float = 0.0,
 | 
						|
                 m: T.Literal['additional', 'exactly'] = 'additional'):
 | 
						|
        super().__init__(elements)
 | 
						|
        self.vpack(h=h, m=m)
 | 
						|
 | 
						|
    def vpack(self, h: float = 0.0,
 | 
						|
              m: T.Literal['additional', 'exactly'] = 'additional',
 | 
						|
              l: float = np.inf) -> None:
 | 
						|
        """
 | 
						|
        Compute the dimensions of the resulting boxes, and to adjust the glue
 | 
						|
        if one of those dimensions is pre-specified.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        h : float, default: 0
 | 
						|
            A height.
 | 
						|
        m : {'exactly', 'additional'}, default: 'additional'
 | 
						|
            Whether to produce a box whose height is 'exactly' *h*; or a box
 | 
						|
            with the natural height of the contents, plus *h* ('additional').
 | 
						|
        l : float, default: np.inf
 | 
						|
            The maximum height.
 | 
						|
 | 
						|
        Notes
 | 
						|
        -----
 | 
						|
        The defaults produce a box with the natural height of the contents.
 | 
						|
        """
 | 
						|
        # I don't know why these get reset in TeX.  Shift_amount is pretty
 | 
						|
        # much useless if we do.
 | 
						|
        # self.shift_amount = 0.
 | 
						|
        w = 0.
 | 
						|
        d = 0.
 | 
						|
        x = 0.
 | 
						|
        total_stretch = [0.] * 4
 | 
						|
        total_shrink = [0.] * 4
 | 
						|
        for p in self.children:
 | 
						|
            if isinstance(p, Box):
 | 
						|
                x += d + p.height
 | 
						|
                d = p.depth
 | 
						|
                if not np.isinf(p.width):
 | 
						|
                    s = getattr(p, 'shift_amount', 0.)
 | 
						|
                    w = max(w, p.width + s)
 | 
						|
            elif isinstance(p, Glue):
 | 
						|
                x += d
 | 
						|
                d = 0.
 | 
						|
                glue_spec = p.glue_spec
 | 
						|
                x += glue_spec.width
 | 
						|
                total_stretch[glue_spec.stretch_order] += glue_spec.stretch
 | 
						|
                total_shrink[glue_spec.shrink_order] += glue_spec.shrink
 | 
						|
            elif isinstance(p, Kern):
 | 
						|
                x += d + p.width
 | 
						|
                d = 0.
 | 
						|
            elif isinstance(p, Char):
 | 
						|
                raise RuntimeError(
 | 
						|
                    "Internal mathtext error: Char node found in Vlist")
 | 
						|
 | 
						|
        self.width = w
 | 
						|
        if d > l:
 | 
						|
            x += d - l
 | 
						|
            self.depth = l
 | 
						|
        else:
 | 
						|
            self.depth = d
 | 
						|
 | 
						|
        if m == 'additional':
 | 
						|
            h += x
 | 
						|
        self.height = h
 | 
						|
        x = h - x
 | 
						|
 | 
						|
        if x == 0:
 | 
						|
            self.glue_sign = 0
 | 
						|
            self.glue_order = 0
 | 
						|
            self.glue_ratio = 0.
 | 
						|
            return
 | 
						|
 | 
						|
        if x > 0.:
 | 
						|
            self._set_glue(x, 1, total_stretch, "Overful")
 | 
						|
        else:
 | 
						|
            self._set_glue(x, -1, total_shrink, "Underful")
 | 
						|
 | 
						|
 | 
						|
class Rule(Box):
 | 
						|
    """
 | 
						|
    A solid black rectangle.
 | 
						|
 | 
						|
    It has *width*, *depth*, and *height* fields just as in an `Hlist`.
 | 
						|
    However, if any of these dimensions is inf, the actual value will be
 | 
						|
    determined by running the rule up to the boundary of the innermost
 | 
						|
    enclosing box.  This is called a "running dimension".  The width is never
 | 
						|
    running in an `Hlist`; the height and depth are never running in a `Vlist`.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, width: float, height: float, depth: float, state: ParserState):
 | 
						|
        super().__init__(width, height, depth)
 | 
						|
        self.fontset = state.fontset
 | 
						|
 | 
						|
    def render(self, output: Output,  # type: ignore[override]
 | 
						|
               x: float, y: float, w: float, h: float) -> None:
 | 
						|
        self.fontset.render_rect_filled(output, x, y, x + w, y + h)
 | 
						|
 | 
						|
 | 
						|
class Hrule(Rule):
 | 
						|
    """Convenience class to create a horizontal rule."""
 | 
						|
 | 
						|
    def __init__(self, state: ParserState, thickness: float | None = None):
 | 
						|
        if thickness is None:
 | 
						|
            thickness = state.get_current_underline_thickness()
 | 
						|
        height = depth = thickness * 0.5
 | 
						|
        super().__init__(np.inf, height, depth, state)
 | 
						|
 | 
						|
 | 
						|
class Vrule(Rule):
 | 
						|
    """Convenience class to create a vertical rule."""
 | 
						|
 | 
						|
    def __init__(self, state: ParserState):
 | 
						|
        thickness = state.get_current_underline_thickness()
 | 
						|
        super().__init__(thickness, np.inf, np.inf, state)
 | 
						|
 | 
						|
 | 
						|
class _GlueSpec(NamedTuple):
 | 
						|
    width: float
 | 
						|
    stretch: float
 | 
						|
    stretch_order: int
 | 
						|
    shrink: float
 | 
						|
    shrink_order: int
 | 
						|
 | 
						|
 | 
						|
_GlueSpec._named = {  # type: ignore[attr-defined]
 | 
						|
    'fil':         _GlueSpec(0., 1., 1, 0., 0),
 | 
						|
    'fill':        _GlueSpec(0., 1., 2, 0., 0),
 | 
						|
    'filll':       _GlueSpec(0., 1., 3, 0., 0),
 | 
						|
    'neg_fil':     _GlueSpec(0., 0., 0, 1., 1),
 | 
						|
    'neg_fill':    _GlueSpec(0., 0., 0, 1., 2),
 | 
						|
    'neg_filll':   _GlueSpec(0., 0., 0, 1., 3),
 | 
						|
    'empty':       _GlueSpec(0., 0., 0, 0., 0),
 | 
						|
    'ss':          _GlueSpec(0., 1., 1, -1., 1),
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
class Glue(Node):
 | 
						|
    """
 | 
						|
    Most of the information in this object is stored in the underlying
 | 
						|
    ``_GlueSpec`` class, which is shared between multiple glue objects.
 | 
						|
    (This is a memory optimization which probably doesn't matter anymore, but
 | 
						|
    it's easier to stick to what TeX does.)
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self,
 | 
						|
                 glue_type: _GlueSpec | T.Literal["fil", "fill", "filll",
 | 
						|
                                                  "neg_fil", "neg_fill", "neg_filll",
 | 
						|
                                                  "empty", "ss"]):
 | 
						|
        super().__init__()
 | 
						|
        if isinstance(glue_type, str):
 | 
						|
            glue_spec = _GlueSpec._named[glue_type]  # type: ignore[attr-defined]
 | 
						|
        elif isinstance(glue_type, _GlueSpec):
 | 
						|
            glue_spec = glue_type
 | 
						|
        else:
 | 
						|
            raise ValueError("glue_type must be a glue spec name or instance")
 | 
						|
        self.glue_spec = glue_spec
 | 
						|
 | 
						|
    def shrink(self) -> None:
 | 
						|
        super().shrink()
 | 
						|
        if self.size < NUM_SIZE_LEVELS:
 | 
						|
            g = self.glue_spec
 | 
						|
            self.glue_spec = g._replace(width=g.width * SHRINK_FACTOR)
 | 
						|
 | 
						|
 | 
						|
class HCentered(Hlist):
 | 
						|
    """
 | 
						|
    A convenience class to create an `Hlist` whose contents are
 | 
						|
    centered within its enclosing box.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, elements: list[Node]):
 | 
						|
        super().__init__([Glue('ss'), *elements, Glue('ss')], do_kern=False)
 | 
						|
 | 
						|
 | 
						|
class VCentered(Vlist):
 | 
						|
    """
 | 
						|
    A convenience class to create a `Vlist` whose contents are
 | 
						|
    centered within its enclosing box.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, elements: list[Node]):
 | 
						|
        super().__init__([Glue('ss'), *elements, Glue('ss')])
 | 
						|
 | 
						|
 | 
						|
class Kern(Node):
 | 
						|
    """
 | 
						|
    A `Kern` node has a width field to specify a (normally
 | 
						|
    negative) amount of spacing. This spacing correction appears in
 | 
						|
    horizontal lists between letters like A and V when the font
 | 
						|
    designer said that it looks better to move them closer together or
 | 
						|
    further apart. A kern node can also appear in a vertical list,
 | 
						|
    when its *width* denotes additional spacing in the vertical
 | 
						|
    direction.
 | 
						|
    """
 | 
						|
 | 
						|
    height = 0
 | 
						|
    depth = 0
 | 
						|
 | 
						|
    def __init__(self, width: float):
 | 
						|
        super().__init__()
 | 
						|
        self.width = width
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return "k%.02f" % self.width
 | 
						|
 | 
						|
    def shrink(self) -> None:
 | 
						|
        super().shrink()
 | 
						|
        if self.size < NUM_SIZE_LEVELS:
 | 
						|
            self.width *= SHRINK_FACTOR
 | 
						|
 | 
						|
 | 
						|
class AutoHeightChar(Hlist):
 | 
						|
    """
 | 
						|
    A character as close to the given height and depth as possible.
 | 
						|
 | 
						|
    When using a font with multiple height versions of some characters (such as
 | 
						|
    the BaKoMa fonts), the correct glyph will be selected, otherwise this will
 | 
						|
    always just return a scaled version of the glyph.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, c: str, height: float, depth: float, state: ParserState,
 | 
						|
                 always: bool = False, factor: float | None = None):
 | 
						|
        alternatives = state.fontset.get_sized_alternatives_for_symbol(
 | 
						|
            state.font, c)
 | 
						|
 | 
						|
        xHeight = state.fontset.get_xheight(
 | 
						|
            state.font, state.fontsize, state.dpi)
 | 
						|
 | 
						|
        state = state.copy()
 | 
						|
        target_total = height + depth
 | 
						|
        for fontname, sym in alternatives:
 | 
						|
            state.font = fontname
 | 
						|
            char = Char(sym, state)
 | 
						|
            # Ensure that size 0 is chosen when the text is regular sized but
 | 
						|
            # with descender glyphs by subtracting 0.2 * xHeight
 | 
						|
            if char.height + char.depth >= target_total - 0.2 * xHeight:
 | 
						|
                break
 | 
						|
 | 
						|
        shift = 0.0
 | 
						|
        if state.font != 0 or len(alternatives) == 1:
 | 
						|
            if factor is None:
 | 
						|
                factor = target_total / (char.height + char.depth)
 | 
						|
            state.fontsize *= factor
 | 
						|
            char = Char(sym, state)
 | 
						|
 | 
						|
            shift = (depth - char.depth)
 | 
						|
 | 
						|
        super().__init__([char])
 | 
						|
        self.shift_amount = shift
 | 
						|
 | 
						|
 | 
						|
class AutoWidthChar(Hlist):
 | 
						|
    """
 | 
						|
    A character as close to the given width as possible.
 | 
						|
 | 
						|
    When using a font with multiple width versions of some characters (such as
 | 
						|
    the BaKoMa fonts), the correct glyph will be selected, otherwise this will
 | 
						|
    always just return a scaled version of the glyph.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, c: str, width: float, state: ParserState, always: bool = False,
 | 
						|
                 char_class: type[Char] = Char):
 | 
						|
        alternatives = state.fontset.get_sized_alternatives_for_symbol(
 | 
						|
            state.font, c)
 | 
						|
 | 
						|
        state = state.copy()
 | 
						|
        for fontname, sym in alternatives:
 | 
						|
            state.font = fontname
 | 
						|
            char = char_class(sym, state)
 | 
						|
            if char.width >= width:
 | 
						|
                break
 | 
						|
 | 
						|
        factor = width / char.width
 | 
						|
        state.fontsize *= factor
 | 
						|
        char = char_class(sym, state)
 | 
						|
 | 
						|
        super().__init__([char])
 | 
						|
        self.width = char.width
 | 
						|
 | 
						|
 | 
						|
def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output:
 | 
						|
    """
 | 
						|
    Ship out *box* at offset *xy*, converting it to an `Output`.
 | 
						|
 | 
						|
    Since boxes can be inside of boxes inside of boxes, the main work of `ship`
 | 
						|
    is done by two mutually recursive routines, `hlist_out` and `vlist_out`,
 | 
						|
    which traverse the `Hlist` nodes and `Vlist` nodes inside of horizontal
 | 
						|
    and vertical boxes.  The global variables used in TeX to store state as it
 | 
						|
    processes have become local variables here.
 | 
						|
    """
 | 
						|
    ox, oy = xy
 | 
						|
    cur_v = 0.
 | 
						|
    cur_h = 0.
 | 
						|
    off_h = ox
 | 
						|
    off_v = oy + box.height
 | 
						|
    output = Output(box)
 | 
						|
 | 
						|
    def clamp(value: float) -> float:
 | 
						|
        return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value
 | 
						|
 | 
						|
    def hlist_out(box: Hlist) -> None:
 | 
						|
        nonlocal cur_v, cur_h, off_h, off_v
 | 
						|
 | 
						|
        cur_g = 0
 | 
						|
        cur_glue = 0.
 | 
						|
        glue_order = box.glue_order
 | 
						|
        glue_sign = box.glue_sign
 | 
						|
        base_line = cur_v
 | 
						|
        left_edge = cur_h
 | 
						|
 | 
						|
        for p in box.children:
 | 
						|
            if isinstance(p, Char):
 | 
						|
                p.render(output, cur_h + off_h, cur_v + off_v)
 | 
						|
                cur_h += p.width
 | 
						|
            elif isinstance(p, Kern):
 | 
						|
                cur_h += p.width
 | 
						|
            elif isinstance(p, List):
 | 
						|
                # node623
 | 
						|
                if len(p.children) == 0:
 | 
						|
                    cur_h += p.width
 | 
						|
                else:
 | 
						|
                    edge = cur_h
 | 
						|
                    cur_v = base_line + p.shift_amount
 | 
						|
                    if isinstance(p, Hlist):
 | 
						|
                        hlist_out(p)
 | 
						|
                    elif isinstance(p, Vlist):
 | 
						|
                        # p.vpack(box.height + box.depth, 'exactly')
 | 
						|
                        vlist_out(p)
 | 
						|
                    else:
 | 
						|
                        assert False, "unreachable code"
 | 
						|
                    cur_h = edge + p.width
 | 
						|
                    cur_v = base_line
 | 
						|
            elif isinstance(p, Box):
 | 
						|
                # node624
 | 
						|
                rule_height = p.height
 | 
						|
                rule_depth = p.depth
 | 
						|
                rule_width = p.width
 | 
						|
                if np.isinf(rule_height):
 | 
						|
                    rule_height = box.height
 | 
						|
                if np.isinf(rule_depth):
 | 
						|
                    rule_depth = box.depth
 | 
						|
                if rule_height > 0 and rule_width > 0:
 | 
						|
                    cur_v = base_line + rule_depth
 | 
						|
                    p.render(output,
 | 
						|
                             cur_h + off_h, cur_v + off_v,
 | 
						|
                             rule_width, rule_height)
 | 
						|
                    cur_v = base_line
 | 
						|
                cur_h += rule_width
 | 
						|
            elif isinstance(p, Glue):
 | 
						|
                # node625
 | 
						|
                glue_spec = p.glue_spec
 | 
						|
                rule_width = glue_spec.width - cur_g
 | 
						|
                if glue_sign != 0:  # normal
 | 
						|
                    if glue_sign == 1:  # stretching
 | 
						|
                        if glue_spec.stretch_order == glue_order:
 | 
						|
                            cur_glue += glue_spec.stretch
 | 
						|
                            cur_g = round(clamp(box.glue_set * cur_glue))
 | 
						|
                    elif glue_spec.shrink_order == glue_order:
 | 
						|
                        cur_glue += glue_spec.shrink
 | 
						|
                        cur_g = round(clamp(box.glue_set * cur_glue))
 | 
						|
                rule_width += cur_g
 | 
						|
                cur_h += rule_width
 | 
						|
 | 
						|
    def vlist_out(box: Vlist) -> None:
 | 
						|
        nonlocal cur_v, cur_h, off_h, off_v
 | 
						|
 | 
						|
        cur_g = 0
 | 
						|
        cur_glue = 0.
 | 
						|
        glue_order = box.glue_order
 | 
						|
        glue_sign = box.glue_sign
 | 
						|
        left_edge = cur_h
 | 
						|
        cur_v -= box.height
 | 
						|
        top_edge = cur_v
 | 
						|
 | 
						|
        for p in box.children:
 | 
						|
            if isinstance(p, Kern):
 | 
						|
                cur_v += p.width
 | 
						|
            elif isinstance(p, List):
 | 
						|
                if len(p.children) == 0:
 | 
						|
                    cur_v += p.height + p.depth
 | 
						|
                else:
 | 
						|
                    cur_v += p.height
 | 
						|
                    cur_h = left_edge + p.shift_amount
 | 
						|
                    save_v = cur_v
 | 
						|
                    p.width = box.width
 | 
						|
                    if isinstance(p, Hlist):
 | 
						|
                        hlist_out(p)
 | 
						|
                    elif isinstance(p, Vlist):
 | 
						|
                        vlist_out(p)
 | 
						|
                    else:
 | 
						|
                        assert False, "unreachable code"
 | 
						|
                    cur_v = save_v + p.depth
 | 
						|
                    cur_h = left_edge
 | 
						|
            elif isinstance(p, Box):
 | 
						|
                rule_height = p.height
 | 
						|
                rule_depth = p.depth
 | 
						|
                rule_width = p.width
 | 
						|
                if np.isinf(rule_width):
 | 
						|
                    rule_width = box.width
 | 
						|
                rule_height += rule_depth
 | 
						|
                if rule_height > 0 and rule_depth > 0:
 | 
						|
                    cur_v += rule_height
 | 
						|
                    p.render(output,
 | 
						|
                             cur_h + off_h, cur_v + off_v,
 | 
						|
                             rule_width, rule_height)
 | 
						|
            elif isinstance(p, Glue):
 | 
						|
                glue_spec = p.glue_spec
 | 
						|
                rule_height = glue_spec.width - cur_g
 | 
						|
                if glue_sign != 0:  # normal
 | 
						|
                    if glue_sign == 1:  # stretching
 | 
						|
                        if glue_spec.stretch_order == glue_order:
 | 
						|
                            cur_glue += glue_spec.stretch
 | 
						|
                            cur_g = round(clamp(box.glue_set * cur_glue))
 | 
						|
                    elif glue_spec.shrink_order == glue_order:  # shrinking
 | 
						|
                        cur_glue += glue_spec.shrink
 | 
						|
                        cur_g = round(clamp(box.glue_set * cur_glue))
 | 
						|
                rule_height += cur_g
 | 
						|
                cur_v += rule_height
 | 
						|
            elif isinstance(p, Char):
 | 
						|
                raise RuntimeError(
 | 
						|
                    "Internal mathtext error: Char node found in vlist")
 | 
						|
 | 
						|
    assert isinstance(box, Hlist)
 | 
						|
    hlist_out(box)
 | 
						|
    return output
 | 
						|
 | 
						|
 | 
						|
##############################################################################
 | 
						|
# PARSER
 | 
						|
 | 
						|
 | 
						|
def Error(msg: str) -> ParserElement:
 | 
						|
    """Helper class to raise parser errors."""
 | 
						|
    def raise_error(s: str, loc: int, toks: ParseResults) -> T.Any:
 | 
						|
        raise ParseFatalException(s, loc, msg)
 | 
						|
 | 
						|
    return Empty().set_parse_action(raise_error)
 | 
						|
 | 
						|
 | 
						|
class ParserState:
 | 
						|
    """
 | 
						|
    Parser state.
 | 
						|
 | 
						|
    States are pushed and popped from a stack as necessary, and the "current"
 | 
						|
    state is always at the top of the stack.
 | 
						|
 | 
						|
    Upon entering and leaving a group { } or math/non-math, the stack is pushed
 | 
						|
    and popped accordingly.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, fontset: Fonts, font: str, font_class: str, fontsize: float,
 | 
						|
                 dpi: float):
 | 
						|
        self.fontset = fontset
 | 
						|
        self._font = font
 | 
						|
        self.font_class = font_class
 | 
						|
        self.fontsize = fontsize
 | 
						|
        self.dpi = dpi
 | 
						|
 | 
						|
    def copy(self) -> ParserState:
 | 
						|
        return copy.copy(self)
 | 
						|
 | 
						|
    @property
 | 
						|
    def font(self) -> str:
 | 
						|
        return self._font
 | 
						|
 | 
						|
    @font.setter
 | 
						|
    def font(self, name: str) -> None:
 | 
						|
        if name in ('rm', 'it', 'bf', 'bfit'):
 | 
						|
            self.font_class = name
 | 
						|
        self._font = name
 | 
						|
 | 
						|
    def get_current_underline_thickness(self) -> float:
 | 
						|
        """Return the underline thickness for this state."""
 | 
						|
        return self.fontset.get_underline_thickness(
 | 
						|
            self.font, self.fontsize, self.dpi)
 | 
						|
 | 
						|
 | 
						|
def cmd(expr: str, args: ParserElement) -> ParserElement:
 | 
						|
    r"""
 | 
						|
    Helper to define TeX commands.
 | 
						|
 | 
						|
    ``cmd("\cmd", args)`` is equivalent to
 | 
						|
    ``"\cmd" - (args | Error("Expected \cmd{arg}{...}"))`` where the names in
 | 
						|
    the error message are taken from element names in *args*.  If *expr*
 | 
						|
    already includes arguments (e.g. "\cmd{arg}{...}"), then they are stripped
 | 
						|
    when constructing the parse element, but kept (and *expr* is used as is) in
 | 
						|
    the error message.
 | 
						|
    """
 | 
						|
 | 
						|
    def names(elt: ParserElement) -> T.Generator[str, None, None]:
 | 
						|
        if isinstance(elt, ParseExpression):
 | 
						|
            for expr in elt.exprs:
 | 
						|
                yield from names(expr)
 | 
						|
        elif elt.resultsName:
 | 
						|
            yield elt.resultsName
 | 
						|
 | 
						|
    csname = expr.split("{", 1)[0]
 | 
						|
    err = (csname + "".join("{%s}" % name for name in names(args))
 | 
						|
           if expr == csname else expr)
 | 
						|
    return csname - (args | Error(f"Expected {err}"))
 | 
						|
 | 
						|
 | 
						|
class Parser:
 | 
						|
    """
 | 
						|
    A pyparsing-based parser for strings containing math expressions.
 | 
						|
 | 
						|
    Raw text may also appear outside of pairs of ``$``.
 | 
						|
 | 
						|
    The grammar is based directly on that in TeX, though it cuts a few corners.
 | 
						|
    """
 | 
						|
 | 
						|
    class _MathStyle(enum.Enum):
 | 
						|
        DISPLAYSTYLE = 0
 | 
						|
        TEXTSTYLE = 1
 | 
						|
        SCRIPTSTYLE = 2
 | 
						|
        SCRIPTSCRIPTSTYLE = 3
 | 
						|
 | 
						|
    _binary_operators = set(
 | 
						|
      '+ * - \N{MINUS SIGN}'
 | 
						|
      r'''
 | 
						|
      \pm             \sqcap                   \rhd
 | 
						|
      \mp             \sqcup                   \unlhd
 | 
						|
      \times          \vee                     \unrhd
 | 
						|
      \div            \wedge                   \oplus
 | 
						|
      \ast            \setminus                \ominus
 | 
						|
      \star           \wr                      \otimes
 | 
						|
      \circ           \diamond                 \oslash
 | 
						|
      \bullet         \bigtriangleup           \odot
 | 
						|
      \cdot           \bigtriangledown         \bigcirc
 | 
						|
      \cap            \triangleleft            \dagger
 | 
						|
      \cup            \triangleright           \ddagger
 | 
						|
      \uplus          \lhd                     \amalg
 | 
						|
      \dotplus        \dotminus                \Cap
 | 
						|
      \Cup            \barwedge                \boxdot
 | 
						|
      \boxminus       \boxplus                 \boxtimes
 | 
						|
      \curlyvee       \curlywedge              \divideontimes
 | 
						|
      \doublebarwedge \leftthreetimes          \rightthreetimes
 | 
						|
      \slash          \veebar                  \barvee
 | 
						|
      \cupdot         \intercal                \amalg
 | 
						|
      \circledcirc    \circleddash             \circledast
 | 
						|
      \boxbar         \obar                    \merge
 | 
						|
      \minuscolon     \dotsminusdots
 | 
						|
      '''.split())
 | 
						|
 | 
						|
    _relation_symbols = set(r'''
 | 
						|
      = < > :
 | 
						|
      \leq          \geq          \equiv       \models
 | 
						|
      \prec         \succ         \sim         \perp
 | 
						|
      \preceq       \succeq       \simeq       \mid
 | 
						|
      \ll           \gg           \asymp       \parallel
 | 
						|
      \subset       \supset       \approx      \bowtie
 | 
						|
      \subseteq     \supseteq     \cong        \Join
 | 
						|
      \sqsubset     \sqsupset     \neq         \smile
 | 
						|
      \sqsubseteq   \sqsupseteq   \doteq       \frown
 | 
						|
      \in           \ni           \propto      \vdash
 | 
						|
      \dashv        \dots         \doteqdot    \leqq
 | 
						|
      \geqq         \lneqq        \gneqq       \lessgtr
 | 
						|
      \leqslant     \geqslant     \eqgtr       \eqless
 | 
						|
      \eqslantless  \eqslantgtr   \lesseqgtr   \backsim
 | 
						|
      \backsimeq    \lesssim      \gtrsim      \precsim
 | 
						|
      \precnsim     \gnsim        \lnsim       \succsim
 | 
						|
      \succnsim     \nsim         \lesseqqgtr  \gtreqqless
 | 
						|
      \gtreqless    \subseteqq    \supseteqq   \subsetneqq
 | 
						|
      \supsetneqq   \lessapprox   \approxeq    \gtrapprox
 | 
						|
      \precapprox   \succapprox   \precnapprox \succnapprox
 | 
						|
      \npreccurlyeq \nsucccurlyeq \nsqsubseteq \nsqsupseteq
 | 
						|
      \sqsubsetneq  \sqsupsetneq  \nlesssim    \ngtrsim
 | 
						|
      \nlessgtr     \ngtrless     \lnapprox    \gnapprox
 | 
						|
      \napprox      \approxeq     \approxident \lll
 | 
						|
      \ggg          \nparallel    \Vdash       \Vvdash
 | 
						|
      \nVdash       \nvdash       \vDash       \nvDash
 | 
						|
      \nVDash       \oequal       \simneqq     \triangle
 | 
						|
      \triangleq         \triangleeq         \triangleleft
 | 
						|
      \triangleright     \ntriangleleft      \ntriangleright
 | 
						|
      \trianglelefteq    \ntrianglelefteq    \trianglerighteq
 | 
						|
      \ntrianglerighteq  \blacktriangleleft  \blacktriangleright
 | 
						|
      \equalparallel     \measuredrightangle \varlrtriangle
 | 
						|
      \Doteq        \Bumpeq       \Subset      \Supset
 | 
						|
      \backepsilon  \because      \therefore   \bot
 | 
						|
      \top          \bumpeq       \circeq      \coloneq
 | 
						|
      \curlyeqprec  \curlyeqsucc  \eqcirc      \eqcolon
 | 
						|
      \eqsim        \fallingdotseq \gtrdot     \gtrless
 | 
						|
      \ltimes       \rtimes       \lessdot     \ne
 | 
						|
      \ncong        \nequiv       \ngeq        \ngtr
 | 
						|
      \nleq         \nless        \nmid        \notin
 | 
						|
      \nprec        \nsubset      \nsubseteq   \nsucc
 | 
						|
      \nsupset      \nsupseteq    \pitchfork   \preccurlyeq
 | 
						|
      \risingdotseq \subsetneq    \succcurlyeq \supsetneq
 | 
						|
      \varpropto    \vartriangleleft \scurel
 | 
						|
      \vartriangleright \rightangle \equal     \backcong
 | 
						|
      \eqdef        \wedgeq       \questeq     \between
 | 
						|
      \veeeq        \disin        \varisins    \isins
 | 
						|
      \isindot      \varisinobar  \isinobar    \isinvb
 | 
						|
      \isinE        \nisd         \varnis      \nis
 | 
						|
      \varniobar    \niobar       \bagmember   \ratio
 | 
						|
      \Equiv        \stareq       \measeq      \arceq
 | 
						|
      \rightassert  \rightModels  \smallin     \smallowns
 | 
						|
      \notsmallowns \nsimeq'''.split())
 | 
						|
 | 
						|
    _arrow_symbols = set(r"""
 | 
						|
     \leftarrow \longleftarrow \uparrow \Leftarrow \Longleftarrow
 | 
						|
     \Uparrow \rightarrow \longrightarrow \downarrow \Rightarrow
 | 
						|
     \Longrightarrow \Downarrow \leftrightarrow \updownarrow
 | 
						|
     \longleftrightarrow \updownarrow \Leftrightarrow
 | 
						|
     \Longleftrightarrow \Updownarrow \mapsto \longmapsto \nearrow
 | 
						|
     \hookleftarrow \hookrightarrow \searrow \leftharpoonup
 | 
						|
     \rightharpoonup \swarrow \leftharpoondown \rightharpoondown
 | 
						|
     \nwarrow \rightleftharpoons \leadsto \dashrightarrow
 | 
						|
     \dashleftarrow \leftleftarrows \leftrightarrows \Lleftarrow
 | 
						|
     \Rrightarrow \twoheadleftarrow \leftarrowtail \looparrowleft
 | 
						|
     \leftrightharpoons \curvearrowleft \circlearrowleft \Lsh
 | 
						|
     \upuparrows \upharpoonleft \downharpoonleft \multimap
 | 
						|
     \leftrightsquigarrow \rightrightarrows \rightleftarrows
 | 
						|
     \rightrightarrows \rightleftarrows \twoheadrightarrow
 | 
						|
     \rightarrowtail \looparrowright \rightleftharpoons
 | 
						|
     \curvearrowright \circlearrowright \Rsh \downdownarrows
 | 
						|
     \upharpoonright \downharpoonright \rightsquigarrow \nleftarrow
 | 
						|
     \nrightarrow \nLeftarrow \nRightarrow \nleftrightarrow
 | 
						|
     \nLeftrightarrow \to \Swarrow \Searrow \Nwarrow \Nearrow
 | 
						|
     \leftsquigarrow \overleftarrow \overleftrightarrow \cwopencirclearrow
 | 
						|
     \downzigzagarrow \cupleftarrow \rightzigzagarrow \twoheaddownarrow
 | 
						|
     \updownarrowbar \twoheaduparrow \rightarrowbar \updownarrows
 | 
						|
     \barleftarrow \mapsfrom \mapsdown \mapsup \Ldsh \Rdsh
 | 
						|
     """.split())
 | 
						|
 | 
						|
    _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols
 | 
						|
 | 
						|
    _punctuation_symbols = set(r', ; . ! \ldotp \cdotp'.split())
 | 
						|
 | 
						|
    _overunder_symbols = set(r'''
 | 
						|
       \sum \prod \coprod \bigcap \bigcup \bigsqcup \bigvee
 | 
						|
       \bigwedge \bigodot \bigotimes \bigoplus \biguplus
 | 
						|
       '''.split())
 | 
						|
 | 
						|
    _overunder_functions = set("lim liminf limsup sup max min".split())
 | 
						|
 | 
						|
    _dropsub_symbols = set(r'\int \oint \iint \oiint \iiint \oiiint \iiiint'.split())
 | 
						|
 | 
						|
    _fontnames = set("rm cal it tt sf bf bfit "
 | 
						|
                     "default bb frak scr regular".split())
 | 
						|
 | 
						|
    _function_names = set("""
 | 
						|
      arccos csc ker min arcsin deg lg Pr arctan det lim sec arg dim
 | 
						|
      liminf sin cos exp limsup sinh cosh gcd ln sup cot hom log tan
 | 
						|
      coth inf max tanh""".split())
 | 
						|
 | 
						|
    _ambi_delims = set(r"""
 | 
						|
      | \| / \backslash \uparrow \downarrow \updownarrow \Uparrow
 | 
						|
      \Downarrow \Updownarrow . \vert \Vert""".split())
 | 
						|
    _left_delims = set(r"""
 | 
						|
      ( [ \{ < \lfloor \langle \lceil \lbrace \leftbrace \lbrack \leftparen \lgroup
 | 
						|
      """.split())
 | 
						|
    _right_delims = set(r"""
 | 
						|
      ) ] \} > \rfloor \rangle \rceil \rbrace \rightbrace \rbrack \rightparen \rgroup
 | 
						|
      """.split())
 | 
						|
    _delims = _left_delims | _right_delims | _ambi_delims
 | 
						|
 | 
						|
    _small_greek = set([unicodedata.name(chr(i)).split()[-1].lower() for i in
 | 
						|
                       range(ord('\N{GREEK SMALL LETTER ALPHA}'),
 | 
						|
                             ord('\N{GREEK SMALL LETTER OMEGA}') + 1)])
 | 
						|
    _latin_alphabets = set(string.ascii_letters)
 | 
						|
 | 
						|
    def __init__(self) -> None:
 | 
						|
        p = types.SimpleNamespace()
 | 
						|
 | 
						|
        def set_names_and_parse_actions() -> None:
 | 
						|
            for key, val in vars(p).items():
 | 
						|
                if not key.startswith('_'):
 | 
						|
                    # Set names on (almost) everything -- very useful for debugging
 | 
						|
                    # token, placeable, and auto_delim are forward references which
 | 
						|
                    # are left without names to ensure useful error messages
 | 
						|
                    if key not in ("token", "placeable", "auto_delim"):
 | 
						|
                        val.set_name(key)
 | 
						|
                    # Set actions
 | 
						|
                    if hasattr(self, key):
 | 
						|
                        val.set_parse_action(getattr(self, key))
 | 
						|
 | 
						|
        # Root definitions.
 | 
						|
 | 
						|
        # In TeX parlance, a csname is a control sequence name (a "\foo").
 | 
						|
        def csnames(group: str, names: Iterable[str]) -> Regex:
 | 
						|
            ends_with_alpha = []
 | 
						|
            ends_with_nonalpha = []
 | 
						|
            for name in names:
 | 
						|
                if name[-1].isalpha():
 | 
						|
                    ends_with_alpha.append(name)
 | 
						|
                else:
 | 
						|
                    ends_with_nonalpha.append(name)
 | 
						|
            return Regex(
 | 
						|
                r"\\(?P<{group}>(?:{alpha})(?![A-Za-z]){additional}{nonalpha})".format(
 | 
						|
                    group=group,
 | 
						|
                    alpha="|".join(map(re.escape, ends_with_alpha)),
 | 
						|
                    additional="|" if ends_with_nonalpha else "",
 | 
						|
                    nonalpha="|".join(map(re.escape, ends_with_nonalpha)),
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
        p.float_literal  = Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)")
 | 
						|
        p.space          = one_of(self._space_widths)("space")
 | 
						|
 | 
						|
        p.style_literal  = one_of(
 | 
						|
            [str(e.value) for e in self._MathStyle])("style_literal")
 | 
						|
 | 
						|
        p.symbol         = Regex(
 | 
						|
            r"[a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|\U00000080-\U0001ffff]"
 | 
						|
            r"|\\[%${}\[\]_|]"
 | 
						|
            + r"|\\(?:{})(?![A-Za-z])".format(
 | 
						|
                "|".join(map(re.escape, tex2uni)))
 | 
						|
        )("sym").leave_whitespace()
 | 
						|
        p.unknown_symbol = Regex(r"\\[A-Za-z]+")("name")
 | 
						|
 | 
						|
        p.font           = csnames("font", self._fontnames)
 | 
						|
        p.start_group    = Optional(r"\math" + one_of(self._fontnames)("font")) + "{"
 | 
						|
        p.end_group      = Literal("}")
 | 
						|
 | 
						|
        p.delim          = one_of(self._delims)
 | 
						|
 | 
						|
        # Mutually recursive definitions.  (Minimizing the number of Forward
 | 
						|
        # elements is important for speed.)
 | 
						|
        p.auto_delim       = Forward()
 | 
						|
        p.placeable        = Forward()
 | 
						|
        p.named_placeable  = Forward()
 | 
						|
        p.required_group   = Forward()
 | 
						|
        p.optional_group   = Forward()
 | 
						|
        p.token            = Forward()
 | 
						|
 | 
						|
        # Workaround for placable being part of a cycle of definitions
 | 
						|
        # calling `p.placeable("name")` results in a copy, so not guaranteed
 | 
						|
        # to get the definition added after it is used.
 | 
						|
        # ref https://github.com/matplotlib/matplotlib/issues/25204
 | 
						|
        # xref https://github.com/pyparsing/pyparsing/issues/95
 | 
						|
        p.named_placeable <<= p.placeable
 | 
						|
 | 
						|
        set_names_and_parse_actions()  # for mutually recursive definitions.
 | 
						|
 | 
						|
        p.optional_group <<= "{" + ZeroOrMore(p.token)("group") + "}"
 | 
						|
        p.required_group <<= "{" + OneOrMore(p.token)("group") + "}"
 | 
						|
 | 
						|
        p.customspace = cmd(r"\hspace", "{" + p.float_literal("space") + "}")
 | 
						|
 | 
						|
        p.accent = (
 | 
						|
            csnames("accent", [*self._accent_map, *self._wide_accents])
 | 
						|
            - p.named_placeable("sym"))
 | 
						|
 | 
						|
        p.function = csnames("name", self._function_names)
 | 
						|
 | 
						|
        p.group = p.start_group + ZeroOrMore(p.token)("group") + p.end_group
 | 
						|
        p.unclosed_group = (p.start_group + ZeroOrMore(p.token)("group") + StringEnd())
 | 
						|
 | 
						|
        p.frac  = cmd(r"\frac", p.required_group("num") + p.required_group("den"))
 | 
						|
        p.dfrac = cmd(r"\dfrac", p.required_group("num") + p.required_group("den"))
 | 
						|
        p.binom = cmd(r"\binom", p.required_group("num") + p.required_group("den"))
 | 
						|
 | 
						|
        p.genfrac = cmd(
 | 
						|
            r"\genfrac",
 | 
						|
            "{" + Optional(p.delim)("ldelim") + "}"
 | 
						|
            + "{" + Optional(p.delim)("rdelim") + "}"
 | 
						|
            + "{" + p.float_literal("rulesize") + "}"
 | 
						|
            + "{" + Optional(p.style_literal)("style") + "}"
 | 
						|
            + p.required_group("num")
 | 
						|
            + p.required_group("den"))
 | 
						|
 | 
						|
        p.sqrt = cmd(
 | 
						|
            r"\sqrt{value}",
 | 
						|
            Optional("[" + OneOrMore(NotAny("]") + p.token)("root") + "]")
 | 
						|
            + p.required_group("value"))
 | 
						|
 | 
						|
        p.overline = cmd(r"\overline", p.required_group("body"))
 | 
						|
 | 
						|
        p.overset  = cmd(
 | 
						|
            r"\overset",
 | 
						|
            p.optional_group("annotation") + p.optional_group("body"))
 | 
						|
        p.underset = cmd(
 | 
						|
            r"\underset",
 | 
						|
            p.optional_group("annotation") + p.optional_group("body"))
 | 
						|
 | 
						|
        p.text = cmd(r"\text", QuotedString('{', '\\', end_quote_char="}"))
 | 
						|
 | 
						|
        p.substack = cmd(r"\substack",
 | 
						|
                           nested_expr(opener="{", closer="}",
 | 
						|
                                       content=Group(OneOrMore(p.token)) +
 | 
						|
                                       ZeroOrMore(Literal("\\\\").suppress()))("parts"))
 | 
						|
 | 
						|
        p.subsuper = (
 | 
						|
            (Optional(p.placeable)("nucleus")
 | 
						|
             + OneOrMore(one_of(["_", "^"]) - p.placeable)("subsuper")
 | 
						|
             + Regex("'*")("apostrophes"))
 | 
						|
            | Regex("'+")("apostrophes")
 | 
						|
            | (p.named_placeable("nucleus") + Regex("'*")("apostrophes"))
 | 
						|
        )
 | 
						|
 | 
						|
        p.simple = p.space | p.customspace | p.font | p.subsuper
 | 
						|
 | 
						|
        p.token <<= (
 | 
						|
            p.simple
 | 
						|
            | p.auto_delim
 | 
						|
            | p.unclosed_group
 | 
						|
            | p.unknown_symbol  # Must be last
 | 
						|
        )
 | 
						|
 | 
						|
        p.operatorname = cmd(r"\operatorname", "{" + ZeroOrMore(p.simple)("name") + "}")
 | 
						|
 | 
						|
        p.boldsymbol = cmd(
 | 
						|
            r"\boldsymbol", "{" + ZeroOrMore(p.simple)("value") + "}")
 | 
						|
 | 
						|
        p.placeable     <<= (
 | 
						|
            p.accent     # Must be before symbol as all accents are symbols
 | 
						|
            | p.symbol   # Must be second to catch all named symbols and single
 | 
						|
                         # chars not in a group
 | 
						|
            | p.function
 | 
						|
            | p.operatorname
 | 
						|
            | p.group
 | 
						|
            | p.frac
 | 
						|
            | p.dfrac
 | 
						|
            | p.binom
 | 
						|
            | p.genfrac
 | 
						|
            | p.overset
 | 
						|
            | p.underset
 | 
						|
            | p.sqrt
 | 
						|
            | p.overline
 | 
						|
            | p.text
 | 
						|
            | p.boldsymbol
 | 
						|
            | p.substack
 | 
						|
        )
 | 
						|
 | 
						|
        mdelim = r"\middle" - (p.delim("mdelim") | Error("Expected a delimiter"))
 | 
						|
        p.auto_delim    <<= (
 | 
						|
            r"\left" - (p.delim("left") | Error("Expected a delimiter"))
 | 
						|
            + ZeroOrMore(p.simple | p.auto_delim | mdelim)("mid")
 | 
						|
            + r"\right" - (p.delim("right") | Error("Expected a delimiter"))
 | 
						|
        )
 | 
						|
 | 
						|
        # Leaf definitions.
 | 
						|
        p.math          = OneOrMore(p.token)
 | 
						|
        p.math_string   = QuotedString('$', '\\', unquote_results=False)
 | 
						|
        p.non_math      = Regex(r"(?:(?:\\[$])|[^$])*").leave_whitespace()
 | 
						|
        p.main          = (
 | 
						|
            p.non_math + ZeroOrMore(p.math_string + p.non_math) + StringEnd()
 | 
						|
        )
 | 
						|
        set_names_and_parse_actions()  # for leaf definitions.
 | 
						|
 | 
						|
        self._expression = p.main
 | 
						|
        self._math_expression = p.math
 | 
						|
 | 
						|
        # To add space to nucleus operators after sub/superscripts
 | 
						|
        self._in_subscript_or_superscript = False
 | 
						|
 | 
						|
    def parse(self, s: str, fonts_object: Fonts, fontsize: float, dpi: float) -> Hlist:
 | 
						|
        """
 | 
						|
        Parse expression *s* using the given *fonts_object* for
 | 
						|
        output, at the given *fontsize* and *dpi*.
 | 
						|
 | 
						|
        Returns the parse tree of `Node` instances.
 | 
						|
        """
 | 
						|
        self._state_stack = [
 | 
						|
            ParserState(fonts_object, 'default', 'rm', fontsize, dpi)]
 | 
						|
        self._em_width_cache: dict[tuple[str, float, float], float] = {}
 | 
						|
        try:
 | 
						|
            result = self._expression.parse_string(s)
 | 
						|
        except ParseBaseException as err:
 | 
						|
            # explain becomes a plain method on pyparsing 3 (err.explain(0)).
 | 
						|
            raise ValueError("\n" + ParseException.explain(err, 0)) from None
 | 
						|
        self._state_stack = []
 | 
						|
        self._in_subscript_or_superscript = False
 | 
						|
        # prevent operator spacing from leaking into a new expression
 | 
						|
        self._em_width_cache = {}
 | 
						|
        ParserElement.reset_cache()
 | 
						|
        return T.cast(Hlist, result[0])  # Known return type from main.
 | 
						|
 | 
						|
    def get_state(self) -> ParserState:
 | 
						|
        """Get the current `State` of the parser."""
 | 
						|
        return self._state_stack[-1]
 | 
						|
 | 
						|
    def pop_state(self) -> None:
 | 
						|
        """Pop a `State` off of the stack."""
 | 
						|
        self._state_stack.pop()
 | 
						|
 | 
						|
    def push_state(self) -> None:
 | 
						|
        """Push a new `State` onto the stack, copying the current state."""
 | 
						|
        self._state_stack.append(self.get_state().copy())
 | 
						|
 | 
						|
    def main(self, toks: ParseResults) -> list[Hlist]:
 | 
						|
        return [Hlist(toks.as_list())]
 | 
						|
 | 
						|
    def math_string(self, toks: ParseResults) -> ParseResults:
 | 
						|
        return self._math_expression.parse_string(toks[0][1:-1], parse_all=True)
 | 
						|
 | 
						|
    def math(self, toks: ParseResults) -> T.Any:
 | 
						|
        hlist = Hlist(toks.as_list())
 | 
						|
        self.pop_state()
 | 
						|
        return [hlist]
 | 
						|
 | 
						|
    def non_math(self, toks: ParseResults) -> T.Any:
 | 
						|
        s = toks[0].replace(r'\$', '$')
 | 
						|
        symbols = [Char(c, self.get_state()) for c in s]
 | 
						|
        hlist = Hlist(symbols)
 | 
						|
        # We're going into math now, so set font to 'it'
 | 
						|
        self.push_state()
 | 
						|
        self.get_state().font = mpl.rcParams['mathtext.default']
 | 
						|
        return [hlist]
 | 
						|
 | 
						|
    float_literal = staticmethod(pyparsing_common.convert_to_float)
 | 
						|
 | 
						|
    def text(self, toks: ParseResults) -> T.Any:
 | 
						|
        self.push_state()
 | 
						|
        state = self.get_state()
 | 
						|
        state.font = 'rm'
 | 
						|
        hlist = Hlist([Char(c, state) for c in toks[1]])
 | 
						|
        self.pop_state()
 | 
						|
        return [hlist]
 | 
						|
 | 
						|
    def _make_space(self, percentage: float) -> Kern:
 | 
						|
        # In TeX, an em (the unit usually used to measure horizontal lengths)
 | 
						|
        # is not the width of the character 'm'; it is the same in different
 | 
						|
        # font styles (e.g. roman or italic). Mathtext, however, uses 'm' in
 | 
						|
        # the italic style so that horizontal spaces don't depend on the
 | 
						|
        # current font style.
 | 
						|
        state = self.get_state()
 | 
						|
        key = (state.font, state.fontsize, state.dpi)
 | 
						|
        width = self._em_width_cache.get(key)
 | 
						|
        if width is None:
 | 
						|
            metrics = state.fontset.get_metrics(
 | 
						|
                'it', mpl.rcParams['mathtext.default'], 'm',
 | 
						|
                state.fontsize, state.dpi)
 | 
						|
            width = metrics.advance
 | 
						|
            self._em_width_cache[key] = width
 | 
						|
        return Kern(width * percentage)
 | 
						|
 | 
						|
    _space_widths = {
 | 
						|
        r'\,':         0.16667,   # 3/18 em = 3 mu
 | 
						|
        r'\thinspace': 0.16667,   # 3/18 em = 3 mu
 | 
						|
        r'\/':         0.16667,   # 3/18 em = 3 mu
 | 
						|
        r'\>':         0.22222,   # 4/18 em = 4 mu
 | 
						|
        r'\:':         0.22222,   # 4/18 em = 4 mu
 | 
						|
        r'\;':         0.27778,   # 5/18 em = 5 mu
 | 
						|
        r'\ ':         0.33333,   # 6/18 em = 6 mu
 | 
						|
        r'~':          0.33333,   # 6/18 em = 6 mu, nonbreakable
 | 
						|
        r'\enspace':   0.5,       # 9/18 em = 9 mu
 | 
						|
        r'\quad':      1,         # 1 em = 18 mu
 | 
						|
        r'\qquad':     2,         # 2 em = 36 mu
 | 
						|
        r'\!':         -0.16667,  # -3/18 em = -3 mu
 | 
						|
    }
 | 
						|
 | 
						|
    def space(self, toks: ParseResults) -> T.Any:
 | 
						|
        num = self._space_widths[toks["space"]]
 | 
						|
        box = self._make_space(num)
 | 
						|
        return [box]
 | 
						|
 | 
						|
    def customspace(self, toks: ParseResults) -> T.Any:
 | 
						|
        return [self._make_space(toks["space"])]
 | 
						|
 | 
						|
    def symbol(self, s: str, loc: int,
 | 
						|
               toks: ParseResults | dict[str, str]) -> T.Any:
 | 
						|
        c = toks["sym"]
 | 
						|
        if c == "-":
 | 
						|
            # "U+2212 minus sign is the preferred representation of the unary
 | 
						|
            # and binary minus sign rather than the ASCII-derived U+002D
 | 
						|
            # hyphen-minus, because minus sign is unambiguous and because it
 | 
						|
            # is rendered with a more desirable length, usually longer than a
 | 
						|
            # hyphen." (https://www.unicode.org/reports/tr25/)
 | 
						|
            c = "\N{MINUS SIGN}"
 | 
						|
        try:
 | 
						|
            char = Char(c, self.get_state())
 | 
						|
        except ValueError as err:
 | 
						|
            raise ParseFatalException(s, loc,
 | 
						|
                                      "Unknown symbol: %s" % c) from err
 | 
						|
 | 
						|
        if c in self._spaced_symbols:
 | 
						|
            # iterate until we find previous character, needed for cases
 | 
						|
            # such as $=-2$, ${ -2}$, $ -2$, or $   -2$.
 | 
						|
            prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
 | 
						|
            # Binary operators at start of string should not be spaced
 | 
						|
            # Also, operators in sub- or superscripts should not be spaced
 | 
						|
            if (self._in_subscript_or_superscript or (
 | 
						|
                    c in self._binary_operators and (
 | 
						|
                    len(s[:loc].split()) == 0 or prev_char in {
 | 
						|
                        '{', *self._left_delims, *self._relation_symbols}))):
 | 
						|
                return [char]
 | 
						|
            else:
 | 
						|
                return [Hlist([self._make_space(0.2),
 | 
						|
                               char,
 | 
						|
                               self._make_space(0.2)],
 | 
						|
                              do_kern=True)]
 | 
						|
        elif c in self._punctuation_symbols:
 | 
						|
            prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
 | 
						|
            next_char = next((c for c in s[loc + 1:] if c != ' '), '')
 | 
						|
 | 
						|
            # Do not space commas between brackets
 | 
						|
            if c == ',':
 | 
						|
                if prev_char == '{' and next_char == '}':
 | 
						|
                    return [char]
 | 
						|
 | 
						|
            # Do not space dots as decimal separators
 | 
						|
            if c == '.' and prev_char.isdigit() and next_char.isdigit():
 | 
						|
                return [char]
 | 
						|
            else:
 | 
						|
                return [Hlist([char, self._make_space(0.2)], do_kern=True)]
 | 
						|
        return [char]
 | 
						|
 | 
						|
    def unknown_symbol(self, s: str, loc: int, toks: ParseResults) -> T.Any:
 | 
						|
        raise ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}")
 | 
						|
 | 
						|
    _accent_map = {
 | 
						|
        r'hat':            r'\circumflexaccent',
 | 
						|
        r'breve':          r'\combiningbreve',
 | 
						|
        r'bar':            r'\combiningoverline',
 | 
						|
        r'grave':          r'\combininggraveaccent',
 | 
						|
        r'acute':          r'\combiningacuteaccent',
 | 
						|
        r'tilde':          r'\combiningtilde',
 | 
						|
        r'dot':            r'\combiningdotabove',
 | 
						|
        r'ddot':           r'\combiningdiaeresis',
 | 
						|
        r'dddot':          r'\combiningthreedotsabove',
 | 
						|
        r'ddddot':         r'\combiningfourdotsabove',
 | 
						|
        r'vec':            r'\combiningrightarrowabove',
 | 
						|
        r'"':              r'\combiningdiaeresis',
 | 
						|
        r"`":              r'\combininggraveaccent',
 | 
						|
        r"'":              r'\combiningacuteaccent',
 | 
						|
        r'~':              r'\combiningtilde',
 | 
						|
        r'.':              r'\combiningdotabove',
 | 
						|
        r'^':              r'\circumflexaccent',
 | 
						|
        r'overrightarrow': r'\rightarrow',
 | 
						|
        r'overleftarrow':  r'\leftarrow',
 | 
						|
        r'mathring':       r'\circ',
 | 
						|
    }
 | 
						|
 | 
						|
    _wide_accents = set(r"widehat widetilde widebar".split())
 | 
						|
 | 
						|
    def accent(self, toks: ParseResults) -> T.Any:
 | 
						|
        state = self.get_state()
 | 
						|
        thickness = state.get_current_underline_thickness()
 | 
						|
        accent = toks["accent"]
 | 
						|
        sym = toks["sym"]
 | 
						|
        accent_box: Node
 | 
						|
        if accent in self._wide_accents:
 | 
						|
            accent_box = AutoWidthChar(
 | 
						|
                '\\' + accent, sym.width, state, char_class=Accent)
 | 
						|
        else:
 | 
						|
            accent_box = Accent(self._accent_map[accent], state)
 | 
						|
        if accent == 'mathring':
 | 
						|
            accent_box.shrink()
 | 
						|
            accent_box.shrink()
 | 
						|
        centered = HCentered([Hbox(sym.width / 4.0), accent_box])
 | 
						|
        centered.hpack(sym.width, 'exactly')
 | 
						|
        return Vlist([
 | 
						|
                centered,
 | 
						|
                Vbox(0., thickness * 2.0),
 | 
						|
                Hlist([sym])
 | 
						|
                ])
 | 
						|
 | 
						|
    def function(self, s: str, loc: int, toks: ParseResults) -> T.Any:
 | 
						|
        hlist = self.operatorname(s, loc, toks)
 | 
						|
        hlist.function_name = toks["name"]
 | 
						|
        return hlist
 | 
						|
 | 
						|
    def operatorname(self, s: str, loc: int, toks: ParseResults) -> T.Any:
 | 
						|
        self.push_state()
 | 
						|
        state = self.get_state()
 | 
						|
        state.font = 'rm'
 | 
						|
        hlist_list: list[Node] = []
 | 
						|
        # Change the font of Chars, but leave Kerns alone
 | 
						|
        name = toks["name"]
 | 
						|
        for c in name:
 | 
						|
            if isinstance(c, Char):
 | 
						|
                c.font = 'rm'
 | 
						|
                c._update_metrics()
 | 
						|
                hlist_list.append(c)
 | 
						|
            elif isinstance(c, str):
 | 
						|
                hlist_list.append(Char(c, state))
 | 
						|
            else:
 | 
						|
                hlist_list.append(c)
 | 
						|
        next_char_loc = loc + len(name) + 1
 | 
						|
        if isinstance(name, ParseResults):
 | 
						|
            next_char_loc += len('operatorname{}')
 | 
						|
        next_char = next((c for c in s[next_char_loc:] if c != ' '), '')
 | 
						|
        delimiters = self._delims | {'^', '_'}
 | 
						|
        if (next_char not in delimiters and
 | 
						|
                name not in self._overunder_functions):
 | 
						|
            # Add thin space except when followed by parenthesis, bracket, etc.
 | 
						|
            hlist_list += [self._make_space(self._space_widths[r'\,'])]
 | 
						|
        self.pop_state()
 | 
						|
        # if followed by a super/subscript, set flag to true
 | 
						|
        # This flag tells subsuper to add space after this operator
 | 
						|
        if next_char in {'^', '_'}:
 | 
						|
            self._in_subscript_or_superscript = True
 | 
						|
        else:
 | 
						|
            self._in_subscript_or_superscript = False
 | 
						|
 | 
						|
        return Hlist(hlist_list)
 | 
						|
 | 
						|
    def start_group(self, toks: ParseResults) -> T.Any:
 | 
						|
        self.push_state()
 | 
						|
        # Deal with LaTeX-style font tokens
 | 
						|
        if toks.get("font"):
 | 
						|
            self.get_state().font = toks.get("font")
 | 
						|
        return []
 | 
						|
 | 
						|
    def group(self, toks: ParseResults) -> T.Any:
 | 
						|
        grp = Hlist(toks.get("group", []))
 | 
						|
        return [grp]
 | 
						|
 | 
						|
    def required_group(self, toks: ParseResults) -> T.Any:
 | 
						|
        return Hlist(toks.get("group", []))
 | 
						|
 | 
						|
    optional_group = required_group
 | 
						|
 | 
						|
    def end_group(self) -> T.Any:
 | 
						|
        self.pop_state()
 | 
						|
        return []
 | 
						|
 | 
						|
    def unclosed_group(self, s: str, loc: int, toks: ParseResults) -> T.Any:
 | 
						|
        raise ParseFatalException(s, len(s), "Expected '}'")
 | 
						|
 | 
						|
    def font(self, toks: ParseResults) -> T.Any:
 | 
						|
        self.get_state().font = toks["font"]
 | 
						|
        return []
 | 
						|
 | 
						|
    def is_overunder(self, nucleus: Node) -> bool:
 | 
						|
        if isinstance(nucleus, Char):
 | 
						|
            return nucleus.c in self._overunder_symbols
 | 
						|
        elif isinstance(nucleus, Hlist) and hasattr(nucleus, 'function_name'):
 | 
						|
            return nucleus.function_name in self._overunder_functions
 | 
						|
        return False
 | 
						|
 | 
						|
    def is_dropsub(self, nucleus: Node) -> bool:
 | 
						|
        if isinstance(nucleus, Char):
 | 
						|
            return nucleus.c in self._dropsub_symbols
 | 
						|
        return False
 | 
						|
 | 
						|
    def is_slanted(self, nucleus: Node) -> bool:
 | 
						|
        if isinstance(nucleus, Char):
 | 
						|
            return nucleus.is_slanted()
 | 
						|
        return False
 | 
						|
 | 
						|
    def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any:
 | 
						|
        nucleus = toks.get("nucleus", Hbox(0))
 | 
						|
        subsuper = toks.get("subsuper", [])
 | 
						|
        napostrophes = len(toks.get("apostrophes", []))
 | 
						|
 | 
						|
        if not subsuper and not napostrophes:
 | 
						|
            return nucleus
 | 
						|
 | 
						|
        sub = super = None
 | 
						|
        while subsuper:
 | 
						|
            op, arg, *subsuper = subsuper
 | 
						|
            if op == '_':
 | 
						|
                if sub is not None:
 | 
						|
                    raise ParseFatalException("Double subscript")
 | 
						|
                sub = arg
 | 
						|
            else:
 | 
						|
                if super is not None:
 | 
						|
                    raise ParseFatalException("Double superscript")
 | 
						|
                super = arg
 | 
						|
 | 
						|
        state = self.get_state()
 | 
						|
        rule_thickness = state.fontset.get_underline_thickness(
 | 
						|
            state.font, state.fontsize, state.dpi)
 | 
						|
        xHeight = state.fontset.get_xheight(
 | 
						|
            state.font, state.fontsize, state.dpi)
 | 
						|
 | 
						|
        if napostrophes:
 | 
						|
            if super is None:
 | 
						|
                super = Hlist([])
 | 
						|
            for i in range(napostrophes):
 | 
						|
                super.children.extend(self.symbol(s, loc, {"sym": "\\prime"}))
 | 
						|
            # kern() and hpack() needed to get the metrics right after
 | 
						|
            # extending
 | 
						|
            super.kern()
 | 
						|
            super.hpack()
 | 
						|
 | 
						|
        # Handle over/under symbols, such as sum or prod
 | 
						|
        if self.is_overunder(nucleus):
 | 
						|
            vlist = []
 | 
						|
            shift = 0.
 | 
						|
            width = nucleus.width
 | 
						|
            if super is not None:
 | 
						|
                super.shrink()
 | 
						|
                width = max(width, super.width)
 | 
						|
            if sub is not None:
 | 
						|
                sub.shrink()
 | 
						|
                width = max(width, sub.width)
 | 
						|
 | 
						|
            vgap = rule_thickness * 3.0
 | 
						|
            if super is not None:
 | 
						|
                hlist = HCentered([super])
 | 
						|
                hlist.hpack(width, 'exactly')
 | 
						|
                vlist.extend([hlist, Vbox(0, vgap)])
 | 
						|
            hlist = HCentered([nucleus])
 | 
						|
            hlist.hpack(width, 'exactly')
 | 
						|
            vlist.append(hlist)
 | 
						|
            if sub is not None:
 | 
						|
                hlist = HCentered([sub])
 | 
						|
                hlist.hpack(width, 'exactly')
 | 
						|
                vlist.extend([Vbox(0, vgap), hlist])
 | 
						|
                shift = hlist.height + vgap + nucleus.depth
 | 
						|
            vlt = Vlist(vlist)
 | 
						|
            vlt.shift_amount = shift
 | 
						|
            result = Hlist([vlt])
 | 
						|
            return [result]
 | 
						|
 | 
						|
        # We remove kerning on the last character for consistency (otherwise
 | 
						|
        # it will compute kerning based on non-shrunk characters and may put
 | 
						|
        # them too close together when superscripted)
 | 
						|
        # We change the width of the last character to match the advance to
 | 
						|
        # consider some fonts with weird metrics: e.g. stix's f has a width of
 | 
						|
        # 7.75 and a kerning of -4.0 for an advance of 3.72, and we want to put
 | 
						|
        # the superscript at the advance
 | 
						|
        last_char = nucleus
 | 
						|
        if isinstance(nucleus, Hlist):
 | 
						|
            new_children = nucleus.children
 | 
						|
            if len(new_children):
 | 
						|
                # remove last kern
 | 
						|
                if (isinstance(new_children[-1], Kern) and
 | 
						|
                        isinstance(new_children[-2], Char)):
 | 
						|
                    new_children = new_children[:-1]
 | 
						|
                last_char = new_children[-1]
 | 
						|
                if isinstance(last_char, Char):
 | 
						|
                    last_char.width = last_char._metrics.advance
 | 
						|
            # create new Hlist without kerning
 | 
						|
            nucleus = Hlist(new_children, do_kern=False)
 | 
						|
        else:
 | 
						|
            if isinstance(nucleus, Char):
 | 
						|
                last_char.width = last_char._metrics.advance
 | 
						|
            nucleus = Hlist([nucleus])
 | 
						|
 | 
						|
        # Handle regular sub/superscripts
 | 
						|
        constants = _get_font_constant_set(state)
 | 
						|
        lc_height   = last_char.height
 | 
						|
        lc_baseline = 0
 | 
						|
        if self.is_dropsub(last_char):
 | 
						|
            lc_baseline = last_char.depth
 | 
						|
 | 
						|
        # Compute kerning for sub and super
 | 
						|
        superkern = constants.delta * xHeight
 | 
						|
        subkern = constants.delta * xHeight
 | 
						|
        if self.is_slanted(last_char):
 | 
						|
            superkern += constants.delta * xHeight
 | 
						|
            superkern += (constants.delta_slanted *
 | 
						|
                          (lc_height - xHeight * 2. / 3.))
 | 
						|
            if self.is_dropsub(last_char):
 | 
						|
                subkern = (3 * constants.delta -
 | 
						|
                           constants.delta_integral) * lc_height
 | 
						|
                superkern = (3 * constants.delta +
 | 
						|
                             constants.delta_integral) * lc_height
 | 
						|
            else:
 | 
						|
                subkern = 0
 | 
						|
 | 
						|
        x: List
 | 
						|
        if super is None:
 | 
						|
            # node757
 | 
						|
            # Note: One of super or sub must be a Node if we're in this function, but
 | 
						|
            # mypy can't know this, since it can't interpret pyparsing expressions,
 | 
						|
            # hence the cast.
 | 
						|
            x = Hlist([Kern(subkern), T.cast(Node, sub)])
 | 
						|
            x.shrink()
 | 
						|
            if self.is_dropsub(last_char):
 | 
						|
                shift_down = lc_baseline + constants.subdrop * xHeight
 | 
						|
            else:
 | 
						|
                shift_down = constants.sub1 * xHeight
 | 
						|
            x.shift_amount = shift_down
 | 
						|
        else:
 | 
						|
            x = Hlist([Kern(superkern), super])
 | 
						|
            x.shrink()
 | 
						|
            if self.is_dropsub(last_char):
 | 
						|
                shift_up = lc_height - constants.subdrop * xHeight
 | 
						|
            else:
 | 
						|
                shift_up = constants.sup1 * xHeight
 | 
						|
            if sub is None:
 | 
						|
                x.shift_amount = -shift_up
 | 
						|
            else:  # Both sub and superscript
 | 
						|
                y = Hlist([Kern(subkern), sub])
 | 
						|
                y.shrink()
 | 
						|
                if self.is_dropsub(last_char):
 | 
						|
                    shift_down = lc_baseline + constants.subdrop * xHeight
 | 
						|
                else:
 | 
						|
                    shift_down = constants.sub2 * xHeight
 | 
						|
                # If sub and superscript collide, move super up
 | 
						|
                clr = (2.0 * rule_thickness -
 | 
						|
                       ((shift_up - x.depth) - (y.height - shift_down)))
 | 
						|
                if clr > 0.:
 | 
						|
                    shift_up += clr
 | 
						|
                x = Vlist([
 | 
						|
                    x,
 | 
						|
                    Kern((shift_up - x.depth) - (y.height - shift_down)),
 | 
						|
                    y])
 | 
						|
                x.shift_amount = shift_down
 | 
						|
 | 
						|
        if not self.is_dropsub(last_char):
 | 
						|
            x.width += constants.script_space * xHeight
 | 
						|
 | 
						|
        # Do we need to add a space after the nucleus?
 | 
						|
        # To find out, check the flag set by operatorname
 | 
						|
        spaced_nucleus: list[Node] = [nucleus, x]
 | 
						|
        if self._in_subscript_or_superscript:
 | 
						|
            spaced_nucleus += [self._make_space(self._space_widths[r'\,'])]
 | 
						|
            self._in_subscript_or_superscript = False
 | 
						|
 | 
						|
        result = Hlist(spaced_nucleus)
 | 
						|
        return [result]
 | 
						|
 | 
						|
    def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathStyle,
 | 
						|
                 num: Hlist, den: Hlist) -> T.Any:
 | 
						|
        state = self.get_state()
 | 
						|
        thickness = state.get_current_underline_thickness()
 | 
						|
 | 
						|
        for _ in range(style.value):
 | 
						|
            num.shrink()
 | 
						|
            den.shrink()
 | 
						|
        cnum = HCentered([num])
 | 
						|
        cden = HCentered([den])
 | 
						|
        width = max(num.width, den.width)
 | 
						|
        cnum.hpack(width, 'exactly')
 | 
						|
        cden.hpack(width, 'exactly')
 | 
						|
        vlist = Vlist([cnum,                      # numerator
 | 
						|
                       Vbox(0, thickness * 2.0),  # space
 | 
						|
                       Hrule(state, rule),        # rule
 | 
						|
                       Vbox(0, thickness * 2.0),  # space
 | 
						|
                       cden                       # denominator
 | 
						|
                       ])
 | 
						|
 | 
						|
        # Shift so the fraction line sits in the middle of the
 | 
						|
        # equals sign
 | 
						|
        metrics = state.fontset.get_metrics(
 | 
						|
            state.font, mpl.rcParams['mathtext.default'],
 | 
						|
            '=', state.fontsize, state.dpi)
 | 
						|
        shift = (cden.height -
 | 
						|
                 ((metrics.ymax + metrics.ymin) / 2 -
 | 
						|
                  thickness * 3.0))
 | 
						|
        vlist.shift_amount = shift
 | 
						|
 | 
						|
        result = [Hlist([vlist, Hbox(thickness * 2.)])]
 | 
						|
        if ldelim or rdelim:
 | 
						|
            if ldelim == '':
 | 
						|
                ldelim = '.'
 | 
						|
            if rdelim == '':
 | 
						|
                rdelim = '.'
 | 
						|
            return self._auto_sized_delimiter(ldelim,
 | 
						|
                                              T.cast(list[Box | Char | str],
 | 
						|
                                                     result),
 | 
						|
                                              rdelim)
 | 
						|
        return result
 | 
						|
 | 
						|
    def style_literal(self, toks: ParseResults) -> T.Any:
 | 
						|
        return self._MathStyle(int(toks["style_literal"]))
 | 
						|
 | 
						|
    def genfrac(self, toks: ParseResults) -> T.Any:
 | 
						|
        return self._genfrac(
 | 
						|
            toks.get("ldelim", ""), toks.get("rdelim", ""),
 | 
						|
            toks["rulesize"], toks.get("style", self._MathStyle.TEXTSTYLE),
 | 
						|
            toks["num"], toks["den"])
 | 
						|
 | 
						|
    def frac(self, toks: ParseResults) -> T.Any:
 | 
						|
        return self._genfrac(
 | 
						|
            "", "", self.get_state().get_current_underline_thickness(),
 | 
						|
            self._MathStyle.TEXTSTYLE, toks["num"], toks["den"])
 | 
						|
 | 
						|
    def dfrac(self, toks: ParseResults) -> T.Any:
 | 
						|
        return self._genfrac(
 | 
						|
            "", "", self.get_state().get_current_underline_thickness(),
 | 
						|
            self._MathStyle.DISPLAYSTYLE, toks["num"], toks["den"])
 | 
						|
 | 
						|
    def binom(self, toks: ParseResults) -> T.Any:
 | 
						|
        return self._genfrac(
 | 
						|
            "(", ")", 0,
 | 
						|
            self._MathStyle.TEXTSTYLE, toks["num"], toks["den"])
 | 
						|
 | 
						|
    def _genset(self, s: str, loc: int, toks: ParseResults) -> T.Any:
 | 
						|
        annotation = toks["annotation"]
 | 
						|
        body = toks["body"]
 | 
						|
        thickness = self.get_state().get_current_underline_thickness()
 | 
						|
 | 
						|
        annotation.shrink()
 | 
						|
        centered_annotation = HCentered([annotation])
 | 
						|
        centered_body = HCentered([body])
 | 
						|
        width = max(centered_annotation.width, centered_body.width)
 | 
						|
        centered_annotation.hpack(width, 'exactly')
 | 
						|
        centered_body.hpack(width, 'exactly')
 | 
						|
 | 
						|
        vgap = thickness * 3
 | 
						|
        if s[loc + 1] == "u":  # \underset
 | 
						|
            vlist = Vlist([
 | 
						|
                centered_body,               # body
 | 
						|
                Vbox(0, vgap),               # space
 | 
						|
                centered_annotation          # annotation
 | 
						|
            ])
 | 
						|
            # Shift so the body sits in the same vertical position
 | 
						|
            vlist.shift_amount = centered_body.depth + centered_annotation.height + vgap
 | 
						|
        else:  # \overset
 | 
						|
            vlist = Vlist([
 | 
						|
                centered_annotation,         # annotation
 | 
						|
                Vbox(0, vgap),               # space
 | 
						|
                centered_body                # body
 | 
						|
            ])
 | 
						|
 | 
						|
        # To add horizontal gap between symbols: wrap the Vlist into
 | 
						|
        # an Hlist and extend it with an Hbox(0, horizontal_gap)
 | 
						|
        return vlist
 | 
						|
 | 
						|
    overset = underset = _genset
 | 
						|
 | 
						|
    def sqrt(self, toks: ParseResults) -> T.Any:
 | 
						|
        root = toks.get("root")
 | 
						|
        body = toks["value"]
 | 
						|
        state = self.get_state()
 | 
						|
        thickness = state.get_current_underline_thickness()
 | 
						|
 | 
						|
        # Determine the height of the body, and add a little extra to
 | 
						|
        # the height so it doesn't seem cramped
 | 
						|
        height = body.height - body.shift_amount + thickness * 5.0
 | 
						|
        depth = body.depth + body.shift_amount
 | 
						|
        check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True)
 | 
						|
        height = check.height - check.shift_amount
 | 
						|
        depth = check.depth + check.shift_amount
 | 
						|
 | 
						|
        # Put a little extra space to the left and right of the body
 | 
						|
        padded_body = Hlist([Hbox(2 * thickness), body, Hbox(2 * thickness)])
 | 
						|
        rightside = Vlist([Hrule(state), Glue('fill'), padded_body])
 | 
						|
        # Stretch the glue between the hrule and the body
 | 
						|
        rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
 | 
						|
                        'exactly', depth)
 | 
						|
 | 
						|
        # Add the root and shift it upward so it is above the tick.
 | 
						|
        # The value of 0.6 is a hard-coded hack ;)
 | 
						|
        if not root:
 | 
						|
            root = Box(check.width * 0.5, 0., 0.)
 | 
						|
        else:
 | 
						|
            root = Hlist(root)
 | 
						|
            root.shrink()
 | 
						|
            root.shrink()
 | 
						|
 | 
						|
        root_vlist = Vlist([Hlist([root])])
 | 
						|
        root_vlist.shift_amount = -height * 0.6
 | 
						|
 | 
						|
        hlist = Hlist([root_vlist,               # Root
 | 
						|
                       # Negative kerning to put root over tick
 | 
						|
                       Kern(-check.width * 0.5),
 | 
						|
                       check,                    # Check
 | 
						|
                       rightside])               # Body
 | 
						|
        return [hlist]
 | 
						|
 | 
						|
    def overline(self, toks: ParseResults) -> T.Any:
 | 
						|
        body = toks["body"]
 | 
						|
 | 
						|
        state = self.get_state()
 | 
						|
        thickness = state.get_current_underline_thickness()
 | 
						|
 | 
						|
        height = body.height - body.shift_amount + thickness * 3.0
 | 
						|
        depth = body.depth + body.shift_amount
 | 
						|
 | 
						|
        # Place overline above body
 | 
						|
        rightside = Vlist([Hrule(state), Glue('fill'), Hlist([body])])
 | 
						|
 | 
						|
        # Stretch the glue between the hrule and the body
 | 
						|
        rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
 | 
						|
                        'exactly', depth)
 | 
						|
 | 
						|
        hlist = Hlist([rightside])
 | 
						|
        return [hlist]
 | 
						|
 | 
						|
    def _auto_sized_delimiter(self, front: str,
 | 
						|
                              middle: list[Box | Char | str],
 | 
						|
                              back: str) -> T.Any:
 | 
						|
        state = self.get_state()
 | 
						|
        if len(middle):
 | 
						|
            height = max([x.height for x in middle if not isinstance(x, str)])
 | 
						|
            depth = max([x.depth for x in middle if not isinstance(x, str)])
 | 
						|
            factor = None
 | 
						|
            for idx, el in enumerate(middle):
 | 
						|
                if el == r'\middle':
 | 
						|
                    c = T.cast(str, middle[idx + 1])  # Should be one of p.delims.
 | 
						|
                    if c != '.':
 | 
						|
                        middle[idx + 1] = AutoHeightChar(
 | 
						|
                                c, height, depth, state, factor=factor)
 | 
						|
                    else:
 | 
						|
                        middle.remove(c)
 | 
						|
                    del middle[idx]
 | 
						|
            # There should only be \middle and its delimiter as str, which have
 | 
						|
            # just been removed.
 | 
						|
            middle_part = T.cast(list[Box | Char], middle)
 | 
						|
        else:
 | 
						|
            height = 0
 | 
						|
            depth = 0
 | 
						|
            factor = 1.0
 | 
						|
            middle_part = []
 | 
						|
 | 
						|
        parts: list[Node] = []
 | 
						|
        # \left. and \right. aren't supposed to produce any symbols
 | 
						|
        if front != '.':
 | 
						|
            parts.append(
 | 
						|
                AutoHeightChar(front, height, depth, state, factor=factor))
 | 
						|
        parts.extend(middle_part)
 | 
						|
        if back != '.':
 | 
						|
            parts.append(
 | 
						|
                AutoHeightChar(back, height, depth, state, factor=factor))
 | 
						|
        hlist = Hlist(parts)
 | 
						|
        return hlist
 | 
						|
 | 
						|
    def auto_delim(self, toks: ParseResults) -> T.Any:
 | 
						|
        return self._auto_sized_delimiter(
 | 
						|
            toks["left"],
 | 
						|
            # if "mid" in toks ... can be removed when requiring pyparsing 3.
 | 
						|
            toks["mid"].as_list() if "mid" in toks else [],
 | 
						|
            toks["right"])
 | 
						|
 | 
						|
    def boldsymbol(self, toks: ParseResults) -> T.Any:
 | 
						|
        self.push_state()
 | 
						|
        state = self.get_state()
 | 
						|
        hlist: list[Node] = []
 | 
						|
        name = toks["value"]
 | 
						|
        for c in name:
 | 
						|
            if isinstance(c, Hlist):
 | 
						|
                k = c.children[1]
 | 
						|
                if isinstance(k, Char):
 | 
						|
                    k.font = "bf"
 | 
						|
                    k._update_metrics()
 | 
						|
                hlist.append(c)
 | 
						|
            elif isinstance(c, Char):
 | 
						|
                c.font = "bf"
 | 
						|
                if (c.c in self._latin_alphabets or
 | 
						|
                   c.c[1:] in self._small_greek):
 | 
						|
                    c.font = "bfit"
 | 
						|
                    c._update_metrics()
 | 
						|
                c._update_metrics()
 | 
						|
                hlist.append(c)
 | 
						|
            else:
 | 
						|
                hlist.append(c)
 | 
						|
        self.pop_state()
 | 
						|
 | 
						|
        return Hlist(hlist)
 | 
						|
 | 
						|
    def substack(self, toks: ParseResults) -> T.Any:
 | 
						|
        parts = toks["parts"]
 | 
						|
        state = self.get_state()
 | 
						|
        thickness = state.get_current_underline_thickness()
 | 
						|
 | 
						|
        hlist = [Hlist(k) for k in parts[0]]
 | 
						|
        max_width = max(map(lambda c: c.width, hlist))
 | 
						|
 | 
						|
        vlist = []
 | 
						|
        for sub in hlist:
 | 
						|
            cp = HCentered([sub])
 | 
						|
            cp.hpack(max_width, 'exactly')
 | 
						|
            vlist.append(cp)
 | 
						|
 | 
						|
        stack = [val
 | 
						|
                 for pair in zip(vlist, [Vbox(0, thickness * 2)] * len(vlist))
 | 
						|
                 for val in pair]
 | 
						|
        del stack[-1]
 | 
						|
        vlt = Vlist(stack)
 | 
						|
        result = [Hlist([vlt])]
 | 
						|
        return result
 |