1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851 |
- """
- 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, NotAny, oneOf, OneOrMore, Optional,
- ParseBaseException, ParseException, ParseExpression, ParseFatalException,
- ParserElement, ParseResults, QuotedString, Regex, StringEnd, ZeroOrMore,
- pyparsing_common, Group)
- 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_DEFAULT
- from packaging.version import parse as parse_version
- from pyparsing import __version__ as pyparsing_version
- if parse_version(pyparsing_version).major < 3:
- from pyparsing import nestedExpr as nested_expr
- else:
- from pyparsing import nested_expr
- if T.TYPE_CHECKING:
- from collections.abc import Iterable
- from .ft2font import Glyph
- ParserElement.enablePackrat()
- _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(np.ceil(w), 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, ox, 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, 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: int):
- """
- Parameters
- ----------
- default_font_prop : `~.font_manager.FontProperties`
- The default non-math font, or the base font for Unicode (generic)
- font rendering.
- load_glyph_flags : int
- 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: int):
- 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.0 for val in glyph.bbox]
- offset = self._get_offset(font, glyph, fontsize, dpi)
- metrics = FontMetrics(
- advance = glyph.linearHoriAdvance/65536.0,
- height = glyph.height/64.0,
- width = glyph.width/64.0,
- xmin = xmin,
- xmax = xmax,
- ymin = ymin+offset,
- ymax = ymax+offset,
- # iceberg is the equivalent of TeX's "height"
- iceberg = glyph.horiBearingY/64.0 + 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: int):
- 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'\rightparent', ')'),
- (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: int):
- # 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: int):
- # 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: int):
- 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().setParseAction(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.setName(key)
- # Set actions
- if hasattr(self, key):
- val.setParseAction(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 = oneOf(self._space_widths)("space")
- p.style_literal = oneOf(
- [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").leaveWhitespace()
- p.unknown_symbol = Regex(r"\\[A-Za-z]+")("name")
- p.font = csnames("font", self._fontnames)
- p.start_group = Optional(r"\math" + oneOf(self._fontnames)("font")) + "{"
- p.end_group = Literal("}")
- p.delim = oneOf(self._delims)
- # Mutually recursive definitions. (Minimizing the number of Forward
- # elements is important for speed.)
- p.auto_delim = Forward()
- p.placeable = Forward()
- p.required_group = Forward()
- p.optional_group = Forward()
- p.token = Forward()
- 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.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('{', '\\', endQuoteChar="}"))
- 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(oneOf(["_", "^"]) - p.placeable)("subsuper")
- + Regex("'*")("apostrophes"))
- | Regex("'+")("apostrophes")
- | (p.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('$', '\\', unquoteResults=False)
- p.non_math = Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace()
- 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.parseString(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.resetCache()
- 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.asList())]
- def math_string(self, toks: ParseResults) -> ParseResults:
- return self._math_expression.parseString(toks[0][1:-1], parseAll=True)
- def math(self, toks: ParseResults) -> T.Any:
- hlist = Hlist(toks.asList())
- 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.convertToFloat)
- 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$, 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 == '{' or
- prev_char in self._left_delims))):
- 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
- hasattr(new_children[-2], '_metrics')):
- new_children = new_children[:-1]
- last_char = new_children[-1]
- if hasattr(last_char, '_metrics'):
- 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 = [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[T.Union[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()
- cannotation = HCentered([annotation])
- cbody = HCentered([body])
- width = max(cannotation.width, cbody.width)
- cannotation.hpack(width, 'exactly')
- cbody.hpack(width, 'exactly')
- vgap = thickness * 3
- if s[loc + 1] == "u": # \underset
- vlist = Vlist([cbody, # body
- Vbox(0, vgap), # space
- cannotation # annotation
- ])
- # Shift so the body sits in the same vertical position
- vlist.shift_amount = cbody.depth + cannotation.height + vgap
- else: # \overset
- vlist = Vlist([cannotation, # annotation
- Vbox(0, vgap), # space
- cbody # 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 isinstance(el, str) and el == '\\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[T.Union[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"].asList() 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
|