Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/_mathtext.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1360 statements  

1""" 

2Implementation details for :mod:`.mathtext`. 

3""" 

4 

5from __future__ import annotations 

6 

7import abc 

8import copy 

9import enum 

10import functools 

11import logging 

12import os 

13import re 

14import types 

15import unicodedata 

16import string 

17import typing as T 

18from typing import NamedTuple 

19 

20import numpy as np 

21from pyparsing import ( 

22 Empty, Forward, Literal, NotAny, oneOf, OneOrMore, Optional, 

23 ParseBaseException, ParseException, ParseExpression, ParseFatalException, 

24 ParserElement, ParseResults, QuotedString, Regex, StringEnd, ZeroOrMore, 

25 pyparsing_common, Group) 

26 

27import matplotlib as mpl 

28from . import cbook 

29from ._mathtext_data import ( 

30 latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) 

31from .font_manager import FontProperties, findfont, get_font 

32from .ft2font import FT2Font, FT2Image, KERNING_DEFAULT 

33 

34from packaging.version import parse as parse_version 

35from pyparsing import __version__ as pyparsing_version 

36if parse_version(pyparsing_version).major < 3: 

37 from pyparsing import nestedExpr as nested_expr 

38else: 

39 from pyparsing import nested_expr 

40 

41if T.TYPE_CHECKING: 

42 from collections.abc import Iterable 

43 from .ft2font import Glyph 

44 

45ParserElement.enablePackrat() 

46_log = logging.getLogger("matplotlib.mathtext") 

47 

48 

49############################################################################## 

50# FONTS 

51 

52 

53def get_unicode_index(symbol: str) -> int: # Publicly exported. 

54 r""" 

55 Return the integer index (from the Unicode table) of *symbol*. 

56 

57 Parameters 

58 ---------- 

59 symbol : str 

60 A single (Unicode) character, a TeX command (e.g. r'\pi') or a Type1 

61 symbol name (e.g. 'phi'). 

62 """ 

63 try: # This will succeed if symbol is a single Unicode char 

64 return ord(symbol) 

65 except TypeError: 

66 pass 

67 try: # Is symbol a TeX symbol (i.e. \alpha) 

68 return tex2uni[symbol.strip("\\")] 

69 except KeyError as err: 

70 raise ValueError( 

71 f"{symbol!r} is not a valid Unicode character or TeX/Type1 symbol" 

72 ) from err 

73 

74 

75class VectorParse(NamedTuple): 

76 """ 

77 The namedtuple type returned by ``MathTextParser("path").parse(...)``. 

78 

79 Attributes 

80 ---------- 

81 width, height, depth : float 

82 The global metrics. 

83 glyphs : list 

84 The glyphs including their positions. 

85 rect : list 

86 The list of rectangles. 

87 """ 

88 width: float 

89 height: float 

90 depth: float 

91 glyphs: list[tuple[FT2Font, float, int, float, float]] 

92 rects: list[tuple[float, float, float, float]] 

93 

94VectorParse.__module__ = "matplotlib.mathtext" 

95 

96 

97class RasterParse(NamedTuple): 

98 """ 

99 The namedtuple type returned by ``MathTextParser("agg").parse(...)``. 

100 

101 Attributes 

102 ---------- 

103 ox, oy : float 

104 The offsets are always zero. 

105 width, height, depth : float 

106 The global metrics. 

107 image : FT2Image 

108 A raster image. 

109 """ 

110 ox: float 

111 oy: float 

112 width: float 

113 height: float 

114 depth: float 

115 image: FT2Image 

116 

117RasterParse.__module__ = "matplotlib.mathtext" 

118 

119 

120class Output: 

121 r""" 

122 Result of `ship`\ping a box: lists of positioned glyphs and rectangles. 

123 

124 This class is not exposed to end users, but converted to a `VectorParse` or 

125 a `RasterParse` by `.MathTextParser.parse`. 

126 """ 

127 

128 def __init__(self, box: Box): 

129 self.box = box 

130 self.glyphs: list[tuple[float, float, FontInfo]] = [] # (ox, oy, info) 

131 self.rects: list[tuple[float, float, float, float]] = [] # (x1, y1, x2, y2) 

132 

133 def to_vector(self) -> VectorParse: 

134 w, h, d = map( 

135 np.ceil, [self.box.width, self.box.height, self.box.depth]) 

136 gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset) 

137 for ox, oy, info in self.glyphs] 

138 rs = [(x1, h - y2, x2 - x1, y2 - y1) 

139 for x1, y1, x2, y2 in self.rects] 

140 return VectorParse(w, h + d, d, gs, rs) 

141 

142 def to_raster(self, *, antialiased: bool) -> RasterParse: 

143 # Metrics y's and mathtext y's are oriented in opposite directions, 

144 # hence the switch between ymin and ymax. 

145 xmin = min([*[ox + info.metrics.xmin for ox, oy, info in self.glyphs], 

146 *[x1 for x1, y1, x2, y2 in self.rects], 0]) - 1 

147 ymin = min([*[oy - info.metrics.ymax for ox, oy, info in self.glyphs], 

148 *[y1 for x1, y1, x2, y2 in self.rects], 0]) - 1 

149 xmax = max([*[ox + info.metrics.xmax for ox, oy, info in self.glyphs], 

150 *[x2 for x1, y1, x2, y2 in self.rects], 0]) + 1 

151 ymax = max([*[oy - info.metrics.ymin for ox, oy, info in self.glyphs], 

152 *[y2 for x1, y1, x2, y2 in self.rects], 0]) + 1 

153 w = xmax - xmin 

154 h = ymax - ymin - self.box.depth 

155 d = ymax - ymin - self.box.height 

156 image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0))) 

157 

158 # Ideally, we could just use self.glyphs and self.rects here, shifting 

159 # their coordinates by (-xmin, -ymin), but this yields slightly 

160 # different results due to floating point slop; shipping twice is the 

161 # old approach and keeps baseline images backcompat. 

162 shifted = ship(self.box, (-xmin, -ymin)) 

163 

164 for ox, oy, info in shifted.glyphs: 

165 info.font.draw_glyph_to_bitmap( 

166 image, ox, oy - info.metrics.iceberg, info.glyph, 

167 antialiased=antialiased) 

168 for x1, y1, x2, y2 in shifted.rects: 

169 height = max(int(y2 - y1) - 1, 0) 

170 if height == 0: 

171 center = (y2 + y1) / 2 

172 y = int(center - (height + 1) / 2) 

173 else: 

174 y = int(y1) 

175 image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) 

176 return RasterParse(0, 0, w, h + d, d, image) 

177 

178 

179class FontMetrics(NamedTuple): 

180 """ 

181 Metrics of a font. 

182 

183 Attributes 

184 ---------- 

185 advance : float 

186 The advance distance (in points) of the glyph. 

187 height : float 

188 The height of the glyph in points. 

189 width : float 

190 The width of the glyph in points. 

191 xmin, xmax, ymin, ymax : float 

192 The ink rectangle of the glyph. 

193 iceberg : float 

194 The distance from the baseline to the top of the glyph. (This corresponds to 

195 TeX's definition of "height".) 

196 slanted : bool 

197 Whether the glyph should be considered as "slanted" (currently used for kerning 

198 sub/superscripts). 

199 """ 

200 advance: float 

201 height: float 

202 width: float 

203 xmin: float 

204 xmax: float 

205 ymin: float 

206 ymax: float 

207 iceberg: float 

208 slanted: bool 

209 

210 

211class FontInfo(NamedTuple): 

212 font: FT2Font 

213 fontsize: float 

214 postscript_name: str 

215 metrics: FontMetrics 

216 num: int 

217 glyph: Glyph 

218 offset: float 

219 

220 

221class Fonts(abc.ABC): 

222 """ 

223 An abstract base class for a system of fonts to use for mathtext. 

224 

225 The class must be able to take symbol keys and font file names and 

226 return the character metrics. It also delegates to a backend class 

227 to do the actual drawing. 

228 """ 

229 

230 def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): 

231 """ 

232 Parameters 

233 ---------- 

234 default_font_prop : `~.font_manager.FontProperties` 

235 The default non-math font, or the base font for Unicode (generic) 

236 font rendering. 

237 load_glyph_flags : int 

238 Flags passed to the glyph loader (e.g. ``FT_Load_Glyph`` and 

239 ``FT_Load_Char`` for FreeType-based fonts). 

240 """ 

241 self.default_font_prop = default_font_prop 

242 self.load_glyph_flags = load_glyph_flags 

243 

244 def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, 

245 font2: str, fontclass2: str, sym2: str, fontsize2: float, 

246 dpi: float) -> float: 

247 """ 

248 Get the kerning distance for font between *sym1* and *sym2*. 

249 

250 See `~.Fonts.get_metrics` for a detailed description of the parameters. 

251 """ 

252 return 0. 

253 

254 def _get_font(self, font: str) -> FT2Font: 

255 raise NotImplementedError 

256 

257 def _get_info(self, font: str, font_class: str, sym: str, fontsize: float, 

258 dpi: float) -> FontInfo: 

259 raise NotImplementedError 

260 

261 def get_metrics(self, font: str, font_class: str, sym: str, fontsize: float, 

262 dpi: float) -> FontMetrics: 

263 r""" 

264 Parameters 

265 ---------- 

266 font : str 

267 One of the TeX font names: "tt", "it", "rm", "cal", "sf", "bf", 

268 "default", "regular", "bb", "frak", "scr". "default" and "regular" 

269 are synonyms and use the non-math font. 

270 font_class : str 

271 One of the TeX font names (as for *font*), but **not** "bb", 

272 "frak", or "scr". This is used to combine two font classes. The 

273 only supported combination currently is ``get_metrics("frak", "bf", 

274 ...)``. 

275 sym : str 

276 A symbol in raw TeX form, e.g., "1", "x", or "\sigma". 

277 fontsize : float 

278 Font size in points. 

279 dpi : float 

280 Rendering dots-per-inch. 

281 

282 Returns 

283 ------- 

284 FontMetrics 

285 """ 

286 info = self._get_info(font, font_class, sym, fontsize, dpi) 

287 return info.metrics 

288 

289 def render_glyph(self, output: Output, ox: float, oy: float, font: str, 

290 font_class: str, sym: str, fontsize: float, dpi: float) -> None: 

291 """ 

292 At position (*ox*, *oy*), draw the glyph specified by the remaining 

293 parameters (see `get_metrics` for their detailed description). 

294 """ 

295 info = self._get_info(font, font_class, sym, fontsize, dpi) 

296 output.glyphs.append((ox, oy, info)) 

297 

298 def render_rect_filled(self, output: Output, 

299 x1: float, y1: float, x2: float, y2: float) -> None: 

300 """ 

301 Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*). 

302 """ 

303 output.rects.append((x1, y1, x2, y2)) 

304 

305 def get_xheight(self, font: str, fontsize: float, dpi: float) -> float: 

306 """ 

307 Get the xheight for the given *font* and *fontsize*. 

308 """ 

309 raise NotImplementedError() 

310 

311 def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float: 

312 """ 

313 Get the line thickness that matches the given font. Used as a 

314 base unit for drawing lines such as in a fraction or radical. 

315 """ 

316 raise NotImplementedError() 

317 

318 def get_sized_alternatives_for_symbol(self, fontname: str, 

319 sym: str) -> list[tuple[str, str]]: 

320 """ 

321 Override if your font provides multiple sizes of the same 

322 symbol. Should return a list of symbols matching *sym* in 

323 various sizes. The expression renderer will select the most 

324 appropriate size for a given situation from this list. 

325 """ 

326 return [(fontname, sym)] 

327 

328 

329class TruetypeFonts(Fonts, metaclass=abc.ABCMeta): 

330 """ 

331 A generic base class for all font setups that use Truetype fonts 

332 (through FT2Font). 

333 """ 

334 

335 def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): 

336 super().__init__(default_font_prop, load_glyph_flags) 

337 # Per-instance cache. 

338 self._get_info = functools.cache(self._get_info) # type: ignore[method-assign] 

339 self._fonts = {} 

340 self.fontmap: dict[str | int, str] = {} 

341 

342 filename = findfont(self.default_font_prop) 

343 default_font = get_font(filename) 

344 self._fonts['default'] = default_font 

345 self._fonts['regular'] = default_font 

346 

347 def _get_font(self, font: str | int) -> FT2Font: 

348 if font in self.fontmap: 

349 basename = self.fontmap[font] 

350 else: 

351 # NOTE: An int is only passed by subclasses which have placed int keys into 

352 # `self.fontmap`, so we must cast this to confirm it to typing. 

353 basename = T.cast(str, font) 

354 cached_font = self._fonts.get(basename) 

355 if cached_font is None and os.path.exists(basename): 

356 cached_font = get_font(basename) 

357 self._fonts[basename] = cached_font 

358 self._fonts[cached_font.postscript_name] = cached_font 

359 self._fonts[cached_font.postscript_name.lower()] = cached_font 

360 return T.cast(FT2Font, cached_font) # FIXME: Not sure this is guaranteed. 

361 

362 def _get_offset(self, font: FT2Font, glyph: Glyph, fontsize: float, 

363 dpi: float) -> float: 

364 if font.postscript_name == 'Cmex10': 

365 return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72) 

366 return 0. 

367 

368 def _get_glyph(self, fontname: str, font_class: str, 

369 sym: str) -> tuple[FT2Font, int, bool]: 

370 raise NotImplementedError 

371 

372 # The return value of _get_info is cached per-instance. 

373 def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, 

374 dpi: float) -> FontInfo: 

375 font, num, slanted = self._get_glyph(fontname, font_class, sym) 

376 font.set_size(fontsize, dpi) 

377 glyph = font.load_char(num, flags=self.load_glyph_flags) 

378 

379 xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] 

380 offset = self._get_offset(font, glyph, fontsize, dpi) 

381 metrics = FontMetrics( 

382 advance = glyph.linearHoriAdvance/65536.0, 

383 height = glyph.height/64.0, 

384 width = glyph.width/64.0, 

385 xmin = xmin, 

386 xmax = xmax, 

387 ymin = ymin+offset, 

388 ymax = ymax+offset, 

389 # iceberg is the equivalent of TeX's "height" 

390 iceberg = glyph.horiBearingY/64.0 + offset, 

391 slanted = slanted 

392 ) 

393 

394 return FontInfo( 

395 font = font, 

396 fontsize = fontsize, 

397 postscript_name = font.postscript_name, 

398 metrics = metrics, 

399 num = num, 

400 glyph = glyph, 

401 offset = offset 

402 ) 

403 

404 def get_xheight(self, fontname: str, fontsize: float, dpi: float) -> float: 

405 font = self._get_font(fontname) 

406 font.set_size(fontsize, dpi) 

407 pclt = font.get_sfnt_table('pclt') 

408 if pclt is None: 

409 # Some fonts don't store the xHeight, so we do a poor man's xHeight 

410 metrics = self.get_metrics( 

411 fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi) 

412 return metrics.iceberg 

413 xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0) 

414 return xHeight 

415 

416 def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float: 

417 # This function used to grab underline thickness from the font 

418 # metrics, but that information is just too un-reliable, so it 

419 # is now hardcoded. 

420 return ((0.75 / 12.0) * fontsize * dpi) / 72.0 

421 

422 def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, 

423 font2: str, fontclass2: str, sym2: str, fontsize2: float, 

424 dpi: float) -> float: 

425 if font1 == font2 and fontsize1 == fontsize2: 

426 info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) 

427 info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) 

428 font = info1.font 

429 return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64 

430 return super().get_kern(font1, fontclass1, sym1, fontsize1, 

431 font2, fontclass2, sym2, fontsize2, dpi) 

432 

433 

434class BakomaFonts(TruetypeFonts): 

435 """ 

436 Use the Bakoma TrueType fonts for rendering. 

437 

438 Symbols are strewn about a number of font files, each of which has 

439 its own proprietary 8-bit encoding. 

440 """ 

441 _fontmap = { 

442 'cal': 'cmsy10', 

443 'rm': 'cmr10', 

444 'tt': 'cmtt10', 

445 'it': 'cmmi10', 

446 'bf': 'cmb10', 

447 'sf': 'cmss10', 

448 'ex': 'cmex10', 

449 } 

450 

451 def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): 

452 self._stix_fallback = StixFonts(default_font_prop, load_glyph_flags) 

453 

454 super().__init__(default_font_prop, load_glyph_flags) 

455 for key, val in self._fontmap.items(): 

456 fullpath = findfont(val) 

457 self.fontmap[key] = fullpath 

458 self.fontmap[val] = fullpath 

459 

460 _slanted_symbols = set(r"\int \oint".split()) 

461 

462 def _get_glyph(self, fontname: str, font_class: str, 

463 sym: str) -> tuple[FT2Font, int, bool]: 

464 font = None 

465 if fontname in self.fontmap and sym in latex_to_bakoma: 

466 basename, num = latex_to_bakoma[sym] 

467 slanted = (basename == "cmmi10") or sym in self._slanted_symbols 

468 font = self._get_font(basename) 

469 elif len(sym) == 1: 

470 slanted = (fontname == "it") 

471 font = self._get_font(fontname) 

472 if font is not None: 

473 num = ord(sym) 

474 if font is not None and font.get_char_index(num) != 0: 

475 return font, num, slanted 

476 else: 

477 return self._stix_fallback._get_glyph(fontname, font_class, sym) 

478 

479 # The Bakoma fonts contain many pre-sized alternatives for the 

480 # delimiters. The AutoSizedChar class will use these alternatives 

481 # and select the best (closest sized) glyph. 

482 _size_alternatives = { 

483 '(': [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'), 

484 ('ex', '\xb5'), ('ex', '\xc3')], 

485 ')': [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'), 

486 ('ex', '\xb6'), ('ex', '\x21')], 

487 '{': [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'), 

488 ('ex', '\xbd'), ('ex', '\x28')], 

489 '}': [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'), 

490 ('ex', '\xbe'), ('ex', '\x29')], 

491 # The fourth size of '[' is mysteriously missing from the BaKoMa 

492 # font, so I've omitted it for both '[' and ']' 

493 '[': [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'), 

494 ('ex', '\x22')], 

495 ']': [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'), 

496 ('ex', '\x23')], 

497 r'\lfloor': [('ex', '\xa5'), ('ex', '\x6a'), 

498 ('ex', '\xb9'), ('ex', '\x24')], 

499 r'\rfloor': [('ex', '\xa6'), ('ex', '\x6b'), 

500 ('ex', '\xba'), ('ex', '\x25')], 

501 r'\lceil': [('ex', '\xa7'), ('ex', '\x6c'), 

502 ('ex', '\xbb'), ('ex', '\x26')], 

503 r'\rceil': [('ex', '\xa8'), ('ex', '\x6d'), 

504 ('ex', '\xbc'), ('ex', '\x27')], 

505 r'\langle': [('ex', '\xad'), ('ex', '\x44'), 

506 ('ex', '\xbf'), ('ex', '\x2a')], 

507 r'\rangle': [('ex', '\xae'), ('ex', '\x45'), 

508 ('ex', '\xc0'), ('ex', '\x2b')], 

509 r'\__sqrt__': [('ex', '\x70'), ('ex', '\x71'), 

510 ('ex', '\x72'), ('ex', '\x73')], 

511 r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'), 

512 ('ex', '\xc2'), ('ex', '\x2d')], 

513 r'/': [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'), 

514 ('ex', '\xcb'), ('ex', '\x2c')], 

515 r'\widehat': [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'), 

516 ('ex', '\x64')], 

517 r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'), 

518 ('ex', '\x67')], 

519 r'<': [('cal', 'h'), ('ex', 'D')], 

520 r'>': [('cal', 'i'), ('ex', 'E')] 

521 } 

522 

523 for alias, target in [(r'\leftparen', '('), 

524 (r'\rightparent', ')'), 

525 (r'\leftbrace', '{'), 

526 (r'\rightbrace', '}'), 

527 (r'\leftbracket', '['), 

528 (r'\rightbracket', ']'), 

529 (r'\{', '{'), 

530 (r'\}', '}'), 

531 (r'\[', '['), 

532 (r'\]', ']')]: 

533 _size_alternatives[alias] = _size_alternatives[target] 

534 

535 def get_sized_alternatives_for_symbol(self, fontname: str, 

536 sym: str) -> list[tuple[str, str]]: 

537 return self._size_alternatives.get(sym, [(fontname, sym)]) 

538 

539 

540class UnicodeFonts(TruetypeFonts): 

541 """ 

542 An abstract base class for handling Unicode fonts. 

543 

544 While some reasonably complete Unicode fonts (such as DejaVu) may 

545 work in some situations, the only Unicode font I'm aware of with a 

546 complete set of math symbols is STIX. 

547 

548 This class will "fallback" on the Bakoma fonts when a required 

549 symbol cannot be found in the font. 

550 """ 

551 

552 # Some glyphs are not present in the `cmr10` font, and must be brought in 

553 # from `cmsy10`. Map the Unicode indices of those glyphs to the indices at 

554 # which they are found in `cmsy10`. 

555 _cmr10_substitutions = { 

556 0x00D7: 0x00A3, # Multiplication sign. 

557 0x2212: 0x00A1, # Minus sign. 

558 } 

559 

560 def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): 

561 # This must come first so the backend's owner is set correctly 

562 fallback_rc = mpl.rcParams['mathtext.fallback'] 

563 font_cls: type[TruetypeFonts] | None = { 

564 'stix': StixFonts, 

565 'stixsans': StixSansFonts, 

566 'cm': BakomaFonts 

567 }.get(fallback_rc) 

568 self._fallback_font = (font_cls(default_font_prop, load_glyph_flags) 

569 if font_cls else None) 

570 

571 super().__init__(default_font_prop, load_glyph_flags) 

572 for texfont in "cal rm tt it bf sf bfit".split(): 

573 prop = mpl.rcParams['mathtext.' + texfont] 

574 font = findfont(prop) 

575 self.fontmap[texfont] = font 

576 prop = FontProperties('cmex10') 

577 font = findfont(prop) 

578 self.fontmap['ex'] = font 

579 

580 # include STIX sized alternatives for glyphs if fallback is STIX 

581 if isinstance(self._fallback_font, StixFonts): 

582 stixsizedaltfonts = { 

583 0: 'STIXGeneral', 

584 1: 'STIXSizeOneSym', 

585 2: 'STIXSizeTwoSym', 

586 3: 'STIXSizeThreeSym', 

587 4: 'STIXSizeFourSym', 

588 5: 'STIXSizeFiveSym'} 

589 

590 for size, name in stixsizedaltfonts.items(): 

591 fullpath = findfont(name) 

592 self.fontmap[size] = fullpath 

593 self.fontmap[name] = fullpath 

594 

595 _slanted_symbols = set(r"\int \oint".split()) 

596 

597 def _map_virtual_font(self, fontname: str, font_class: str, 

598 uniindex: int) -> tuple[str, int]: 

599 return fontname, uniindex 

600 

601 def _get_glyph(self, fontname: str, font_class: str, 

602 sym: str) -> tuple[FT2Font, int, bool]: 

603 try: 

604 uniindex = get_unicode_index(sym) 

605 found_symbol = True 

606 except ValueError: 

607 uniindex = ord('?') 

608 found_symbol = False 

609 _log.warning("No TeX to Unicode mapping for %a.", sym) 

610 

611 fontname, uniindex = self._map_virtual_font( 

612 fontname, font_class, uniindex) 

613 

614 new_fontname = fontname 

615 

616 # Only characters in the "Letter" class should be italicized in 'it' 

617 # mode. Greek capital letters should be Roman. 

618 if found_symbol: 

619 if fontname == 'it' and uniindex < 0x10000: 

620 char = chr(uniindex) 

621 if (unicodedata.category(char)[0] != "L" 

622 or unicodedata.name(char).startswith("GREEK CAPITAL")): 

623 new_fontname = 'rm' 

624 

625 slanted = (new_fontname == 'it') or sym in self._slanted_symbols 

626 found_symbol = False 

627 font = self._get_font(new_fontname) 

628 if font is not None: 

629 if (uniindex in self._cmr10_substitutions 

630 and font.family_name == "cmr10"): 

631 font = get_font( 

632 cbook._get_data_path("fonts/ttf/cmsy10.ttf")) 

633 uniindex = self._cmr10_substitutions[uniindex] 

634 glyphindex = font.get_char_index(uniindex) 

635 if glyphindex != 0: 

636 found_symbol = True 

637 

638 if not found_symbol: 

639 if self._fallback_font: 

640 if (fontname in ('it', 'regular') 

641 and isinstance(self._fallback_font, StixFonts)): 

642 fontname = 'rm' 

643 

644 g = self._fallback_font._get_glyph(fontname, font_class, sym) 

645 family = g[0].family_name 

646 if family in list(BakomaFonts._fontmap.values()): 

647 family = "Computer Modern" 

648 _log.info("Substituting symbol %s from %s", sym, family) 

649 return g 

650 

651 else: 

652 if (fontname in ('it', 'regular') 

653 and isinstance(self, StixFonts)): 

654 return self._get_glyph('rm', font_class, sym) 

655 _log.warning("Font %r does not have a glyph for %a [U+%x], " 

656 "substituting with a dummy symbol.", 

657 new_fontname, sym, uniindex) 

658 font = self._get_font('rm') 

659 uniindex = 0xA4 # currency char, for lack of anything better 

660 slanted = False 

661 

662 return font, uniindex, slanted 

663 

664 def get_sized_alternatives_for_symbol(self, fontname: str, 

665 sym: str) -> list[tuple[str, str]]: 

666 if self._fallback_font: 

667 return self._fallback_font.get_sized_alternatives_for_symbol( 

668 fontname, sym) 

669 return [(fontname, sym)] 

670 

671 

672class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta): 

673 _fontmap: dict[str | int, str] = {} 

674 

675 def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): 

676 # This must come first so the backend's owner is set correctly 

677 if isinstance(self, DejaVuSerifFonts): 

678 self._fallback_font = StixFonts(default_font_prop, load_glyph_flags) 

679 else: 

680 self._fallback_font = StixSansFonts(default_font_prop, load_glyph_flags) 

681 self.bakoma = BakomaFonts(default_font_prop, load_glyph_flags) 

682 TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags) 

683 # Include Stix sized alternatives for glyphs 

684 self._fontmap.update({ 

685 1: 'STIXSizeOneSym', 

686 2: 'STIXSizeTwoSym', 

687 3: 'STIXSizeThreeSym', 

688 4: 'STIXSizeFourSym', 

689 5: 'STIXSizeFiveSym', 

690 }) 

691 for key, name in self._fontmap.items(): 

692 fullpath = findfont(name) 

693 self.fontmap[key] = fullpath 

694 self.fontmap[name] = fullpath 

695 

696 def _get_glyph(self, fontname: str, font_class: str, 

697 sym: str) -> tuple[FT2Font, int, bool]: 

698 # Override prime symbol to use Bakoma. 

699 if sym == r'\prime': 

700 return self.bakoma._get_glyph(fontname, font_class, sym) 

701 else: 

702 # check whether the glyph is available in the display font 

703 uniindex = get_unicode_index(sym) 

704 font = self._get_font('ex') 

705 if font is not None: 

706 glyphindex = font.get_char_index(uniindex) 

707 if glyphindex != 0: 

708 return super()._get_glyph('ex', font_class, sym) 

709 # otherwise return regular glyph 

710 return super()._get_glyph(fontname, font_class, sym) 

711 

712 

713class DejaVuSerifFonts(DejaVuFonts): 

714 """ 

715 A font handling class for the DejaVu Serif fonts 

716 

717 If a glyph is not found it will fallback to Stix Serif 

718 """ 

719 _fontmap = { 

720 'rm': 'DejaVu Serif', 

721 'it': 'DejaVu Serif:italic', 

722 'bf': 'DejaVu Serif:weight=bold', 

723 'bfit': 'DejaVu Serif:italic:bold', 

724 'sf': 'DejaVu Sans', 

725 'tt': 'DejaVu Sans Mono', 

726 'ex': 'DejaVu Serif Display', 

727 0: 'DejaVu Serif', 

728 } 

729 

730 

731class DejaVuSansFonts(DejaVuFonts): 

732 """ 

733 A font handling class for the DejaVu Sans fonts 

734 

735 If a glyph is not found it will fallback to Stix Sans 

736 """ 

737 _fontmap = { 

738 'rm': 'DejaVu Sans', 

739 'it': 'DejaVu Sans:italic', 

740 'bf': 'DejaVu Sans:weight=bold', 

741 'bfit': 'DejaVu Sans:italic:bold', 

742 'sf': 'DejaVu Sans', 

743 'tt': 'DejaVu Sans Mono', 

744 'ex': 'DejaVu Sans Display', 

745 0: 'DejaVu Sans', 

746 } 

747 

748 

749class StixFonts(UnicodeFonts): 

750 """ 

751 A font handling class for the STIX fonts. 

752 

753 In addition to what UnicodeFonts provides, this class: 

754 

755 - supports "virtual fonts" which are complete alpha numeric 

756 character sets with different font styles at special Unicode 

757 code points, such as "Blackboard". 

758 

759 - handles sized alternative characters for the STIXSizeX fonts. 

760 """ 

761 _fontmap: dict[str | int, str] = { 

762 'rm': 'STIXGeneral', 

763 'it': 'STIXGeneral:italic', 

764 'bf': 'STIXGeneral:weight=bold', 

765 'bfit': 'STIXGeneral:italic:bold', 

766 'nonunirm': 'STIXNonUnicode', 

767 'nonuniit': 'STIXNonUnicode:italic', 

768 'nonunibf': 'STIXNonUnicode:weight=bold', 

769 0: 'STIXGeneral', 

770 1: 'STIXSizeOneSym', 

771 2: 'STIXSizeTwoSym', 

772 3: 'STIXSizeThreeSym', 

773 4: 'STIXSizeFourSym', 

774 5: 'STIXSizeFiveSym', 

775 } 

776 _fallback_font = None 

777 _sans = False 

778 

779 def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): 

780 TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags) 

781 for key, name in self._fontmap.items(): 

782 fullpath = findfont(name) 

783 self.fontmap[key] = fullpath 

784 self.fontmap[name] = fullpath 

785 

786 def _map_virtual_font(self, fontname: str, font_class: str, 

787 uniindex: int) -> tuple[str, int]: 

788 # Handle these "fonts" that are actually embedded in 

789 # other fonts. 

790 font_mapping = stix_virtual_fonts.get(fontname) 

791 if (self._sans and font_mapping is None 

792 and fontname not in ('regular', 'default')): 

793 font_mapping = stix_virtual_fonts['sf'] 

794 doing_sans_conversion = True 

795 else: 

796 doing_sans_conversion = False 

797 

798 if isinstance(font_mapping, dict): 

799 try: 

800 mapping = font_mapping[font_class] 

801 except KeyError: 

802 mapping = font_mapping['rm'] 

803 elif isinstance(font_mapping, list): 

804 mapping = font_mapping 

805 else: 

806 mapping = None 

807 

808 if mapping is not None: 

809 # Binary search for the source glyph 

810 lo = 0 

811 hi = len(mapping) 

812 while lo < hi: 

813 mid = (lo+hi)//2 

814 range = mapping[mid] 

815 if uniindex < range[0]: 

816 hi = mid 

817 elif uniindex <= range[1]: 

818 break 

819 else: 

820 lo = mid + 1 

821 

822 if range[0] <= uniindex <= range[1]: 

823 uniindex = uniindex - range[0] + range[3] 

824 fontname = range[2] 

825 elif not doing_sans_conversion: 

826 # This will generate a dummy character 

827 uniindex = 0x1 

828 fontname = mpl.rcParams['mathtext.default'] 

829 

830 # Fix some incorrect glyphs. 

831 if fontname in ('rm', 'it'): 

832 uniindex = stix_glyph_fixes.get(uniindex, uniindex) 

833 

834 # Handle private use area glyphs 

835 if fontname in ('it', 'rm', 'bf', 'bfit') and 0xe000 <= uniindex <= 0xf8ff: 

836 fontname = 'nonuni' + fontname 

837 

838 return fontname, uniindex 

839 

840 @functools.cache 

841 def get_sized_alternatives_for_symbol( # type: ignore[override] 

842 self, 

843 fontname: str, 

844 sym: str) -> list[tuple[str, str]] | list[tuple[int, str]]: 

845 fixes = { 

846 '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']', 

847 '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}', 

848 '>': '\N{MATHEMATICAL RIGHT ANGLE BRACKET}', 

849 } 

850 sym = fixes.get(sym, sym) 

851 try: 

852 uniindex = get_unicode_index(sym) 

853 except ValueError: 

854 return [(fontname, sym)] 

855 alternatives = [(i, chr(uniindex)) for i in range(6) 

856 if self._get_font(i).get_char_index(uniindex) != 0] 

857 # The largest size of the radical symbol in STIX has incorrect 

858 # metrics that cause it to be disconnected from the stem. 

859 if sym == r'\__sqrt__': 

860 alternatives = alternatives[:-1] 

861 return alternatives 

862 

863 

864class StixSansFonts(StixFonts): 

865 """ 

866 A font handling class for the STIX fonts (that uses sans-serif 

867 characters by default). 

868 """ 

869 _sans = True 

870 

871 

872############################################################################## 

873# TeX-LIKE BOX MODEL 

874 

875# The following is based directly on the document 'woven' from the 

876# TeX82 source code. This information is also available in printed 

877# form: 

878# 

879# Knuth, Donald E.. 1986. Computers and Typesetting, Volume B: 

880# TeX: The Program. Addison-Wesley Professional. 

881# 

882# The most relevant "chapters" are: 

883# Data structures for boxes and their friends 

884# Shipping pages out (ship()) 

885# Packaging (hpack() and vpack()) 

886# Data structures for math mode 

887# Subroutines for math mode 

888# Typesetting math formulas 

889# 

890# Many of the docstrings below refer to a numbered "node" in that 

891# book, e.g., node123 

892# 

893# Note that (as TeX) y increases downward, unlike many other parts of 

894# matplotlib. 

895 

896# How much text shrinks when going to the next-smallest level. 

897SHRINK_FACTOR = 0.7 

898# The number of different sizes of chars to use, beyond which they will not 

899# get any smaller 

900NUM_SIZE_LEVELS = 6 

901 

902 

903class FontConstantsBase: 

904 """ 

905 A set of constants that controls how certain things, such as sub- 

906 and superscripts are laid out. These are all metrics that can't 

907 be reliably retrieved from the font metrics in the font itself. 

908 """ 

909 # Percentage of x-height of additional horiz. space after sub/superscripts 

910 script_space: T.ClassVar[float] = 0.05 

911 

912 # Percentage of x-height that sub/superscripts drop below the baseline 

913 subdrop: T.ClassVar[float] = 0.4 

914 

915 # Percentage of x-height that superscripts are raised from the baseline 

916 sup1: T.ClassVar[float] = 0.7 

917 

918 # Percentage of x-height that subscripts drop below the baseline 

919 sub1: T.ClassVar[float] = 0.3 

920 

921 # Percentage of x-height that subscripts drop below the baseline when a 

922 # superscript is present 

923 sub2: T.ClassVar[float] = 0.5 

924 

925 # Percentage of x-height that sub/superscripts are offset relative to the 

926 # nucleus edge for non-slanted nuclei 

927 delta: T.ClassVar[float] = 0.025 

928 

929 # Additional percentage of last character height above 2/3 of the 

930 # x-height that superscripts are offset relative to the subscript 

931 # for slanted nuclei 

932 delta_slanted: T.ClassVar[float] = 0.2 

933 

934 # Percentage of x-height that superscripts and subscripts are offset for 

935 # integrals 

936 delta_integral: T.ClassVar[float] = 0.1 

937 

938 

939class ComputerModernFontConstants(FontConstantsBase): 

940 script_space = 0.075 

941 subdrop = 0.2 

942 sup1 = 0.45 

943 sub1 = 0.2 

944 sub2 = 0.3 

945 delta = 0.075 

946 delta_slanted = 0.3 

947 delta_integral = 0.3 

948 

949 

950class STIXFontConstants(FontConstantsBase): 

951 script_space = 0.1 

952 sup1 = 0.8 

953 sub2 = 0.6 

954 delta = 0.05 

955 delta_slanted = 0.3 

956 delta_integral = 0.3 

957 

958 

959class STIXSansFontConstants(FontConstantsBase): 

960 script_space = 0.05 

961 sup1 = 0.8 

962 delta_slanted = 0.6 

963 delta_integral = 0.3 

964 

965 

966class DejaVuSerifFontConstants(FontConstantsBase): 

967 pass 

968 

969 

970class DejaVuSansFontConstants(FontConstantsBase): 

971 pass 

972 

973 

974# Maps font family names to the FontConstantBase subclass to use 

975_font_constant_mapping = { 

976 'DejaVu Sans': DejaVuSansFontConstants, 

977 'DejaVu Sans Mono': DejaVuSansFontConstants, 

978 'DejaVu Serif': DejaVuSerifFontConstants, 

979 'cmb10': ComputerModernFontConstants, 

980 'cmex10': ComputerModernFontConstants, 

981 'cmmi10': ComputerModernFontConstants, 

982 'cmr10': ComputerModernFontConstants, 

983 'cmss10': ComputerModernFontConstants, 

984 'cmsy10': ComputerModernFontConstants, 

985 'cmtt10': ComputerModernFontConstants, 

986 'STIXGeneral': STIXFontConstants, 

987 'STIXNonUnicode': STIXFontConstants, 

988 'STIXSizeFiveSym': STIXFontConstants, 

989 'STIXSizeFourSym': STIXFontConstants, 

990 'STIXSizeThreeSym': STIXFontConstants, 

991 'STIXSizeTwoSym': STIXFontConstants, 

992 'STIXSizeOneSym': STIXFontConstants, 

993 # Map the fonts we used to ship, just for good measure 

994 'Bitstream Vera Sans': DejaVuSansFontConstants, 

995 'Bitstream Vera': DejaVuSansFontConstants, 

996 } 

997 

998 

999def _get_font_constant_set(state: ParserState) -> type[FontConstantsBase]: 

1000 constants = _font_constant_mapping.get( 

1001 state.fontset._get_font(state.font).family_name, FontConstantsBase) 

1002 # STIX sans isn't really its own fonts, just different code points 

1003 # in the STIX fonts, so we have to detect this one separately. 

1004 if (constants is STIXFontConstants and 

1005 isinstance(state.fontset, StixSansFonts)): 

1006 return STIXSansFontConstants 

1007 return constants 

1008 

1009 

1010class Node: 

1011 """A node in the TeX box model.""" 

1012 

1013 def __init__(self) -> None: 

1014 self.size = 0 

1015 

1016 def __repr__(self) -> str: 

1017 return type(self).__name__ 

1018 

1019 def get_kerning(self, next: Node | None) -> float: 

1020 return 0.0 

1021 

1022 def shrink(self) -> None: 

1023 """ 

1024 Shrinks one level smaller. There are only three levels of 

1025 sizes, after which things will no longer get smaller. 

1026 """ 

1027 self.size += 1 

1028 

1029 def render(self, output: Output, x: float, y: float) -> None: 

1030 """Render this node.""" 

1031 

1032 

1033class Box(Node): 

1034 """A node with a physical location.""" 

1035 

1036 def __init__(self, width: float, height: float, depth: float) -> None: 

1037 super().__init__() 

1038 self.width = width 

1039 self.height = height 

1040 self.depth = depth 

1041 

1042 def shrink(self) -> None: 

1043 super().shrink() 

1044 if self.size < NUM_SIZE_LEVELS: 

1045 self.width *= SHRINK_FACTOR 

1046 self.height *= SHRINK_FACTOR 

1047 self.depth *= SHRINK_FACTOR 

1048 

1049 def render(self, output: Output, # type: ignore[override] 

1050 x1: float, y1: float, x2: float, y2: float) -> None: 

1051 pass 

1052 

1053 

1054class Vbox(Box): 

1055 """A box with only height (zero width).""" 

1056 

1057 def __init__(self, height: float, depth: float): 

1058 super().__init__(0., height, depth) 

1059 

1060 

1061class Hbox(Box): 

1062 """A box with only width (zero height and depth).""" 

1063 

1064 def __init__(self, width: float): 

1065 super().__init__(width, 0., 0.) 

1066 

1067 

1068class Char(Node): 

1069 """ 

1070 A single character. 

1071 

1072 Unlike TeX, the font information and metrics are stored with each `Char` 

1073 to make it easier to lookup the font metrics when needed. Note that TeX 

1074 boxes have a width, height, and depth, unlike Type1 and TrueType which use 

1075 a full bounding box and an advance in the x-direction. The metrics must 

1076 be converted to the TeX model, and the advance (if different from width) 

1077 must be converted into a `Kern` node when the `Char` is added to its parent 

1078 `Hlist`. 

1079 """ 

1080 

1081 def __init__(self, c: str, state: ParserState): 

1082 super().__init__() 

1083 self.c = c 

1084 self.fontset = state.fontset 

1085 self.font = state.font 

1086 self.font_class = state.font_class 

1087 self.fontsize = state.fontsize 

1088 self.dpi = state.dpi 

1089 # The real width, height and depth will be set during the 

1090 # pack phase, after we know the real fontsize 

1091 self._update_metrics() 

1092 

1093 def __repr__(self) -> str: 

1094 return '`%s`' % self.c 

1095 

1096 def _update_metrics(self) -> None: 

1097 metrics = self._metrics = self.fontset.get_metrics( 

1098 self.font, self.font_class, self.c, self.fontsize, self.dpi) 

1099 if self.c == ' ': 

1100 self.width = metrics.advance 

1101 else: 

1102 self.width = metrics.width 

1103 self.height = metrics.iceberg 

1104 self.depth = -(metrics.iceberg - metrics.height) 

1105 

1106 def is_slanted(self) -> bool: 

1107 return self._metrics.slanted 

1108 

1109 def get_kerning(self, next: Node | None) -> float: 

1110 """ 

1111 Return the amount of kerning between this and the given character. 

1112 

1113 This method is called when characters are strung together into `Hlist` 

1114 to create `Kern` nodes. 

1115 """ 

1116 advance = self._metrics.advance - self.width 

1117 kern = 0. 

1118 if isinstance(next, Char): 

1119 kern = self.fontset.get_kern( 

1120 self.font, self.font_class, self.c, self.fontsize, 

1121 next.font, next.font_class, next.c, next.fontsize, 

1122 self.dpi) 

1123 return advance + kern 

1124 

1125 def render(self, output: Output, x: float, y: float) -> None: 

1126 self.fontset.render_glyph( 

1127 output, x, y, 

1128 self.font, self.font_class, self.c, self.fontsize, self.dpi) 

1129 

1130 def shrink(self) -> None: 

1131 super().shrink() 

1132 if self.size < NUM_SIZE_LEVELS: 

1133 self.fontsize *= SHRINK_FACTOR 

1134 self.width *= SHRINK_FACTOR 

1135 self.height *= SHRINK_FACTOR 

1136 self.depth *= SHRINK_FACTOR 

1137 

1138 

1139class Accent(Char): 

1140 """ 

1141 The font metrics need to be dealt with differently for accents, 

1142 since they are already offset correctly from the baseline in 

1143 TrueType fonts. 

1144 """ 

1145 def _update_metrics(self) -> None: 

1146 metrics = self._metrics = self.fontset.get_metrics( 

1147 self.font, self.font_class, self.c, self.fontsize, self.dpi) 

1148 self.width = metrics.xmax - metrics.xmin 

1149 self.height = metrics.ymax - metrics.ymin 

1150 self.depth = 0 

1151 

1152 def shrink(self) -> None: 

1153 super().shrink() 

1154 self._update_metrics() 

1155 

1156 def render(self, output: Output, x: float, y: float) -> None: 

1157 self.fontset.render_glyph( 

1158 output, x - self._metrics.xmin, y + self._metrics.ymin, 

1159 self.font, self.font_class, self.c, self.fontsize, self.dpi) 

1160 

1161 

1162class List(Box): 

1163 """A list of nodes (either horizontal or vertical).""" 

1164 

1165 def __init__(self, elements: T.Sequence[Node]): 

1166 super().__init__(0., 0., 0.) 

1167 self.shift_amount = 0. # An arbitrary offset 

1168 self.children = [*elements] # The child nodes of this list 

1169 # The following parameters are set in the vpack and hpack functions 

1170 self.glue_set = 0. # The glue setting of this list 

1171 self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching 

1172 self.glue_order = 0 # The order of infinity (0 - 3) for the glue 

1173 

1174 def __repr__(self) -> str: 

1175 return '{}<w={:.02f} h={:.02f} d={:.02f} s={:.02f}>[{}]'.format( 

1176 super().__repr__(), 

1177 self.width, self.height, 

1178 self.depth, self.shift_amount, 

1179 ', '.join([repr(x) for x in self.children])) 

1180 

1181 def _set_glue(self, x: float, sign: int, totals: list[float], 

1182 error_type: str) -> None: 

1183 self.glue_order = o = next( 

1184 # Highest order of glue used by the members of this list. 

1185 (i for i in range(len(totals))[::-1] if totals[i] != 0), 0) 

1186 self.glue_sign = sign 

1187 if totals[o] != 0.: 

1188 self.glue_set = x / totals[o] 

1189 else: 

1190 self.glue_sign = 0 

1191 self.glue_ratio = 0. 

1192 if o == 0: 

1193 if len(self.children): 

1194 _log.warning("%s %s: %r", 

1195 error_type, type(self).__name__, self) 

1196 

1197 def shrink(self) -> None: 

1198 for child in self.children: 

1199 child.shrink() 

1200 super().shrink() 

1201 if self.size < NUM_SIZE_LEVELS: 

1202 self.shift_amount *= SHRINK_FACTOR 

1203 self.glue_set *= SHRINK_FACTOR 

1204 

1205 

1206class Hlist(List): 

1207 """A horizontal list of boxes.""" 

1208 

1209 def __init__(self, elements: T.Sequence[Node], w: float = 0.0, 

1210 m: T.Literal['additional', 'exactly'] = 'additional', 

1211 do_kern: bool = True): 

1212 super().__init__(elements) 

1213 if do_kern: 

1214 self.kern() 

1215 self.hpack(w=w, m=m) 

1216 

1217 def kern(self) -> None: 

1218 """ 

1219 Insert `Kern` nodes between `Char` nodes to set kerning. 

1220 

1221 The `Char` nodes themselves determine the amount of kerning they need 

1222 (in `~Char.get_kerning`), and this function just creates the correct 

1223 linked list. 

1224 """ 

1225 new_children = [] 

1226 num_children = len(self.children) 

1227 if num_children: 

1228 for i in range(num_children): 

1229 elem = self.children[i] 

1230 if i < num_children - 1: 

1231 next = self.children[i + 1] 

1232 else: 

1233 next = None 

1234 

1235 new_children.append(elem) 

1236 kerning_distance = elem.get_kerning(next) 

1237 if kerning_distance != 0.: 

1238 kern = Kern(kerning_distance) 

1239 new_children.append(kern) 

1240 self.children = new_children 

1241 

1242 def hpack(self, w: float = 0.0, 

1243 m: T.Literal['additional', 'exactly'] = 'additional') -> None: 

1244 r""" 

1245 Compute the dimensions of the resulting boxes, and adjust the glue if 

1246 one of those dimensions is pre-specified. The computed sizes normally 

1247 enclose all of the material inside the new box; but some items may 

1248 stick out if negative glue is used, if the box is overfull, or if a 

1249 ``\vbox`` includes other boxes that have been shifted left. 

1250 

1251 Parameters 

1252 ---------- 

1253 w : float, default: 0 

1254 A width. 

1255 m : {'exactly', 'additional'}, default: 'additional' 

1256 Whether to produce a box whose width is 'exactly' *w*; or a box 

1257 with the natural width of the contents, plus *w* ('additional'). 

1258 

1259 Notes 

1260 ----- 

1261 The defaults produce a box with the natural width of the contents. 

1262 """ 

1263 # I don't know why these get reset in TeX. Shift_amount is pretty 

1264 # much useless if we do. 

1265 # self.shift_amount = 0. 

1266 h = 0. 

1267 d = 0. 

1268 x = 0. 

1269 total_stretch = [0.] * 4 

1270 total_shrink = [0.] * 4 

1271 for p in self.children: 

1272 if isinstance(p, Char): 

1273 x += p.width 

1274 h = max(h, p.height) 

1275 d = max(d, p.depth) 

1276 elif isinstance(p, Box): 

1277 x += p.width 

1278 if not np.isinf(p.height) and not np.isinf(p.depth): 

1279 s = getattr(p, 'shift_amount', 0.) 

1280 h = max(h, p.height - s) 

1281 d = max(d, p.depth + s) 

1282 elif isinstance(p, Glue): 

1283 glue_spec = p.glue_spec 

1284 x += glue_spec.width 

1285 total_stretch[glue_spec.stretch_order] += glue_spec.stretch 

1286 total_shrink[glue_spec.shrink_order] += glue_spec.shrink 

1287 elif isinstance(p, Kern): 

1288 x += p.width 

1289 self.height = h 

1290 self.depth = d 

1291 

1292 if m == 'additional': 

1293 w += x 

1294 self.width = w 

1295 x = w - x 

1296 

1297 if x == 0.: 

1298 self.glue_sign = 0 

1299 self.glue_order = 0 

1300 self.glue_ratio = 0. 

1301 return 

1302 if x > 0.: 

1303 self._set_glue(x, 1, total_stretch, "Overful") 

1304 else: 

1305 self._set_glue(x, -1, total_shrink, "Underful") 

1306 

1307 

1308class Vlist(List): 

1309 """A vertical list of boxes.""" 

1310 

1311 def __init__(self, elements: T.Sequence[Node], h: float = 0.0, 

1312 m: T.Literal['additional', 'exactly'] = 'additional'): 

1313 super().__init__(elements) 

1314 self.vpack(h=h, m=m) 

1315 

1316 def vpack(self, h: float = 0.0, 

1317 m: T.Literal['additional', 'exactly'] = 'additional', 

1318 l: float = np.inf) -> None: 

1319 """ 

1320 Compute the dimensions of the resulting boxes, and to adjust the glue 

1321 if one of those dimensions is pre-specified. 

1322 

1323 Parameters 

1324 ---------- 

1325 h : float, default: 0 

1326 A height. 

1327 m : {'exactly', 'additional'}, default: 'additional' 

1328 Whether to produce a box whose height is 'exactly' *h*; or a box 

1329 with the natural height of the contents, plus *h* ('additional'). 

1330 l : float, default: np.inf 

1331 The maximum height. 

1332 

1333 Notes 

1334 ----- 

1335 The defaults produce a box with the natural height of the contents. 

1336 """ 

1337 # I don't know why these get reset in TeX. Shift_amount is pretty 

1338 # much useless if we do. 

1339 # self.shift_amount = 0. 

1340 w = 0. 

1341 d = 0. 

1342 x = 0. 

1343 total_stretch = [0.] * 4 

1344 total_shrink = [0.] * 4 

1345 for p in self.children: 

1346 if isinstance(p, Box): 

1347 x += d + p.height 

1348 d = p.depth 

1349 if not np.isinf(p.width): 

1350 s = getattr(p, 'shift_amount', 0.) 

1351 w = max(w, p.width + s) 

1352 elif isinstance(p, Glue): 

1353 x += d 

1354 d = 0. 

1355 glue_spec = p.glue_spec 

1356 x += glue_spec.width 

1357 total_stretch[glue_spec.stretch_order] += glue_spec.stretch 

1358 total_shrink[glue_spec.shrink_order] += glue_spec.shrink 

1359 elif isinstance(p, Kern): 

1360 x += d + p.width 

1361 d = 0. 

1362 elif isinstance(p, Char): 

1363 raise RuntimeError( 

1364 "Internal mathtext error: Char node found in Vlist") 

1365 

1366 self.width = w 

1367 if d > l: 

1368 x += d - l 

1369 self.depth = l 

1370 else: 

1371 self.depth = d 

1372 

1373 if m == 'additional': 

1374 h += x 

1375 self.height = h 

1376 x = h - x 

1377 

1378 if x == 0: 

1379 self.glue_sign = 0 

1380 self.glue_order = 0 

1381 self.glue_ratio = 0. 

1382 return 

1383 

1384 if x > 0.: 

1385 self._set_glue(x, 1, total_stretch, "Overful") 

1386 else: 

1387 self._set_glue(x, -1, total_shrink, "Underful") 

1388 

1389 

1390class Rule(Box): 

1391 """ 

1392 A solid black rectangle. 

1393 

1394 It has *width*, *depth*, and *height* fields just as in an `Hlist`. 

1395 However, if any of these dimensions is inf, the actual value will be 

1396 determined by running the rule up to the boundary of the innermost 

1397 enclosing box. This is called a "running dimension". The width is never 

1398 running in an `Hlist`; the height and depth are never running in a `Vlist`. 

1399 """ 

1400 

1401 def __init__(self, width: float, height: float, depth: float, state: ParserState): 

1402 super().__init__(width, height, depth) 

1403 self.fontset = state.fontset 

1404 

1405 def render(self, output: Output, # type: ignore[override] 

1406 x: float, y: float, w: float, h: float) -> None: 

1407 self.fontset.render_rect_filled(output, x, y, x + w, y + h) 

1408 

1409 

1410class Hrule(Rule): 

1411 """Convenience class to create a horizontal rule.""" 

1412 

1413 def __init__(self, state: ParserState, thickness: float | None = None): 

1414 if thickness is None: 

1415 thickness = state.get_current_underline_thickness() 

1416 height = depth = thickness * 0.5 

1417 super().__init__(np.inf, height, depth, state) 

1418 

1419 

1420class Vrule(Rule): 

1421 """Convenience class to create a vertical rule.""" 

1422 

1423 def __init__(self, state: ParserState): 

1424 thickness = state.get_current_underline_thickness() 

1425 super().__init__(thickness, np.inf, np.inf, state) 

1426 

1427 

1428class _GlueSpec(NamedTuple): 

1429 width: float 

1430 stretch: float 

1431 stretch_order: int 

1432 shrink: float 

1433 shrink_order: int 

1434 

1435 

1436_GlueSpec._named = { # type: ignore[attr-defined] 

1437 'fil': _GlueSpec(0., 1., 1, 0., 0), 

1438 'fill': _GlueSpec(0., 1., 2, 0., 0), 

1439 'filll': _GlueSpec(0., 1., 3, 0., 0), 

1440 'neg_fil': _GlueSpec(0., 0., 0, 1., 1), 

1441 'neg_fill': _GlueSpec(0., 0., 0, 1., 2), 

1442 'neg_filll': _GlueSpec(0., 0., 0, 1., 3), 

1443 'empty': _GlueSpec(0., 0., 0, 0., 0), 

1444 'ss': _GlueSpec(0., 1., 1, -1., 1), 

1445} 

1446 

1447 

1448class Glue(Node): 

1449 """ 

1450 Most of the information in this object is stored in the underlying 

1451 ``_GlueSpec`` class, which is shared between multiple glue objects. 

1452 (This is a memory optimization which probably doesn't matter anymore, but 

1453 it's easier to stick to what TeX does.) 

1454 """ 

1455 

1456 def __init__(self, 

1457 glue_type: _GlueSpec | T.Literal["fil", "fill", "filll", 

1458 "neg_fil", "neg_fill", "neg_filll", 

1459 "empty", "ss"]): 

1460 super().__init__() 

1461 if isinstance(glue_type, str): 

1462 glue_spec = _GlueSpec._named[glue_type] # type: ignore[attr-defined] 

1463 elif isinstance(glue_type, _GlueSpec): 

1464 glue_spec = glue_type 

1465 else: 

1466 raise ValueError("glue_type must be a glue spec name or instance") 

1467 self.glue_spec = glue_spec 

1468 

1469 def shrink(self) -> None: 

1470 super().shrink() 

1471 if self.size < NUM_SIZE_LEVELS: 

1472 g = self.glue_spec 

1473 self.glue_spec = g._replace(width=g.width * SHRINK_FACTOR) 

1474 

1475 

1476class HCentered(Hlist): 

1477 """ 

1478 A convenience class to create an `Hlist` whose contents are 

1479 centered within its enclosing box. 

1480 """ 

1481 

1482 def __init__(self, elements: list[Node]): 

1483 super().__init__([Glue('ss'), *elements, Glue('ss')], do_kern=False) 

1484 

1485 

1486class VCentered(Vlist): 

1487 """ 

1488 A convenience class to create a `Vlist` whose contents are 

1489 centered within its enclosing box. 

1490 """ 

1491 

1492 def __init__(self, elements: list[Node]): 

1493 super().__init__([Glue('ss'), *elements, Glue('ss')]) 

1494 

1495 

1496class Kern(Node): 

1497 """ 

1498 A `Kern` node has a width field to specify a (normally 

1499 negative) amount of spacing. This spacing correction appears in 

1500 horizontal lists between letters like A and V when the font 

1501 designer said that it looks better to move them closer together or 

1502 further apart. A kern node can also appear in a vertical list, 

1503 when its *width* denotes additional spacing in the vertical 

1504 direction. 

1505 """ 

1506 

1507 height = 0 

1508 depth = 0 

1509 

1510 def __init__(self, width: float): 

1511 super().__init__() 

1512 self.width = width 

1513 

1514 def __repr__(self) -> str: 

1515 return "k%.02f" % self.width 

1516 

1517 def shrink(self) -> None: 

1518 super().shrink() 

1519 if self.size < NUM_SIZE_LEVELS: 

1520 self.width *= SHRINK_FACTOR 

1521 

1522 

1523class AutoHeightChar(Hlist): 

1524 """ 

1525 A character as close to the given height and depth as possible. 

1526 

1527 When using a font with multiple height versions of some characters (such as 

1528 the BaKoMa fonts), the correct glyph will be selected, otherwise this will 

1529 always just return a scaled version of the glyph. 

1530 """ 

1531 

1532 def __init__(self, c: str, height: float, depth: float, state: ParserState, 

1533 always: bool = False, factor: float | None = None): 

1534 alternatives = state.fontset.get_sized_alternatives_for_symbol( 

1535 state.font, c) 

1536 

1537 xHeight = state.fontset.get_xheight( 

1538 state.font, state.fontsize, state.dpi) 

1539 

1540 state = state.copy() 

1541 target_total = height + depth 

1542 for fontname, sym in alternatives: 

1543 state.font = fontname 

1544 char = Char(sym, state) 

1545 # Ensure that size 0 is chosen when the text is regular sized but 

1546 # with descender glyphs by subtracting 0.2 * xHeight 

1547 if char.height + char.depth >= target_total - 0.2 * xHeight: 

1548 break 

1549 

1550 shift = 0.0 

1551 if state.font != 0 or len(alternatives) == 1: 

1552 if factor is None: 

1553 factor = target_total / (char.height + char.depth) 

1554 state.fontsize *= factor 

1555 char = Char(sym, state) 

1556 

1557 shift = (depth - char.depth) 

1558 

1559 super().__init__([char]) 

1560 self.shift_amount = shift 

1561 

1562 

1563class AutoWidthChar(Hlist): 

1564 """ 

1565 A character as close to the given width as possible. 

1566 

1567 When using a font with multiple width versions of some characters (such as 

1568 the BaKoMa fonts), the correct glyph will be selected, otherwise this will 

1569 always just return a scaled version of the glyph. 

1570 """ 

1571 

1572 def __init__(self, c: str, width: float, state: ParserState, always: bool = False, 

1573 char_class: type[Char] = Char): 

1574 alternatives = state.fontset.get_sized_alternatives_for_symbol( 

1575 state.font, c) 

1576 

1577 state = state.copy() 

1578 for fontname, sym in alternatives: 

1579 state.font = fontname 

1580 char = char_class(sym, state) 

1581 if char.width >= width: 

1582 break 

1583 

1584 factor = width / char.width 

1585 state.fontsize *= factor 

1586 char = char_class(sym, state) 

1587 

1588 super().__init__([char]) 

1589 self.width = char.width 

1590 

1591 

1592def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output: 

1593 """ 

1594 Ship out *box* at offset *xy*, converting it to an `Output`. 

1595 

1596 Since boxes can be inside of boxes inside of boxes, the main work of `ship` 

1597 is done by two mutually recursive routines, `hlist_out` and `vlist_out`, 

1598 which traverse the `Hlist` nodes and `Vlist` nodes inside of horizontal 

1599 and vertical boxes. The global variables used in TeX to store state as it 

1600 processes have become local variables here. 

1601 """ 

1602 ox, oy = xy 

1603 cur_v = 0. 

1604 cur_h = 0. 

1605 off_h = ox 

1606 off_v = oy + box.height 

1607 output = Output(box) 

1608 

1609 def clamp(value: float) -> float: 

1610 return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value 

1611 

1612 def hlist_out(box: Hlist) -> None: 

1613 nonlocal cur_v, cur_h, off_h, off_v 

1614 

1615 cur_g = 0 

1616 cur_glue = 0. 

1617 glue_order = box.glue_order 

1618 glue_sign = box.glue_sign 

1619 base_line = cur_v 

1620 left_edge = cur_h 

1621 

1622 for p in box.children: 

1623 if isinstance(p, Char): 

1624 p.render(output, cur_h + off_h, cur_v + off_v) 

1625 cur_h += p.width 

1626 elif isinstance(p, Kern): 

1627 cur_h += p.width 

1628 elif isinstance(p, List): 

1629 # node623 

1630 if len(p.children) == 0: 

1631 cur_h += p.width 

1632 else: 

1633 edge = cur_h 

1634 cur_v = base_line + p.shift_amount 

1635 if isinstance(p, Hlist): 

1636 hlist_out(p) 

1637 elif isinstance(p, Vlist): 

1638 # p.vpack(box.height + box.depth, 'exactly') 

1639 vlist_out(p) 

1640 else: 

1641 assert False, "unreachable code" 

1642 cur_h = edge + p.width 

1643 cur_v = base_line 

1644 elif isinstance(p, Box): 

1645 # node624 

1646 rule_height = p.height 

1647 rule_depth = p.depth 

1648 rule_width = p.width 

1649 if np.isinf(rule_height): 

1650 rule_height = box.height 

1651 if np.isinf(rule_depth): 

1652 rule_depth = box.depth 

1653 if rule_height > 0 and rule_width > 0: 

1654 cur_v = base_line + rule_depth 

1655 p.render(output, 

1656 cur_h + off_h, cur_v + off_v, 

1657 rule_width, rule_height) 

1658 cur_v = base_line 

1659 cur_h += rule_width 

1660 elif isinstance(p, Glue): 

1661 # node625 

1662 glue_spec = p.glue_spec 

1663 rule_width = glue_spec.width - cur_g 

1664 if glue_sign != 0: # normal 

1665 if glue_sign == 1: # stretching 

1666 if glue_spec.stretch_order == glue_order: 

1667 cur_glue += glue_spec.stretch 

1668 cur_g = round(clamp(box.glue_set * cur_glue)) 

1669 elif glue_spec.shrink_order == glue_order: 

1670 cur_glue += glue_spec.shrink 

1671 cur_g = round(clamp(box.glue_set * cur_glue)) 

1672 rule_width += cur_g 

1673 cur_h += rule_width 

1674 

1675 def vlist_out(box: Vlist) -> None: 

1676 nonlocal cur_v, cur_h, off_h, off_v 

1677 

1678 cur_g = 0 

1679 cur_glue = 0. 

1680 glue_order = box.glue_order 

1681 glue_sign = box.glue_sign 

1682 left_edge = cur_h 

1683 cur_v -= box.height 

1684 top_edge = cur_v 

1685 

1686 for p in box.children: 

1687 if isinstance(p, Kern): 

1688 cur_v += p.width 

1689 elif isinstance(p, List): 

1690 if len(p.children) == 0: 

1691 cur_v += p.height + p.depth 

1692 else: 

1693 cur_v += p.height 

1694 cur_h = left_edge + p.shift_amount 

1695 save_v = cur_v 

1696 p.width = box.width 

1697 if isinstance(p, Hlist): 

1698 hlist_out(p) 

1699 elif isinstance(p, Vlist): 

1700 vlist_out(p) 

1701 else: 

1702 assert False, "unreachable code" 

1703 cur_v = save_v + p.depth 

1704 cur_h = left_edge 

1705 elif isinstance(p, Box): 

1706 rule_height = p.height 

1707 rule_depth = p.depth 

1708 rule_width = p.width 

1709 if np.isinf(rule_width): 

1710 rule_width = box.width 

1711 rule_height += rule_depth 

1712 if rule_height > 0 and rule_depth > 0: 

1713 cur_v += rule_height 

1714 p.render(output, 

1715 cur_h + off_h, cur_v + off_v, 

1716 rule_width, rule_height) 

1717 elif isinstance(p, Glue): 

1718 glue_spec = p.glue_spec 

1719 rule_height = glue_spec.width - cur_g 

1720 if glue_sign != 0: # normal 

1721 if glue_sign == 1: # stretching 

1722 if glue_spec.stretch_order == glue_order: 

1723 cur_glue += glue_spec.stretch 

1724 cur_g = round(clamp(box.glue_set * cur_glue)) 

1725 elif glue_spec.shrink_order == glue_order: # shrinking 

1726 cur_glue += glue_spec.shrink 

1727 cur_g = round(clamp(box.glue_set * cur_glue)) 

1728 rule_height += cur_g 

1729 cur_v += rule_height 

1730 elif isinstance(p, Char): 

1731 raise RuntimeError( 

1732 "Internal mathtext error: Char node found in vlist") 

1733 

1734 assert isinstance(box, Hlist) 

1735 hlist_out(box) 

1736 return output 

1737 

1738 

1739############################################################################## 

1740# PARSER 

1741 

1742 

1743def Error(msg: str) -> ParserElement: 

1744 """Helper class to raise parser errors.""" 

1745 def raise_error(s: str, loc: int, toks: ParseResults) -> T.Any: 

1746 raise ParseFatalException(s, loc, msg) 

1747 

1748 return Empty().setParseAction(raise_error) 

1749 

1750 

1751class ParserState: 

1752 """ 

1753 Parser state. 

1754 

1755 States are pushed and popped from a stack as necessary, and the "current" 

1756 state is always at the top of the stack. 

1757 

1758 Upon entering and leaving a group { } or math/non-math, the stack is pushed 

1759 and popped accordingly. 

1760 """ 

1761 

1762 def __init__(self, fontset: Fonts, font: str, font_class: str, fontsize: float, 

1763 dpi: float): 

1764 self.fontset = fontset 

1765 self._font = font 

1766 self.font_class = font_class 

1767 self.fontsize = fontsize 

1768 self.dpi = dpi 

1769 

1770 def copy(self) -> ParserState: 

1771 return copy.copy(self) 

1772 

1773 @property 

1774 def font(self) -> str: 

1775 return self._font 

1776 

1777 @font.setter 

1778 def font(self, name: str) -> None: 

1779 if name in ('rm', 'it', 'bf', 'bfit'): 

1780 self.font_class = name 

1781 self._font = name 

1782 

1783 def get_current_underline_thickness(self) -> float: 

1784 """Return the underline thickness for this state.""" 

1785 return self.fontset.get_underline_thickness( 

1786 self.font, self.fontsize, self.dpi) 

1787 

1788 

1789def cmd(expr: str, args: ParserElement) -> ParserElement: 

1790 r""" 

1791 Helper to define TeX commands. 

1792 

1793 ``cmd("\cmd", args)`` is equivalent to 

1794 ``"\cmd" - (args | Error("Expected \cmd{arg}{...}"))`` where the names in 

1795 the error message are taken from element names in *args*. If *expr* 

1796 already includes arguments (e.g. "\cmd{arg}{...}"), then they are stripped 

1797 when constructing the parse element, but kept (and *expr* is used as is) in 

1798 the error message. 

1799 """ 

1800 

1801 def names(elt: ParserElement) -> T.Generator[str, None, None]: 

1802 if isinstance(elt, ParseExpression): 

1803 for expr in elt.exprs: 

1804 yield from names(expr) 

1805 elif elt.resultsName: 

1806 yield elt.resultsName 

1807 

1808 csname = expr.split("{", 1)[0] 

1809 err = (csname + "".join("{%s}" % name for name in names(args)) 

1810 if expr == csname else expr) 

1811 return csname - (args | Error(f"Expected {err}")) 

1812 

1813 

1814class Parser: 

1815 """ 

1816 A pyparsing-based parser for strings containing math expressions. 

1817 

1818 Raw text may also appear outside of pairs of ``$``. 

1819 

1820 The grammar is based directly on that in TeX, though it cuts a few corners. 

1821 """ 

1822 

1823 class _MathStyle(enum.Enum): 

1824 DISPLAYSTYLE = 0 

1825 TEXTSTYLE = 1 

1826 SCRIPTSTYLE = 2 

1827 SCRIPTSCRIPTSTYLE = 3 

1828 

1829 _binary_operators = set( 

1830 '+ * - \N{MINUS SIGN}' 

1831 r''' 

1832 \pm \sqcap \rhd 

1833 \mp \sqcup \unlhd 

1834 \times \vee \unrhd 

1835 \div \wedge \oplus 

1836 \ast \setminus \ominus 

1837 \star \wr \otimes 

1838 \circ \diamond \oslash 

1839 \bullet \bigtriangleup \odot 

1840 \cdot \bigtriangledown \bigcirc 

1841 \cap \triangleleft \dagger 

1842 \cup \triangleright \ddagger 

1843 \uplus \lhd \amalg 

1844 \dotplus \dotminus \Cap 

1845 \Cup \barwedge \boxdot 

1846 \boxminus \boxplus \boxtimes 

1847 \curlyvee \curlywedge \divideontimes 

1848 \doublebarwedge \leftthreetimes \rightthreetimes 

1849 \slash \veebar \barvee 

1850 \cupdot \intercal \amalg 

1851 \circledcirc \circleddash \circledast 

1852 \boxbar \obar \merge 

1853 \minuscolon \dotsminusdots 

1854 '''.split()) 

1855 

1856 _relation_symbols = set(r''' 

1857 = < > : 

1858 \leq \geq \equiv \models 

1859 \prec \succ \sim \perp 

1860 \preceq \succeq \simeq \mid 

1861 \ll \gg \asymp \parallel 

1862 \subset \supset \approx \bowtie 

1863 \subseteq \supseteq \cong \Join 

1864 \sqsubset \sqsupset \neq \smile 

1865 \sqsubseteq \sqsupseteq \doteq \frown 

1866 \in \ni \propto \vdash 

1867 \dashv \dots \doteqdot \leqq 

1868 \geqq \lneqq \gneqq \lessgtr 

1869 \leqslant \geqslant \eqgtr \eqless 

1870 \eqslantless \eqslantgtr \lesseqgtr \backsim 

1871 \backsimeq \lesssim \gtrsim \precsim 

1872 \precnsim \gnsim \lnsim \succsim 

1873 \succnsim \nsim \lesseqqgtr \gtreqqless 

1874 \gtreqless \subseteqq \supseteqq \subsetneqq 

1875 \supsetneqq \lessapprox \approxeq \gtrapprox 

1876 \precapprox \succapprox \precnapprox \succnapprox 

1877 \npreccurlyeq \nsucccurlyeq \nsqsubseteq \nsqsupseteq 

1878 \sqsubsetneq \sqsupsetneq \nlesssim \ngtrsim 

1879 \nlessgtr \ngtrless \lnapprox \gnapprox 

1880 \napprox \approxeq \approxident \lll 

1881 \ggg \nparallel \Vdash \Vvdash 

1882 \nVdash \nvdash \vDash \nvDash 

1883 \nVDash \oequal \simneqq \triangle 

1884 \triangleq \triangleeq \triangleleft 

1885 \triangleright \ntriangleleft \ntriangleright 

1886 \trianglelefteq \ntrianglelefteq \trianglerighteq 

1887 \ntrianglerighteq \blacktriangleleft \blacktriangleright 

1888 \equalparallel \measuredrightangle \varlrtriangle 

1889 \Doteq \Bumpeq \Subset \Supset 

1890 \backepsilon \because \therefore \bot 

1891 \top \bumpeq \circeq \coloneq 

1892 \curlyeqprec \curlyeqsucc \eqcirc \eqcolon 

1893 \eqsim \fallingdotseq \gtrdot \gtrless 

1894 \ltimes \rtimes \lessdot \ne 

1895 \ncong \nequiv \ngeq \ngtr 

1896 \nleq \nless \nmid \notin 

1897 \nprec \nsubset \nsubseteq \nsucc 

1898 \nsupset \nsupseteq \pitchfork \preccurlyeq 

1899 \risingdotseq \subsetneq \succcurlyeq \supsetneq 

1900 \varpropto \vartriangleleft \scurel 

1901 \vartriangleright \rightangle \equal \backcong 

1902 \eqdef \wedgeq \questeq \between 

1903 \veeeq \disin \varisins \isins 

1904 \isindot \varisinobar \isinobar \isinvb 

1905 \isinE \nisd \varnis \nis 

1906 \varniobar \niobar \bagmember \ratio 

1907 \Equiv \stareq \measeq \arceq 

1908 \rightassert \rightModels \smallin \smallowns 

1909 \notsmallowns \nsimeq'''.split()) 

1910 

1911 _arrow_symbols = set(r""" 

1912 \leftarrow \longleftarrow \uparrow \Leftarrow \Longleftarrow 

1913 \Uparrow \rightarrow \longrightarrow \downarrow \Rightarrow 

1914 \Longrightarrow \Downarrow \leftrightarrow \updownarrow 

1915 \longleftrightarrow \updownarrow \Leftrightarrow 

1916 \Longleftrightarrow \Updownarrow \mapsto \longmapsto \nearrow 

1917 \hookleftarrow \hookrightarrow \searrow \leftharpoonup 

1918 \rightharpoonup \swarrow \leftharpoondown \rightharpoondown 

1919 \nwarrow \rightleftharpoons \leadsto \dashrightarrow 

1920 \dashleftarrow \leftleftarrows \leftrightarrows \Lleftarrow 

1921 \Rrightarrow \twoheadleftarrow \leftarrowtail \looparrowleft 

1922 \leftrightharpoons \curvearrowleft \circlearrowleft \Lsh 

1923 \upuparrows \upharpoonleft \downharpoonleft \multimap 

1924 \leftrightsquigarrow \rightrightarrows \rightleftarrows 

1925 \rightrightarrows \rightleftarrows \twoheadrightarrow 

1926 \rightarrowtail \looparrowright \rightleftharpoons 

1927 \curvearrowright \circlearrowright \Rsh \downdownarrows 

1928 \upharpoonright \downharpoonright \rightsquigarrow \nleftarrow 

1929 \nrightarrow \nLeftarrow \nRightarrow \nleftrightarrow 

1930 \nLeftrightarrow \to \Swarrow \Searrow \Nwarrow \Nearrow 

1931 \leftsquigarrow \overleftarrow \overleftrightarrow \cwopencirclearrow 

1932 \downzigzagarrow \cupleftarrow \rightzigzagarrow \twoheaddownarrow 

1933 \updownarrowbar \twoheaduparrow \rightarrowbar \updownarrows 

1934 \barleftarrow \mapsfrom \mapsdown \mapsup \Ldsh \Rdsh 

1935 """.split()) 

1936 

1937 _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols 

1938 

1939 _punctuation_symbols = set(r', ; . ! \ldotp \cdotp'.split()) 

1940 

1941 _overunder_symbols = set(r''' 

1942 \sum \prod \coprod \bigcap \bigcup \bigsqcup \bigvee 

1943 \bigwedge \bigodot \bigotimes \bigoplus \biguplus 

1944 '''.split()) 

1945 

1946 _overunder_functions = set("lim liminf limsup sup max min".split()) 

1947 

1948 _dropsub_symbols = set(r'\int \oint \iint \oiint \iiint \oiiint \iiiint'.split()) 

1949 

1950 _fontnames = set("rm cal it tt sf bf bfit " 

1951 "default bb frak scr regular".split()) 

1952 

1953 _function_names = set(""" 

1954 arccos csc ker min arcsin deg lg Pr arctan det lim sec arg dim 

1955 liminf sin cos exp limsup sinh cosh gcd ln sup cot hom log tan 

1956 coth inf max tanh""".split()) 

1957 

1958 _ambi_delims = set(r""" 

1959 | \| / \backslash \uparrow \downarrow \updownarrow \Uparrow 

1960 \Downarrow \Updownarrow . \vert \Vert""".split()) 

1961 _left_delims = set(r""" 

1962 ( [ \{ < \lfloor \langle \lceil \lbrace \leftbrace \lbrack \leftparen \lgroup 

1963 """.split()) 

1964 _right_delims = set(r""" 

1965 ) ] \} > \rfloor \rangle \rceil \rbrace \rightbrace \rbrack \rightparen \rgroup 

1966 """.split()) 

1967 _delims = _left_delims | _right_delims | _ambi_delims 

1968 

1969 _small_greek = set([unicodedata.name(chr(i)).split()[-1].lower() for i in 

1970 range(ord('\N{GREEK SMALL LETTER ALPHA}'), 

1971 ord('\N{GREEK SMALL LETTER OMEGA}') + 1)]) 

1972 _latin_alphabets = set(string.ascii_letters) 

1973 

1974 def __init__(self) -> None: 

1975 p = types.SimpleNamespace() 

1976 

1977 def set_names_and_parse_actions() -> None: 

1978 for key, val in vars(p).items(): 

1979 if not key.startswith('_'): 

1980 # Set names on (almost) everything -- very useful for debugging 

1981 # token, placeable, and auto_delim are forward references which 

1982 # are left without names to ensure useful error messages 

1983 if key not in ("token", "placeable", "auto_delim"): 

1984 val.setName(key) 

1985 # Set actions 

1986 if hasattr(self, key): 

1987 val.setParseAction(getattr(self, key)) 

1988 

1989 # Root definitions. 

1990 

1991 # In TeX parlance, a csname is a control sequence name (a "\foo"). 

1992 def csnames(group: str, names: Iterable[str]) -> Regex: 

1993 ends_with_alpha = [] 

1994 ends_with_nonalpha = [] 

1995 for name in names: 

1996 if name[-1].isalpha(): 

1997 ends_with_alpha.append(name) 

1998 else: 

1999 ends_with_nonalpha.append(name) 

2000 return Regex( 

2001 r"\\(?P<{group}>(?:{alpha})(?![A-Za-z]){additional}{nonalpha})".format( 

2002 group=group, 

2003 alpha="|".join(map(re.escape, ends_with_alpha)), 

2004 additional="|" if ends_with_nonalpha else "", 

2005 nonalpha="|".join(map(re.escape, ends_with_nonalpha)), 

2006 ) 

2007 ) 

2008 

2009 p.float_literal = Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)") 

2010 p.space = oneOf(self._space_widths)("space") 

2011 

2012 p.style_literal = oneOf( 

2013 [str(e.value) for e in self._MathStyle])("style_literal") 

2014 

2015 p.symbol = Regex( 

2016 r"[a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|\U00000080-\U0001ffff]" 

2017 r"|\\[%${}\[\]_|]" 

2018 + r"|\\(?:{})(?![A-Za-z])".format( 

2019 "|".join(map(re.escape, tex2uni))) 

2020 )("sym").leaveWhitespace() 

2021 p.unknown_symbol = Regex(r"\\[A-Za-z]+")("name") 

2022 

2023 p.font = csnames("font", self._fontnames) 

2024 p.start_group = Optional(r"\math" + oneOf(self._fontnames)("font")) + "{" 

2025 p.end_group = Literal("}") 

2026 

2027 p.delim = oneOf(self._delims) 

2028 

2029 # Mutually recursive definitions. (Minimizing the number of Forward 

2030 # elements is important for speed.) 

2031 p.auto_delim = Forward() 

2032 p.placeable = Forward() 

2033 p.named_placeable = Forward() 

2034 p.required_group = Forward() 

2035 p.optional_group = Forward() 

2036 p.token = Forward() 

2037 

2038 # Workaround for placable being part of a cycle of definitions 

2039 # calling `p.placeable("name")` results in a copy, so not guaranteed 

2040 # to get the definition added after it is used. 

2041 # ref https://github.com/matplotlib/matplotlib/issues/25204 

2042 # xref https://github.com/pyparsing/pyparsing/issues/95 

2043 p.named_placeable <<= p.placeable 

2044 

2045 set_names_and_parse_actions() # for mutually recursive definitions. 

2046 

2047 p.optional_group <<= "{" + ZeroOrMore(p.token)("group") + "}" 

2048 p.required_group <<= "{" + OneOrMore(p.token)("group") + "}" 

2049 

2050 p.customspace = cmd(r"\hspace", "{" + p.float_literal("space") + "}") 

2051 

2052 p.accent = ( 

2053 csnames("accent", [*self._accent_map, *self._wide_accents]) 

2054 - p.named_placeable("sym")) 

2055 

2056 p.function = csnames("name", self._function_names) 

2057 

2058 p.group = p.start_group + ZeroOrMore(p.token)("group") + p.end_group 

2059 p.unclosed_group = (p.start_group + ZeroOrMore(p.token)("group") + StringEnd()) 

2060 

2061 p.frac = cmd(r"\frac", p.required_group("num") + p.required_group("den")) 

2062 p.dfrac = cmd(r"\dfrac", p.required_group("num") + p.required_group("den")) 

2063 p.binom = cmd(r"\binom", p.required_group("num") + p.required_group("den")) 

2064 

2065 p.genfrac = cmd( 

2066 r"\genfrac", 

2067 "{" + Optional(p.delim)("ldelim") + "}" 

2068 + "{" + Optional(p.delim)("rdelim") + "}" 

2069 + "{" + p.float_literal("rulesize") + "}" 

2070 + "{" + Optional(p.style_literal)("style") + "}" 

2071 + p.required_group("num") 

2072 + p.required_group("den")) 

2073 

2074 p.sqrt = cmd( 

2075 r"\sqrt{value}", 

2076 Optional("[" + OneOrMore(NotAny("]") + p.token)("root") + "]") 

2077 + p.required_group("value")) 

2078 

2079 p.overline = cmd(r"\overline", p.required_group("body")) 

2080 

2081 p.overset = cmd( 

2082 r"\overset", 

2083 p.optional_group("annotation") + p.optional_group("body")) 

2084 p.underset = cmd( 

2085 r"\underset", 

2086 p.optional_group("annotation") + p.optional_group("body")) 

2087 

2088 p.text = cmd(r"\text", QuotedString('{', '\\', endQuoteChar="}")) 

2089 

2090 p.substack = cmd(r"\substack", 

2091 nested_expr(opener="{", closer="}", 

2092 content=Group(OneOrMore(p.token)) + 

2093 ZeroOrMore(Literal("\\\\").suppress()))("parts")) 

2094 

2095 p.subsuper = ( 

2096 (Optional(p.placeable)("nucleus") 

2097 + OneOrMore(oneOf(["_", "^"]) - p.placeable)("subsuper") 

2098 + Regex("'*")("apostrophes")) 

2099 | Regex("'+")("apostrophes") 

2100 | (p.named_placeable("nucleus") + Regex("'*")("apostrophes")) 

2101 ) 

2102 

2103 p.simple = p.space | p.customspace | p.font | p.subsuper 

2104 

2105 p.token <<= ( 

2106 p.simple 

2107 | p.auto_delim 

2108 | p.unclosed_group 

2109 | p.unknown_symbol # Must be last 

2110 ) 

2111 

2112 p.operatorname = cmd(r"\operatorname", "{" + ZeroOrMore(p.simple)("name") + "}") 

2113 

2114 p.boldsymbol = cmd( 

2115 r"\boldsymbol", "{" + ZeroOrMore(p.simple)("value") + "}") 

2116 

2117 p.placeable <<= ( 

2118 p.accent # Must be before symbol as all accents are symbols 

2119 | p.symbol # Must be second to catch all named symbols and single 

2120 # chars not in a group 

2121 | p.function 

2122 | p.operatorname 

2123 | p.group 

2124 | p.frac 

2125 | p.dfrac 

2126 | p.binom 

2127 | p.genfrac 

2128 | p.overset 

2129 | p.underset 

2130 | p.sqrt 

2131 | p.overline 

2132 | p.text 

2133 | p.boldsymbol 

2134 | p.substack 

2135 ) 

2136 

2137 mdelim = r"\middle" - (p.delim("mdelim") | Error("Expected a delimiter")) 

2138 p.auto_delim <<= ( 

2139 r"\left" - (p.delim("left") | Error("Expected a delimiter")) 

2140 + ZeroOrMore(p.simple | p.auto_delim | mdelim)("mid") 

2141 + r"\right" - (p.delim("right") | Error("Expected a delimiter")) 

2142 ) 

2143 

2144 # Leaf definitions. 

2145 p.math = OneOrMore(p.token) 

2146 p.math_string = QuotedString('$', '\\', unquoteResults=False) 

2147 p.non_math = Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace() 

2148 p.main = ( 

2149 p.non_math + ZeroOrMore(p.math_string + p.non_math) + StringEnd() 

2150 ) 

2151 set_names_and_parse_actions() # for leaf definitions. 

2152 

2153 self._expression = p.main 

2154 self._math_expression = p.math 

2155 

2156 # To add space to nucleus operators after sub/superscripts 

2157 self._in_subscript_or_superscript = False 

2158 

2159 def parse(self, s: str, fonts_object: Fonts, fontsize: float, dpi: float) -> Hlist: 

2160 """ 

2161 Parse expression *s* using the given *fonts_object* for 

2162 output, at the given *fontsize* and *dpi*. 

2163 

2164 Returns the parse tree of `Node` instances. 

2165 """ 

2166 self._state_stack = [ 

2167 ParserState(fonts_object, 'default', 'rm', fontsize, dpi)] 

2168 self._em_width_cache: dict[tuple[str, float, float], float] = {} 

2169 try: 

2170 result = self._expression.parseString(s) 

2171 except ParseBaseException as err: 

2172 # explain becomes a plain method on pyparsing 3 (err.explain(0)). 

2173 raise ValueError("\n" + ParseException.explain(err, 0)) from None 

2174 self._state_stack = [] 

2175 self._in_subscript_or_superscript = False 

2176 # prevent operator spacing from leaking into a new expression 

2177 self._em_width_cache = {} 

2178 ParserElement.resetCache() 

2179 return T.cast(Hlist, result[0]) # Known return type from main. 

2180 

2181 def get_state(self) -> ParserState: 

2182 """Get the current `State` of the parser.""" 

2183 return self._state_stack[-1] 

2184 

2185 def pop_state(self) -> None: 

2186 """Pop a `State` off of the stack.""" 

2187 self._state_stack.pop() 

2188 

2189 def push_state(self) -> None: 

2190 """Push a new `State` onto the stack, copying the current state.""" 

2191 self._state_stack.append(self.get_state().copy()) 

2192 

2193 def main(self, toks: ParseResults) -> list[Hlist]: 

2194 return [Hlist(toks.asList())] 

2195 

2196 def math_string(self, toks: ParseResults) -> ParseResults: 

2197 return self._math_expression.parseString(toks[0][1:-1], parseAll=True) 

2198 

2199 def math(self, toks: ParseResults) -> T.Any: 

2200 hlist = Hlist(toks.asList()) 

2201 self.pop_state() 

2202 return [hlist] 

2203 

2204 def non_math(self, toks: ParseResults) -> T.Any: 

2205 s = toks[0].replace(r'\$', '$') 

2206 symbols = [Char(c, self.get_state()) for c in s] 

2207 hlist = Hlist(symbols) 

2208 # We're going into math now, so set font to 'it' 

2209 self.push_state() 

2210 self.get_state().font = mpl.rcParams['mathtext.default'] 

2211 return [hlist] 

2212 

2213 float_literal = staticmethod(pyparsing_common.convertToFloat) 

2214 

2215 def text(self, toks: ParseResults) -> T.Any: 

2216 self.push_state() 

2217 state = self.get_state() 

2218 state.font = 'rm' 

2219 hlist = Hlist([Char(c, state) for c in toks[1]]) 

2220 self.pop_state() 

2221 return [hlist] 

2222 

2223 def _make_space(self, percentage: float) -> Kern: 

2224 # In TeX, an em (the unit usually used to measure horizontal lengths) 

2225 # is not the width of the character 'm'; it is the same in different 

2226 # font styles (e.g. roman or italic). Mathtext, however, uses 'm' in 

2227 # the italic style so that horizontal spaces don't depend on the 

2228 # current font style. 

2229 state = self.get_state() 

2230 key = (state.font, state.fontsize, state.dpi) 

2231 width = self._em_width_cache.get(key) 

2232 if width is None: 

2233 metrics = state.fontset.get_metrics( 

2234 'it', mpl.rcParams['mathtext.default'], 'm', 

2235 state.fontsize, state.dpi) 

2236 width = metrics.advance 

2237 self._em_width_cache[key] = width 

2238 return Kern(width * percentage) 

2239 

2240 _space_widths = { 

2241 r'\,': 0.16667, # 3/18 em = 3 mu 

2242 r'\thinspace': 0.16667, # 3/18 em = 3 mu 

2243 r'\/': 0.16667, # 3/18 em = 3 mu 

2244 r'\>': 0.22222, # 4/18 em = 4 mu 

2245 r'\:': 0.22222, # 4/18 em = 4 mu 

2246 r'\;': 0.27778, # 5/18 em = 5 mu 

2247 r'\ ': 0.33333, # 6/18 em = 6 mu 

2248 r'~': 0.33333, # 6/18 em = 6 mu, nonbreakable 

2249 r'\enspace': 0.5, # 9/18 em = 9 mu 

2250 r'\quad': 1, # 1 em = 18 mu 

2251 r'\qquad': 2, # 2 em = 36 mu 

2252 r'\!': -0.16667, # -3/18 em = -3 mu 

2253 } 

2254 

2255 def space(self, toks: ParseResults) -> T.Any: 

2256 num = self._space_widths[toks["space"]] 

2257 box = self._make_space(num) 

2258 return [box] 

2259 

2260 def customspace(self, toks: ParseResults) -> T.Any: 

2261 return [self._make_space(toks["space"])] 

2262 

2263 def symbol(self, s: str, loc: int, 

2264 toks: ParseResults | dict[str, str]) -> T.Any: 

2265 c = toks["sym"] 

2266 if c == "-": 

2267 # "U+2212 minus sign is the preferred representation of the unary 

2268 # and binary minus sign rather than the ASCII-derived U+002D 

2269 # hyphen-minus, because minus sign is unambiguous and because it 

2270 # is rendered with a more desirable length, usually longer than a 

2271 # hyphen." (https://www.unicode.org/reports/tr25/) 

2272 c = "\N{MINUS SIGN}" 

2273 try: 

2274 char = Char(c, self.get_state()) 

2275 except ValueError as err: 

2276 raise ParseFatalException(s, loc, 

2277 "Unknown symbol: %s" % c) from err 

2278 

2279 if c in self._spaced_symbols: 

2280 # iterate until we find previous character, needed for cases 

2281 # such as ${ -2}$, $ -2$, or $ -2$. 

2282 prev_char = next((c for c in s[:loc][::-1] if c != ' '), '') 

2283 # Binary operators at start of string should not be spaced 

2284 # Also, operators in sub- or superscripts should not be spaced 

2285 if (self._in_subscript_or_superscript or ( 

2286 c in self._binary_operators and ( 

2287 len(s[:loc].split()) == 0 or prev_char == '{' or 

2288 prev_char in self._left_delims))): 

2289 return [char] 

2290 else: 

2291 return [Hlist([self._make_space(0.2), 

2292 char, 

2293 self._make_space(0.2)], 

2294 do_kern=True)] 

2295 elif c in self._punctuation_symbols: 

2296 prev_char = next((c for c in s[:loc][::-1] if c != ' '), '') 

2297 next_char = next((c for c in s[loc + 1:] if c != ' '), '') 

2298 

2299 # Do not space commas between brackets 

2300 if c == ',': 

2301 if prev_char == '{' and next_char == '}': 

2302 return [char] 

2303 

2304 # Do not space dots as decimal separators 

2305 if c == '.' and prev_char.isdigit() and next_char.isdigit(): 

2306 return [char] 

2307 else: 

2308 return [Hlist([char, self._make_space(0.2)], do_kern=True)] 

2309 return [char] 

2310 

2311 def unknown_symbol(self, s: str, loc: int, toks: ParseResults) -> T.Any: 

2312 raise ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}") 

2313 

2314 _accent_map = { 

2315 r'hat': r'\circumflexaccent', 

2316 r'breve': r'\combiningbreve', 

2317 r'bar': r'\combiningoverline', 

2318 r'grave': r'\combininggraveaccent', 

2319 r'acute': r'\combiningacuteaccent', 

2320 r'tilde': r'\combiningtilde', 

2321 r'dot': r'\combiningdotabove', 

2322 r'ddot': r'\combiningdiaeresis', 

2323 r'dddot': r'\combiningthreedotsabove', 

2324 r'ddddot': r'\combiningfourdotsabove', 

2325 r'vec': r'\combiningrightarrowabove', 

2326 r'"': r'\combiningdiaeresis', 

2327 r"`": r'\combininggraveaccent', 

2328 r"'": r'\combiningacuteaccent', 

2329 r'~': r'\combiningtilde', 

2330 r'.': r'\combiningdotabove', 

2331 r'^': r'\circumflexaccent', 

2332 r'overrightarrow': r'\rightarrow', 

2333 r'overleftarrow': r'\leftarrow', 

2334 r'mathring': r'\circ', 

2335 } 

2336 

2337 _wide_accents = set(r"widehat widetilde widebar".split()) 

2338 

2339 def accent(self, toks: ParseResults) -> T.Any: 

2340 state = self.get_state() 

2341 thickness = state.get_current_underline_thickness() 

2342 accent = toks["accent"] 

2343 sym = toks["sym"] 

2344 accent_box: Node 

2345 if accent in self._wide_accents: 

2346 accent_box = AutoWidthChar( 

2347 '\\' + accent, sym.width, state, char_class=Accent) 

2348 else: 

2349 accent_box = Accent(self._accent_map[accent], state) 

2350 if accent == 'mathring': 

2351 accent_box.shrink() 

2352 accent_box.shrink() 

2353 centered = HCentered([Hbox(sym.width / 4.0), accent_box]) 

2354 centered.hpack(sym.width, 'exactly') 

2355 return Vlist([ 

2356 centered, 

2357 Vbox(0., thickness * 2.0), 

2358 Hlist([sym]) 

2359 ]) 

2360 

2361 def function(self, s: str, loc: int, toks: ParseResults) -> T.Any: 

2362 hlist = self.operatorname(s, loc, toks) 

2363 hlist.function_name = toks["name"] 

2364 return hlist 

2365 

2366 def operatorname(self, s: str, loc: int, toks: ParseResults) -> T.Any: 

2367 self.push_state() 

2368 state = self.get_state() 

2369 state.font = 'rm' 

2370 hlist_list: list[Node] = [] 

2371 # Change the font of Chars, but leave Kerns alone 

2372 name = toks["name"] 

2373 for c in name: 

2374 if isinstance(c, Char): 

2375 c.font = 'rm' 

2376 c._update_metrics() 

2377 hlist_list.append(c) 

2378 elif isinstance(c, str): 

2379 hlist_list.append(Char(c, state)) 

2380 else: 

2381 hlist_list.append(c) 

2382 next_char_loc = loc + len(name) + 1 

2383 if isinstance(name, ParseResults): 

2384 next_char_loc += len('operatorname{}') 

2385 next_char = next((c for c in s[next_char_loc:] if c != ' '), '') 

2386 delimiters = self._delims | {'^', '_'} 

2387 if (next_char not in delimiters and 

2388 name not in self._overunder_functions): 

2389 # Add thin space except when followed by parenthesis, bracket, etc. 

2390 hlist_list += [self._make_space(self._space_widths[r'\,'])] 

2391 self.pop_state() 

2392 # if followed by a super/subscript, set flag to true 

2393 # This flag tells subsuper to add space after this operator 

2394 if next_char in {'^', '_'}: 

2395 self._in_subscript_or_superscript = True 

2396 else: 

2397 self._in_subscript_or_superscript = False 

2398 

2399 return Hlist(hlist_list) 

2400 

2401 def start_group(self, toks: ParseResults) -> T.Any: 

2402 self.push_state() 

2403 # Deal with LaTeX-style font tokens 

2404 if toks.get("font"): 

2405 self.get_state().font = toks.get("font") 

2406 return [] 

2407 

2408 def group(self, toks: ParseResults) -> T.Any: 

2409 grp = Hlist(toks.get("group", [])) 

2410 return [grp] 

2411 

2412 def required_group(self, toks: ParseResults) -> T.Any: 

2413 return Hlist(toks.get("group", [])) 

2414 

2415 optional_group = required_group 

2416 

2417 def end_group(self) -> T.Any: 

2418 self.pop_state() 

2419 return [] 

2420 

2421 def unclosed_group(self, s: str, loc: int, toks: ParseResults) -> T.Any: 

2422 raise ParseFatalException(s, len(s), "Expected '}'") 

2423 

2424 def font(self, toks: ParseResults) -> T.Any: 

2425 self.get_state().font = toks["font"] 

2426 return [] 

2427 

2428 def is_overunder(self, nucleus: Node) -> bool: 

2429 if isinstance(nucleus, Char): 

2430 return nucleus.c in self._overunder_symbols 

2431 elif isinstance(nucleus, Hlist) and hasattr(nucleus, 'function_name'): 

2432 return nucleus.function_name in self._overunder_functions 

2433 return False 

2434 

2435 def is_dropsub(self, nucleus: Node) -> bool: 

2436 if isinstance(nucleus, Char): 

2437 return nucleus.c in self._dropsub_symbols 

2438 return False 

2439 

2440 def is_slanted(self, nucleus: Node) -> bool: 

2441 if isinstance(nucleus, Char): 

2442 return nucleus.is_slanted() 

2443 return False 

2444 

2445 def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: 

2446 nucleus = toks.get("nucleus", Hbox(0)) 

2447 subsuper = toks.get("subsuper", []) 

2448 napostrophes = len(toks.get("apostrophes", [])) 

2449 

2450 if not subsuper and not napostrophes: 

2451 return nucleus 

2452 

2453 sub = super = None 

2454 while subsuper: 

2455 op, arg, *subsuper = subsuper 

2456 if op == '_': 

2457 if sub is not None: 

2458 raise ParseFatalException("Double subscript") 

2459 sub = arg 

2460 else: 

2461 if super is not None: 

2462 raise ParseFatalException("Double superscript") 

2463 super = arg 

2464 

2465 state = self.get_state() 

2466 rule_thickness = state.fontset.get_underline_thickness( 

2467 state.font, state.fontsize, state.dpi) 

2468 xHeight = state.fontset.get_xheight( 

2469 state.font, state.fontsize, state.dpi) 

2470 

2471 if napostrophes: 

2472 if super is None: 

2473 super = Hlist([]) 

2474 for i in range(napostrophes): 

2475 super.children.extend(self.symbol(s, loc, {"sym": "\\prime"})) 

2476 # kern() and hpack() needed to get the metrics right after 

2477 # extending 

2478 super.kern() 

2479 super.hpack() 

2480 

2481 # Handle over/under symbols, such as sum or prod 

2482 if self.is_overunder(nucleus): 

2483 vlist = [] 

2484 shift = 0. 

2485 width = nucleus.width 

2486 if super is not None: 

2487 super.shrink() 

2488 width = max(width, super.width) 

2489 if sub is not None: 

2490 sub.shrink() 

2491 width = max(width, sub.width) 

2492 

2493 vgap = rule_thickness * 3.0 

2494 if super is not None: 

2495 hlist = HCentered([super]) 

2496 hlist.hpack(width, 'exactly') 

2497 vlist.extend([hlist, Vbox(0, vgap)]) 

2498 hlist = HCentered([nucleus]) 

2499 hlist.hpack(width, 'exactly') 

2500 vlist.append(hlist) 

2501 if sub is not None: 

2502 hlist = HCentered([sub]) 

2503 hlist.hpack(width, 'exactly') 

2504 vlist.extend([Vbox(0, vgap), hlist]) 

2505 shift = hlist.height + vgap + nucleus.depth 

2506 vlt = Vlist(vlist) 

2507 vlt.shift_amount = shift 

2508 result = Hlist([vlt]) 

2509 return [result] 

2510 

2511 # We remove kerning on the last character for consistency (otherwise 

2512 # it will compute kerning based on non-shrunk characters and may put 

2513 # them too close together when superscripted) 

2514 # We change the width of the last character to match the advance to 

2515 # consider some fonts with weird metrics: e.g. stix's f has a width of 

2516 # 7.75 and a kerning of -4.0 for an advance of 3.72, and we want to put 

2517 # the superscript at the advance 

2518 last_char = nucleus 

2519 if isinstance(nucleus, Hlist): 

2520 new_children = nucleus.children 

2521 if len(new_children): 

2522 # remove last kern 

2523 if (isinstance(new_children[-1], Kern) and 

2524 hasattr(new_children[-2], '_metrics')): 

2525 new_children = new_children[:-1] 

2526 last_char = new_children[-1] 

2527 if hasattr(last_char, '_metrics'): 

2528 last_char.width = last_char._metrics.advance 

2529 # create new Hlist without kerning 

2530 nucleus = Hlist(new_children, do_kern=False) 

2531 else: 

2532 if isinstance(nucleus, Char): 

2533 last_char.width = last_char._metrics.advance 

2534 nucleus = Hlist([nucleus]) 

2535 

2536 # Handle regular sub/superscripts 

2537 constants = _get_font_constant_set(state) 

2538 lc_height = last_char.height 

2539 lc_baseline = 0 

2540 if self.is_dropsub(last_char): 

2541 lc_baseline = last_char.depth 

2542 

2543 # Compute kerning for sub and super 

2544 superkern = constants.delta * xHeight 

2545 subkern = constants.delta * xHeight 

2546 if self.is_slanted(last_char): 

2547 superkern += constants.delta * xHeight 

2548 superkern += (constants.delta_slanted * 

2549 (lc_height - xHeight * 2. / 3.)) 

2550 if self.is_dropsub(last_char): 

2551 subkern = (3 * constants.delta - 

2552 constants.delta_integral) * lc_height 

2553 superkern = (3 * constants.delta + 

2554 constants.delta_integral) * lc_height 

2555 else: 

2556 subkern = 0 

2557 

2558 x: List 

2559 if super is None: 

2560 # node757 

2561 # Note: One of super or sub must be a Node if we're in this function, but 

2562 # mypy can't know this, since it can't interpret pyparsing expressions, 

2563 # hence the cast. 

2564 x = Hlist([Kern(subkern), T.cast(Node, sub)]) 

2565 x.shrink() 

2566 if self.is_dropsub(last_char): 

2567 shift_down = lc_baseline + constants.subdrop * xHeight 

2568 else: 

2569 shift_down = constants.sub1 * xHeight 

2570 x.shift_amount = shift_down 

2571 else: 

2572 x = Hlist([Kern(superkern), super]) 

2573 x.shrink() 

2574 if self.is_dropsub(last_char): 

2575 shift_up = lc_height - constants.subdrop * xHeight 

2576 else: 

2577 shift_up = constants.sup1 * xHeight 

2578 if sub is None: 

2579 x.shift_amount = -shift_up 

2580 else: # Both sub and superscript 

2581 y = Hlist([Kern(subkern), sub]) 

2582 y.shrink() 

2583 if self.is_dropsub(last_char): 

2584 shift_down = lc_baseline + constants.subdrop * xHeight 

2585 else: 

2586 shift_down = constants.sub2 * xHeight 

2587 # If sub and superscript collide, move super up 

2588 clr = (2.0 * rule_thickness - 

2589 ((shift_up - x.depth) - (y.height - shift_down))) 

2590 if clr > 0.: 

2591 shift_up += clr 

2592 x = Vlist([ 

2593 x, 

2594 Kern((shift_up - x.depth) - (y.height - shift_down)), 

2595 y]) 

2596 x.shift_amount = shift_down 

2597 

2598 if not self.is_dropsub(last_char): 

2599 x.width += constants.script_space * xHeight 

2600 

2601 # Do we need to add a space after the nucleus? 

2602 # To find out, check the flag set by operatorname 

2603 spaced_nucleus = [nucleus, x] 

2604 if self._in_subscript_or_superscript: 

2605 spaced_nucleus += [self._make_space(self._space_widths[r'\,'])] 

2606 self._in_subscript_or_superscript = False 

2607 

2608 result = Hlist(spaced_nucleus) 

2609 return [result] 

2610 

2611 def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathStyle, 

2612 num: Hlist, den: Hlist) -> T.Any: 

2613 state = self.get_state() 

2614 thickness = state.get_current_underline_thickness() 

2615 

2616 for _ in range(style.value): 

2617 num.shrink() 

2618 den.shrink() 

2619 cnum = HCentered([num]) 

2620 cden = HCentered([den]) 

2621 width = max(num.width, den.width) 

2622 cnum.hpack(width, 'exactly') 

2623 cden.hpack(width, 'exactly') 

2624 vlist = Vlist([cnum, # numerator 

2625 Vbox(0, thickness * 2.0), # space 

2626 Hrule(state, rule), # rule 

2627 Vbox(0, thickness * 2.0), # space 

2628 cden # denominator 

2629 ]) 

2630 

2631 # Shift so the fraction line sits in the middle of the 

2632 # equals sign 

2633 metrics = state.fontset.get_metrics( 

2634 state.font, mpl.rcParams['mathtext.default'], 

2635 '=', state.fontsize, state.dpi) 

2636 shift = (cden.height - 

2637 ((metrics.ymax + metrics.ymin) / 2 - 

2638 thickness * 3.0)) 

2639 vlist.shift_amount = shift 

2640 

2641 result = [Hlist([vlist, Hbox(thickness * 2.)])] 

2642 if ldelim or rdelim: 

2643 if ldelim == '': 

2644 ldelim = '.' 

2645 if rdelim == '': 

2646 rdelim = '.' 

2647 return self._auto_sized_delimiter(ldelim, 

2648 T.cast(list[T.Union[Box, Char, str]], 

2649 result), 

2650 rdelim) 

2651 return result 

2652 

2653 def style_literal(self, toks: ParseResults) -> T.Any: 

2654 return self._MathStyle(int(toks["style_literal"])) 

2655 

2656 def genfrac(self, toks: ParseResults) -> T.Any: 

2657 return self._genfrac( 

2658 toks.get("ldelim", ""), toks.get("rdelim", ""), 

2659 toks["rulesize"], toks.get("style", self._MathStyle.TEXTSTYLE), 

2660 toks["num"], toks["den"]) 

2661 

2662 def frac(self, toks: ParseResults) -> T.Any: 

2663 return self._genfrac( 

2664 "", "", self.get_state().get_current_underline_thickness(), 

2665 self._MathStyle.TEXTSTYLE, toks["num"], toks["den"]) 

2666 

2667 def dfrac(self, toks: ParseResults) -> T.Any: 

2668 return self._genfrac( 

2669 "", "", self.get_state().get_current_underline_thickness(), 

2670 self._MathStyle.DISPLAYSTYLE, toks["num"], toks["den"]) 

2671 

2672 def binom(self, toks: ParseResults) -> T.Any: 

2673 return self._genfrac( 

2674 "(", ")", 0, 

2675 self._MathStyle.TEXTSTYLE, toks["num"], toks["den"]) 

2676 

2677 def _genset(self, s: str, loc: int, toks: ParseResults) -> T.Any: 

2678 annotation = toks["annotation"] 

2679 body = toks["body"] 

2680 thickness = self.get_state().get_current_underline_thickness() 

2681 

2682 annotation.shrink() 

2683 centered_annotation = HCentered([annotation]) 

2684 centered_body = HCentered([body]) 

2685 width = max(centered_annotation.width, centered_body.width) 

2686 centered_annotation.hpack(width, 'exactly') 

2687 centered_body.hpack(width, 'exactly') 

2688 

2689 vgap = thickness * 3 

2690 if s[loc + 1] == "u": # \underset 

2691 vlist = Vlist([ 

2692 centered_body, # body 

2693 Vbox(0, vgap), # space 

2694 centered_annotation # annotation 

2695 ]) 

2696 # Shift so the body sits in the same vertical position 

2697 vlist.shift_amount = centered_body.depth + centered_annotation.height + vgap 

2698 else: # \overset 

2699 vlist = Vlist([ 

2700 centered_annotation, # annotation 

2701 Vbox(0, vgap), # space 

2702 centered_body # body 

2703 ]) 

2704 

2705 # To add horizontal gap between symbols: wrap the Vlist into 

2706 # an Hlist and extend it with an Hbox(0, horizontal_gap) 

2707 return vlist 

2708 

2709 overset = underset = _genset 

2710 

2711 def sqrt(self, toks: ParseResults) -> T.Any: 

2712 root = toks.get("root") 

2713 body = toks["value"] 

2714 state = self.get_state() 

2715 thickness = state.get_current_underline_thickness() 

2716 

2717 # Determine the height of the body, and add a little extra to 

2718 # the height so it doesn't seem cramped 

2719 height = body.height - body.shift_amount + thickness * 5.0 

2720 depth = body.depth + body.shift_amount 

2721 check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True) 

2722 height = check.height - check.shift_amount 

2723 depth = check.depth + check.shift_amount 

2724 

2725 # Put a little extra space to the left and right of the body 

2726 padded_body = Hlist([Hbox(2 * thickness), body, Hbox(2 * thickness)]) 

2727 rightside = Vlist([Hrule(state), Glue('fill'), padded_body]) 

2728 # Stretch the glue between the hrule and the body 

2729 rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0), 

2730 'exactly', depth) 

2731 

2732 # Add the root and shift it upward so it is above the tick. 

2733 # The value of 0.6 is a hard-coded hack ;) 

2734 if not root: 

2735 root = Box(check.width * 0.5, 0., 0.) 

2736 else: 

2737 root = Hlist(root) 

2738 root.shrink() 

2739 root.shrink() 

2740 

2741 root_vlist = Vlist([Hlist([root])]) 

2742 root_vlist.shift_amount = -height * 0.6 

2743 

2744 hlist = Hlist([root_vlist, # Root 

2745 # Negative kerning to put root over tick 

2746 Kern(-check.width * 0.5), 

2747 check, # Check 

2748 rightside]) # Body 

2749 return [hlist] 

2750 

2751 def overline(self, toks: ParseResults) -> T.Any: 

2752 body = toks["body"] 

2753 

2754 state = self.get_state() 

2755 thickness = state.get_current_underline_thickness() 

2756 

2757 height = body.height - body.shift_amount + thickness * 3.0 

2758 depth = body.depth + body.shift_amount 

2759 

2760 # Place overline above body 

2761 rightside = Vlist([Hrule(state), Glue('fill'), Hlist([body])]) 

2762 

2763 # Stretch the glue between the hrule and the body 

2764 rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0), 

2765 'exactly', depth) 

2766 

2767 hlist = Hlist([rightside]) 

2768 return [hlist] 

2769 

2770 def _auto_sized_delimiter(self, front: str, 

2771 middle: list[Box | Char | str], 

2772 back: str) -> T.Any: 

2773 state = self.get_state() 

2774 if len(middle): 

2775 height = max([x.height for x in middle if not isinstance(x, str)]) 

2776 depth = max([x.depth for x in middle if not isinstance(x, str)]) 

2777 factor = None 

2778 for idx, el in enumerate(middle): 

2779 if el == r'\middle': 

2780 c = T.cast(str, middle[idx + 1]) # Should be one of p.delims. 

2781 if c != '.': 

2782 middle[idx + 1] = AutoHeightChar( 

2783 c, height, depth, state, factor=factor) 

2784 else: 

2785 middle.remove(c) 

2786 del middle[idx] 

2787 # There should only be \middle and its delimiter as str, which have 

2788 # just been removed. 

2789 middle_part = T.cast(list[T.Union[Box, Char]], middle) 

2790 else: 

2791 height = 0 

2792 depth = 0 

2793 factor = 1.0 

2794 middle_part = [] 

2795 

2796 parts: list[Node] = [] 

2797 # \left. and \right. aren't supposed to produce any symbols 

2798 if front != '.': 

2799 parts.append( 

2800 AutoHeightChar(front, height, depth, state, factor=factor)) 

2801 parts.extend(middle_part) 

2802 if back != '.': 

2803 parts.append( 

2804 AutoHeightChar(back, height, depth, state, factor=factor)) 

2805 hlist = Hlist(parts) 

2806 return hlist 

2807 

2808 def auto_delim(self, toks: ParseResults) -> T.Any: 

2809 return self._auto_sized_delimiter( 

2810 toks["left"], 

2811 # if "mid" in toks ... can be removed when requiring pyparsing 3. 

2812 toks["mid"].asList() if "mid" in toks else [], 

2813 toks["right"]) 

2814 

2815 def boldsymbol(self, toks: ParseResults) -> T.Any: 

2816 self.push_state() 

2817 state = self.get_state() 

2818 hlist: list[Node] = [] 

2819 name = toks["value"] 

2820 for c in name: 

2821 if isinstance(c, Hlist): 

2822 k = c.children[1] 

2823 if isinstance(k, Char): 

2824 k.font = "bf" 

2825 k._update_metrics() 

2826 hlist.append(c) 

2827 elif isinstance(c, Char): 

2828 c.font = "bf" 

2829 if (c.c in self._latin_alphabets or 

2830 c.c[1:] in self._small_greek): 

2831 c.font = "bfit" 

2832 c._update_metrics() 

2833 c._update_metrics() 

2834 hlist.append(c) 

2835 else: 

2836 hlist.append(c) 

2837 self.pop_state() 

2838 

2839 return Hlist(hlist) 

2840 

2841 def substack(self, toks: ParseResults) -> T.Any: 

2842 parts = toks["parts"] 

2843 state = self.get_state() 

2844 thickness = state.get_current_underline_thickness() 

2845 

2846 hlist = [Hlist(k) for k in parts[0]] 

2847 max_width = max(map(lambda c: c.width, hlist)) 

2848 

2849 vlist = [] 

2850 for sub in hlist: 

2851 cp = HCentered([sub]) 

2852 cp.hpack(max_width, 'exactly') 

2853 vlist.append(cp) 

2854 

2855 stack = [val 

2856 for pair in zip(vlist, [Vbox(0, thickness * 2)] * len(vlist)) 

2857 for val in pair] 

2858 del stack[-1] 

2859 vlt = Vlist(stack) 

2860 result = [Hlist([vlt])] 

2861 return result