Coverage for /usr/lib/python3/dist-packages/matplotlib/_mathtext.py: 20%

1258 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1""" 

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

3""" 

4 

5import copy 

6from collections import namedtuple 

7import enum 

8import functools 

9import logging 

10import os 

11import re 

12import types 

13import unicodedata 

14 

15import numpy as np 

16from pyparsing import ( 

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

18 ParseBaseException, ParseExpression, ParseFatalException, ParserElement, 

19 ParseResults, QuotedString, Regex, StringEnd, ZeroOrMore, pyparsing_common) 

20 

21import matplotlib as mpl 

22from . import _api, cbook 

23from ._mathtext_data import ( 

24 latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) 

25from .font_manager import FontProperties, findfont, get_font 

26from .ft2font import FT2Image, KERNING_DEFAULT 

27 

28 

29ParserElement.enablePackrat() 

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

31 

32 

33############################################################################## 

34# FONTS 

35 

36 

37@_api.delete_parameter("3.6", "math") 

38def get_unicode_index(symbol, math=False): # Publicly exported. 

39 r""" 

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

41 

42 Parameters 

43 ---------- 

44 symbol : str 

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

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

47 math : bool, default: False 

48 If True (deprecated), replace ASCII hyphen-minus by Unicode minus. 

49 """ 

50 # From UTF #25: U+2212 minus sign is the preferred 

51 # representation of the unary and binary minus sign rather than 

52 # the ASCII-derived U+002D hyphen-minus, because minus sign is 

53 # unambiguous and because it is rendered with a more desirable 

54 # length, usually longer than a hyphen. 

55 # Remove this block when the 'math' parameter is deleted. 

56 if math and symbol == '-': 

57 return 0x2212 

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

59 return ord(symbol) 

60 except TypeError: 

61 pass 

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

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

64 except KeyError as err: 

65 raise ValueError( 

66 "'{}' is not a valid Unicode character or TeX/Type1 symbol" 

67 .format(symbol)) from err 

68 

69 

70VectorParse = namedtuple("VectorParse", "width height depth glyphs rects", 

71 module="matplotlib.mathtext") 

72VectorParse.__doc__ = r""" 

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

74 

75This tuple contains the global metrics (*width*, *height*, *depth*), a list of 

76*glyphs* (including their positions) and of *rect*\angles. 

77""" 

78 

79 

80RasterParse = namedtuple("RasterParse", "ox oy width height depth image", 

81 module="matplotlib.mathtext") 

82RasterParse.__doc__ = r""" 

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

84 

85This tuple contains the global metrics (*width*, *height*, *depth*), and a 

86raster *image*. The offsets *ox*, *oy* are always zero. 

87""" 

88 

89 

90class Output: 

91 r""" 

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

93 

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

95 a `RasterParse` by `.MathTextParser.parse`. 

96 """ 

97 

98 def __init__(self, box): 

99 self.box = box 

100 self.glyphs = [] # (ox, oy, info) 

101 self.rects = [] # (x1, y1, x2, y2) 

102 

103 def to_vector(self): 

104 w, h, d = map( 

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

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

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

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

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

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

111 

112 def to_raster(self): 

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

114 # hence the switch between ymin and ymax. 

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

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

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

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

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

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

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

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

123 w = xmax - xmin 

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

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

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

127 

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

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

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

131 # old approach and keeps baseline images backcompat. 

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

133 

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

135 info.font.draw_glyph_to_bitmap( 

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

137 antialiased=mpl.rcParams['text.antialiased']) 

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

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

140 if height == 0: 

141 center = (y2 + y1) / 2 

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

143 else: 

144 y = int(y1) 

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

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

147 

148 

149class Fonts: 

150 """ 

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

152 

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

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

155 to do the actual drawing. 

156 """ 

157 

158 def __init__(self, default_font_prop, load_glyph_flags): 

159 """ 

160 Parameters 

161 ---------- 

162 default_font_prop : `~.font_manager.FontProperties` 

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

164 font rendering. 

165 load_glyph_flags : int 

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

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

168 """ 

169 self.default_font_prop = default_font_prop 

170 self.load_glyph_flags = load_glyph_flags 

171 

172 def get_kern(self, font1, fontclass1, sym1, fontsize1, 

173 font2, fontclass2, sym2, fontsize2, dpi): 

174 """ 

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

176 

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

178 """ 

179 return 0. 

180 

181 def get_metrics(self, font, font_class, sym, fontsize, dpi): 

182 r""" 

183 Parameters 

184 ---------- 

185 font : str 

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

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

188 are synonyms and use the non-math font. 

189 font_class : str 

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

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

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

193 ...)``. 

194 sym : str 

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

196 fontsize : float 

197 Font size in points. 

198 dpi : float 

199 Rendering dots-per-inch. 

200 

201 Returns 

202 ------- 

203 object 

204 

205 The returned object has the following attributes (all floats, 

206 except *slanted*): 

207 

208 - *advance*: The advance distance (in points) of the glyph. 

209 - *height*: The height of the glyph in points. 

210 - *width*: The width of the glyph in points. 

211 - *xmin*, *xmax*, *ymin*, *ymax*: The ink rectangle of the glyph 

212 - *iceberg*: The distance from the baseline to the top of the 

213 glyph. (This corresponds to TeX's definition of "height".) 

214 - *slanted*: Whether the glyph should be considered as "slanted" 

215 (currently used for kerning sub/superscripts). 

216 """ 

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

218 return info.metrics 

219 

220 def render_glyph( 

221 self, output, ox, oy, font, font_class, sym, fontsize, dpi): 

222 """ 

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

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

225 """ 

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

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

228 

229 def render_rect_filled(self, output, x1, y1, x2, y2): 

230 """ 

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

232 """ 

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

234 

235 def get_xheight(self, font, fontsize, dpi): 

236 """ 

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

238 """ 

239 raise NotImplementedError() 

240 

241 def get_underline_thickness(self, font, fontsize, dpi): 

242 """ 

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

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

245 """ 

246 raise NotImplementedError() 

247 

248 def get_used_characters(self): 

249 """ 

250 Get the set of characters that were used in the math 

251 expression. Used by backends that need to subset fonts so 

252 they know which glyphs to include. 

253 """ 

254 return self.used_characters 

255 

256 def get_sized_alternatives_for_symbol(self, fontname, sym): 

257 """ 

258 Override if your font provides multiple sizes of the same 

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

260 various sizes. The expression renderer will select the most 

261 appropriate size for a given situation from this list. 

262 """ 

263 return [(fontname, sym)] 

264 

265 

266class TruetypeFonts(Fonts): 

267 """ 

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

269 (through FT2Font). 

270 """ 

271 

272 def __init__(self, *args, **kwargs): 

273 super().__init__(*args, **kwargs) 

274 # Per-instance cache. 

275 self._get_info = functools.lru_cache(None)(self._get_info) 

276 self._fonts = {} 

277 

278 filename = findfont(self.default_font_prop) 

279 default_font = get_font(filename) 

280 self._fonts['default'] = default_font 

281 self._fonts['regular'] = default_font 

282 

283 def _get_font(self, font): 

284 if font in self.fontmap: 

285 basename = self.fontmap[font] 

286 else: 

287 basename = font 

288 cached_font = self._fonts.get(basename) 

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

290 cached_font = get_font(basename) 

291 self._fonts[basename] = cached_font 

292 self._fonts[cached_font.postscript_name] = cached_font 

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

294 return cached_font 

295 

296 def _get_offset(self, font, glyph, fontsize, dpi): 

297 if font.postscript_name == 'Cmex10': 

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

299 return 0. 

300 

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

302 def _get_info(self, fontname, font_class, sym, fontsize, dpi): 

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

304 font.set_size(fontsize, dpi) 

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

306 

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

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

309 metrics = types.SimpleNamespace( 

310 advance = glyph.linearHoriAdvance/65536.0, 

311 height = glyph.height/64.0, 

312 width = glyph.width/64.0, 

313 xmin = xmin, 

314 xmax = xmax, 

315 ymin = ymin+offset, 

316 ymax = ymax+offset, 

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

318 iceberg = glyph.horiBearingY/64.0 + offset, 

319 slanted = slanted 

320 ) 

321 

322 return types.SimpleNamespace( 

323 font = font, 

324 fontsize = fontsize, 

325 postscript_name = font.postscript_name, 

326 metrics = metrics, 

327 num = num, 

328 glyph = glyph, 

329 offset = offset 

330 ) 

331 

332 def get_xheight(self, fontname, fontsize, dpi): 

333 font = self._get_font(fontname) 

334 font.set_size(fontsize, dpi) 

335 pclt = font.get_sfnt_table('pclt') 

336 if pclt is None: 

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

338 metrics = self.get_metrics( 

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

340 return metrics.iceberg 

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

342 return xHeight 

343 

344 def get_underline_thickness(self, font, fontsize, dpi): 

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

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

347 # is now hardcoded. 

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

349 

350 def get_kern(self, font1, fontclass1, sym1, fontsize1, 

351 font2, fontclass2, sym2, fontsize2, dpi): 

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

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

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

355 font = info1.font 

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

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

358 font2, fontclass2, sym2, fontsize2, dpi) 

359 

360 

361class BakomaFonts(TruetypeFonts): 

362 """ 

363 Use the Bakoma TrueType fonts for rendering. 

364 

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

366 its own proprietary 8-bit encoding. 

367 """ 

368 _fontmap = { 

369 'cal': 'cmsy10', 

370 'rm': 'cmr10', 

371 'tt': 'cmtt10', 

372 'it': 'cmmi10', 

373 'bf': 'cmb10', 

374 'sf': 'cmss10', 

375 'ex': 'cmex10', 

376 } 

377 

378 def __init__(self, *args, **kwargs): 

379 self._stix_fallback = StixFonts(*args, **kwargs) 

380 

381 super().__init__(*args, **kwargs) 

382 self.fontmap = {} 

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

384 fullpath = findfont(val) 

385 self.fontmap[key] = fullpath 

386 self.fontmap[val] = fullpath 

387 

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

389 

390 def _get_glyph(self, fontname, font_class, sym): 

391 font = None 

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

393 basename, num = latex_to_bakoma[sym] 

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

395 font = self._get_font(basename) 

396 elif len(sym) == 1: 

397 slanted = (fontname == "it") 

398 font = self._get_font(fontname) 

399 if font is not None: 

400 num = ord(sym) 

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

402 return font, num, slanted 

403 else: 

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

405 

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

407 # delimiters. The AutoSizedChar class will use these alternatives 

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

409 _size_alternatives = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

448 } 

449 

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

451 (r'\rightparent', ')'), 

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

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

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

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

456 (r'\{', '{'), 

457 (r'\}', '}'), 

458 (r'\[', '['), 

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

460 _size_alternatives[alias] = _size_alternatives[target] 

461 

462 def get_sized_alternatives_for_symbol(self, fontname, sym): 

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

464 

465 

466class UnicodeFonts(TruetypeFonts): 

467 """ 

468 An abstract base class for handling Unicode fonts. 

469 

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

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

472 complete set of math symbols is STIX. 

473 

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

475 symbol can not be found in the font. 

476 """ 

477 

478 def __init__(self, *args, **kwargs): 

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

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

481 font_cls = {'stix': StixFonts, 

482 'stixsans': StixSansFonts, 

483 'cm': BakomaFonts 

484 }.get(fallback_rc) 

485 self._fallback_font = font_cls(*args, **kwargs) if font_cls else None 

486 

487 super().__init__(*args, **kwargs) 

488 self.fontmap = {} 

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

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

491 font = findfont(prop) 

492 self.fontmap[texfont] = font 

493 prop = FontProperties('cmex10') 

494 font = findfont(prop) 

495 self.fontmap['ex'] = font 

496 

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

498 if isinstance(self._fallback_font, StixFonts): 

499 stixsizedaltfonts = { 

500 0: 'STIXGeneral', 

501 1: 'STIXSizeOneSym', 

502 2: 'STIXSizeTwoSym', 

503 3: 'STIXSizeThreeSym', 

504 4: 'STIXSizeFourSym', 

505 5: 'STIXSizeFiveSym'} 

506 

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

508 fullpath = findfont(name) 

509 self.fontmap[size] = fullpath 

510 self.fontmap[name] = fullpath 

511 

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

513 

514 def _map_virtual_font(self, fontname, font_class, uniindex): 

515 return fontname, uniindex 

516 

517 def _get_glyph(self, fontname, font_class, sym): 

518 try: 

519 uniindex = get_unicode_index(sym) 

520 found_symbol = True 

521 except ValueError: 

522 uniindex = ord('?') 

523 found_symbol = False 

524 _log.warning("No TeX to Unicode mapping for {!a}.".format(sym)) 

525 

526 fontname, uniindex = self._map_virtual_font( 

527 fontname, font_class, uniindex) 

528 

529 new_fontname = fontname 

530 

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

532 # mode. Greek capital letters should be Roman. 

533 if found_symbol: 

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

535 char = chr(uniindex) 

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

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

538 new_fontname = 'rm' 

539 

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

541 found_symbol = False 

542 font = self._get_font(new_fontname) 

543 if font is not None: 

544 if font.family_name == "cmr10" and uniindex == 0x2212: 

545 # minus sign exists in cmsy10 (not cmr10) 

546 font = get_font( 

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

548 uniindex = 0xa1 

549 glyphindex = font.get_char_index(uniindex) 

550 if glyphindex != 0: 

551 found_symbol = True 

552 

553 if not found_symbol: 

554 if self._fallback_font: 

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

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

557 fontname = 'rm' 

558 

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

560 family = g[0].family_name 

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

562 family = "Computer Modern" 

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

564 return g 

565 

566 else: 

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

568 and isinstance(self, StixFonts)): 

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

570 _log.warning("Font {!r} does not have a glyph for {!a} " 

571 "[U+{:x}], substituting with a dummy " 

572 "symbol.".format(new_fontname, sym, uniindex)) 

573 font = self._get_font('rm') 

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

575 slanted = False 

576 

577 return font, uniindex, slanted 

578 

579 def get_sized_alternatives_for_symbol(self, fontname, sym): 

580 if self._fallback_font: 

581 return self._fallback_font.get_sized_alternatives_for_symbol( 

582 fontname, sym) 

583 return [(fontname, sym)] 

584 

585 

586class DejaVuFonts(UnicodeFonts): 

587 

588 def __init__(self, *args, **kwargs): 

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

590 if isinstance(self, DejaVuSerifFonts): 

591 self._fallback_font = StixFonts(*args, **kwargs) 

592 else: 

593 self._fallback_font = StixSansFonts(*args, **kwargs) 

594 self.bakoma = BakomaFonts(*args, **kwargs) 

595 TruetypeFonts.__init__(self, *args, **kwargs) 

596 self.fontmap = {} 

597 # Include Stix sized alternatives for glyphs 

598 self._fontmap.update({ 

599 1: 'STIXSizeOneSym', 

600 2: 'STIXSizeTwoSym', 

601 3: 'STIXSizeThreeSym', 

602 4: 'STIXSizeFourSym', 

603 5: 'STIXSizeFiveSym', 

604 }) 

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

606 fullpath = findfont(name) 

607 self.fontmap[key] = fullpath 

608 self.fontmap[name] = fullpath 

609 

610 def _get_glyph(self, fontname, font_class, sym): 

611 # Override prime symbol to use Bakoma. 

612 if sym == r'\prime': 

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

614 else: 

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

616 uniindex = get_unicode_index(sym) 

617 font = self._get_font('ex') 

618 if font is not None: 

619 glyphindex = font.get_char_index(uniindex) 

620 if glyphindex != 0: 

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

622 # otherwise return regular glyph 

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

624 

625 

626class DejaVuSerifFonts(DejaVuFonts): 

627 """ 

628 A font handling class for the DejaVu Serif fonts 

629 

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

631 """ 

632 _fontmap = { 

633 'rm': 'DejaVu Serif', 

634 'it': 'DejaVu Serif:italic', 

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

636 'sf': 'DejaVu Sans', 

637 'tt': 'DejaVu Sans Mono', 

638 'ex': 'DejaVu Serif Display', 

639 0: 'DejaVu Serif', 

640 } 

641 

642 

643class DejaVuSansFonts(DejaVuFonts): 

644 """ 

645 A font handling class for the DejaVu Sans fonts 

646 

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

648 """ 

649 _fontmap = { 

650 'rm': 'DejaVu Sans', 

651 'it': 'DejaVu Sans:italic', 

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

653 'sf': 'DejaVu Sans', 

654 'tt': 'DejaVu Sans Mono', 

655 'ex': 'DejaVu Sans Display', 

656 0: 'DejaVu Sans', 

657 } 

658 

659 

660class StixFonts(UnicodeFonts): 

661 """ 

662 A font handling class for the STIX fonts. 

663 

664 In addition to what UnicodeFonts provides, this class: 

665 

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

667 character sets with different font styles at special Unicode 

668 code points, such as "Blackboard". 

669 

670 - handles sized alternative characters for the STIXSizeX fonts. 

671 """ 

672 _fontmap = { 

673 'rm': 'STIXGeneral', 

674 'it': 'STIXGeneral:italic', 

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

676 'nonunirm': 'STIXNonUnicode', 

677 'nonuniit': 'STIXNonUnicode:italic', 

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

679 0: 'STIXGeneral', 

680 1: 'STIXSizeOneSym', 

681 2: 'STIXSizeTwoSym', 

682 3: 'STIXSizeThreeSym', 

683 4: 'STIXSizeFourSym', 

684 5: 'STIXSizeFiveSym', 

685 } 

686 _fallback_font = False 

687 _sans = False 

688 

689 def __init__(self, *args, **kwargs): 

690 TruetypeFonts.__init__(self, *args, **kwargs) 

691 self.fontmap = {} 

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

693 fullpath = findfont(name) 

694 self.fontmap[key] = fullpath 

695 self.fontmap[name] = fullpath 

696 

697 def _map_virtual_font(self, fontname, font_class, uniindex): 

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

699 # other fonts. 

700 mapping = stix_virtual_fonts.get(fontname) 

701 if (self._sans and mapping is None 

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

703 mapping = stix_virtual_fonts['sf'] 

704 doing_sans_conversion = True 

705 else: 

706 doing_sans_conversion = False 

707 

708 if mapping is not None: 

709 if isinstance(mapping, dict): 

710 try: 

711 mapping = mapping[font_class] 

712 except KeyError: 

713 mapping = mapping['rm'] 

714 

715 # Binary search for the source glyph 

716 lo = 0 

717 hi = len(mapping) 

718 while lo < hi: 

719 mid = (lo+hi)//2 

720 range = mapping[mid] 

721 if uniindex < range[0]: 

722 hi = mid 

723 elif uniindex <= range[1]: 

724 break 

725 else: 

726 lo = mid + 1 

727 

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

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

730 fontname = range[2] 

731 elif not doing_sans_conversion: 

732 # This will generate a dummy character 

733 uniindex = 0x1 

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

735 

736 # Fix some incorrect glyphs. 

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

738 uniindex = stix_glyph_fixes.get(uniindex, uniindex) 

739 

740 # Handle private use area glyphs 

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

742 fontname = 'nonuni' + fontname 

743 

744 return fontname, uniindex 

745 

746 @functools.lru_cache() 

747 def get_sized_alternatives_for_symbol(self, fontname, sym): 

748 fixes = { 

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

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

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

752 } 

753 sym = fixes.get(sym, sym) 

754 try: 

755 uniindex = get_unicode_index(sym) 

756 except ValueError: 

757 return [(fontname, sym)] 

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

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

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

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

762 if sym == r'\__sqrt__': 

763 alternatives = alternatives[:-1] 

764 return alternatives 

765 

766 

767class StixSansFonts(StixFonts): 

768 """ 

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

770 characters by default). 

771 """ 

772 _sans = True 

773 

774 

775############################################################################## 

776# TeX-LIKE BOX MODEL 

777 

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

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

780# form: 

781# 

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

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

784# 

785# The most relevant "chapters" are: 

786# Data structures for boxes and their friends 

787# Shipping pages out (ship()) 

788# Packaging (hpack() and vpack()) 

789# Data structures for math mode 

790# Subroutines for math mode 

791# Typesetting math formulas 

792# 

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

794# book, e.g., node123 

795# 

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

797# matplotlib. 

798 

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

800SHRINK_FACTOR = 0.7 

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

802# get any smaller 

803NUM_SIZE_LEVELS = 6 

804 

805 

806class FontConstantsBase: 

807 """ 

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

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

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

811 """ 

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

813 script_space = 0.05 

814 

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

816 subdrop = 0.4 

817 

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

819 sup1 = 0.7 

820 

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

822 sub1 = 0.3 

823 

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

825 # superscript is present 

826 sub2 = 0.5 

827 

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

829 # nucleus edge for non-slanted nuclei 

830 delta = 0.025 

831 

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

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

834 # for slanted nuclei 

835 delta_slanted = 0.2 

836 

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

838 # integrals 

839 delta_integral = 0.1 

840 

841 

842class ComputerModernFontConstants(FontConstantsBase): 

843 script_space = 0.075 

844 subdrop = 0.2 

845 sup1 = 0.45 

846 sub1 = 0.2 

847 sub2 = 0.3 

848 delta = 0.075 

849 delta_slanted = 0.3 

850 delta_integral = 0.3 

851 

852 

853class STIXFontConstants(FontConstantsBase): 

854 script_space = 0.1 

855 sup1 = 0.8 

856 sub2 = 0.6 

857 delta = 0.05 

858 delta_slanted = 0.3 

859 delta_integral = 0.3 

860 

861 

862class STIXSansFontConstants(FontConstantsBase): 

863 script_space = 0.05 

864 sup1 = 0.8 

865 delta_slanted = 0.6 

866 delta_integral = 0.3 

867 

868 

869class DejaVuSerifFontConstants(FontConstantsBase): 

870 pass 

871 

872 

873class DejaVuSansFontConstants(FontConstantsBase): 

874 pass 

875 

876 

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

878_font_constant_mapping = { 

879 'DejaVu Sans': DejaVuSansFontConstants, 

880 'DejaVu Sans Mono': DejaVuSansFontConstants, 

881 'DejaVu Serif': DejaVuSerifFontConstants, 

882 'cmb10': ComputerModernFontConstants, 

883 'cmex10': ComputerModernFontConstants, 

884 'cmmi10': ComputerModernFontConstants, 

885 'cmr10': ComputerModernFontConstants, 

886 'cmss10': ComputerModernFontConstants, 

887 'cmsy10': ComputerModernFontConstants, 

888 'cmtt10': ComputerModernFontConstants, 

889 'STIXGeneral': STIXFontConstants, 

890 'STIXNonUnicode': STIXFontConstants, 

891 'STIXSizeFiveSym': STIXFontConstants, 

892 'STIXSizeFourSym': STIXFontConstants, 

893 'STIXSizeThreeSym': STIXFontConstants, 

894 'STIXSizeTwoSym': STIXFontConstants, 

895 'STIXSizeOneSym': STIXFontConstants, 

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

897 'Bitstream Vera Sans': DejaVuSansFontConstants, 

898 'Bitstream Vera': DejaVuSansFontConstants, 

899 } 

900 

901 

902def _get_font_constant_set(state): 

903 constants = _font_constant_mapping.get( 

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

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

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

907 if (constants is STIXFontConstants and 

908 isinstance(state.fontset, StixSansFonts)): 

909 return STIXSansFontConstants 

910 return constants 

911 

912 

913class Node: 

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

915 

916 def __init__(self): 

917 self.size = 0 

918 

919 def __repr__(self): 

920 return type(self).__name__ 

921 

922 def get_kerning(self, next): 

923 return 0.0 

924 

925 def shrink(self): 

926 """ 

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

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

929 """ 

930 self.size += 1 

931 

932 def render(self, output, x, y): 

933 """Render this node.""" 

934 

935 

936class Box(Node): 

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

938 

939 def __init__(self, width, height, depth): 

940 super().__init__() 

941 self.width = width 

942 self.height = height 

943 self.depth = depth 

944 

945 def shrink(self): 

946 super().shrink() 

947 if self.size < NUM_SIZE_LEVELS: 

948 self.width *= SHRINK_FACTOR 

949 self.height *= SHRINK_FACTOR 

950 self.depth *= SHRINK_FACTOR 

951 

952 def render(self, output, x1, y1, x2, y2): 

953 pass 

954 

955 

956class Vbox(Box): 

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

958 

959 def __init__(self, height, depth): 

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

961 

962 

963class Hbox(Box): 

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

965 

966 def __init__(self, width): 

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

968 

969 

970class Char(Node): 

971 """ 

972 A single character. 

973 

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

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

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

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

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

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

980 `Hlist`. 

981 """ 

982 

983 def __init__(self, c, state): 

984 super().__init__() 

985 self.c = c 

986 self.fontset = state.fontset 

987 self.font = state.font 

988 self.font_class = state.font_class 

989 self.fontsize = state.fontsize 

990 self.dpi = state.dpi 

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

992 # pack phase, after we know the real fontsize 

993 self._update_metrics() 

994 

995 def __repr__(self): 

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

997 

998 def _update_metrics(self): 

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

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

1001 if self.c == ' ': 

1002 self.width = metrics.advance 

1003 else: 

1004 self.width = metrics.width 

1005 self.height = metrics.iceberg 

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

1007 

1008 def is_slanted(self): 

1009 return self._metrics.slanted 

1010 

1011 def get_kerning(self, next): 

1012 """ 

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

1014 

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

1016 to create `Kern` nodes. 

1017 """ 

1018 advance = self._metrics.advance - self.width 

1019 kern = 0. 

1020 if isinstance(next, Char): 

1021 kern = self.fontset.get_kern( 

1022 self.font, self.font_class, self.c, self.fontsize, 

1023 next.font, next.font_class, next.c, next.fontsize, 

1024 self.dpi) 

1025 return advance + kern 

1026 

1027 def render(self, output, x, y): 

1028 self.fontset.render_glyph( 

1029 output, x, y, 

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

1031 

1032 def shrink(self): 

1033 super().shrink() 

1034 if self.size < NUM_SIZE_LEVELS: 

1035 self.fontsize *= SHRINK_FACTOR 

1036 self.width *= SHRINK_FACTOR 

1037 self.height *= SHRINK_FACTOR 

1038 self.depth *= SHRINK_FACTOR 

1039 

1040 

1041class Accent(Char): 

1042 """ 

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

1044 since they are already offset correctly from the baseline in 

1045 TrueType fonts. 

1046 """ 

1047 def _update_metrics(self): 

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

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

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

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

1052 self.depth = 0 

1053 

1054 def shrink(self): 

1055 super().shrink() 

1056 self._update_metrics() 

1057 

1058 def render(self, output, x, y): 

1059 self.fontset.render_glyph( 

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

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

1062 

1063 

1064class List(Box): 

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

1066 

1067 def __init__(self, elements): 

1068 super().__init__(0., 0., 0.) 

1069 self.shift_amount = 0. # An arbitrary offset 

1070 self.children = elements # The child nodes of this list 

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

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

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

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

1075 

1076 def __repr__(self): 

1077 return '%s<w=%.02f h=%.02f d=%.02f s=%.02f>[%s]' % ( 

1078 super().__repr__(), 

1079 self.width, self.height, 

1080 self.depth, self.shift_amount, 

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

1082 

1083 def _set_glue(self, x, sign, totals, error_type): 

1084 self.glue_order = o = next( 

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

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

1087 self.glue_sign = sign 

1088 if totals[o] != 0.: 

1089 self.glue_set = x / totals[o] 

1090 else: 

1091 self.glue_sign = 0 

1092 self.glue_ratio = 0. 

1093 if o == 0: 

1094 if len(self.children): 

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

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

1097 

1098 def shrink(self): 

1099 for child in self.children: 

1100 child.shrink() 

1101 super().shrink() 

1102 if self.size < NUM_SIZE_LEVELS: 

1103 self.shift_amount *= SHRINK_FACTOR 

1104 self.glue_set *= SHRINK_FACTOR 

1105 

1106 

1107class Hlist(List): 

1108 """A horizontal list of boxes.""" 

1109 

1110 def __init__(self, elements, w=0., m='additional', do_kern=True): 

1111 super().__init__(elements) 

1112 if do_kern: 

1113 self.kern() 

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

1115 

1116 def kern(self): 

1117 """ 

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

1119 

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

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

1122 linked list. 

1123 """ 

1124 new_children = [] 

1125 num_children = len(self.children) 

1126 if num_children: 

1127 for i in range(num_children): 

1128 elem = self.children[i] 

1129 if i < num_children - 1: 

1130 next = self.children[i + 1] 

1131 else: 

1132 next = None 

1133 

1134 new_children.append(elem) 

1135 kerning_distance = elem.get_kerning(next) 

1136 if kerning_distance != 0.: 

1137 kern = Kern(kerning_distance) 

1138 new_children.append(kern) 

1139 self.children = new_children 

1140 

1141 # This is a failed experiment to fake cross-font kerning. 

1142# def get_kerning(self, next): 

1143# if len(self.children) >= 2 and isinstance(self.children[-2], Char): 

1144# if isinstance(next, Char): 

1145# print "CASE A" 

1146# return self.children[-2].get_kerning(next) 

1147# elif (isinstance(next, Hlist) and len(next.children) 

1148# and isinstance(next.children[0], Char)): 

1149# print "CASE B" 

1150# result = self.children[-2].get_kerning(next.children[0]) 

1151# print result 

1152# return result 

1153# return 0.0 

1154 

1155 def hpack(self, w=0., m='additional'): 

1156 r""" 

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

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

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

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

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

1162 

1163 Parameters 

1164 ---------- 

1165 w : float, default: 0 

1166 A width. 

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

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

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

1170 

1171 Notes 

1172 ----- 

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

1174 """ 

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

1176 # much useless if we do. 

1177 # self.shift_amount = 0. 

1178 h = 0. 

1179 d = 0. 

1180 x = 0. 

1181 total_stretch = [0.] * 4 

1182 total_shrink = [0.] * 4 

1183 for p in self.children: 

1184 if isinstance(p, Char): 

1185 x += p.width 

1186 h = max(h, p.height) 

1187 d = max(d, p.depth) 

1188 elif isinstance(p, Box): 

1189 x += p.width 

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

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

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

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

1194 elif isinstance(p, Glue): 

1195 glue_spec = p.glue_spec 

1196 x += glue_spec.width 

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

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

1199 elif isinstance(p, Kern): 

1200 x += p.width 

1201 self.height = h 

1202 self.depth = d 

1203 

1204 if m == 'additional': 

1205 w += x 

1206 self.width = w 

1207 x = w - x 

1208 

1209 if x == 0.: 

1210 self.glue_sign = 0 

1211 self.glue_order = 0 

1212 self.glue_ratio = 0. 

1213 return 

1214 if x > 0.: 

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

1216 else: 

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

1218 

1219 

1220class Vlist(List): 

1221 """A vertical list of boxes.""" 

1222 

1223 def __init__(self, elements, h=0., m='additional'): 

1224 super().__init__(elements) 

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

1226 

1227 def vpack(self, h=0., m='additional', l=np.inf): 

1228 """ 

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

1230 if one of those dimensions is pre-specified. 

1231 

1232 Parameters 

1233 ---------- 

1234 h : float, default: 0 

1235 A height. 

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

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

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

1239 l : float, default: np.inf 

1240 The maximum height. 

1241 

1242 Notes 

1243 ----- 

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

1245 """ 

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

1247 # much useless if we do. 

1248 # self.shift_amount = 0. 

1249 w = 0. 

1250 d = 0. 

1251 x = 0. 

1252 total_stretch = [0.] * 4 

1253 total_shrink = [0.] * 4 

1254 for p in self.children: 

1255 if isinstance(p, Box): 

1256 x += d + p.height 

1257 d = p.depth 

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

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

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

1261 elif isinstance(p, Glue): 

1262 x += d 

1263 d = 0. 

1264 glue_spec = p.glue_spec 

1265 x += glue_spec.width 

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

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

1268 elif isinstance(p, Kern): 

1269 x += d + p.width 

1270 d = 0. 

1271 elif isinstance(p, Char): 

1272 raise RuntimeError( 

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

1274 

1275 self.width = w 

1276 if d > l: 

1277 x += d - l 

1278 self.depth = l 

1279 else: 

1280 self.depth = d 

1281 

1282 if m == 'additional': 

1283 h += x 

1284 self.height = h 

1285 x = h - x 

1286 

1287 if x == 0: 

1288 self.glue_sign = 0 

1289 self.glue_order = 0 

1290 self.glue_ratio = 0. 

1291 return 

1292 

1293 if x > 0.: 

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

1295 else: 

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

1297 

1298 

1299class Rule(Box): 

1300 """ 

1301 A solid black rectangle. 

1302 

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

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

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

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

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

1308 """ 

1309 

1310 def __init__(self, width, height, depth, state): 

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

1312 self.fontset = state.fontset 

1313 

1314 def render(self, output, x, y, w, h): 

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

1316 

1317 

1318class Hrule(Rule): 

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

1320 

1321 def __init__(self, state, thickness=None): 

1322 if thickness is None: 

1323 thickness = state.get_current_underline_thickness() 

1324 height = depth = thickness * 0.5 

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

1326 

1327 

1328class Vrule(Rule): 

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

1330 

1331 def __init__(self, state): 

1332 thickness = state.get_current_underline_thickness() 

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

1334 

1335 

1336_GlueSpec = namedtuple( 

1337 "_GlueSpec", "width stretch stretch_order shrink shrink_order") 

1338_GlueSpec._named = { 

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

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

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

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

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

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

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

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

1347} 

1348 

1349 

1350class Glue(Node): 

1351 """ 

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

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

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

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

1356 """ 

1357 

1358 def __init__(self, glue_type): 

1359 super().__init__() 

1360 if isinstance(glue_type, str): 

1361 glue_spec = _GlueSpec._named[glue_type] 

1362 elif isinstance(glue_type, _GlueSpec): 

1363 glue_spec = glue_type 

1364 else: 

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

1366 self.glue_spec = glue_spec 

1367 

1368 def shrink(self): 

1369 super().shrink() 

1370 if self.size < NUM_SIZE_LEVELS: 

1371 g = self.glue_spec 

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

1373 

1374 

1375class HCentered(Hlist): 

1376 """ 

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

1378 centered within its enclosing box. 

1379 """ 

1380 

1381 def __init__(self, elements): 

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

1383 

1384 

1385class VCentered(Vlist): 

1386 """ 

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

1388 centered within its enclosing box. 

1389 """ 

1390 

1391 def __init__(self, elements): 

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

1393 

1394 

1395class Kern(Node): 

1396 """ 

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

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

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

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

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

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

1403 direction. 

1404 """ 

1405 

1406 height = 0 

1407 depth = 0 

1408 

1409 def __init__(self, width): 

1410 super().__init__() 

1411 self.width = width 

1412 

1413 def __repr__(self): 

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

1415 

1416 def shrink(self): 

1417 super().shrink() 

1418 if self.size < NUM_SIZE_LEVELS: 

1419 self.width *= SHRINK_FACTOR 

1420 

1421 

1422class AutoHeightChar(Hlist): 

1423 """ 

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

1425 

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

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

1428 always just return a scaled version of the glyph. 

1429 """ 

1430 

1431 def __init__(self, c, height, depth, state, always=False, factor=None): 

1432 alternatives = state.fontset.get_sized_alternatives_for_symbol( 

1433 state.font, c) 

1434 

1435 xHeight = state.fontset.get_xheight( 

1436 state.font, state.fontsize, state.dpi) 

1437 

1438 state = state.copy() 

1439 target_total = height + depth 

1440 for fontname, sym in alternatives: 

1441 state.font = fontname 

1442 char = Char(sym, state) 

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

1444 # with descender glyphs by subtracting 0.2 * xHeight 

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

1446 break 

1447 

1448 shift = 0 

1449 if state.font != 0: 

1450 if factor is None: 

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

1452 state.fontsize *= factor 

1453 char = Char(sym, state) 

1454 

1455 shift = (depth - char.depth) 

1456 

1457 super().__init__([char]) 

1458 self.shift_amount = shift 

1459 

1460 

1461class AutoWidthChar(Hlist): 

1462 """ 

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

1464 

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

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

1467 always just return a scaled version of the glyph. 

1468 """ 

1469 

1470 def __init__(self, c, width, state, always=False, char_class=Char): 

1471 alternatives = state.fontset.get_sized_alternatives_for_symbol( 

1472 state.font, c) 

1473 

1474 state = state.copy() 

1475 for fontname, sym in alternatives: 

1476 state.font = fontname 

1477 char = char_class(sym, state) 

1478 if char.width >= width: 

1479 break 

1480 

1481 factor = width / char.width 

1482 state.fontsize *= factor 

1483 char = char_class(sym, state) 

1484 

1485 super().__init__([char]) 

1486 self.width = char.width 

1487 

1488 

1489def ship(box, xy=(0, 0)): 

1490 """ 

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

1492 

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

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

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

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

1497 processes have become local variables here. 

1498 """ 

1499 ox, oy = xy 

1500 cur_v = 0. 

1501 cur_h = 0. 

1502 off_h = ox 

1503 off_v = oy + box.height 

1504 output = Output(box) 

1505 

1506 def clamp(value): 

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

1508 

1509 def hlist_out(box): 

1510 nonlocal cur_v, cur_h, off_h, off_v 

1511 

1512 cur_g = 0 

1513 cur_glue = 0. 

1514 glue_order = box.glue_order 

1515 glue_sign = box.glue_sign 

1516 base_line = cur_v 

1517 left_edge = cur_h 

1518 

1519 for p in box.children: 

1520 if isinstance(p, Char): 

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

1522 cur_h += p.width 

1523 elif isinstance(p, Kern): 

1524 cur_h += p.width 

1525 elif isinstance(p, List): 

1526 # node623 

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

1528 cur_h += p.width 

1529 else: 

1530 edge = cur_h 

1531 cur_v = base_line + p.shift_amount 

1532 if isinstance(p, Hlist): 

1533 hlist_out(p) 

1534 else: 

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

1536 vlist_out(p) 

1537 cur_h = edge + p.width 

1538 cur_v = base_line 

1539 elif isinstance(p, Box): 

1540 # node624 

1541 rule_height = p.height 

1542 rule_depth = p.depth 

1543 rule_width = p.width 

1544 if np.isinf(rule_height): 

1545 rule_height = box.height 

1546 if np.isinf(rule_depth): 

1547 rule_depth = box.depth 

1548 if rule_height > 0 and rule_width > 0: 

1549 cur_v = base_line + rule_depth 

1550 p.render(output, 

1551 cur_h + off_h, cur_v + off_v, 

1552 rule_width, rule_height) 

1553 cur_v = base_line 

1554 cur_h += rule_width 

1555 elif isinstance(p, Glue): 

1556 # node625 

1557 glue_spec = p.glue_spec 

1558 rule_width = glue_spec.width - cur_g 

1559 if glue_sign != 0: # normal 

1560 if glue_sign == 1: # stretching 

1561 if glue_spec.stretch_order == glue_order: 

1562 cur_glue += glue_spec.stretch 

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

1564 elif glue_spec.shrink_order == glue_order: 

1565 cur_glue += glue_spec.shrink 

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

1567 rule_width += cur_g 

1568 cur_h += rule_width 

1569 

1570 def vlist_out(box): 

1571 nonlocal cur_v, cur_h, off_h, off_v 

1572 

1573 cur_g = 0 

1574 cur_glue = 0. 

1575 glue_order = box.glue_order 

1576 glue_sign = box.glue_sign 

1577 left_edge = cur_h 

1578 cur_v -= box.height 

1579 top_edge = cur_v 

1580 

1581 for p in box.children: 

1582 if isinstance(p, Kern): 

1583 cur_v += p.width 

1584 elif isinstance(p, List): 

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

1586 cur_v += p.height + p.depth 

1587 else: 

1588 cur_v += p.height 

1589 cur_h = left_edge + p.shift_amount 

1590 save_v = cur_v 

1591 p.width = box.width 

1592 if isinstance(p, Hlist): 

1593 hlist_out(p) 

1594 else: 

1595 vlist_out(p) 

1596 cur_v = save_v + p.depth 

1597 cur_h = left_edge 

1598 elif isinstance(p, Box): 

1599 rule_height = p.height 

1600 rule_depth = p.depth 

1601 rule_width = p.width 

1602 if np.isinf(rule_width): 

1603 rule_width = box.width 

1604 rule_height += rule_depth 

1605 if rule_height > 0 and rule_depth > 0: 

1606 cur_v += rule_height 

1607 p.render(output, 

1608 cur_h + off_h, cur_v + off_v, 

1609 rule_width, rule_height) 

1610 elif isinstance(p, Glue): 

1611 glue_spec = p.glue_spec 

1612 rule_height = glue_spec.width - cur_g 

1613 if glue_sign != 0: # normal 

1614 if glue_sign == 1: # stretching 

1615 if glue_spec.stretch_order == glue_order: 

1616 cur_glue += glue_spec.stretch 

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

1618 elif glue_spec.shrink_order == glue_order: # shrinking 

1619 cur_glue += glue_spec.shrink 

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

1621 rule_height += cur_g 

1622 cur_v += rule_height 

1623 elif isinstance(p, Char): 

1624 raise RuntimeError( 

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

1626 

1627 hlist_out(box) 

1628 return output 

1629 

1630 

1631############################################################################## 

1632# PARSER 

1633 

1634 

1635def Error(msg): 

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

1637 def raise_error(s, loc, toks): 

1638 raise ParseFatalException(s, loc, msg) 

1639 

1640 return Empty().setParseAction(raise_error) 

1641 

1642 

1643class ParserState: 

1644 """ 

1645 Parser state. 

1646 

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

1648 state is always at the top of the stack. 

1649 

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

1651 and popped accordingly. 

1652 """ 

1653 

1654 def __init__(self, fontset, font, font_class, fontsize, dpi): 

1655 self.fontset = fontset 

1656 self._font = font 

1657 self.font_class = font_class 

1658 self.fontsize = fontsize 

1659 self.dpi = dpi 

1660 

1661 def copy(self): 

1662 return copy.copy(self) 

1663 

1664 @property 

1665 def font(self): 

1666 return self._font 

1667 

1668 @font.setter 

1669 def font(self, name): 

1670 if name in ('rm', 'it', 'bf'): 

1671 self.font_class = name 

1672 self._font = name 

1673 

1674 def get_current_underline_thickness(self): 

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

1676 return self.fontset.get_underline_thickness( 

1677 self.font, self.fontsize, self.dpi) 

1678 

1679 

1680def cmd(expr, args): 

1681 r""" 

1682 Helper to define TeX commands. 

1683 

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

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

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

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

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

1689 the error message. 

1690 """ 

1691 

1692 def names(elt): 

1693 if isinstance(elt, ParseExpression): 

1694 for expr in elt.exprs: 

1695 yield from names(expr) 

1696 elif elt.resultsName: 

1697 yield elt.resultsName 

1698 

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

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

1701 if expr == csname else expr) 

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

1703 

1704 

1705class Parser: 

1706 """ 

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

1708 

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

1710 

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

1712 """ 

1713 

1714 class _MathStyle(enum.Enum): 

1715 DISPLAYSTYLE = 0 

1716 TEXTSTYLE = 1 

1717 SCRIPTSTYLE = 2 

1718 SCRIPTSCRIPTSTYLE = 3 

1719 

1720 _binary_operators = set( 

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

1722 r''' 

1723 \pm \sqcap \rhd 

1724 \mp \sqcup \unlhd 

1725 \times \vee \unrhd 

1726 \div \wedge \oplus 

1727 \ast \setminus \ominus 

1728 \star \wr \otimes 

1729 \circ \diamond \oslash 

1730 \bullet \bigtriangleup \odot 

1731 \cdot \bigtriangledown \bigcirc 

1732 \cap \triangleleft \dagger 

1733 \cup \triangleright \ddagger 

1734 \uplus \lhd \amalg 

1735 \dotplus \dotminus'''.split()) 

1736 

1737 _relation_symbols = set(r''' 

1738 = < > : 

1739 \leq \geq \equiv \models 

1740 \prec \succ \sim \perp 

1741 \preceq \succeq \simeq \mid 

1742 \ll \gg \asymp \parallel 

1743 \subset \supset \approx \bowtie 

1744 \subseteq \supseteq \cong \Join 

1745 \sqsubset \sqsupset \neq \smile 

1746 \sqsubseteq \sqsupseteq \doteq \frown 

1747 \in \ni \propto \vdash 

1748 \dashv \dots \doteqdot'''.split()) 

1749 

1750 _arrow_symbols = set(r''' 

1751 \leftarrow \longleftarrow \uparrow 

1752 \Leftarrow \Longleftarrow \Uparrow 

1753 \rightarrow \longrightarrow \downarrow 

1754 \Rightarrow \Longrightarrow \Downarrow 

1755 \leftrightarrow \longleftrightarrow \updownarrow 

1756 \Leftrightarrow \Longleftrightarrow \Updownarrow 

1757 \mapsto \longmapsto \nearrow 

1758 \hookleftarrow \hookrightarrow \searrow 

1759 \leftharpoonup \rightharpoonup \swarrow 

1760 \leftharpoondown \rightharpoondown \nwarrow 

1761 \rightleftharpoons \leadsto'''.split()) 

1762 

1763 _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols 

1764 

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

1766 

1767 _overunder_symbols = set(r''' 

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

1769 \bigwedge \bigodot \bigotimes \bigoplus \biguplus 

1770 '''.split()) 

1771 

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

1773 

1774 _dropsub_symbols = set(r'''\int \oint'''.split()) 

1775 

1776 _fontnames = set("rm cal it tt sf bf default bb frak scr regular".split()) 

1777 

1778 _function_names = set(""" 

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

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

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

1782 

1783 _ambi_delims = set(r""" 

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

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

1786 _left_delims = set(r"( [ \{ < \lfloor \langle \lceil".split()) 

1787 _right_delims = set(r") ] \} > \rfloor \rangle \rceil".split()) 

1788 _delims = _left_delims | _right_delims | _ambi_delims 

1789 

1790 def __init__(self): 

1791 p = types.SimpleNamespace() 

1792 

1793 def set_names_and_parse_actions(): 

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

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

1796 # Set names on everything -- very useful for debugging 

1797 val.setName(key) 

1798 # Set actions 

1799 if hasattr(self, key): 

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

1801 

1802 # Root definitions. 

1803 

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

1805 def csnames(group, names): 

1806 ends_with_alpha = [] 

1807 ends_with_nonalpha = [] 

1808 for name in names: 

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

1810 ends_with_alpha.append(name) 

1811 else: 

1812 ends_with_nonalpha.append(name) 

1813 return Regex(r"\\(?P<{}>(?:{})(?![A-Za-z]){})".format( 

1814 group, 

1815 "|".join(map(re.escape, ends_with_alpha)), 

1816 "".join(f"|{s}" for s in map(re.escape, ends_with_nonalpha)), 

1817 )) 

1818 

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

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

1821 

1822 p.style_literal = oneOf( 

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

1824 

1825 p.symbol = Regex( 

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

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

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

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

1830 )("sym").leaveWhitespace() 

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

1832 

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

1834 p.start_group = ( 

1835 Optional(r"\math" + oneOf(self._fontnames)("font")) + "{") 

1836 p.end_group = Literal("}") 

1837 

1838 p.delim = oneOf(self._delims) 

1839 

1840 set_names_and_parse_actions() # for root definitions. 

1841 

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

1843 # elements is important for speed.) 

1844 p.accent = Forward() 

1845 p.auto_delim = Forward() 

1846 p.binom = Forward() 

1847 p.customspace = Forward() 

1848 p.frac = Forward() 

1849 p.dfrac = Forward() 

1850 p.function = Forward() 

1851 p.genfrac = Forward() 

1852 p.group = Forward() 

1853 p.operatorname = Forward() 

1854 p.overline = Forward() 

1855 p.overset = Forward() 

1856 p.placeable = Forward() 

1857 p.required_group = Forward() 

1858 p.simple = Forward() 

1859 p.optional_group = Forward() 

1860 p.sqrt = Forward() 

1861 p.subsuper = Forward() 

1862 p.token = Forward() 

1863 p.underset = Forward() 

1864 

1865 set_names_and_parse_actions() # for mutually recursive definitions. 

1866 

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

1868 

1869 p.accent <<= ( 

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

1871 - p.placeable("sym")) 

1872 

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

1874 p.operatorname <<= cmd( 

1875 r"\operatorname", 

1876 "{" + ZeroOrMore(p.simple | p.unknown_symbol)("name") + "}") 

1877 

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

1879 

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

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

1882 

1883 p.frac <<= cmd( 

1884 r"\frac", p.required_group("num") + p.required_group("den")) 

1885 p.dfrac <<= cmd( 

1886 r"\dfrac", p.required_group("num") + p.required_group("den")) 

1887 p.binom <<= cmd( 

1888 r"\binom", p.required_group("num") + p.required_group("den")) 

1889 

1890 p.genfrac <<= cmd( 

1891 r"\genfrac", 

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

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

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

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

1896 + p.required_group("num") 

1897 + p.required_group("den")) 

1898 

1899 p.sqrt <<= cmd( 

1900 r"\sqrt{value}", 

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

1902 + p.required_group("value")) 

1903 

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

1905 

1906 p.overset <<= cmd( 

1907 r"\overset", 

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

1909 p.underset <<= cmd( 

1910 r"\underset", 

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

1912 

1913 p.placeable <<= ( 

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

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

1916 # chars not in a group 

1917 | p.function 

1918 | p.operatorname 

1919 | p.group 

1920 | p.frac 

1921 | p.dfrac 

1922 | p.binom 

1923 | p.genfrac 

1924 | p.overset 

1925 | p.underset 

1926 | p.sqrt 

1927 | p.overline 

1928 ) 

1929 

1930 p.simple <<= ( 

1931 p.space 

1932 | p.customspace 

1933 | p.font 

1934 | p.subsuper 

1935 ) 

1936 

1937 p.subsuper <<= ( 

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

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

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

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

1942 | (p.placeable("nucleus") + Regex("'*")("apostrophes")) 

1943 ) 

1944 

1945 p.token <<= ( 

1946 p.simple 

1947 | p.auto_delim 

1948 | p.unknown_symbol # Must be last 

1949 ) 

1950 

1951 p.auto_delim <<= ( 

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

1953 + ZeroOrMore(p.simple | p.auto_delim)("mid") 

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

1955 ) 

1956 

1957 # Leaf definitions. 

1958 p.math = OneOrMore(p.token) 

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

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

1961 p.main = ( 

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

1963 ) 

1964 set_names_and_parse_actions() # for leaf definitions. 

1965 

1966 self._expression = p.main 

1967 self._math_expression = p.math 

1968 

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

1970 self._in_subscript_or_superscript = False 

1971 

1972 def parse(self, s, fonts_object, fontsize, dpi): 

1973 """ 

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

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

1976 

1977 Returns the parse tree of `Node` instances. 

1978 """ 

1979 self._state_stack = [ 

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

1981 self._em_width_cache = {} 

1982 try: 

1983 result = self._expression.parseString(s) 

1984 except ParseBaseException as err: 

1985 raise ValueError("\n".join(["", 

1986 err.line, 

1987 " " * (err.column - 1) + "^", 

1988 str(err)])) from err 

1989 self._state_stack = None 

1990 self._in_subscript_or_superscript = False 

1991 # prevent operator spacing from leaking into a new expression 

1992 self._em_width_cache = {} 

1993 self._expression.resetCache() 

1994 return result[0] 

1995 

1996 def get_state(self): 

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

1998 return self._state_stack[-1] 

1999 

2000 def pop_state(self): 

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

2002 self._state_stack.pop() 

2003 

2004 def push_state(self): 

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

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

2007 

2008 def main(self, s, loc, toks): 

2009 return [Hlist(toks)] 

2010 

2011 def math_string(self, s, loc, toks): 

2012 return self._math_expression.parseString(toks[0][1:-1]) 

2013 

2014 def math(self, s, loc, toks): 

2015 hlist = Hlist(toks) 

2016 self.pop_state() 

2017 return [hlist] 

2018 

2019 def non_math(self, s, loc, toks): 

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

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

2022 hlist = Hlist(symbols) 

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

2024 self.push_state() 

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

2026 return [hlist] 

2027 

2028 float_literal = staticmethod(pyparsing_common.convertToFloat) 

2029 

2030 def _make_space(self, percentage): 

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

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

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

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

2035 # current font style. 

2036 state = self.get_state() 

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

2038 width = self._em_width_cache.get(key) 

2039 if width is None: 

2040 metrics = state.fontset.get_metrics( 

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

2042 state.fontsize, state.dpi) 

2043 width = metrics.advance 

2044 self._em_width_cache[key] = width 

2045 return Kern(width * percentage) 

2046 

2047 _space_widths = { 

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

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

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

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

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

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

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

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

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

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

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

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

2060 } 

2061 

2062 def space(self, s, loc, toks): 

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

2064 box = self._make_space(num) 

2065 return [box] 

2066 

2067 def customspace(self, s, loc, toks): 

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

2069 

2070 def symbol(self, s, loc, toks): 

2071 c = toks["sym"] 

2072 if c == "-": 

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

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

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

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

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

2078 c = "\N{MINUS SIGN}" 

2079 try: 

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

2081 except ValueError as err: 

2082 raise ParseFatalException(s, loc, 

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

2084 

2085 if c in self._spaced_symbols: 

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

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

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

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

2090 if (c in self._binary_operators and 

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

2092 prev_char in self._left_delims)): 

2093 return [char] 

2094 else: 

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

2096 char, 

2097 self._make_space(0.2)], 

2098 do_kern=True)] 

2099 elif c in self._punctuation_symbols: 

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

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

2102 

2103 # Do not space commas between brackets 

2104 if c == ',': 

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

2106 return [char] 

2107 

2108 # Do not space dots as decimal separators 

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

2110 return [char] 

2111 else: 

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

2113 return [char] 

2114 

2115 def unknown_symbol(self, s, loc, toks): 

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

2117 

2118 _accent_map = { 

2119 r'hat': r'\circumflexaccent', 

2120 r'breve': r'\combiningbreve', 

2121 r'bar': r'\combiningoverline', 

2122 r'grave': r'\combininggraveaccent', 

2123 r'acute': r'\combiningacuteaccent', 

2124 r'tilde': r'\combiningtilde', 

2125 r'dot': r'\combiningdotabove', 

2126 r'ddot': r'\combiningdiaeresis', 

2127 r'dddot': r'\combiningthreedotsabove', 

2128 r'ddddot': r'\combiningfourdotsabove', 

2129 r'vec': r'\combiningrightarrowabove', 

2130 r'"': r'\combiningdiaeresis', 

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

2132 r"'": r'\combiningacuteaccent', 

2133 r'~': r'\combiningtilde', 

2134 r'.': r'\combiningdotabove', 

2135 r'^': r'\circumflexaccent', 

2136 r'overrightarrow': r'\rightarrow', 

2137 r'overleftarrow': r'\leftarrow', 

2138 r'mathring': r'\circ', 

2139 } 

2140 

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

2142 

2143 def accent(self, s, loc, toks): 

2144 state = self.get_state() 

2145 thickness = state.get_current_underline_thickness() 

2146 accent = toks["accent"] 

2147 sym = toks["sym"] 

2148 if accent in self._wide_accents: 

2149 accent_box = AutoWidthChar( 

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

2151 else: 

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

2153 if accent == 'mathring': 

2154 accent_box.shrink() 

2155 accent_box.shrink() 

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

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

2158 return Vlist([ 

2159 centered, 

2160 Vbox(0., thickness * 2.0), 

2161 Hlist([sym]) 

2162 ]) 

2163 

2164 def function(self, s, loc, toks): 

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

2166 hlist.function_name = toks["name"] 

2167 return hlist 

2168 

2169 def operatorname(self, s, loc, toks): 

2170 self.push_state() 

2171 state = self.get_state() 

2172 state.font = 'rm' 

2173 hlist_list = [] 

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

2175 name = toks["name"] 

2176 for c in name: 

2177 if isinstance(c, Char): 

2178 c.font = 'rm' 

2179 c._update_metrics() 

2180 hlist_list.append(c) 

2181 elif isinstance(c, str): 

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

2183 else: 

2184 hlist_list.append(c) 

2185 next_char_loc = loc + len(name) + 1 

2186 if isinstance(name, ParseResults): 

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

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

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

2190 if (next_char not in delimiters and 

2191 name not in self._overunder_functions): 

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

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

2194 self.pop_state() 

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

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

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

2198 self._in_subscript_or_superscript = True 

2199 else: 

2200 self._in_subscript_or_superscript = False 

2201 

2202 return Hlist(hlist_list) 

2203 

2204 def start_group(self, s, loc, toks): 

2205 self.push_state() 

2206 # Deal with LaTeX-style font tokens 

2207 if toks.get("font"): 

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

2209 return [] 

2210 

2211 def group(self, s, loc, toks): 

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

2213 return [grp] 

2214 

2215 def required_group(self, s, loc, toks): 

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

2217 

2218 optional_group = required_group 

2219 

2220 def end_group(self, s, loc, toks): 

2221 self.pop_state() 

2222 return [] 

2223 

2224 def font(self, s, loc, toks): 

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

2226 return [] 

2227 

2228 def is_overunder(self, nucleus): 

2229 if isinstance(nucleus, Char): 

2230 return nucleus.c in self._overunder_symbols 

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

2232 return nucleus.function_name in self._overunder_functions 

2233 return False 

2234 

2235 def is_dropsub(self, nucleus): 

2236 if isinstance(nucleus, Char): 

2237 return nucleus.c in self._dropsub_symbols 

2238 return False 

2239 

2240 def is_slanted(self, nucleus): 

2241 if isinstance(nucleus, Char): 

2242 return nucleus.is_slanted() 

2243 return False 

2244 

2245 def is_between_brackets(self, s, loc): 

2246 return False 

2247 

2248 def subsuper(self, s, loc, toks): 

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

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

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

2252 

2253 if not subsuper and not napostrophes: 

2254 return nucleus 

2255 

2256 sub = super = None 

2257 while subsuper: 

2258 op, arg, *subsuper = subsuper 

2259 if op == '_': 

2260 if sub is not None: 

2261 raise ParseFatalException("Double subscript") 

2262 sub = arg 

2263 else: 

2264 if super is not None: 

2265 raise ParseFatalException("Double superscript") 

2266 super = arg 

2267 

2268 state = self.get_state() 

2269 rule_thickness = state.fontset.get_underline_thickness( 

2270 state.font, state.fontsize, state.dpi) 

2271 xHeight = state.fontset.get_xheight( 

2272 state.font, state.fontsize, state.dpi) 

2273 

2274 if napostrophes: 

2275 if super is None: 

2276 super = Hlist([]) 

2277 for i in range(napostrophes): 

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

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

2280 # extending 

2281 super.kern() 

2282 super.hpack() 

2283 

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

2285 if self.is_overunder(nucleus): 

2286 vlist = [] 

2287 shift = 0. 

2288 width = nucleus.width 

2289 if super is not None: 

2290 super.shrink() 

2291 width = max(width, super.width) 

2292 if sub is not None: 

2293 sub.shrink() 

2294 width = max(width, sub.width) 

2295 

2296 vgap = rule_thickness * 3.0 

2297 if super is not None: 

2298 hlist = HCentered([super]) 

2299 hlist.hpack(width, 'exactly') 

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

2301 hlist = HCentered([nucleus]) 

2302 hlist.hpack(width, 'exactly') 

2303 vlist.append(hlist) 

2304 if sub is not None: 

2305 hlist = HCentered([sub]) 

2306 hlist.hpack(width, 'exactly') 

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

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

2309 vlist = Vlist(vlist) 

2310 vlist.shift_amount = shift 

2311 result = Hlist([vlist]) 

2312 return [result] 

2313 

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

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

2316 # them too close together when superscripted) 

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

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

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

2320 # the superscript at the advance 

2321 last_char = nucleus 

2322 if isinstance(nucleus, Hlist): 

2323 new_children = nucleus.children 

2324 if len(new_children): 

2325 # remove last kern 

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

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

2328 new_children = new_children[:-1] 

2329 last_char = new_children[-1] 

2330 if hasattr(last_char, '_metrics'): 

2331 last_char.width = last_char._metrics.advance 

2332 # create new Hlist without kerning 

2333 nucleus = Hlist(new_children, do_kern=False) 

2334 else: 

2335 if isinstance(nucleus, Char): 

2336 last_char.width = last_char._metrics.advance 

2337 nucleus = Hlist([nucleus]) 

2338 

2339 # Handle regular sub/superscripts 

2340 constants = _get_font_constant_set(state) 

2341 lc_height = last_char.height 

2342 lc_baseline = 0 

2343 if self.is_dropsub(last_char): 

2344 lc_baseline = last_char.depth 

2345 

2346 # Compute kerning for sub and super 

2347 superkern = constants.delta * xHeight 

2348 subkern = constants.delta * xHeight 

2349 if self.is_slanted(last_char): 

2350 superkern += constants.delta * xHeight 

2351 superkern += (constants.delta_slanted * 

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

2353 if self.is_dropsub(last_char): 

2354 subkern = (3 * constants.delta - 

2355 constants.delta_integral) * lc_height 

2356 superkern = (3 * constants.delta + 

2357 constants.delta_integral) * lc_height 

2358 else: 

2359 subkern = 0 

2360 

2361 if super is None: 

2362 # node757 

2363 x = Hlist([Kern(subkern), sub]) 

2364 x.shrink() 

2365 if self.is_dropsub(last_char): 

2366 shift_down = lc_baseline + constants.subdrop * xHeight 

2367 else: 

2368 shift_down = constants.sub1 * xHeight 

2369 x.shift_amount = shift_down 

2370 else: 

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

2372 x.shrink() 

2373 if self.is_dropsub(last_char): 

2374 shift_up = lc_height - constants.subdrop * xHeight 

2375 else: 

2376 shift_up = constants.sup1 * xHeight 

2377 if sub is None: 

2378 x.shift_amount = -shift_up 

2379 else: # Both sub and superscript 

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

2381 y.shrink() 

2382 if self.is_dropsub(last_char): 

2383 shift_down = lc_baseline + constants.subdrop * xHeight 

2384 else: 

2385 shift_down = constants.sub2 * xHeight 

2386 # If sub and superscript collide, move super up 

2387 clr = (2.0 * rule_thickness - 

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

2389 if clr > 0.: 

2390 shift_up += clr 

2391 x = Vlist([ 

2392 x, 

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

2394 y]) 

2395 x.shift_amount = shift_down 

2396 

2397 if not self.is_dropsub(last_char): 

2398 x.width += constants.script_space * xHeight 

2399 

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

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

2402 spaced_nucleus = [nucleus, x] 

2403 if self._in_subscript_or_superscript: 

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

2405 self._in_subscript_or_superscript = False 

2406 

2407 result = Hlist(spaced_nucleus) 

2408 return [result] 

2409 

2410 def _genfrac(self, ldelim, rdelim, rule, style, num, den): 

2411 state = self.get_state() 

2412 thickness = state.get_current_underline_thickness() 

2413 

2414 for _ in range(style.value): 

2415 num.shrink() 

2416 den.shrink() 

2417 cnum = HCentered([num]) 

2418 cden = HCentered([den]) 

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

2420 cnum.hpack(width, 'exactly') 

2421 cden.hpack(width, 'exactly') 

2422 vlist = Vlist([cnum, # numerator 

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

2424 Hrule(state, rule), # rule 

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

2426 cden # denominator 

2427 ]) 

2428 

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

2430 # equals sign 

2431 metrics = state.fontset.get_metrics( 

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

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

2434 shift = (cden.height - 

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

2436 thickness * 3.0)) 

2437 vlist.shift_amount = shift 

2438 

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

2440 if ldelim or rdelim: 

2441 if ldelim == '': 

2442 ldelim = '.' 

2443 if rdelim == '': 

2444 rdelim = '.' 

2445 return self._auto_sized_delimiter(ldelim, result, rdelim) 

2446 return result 

2447 

2448 def style_literal(self, s, loc, toks): 

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

2450 

2451 def genfrac(self, s, loc, toks): 

2452 return self._genfrac( 

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

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

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

2456 

2457 def frac(self, s, loc, toks): 

2458 return self._genfrac( 

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

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

2461 

2462 def dfrac(self, s, loc, toks): 

2463 return self._genfrac( 

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

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

2466 

2467 def binom(self, s, loc, toks): 

2468 return self._genfrac( 

2469 "(", ")", 0, 

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

2471 

2472 def _genset(self, s, loc, toks): 

2473 annotation = toks["annotation"] 

2474 body = toks["body"] 

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

2476 

2477 annotation.shrink() 

2478 cannotation = HCentered([annotation]) 

2479 cbody = HCentered([body]) 

2480 width = max(cannotation.width, cbody.width) 

2481 cannotation.hpack(width, 'exactly') 

2482 cbody.hpack(width, 'exactly') 

2483 

2484 vgap = thickness * 3 

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

2486 vlist = Vlist([cbody, # body 

2487 Vbox(0, vgap), # space 

2488 cannotation # annotation 

2489 ]) 

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

2491 vlist.shift_amount = cbody.depth + cannotation.height + vgap 

2492 else: # \overset 

2493 vlist = Vlist([cannotation, # annotation 

2494 Vbox(0, vgap), # space 

2495 cbody # body 

2496 ]) 

2497 

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

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

2500 return vlist 

2501 

2502 overset = underset = _genset 

2503 

2504 def sqrt(self, s, loc, toks): 

2505 root = toks.get("root") 

2506 body = toks["value"] 

2507 state = self.get_state() 

2508 thickness = state.get_current_underline_thickness() 

2509 

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

2511 # the height so it doesn't seem cramped 

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

2513 depth = body.depth + body.shift_amount 

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

2515 height = check.height - check.shift_amount 

2516 depth = check.depth + check.shift_amount 

2517 

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

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

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

2521 # Stretch the glue between the hrule and the body 

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

2523 'exactly', depth) 

2524 

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

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

2527 if not root: 

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

2529 else: 

2530 root = Hlist(root) 

2531 root.shrink() 

2532 root.shrink() 

2533 

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

2535 root_vlist.shift_amount = -height * 0.6 

2536 

2537 hlist = Hlist([root_vlist, # Root 

2538 # Negative kerning to put root over tick 

2539 Kern(-check.width * 0.5), 

2540 check, # Check 

2541 rightside]) # Body 

2542 return [hlist] 

2543 

2544 def overline(self, s, loc, toks): 

2545 body = toks["body"] 

2546 

2547 state = self.get_state() 

2548 thickness = state.get_current_underline_thickness() 

2549 

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

2551 depth = body.depth + body.shift_amount 

2552 

2553 # Place overline above body 

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

2555 

2556 # Stretch the glue between the hrule and the body 

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

2558 'exactly', depth) 

2559 

2560 hlist = Hlist([rightside]) 

2561 return [hlist] 

2562 

2563 def _auto_sized_delimiter(self, front, middle, back): 

2564 state = self.get_state() 

2565 if len(middle): 

2566 height = max(x.height for x in middle) 

2567 depth = max(x.depth for x in middle) 

2568 factor = None 

2569 else: 

2570 height = 0 

2571 depth = 0 

2572 factor = 1.0 

2573 parts = [] 

2574 # \left. and \right. aren't supposed to produce any symbols 

2575 if front != '.': 

2576 parts.append( 

2577 AutoHeightChar(front, height, depth, state, factor=factor)) 

2578 parts.extend(middle) 

2579 if back != '.': 

2580 parts.append( 

2581 AutoHeightChar(back, height, depth, state, factor=factor)) 

2582 hlist = Hlist(parts) 

2583 return hlist 

2584 

2585 def auto_delim(self, s, loc, toks): 

2586 return self._auto_sized_delimiter( 

2587 toks["left"], 

2588 # if "mid" in toks ... can be removed when requiring pyparsing 3. 

2589 toks["mid"].asList() if "mid" in toks else [], 

2590 toks["right"])