_mathtext.py 105 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855
  1. """
  2. Implementation details for :mod:`.mathtext`.
  3. """
  4. from __future__ import annotations
  5. import abc
  6. import copy
  7. import enum
  8. import functools
  9. import logging
  10. import os
  11. import re
  12. import types
  13. import unicodedata
  14. import string
  15. import typing as T
  16. from typing import NamedTuple
  17. import numpy as np
  18. from pyparsing import (
  19. Empty, Forward, Literal, Group, NotAny, OneOrMore, Optional,
  20. ParseBaseException, ParseException, ParseExpression, ParseFatalException,
  21. ParserElement, ParseResults, QuotedString, Regex, StringEnd, ZeroOrMore,
  22. pyparsing_common, nested_expr, one_of)
  23. import matplotlib as mpl
  24. from . import cbook
  25. from ._mathtext_data import (
  26. latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni)
  27. from .font_manager import FontProperties, findfont, get_font
  28. from .ft2font import FT2Font, FT2Image, Kerning, LoadFlags
  29. if T.TYPE_CHECKING:
  30. from collections.abc import Iterable
  31. from .ft2font import Glyph
  32. ParserElement.enable_packrat()
  33. _log = logging.getLogger("matplotlib.mathtext")
  34. ##############################################################################
  35. # FONTS
  36. def get_unicode_index(symbol: str) -> int: # Publicly exported.
  37. r"""
  38. Return the integer index (from the Unicode table) of *symbol*.
  39. Parameters
  40. ----------
  41. symbol : str
  42. A single (Unicode) character, a TeX command (e.g. r'\pi') or a Type1
  43. symbol name (e.g. 'phi').
  44. """
  45. try: # This will succeed if symbol is a single Unicode char
  46. return ord(symbol)
  47. except TypeError:
  48. pass
  49. try: # Is symbol a TeX symbol (i.e. \alpha)
  50. return tex2uni[symbol.strip("\\")]
  51. except KeyError as err:
  52. raise ValueError(
  53. f"{symbol!r} is not a valid Unicode character or TeX/Type1 symbol"
  54. ) from err
  55. class VectorParse(NamedTuple):
  56. """
  57. The namedtuple type returned by ``MathTextParser("path").parse(...)``.
  58. Attributes
  59. ----------
  60. width, height, depth : float
  61. The global metrics.
  62. glyphs : list
  63. The glyphs including their positions.
  64. rect : list
  65. The list of rectangles.
  66. """
  67. width: float
  68. height: float
  69. depth: float
  70. glyphs: list[tuple[FT2Font, float, int, float, float]]
  71. rects: list[tuple[float, float, float, float]]
  72. VectorParse.__module__ = "matplotlib.mathtext"
  73. class RasterParse(NamedTuple):
  74. """
  75. The namedtuple type returned by ``MathTextParser("agg").parse(...)``.
  76. Attributes
  77. ----------
  78. ox, oy : float
  79. The offsets are always zero.
  80. width, height, depth : float
  81. The global metrics.
  82. image : FT2Image
  83. A raster image.
  84. """
  85. ox: float
  86. oy: float
  87. width: float
  88. height: float
  89. depth: float
  90. image: FT2Image
  91. RasterParse.__module__ = "matplotlib.mathtext"
  92. class Output:
  93. r"""
  94. Result of `ship`\ping a box: lists of positioned glyphs and rectangles.
  95. This class is not exposed to end users, but converted to a `VectorParse` or
  96. a `RasterParse` by `.MathTextParser.parse`.
  97. """
  98. def __init__(self, box: Box):
  99. self.box = box
  100. self.glyphs: list[tuple[float, float, FontInfo]] = [] # (ox, oy, info)
  101. self.rects: list[tuple[float, float, float, float]] = [] # (x1, y1, x2, y2)
  102. def to_vector(self) -> VectorParse:
  103. w, h, d = map(
  104. np.ceil, [self.box.width, self.box.height, self.box.depth])
  105. gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset)
  106. for ox, oy, info in self.glyphs]
  107. rs = [(x1, h - y2, x2 - x1, y2 - y1)
  108. for x1, y1, x2, y2 in self.rects]
  109. return VectorParse(w, h + d, d, gs, rs)
  110. def to_raster(self, *, antialiased: bool) -> RasterParse:
  111. # Metrics y's and mathtext y's are oriented in opposite directions,
  112. # hence the switch between ymin and ymax.
  113. xmin = min([*[ox + info.metrics.xmin for ox, oy, info in self.glyphs],
  114. *[x1 for x1, y1, x2, y2 in self.rects], 0]) - 1
  115. ymin = min([*[oy - info.metrics.ymax for ox, oy, info in self.glyphs],
  116. *[y1 for x1, y1, x2, y2 in self.rects], 0]) - 1
  117. xmax = max([*[ox + info.metrics.xmax for ox, oy, info in self.glyphs],
  118. *[x2 for x1, y1, x2, y2 in self.rects], 0]) + 1
  119. ymax = max([*[oy - info.metrics.ymin for ox, oy, info in self.glyphs],
  120. *[y2 for x1, y1, x2, y2 in self.rects], 0]) + 1
  121. w = xmax - xmin
  122. h = ymax - ymin - self.box.depth
  123. d = ymax - ymin - self.box.height
  124. image = FT2Image(int(np.ceil(w)), int(np.ceil(h + max(d, 0))))
  125. # Ideally, we could just use self.glyphs and self.rects here, shifting
  126. # their coordinates by (-xmin, -ymin), but this yields slightly
  127. # different results due to floating point slop; shipping twice is the
  128. # old approach and keeps baseline images backcompat.
  129. shifted = ship(self.box, (-xmin, -ymin))
  130. for ox, oy, info in shifted.glyphs:
  131. info.font.draw_glyph_to_bitmap(
  132. image, int(ox), int(oy - info.metrics.iceberg), info.glyph,
  133. antialiased=antialiased)
  134. for x1, y1, x2, y2 in shifted.rects:
  135. height = max(int(y2 - y1) - 1, 0)
  136. if height == 0:
  137. center = (y2 + y1) / 2
  138. y = int(center - (height + 1) / 2)
  139. else:
  140. y = int(y1)
  141. image.draw_rect_filled(int(x1), y, int(np.ceil(x2)), y + height)
  142. return RasterParse(0, 0, w, h + d, d, image)
  143. class FontMetrics(NamedTuple):
  144. """
  145. Metrics of a font.
  146. Attributes
  147. ----------
  148. advance : float
  149. The advance distance (in points) of the glyph.
  150. height : float
  151. The height of the glyph in points.
  152. width : float
  153. The width of the glyph in points.
  154. xmin, xmax, ymin, ymax : float
  155. The ink rectangle of the glyph.
  156. iceberg : float
  157. The distance from the baseline to the top of the glyph. (This corresponds to
  158. TeX's definition of "height".)
  159. slanted : bool
  160. Whether the glyph should be considered as "slanted" (currently used for kerning
  161. sub/superscripts).
  162. """
  163. advance: float
  164. height: float
  165. width: float
  166. xmin: float
  167. xmax: float
  168. ymin: float
  169. ymax: float
  170. iceberg: float
  171. slanted: bool
  172. class FontInfo(NamedTuple):
  173. font: FT2Font
  174. fontsize: float
  175. postscript_name: str
  176. metrics: FontMetrics
  177. num: int
  178. glyph: Glyph
  179. offset: float
  180. class Fonts(abc.ABC):
  181. """
  182. An abstract base class for a system of fonts to use for mathtext.
  183. The class must be able to take symbol keys and font file names and
  184. return the character metrics. It also delegates to a backend class
  185. to do the actual drawing.
  186. """
  187. def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
  188. """
  189. Parameters
  190. ----------
  191. default_font_prop : `~.font_manager.FontProperties`
  192. The default non-math font, or the base font for Unicode (generic)
  193. font rendering.
  194. load_glyph_flags : `.ft2font.LoadFlags`
  195. Flags passed to the glyph loader (e.g. ``FT_Load_Glyph`` and
  196. ``FT_Load_Char`` for FreeType-based fonts).
  197. """
  198. self.default_font_prop = default_font_prop
  199. self.load_glyph_flags = load_glyph_flags
  200. def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float,
  201. font2: str, fontclass2: str, sym2: str, fontsize2: float,
  202. dpi: float) -> float:
  203. """
  204. Get the kerning distance for font between *sym1* and *sym2*.
  205. See `~.Fonts.get_metrics` for a detailed description of the parameters.
  206. """
  207. return 0.
  208. def _get_font(self, font: str) -> FT2Font:
  209. raise NotImplementedError
  210. def _get_info(self, font: str, font_class: str, sym: str, fontsize: float,
  211. dpi: float) -> FontInfo:
  212. raise NotImplementedError
  213. def get_metrics(self, font: str, font_class: str, sym: str, fontsize: float,
  214. dpi: float) -> FontMetrics:
  215. r"""
  216. Parameters
  217. ----------
  218. font : str
  219. One of the TeX font names: "tt", "it", "rm", "cal", "sf", "bf",
  220. "default", "regular", "bb", "frak", "scr". "default" and "regular"
  221. are synonyms and use the non-math font.
  222. font_class : str
  223. One of the TeX font names (as for *font*), but **not** "bb",
  224. "frak", or "scr". This is used to combine two font classes. The
  225. only supported combination currently is ``get_metrics("frak", "bf",
  226. ...)``.
  227. sym : str
  228. A symbol in raw TeX form, e.g., "1", "x", or "\sigma".
  229. fontsize : float
  230. Font size in points.
  231. dpi : float
  232. Rendering dots-per-inch.
  233. Returns
  234. -------
  235. FontMetrics
  236. """
  237. info = self._get_info(font, font_class, sym, fontsize, dpi)
  238. return info.metrics
  239. def render_glyph(self, output: Output, ox: float, oy: float, font: str,
  240. font_class: str, sym: str, fontsize: float, dpi: float) -> None:
  241. """
  242. At position (*ox*, *oy*), draw the glyph specified by the remaining
  243. parameters (see `get_metrics` for their detailed description).
  244. """
  245. info = self._get_info(font, font_class, sym, fontsize, dpi)
  246. output.glyphs.append((ox, oy, info))
  247. def render_rect_filled(self, output: Output,
  248. x1: float, y1: float, x2: float, y2: float) -> None:
  249. """
  250. Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*).
  251. """
  252. output.rects.append((x1, y1, x2, y2))
  253. def get_xheight(self, font: str, fontsize: float, dpi: float) -> float:
  254. """
  255. Get the xheight for the given *font* and *fontsize*.
  256. """
  257. raise NotImplementedError()
  258. def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float:
  259. """
  260. Get the line thickness that matches the given font. Used as a
  261. base unit for drawing lines such as in a fraction or radical.
  262. """
  263. raise NotImplementedError()
  264. def get_sized_alternatives_for_symbol(self, fontname: str,
  265. sym: str) -> list[tuple[str, str]]:
  266. """
  267. Override if your font provides multiple sizes of the same
  268. symbol. Should return a list of symbols matching *sym* in
  269. various sizes. The expression renderer will select the most
  270. appropriate size for a given situation from this list.
  271. """
  272. return [(fontname, sym)]
  273. class TruetypeFonts(Fonts, metaclass=abc.ABCMeta):
  274. """
  275. A generic base class for all font setups that use Truetype fonts
  276. (through FT2Font).
  277. """
  278. def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
  279. super().__init__(default_font_prop, load_glyph_flags)
  280. # Per-instance cache.
  281. self._get_info = functools.cache(self._get_info) # type: ignore[method-assign]
  282. self._fonts = {}
  283. self.fontmap: dict[str | int, str] = {}
  284. filename = findfont(self.default_font_prop)
  285. default_font = get_font(filename)
  286. self._fonts['default'] = default_font
  287. self._fonts['regular'] = default_font
  288. def _get_font(self, font: str | int) -> FT2Font:
  289. if font in self.fontmap:
  290. basename = self.fontmap[font]
  291. else:
  292. # NOTE: An int is only passed by subclasses which have placed int keys into
  293. # `self.fontmap`, so we must cast this to confirm it to typing.
  294. basename = T.cast(str, font)
  295. cached_font = self._fonts.get(basename)
  296. if cached_font is None and os.path.exists(basename):
  297. cached_font = get_font(basename)
  298. self._fonts[basename] = cached_font
  299. self._fonts[cached_font.postscript_name] = cached_font
  300. self._fonts[cached_font.postscript_name.lower()] = cached_font
  301. return T.cast(FT2Font, cached_font) # FIXME: Not sure this is guaranteed.
  302. def _get_offset(self, font: FT2Font, glyph: Glyph, fontsize: float,
  303. dpi: float) -> float:
  304. if font.postscript_name == 'Cmex10':
  305. return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72)
  306. return 0.
  307. def _get_glyph(self, fontname: str, font_class: str,
  308. sym: str) -> tuple[FT2Font, int, bool]:
  309. raise NotImplementedError
  310. # The return value of _get_info is cached per-instance.
  311. def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float,
  312. dpi: float) -> FontInfo:
  313. font, num, slanted = self._get_glyph(fontname, font_class, sym)
  314. font.set_size(fontsize, dpi)
  315. glyph = font.load_char(num, flags=self.load_glyph_flags)
  316. xmin, ymin, xmax, ymax = (val / 64 for val in glyph.bbox)
  317. offset = self._get_offset(font, glyph, fontsize, dpi)
  318. metrics = FontMetrics(
  319. advance=glyph.linearHoriAdvance / 65536,
  320. height=glyph.height / 64,
  321. width=glyph.width / 64,
  322. xmin=xmin,
  323. xmax=xmax,
  324. ymin=ymin + offset,
  325. ymax=ymax + offset,
  326. # iceberg is the equivalent of TeX's "height"
  327. iceberg=glyph.horiBearingY / 64 + offset,
  328. slanted=slanted
  329. )
  330. return FontInfo(
  331. font=font,
  332. fontsize=fontsize,
  333. postscript_name=font.postscript_name,
  334. metrics=metrics,
  335. num=num,
  336. glyph=glyph,
  337. offset=offset
  338. )
  339. def get_xheight(self, fontname: str, fontsize: float, dpi: float) -> float:
  340. font = self._get_font(fontname)
  341. font.set_size(fontsize, dpi)
  342. pclt = font.get_sfnt_table('pclt')
  343. if pclt is None:
  344. # Some fonts don't store the xHeight, so we do a poor man's xHeight
  345. metrics = self.get_metrics(
  346. fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi)
  347. return metrics.iceberg
  348. xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
  349. return xHeight
  350. def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float:
  351. # This function used to grab underline thickness from the font
  352. # metrics, but that information is just too un-reliable, so it
  353. # is now hardcoded.
  354. return ((0.75 / 12.0) * fontsize * dpi) / 72.0
  355. def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float,
  356. font2: str, fontclass2: str, sym2: str, fontsize2: float,
  357. dpi: float) -> float:
  358. if font1 == font2 and fontsize1 == fontsize2:
  359. info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
  360. info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
  361. font = info1.font
  362. return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64
  363. return super().get_kern(font1, fontclass1, sym1, fontsize1,
  364. font2, fontclass2, sym2, fontsize2, dpi)
  365. class BakomaFonts(TruetypeFonts):
  366. """
  367. Use the Bakoma TrueType fonts for rendering.
  368. Symbols are strewn about a number of font files, each of which has
  369. its own proprietary 8-bit encoding.
  370. """
  371. _fontmap = {
  372. 'cal': 'cmsy10',
  373. 'rm': 'cmr10',
  374. 'tt': 'cmtt10',
  375. 'it': 'cmmi10',
  376. 'bf': 'cmb10',
  377. 'sf': 'cmss10',
  378. 'ex': 'cmex10',
  379. }
  380. def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
  381. self._stix_fallback = StixFonts(default_font_prop, load_glyph_flags)
  382. super().__init__(default_font_prop, load_glyph_flags)
  383. for key, val in self._fontmap.items():
  384. fullpath = findfont(val)
  385. self.fontmap[key] = fullpath
  386. self.fontmap[val] = fullpath
  387. _slanted_symbols = set(r"\int \oint".split())
  388. def _get_glyph(self, fontname: str, font_class: str,
  389. sym: str) -> tuple[FT2Font, int, bool]:
  390. font = None
  391. if fontname in self.fontmap and sym in latex_to_bakoma:
  392. basename, num = latex_to_bakoma[sym]
  393. slanted = (basename == "cmmi10") or sym in self._slanted_symbols
  394. font = self._get_font(basename)
  395. elif len(sym) == 1:
  396. slanted = (fontname == "it")
  397. font = self._get_font(fontname)
  398. if font is not None:
  399. num = ord(sym)
  400. if font is not None and font.get_char_index(num) != 0:
  401. return font, num, slanted
  402. else:
  403. return self._stix_fallback._get_glyph(fontname, font_class, sym)
  404. # The Bakoma fonts contain many pre-sized alternatives for the
  405. # delimiters. The AutoSizedChar class will use these alternatives
  406. # and select the best (closest sized) glyph.
  407. _size_alternatives = {
  408. '(': [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
  409. ('ex', '\xb5'), ('ex', '\xc3')],
  410. ')': [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
  411. ('ex', '\xb6'), ('ex', '\x21')],
  412. '{': [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'),
  413. ('ex', '\xbd'), ('ex', '\x28')],
  414. '}': [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'),
  415. ('ex', '\xbe'), ('ex', '\x29')],
  416. # The fourth size of '[' is mysteriously missing from the BaKoMa
  417. # font, so I've omitted it for both '[' and ']'
  418. '[': [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'),
  419. ('ex', '\x22')],
  420. ']': [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'),
  421. ('ex', '\x23')],
  422. r'\lfloor': [('ex', '\xa5'), ('ex', '\x6a'),
  423. ('ex', '\xb9'), ('ex', '\x24')],
  424. r'\rfloor': [('ex', '\xa6'), ('ex', '\x6b'),
  425. ('ex', '\xba'), ('ex', '\x25')],
  426. r'\lceil': [('ex', '\xa7'), ('ex', '\x6c'),
  427. ('ex', '\xbb'), ('ex', '\x26')],
  428. r'\rceil': [('ex', '\xa8'), ('ex', '\x6d'),
  429. ('ex', '\xbc'), ('ex', '\x27')],
  430. r'\langle': [('ex', '\xad'), ('ex', '\x44'),
  431. ('ex', '\xbf'), ('ex', '\x2a')],
  432. r'\rangle': [('ex', '\xae'), ('ex', '\x45'),
  433. ('ex', '\xc0'), ('ex', '\x2b')],
  434. r'\__sqrt__': [('ex', '\x70'), ('ex', '\x71'),
  435. ('ex', '\x72'), ('ex', '\x73')],
  436. r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'),
  437. ('ex', '\xc2'), ('ex', '\x2d')],
  438. r'/': [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'),
  439. ('ex', '\xcb'), ('ex', '\x2c')],
  440. r'\widehat': [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'),
  441. ('ex', '\x64')],
  442. r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'),
  443. ('ex', '\x67')],
  444. r'<': [('cal', 'h'), ('ex', 'D')],
  445. r'>': [('cal', 'i'), ('ex', 'E')]
  446. }
  447. for alias, target in [(r'\leftparen', '('),
  448. (r'\rightparen', ')'),
  449. (r'\leftbrace', '{'),
  450. (r'\rightbrace', '}'),
  451. (r'\leftbracket', '['),
  452. (r'\rightbracket', ']'),
  453. (r'\{', '{'),
  454. (r'\}', '}'),
  455. (r'\[', '['),
  456. (r'\]', ']')]:
  457. _size_alternatives[alias] = _size_alternatives[target]
  458. def get_sized_alternatives_for_symbol(self, fontname: str,
  459. sym: str) -> list[tuple[str, str]]:
  460. return self._size_alternatives.get(sym, [(fontname, sym)])
  461. class UnicodeFonts(TruetypeFonts):
  462. """
  463. An abstract base class for handling Unicode fonts.
  464. While some reasonably complete Unicode fonts (such as DejaVu) may
  465. work in some situations, the only Unicode font I'm aware of with a
  466. complete set of math symbols is STIX.
  467. This class will "fallback" on the Bakoma fonts when a required
  468. symbol cannot be found in the font.
  469. """
  470. # Some glyphs are not present in the `cmr10` font, and must be brought in
  471. # from `cmsy10`. Map the Unicode indices of those glyphs to the indices at
  472. # which they are found in `cmsy10`.
  473. _cmr10_substitutions = {
  474. 0x00D7: 0x00A3, # Multiplication sign.
  475. 0x2212: 0x00A1, # Minus sign.
  476. }
  477. def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
  478. # This must come first so the backend's owner is set correctly
  479. fallback_rc = mpl.rcParams['mathtext.fallback']
  480. font_cls: type[TruetypeFonts] | None = {
  481. 'stix': StixFonts,
  482. 'stixsans': StixSansFonts,
  483. 'cm': BakomaFonts
  484. }.get(fallback_rc)
  485. self._fallback_font = (font_cls(default_font_prop, load_glyph_flags)
  486. if font_cls else None)
  487. super().__init__(default_font_prop, load_glyph_flags)
  488. for texfont in "cal rm tt it bf sf bfit".split():
  489. prop = mpl.rcParams['mathtext.' + texfont]
  490. font = findfont(prop)
  491. self.fontmap[texfont] = font
  492. prop = FontProperties('cmex10')
  493. font = findfont(prop)
  494. self.fontmap['ex'] = font
  495. # include STIX sized alternatives for glyphs if fallback is STIX
  496. if isinstance(self._fallback_font, StixFonts):
  497. stixsizedaltfonts = {
  498. 0: 'STIXGeneral',
  499. 1: 'STIXSizeOneSym',
  500. 2: 'STIXSizeTwoSym',
  501. 3: 'STIXSizeThreeSym',
  502. 4: 'STIXSizeFourSym',
  503. 5: 'STIXSizeFiveSym'}
  504. for size, name in stixsizedaltfonts.items():
  505. fullpath = findfont(name)
  506. self.fontmap[size] = fullpath
  507. self.fontmap[name] = fullpath
  508. _slanted_symbols = set(r"\int \oint".split())
  509. def _map_virtual_font(self, fontname: str, font_class: str,
  510. uniindex: int) -> tuple[str, int]:
  511. return fontname, uniindex
  512. def _get_glyph(self, fontname: str, font_class: str,
  513. sym: str) -> tuple[FT2Font, int, bool]:
  514. try:
  515. uniindex = get_unicode_index(sym)
  516. found_symbol = True
  517. except ValueError:
  518. uniindex = ord('?')
  519. found_symbol = False
  520. _log.warning("No TeX to Unicode mapping for %a.", sym)
  521. fontname, uniindex = self._map_virtual_font(
  522. fontname, font_class, uniindex)
  523. new_fontname = fontname
  524. # Only characters in the "Letter" class should be italicized in 'it'
  525. # mode. Greek capital letters should be Roman.
  526. if found_symbol:
  527. if fontname == 'it' and uniindex < 0x10000:
  528. char = chr(uniindex)
  529. if (unicodedata.category(char)[0] != "L"
  530. or unicodedata.name(char).startswith("GREEK CAPITAL")):
  531. new_fontname = 'rm'
  532. slanted = (new_fontname == 'it') or sym in self._slanted_symbols
  533. found_symbol = False
  534. font = self._get_font(new_fontname)
  535. if font is not None:
  536. if (uniindex in self._cmr10_substitutions
  537. and font.family_name == "cmr10"):
  538. font = get_font(
  539. cbook._get_data_path("fonts/ttf/cmsy10.ttf"))
  540. uniindex = self._cmr10_substitutions[uniindex]
  541. glyphindex = font.get_char_index(uniindex)
  542. if glyphindex != 0:
  543. found_symbol = True
  544. if not found_symbol:
  545. if self._fallback_font:
  546. if (fontname in ('it', 'regular')
  547. and isinstance(self._fallback_font, StixFonts)):
  548. fontname = 'rm'
  549. g = self._fallback_font._get_glyph(fontname, font_class, sym)
  550. family = g[0].family_name
  551. if family in list(BakomaFonts._fontmap.values()):
  552. family = "Computer Modern"
  553. _log.info("Substituting symbol %s from %s", sym, family)
  554. return g
  555. else:
  556. if (fontname in ('it', 'regular')
  557. and isinstance(self, StixFonts)):
  558. return self._get_glyph('rm', font_class, sym)
  559. _log.warning("Font %r does not have a glyph for %a [U+%x], "
  560. "substituting with a dummy symbol.",
  561. new_fontname, sym, uniindex)
  562. font = self._get_font('rm')
  563. uniindex = 0xA4 # currency char, for lack of anything better
  564. slanted = False
  565. return font, uniindex, slanted
  566. def get_sized_alternatives_for_symbol(self, fontname: str,
  567. sym: str) -> list[tuple[str, str]]:
  568. if self._fallback_font:
  569. return self._fallback_font.get_sized_alternatives_for_symbol(
  570. fontname, sym)
  571. return [(fontname, sym)]
  572. class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta):
  573. _fontmap: dict[str | int, str] = {}
  574. def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
  575. # This must come first so the backend's owner is set correctly
  576. if isinstance(self, DejaVuSerifFonts):
  577. self._fallback_font = StixFonts(default_font_prop, load_glyph_flags)
  578. else:
  579. self._fallback_font = StixSansFonts(default_font_prop, load_glyph_flags)
  580. self.bakoma = BakomaFonts(default_font_prop, load_glyph_flags)
  581. TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags)
  582. # Include Stix sized alternatives for glyphs
  583. self._fontmap.update({
  584. 1: 'STIXSizeOneSym',
  585. 2: 'STIXSizeTwoSym',
  586. 3: 'STIXSizeThreeSym',
  587. 4: 'STIXSizeFourSym',
  588. 5: 'STIXSizeFiveSym',
  589. })
  590. for key, name in self._fontmap.items():
  591. fullpath = findfont(name)
  592. self.fontmap[key] = fullpath
  593. self.fontmap[name] = fullpath
  594. def _get_glyph(self, fontname: str, font_class: str,
  595. sym: str) -> tuple[FT2Font, int, bool]:
  596. # Override prime symbol to use Bakoma.
  597. if sym == r'\prime':
  598. return self.bakoma._get_glyph(fontname, font_class, sym)
  599. else:
  600. # check whether the glyph is available in the display font
  601. uniindex = get_unicode_index(sym)
  602. font = self._get_font('ex')
  603. if font is not None:
  604. glyphindex = font.get_char_index(uniindex)
  605. if glyphindex != 0:
  606. return super()._get_glyph('ex', font_class, sym)
  607. # otherwise return regular glyph
  608. return super()._get_glyph(fontname, font_class, sym)
  609. class DejaVuSerifFonts(DejaVuFonts):
  610. """
  611. A font handling class for the DejaVu Serif fonts
  612. If a glyph is not found it will fallback to Stix Serif
  613. """
  614. _fontmap = {
  615. 'rm': 'DejaVu Serif',
  616. 'it': 'DejaVu Serif:italic',
  617. 'bf': 'DejaVu Serif:weight=bold',
  618. 'bfit': 'DejaVu Serif:italic:bold',
  619. 'sf': 'DejaVu Sans',
  620. 'tt': 'DejaVu Sans Mono',
  621. 'ex': 'DejaVu Serif Display',
  622. 0: 'DejaVu Serif',
  623. }
  624. class DejaVuSansFonts(DejaVuFonts):
  625. """
  626. A font handling class for the DejaVu Sans fonts
  627. If a glyph is not found it will fallback to Stix Sans
  628. """
  629. _fontmap = {
  630. 'rm': 'DejaVu Sans',
  631. 'it': 'DejaVu Sans:italic',
  632. 'bf': 'DejaVu Sans:weight=bold',
  633. 'bfit': 'DejaVu Sans:italic:bold',
  634. 'sf': 'DejaVu Sans',
  635. 'tt': 'DejaVu Sans Mono',
  636. 'ex': 'DejaVu Sans Display',
  637. 0: 'DejaVu Sans',
  638. }
  639. class StixFonts(UnicodeFonts):
  640. """
  641. A font handling class for the STIX fonts.
  642. In addition to what UnicodeFonts provides, this class:
  643. - supports "virtual fonts" which are complete alpha numeric
  644. character sets with different font styles at special Unicode
  645. code points, such as "Blackboard".
  646. - handles sized alternative characters for the STIXSizeX fonts.
  647. """
  648. _fontmap: dict[str | int, str] = {
  649. 'rm': 'STIXGeneral',
  650. 'it': 'STIXGeneral:italic',
  651. 'bf': 'STIXGeneral:weight=bold',
  652. 'bfit': 'STIXGeneral:italic:bold',
  653. 'nonunirm': 'STIXNonUnicode',
  654. 'nonuniit': 'STIXNonUnicode:italic',
  655. 'nonunibf': 'STIXNonUnicode:weight=bold',
  656. 0: 'STIXGeneral',
  657. 1: 'STIXSizeOneSym',
  658. 2: 'STIXSizeTwoSym',
  659. 3: 'STIXSizeThreeSym',
  660. 4: 'STIXSizeFourSym',
  661. 5: 'STIXSizeFiveSym',
  662. }
  663. _fallback_font = None
  664. _sans = False
  665. def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
  666. TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags)
  667. for key, name in self._fontmap.items():
  668. fullpath = findfont(name)
  669. self.fontmap[key] = fullpath
  670. self.fontmap[name] = fullpath
  671. def _map_virtual_font(self, fontname: str, font_class: str,
  672. uniindex: int) -> tuple[str, int]:
  673. # Handle these "fonts" that are actually embedded in
  674. # other fonts.
  675. font_mapping = stix_virtual_fonts.get(fontname)
  676. if (self._sans and font_mapping is None
  677. and fontname not in ('regular', 'default')):
  678. font_mapping = stix_virtual_fonts['sf']
  679. doing_sans_conversion = True
  680. else:
  681. doing_sans_conversion = False
  682. if isinstance(font_mapping, dict):
  683. try:
  684. mapping = font_mapping[font_class]
  685. except KeyError:
  686. mapping = font_mapping['rm']
  687. elif isinstance(font_mapping, list):
  688. mapping = font_mapping
  689. else:
  690. mapping = None
  691. if mapping is not None:
  692. # Binary search for the source glyph
  693. lo = 0
  694. hi = len(mapping)
  695. while lo < hi:
  696. mid = (lo+hi)//2
  697. range = mapping[mid]
  698. if uniindex < range[0]:
  699. hi = mid
  700. elif uniindex <= range[1]:
  701. break
  702. else:
  703. lo = mid + 1
  704. if range[0] <= uniindex <= range[1]:
  705. uniindex = uniindex - range[0] + range[3]
  706. fontname = range[2]
  707. elif not doing_sans_conversion:
  708. # This will generate a dummy character
  709. uniindex = 0x1
  710. fontname = mpl.rcParams['mathtext.default']
  711. # Fix some incorrect glyphs.
  712. if fontname in ('rm', 'it'):
  713. uniindex = stix_glyph_fixes.get(uniindex, uniindex)
  714. # Handle private use area glyphs
  715. if fontname in ('it', 'rm', 'bf', 'bfit') and 0xe000 <= uniindex <= 0xf8ff:
  716. fontname = 'nonuni' + fontname
  717. return fontname, uniindex
  718. @functools.cache
  719. def get_sized_alternatives_for_symbol( # type: ignore[override]
  720. self,
  721. fontname: str,
  722. sym: str) -> list[tuple[str, str]] | list[tuple[int, str]]:
  723. fixes = {
  724. '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']',
  725. '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}',
  726. '>': '\N{MATHEMATICAL RIGHT ANGLE BRACKET}',
  727. }
  728. sym = fixes.get(sym, sym)
  729. try:
  730. uniindex = get_unicode_index(sym)
  731. except ValueError:
  732. return [(fontname, sym)]
  733. alternatives = [(i, chr(uniindex)) for i in range(6)
  734. if self._get_font(i).get_char_index(uniindex) != 0]
  735. # The largest size of the radical symbol in STIX has incorrect
  736. # metrics that cause it to be disconnected from the stem.
  737. if sym == r'\__sqrt__':
  738. alternatives = alternatives[:-1]
  739. return alternatives
  740. class StixSansFonts(StixFonts):
  741. """
  742. A font handling class for the STIX fonts (that uses sans-serif
  743. characters by default).
  744. """
  745. _sans = True
  746. ##############################################################################
  747. # TeX-LIKE BOX MODEL
  748. # The following is based directly on the document 'woven' from the
  749. # TeX82 source code. This information is also available in printed
  750. # form:
  751. #
  752. # Knuth, Donald E.. 1986. Computers and Typesetting, Volume B:
  753. # TeX: The Program. Addison-Wesley Professional.
  754. #
  755. # The most relevant "chapters" are:
  756. # Data structures for boxes and their friends
  757. # Shipping pages out (ship())
  758. # Packaging (hpack() and vpack())
  759. # Data structures for math mode
  760. # Subroutines for math mode
  761. # Typesetting math formulas
  762. #
  763. # Many of the docstrings below refer to a numbered "node" in that
  764. # book, e.g., node123
  765. #
  766. # Note that (as TeX) y increases downward, unlike many other parts of
  767. # matplotlib.
  768. # How much text shrinks when going to the next-smallest level.
  769. SHRINK_FACTOR = 0.7
  770. # The number of different sizes of chars to use, beyond which they will not
  771. # get any smaller
  772. NUM_SIZE_LEVELS = 6
  773. class FontConstantsBase:
  774. """
  775. A set of constants that controls how certain things, such as sub-
  776. and superscripts are laid out. These are all metrics that can't
  777. be reliably retrieved from the font metrics in the font itself.
  778. """
  779. # Percentage of x-height of additional horiz. space after sub/superscripts
  780. script_space: T.ClassVar[float] = 0.05
  781. # Percentage of x-height that sub/superscripts drop below the baseline
  782. subdrop: T.ClassVar[float] = 0.4
  783. # Percentage of x-height that superscripts are raised from the baseline
  784. sup1: T.ClassVar[float] = 0.7
  785. # Percentage of x-height that subscripts drop below the baseline
  786. sub1: T.ClassVar[float] = 0.3
  787. # Percentage of x-height that subscripts drop below the baseline when a
  788. # superscript is present
  789. sub2: T.ClassVar[float] = 0.5
  790. # Percentage of x-height that sub/superscripts are offset relative to the
  791. # nucleus edge for non-slanted nuclei
  792. delta: T.ClassVar[float] = 0.025
  793. # Additional percentage of last character height above 2/3 of the
  794. # x-height that superscripts are offset relative to the subscript
  795. # for slanted nuclei
  796. delta_slanted: T.ClassVar[float] = 0.2
  797. # Percentage of x-height that superscripts and subscripts are offset for
  798. # integrals
  799. delta_integral: T.ClassVar[float] = 0.1
  800. class ComputerModernFontConstants(FontConstantsBase):
  801. script_space = 0.075
  802. subdrop = 0.2
  803. sup1 = 0.45
  804. sub1 = 0.2
  805. sub2 = 0.3
  806. delta = 0.075
  807. delta_slanted = 0.3
  808. delta_integral = 0.3
  809. class STIXFontConstants(FontConstantsBase):
  810. script_space = 0.1
  811. sup1 = 0.8
  812. sub2 = 0.6
  813. delta = 0.05
  814. delta_slanted = 0.3
  815. delta_integral = 0.3
  816. class STIXSansFontConstants(FontConstantsBase):
  817. script_space = 0.05
  818. sup1 = 0.8
  819. delta_slanted = 0.6
  820. delta_integral = 0.3
  821. class DejaVuSerifFontConstants(FontConstantsBase):
  822. pass
  823. class DejaVuSansFontConstants(FontConstantsBase):
  824. pass
  825. # Maps font family names to the FontConstantBase subclass to use
  826. _font_constant_mapping = {
  827. 'DejaVu Sans': DejaVuSansFontConstants,
  828. 'DejaVu Sans Mono': DejaVuSansFontConstants,
  829. 'DejaVu Serif': DejaVuSerifFontConstants,
  830. 'cmb10': ComputerModernFontConstants,
  831. 'cmex10': ComputerModernFontConstants,
  832. 'cmmi10': ComputerModernFontConstants,
  833. 'cmr10': ComputerModernFontConstants,
  834. 'cmss10': ComputerModernFontConstants,
  835. 'cmsy10': ComputerModernFontConstants,
  836. 'cmtt10': ComputerModernFontConstants,
  837. 'STIXGeneral': STIXFontConstants,
  838. 'STIXNonUnicode': STIXFontConstants,
  839. 'STIXSizeFiveSym': STIXFontConstants,
  840. 'STIXSizeFourSym': STIXFontConstants,
  841. 'STIXSizeThreeSym': STIXFontConstants,
  842. 'STIXSizeTwoSym': STIXFontConstants,
  843. 'STIXSizeOneSym': STIXFontConstants,
  844. # Map the fonts we used to ship, just for good measure
  845. 'Bitstream Vera Sans': DejaVuSansFontConstants,
  846. 'Bitstream Vera': DejaVuSansFontConstants,
  847. }
  848. def _get_font_constant_set(state: ParserState) -> type[FontConstantsBase]:
  849. constants = _font_constant_mapping.get(
  850. state.fontset._get_font(state.font).family_name, FontConstantsBase)
  851. # STIX sans isn't really its own fonts, just different code points
  852. # in the STIX fonts, so we have to detect this one separately.
  853. if (constants is STIXFontConstants and
  854. isinstance(state.fontset, StixSansFonts)):
  855. return STIXSansFontConstants
  856. return constants
  857. class Node:
  858. """A node in the TeX box model."""
  859. def __init__(self) -> None:
  860. self.size = 0
  861. def __repr__(self) -> str:
  862. return type(self).__name__
  863. def get_kerning(self, next: Node | None) -> float:
  864. return 0.0
  865. def shrink(self) -> None:
  866. """
  867. Shrinks one level smaller. There are only three levels of
  868. sizes, after which things will no longer get smaller.
  869. """
  870. self.size += 1
  871. def render(self, output: Output, x: float, y: float) -> None:
  872. """Render this node."""
  873. class Box(Node):
  874. """A node with a physical location."""
  875. def __init__(self, width: float, height: float, depth: float) -> None:
  876. super().__init__()
  877. self.width = width
  878. self.height = height
  879. self.depth = depth
  880. def shrink(self) -> None:
  881. super().shrink()
  882. if self.size < NUM_SIZE_LEVELS:
  883. self.width *= SHRINK_FACTOR
  884. self.height *= SHRINK_FACTOR
  885. self.depth *= SHRINK_FACTOR
  886. def render(self, output: Output, # type: ignore[override]
  887. x1: float, y1: float, x2: float, y2: float) -> None:
  888. pass
  889. class Vbox(Box):
  890. """A box with only height (zero width)."""
  891. def __init__(self, height: float, depth: float):
  892. super().__init__(0., height, depth)
  893. class Hbox(Box):
  894. """A box with only width (zero height and depth)."""
  895. def __init__(self, width: float):
  896. super().__init__(width, 0., 0.)
  897. class Char(Node):
  898. """
  899. A single character.
  900. Unlike TeX, the font information and metrics are stored with each `Char`
  901. to make it easier to lookup the font metrics when needed. Note that TeX
  902. boxes have a width, height, and depth, unlike Type1 and TrueType which use
  903. a full bounding box and an advance in the x-direction. The metrics must
  904. be converted to the TeX model, and the advance (if different from width)
  905. must be converted into a `Kern` node when the `Char` is added to its parent
  906. `Hlist`.
  907. """
  908. def __init__(self, c: str, state: ParserState):
  909. super().__init__()
  910. self.c = c
  911. self.fontset = state.fontset
  912. self.font = state.font
  913. self.font_class = state.font_class
  914. self.fontsize = state.fontsize
  915. self.dpi = state.dpi
  916. # The real width, height and depth will be set during the
  917. # pack phase, after we know the real fontsize
  918. self._update_metrics()
  919. def __repr__(self) -> str:
  920. return '`%s`' % self.c
  921. def _update_metrics(self) -> None:
  922. metrics = self._metrics = self.fontset.get_metrics(
  923. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  924. if self.c == ' ':
  925. self.width = metrics.advance
  926. else:
  927. self.width = metrics.width
  928. self.height = metrics.iceberg
  929. self.depth = -(metrics.iceberg - metrics.height)
  930. def is_slanted(self) -> bool:
  931. return self._metrics.slanted
  932. def get_kerning(self, next: Node | None) -> float:
  933. """
  934. Return the amount of kerning between this and the given character.
  935. This method is called when characters are strung together into `Hlist`
  936. to create `Kern` nodes.
  937. """
  938. advance = self._metrics.advance - self.width
  939. kern = 0.
  940. if isinstance(next, Char):
  941. kern = self.fontset.get_kern(
  942. self.font, self.font_class, self.c, self.fontsize,
  943. next.font, next.font_class, next.c, next.fontsize,
  944. self.dpi)
  945. return advance + kern
  946. def render(self, output: Output, x: float, y: float) -> None:
  947. self.fontset.render_glyph(
  948. output, x, y,
  949. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  950. def shrink(self) -> None:
  951. super().shrink()
  952. if self.size < NUM_SIZE_LEVELS:
  953. self.fontsize *= SHRINK_FACTOR
  954. self.width *= SHRINK_FACTOR
  955. self.height *= SHRINK_FACTOR
  956. self.depth *= SHRINK_FACTOR
  957. class Accent(Char):
  958. """
  959. The font metrics need to be dealt with differently for accents,
  960. since they are already offset correctly from the baseline in
  961. TrueType fonts.
  962. """
  963. def _update_metrics(self) -> None:
  964. metrics = self._metrics = self.fontset.get_metrics(
  965. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  966. self.width = metrics.xmax - metrics.xmin
  967. self.height = metrics.ymax - metrics.ymin
  968. self.depth = 0
  969. def shrink(self) -> None:
  970. super().shrink()
  971. self._update_metrics()
  972. def render(self, output: Output, x: float, y: float) -> None:
  973. self.fontset.render_glyph(
  974. output, x - self._metrics.xmin, y + self._metrics.ymin,
  975. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  976. class List(Box):
  977. """A list of nodes (either horizontal or vertical)."""
  978. def __init__(self, elements: T.Sequence[Node]):
  979. super().__init__(0., 0., 0.)
  980. self.shift_amount = 0. # An arbitrary offset
  981. self.children = [*elements] # The child nodes of this list
  982. # The following parameters are set in the vpack and hpack functions
  983. self.glue_set = 0. # The glue setting of this list
  984. self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching
  985. self.glue_order = 0 # The order of infinity (0 - 3) for the glue
  986. def __repr__(self) -> str:
  987. return '{}<w={:.02f} h={:.02f} d={:.02f} s={:.02f}>[{}]'.format(
  988. super().__repr__(),
  989. self.width, self.height,
  990. self.depth, self.shift_amount,
  991. ', '.join([repr(x) for x in self.children]))
  992. def _set_glue(self, x: float, sign: int, totals: list[float],
  993. error_type: str) -> None:
  994. self.glue_order = o = next(
  995. # Highest order of glue used by the members of this list.
  996. (i for i in range(len(totals))[::-1] if totals[i] != 0), 0)
  997. self.glue_sign = sign
  998. if totals[o] != 0.:
  999. self.glue_set = x / totals[o]
  1000. else:
  1001. self.glue_sign = 0
  1002. self.glue_ratio = 0.
  1003. if o == 0:
  1004. if len(self.children):
  1005. _log.warning("%s %s: %r",
  1006. error_type, type(self).__name__, self)
  1007. def shrink(self) -> None:
  1008. for child in self.children:
  1009. child.shrink()
  1010. super().shrink()
  1011. if self.size < NUM_SIZE_LEVELS:
  1012. self.shift_amount *= SHRINK_FACTOR
  1013. self.glue_set *= SHRINK_FACTOR
  1014. class Hlist(List):
  1015. """A horizontal list of boxes."""
  1016. def __init__(self, elements: T.Sequence[Node], w: float = 0.0,
  1017. m: T.Literal['additional', 'exactly'] = 'additional',
  1018. do_kern: bool = True):
  1019. super().__init__(elements)
  1020. if do_kern:
  1021. self.kern()
  1022. self.hpack(w=w, m=m)
  1023. def kern(self) -> None:
  1024. """
  1025. Insert `Kern` nodes between `Char` nodes to set kerning.
  1026. The `Char` nodes themselves determine the amount of kerning they need
  1027. (in `~Char.get_kerning`), and this function just creates the correct
  1028. linked list.
  1029. """
  1030. new_children = []
  1031. num_children = len(self.children)
  1032. if num_children:
  1033. for i in range(num_children):
  1034. elem = self.children[i]
  1035. if i < num_children - 1:
  1036. next = self.children[i + 1]
  1037. else:
  1038. next = None
  1039. new_children.append(elem)
  1040. kerning_distance = elem.get_kerning(next)
  1041. if kerning_distance != 0.:
  1042. kern = Kern(kerning_distance)
  1043. new_children.append(kern)
  1044. self.children = new_children
  1045. def hpack(self, w: float = 0.0,
  1046. m: T.Literal['additional', 'exactly'] = 'additional') -> None:
  1047. r"""
  1048. Compute the dimensions of the resulting boxes, and adjust the glue if
  1049. one of those dimensions is pre-specified. The computed sizes normally
  1050. enclose all of the material inside the new box; but some items may
  1051. stick out if negative glue is used, if the box is overfull, or if a
  1052. ``\vbox`` includes other boxes that have been shifted left.
  1053. Parameters
  1054. ----------
  1055. w : float, default: 0
  1056. A width.
  1057. m : {'exactly', 'additional'}, default: 'additional'
  1058. Whether to produce a box whose width is 'exactly' *w*; or a box
  1059. with the natural width of the contents, plus *w* ('additional').
  1060. Notes
  1061. -----
  1062. The defaults produce a box with the natural width of the contents.
  1063. """
  1064. # I don't know why these get reset in TeX. Shift_amount is pretty
  1065. # much useless if we do.
  1066. # self.shift_amount = 0.
  1067. h = 0.
  1068. d = 0.
  1069. x = 0.
  1070. total_stretch = [0.] * 4
  1071. total_shrink = [0.] * 4
  1072. for p in self.children:
  1073. if isinstance(p, Char):
  1074. x += p.width
  1075. h = max(h, p.height)
  1076. d = max(d, p.depth)
  1077. elif isinstance(p, Box):
  1078. x += p.width
  1079. if not np.isinf(p.height) and not np.isinf(p.depth):
  1080. s = getattr(p, 'shift_amount', 0.)
  1081. h = max(h, p.height - s)
  1082. d = max(d, p.depth + s)
  1083. elif isinstance(p, Glue):
  1084. glue_spec = p.glue_spec
  1085. x += glue_spec.width
  1086. total_stretch[glue_spec.stretch_order] += glue_spec.stretch
  1087. total_shrink[glue_spec.shrink_order] += glue_spec.shrink
  1088. elif isinstance(p, Kern):
  1089. x += p.width
  1090. self.height = h
  1091. self.depth = d
  1092. if m == 'additional':
  1093. w += x
  1094. self.width = w
  1095. x = w - x
  1096. if x == 0.:
  1097. self.glue_sign = 0
  1098. self.glue_order = 0
  1099. self.glue_ratio = 0.
  1100. return
  1101. if x > 0.:
  1102. self._set_glue(x, 1, total_stretch, "Overful")
  1103. else:
  1104. self._set_glue(x, -1, total_shrink, "Underful")
  1105. class Vlist(List):
  1106. """A vertical list of boxes."""
  1107. def __init__(self, elements: T.Sequence[Node], h: float = 0.0,
  1108. m: T.Literal['additional', 'exactly'] = 'additional'):
  1109. super().__init__(elements)
  1110. self.vpack(h=h, m=m)
  1111. def vpack(self, h: float = 0.0,
  1112. m: T.Literal['additional', 'exactly'] = 'additional',
  1113. l: float = np.inf) -> None:
  1114. """
  1115. Compute the dimensions of the resulting boxes, and to adjust the glue
  1116. if one of those dimensions is pre-specified.
  1117. Parameters
  1118. ----------
  1119. h : float, default: 0
  1120. A height.
  1121. m : {'exactly', 'additional'}, default: 'additional'
  1122. Whether to produce a box whose height is 'exactly' *h*; or a box
  1123. with the natural height of the contents, plus *h* ('additional').
  1124. l : float, default: np.inf
  1125. The maximum height.
  1126. Notes
  1127. -----
  1128. The defaults produce a box with the natural height of the contents.
  1129. """
  1130. # I don't know why these get reset in TeX. Shift_amount is pretty
  1131. # much useless if we do.
  1132. # self.shift_amount = 0.
  1133. w = 0.
  1134. d = 0.
  1135. x = 0.
  1136. total_stretch = [0.] * 4
  1137. total_shrink = [0.] * 4
  1138. for p in self.children:
  1139. if isinstance(p, Box):
  1140. x += d + p.height
  1141. d = p.depth
  1142. if not np.isinf(p.width):
  1143. s = getattr(p, 'shift_amount', 0.)
  1144. w = max(w, p.width + s)
  1145. elif isinstance(p, Glue):
  1146. x += d
  1147. d = 0.
  1148. glue_spec = p.glue_spec
  1149. x += glue_spec.width
  1150. total_stretch[glue_spec.stretch_order] += glue_spec.stretch
  1151. total_shrink[glue_spec.shrink_order] += glue_spec.shrink
  1152. elif isinstance(p, Kern):
  1153. x += d + p.width
  1154. d = 0.
  1155. elif isinstance(p, Char):
  1156. raise RuntimeError(
  1157. "Internal mathtext error: Char node found in Vlist")
  1158. self.width = w
  1159. if d > l:
  1160. x += d - l
  1161. self.depth = l
  1162. else:
  1163. self.depth = d
  1164. if m == 'additional':
  1165. h += x
  1166. self.height = h
  1167. x = h - x
  1168. if x == 0:
  1169. self.glue_sign = 0
  1170. self.glue_order = 0
  1171. self.glue_ratio = 0.
  1172. return
  1173. if x > 0.:
  1174. self._set_glue(x, 1, total_stretch, "Overful")
  1175. else:
  1176. self._set_glue(x, -1, total_shrink, "Underful")
  1177. class Rule(Box):
  1178. """
  1179. A solid black rectangle.
  1180. It has *width*, *depth*, and *height* fields just as in an `Hlist`.
  1181. However, if any of these dimensions is inf, the actual value will be
  1182. determined by running the rule up to the boundary of the innermost
  1183. enclosing box. This is called a "running dimension". The width is never
  1184. running in an `Hlist`; the height and depth are never running in a `Vlist`.
  1185. """
  1186. def __init__(self, width: float, height: float, depth: float, state: ParserState):
  1187. super().__init__(width, height, depth)
  1188. self.fontset = state.fontset
  1189. def render(self, output: Output, # type: ignore[override]
  1190. x: float, y: float, w: float, h: float) -> None:
  1191. self.fontset.render_rect_filled(output, x, y, x + w, y + h)
  1192. class Hrule(Rule):
  1193. """Convenience class to create a horizontal rule."""
  1194. def __init__(self, state: ParserState, thickness: float | None = None):
  1195. if thickness is None:
  1196. thickness = state.get_current_underline_thickness()
  1197. height = depth = thickness * 0.5
  1198. super().__init__(np.inf, height, depth, state)
  1199. class Vrule(Rule):
  1200. """Convenience class to create a vertical rule."""
  1201. def __init__(self, state: ParserState):
  1202. thickness = state.get_current_underline_thickness()
  1203. super().__init__(thickness, np.inf, np.inf, state)
  1204. class _GlueSpec(NamedTuple):
  1205. width: float
  1206. stretch: float
  1207. stretch_order: int
  1208. shrink: float
  1209. shrink_order: int
  1210. _GlueSpec._named = { # type: ignore[attr-defined]
  1211. 'fil': _GlueSpec(0., 1., 1, 0., 0),
  1212. 'fill': _GlueSpec(0., 1., 2, 0., 0),
  1213. 'filll': _GlueSpec(0., 1., 3, 0., 0),
  1214. 'neg_fil': _GlueSpec(0., 0., 0, 1., 1),
  1215. 'neg_fill': _GlueSpec(0., 0., 0, 1., 2),
  1216. 'neg_filll': _GlueSpec(0., 0., 0, 1., 3),
  1217. 'empty': _GlueSpec(0., 0., 0, 0., 0),
  1218. 'ss': _GlueSpec(0., 1., 1, -1., 1),
  1219. }
  1220. class Glue(Node):
  1221. """
  1222. Most of the information in this object is stored in the underlying
  1223. ``_GlueSpec`` class, which is shared between multiple glue objects.
  1224. (This is a memory optimization which probably doesn't matter anymore, but
  1225. it's easier to stick to what TeX does.)
  1226. """
  1227. def __init__(self,
  1228. glue_type: _GlueSpec | T.Literal["fil", "fill", "filll",
  1229. "neg_fil", "neg_fill", "neg_filll",
  1230. "empty", "ss"]):
  1231. super().__init__()
  1232. if isinstance(glue_type, str):
  1233. glue_spec = _GlueSpec._named[glue_type] # type: ignore[attr-defined]
  1234. elif isinstance(glue_type, _GlueSpec):
  1235. glue_spec = glue_type
  1236. else:
  1237. raise ValueError("glue_type must be a glue spec name or instance")
  1238. self.glue_spec = glue_spec
  1239. def shrink(self) -> None:
  1240. super().shrink()
  1241. if self.size < NUM_SIZE_LEVELS:
  1242. g = self.glue_spec
  1243. self.glue_spec = g._replace(width=g.width * SHRINK_FACTOR)
  1244. class HCentered(Hlist):
  1245. """
  1246. A convenience class to create an `Hlist` whose contents are
  1247. centered within its enclosing box.
  1248. """
  1249. def __init__(self, elements: list[Node]):
  1250. super().__init__([Glue('ss'), *elements, Glue('ss')], do_kern=False)
  1251. class VCentered(Vlist):
  1252. """
  1253. A convenience class to create a `Vlist` whose contents are
  1254. centered within its enclosing box.
  1255. """
  1256. def __init__(self, elements: list[Node]):
  1257. super().__init__([Glue('ss'), *elements, Glue('ss')])
  1258. class Kern(Node):
  1259. """
  1260. A `Kern` node has a width field to specify a (normally
  1261. negative) amount of spacing. This spacing correction appears in
  1262. horizontal lists between letters like A and V when the font
  1263. designer said that it looks better to move them closer together or
  1264. further apart. A kern node can also appear in a vertical list,
  1265. when its *width* denotes additional spacing in the vertical
  1266. direction.
  1267. """
  1268. height = 0
  1269. depth = 0
  1270. def __init__(self, width: float):
  1271. super().__init__()
  1272. self.width = width
  1273. def __repr__(self) -> str:
  1274. return "k%.02f" % self.width
  1275. def shrink(self) -> None:
  1276. super().shrink()
  1277. if self.size < NUM_SIZE_LEVELS:
  1278. self.width *= SHRINK_FACTOR
  1279. class AutoHeightChar(Hlist):
  1280. """
  1281. A character as close to the given height and depth as possible.
  1282. When using a font with multiple height versions of some characters (such as
  1283. the BaKoMa fonts), the correct glyph will be selected, otherwise this will
  1284. always just return a scaled version of the glyph.
  1285. """
  1286. def __init__(self, c: str, height: float, depth: float, state: ParserState,
  1287. always: bool = False, factor: float | None = None):
  1288. alternatives = state.fontset.get_sized_alternatives_for_symbol(
  1289. state.font, c)
  1290. xHeight = state.fontset.get_xheight(
  1291. state.font, state.fontsize, state.dpi)
  1292. state = state.copy()
  1293. target_total = height + depth
  1294. for fontname, sym in alternatives:
  1295. state.font = fontname
  1296. char = Char(sym, state)
  1297. # Ensure that size 0 is chosen when the text is regular sized but
  1298. # with descender glyphs by subtracting 0.2 * xHeight
  1299. if char.height + char.depth >= target_total - 0.2 * xHeight:
  1300. break
  1301. shift = 0.0
  1302. if state.font != 0 or len(alternatives) == 1:
  1303. if factor is None:
  1304. factor = target_total / (char.height + char.depth)
  1305. state.fontsize *= factor
  1306. char = Char(sym, state)
  1307. shift = (depth - char.depth)
  1308. super().__init__([char])
  1309. self.shift_amount = shift
  1310. class AutoWidthChar(Hlist):
  1311. """
  1312. A character as close to the given width as possible.
  1313. When using a font with multiple width versions of some characters (such as
  1314. the BaKoMa fonts), the correct glyph will be selected, otherwise this will
  1315. always just return a scaled version of the glyph.
  1316. """
  1317. def __init__(self, c: str, width: float, state: ParserState, always: bool = False,
  1318. char_class: type[Char] = Char):
  1319. alternatives = state.fontset.get_sized_alternatives_for_symbol(
  1320. state.font, c)
  1321. state = state.copy()
  1322. for fontname, sym in alternatives:
  1323. state.font = fontname
  1324. char = char_class(sym, state)
  1325. if char.width >= width:
  1326. break
  1327. factor = width / char.width
  1328. state.fontsize *= factor
  1329. char = char_class(sym, state)
  1330. super().__init__([char])
  1331. self.width = char.width
  1332. def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output:
  1333. """
  1334. Ship out *box* at offset *xy*, converting it to an `Output`.
  1335. Since boxes can be inside of boxes inside of boxes, the main work of `ship`
  1336. is done by two mutually recursive routines, `hlist_out` and `vlist_out`,
  1337. which traverse the `Hlist` nodes and `Vlist` nodes inside of horizontal
  1338. and vertical boxes. The global variables used in TeX to store state as it
  1339. processes have become local variables here.
  1340. """
  1341. ox, oy = xy
  1342. cur_v = 0.
  1343. cur_h = 0.
  1344. off_h = ox
  1345. off_v = oy + box.height
  1346. output = Output(box)
  1347. def clamp(value: float) -> float:
  1348. return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value
  1349. def hlist_out(box: Hlist) -> None:
  1350. nonlocal cur_v, cur_h, off_h, off_v
  1351. cur_g = 0
  1352. cur_glue = 0.
  1353. glue_order = box.glue_order
  1354. glue_sign = box.glue_sign
  1355. base_line = cur_v
  1356. left_edge = cur_h
  1357. for p in box.children:
  1358. if isinstance(p, Char):
  1359. p.render(output, cur_h + off_h, cur_v + off_v)
  1360. cur_h += p.width
  1361. elif isinstance(p, Kern):
  1362. cur_h += p.width
  1363. elif isinstance(p, List):
  1364. # node623
  1365. if len(p.children) == 0:
  1366. cur_h += p.width
  1367. else:
  1368. edge = cur_h
  1369. cur_v = base_line + p.shift_amount
  1370. if isinstance(p, Hlist):
  1371. hlist_out(p)
  1372. elif isinstance(p, Vlist):
  1373. # p.vpack(box.height + box.depth, 'exactly')
  1374. vlist_out(p)
  1375. else:
  1376. assert False, "unreachable code"
  1377. cur_h = edge + p.width
  1378. cur_v = base_line
  1379. elif isinstance(p, Box):
  1380. # node624
  1381. rule_height = p.height
  1382. rule_depth = p.depth
  1383. rule_width = p.width
  1384. if np.isinf(rule_height):
  1385. rule_height = box.height
  1386. if np.isinf(rule_depth):
  1387. rule_depth = box.depth
  1388. if rule_height > 0 and rule_width > 0:
  1389. cur_v = base_line + rule_depth
  1390. p.render(output,
  1391. cur_h + off_h, cur_v + off_v,
  1392. rule_width, rule_height)
  1393. cur_v = base_line
  1394. cur_h += rule_width
  1395. elif isinstance(p, Glue):
  1396. # node625
  1397. glue_spec = p.glue_spec
  1398. rule_width = glue_spec.width - cur_g
  1399. if glue_sign != 0: # normal
  1400. if glue_sign == 1: # stretching
  1401. if glue_spec.stretch_order == glue_order:
  1402. cur_glue += glue_spec.stretch
  1403. cur_g = round(clamp(box.glue_set * cur_glue))
  1404. elif glue_spec.shrink_order == glue_order:
  1405. cur_glue += glue_spec.shrink
  1406. cur_g = round(clamp(box.glue_set * cur_glue))
  1407. rule_width += cur_g
  1408. cur_h += rule_width
  1409. def vlist_out(box: Vlist) -> None:
  1410. nonlocal cur_v, cur_h, off_h, off_v
  1411. cur_g = 0
  1412. cur_glue = 0.
  1413. glue_order = box.glue_order
  1414. glue_sign = box.glue_sign
  1415. left_edge = cur_h
  1416. cur_v -= box.height
  1417. top_edge = cur_v
  1418. for p in box.children:
  1419. if isinstance(p, Kern):
  1420. cur_v += p.width
  1421. elif isinstance(p, List):
  1422. if len(p.children) == 0:
  1423. cur_v += p.height + p.depth
  1424. else:
  1425. cur_v += p.height
  1426. cur_h = left_edge + p.shift_amount
  1427. save_v = cur_v
  1428. p.width = box.width
  1429. if isinstance(p, Hlist):
  1430. hlist_out(p)
  1431. elif isinstance(p, Vlist):
  1432. vlist_out(p)
  1433. else:
  1434. assert False, "unreachable code"
  1435. cur_v = save_v + p.depth
  1436. cur_h = left_edge
  1437. elif isinstance(p, Box):
  1438. rule_height = p.height
  1439. rule_depth = p.depth
  1440. rule_width = p.width
  1441. if np.isinf(rule_width):
  1442. rule_width = box.width
  1443. rule_height += rule_depth
  1444. if rule_height > 0 and rule_depth > 0:
  1445. cur_v += rule_height
  1446. p.render(output,
  1447. cur_h + off_h, cur_v + off_v,
  1448. rule_width, rule_height)
  1449. elif isinstance(p, Glue):
  1450. glue_spec = p.glue_spec
  1451. rule_height = glue_spec.width - cur_g
  1452. if glue_sign != 0: # normal
  1453. if glue_sign == 1: # stretching
  1454. if glue_spec.stretch_order == glue_order:
  1455. cur_glue += glue_spec.stretch
  1456. cur_g = round(clamp(box.glue_set * cur_glue))
  1457. elif glue_spec.shrink_order == glue_order: # shrinking
  1458. cur_glue += glue_spec.shrink
  1459. cur_g = round(clamp(box.glue_set * cur_glue))
  1460. rule_height += cur_g
  1461. cur_v += rule_height
  1462. elif isinstance(p, Char):
  1463. raise RuntimeError(
  1464. "Internal mathtext error: Char node found in vlist")
  1465. assert isinstance(box, Hlist)
  1466. hlist_out(box)
  1467. return output
  1468. ##############################################################################
  1469. # PARSER
  1470. def Error(msg: str) -> ParserElement:
  1471. """Helper class to raise parser errors."""
  1472. def raise_error(s: str, loc: int, toks: ParseResults) -> T.Any:
  1473. raise ParseFatalException(s, loc, msg)
  1474. return Empty().set_parse_action(raise_error)
  1475. class ParserState:
  1476. """
  1477. Parser state.
  1478. States are pushed and popped from a stack as necessary, and the "current"
  1479. state is always at the top of the stack.
  1480. Upon entering and leaving a group { } or math/non-math, the stack is pushed
  1481. and popped accordingly.
  1482. """
  1483. def __init__(self, fontset: Fonts, font: str, font_class: str, fontsize: float,
  1484. dpi: float):
  1485. self.fontset = fontset
  1486. self._font = font
  1487. self.font_class = font_class
  1488. self.fontsize = fontsize
  1489. self.dpi = dpi
  1490. def copy(self) -> ParserState:
  1491. return copy.copy(self)
  1492. @property
  1493. def font(self) -> str:
  1494. return self._font
  1495. @font.setter
  1496. def font(self, name: str) -> None:
  1497. if name in ('rm', 'it', 'bf', 'bfit'):
  1498. self.font_class = name
  1499. self._font = name
  1500. def get_current_underline_thickness(self) -> float:
  1501. """Return the underline thickness for this state."""
  1502. return self.fontset.get_underline_thickness(
  1503. self.font, self.fontsize, self.dpi)
  1504. def cmd(expr: str, args: ParserElement) -> ParserElement:
  1505. r"""
  1506. Helper to define TeX commands.
  1507. ``cmd("\cmd", args)`` is equivalent to
  1508. ``"\cmd" - (args | Error("Expected \cmd{arg}{...}"))`` where the names in
  1509. the error message are taken from element names in *args*. If *expr*
  1510. already includes arguments (e.g. "\cmd{arg}{...}"), then they are stripped
  1511. when constructing the parse element, but kept (and *expr* is used as is) in
  1512. the error message.
  1513. """
  1514. def names(elt: ParserElement) -> T.Generator[str, None, None]:
  1515. if isinstance(elt, ParseExpression):
  1516. for expr in elt.exprs:
  1517. yield from names(expr)
  1518. elif elt.resultsName:
  1519. yield elt.resultsName
  1520. csname = expr.split("{", 1)[0]
  1521. err = (csname + "".join("{%s}" % name for name in names(args))
  1522. if expr == csname else expr)
  1523. return csname - (args | Error(f"Expected {err}"))
  1524. class Parser:
  1525. """
  1526. A pyparsing-based parser for strings containing math expressions.
  1527. Raw text may also appear outside of pairs of ``$``.
  1528. The grammar is based directly on that in TeX, though it cuts a few corners.
  1529. """
  1530. class _MathStyle(enum.Enum):
  1531. DISPLAYSTYLE = 0
  1532. TEXTSTYLE = 1
  1533. SCRIPTSTYLE = 2
  1534. SCRIPTSCRIPTSTYLE = 3
  1535. _binary_operators = set(
  1536. '+ * - \N{MINUS SIGN}'
  1537. r'''
  1538. \pm \sqcap \rhd
  1539. \mp \sqcup \unlhd
  1540. \times \vee \unrhd
  1541. \div \wedge \oplus
  1542. \ast \setminus \ominus
  1543. \star \wr \otimes
  1544. \circ \diamond \oslash
  1545. \bullet \bigtriangleup \odot
  1546. \cdot \bigtriangledown \bigcirc
  1547. \cap \triangleleft \dagger
  1548. \cup \triangleright \ddagger
  1549. \uplus \lhd \amalg
  1550. \dotplus \dotminus \Cap
  1551. \Cup \barwedge \boxdot
  1552. \boxminus \boxplus \boxtimes
  1553. \curlyvee \curlywedge \divideontimes
  1554. \doublebarwedge \leftthreetimes \rightthreetimes
  1555. \slash \veebar \barvee
  1556. \cupdot \intercal \amalg
  1557. \circledcirc \circleddash \circledast
  1558. \boxbar \obar \merge
  1559. \minuscolon \dotsminusdots
  1560. '''.split())
  1561. _relation_symbols = set(r'''
  1562. = < > :
  1563. \leq \geq \equiv \models
  1564. \prec \succ \sim \perp
  1565. \preceq \succeq \simeq \mid
  1566. \ll \gg \asymp \parallel
  1567. \subset \supset \approx \bowtie
  1568. \subseteq \supseteq \cong \Join
  1569. \sqsubset \sqsupset \neq \smile
  1570. \sqsubseteq \sqsupseteq \doteq \frown
  1571. \in \ni \propto \vdash
  1572. \dashv \dots \doteqdot \leqq
  1573. \geqq \lneqq \gneqq \lessgtr
  1574. \leqslant \geqslant \eqgtr \eqless
  1575. \eqslantless \eqslantgtr \lesseqgtr \backsim
  1576. \backsimeq \lesssim \gtrsim \precsim
  1577. \precnsim \gnsim \lnsim \succsim
  1578. \succnsim \nsim \lesseqqgtr \gtreqqless
  1579. \gtreqless \subseteqq \supseteqq \subsetneqq
  1580. \supsetneqq \lessapprox \approxeq \gtrapprox
  1581. \precapprox \succapprox \precnapprox \succnapprox
  1582. \npreccurlyeq \nsucccurlyeq \nsqsubseteq \nsqsupseteq
  1583. \sqsubsetneq \sqsupsetneq \nlesssim \ngtrsim
  1584. \nlessgtr \ngtrless \lnapprox \gnapprox
  1585. \napprox \approxeq \approxident \lll
  1586. \ggg \nparallel \Vdash \Vvdash
  1587. \nVdash \nvdash \vDash \nvDash
  1588. \nVDash \oequal \simneqq \triangle
  1589. \triangleq \triangleeq \triangleleft
  1590. \triangleright \ntriangleleft \ntriangleright
  1591. \trianglelefteq \ntrianglelefteq \trianglerighteq
  1592. \ntrianglerighteq \blacktriangleleft \blacktriangleright
  1593. \equalparallel \measuredrightangle \varlrtriangle
  1594. \Doteq \Bumpeq \Subset \Supset
  1595. \backepsilon \because \therefore \bot
  1596. \top \bumpeq \circeq \coloneq
  1597. \curlyeqprec \curlyeqsucc \eqcirc \eqcolon
  1598. \eqsim \fallingdotseq \gtrdot \gtrless
  1599. \ltimes \rtimes \lessdot \ne
  1600. \ncong \nequiv \ngeq \ngtr
  1601. \nleq \nless \nmid \notin
  1602. \nprec \nsubset \nsubseteq \nsucc
  1603. \nsupset \nsupseteq \pitchfork \preccurlyeq
  1604. \risingdotseq \subsetneq \succcurlyeq \supsetneq
  1605. \varpropto \vartriangleleft \scurel
  1606. \vartriangleright \rightangle \equal \backcong
  1607. \eqdef \wedgeq \questeq \between
  1608. \veeeq \disin \varisins \isins
  1609. \isindot \varisinobar \isinobar \isinvb
  1610. \isinE \nisd \varnis \nis
  1611. \varniobar \niobar \bagmember \ratio
  1612. \Equiv \stareq \measeq \arceq
  1613. \rightassert \rightModels \smallin \smallowns
  1614. \notsmallowns \nsimeq'''.split())
  1615. _arrow_symbols = set(r"""
  1616. \leftarrow \longleftarrow \uparrow \Leftarrow \Longleftarrow
  1617. \Uparrow \rightarrow \longrightarrow \downarrow \Rightarrow
  1618. \Longrightarrow \Downarrow \leftrightarrow \updownarrow
  1619. \longleftrightarrow \updownarrow \Leftrightarrow
  1620. \Longleftrightarrow \Updownarrow \mapsto \longmapsto \nearrow
  1621. \hookleftarrow \hookrightarrow \searrow \leftharpoonup
  1622. \rightharpoonup \swarrow \leftharpoondown \rightharpoondown
  1623. \nwarrow \rightleftharpoons \leadsto \dashrightarrow
  1624. \dashleftarrow \leftleftarrows \leftrightarrows \Lleftarrow
  1625. \Rrightarrow \twoheadleftarrow \leftarrowtail \looparrowleft
  1626. \leftrightharpoons \curvearrowleft \circlearrowleft \Lsh
  1627. \upuparrows \upharpoonleft \downharpoonleft \multimap
  1628. \leftrightsquigarrow \rightrightarrows \rightleftarrows
  1629. \rightrightarrows \rightleftarrows \twoheadrightarrow
  1630. \rightarrowtail \looparrowright \rightleftharpoons
  1631. \curvearrowright \circlearrowright \Rsh \downdownarrows
  1632. \upharpoonright \downharpoonright \rightsquigarrow \nleftarrow
  1633. \nrightarrow \nLeftarrow \nRightarrow \nleftrightarrow
  1634. \nLeftrightarrow \to \Swarrow \Searrow \Nwarrow \Nearrow
  1635. \leftsquigarrow \overleftarrow \overleftrightarrow \cwopencirclearrow
  1636. \downzigzagarrow \cupleftarrow \rightzigzagarrow \twoheaddownarrow
  1637. \updownarrowbar \twoheaduparrow \rightarrowbar \updownarrows
  1638. \barleftarrow \mapsfrom \mapsdown \mapsup \Ldsh \Rdsh
  1639. """.split())
  1640. _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols
  1641. _punctuation_symbols = set(r', ; . ! \ldotp \cdotp'.split())
  1642. _overunder_symbols = set(r'''
  1643. \sum \prod \coprod \bigcap \bigcup \bigsqcup \bigvee
  1644. \bigwedge \bigodot \bigotimes \bigoplus \biguplus
  1645. '''.split())
  1646. _overunder_functions = set("lim liminf limsup sup max min".split())
  1647. _dropsub_symbols = set(r'\int \oint \iint \oiint \iiint \oiiint \iiiint'.split())
  1648. _fontnames = set("rm cal it tt sf bf bfit "
  1649. "default bb frak scr regular".split())
  1650. _function_names = set("""
  1651. arccos csc ker min arcsin deg lg Pr arctan det lim sec arg dim
  1652. liminf sin cos exp limsup sinh cosh gcd ln sup cot hom log tan
  1653. coth inf max tanh""".split())
  1654. _ambi_delims = set(r"""
  1655. | \| / \backslash \uparrow \downarrow \updownarrow \Uparrow
  1656. \Downarrow \Updownarrow . \vert \Vert""".split())
  1657. _left_delims = set(r"""
  1658. ( [ \{ < \lfloor \langle \lceil \lbrace \leftbrace \lbrack \leftparen \lgroup
  1659. """.split())
  1660. _right_delims = set(r"""
  1661. ) ] \} > \rfloor \rangle \rceil \rbrace \rightbrace \rbrack \rightparen \rgroup
  1662. """.split())
  1663. _delims = _left_delims | _right_delims | _ambi_delims
  1664. _small_greek = set([unicodedata.name(chr(i)).split()[-1].lower() for i in
  1665. range(ord('\N{GREEK SMALL LETTER ALPHA}'),
  1666. ord('\N{GREEK SMALL LETTER OMEGA}') + 1)])
  1667. _latin_alphabets = set(string.ascii_letters)
  1668. def __init__(self) -> None:
  1669. p = types.SimpleNamespace()
  1670. def set_names_and_parse_actions() -> None:
  1671. for key, val in vars(p).items():
  1672. if not key.startswith('_'):
  1673. # Set names on (almost) everything -- very useful for debugging
  1674. # token, placeable, and auto_delim are forward references which
  1675. # are left without names to ensure useful error messages
  1676. if key not in ("token", "placeable", "auto_delim"):
  1677. val.set_name(key)
  1678. # Set actions
  1679. if hasattr(self, key):
  1680. val.set_parse_action(getattr(self, key))
  1681. # Root definitions.
  1682. # In TeX parlance, a csname is a control sequence name (a "\foo").
  1683. def csnames(group: str, names: Iterable[str]) -> Regex:
  1684. ends_with_alpha = []
  1685. ends_with_nonalpha = []
  1686. for name in names:
  1687. if name[-1].isalpha():
  1688. ends_with_alpha.append(name)
  1689. else:
  1690. ends_with_nonalpha.append(name)
  1691. return Regex(
  1692. r"\\(?P<{group}>(?:{alpha})(?![A-Za-z]){additional}{nonalpha})".format(
  1693. group=group,
  1694. alpha="|".join(map(re.escape, ends_with_alpha)),
  1695. additional="|" if ends_with_nonalpha else "",
  1696. nonalpha="|".join(map(re.escape, ends_with_nonalpha)),
  1697. )
  1698. )
  1699. p.float_literal = Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)")
  1700. p.space = one_of(self._space_widths)("space")
  1701. p.style_literal = one_of(
  1702. [str(e.value) for e in self._MathStyle])("style_literal")
  1703. p.symbol = Regex(
  1704. r"[a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|\U00000080-\U0001ffff]"
  1705. r"|\\[%${}\[\]_|]"
  1706. + r"|\\(?:{})(?![A-Za-z])".format(
  1707. "|".join(map(re.escape, tex2uni)))
  1708. )("sym").leave_whitespace()
  1709. p.unknown_symbol = Regex(r"\\[A-Za-z]+")("name")
  1710. p.font = csnames("font", self._fontnames)
  1711. p.start_group = Optional(r"\math" + one_of(self._fontnames)("font")) + "{"
  1712. p.end_group = Literal("}")
  1713. p.delim = one_of(self._delims)
  1714. # Mutually recursive definitions. (Minimizing the number of Forward
  1715. # elements is important for speed.)
  1716. p.auto_delim = Forward()
  1717. p.placeable = Forward()
  1718. p.named_placeable = Forward()
  1719. p.required_group = Forward()
  1720. p.optional_group = Forward()
  1721. p.token = Forward()
  1722. # Workaround for placable being part of a cycle of definitions
  1723. # calling `p.placeable("name")` results in a copy, so not guaranteed
  1724. # to get the definition added after it is used.
  1725. # ref https://github.com/matplotlib/matplotlib/issues/25204
  1726. # xref https://github.com/pyparsing/pyparsing/issues/95
  1727. p.named_placeable <<= p.placeable
  1728. set_names_and_parse_actions() # for mutually recursive definitions.
  1729. p.optional_group <<= "{" + ZeroOrMore(p.token)("group") + "}"
  1730. p.required_group <<= "{" + OneOrMore(p.token)("group") + "}"
  1731. p.customspace = cmd(r"\hspace", "{" + p.float_literal("space") + "}")
  1732. p.accent = (
  1733. csnames("accent", [*self._accent_map, *self._wide_accents])
  1734. - p.named_placeable("sym"))
  1735. p.function = csnames("name", self._function_names)
  1736. p.group = p.start_group + ZeroOrMore(p.token)("group") + p.end_group
  1737. p.unclosed_group = (p.start_group + ZeroOrMore(p.token)("group") + StringEnd())
  1738. p.frac = cmd(r"\frac", p.required_group("num") + p.required_group("den"))
  1739. p.dfrac = cmd(r"\dfrac", p.required_group("num") + p.required_group("den"))
  1740. p.binom = cmd(r"\binom", p.required_group("num") + p.required_group("den"))
  1741. p.genfrac = cmd(
  1742. r"\genfrac",
  1743. "{" + Optional(p.delim)("ldelim") + "}"
  1744. + "{" + Optional(p.delim)("rdelim") + "}"
  1745. + "{" + p.float_literal("rulesize") + "}"
  1746. + "{" + Optional(p.style_literal)("style") + "}"
  1747. + p.required_group("num")
  1748. + p.required_group("den"))
  1749. p.sqrt = cmd(
  1750. r"\sqrt{value}",
  1751. Optional("[" + OneOrMore(NotAny("]") + p.token)("root") + "]")
  1752. + p.required_group("value"))
  1753. p.overline = cmd(r"\overline", p.required_group("body"))
  1754. p.overset = cmd(
  1755. r"\overset",
  1756. p.optional_group("annotation") + p.optional_group("body"))
  1757. p.underset = cmd(
  1758. r"\underset",
  1759. p.optional_group("annotation") + p.optional_group("body"))
  1760. p.text = cmd(r"\text", QuotedString('{', '\\', end_quote_char="}"))
  1761. p.substack = cmd(r"\substack",
  1762. nested_expr(opener="{", closer="}",
  1763. content=Group(OneOrMore(p.token)) +
  1764. ZeroOrMore(Literal("\\\\").suppress()))("parts"))
  1765. p.subsuper = (
  1766. (Optional(p.placeable)("nucleus")
  1767. + OneOrMore(one_of(["_", "^"]) - p.placeable)("subsuper")
  1768. + Regex("'*")("apostrophes"))
  1769. | Regex("'+")("apostrophes")
  1770. | (p.named_placeable("nucleus") + Regex("'*")("apostrophes"))
  1771. )
  1772. p.simple = p.space | p.customspace | p.font | p.subsuper
  1773. p.token <<= (
  1774. p.simple
  1775. | p.auto_delim
  1776. | p.unclosed_group
  1777. | p.unknown_symbol # Must be last
  1778. )
  1779. p.operatorname = cmd(r"\operatorname", "{" + ZeroOrMore(p.simple)("name") + "}")
  1780. p.boldsymbol = cmd(
  1781. r"\boldsymbol", "{" + ZeroOrMore(p.simple)("value") + "}")
  1782. p.placeable <<= (
  1783. p.accent # Must be before symbol as all accents are symbols
  1784. | p.symbol # Must be second to catch all named symbols and single
  1785. # chars not in a group
  1786. | p.function
  1787. | p.operatorname
  1788. | p.group
  1789. | p.frac
  1790. | p.dfrac
  1791. | p.binom
  1792. | p.genfrac
  1793. | p.overset
  1794. | p.underset
  1795. | p.sqrt
  1796. | p.overline
  1797. | p.text
  1798. | p.boldsymbol
  1799. | p.substack
  1800. )
  1801. mdelim = r"\middle" - (p.delim("mdelim") | Error("Expected a delimiter"))
  1802. p.auto_delim <<= (
  1803. r"\left" - (p.delim("left") | Error("Expected a delimiter"))
  1804. + ZeroOrMore(p.simple | p.auto_delim | mdelim)("mid")
  1805. + r"\right" - (p.delim("right") | Error("Expected a delimiter"))
  1806. )
  1807. # Leaf definitions.
  1808. p.math = OneOrMore(p.token)
  1809. p.math_string = QuotedString('$', '\\', unquote_results=False)
  1810. p.non_math = Regex(r"(?:(?:\\[$])|[^$])*").leave_whitespace()
  1811. p.main = (
  1812. p.non_math + ZeroOrMore(p.math_string + p.non_math) + StringEnd()
  1813. )
  1814. set_names_and_parse_actions() # for leaf definitions.
  1815. self._expression = p.main
  1816. self._math_expression = p.math
  1817. # To add space to nucleus operators after sub/superscripts
  1818. self._in_subscript_or_superscript = False
  1819. def parse(self, s: str, fonts_object: Fonts, fontsize: float, dpi: float) -> Hlist:
  1820. """
  1821. Parse expression *s* using the given *fonts_object* for
  1822. output, at the given *fontsize* and *dpi*.
  1823. Returns the parse tree of `Node` instances.
  1824. """
  1825. self._state_stack = [
  1826. ParserState(fonts_object, 'default', 'rm', fontsize, dpi)]
  1827. self._em_width_cache: dict[tuple[str, float, float], float] = {}
  1828. try:
  1829. result = self._expression.parse_string(s)
  1830. except ParseBaseException as err:
  1831. # explain becomes a plain method on pyparsing 3 (err.explain(0)).
  1832. raise ValueError("\n" + ParseException.explain(err, 0)) from None
  1833. self._state_stack = []
  1834. self._in_subscript_or_superscript = False
  1835. # prevent operator spacing from leaking into a new expression
  1836. self._em_width_cache = {}
  1837. ParserElement.reset_cache()
  1838. return T.cast(Hlist, result[0]) # Known return type from main.
  1839. def get_state(self) -> ParserState:
  1840. """Get the current `State` of the parser."""
  1841. return self._state_stack[-1]
  1842. def pop_state(self) -> None:
  1843. """Pop a `State` off of the stack."""
  1844. self._state_stack.pop()
  1845. def push_state(self) -> None:
  1846. """Push a new `State` onto the stack, copying the current state."""
  1847. self._state_stack.append(self.get_state().copy())
  1848. def main(self, toks: ParseResults) -> list[Hlist]:
  1849. return [Hlist(toks.as_list())]
  1850. def math_string(self, toks: ParseResults) -> ParseResults:
  1851. return self._math_expression.parse_string(toks[0][1:-1], parse_all=True)
  1852. def math(self, toks: ParseResults) -> T.Any:
  1853. hlist = Hlist(toks.as_list())
  1854. self.pop_state()
  1855. return [hlist]
  1856. def non_math(self, toks: ParseResults) -> T.Any:
  1857. s = toks[0].replace(r'\$', '$')
  1858. symbols = [Char(c, self.get_state()) for c in s]
  1859. hlist = Hlist(symbols)
  1860. # We're going into math now, so set font to 'it'
  1861. self.push_state()
  1862. self.get_state().font = mpl.rcParams['mathtext.default']
  1863. return [hlist]
  1864. float_literal = staticmethod(pyparsing_common.convert_to_float)
  1865. def text(self, toks: ParseResults) -> T.Any:
  1866. self.push_state()
  1867. state = self.get_state()
  1868. state.font = 'rm'
  1869. hlist = Hlist([Char(c, state) for c in toks[1]])
  1870. self.pop_state()
  1871. return [hlist]
  1872. def _make_space(self, percentage: float) -> Kern:
  1873. # In TeX, an em (the unit usually used to measure horizontal lengths)
  1874. # is not the width of the character 'm'; it is the same in different
  1875. # font styles (e.g. roman or italic). Mathtext, however, uses 'm' in
  1876. # the italic style so that horizontal spaces don't depend on the
  1877. # current font style.
  1878. state = self.get_state()
  1879. key = (state.font, state.fontsize, state.dpi)
  1880. width = self._em_width_cache.get(key)
  1881. if width is None:
  1882. metrics = state.fontset.get_metrics(
  1883. 'it', mpl.rcParams['mathtext.default'], 'm',
  1884. state.fontsize, state.dpi)
  1885. width = metrics.advance
  1886. self._em_width_cache[key] = width
  1887. return Kern(width * percentage)
  1888. _space_widths = {
  1889. r'\,': 0.16667, # 3/18 em = 3 mu
  1890. r'\thinspace': 0.16667, # 3/18 em = 3 mu
  1891. r'\/': 0.16667, # 3/18 em = 3 mu
  1892. r'\>': 0.22222, # 4/18 em = 4 mu
  1893. r'\:': 0.22222, # 4/18 em = 4 mu
  1894. r'\;': 0.27778, # 5/18 em = 5 mu
  1895. r'\ ': 0.33333, # 6/18 em = 6 mu
  1896. r'~': 0.33333, # 6/18 em = 6 mu, nonbreakable
  1897. r'\enspace': 0.5, # 9/18 em = 9 mu
  1898. r'\quad': 1, # 1 em = 18 mu
  1899. r'\qquad': 2, # 2 em = 36 mu
  1900. r'\!': -0.16667, # -3/18 em = -3 mu
  1901. }
  1902. def space(self, toks: ParseResults) -> T.Any:
  1903. num = self._space_widths[toks["space"]]
  1904. box = self._make_space(num)
  1905. return [box]
  1906. def customspace(self, toks: ParseResults) -> T.Any:
  1907. return [self._make_space(toks["space"])]
  1908. def symbol(self, s: str, loc: int,
  1909. toks: ParseResults | dict[str, str]) -> T.Any:
  1910. c = toks["sym"]
  1911. if c == "-":
  1912. # "U+2212 minus sign is the preferred representation of the unary
  1913. # and binary minus sign rather than the ASCII-derived U+002D
  1914. # hyphen-minus, because minus sign is unambiguous and because it
  1915. # is rendered with a more desirable length, usually longer than a
  1916. # hyphen." (https://www.unicode.org/reports/tr25/)
  1917. c = "\N{MINUS SIGN}"
  1918. try:
  1919. char = Char(c, self.get_state())
  1920. except ValueError as err:
  1921. raise ParseFatalException(s, loc,
  1922. "Unknown symbol: %s" % c) from err
  1923. if c in self._spaced_symbols:
  1924. # iterate until we find previous character, needed for cases
  1925. # such as $=-2$, ${ -2}$, $ -2$, or $ -2$.
  1926. prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
  1927. # Binary operators at start of string should not be spaced
  1928. # Also, operators in sub- or superscripts should not be spaced
  1929. if (self._in_subscript_or_superscript or (
  1930. c in self._binary_operators and (
  1931. len(s[:loc].split()) == 0 or prev_char in {
  1932. '{', *self._left_delims, *self._relation_symbols}))):
  1933. return [char]
  1934. else:
  1935. return [Hlist([self._make_space(0.2),
  1936. char,
  1937. self._make_space(0.2)],
  1938. do_kern=True)]
  1939. elif c in self._punctuation_symbols:
  1940. prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
  1941. next_char = next((c for c in s[loc + 1:] if c != ' '), '')
  1942. # Do not space commas between brackets
  1943. if c == ',':
  1944. if prev_char == '{' and next_char == '}':
  1945. return [char]
  1946. # Do not space dots as decimal separators
  1947. if c == '.' and prev_char.isdigit() and next_char.isdigit():
  1948. return [char]
  1949. else:
  1950. return [Hlist([char, self._make_space(0.2)], do_kern=True)]
  1951. return [char]
  1952. def unknown_symbol(self, s: str, loc: int, toks: ParseResults) -> T.Any:
  1953. raise ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}")
  1954. _accent_map = {
  1955. r'hat': r'\circumflexaccent',
  1956. r'breve': r'\combiningbreve',
  1957. r'bar': r'\combiningoverline',
  1958. r'grave': r'\combininggraveaccent',
  1959. r'acute': r'\combiningacuteaccent',
  1960. r'tilde': r'\combiningtilde',
  1961. r'dot': r'\combiningdotabove',
  1962. r'ddot': r'\combiningdiaeresis',
  1963. r'dddot': r'\combiningthreedotsabove',
  1964. r'ddddot': r'\combiningfourdotsabove',
  1965. r'vec': r'\combiningrightarrowabove',
  1966. r'"': r'\combiningdiaeresis',
  1967. r"`": r'\combininggraveaccent',
  1968. r"'": r'\combiningacuteaccent',
  1969. r'~': r'\combiningtilde',
  1970. r'.': r'\combiningdotabove',
  1971. r'^': r'\circumflexaccent',
  1972. r'overrightarrow': r'\rightarrow',
  1973. r'overleftarrow': r'\leftarrow',
  1974. r'mathring': r'\circ',
  1975. }
  1976. _wide_accents = set(r"widehat widetilde widebar".split())
  1977. def accent(self, toks: ParseResults) -> T.Any:
  1978. state = self.get_state()
  1979. thickness = state.get_current_underline_thickness()
  1980. accent = toks["accent"]
  1981. sym = toks["sym"]
  1982. accent_box: Node
  1983. if accent in self._wide_accents:
  1984. accent_box = AutoWidthChar(
  1985. '\\' + accent, sym.width, state, char_class=Accent)
  1986. else:
  1987. accent_box = Accent(self._accent_map[accent], state)
  1988. if accent == 'mathring':
  1989. accent_box.shrink()
  1990. accent_box.shrink()
  1991. centered = HCentered([Hbox(sym.width / 4.0), accent_box])
  1992. centered.hpack(sym.width, 'exactly')
  1993. return Vlist([
  1994. centered,
  1995. Vbox(0., thickness * 2.0),
  1996. Hlist([sym])
  1997. ])
  1998. def function(self, s: str, loc: int, toks: ParseResults) -> T.Any:
  1999. hlist = self.operatorname(s, loc, toks)
  2000. hlist.function_name = toks["name"]
  2001. return hlist
  2002. def operatorname(self, s: str, loc: int, toks: ParseResults) -> T.Any:
  2003. self.push_state()
  2004. state = self.get_state()
  2005. state.font = 'rm'
  2006. hlist_list: list[Node] = []
  2007. # Change the font of Chars, but leave Kerns alone
  2008. name = toks["name"]
  2009. for c in name:
  2010. if isinstance(c, Char):
  2011. c.font = 'rm'
  2012. c._update_metrics()
  2013. hlist_list.append(c)
  2014. elif isinstance(c, str):
  2015. hlist_list.append(Char(c, state))
  2016. else:
  2017. hlist_list.append(c)
  2018. next_char_loc = loc + len(name) + 1
  2019. if isinstance(name, ParseResults):
  2020. next_char_loc += len('operatorname{}')
  2021. next_char = next((c for c in s[next_char_loc:] if c != ' '), '')
  2022. delimiters = self._delims | {'^', '_'}
  2023. if (next_char not in delimiters and
  2024. name not in self._overunder_functions):
  2025. # Add thin space except when followed by parenthesis, bracket, etc.
  2026. hlist_list += [self._make_space(self._space_widths[r'\,'])]
  2027. self.pop_state()
  2028. # if followed by a super/subscript, set flag to true
  2029. # This flag tells subsuper to add space after this operator
  2030. if next_char in {'^', '_'}:
  2031. self._in_subscript_or_superscript = True
  2032. else:
  2033. self._in_subscript_or_superscript = False
  2034. return Hlist(hlist_list)
  2035. def start_group(self, toks: ParseResults) -> T.Any:
  2036. self.push_state()
  2037. # Deal with LaTeX-style font tokens
  2038. if toks.get("font"):
  2039. self.get_state().font = toks.get("font")
  2040. return []
  2041. def group(self, toks: ParseResults) -> T.Any:
  2042. grp = Hlist(toks.get("group", []))
  2043. return [grp]
  2044. def required_group(self, toks: ParseResults) -> T.Any:
  2045. return Hlist(toks.get("group", []))
  2046. optional_group = required_group
  2047. def end_group(self) -> T.Any:
  2048. self.pop_state()
  2049. return []
  2050. def unclosed_group(self, s: str, loc: int, toks: ParseResults) -> T.Any:
  2051. raise ParseFatalException(s, len(s), "Expected '}'")
  2052. def font(self, toks: ParseResults) -> T.Any:
  2053. self.get_state().font = toks["font"]
  2054. return []
  2055. def is_overunder(self, nucleus: Node) -> bool:
  2056. if isinstance(nucleus, Char):
  2057. return nucleus.c in self._overunder_symbols
  2058. elif isinstance(nucleus, Hlist) and hasattr(nucleus, 'function_name'):
  2059. return nucleus.function_name in self._overunder_functions
  2060. return False
  2061. def is_dropsub(self, nucleus: Node) -> bool:
  2062. if isinstance(nucleus, Char):
  2063. return nucleus.c in self._dropsub_symbols
  2064. return False
  2065. def is_slanted(self, nucleus: Node) -> bool:
  2066. if isinstance(nucleus, Char):
  2067. return nucleus.is_slanted()
  2068. return False
  2069. def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any:
  2070. nucleus = toks.get("nucleus", Hbox(0))
  2071. subsuper = toks.get("subsuper", [])
  2072. napostrophes = len(toks.get("apostrophes", []))
  2073. if not subsuper and not napostrophes:
  2074. return nucleus
  2075. sub = super = None
  2076. while subsuper:
  2077. op, arg, *subsuper = subsuper
  2078. if op == '_':
  2079. if sub is not None:
  2080. raise ParseFatalException("Double subscript")
  2081. sub = arg
  2082. else:
  2083. if super is not None:
  2084. raise ParseFatalException("Double superscript")
  2085. super = arg
  2086. state = self.get_state()
  2087. rule_thickness = state.fontset.get_underline_thickness(
  2088. state.font, state.fontsize, state.dpi)
  2089. xHeight = state.fontset.get_xheight(
  2090. state.font, state.fontsize, state.dpi)
  2091. if napostrophes:
  2092. if super is None:
  2093. super = Hlist([])
  2094. for i in range(napostrophes):
  2095. super.children.extend(self.symbol(s, loc, {"sym": "\\prime"}))
  2096. # kern() and hpack() needed to get the metrics right after
  2097. # extending
  2098. super.kern()
  2099. super.hpack()
  2100. # Handle over/under symbols, such as sum or prod
  2101. if self.is_overunder(nucleus):
  2102. vlist = []
  2103. shift = 0.
  2104. width = nucleus.width
  2105. if super is not None:
  2106. super.shrink()
  2107. width = max(width, super.width)
  2108. if sub is not None:
  2109. sub.shrink()
  2110. width = max(width, sub.width)
  2111. vgap = rule_thickness * 3.0
  2112. if super is not None:
  2113. hlist = HCentered([super])
  2114. hlist.hpack(width, 'exactly')
  2115. vlist.extend([hlist, Vbox(0, vgap)])
  2116. hlist = HCentered([nucleus])
  2117. hlist.hpack(width, 'exactly')
  2118. vlist.append(hlist)
  2119. if sub is not None:
  2120. hlist = HCentered([sub])
  2121. hlist.hpack(width, 'exactly')
  2122. vlist.extend([Vbox(0, vgap), hlist])
  2123. shift = hlist.height + vgap + nucleus.depth
  2124. vlt = Vlist(vlist)
  2125. vlt.shift_amount = shift
  2126. result = Hlist([vlt])
  2127. return [result]
  2128. # We remove kerning on the last character for consistency (otherwise
  2129. # it will compute kerning based on non-shrunk characters and may put
  2130. # them too close together when superscripted)
  2131. # We change the width of the last character to match the advance to
  2132. # consider some fonts with weird metrics: e.g. stix's f has a width of
  2133. # 7.75 and a kerning of -4.0 for an advance of 3.72, and we want to put
  2134. # the superscript at the advance
  2135. last_char = nucleus
  2136. if isinstance(nucleus, Hlist):
  2137. new_children = nucleus.children
  2138. if len(new_children):
  2139. # remove last kern
  2140. if (isinstance(new_children[-1], Kern) and
  2141. isinstance(new_children[-2], Char)):
  2142. new_children = new_children[:-1]
  2143. last_char = new_children[-1]
  2144. if isinstance(last_char, Char):
  2145. last_char.width = last_char._metrics.advance
  2146. # create new Hlist without kerning
  2147. nucleus = Hlist(new_children, do_kern=False)
  2148. else:
  2149. if isinstance(nucleus, Char):
  2150. last_char.width = last_char._metrics.advance
  2151. nucleus = Hlist([nucleus])
  2152. # Handle regular sub/superscripts
  2153. constants = _get_font_constant_set(state)
  2154. lc_height = last_char.height
  2155. lc_baseline = 0
  2156. if self.is_dropsub(last_char):
  2157. lc_baseline = last_char.depth
  2158. # Compute kerning for sub and super
  2159. superkern = constants.delta * xHeight
  2160. subkern = constants.delta * xHeight
  2161. if self.is_slanted(last_char):
  2162. superkern += constants.delta * xHeight
  2163. superkern += (constants.delta_slanted *
  2164. (lc_height - xHeight * 2. / 3.))
  2165. if self.is_dropsub(last_char):
  2166. subkern = (3 * constants.delta -
  2167. constants.delta_integral) * lc_height
  2168. superkern = (3 * constants.delta +
  2169. constants.delta_integral) * lc_height
  2170. else:
  2171. subkern = 0
  2172. x: List
  2173. if super is None:
  2174. # node757
  2175. # Note: One of super or sub must be a Node if we're in this function, but
  2176. # mypy can't know this, since it can't interpret pyparsing expressions,
  2177. # hence the cast.
  2178. x = Hlist([Kern(subkern), T.cast(Node, sub)])
  2179. x.shrink()
  2180. if self.is_dropsub(last_char):
  2181. shift_down = lc_baseline + constants.subdrop * xHeight
  2182. else:
  2183. shift_down = constants.sub1 * xHeight
  2184. x.shift_amount = shift_down
  2185. else:
  2186. x = Hlist([Kern(superkern), super])
  2187. x.shrink()
  2188. if self.is_dropsub(last_char):
  2189. shift_up = lc_height - constants.subdrop * xHeight
  2190. else:
  2191. shift_up = constants.sup1 * xHeight
  2192. if sub is None:
  2193. x.shift_amount = -shift_up
  2194. else: # Both sub and superscript
  2195. y = Hlist([Kern(subkern), sub])
  2196. y.shrink()
  2197. if self.is_dropsub(last_char):
  2198. shift_down = lc_baseline + constants.subdrop * xHeight
  2199. else:
  2200. shift_down = constants.sub2 * xHeight
  2201. # If sub and superscript collide, move super up
  2202. clr = (2.0 * rule_thickness -
  2203. ((shift_up - x.depth) - (y.height - shift_down)))
  2204. if clr > 0.:
  2205. shift_up += clr
  2206. x = Vlist([
  2207. x,
  2208. Kern((shift_up - x.depth) - (y.height - shift_down)),
  2209. y])
  2210. x.shift_amount = shift_down
  2211. if not self.is_dropsub(last_char):
  2212. x.width += constants.script_space * xHeight
  2213. # Do we need to add a space after the nucleus?
  2214. # To find out, check the flag set by operatorname
  2215. spaced_nucleus: list[Node] = [nucleus, x]
  2216. if self._in_subscript_or_superscript:
  2217. spaced_nucleus += [self._make_space(self._space_widths[r'\,'])]
  2218. self._in_subscript_or_superscript = False
  2219. result = Hlist(spaced_nucleus)
  2220. return [result]
  2221. def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathStyle,
  2222. num: Hlist, den: Hlist) -> T.Any:
  2223. state = self.get_state()
  2224. thickness = state.get_current_underline_thickness()
  2225. for _ in range(style.value):
  2226. num.shrink()
  2227. den.shrink()
  2228. cnum = HCentered([num])
  2229. cden = HCentered([den])
  2230. width = max(num.width, den.width)
  2231. cnum.hpack(width, 'exactly')
  2232. cden.hpack(width, 'exactly')
  2233. vlist = Vlist([cnum, # numerator
  2234. Vbox(0, thickness * 2.0), # space
  2235. Hrule(state, rule), # rule
  2236. Vbox(0, thickness * 2.0), # space
  2237. cden # denominator
  2238. ])
  2239. # Shift so the fraction line sits in the middle of the
  2240. # equals sign
  2241. metrics = state.fontset.get_metrics(
  2242. state.font, mpl.rcParams['mathtext.default'],
  2243. '=', state.fontsize, state.dpi)
  2244. shift = (cden.height -
  2245. ((metrics.ymax + metrics.ymin) / 2 -
  2246. thickness * 3.0))
  2247. vlist.shift_amount = shift
  2248. result = [Hlist([vlist, Hbox(thickness * 2.)])]
  2249. if ldelim or rdelim:
  2250. if ldelim == '':
  2251. ldelim = '.'
  2252. if rdelim == '':
  2253. rdelim = '.'
  2254. return self._auto_sized_delimiter(ldelim,
  2255. T.cast(list[Box | Char | str],
  2256. result),
  2257. rdelim)
  2258. return result
  2259. def style_literal(self, toks: ParseResults) -> T.Any:
  2260. return self._MathStyle(int(toks["style_literal"]))
  2261. def genfrac(self, toks: ParseResults) -> T.Any:
  2262. return self._genfrac(
  2263. toks.get("ldelim", ""), toks.get("rdelim", ""),
  2264. toks["rulesize"], toks.get("style", self._MathStyle.TEXTSTYLE),
  2265. toks["num"], toks["den"])
  2266. def frac(self, toks: ParseResults) -> T.Any:
  2267. return self._genfrac(
  2268. "", "", self.get_state().get_current_underline_thickness(),
  2269. self._MathStyle.TEXTSTYLE, toks["num"], toks["den"])
  2270. def dfrac(self, toks: ParseResults) -> T.Any:
  2271. return self._genfrac(
  2272. "", "", self.get_state().get_current_underline_thickness(),
  2273. self._MathStyle.DISPLAYSTYLE, toks["num"], toks["den"])
  2274. def binom(self, toks: ParseResults) -> T.Any:
  2275. return self._genfrac(
  2276. "(", ")", 0,
  2277. self._MathStyle.TEXTSTYLE, toks["num"], toks["den"])
  2278. def _genset(self, s: str, loc: int, toks: ParseResults) -> T.Any:
  2279. annotation = toks["annotation"]
  2280. body = toks["body"]
  2281. thickness = self.get_state().get_current_underline_thickness()
  2282. annotation.shrink()
  2283. centered_annotation = HCentered([annotation])
  2284. centered_body = HCentered([body])
  2285. width = max(centered_annotation.width, centered_body.width)
  2286. centered_annotation.hpack(width, 'exactly')
  2287. centered_body.hpack(width, 'exactly')
  2288. vgap = thickness * 3
  2289. if s[loc + 1] == "u": # \underset
  2290. vlist = Vlist([
  2291. centered_body, # body
  2292. Vbox(0, vgap), # space
  2293. centered_annotation # annotation
  2294. ])
  2295. # Shift so the body sits in the same vertical position
  2296. vlist.shift_amount = centered_body.depth + centered_annotation.height + vgap
  2297. else: # \overset
  2298. vlist = Vlist([
  2299. centered_annotation, # annotation
  2300. Vbox(0, vgap), # space
  2301. centered_body # body
  2302. ])
  2303. # To add horizontal gap between symbols: wrap the Vlist into
  2304. # an Hlist and extend it with an Hbox(0, horizontal_gap)
  2305. return vlist
  2306. overset = underset = _genset
  2307. def sqrt(self, toks: ParseResults) -> T.Any:
  2308. root = toks.get("root")
  2309. body = toks["value"]
  2310. state = self.get_state()
  2311. thickness = state.get_current_underline_thickness()
  2312. # Determine the height of the body, and add a little extra to
  2313. # the height so it doesn't seem cramped
  2314. height = body.height - body.shift_amount + thickness * 5.0
  2315. depth = body.depth + body.shift_amount
  2316. check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True)
  2317. height = check.height - check.shift_amount
  2318. depth = check.depth + check.shift_amount
  2319. # Put a little extra space to the left and right of the body
  2320. padded_body = Hlist([Hbox(2 * thickness), body, Hbox(2 * thickness)])
  2321. rightside = Vlist([Hrule(state), Glue('fill'), padded_body])
  2322. # Stretch the glue between the hrule and the body
  2323. rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
  2324. 'exactly', depth)
  2325. # Add the root and shift it upward so it is above the tick.
  2326. # The value of 0.6 is a hard-coded hack ;)
  2327. if not root:
  2328. root = Box(check.width * 0.5, 0., 0.)
  2329. else:
  2330. root = Hlist(root)
  2331. root.shrink()
  2332. root.shrink()
  2333. root_vlist = Vlist([Hlist([root])])
  2334. root_vlist.shift_amount = -height * 0.6
  2335. hlist = Hlist([root_vlist, # Root
  2336. # Negative kerning to put root over tick
  2337. Kern(-check.width * 0.5),
  2338. check, # Check
  2339. rightside]) # Body
  2340. return [hlist]
  2341. def overline(self, toks: ParseResults) -> T.Any:
  2342. body = toks["body"]
  2343. state = self.get_state()
  2344. thickness = state.get_current_underline_thickness()
  2345. height = body.height - body.shift_amount + thickness * 3.0
  2346. depth = body.depth + body.shift_amount
  2347. # Place overline above body
  2348. rightside = Vlist([Hrule(state), Glue('fill'), Hlist([body])])
  2349. # Stretch the glue between the hrule and the body
  2350. rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
  2351. 'exactly', depth)
  2352. hlist = Hlist([rightside])
  2353. return [hlist]
  2354. def _auto_sized_delimiter(self, front: str,
  2355. middle: list[Box | Char | str],
  2356. back: str) -> T.Any:
  2357. state = self.get_state()
  2358. if len(middle):
  2359. height = max([x.height for x in middle if not isinstance(x, str)])
  2360. depth = max([x.depth for x in middle if not isinstance(x, str)])
  2361. factor = None
  2362. for idx, el in enumerate(middle):
  2363. if el == r'\middle':
  2364. c = T.cast(str, middle[idx + 1]) # Should be one of p.delims.
  2365. if c != '.':
  2366. middle[idx + 1] = AutoHeightChar(
  2367. c, height, depth, state, factor=factor)
  2368. else:
  2369. middle.remove(c)
  2370. del middle[idx]
  2371. # There should only be \middle and its delimiter as str, which have
  2372. # just been removed.
  2373. middle_part = T.cast(list[Box | Char], middle)
  2374. else:
  2375. height = 0
  2376. depth = 0
  2377. factor = 1.0
  2378. middle_part = []
  2379. parts: list[Node] = []
  2380. # \left. and \right. aren't supposed to produce any symbols
  2381. if front != '.':
  2382. parts.append(
  2383. AutoHeightChar(front, height, depth, state, factor=factor))
  2384. parts.extend(middle_part)
  2385. if back != '.':
  2386. parts.append(
  2387. AutoHeightChar(back, height, depth, state, factor=factor))
  2388. hlist = Hlist(parts)
  2389. return hlist
  2390. def auto_delim(self, toks: ParseResults) -> T.Any:
  2391. return self._auto_sized_delimiter(
  2392. toks["left"],
  2393. # if "mid" in toks ... can be removed when requiring pyparsing 3.
  2394. toks["mid"].as_list() if "mid" in toks else [],
  2395. toks["right"])
  2396. def boldsymbol(self, toks: ParseResults) -> T.Any:
  2397. self.push_state()
  2398. state = self.get_state()
  2399. hlist: list[Node] = []
  2400. name = toks["value"]
  2401. for c in name:
  2402. if isinstance(c, Hlist):
  2403. k = c.children[1]
  2404. if isinstance(k, Char):
  2405. k.font = "bf"
  2406. k._update_metrics()
  2407. hlist.append(c)
  2408. elif isinstance(c, Char):
  2409. c.font = "bf"
  2410. if (c.c in self._latin_alphabets or
  2411. c.c[1:] in self._small_greek):
  2412. c.font = "bfit"
  2413. c._update_metrics()
  2414. c._update_metrics()
  2415. hlist.append(c)
  2416. else:
  2417. hlist.append(c)
  2418. self.pop_state()
  2419. return Hlist(hlist)
  2420. def substack(self, toks: ParseResults) -> T.Any:
  2421. parts = toks["parts"]
  2422. state = self.get_state()
  2423. thickness = state.get_current_underline_thickness()
  2424. hlist = [Hlist(k) for k in parts[0]]
  2425. max_width = max(map(lambda c: c.width, hlist))
  2426. vlist = []
  2427. for sub in hlist:
  2428. cp = HCentered([sub])
  2429. cp.hpack(max_width, 'exactly')
  2430. vlist.append(cp)
  2431. stack = [val
  2432. for pair in zip(vlist, [Vbox(0, thickness * 2)] * len(vlist))
  2433. for val in pair]
  2434. del stack[-1]
  2435. vlt = Vlist(stack)
  2436. result = [Hlist([vlt])]
  2437. return result