1""" 2 sphinx.writers.latex 3 ~~~~~~~~~~~~~~~~~~~~ 4 5 Custom docutils writer for LaTeX. 6 7 Much of this code is adapted from Dave Kuhlman's "docpy" writer from his 8 docutils sandbox. 9 10 :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. 11 :license: BSD, see LICENSE for details. 12""" 13 14import re 15import warnings 16from collections import defaultdict 17from os import path 18from typing import Any, Dict, Iterable, Iterator, List, Set, Tuple, Union, cast 19 20from docutils import nodes, writers 21from docutils.nodes import Element, Node, Text 22 23from sphinx import addnodes, highlighting 24from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning, 25 deprecated_alias) 26from sphinx.domains import IndexEntry 27from sphinx.domains.std import StandardDomain 28from sphinx.errors import SphinxError 29from sphinx.locale import _, __, admonitionlabels 30from sphinx.util import logging, split_into, texescape 31from sphinx.util.docutils import SphinxTranslator 32from sphinx.util.nodes import clean_astext, get_prev_node 33from sphinx.util.template import LaTeXRenderer 34from sphinx.util.texescape import tex_replace_map 35 36try: 37 from docutils.utils.roman import toRoman 38except ImportError: 39 # In Debain/Ubuntu, roman package is provided as roman, not as docutils.utils.roman 40 from roman import toRoman # type: ignore 41 42if False: 43 # For type annotation 44 from sphinx.builders.latex import LaTeXBuilder 45 from sphinx.builders.latex.theming import Theme 46 47 48logger = logging.getLogger(__name__) 49 50MAX_CITATION_LABEL_LENGTH = 8 51LATEXSECTIONNAMES = ["part", "chapter", "section", "subsection", 52 "subsubsection", "paragraph", "subparagraph"] 53ENUMERATE_LIST_STYLE = defaultdict(lambda: r'\arabic', 54 { 55 'arabic': r'\arabic', 56 'loweralpha': r'\alph', 57 'upperalpha': r'\Alph', 58 'lowerroman': r'\roman', 59 'upperroman': r'\Roman', 60 }) 61 62CR = '\n' 63BLANKLINE = '\n\n' 64EXTRA_RE = re.compile(r'^(.*\S)\s+\(([^()]*)\)\s*$') 65 66 67class collected_footnote(nodes.footnote): 68 """Footnotes that are collected are assigned this class.""" 69 70 71class UnsupportedError(SphinxError): 72 category = 'Markup is unsupported in LaTeX' 73 74 75class LaTeXWriter(writers.Writer): 76 77 supported = ('sphinxlatex',) 78 79 settings_spec = ('LaTeX writer options', '', ( 80 ('Document name', ['--docname'], {'default': ''}), 81 ('Document class', ['--docclass'], {'default': 'manual'}), 82 ('Author', ['--author'], {'default': ''}), 83 )) 84 settings_defaults = {} # type: Dict 85 86 output = None 87 88 def __init__(self, builder: "LaTeXBuilder") -> None: 89 super().__init__() 90 self.builder = builder 91 self.theme = None # type: Theme 92 93 def translate(self) -> None: 94 try: 95 visitor = self.builder.create_translator(self.document, self.builder, self.theme) 96 except TypeError: 97 warnings.warn('LaTeXTranslator now takes 3rd argument; "theme".', 98 RemovedInSphinx50Warning, stacklevel=2) 99 visitor = self.builder.create_translator(self.document, self.builder) 100 101 self.document.walkabout(visitor) 102 self.output = cast(LaTeXTranslator, visitor).astext() 103 104 105# Helper classes 106 107class Table: 108 """A table data""" 109 110 def __init__(self, node: Element) -> None: 111 self.header = [] # type: List[str] 112 self.body = [] # type: List[str] 113 self.align = node.get('align') 114 self.colcount = 0 115 self.colspec = None # type: str 116 self.colwidths = [] # type: List[int] 117 self.has_problematic = False 118 self.has_oldproblematic = False 119 self.has_verbatim = False 120 self.caption = None # type: List[str] 121 self.stubs = [] # type: List[int] 122 123 # current position 124 self.col = 0 125 self.row = 0 126 127 # for internal use 128 self.classes = node.get('classes', []) # type: List[str] 129 self.cells = defaultdict(int) # type: Dict[Tuple[int, int], int] 130 # it maps table location to cell_id 131 # (cell = rectangular area) 132 self.cell_id = 0 # last assigned cell_id 133 134 def is_longtable(self) -> bool: 135 """True if and only if table uses longtable environment.""" 136 return self.row > 30 or 'longtable' in self.classes 137 138 def get_table_type(self) -> str: 139 """Returns the LaTeX environment name for the table. 140 141 The class currently supports: 142 143 * longtable 144 * tabular 145 * tabulary 146 """ 147 if self.is_longtable(): 148 return 'longtable' 149 elif self.has_verbatim: 150 return 'tabular' 151 elif self.colspec: 152 return 'tabulary' 153 elif self.has_problematic or (self.colwidths and 'colwidths-given' in self.classes): 154 return 'tabular' 155 else: 156 return 'tabulary' 157 158 def get_colspec(self) -> str: 159 """Returns a column spec of table. 160 161 This is what LaTeX calls the 'preamble argument' of the used table environment. 162 163 .. note:: the ``\\X`` and ``T`` column type specifiers are defined in ``sphinx.sty``. 164 """ 165 if self.colspec: 166 return self.colspec 167 elif self.colwidths and 'colwidths-given' in self.classes: 168 total = sum(self.colwidths) 169 colspecs = ['\\X{%d}{%d}' % (width, total) for width in self.colwidths] 170 return '{|%s|}' % '|'.join(colspecs) + CR 171 elif self.has_problematic: 172 return '{|*{%d}{\\X{1}{%d}|}}' % (self.colcount, self.colcount) + CR 173 elif self.get_table_type() == 'tabulary': 174 # sphinx.sty sets T to be J by default. 175 return '{|' + ('T|' * self.colcount) + '}' + CR 176 elif self.has_oldproblematic: 177 return '{|*{%d}{\\X{1}{%d}|}}' % (self.colcount, self.colcount) + CR 178 else: 179 return '{|' + ('l|' * self.colcount) + '}' + CR 180 181 def add_cell(self, height: int, width: int) -> None: 182 """Adds a new cell to a table. 183 184 It will be located at current position: (``self.row``, ``self.col``). 185 """ 186 self.cell_id += 1 187 for col in range(width): 188 for row in range(height): 189 assert self.cells[(self.row + row, self.col + col)] == 0 190 self.cells[(self.row + row, self.col + col)] = self.cell_id 191 192 def cell(self, row: int = None, col: int = None) -> "TableCell": 193 """Returns a cell object (i.e. rectangular area) containing given position. 194 195 If no option arguments: ``row`` or ``col`` are given, the current position; 196 ``self.row`` and ``self.col`` are used to get a cell object by default. 197 """ 198 try: 199 if row is None: 200 row = self.row 201 if col is None: 202 col = self.col 203 return TableCell(self, row, col) 204 except IndexError: 205 return None 206 207 208class TableCell: 209 """A cell data of tables.""" 210 211 def __init__(self, table: Table, row: int, col: int) -> None: 212 if table.cells[(row, col)] == 0: 213 raise IndexError 214 215 self.table = table 216 self.cell_id = table.cells[(row, col)] 217 self.row = row 218 self.col = col 219 220 # adjust position for multirow/multicol cell 221 while table.cells[(self.row - 1, self.col)] == self.cell_id: 222 self.row -= 1 223 while table.cells[(self.row, self.col - 1)] == self.cell_id: 224 self.col -= 1 225 226 @property 227 def width(self) -> int: 228 """Returns the cell width.""" 229 width = 0 230 while self.table.cells[(self.row, self.col + width)] == self.cell_id: 231 width += 1 232 return width 233 234 @property 235 def height(self) -> int: 236 """Returns the cell height.""" 237 height = 0 238 while self.table.cells[(self.row + height, self.col)] == self.cell_id: 239 height += 1 240 return height 241 242 243def escape_abbr(text: str) -> str: 244 """Adjust spacing after abbreviations.""" 245 return re.sub(r'\.(?=\s|$)', r'.\@', text) 246 247 248def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str: 249 """Convert `width_str` with rst length to LaTeX length.""" 250 match = re.match(r'^(\d*\.?\d*)\s*(\S*)$', width_str) 251 if not match: 252 raise ValueError 253 res = width_str 254 amount, unit = match.groups()[:2] 255 if scale == 100: 256 float(amount) # validate amount is float 257 if unit in ('', "px"): 258 res = "%s\\sphinxpxdimen" % amount 259 elif unit == 'pt': 260 res = '%sbp' % amount # convert to 'bp' 261 elif unit == "%": 262 res = "%.3f\\linewidth" % (float(amount) / 100.0) 263 else: 264 amount_float = float(amount) * scale / 100.0 265 if unit in ('', "px"): 266 res = "%.5f\\sphinxpxdimen" % amount_float 267 elif unit == 'pt': 268 res = '%.5fbp' % amount_float 269 elif unit == "%": 270 res = "%.5f\\linewidth" % (amount_float / 100.0) 271 else: 272 res = "%.5f%s" % (amount_float, unit) 273 return res 274 275 276class LaTeXTranslator(SphinxTranslator): 277 builder = None # type: LaTeXBuilder 278 279 secnumdepth = 2 # legacy sphinxhowto.cls uses this, whereas article.cls 280 # default is originally 3. For book/report, 2 is already LaTeX default. 281 ignore_missing_images = False 282 283 # sphinx specific document classes 284 docclasses = ('howto', 'manual') 285 286 def __init__(self, document: nodes.document, builder: "LaTeXBuilder", 287 theme: "Theme" = None) -> None: 288 super().__init__(document, builder) 289 self.body = [] # type: List[str] 290 self.theme = theme 291 292 if theme is None: 293 warnings.warn('LaTeXTranslator now takes 3rd argument; "theme".', 294 RemovedInSphinx50Warning, stacklevel=2) 295 296 # flags 297 self.in_title = 0 298 self.in_production_list = 0 299 self.in_footnote = 0 300 self.in_caption = 0 301 self.in_term = 0 302 self.needs_linetrimming = 0 303 self.in_minipage = 0 304 self.no_latex_floats = 0 305 self.first_document = 1 306 self.this_is_the_title = 1 307 self.literal_whitespace = 0 308 self.in_parsed_literal = 0 309 self.compact_list = 0 310 self.first_param = 0 311 312 sphinxpkgoptions = [] 313 314 # sort out some elements 315 self.elements = self.builder.context.copy() 316 317 # initial section names 318 self.sectionnames = LATEXSECTIONNAMES[:] 319 320 if self.theme: 321 # new style: control sectioning via theme's setting 322 # 323 # .. note:: template variables(elements) are already assigned in builder 324 docclass = self.theme.docclass 325 if self.theme.toplevel_sectioning == 'section': 326 self.sectionnames.remove('chapter') 327 else: 328 # old style: sectioning control is hard-coded 329 # but some have other interface in config file 330 self.elements['wrapperclass'] = self.format_docclass(self.settings.docclass) 331 332 # we assume LaTeX class provides \chapter command except in case 333 # of non-Japanese 'howto' case 334 if document.get('docclass') == 'howto': 335 docclass = self.config.latex_docclass.get('howto', 'article') 336 if docclass[0] == 'j': # Japanese class... 337 pass 338 else: 339 self.sectionnames.remove('chapter') 340 else: 341 docclass = self.config.latex_docclass.get('manual', 'report') 342 self.elements['docclass'] = docclass 343 344 # determine top section level 345 self.top_sectionlevel = 1 346 if self.config.latex_toplevel_sectioning: 347 try: 348 self.top_sectionlevel = \ 349 self.sectionnames.index(self.config.latex_toplevel_sectioning) 350 except ValueError: 351 logger.warning(__('unknown %r toplevel_sectioning for class %r') % 352 (self.config.latex_toplevel_sectioning, docclass)) 353 354 if self.config.numfig: 355 self.numfig_secnum_depth = self.config.numfig_secnum_depth 356 if self.numfig_secnum_depth > 0: # default is 1 357 # numfig_secnum_depth as passed to sphinx.sty indices same names as in 358 # LATEXSECTIONNAMES but with -1 for part, 0 for chapter, 1 for section... 359 if len(self.sectionnames) < len(LATEXSECTIONNAMES) and \ 360 self.top_sectionlevel > 0: 361 self.numfig_secnum_depth += self.top_sectionlevel 362 else: 363 self.numfig_secnum_depth += self.top_sectionlevel - 1 364 # this (minus one) will serve as minimum to LaTeX's secnumdepth 365 self.numfig_secnum_depth = min(self.numfig_secnum_depth, 366 len(LATEXSECTIONNAMES) - 1) 367 # if passed key value is < 1 LaTeX will act as if 0; see sphinx.sty 368 sphinxpkgoptions.append('numfigreset=%s' % self.numfig_secnum_depth) 369 else: 370 sphinxpkgoptions.append('nonumfigreset') 371 372 if self.config.numfig and self.config.math_numfig: 373 sphinxpkgoptions.append('mathnumfig') 374 375 if (self.config.language not in {None, 'en', 'ja'} and 376 'fncychap' not in self.config.latex_elements): 377 # use Sonny style if any language specified (except English) 378 self.elements['fncychap'] = ('\\usepackage[Sonny]{fncychap}' + CR + 379 '\\ChNameVar{\\Large\\normalfont\\sffamily}' + CR + 380 '\\ChTitleVar{\\Large\\normalfont\\sffamily}') 381 382 self.babel = self.builder.babel 383 if self.config.language and not self.babel.is_supported_language(): 384 # emit warning if specified language is invalid 385 # (only emitting, nothing changed to processing) 386 logger.warning(__('no Babel option known for language %r'), 387 self.config.language) 388 389 minsecnumdepth = self.secnumdepth # 2 from legacy sphinx manual/howto 390 if self.document.get('tocdepth'): 391 # reduce tocdepth if `part` or `chapter` is used for top_sectionlevel 392 # tocdepth = -1: show only parts 393 # tocdepth = 0: show parts and chapters 394 # tocdepth = 1: show parts, chapters and sections 395 # tocdepth = 2: show parts, chapters, sections and subsections 396 # ... 397 tocdepth = self.document.get('tocdepth', 999) + self.top_sectionlevel - 2 398 if len(self.sectionnames) < len(LATEXSECTIONNAMES) and \ 399 self.top_sectionlevel > 0: 400 tocdepth += 1 # because top_sectionlevel is shifted by -1 401 if tocdepth > len(LATEXSECTIONNAMES) - 2: # default is 5 <-> subparagraph 402 logger.warning(__('too large :maxdepth:, ignored.')) 403 tocdepth = len(LATEXSECTIONNAMES) - 2 404 405 self.elements['tocdepth'] = '\\setcounter{tocdepth}{%d}' % tocdepth 406 minsecnumdepth = max(minsecnumdepth, tocdepth) 407 408 if self.config.numfig and (self.config.numfig_secnum_depth > 0): 409 minsecnumdepth = max(minsecnumdepth, self.numfig_secnum_depth - 1) 410 411 if minsecnumdepth > self.secnumdepth: 412 self.elements['secnumdepth'] = '\\setcounter{secnumdepth}{%d}' %\ 413 minsecnumdepth 414 415 contentsname = document.get('contentsname') 416 if contentsname: 417 self.elements['contentsname'] = self.babel_renewcommand('\\contentsname', 418 contentsname) 419 420 if self.elements['maxlistdepth']: 421 sphinxpkgoptions.append('maxlistdepth=%s' % self.elements['maxlistdepth']) 422 if sphinxpkgoptions: 423 self.elements['sphinxpkgoptions'] = '[,%s]' % ','.join(sphinxpkgoptions) 424 if self.elements['sphinxsetup']: 425 self.elements['sphinxsetup'] = ('\\sphinxsetup{%s}' % 426 self.elements['sphinxsetup']) 427 if self.elements['extraclassoptions']: 428 self.elements['classoptions'] += ',' + \ 429 self.elements['extraclassoptions'] 430 431 self.highlighter = highlighting.PygmentsBridge('latex', self.config.pygments_style, 432 latex_engine=self.config.latex_engine) 433 self.context = [] # type: List[Any] 434 self.descstack = [] # type: List[str] 435 self.tables = [] # type: List[Table] 436 self.next_table_colspec = None # type: str 437 self.bodystack = [] # type: List[List[str]] 438 self.footnote_restricted = None # type: nodes.Element 439 self.pending_footnotes = [] # type: List[nodes.footnote_reference] 440 self.curfilestack = [] # type: List[str] 441 self.handled_abbrs = set() # type: Set[str] 442 443 def pushbody(self, newbody: List[str]) -> None: 444 self.bodystack.append(self.body) 445 self.body = newbody 446 447 def popbody(self) -> List[str]: 448 body = self.body 449 self.body = self.bodystack.pop() 450 return body 451 452 def format_docclass(self, docclass: str) -> str: 453 """ prepends prefix to sphinx document classes 454 """ 455 warnings.warn('LaTeXWriter.format_docclass() is deprecated.', 456 RemovedInSphinx50Warning, stacklevel=2) 457 if docclass in self.docclasses: 458 docclass = 'sphinx' + docclass 459 return docclass 460 461 def astext(self) -> str: 462 self.elements.update({ 463 'body': ''.join(self.body), 464 'indices': self.generate_indices() 465 }) 466 return self.render('latex.tex_t', self.elements) 467 468 def hypertarget(self, id: str, withdoc: bool = True, anchor: bool = True) -> str: 469 if withdoc: 470 id = self.curfilestack[-1] + ':' + id 471 return ('\\phantomsection' if anchor else '') + \ 472 '\\label{%s}' % self.idescape(id) 473 474 def hypertarget_to(self, node: Element, anchor: bool = False) -> str: 475 labels = ''.join(self.hypertarget(node_id, anchor=False) for node_id in node['ids']) 476 if anchor: 477 return r'\phantomsection' + labels 478 else: 479 return labels 480 481 def hyperlink(self, id: str) -> str: 482 return '{\\hyperref[%s]{' % self.idescape(id) 483 484 def hyperpageref(self, id: str) -> str: 485 return '\\autopageref*{%s}' % self.idescape(id) 486 487 def escape(self, s: str) -> str: 488 return texescape.escape(s, self.config.latex_engine) 489 490 def idescape(self, id: str) -> str: 491 return '\\detokenize{%s}' % str(id).translate(tex_replace_map).\ 492 encode('ascii', 'backslashreplace').decode('ascii').\ 493 replace('\\', '_') 494 495 def babel_renewcommand(self, command: str, definition: str) -> str: 496 if self.elements['multilingual']: 497 prefix = '\\addto\\captions%s{' % self.babel.get_language() 498 suffix = '}' 499 else: # babel is disabled (mainly for Japanese environment) 500 prefix = '' 501 suffix = '' 502 503 return '%s\\renewcommand{%s}{%s}%s' % (prefix, command, definition, suffix) + CR 504 505 def generate_indices(self) -> str: 506 def generate(content: List[Tuple[str, List[IndexEntry]]], collapsed: bool) -> None: 507 ret.append('\\begin{sphinxtheindex}' + CR) 508 ret.append('\\let\\bigletter\\sphinxstyleindexlettergroup' + CR) 509 for i, (letter, entries) in enumerate(content): 510 if i > 0: 511 ret.append('\\indexspace' + CR) 512 ret.append('\\bigletter{%s}' % self.escape(letter) + CR) 513 for entry in entries: 514 if not entry[3]: 515 continue 516 ret.append('\\item\\relax\\sphinxstyleindexentry{%s}' % 517 self.encode(entry[0])) 518 if entry[4]: 519 # add "extra" info 520 ret.append('\\sphinxstyleindexextra{%s}' % self.encode(entry[4])) 521 ret.append('\\sphinxstyleindexpageref{%s:%s}' % 522 (entry[2], self.idescape(entry[3])) + CR) 523 ret.append('\\end{sphinxtheindex}' + CR) 524 525 ret = [] 526 # latex_domain_indices can be False/True or a list of index names 527 indices_config = self.config.latex_domain_indices 528 if indices_config: 529 for domain in self.builder.env.domains.values(): 530 for indexcls in domain.indices: 531 indexname = '%s-%s' % (domain.name, indexcls.name) 532 if isinstance(indices_config, list): 533 if indexname not in indices_config: 534 continue 535 content, collapsed = indexcls(domain).generate( 536 self.builder.docnames) 537 if not content: 538 continue 539 ret.append('\\renewcommand{\\indexname}{%s}' % indexcls.localname + CR) 540 generate(content, collapsed) 541 542 return ''.join(ret) 543 544 def render(self, template_name: str, variables: Dict) -> str: 545 renderer = LaTeXRenderer(latex_engine=self.config.latex_engine) 546 for template_dir in self.config.templates_path: 547 template = path.join(self.builder.confdir, template_dir, 548 template_name) 549 if path.exists(template): 550 return renderer.render(template, variables) 551 552 return renderer.render(template_name, variables) 553 554 @property 555 def table(self) -> Table: 556 """Get current table.""" 557 if self.tables: 558 return self.tables[-1] 559 else: 560 return None 561 562 def visit_document(self, node: Element) -> None: 563 self.curfilestack.append(node.get('docname', '')) 564 if self.first_document == 1: 565 # the first document is all the regular content ... 566 self.first_document = 0 567 elif self.first_document == 0: 568 # ... and all others are the appendices 569 self.body.append(CR + '\\appendix' + CR) 570 self.first_document = -1 571 if 'docname' in node: 572 self.body.append(self.hypertarget(':doc')) 573 # "- 1" because the level is increased before the title is visited 574 self.sectionlevel = self.top_sectionlevel - 1 575 576 def depart_document(self, node: Element) -> None: 577 pass 578 579 def visit_start_of_file(self, node: Element) -> None: 580 self.curfilestack.append(node['docname']) 581 582 def depart_start_of_file(self, node: Element) -> None: 583 self.curfilestack.pop() 584 585 def visit_section(self, node: Element) -> None: 586 if not self.this_is_the_title: 587 self.sectionlevel += 1 588 self.body.append(BLANKLINE) 589 590 def depart_section(self, node: Element) -> None: 591 self.sectionlevel = max(self.sectionlevel - 1, 592 self.top_sectionlevel - 1) 593 594 def visit_problematic(self, node: Element) -> None: 595 self.body.append(r'{\color{red}\bfseries{}') 596 597 def depart_problematic(self, node: Element) -> None: 598 self.body.append('}') 599 600 def visit_topic(self, node: Element) -> None: 601 self.in_minipage = 1 602 self.body.append(CR + '\\begin{sphinxShadowBox}' + CR) 603 604 def depart_topic(self, node: Element) -> None: 605 self.in_minipage = 0 606 self.body.append('\\end{sphinxShadowBox}' + CR) 607 visit_sidebar = visit_topic 608 depart_sidebar = depart_topic 609 610 def visit_glossary(self, node: Element) -> None: 611 pass 612 613 def depart_glossary(self, node: Element) -> None: 614 pass 615 616 def visit_productionlist(self, node: Element) -> None: 617 self.body.append(BLANKLINE) 618 self.body.append('\\begin{productionlist}' + CR) 619 self.in_production_list = 1 620 621 def depart_productionlist(self, node: Element) -> None: 622 self.body.append('\\end{productionlist}' + BLANKLINE) 623 self.in_production_list = 0 624 625 def visit_production(self, node: Element) -> None: 626 if node['tokenname']: 627 tn = node['tokenname'] 628 self.body.append(self.hypertarget('grammar-token-' + tn)) 629 self.body.append('\\production{%s}{' % self.encode(tn)) 630 else: 631 self.body.append('\\productioncont{') 632 633 def depart_production(self, node: Element) -> None: 634 self.body.append('}' + CR) 635 636 def visit_transition(self, node: Element) -> None: 637 self.body.append(self.elements['transition']) 638 639 def depart_transition(self, node: Element) -> None: 640 pass 641 642 def visit_title(self, node: Element) -> None: 643 parent = node.parent 644 if isinstance(parent, addnodes.seealso): 645 # the environment already handles this 646 raise nodes.SkipNode 647 elif isinstance(parent, nodes.section): 648 if self.this_is_the_title: 649 if len(node.children) != 1 and not isinstance(node.children[0], 650 nodes.Text): 651 logger.warning(__('document title is not a single Text node'), 652 location=node) 653 if not self.elements['title']: 654 # text needs to be escaped since it is inserted into 655 # the output literally 656 self.elements['title'] = self.escape(node.astext()) 657 self.this_is_the_title = 0 658 raise nodes.SkipNode 659 else: 660 short = '' 661 if node.traverse(nodes.image): 662 short = ('[%s]' % self.escape(' '.join(clean_astext(node).split()))) 663 664 try: 665 self.body.append(r'\%s%s{' % (self.sectionnames[self.sectionlevel], short)) 666 except IndexError: 667 # just use "subparagraph", it's not numbered anyway 668 self.body.append(r'\%s%s{' % (self.sectionnames[-1], short)) 669 self.context.append('}' + CR + self.hypertarget_to(node.parent)) 670 elif isinstance(parent, nodes.topic): 671 self.body.append(r'\sphinxstyletopictitle{') 672 self.context.append('}' + CR) 673 elif isinstance(parent, nodes.sidebar): 674 self.body.append(r'\sphinxstylesidebartitle{') 675 self.context.append('}' + CR) 676 elif isinstance(parent, nodes.Admonition): 677 self.body.append('{') 678 self.context.append('}' + CR) 679 elif isinstance(parent, nodes.table): 680 # Redirect body output until title is finished. 681 self.pushbody([]) 682 else: 683 logger.warning(__('encountered title node not in section, topic, table, ' 684 'admonition or sidebar'), 685 location=node) 686 self.body.append('\\sphinxstyleothertitle{') 687 self.context.append('}' + CR) 688 self.in_title = 1 689 690 def depart_title(self, node: Element) -> None: 691 self.in_title = 0 692 if isinstance(node.parent, nodes.table): 693 self.table.caption = self.popbody() 694 else: 695 self.body.append(self.context.pop()) 696 697 def visit_subtitle(self, node: Element) -> None: 698 if isinstance(node.parent, nodes.sidebar): 699 self.body.append('\\sphinxstylesidebarsubtitle{') 700 self.context.append('}' + CR) 701 else: 702 self.context.append('') 703 704 def depart_subtitle(self, node: Element) -> None: 705 self.body.append(self.context.pop()) 706 707 def visit_desc(self, node: Element) -> None: 708 if self.config.latex_show_urls == 'footnote': 709 self.body.append(BLANKLINE) 710 self.body.append('\\begin{savenotes}\\begin{fulllineitems}' + CR) 711 else: 712 self.body.append(BLANKLINE) 713 self.body.append('\\begin{fulllineitems}' + CR) 714 if self.table: 715 self.table.has_problematic = True 716 717 def depart_desc(self, node: Element) -> None: 718 if self.config.latex_show_urls == 'footnote': 719 self.body.append(CR + '\\end{fulllineitems}\\end{savenotes}' + BLANKLINE) 720 else: 721 self.body.append(CR + '\\end{fulllineitems}' + BLANKLINE) 722 723 def _visit_signature_line(self, node: Element) -> None: 724 for child in node: 725 if isinstance(child, addnodes.desc_parameterlist): 726 self.body.append(r'\pysiglinewithargsret{') 727 break 728 else: 729 self.body.append(r'\pysigline{') 730 731 def _depart_signature_line(self, node: Element) -> None: 732 self.body.append('}') 733 734 def visit_desc_signature(self, node: Element) -> None: 735 if node.parent['objtype'] != 'describe' and node['ids']: 736 hyper = self.hypertarget(node['ids'][0]) 737 else: 738 hyper = '' 739 self.body.append(hyper) 740 if not node.get('is_multiline'): 741 self._visit_signature_line(node) 742 else: 743 self.body.append('%' + CR) 744 self.body.append('\\pysigstartmultiline' + CR) 745 746 def depart_desc_signature(self, node: Element) -> None: 747 if not node.get('is_multiline'): 748 self._depart_signature_line(node) 749 else: 750 self.body.append('%' + CR) 751 self.body.append('\\pysigstopmultiline') 752 753 def visit_desc_signature_line(self, node: Element) -> None: 754 self._visit_signature_line(node) 755 756 def depart_desc_signature_line(self, node: Element) -> None: 757 self._depart_signature_line(node) 758 759 def visit_desc_addname(self, node: Element) -> None: 760 self.body.append(r'\sphinxcode{\sphinxupquote{') 761 self.literal_whitespace += 1 762 763 def depart_desc_addname(self, node: Element) -> None: 764 self.body.append('}}') 765 self.literal_whitespace -= 1 766 767 def visit_desc_type(self, node: Element) -> None: 768 pass 769 770 def depart_desc_type(self, node: Element) -> None: 771 pass 772 773 def visit_desc_returns(self, node: Element) -> None: 774 self.body.append(r'{ $\rightarrow$ ') 775 776 def depart_desc_returns(self, node: Element) -> None: 777 self.body.append(r'}') 778 779 def visit_desc_name(self, node: Element) -> None: 780 self.body.append(r'\sphinxbfcode{\sphinxupquote{') 781 self.literal_whitespace += 1 782 783 def depart_desc_name(self, node: Element) -> None: 784 self.body.append('}}') 785 self.literal_whitespace -= 1 786 787 def visit_desc_parameterlist(self, node: Element) -> None: 788 # close name, open parameterlist 789 self.body.append('}{') 790 self.first_param = 1 791 792 def depart_desc_parameterlist(self, node: Element) -> None: 793 # close parameterlist, open return annotation 794 self.body.append('}{') 795 796 def visit_desc_parameter(self, node: Element) -> None: 797 if not self.first_param: 798 self.body.append(', ') 799 else: 800 self.first_param = 0 801 if not node.hasattr('noemph'): 802 self.body.append(r'\emph{') 803 804 def depart_desc_parameter(self, node: Element) -> None: 805 if not node.hasattr('noemph'): 806 self.body.append('}') 807 808 def visit_desc_optional(self, node: Element) -> None: 809 self.body.append(r'\sphinxoptional{') 810 811 def depart_desc_optional(self, node: Element) -> None: 812 self.body.append('}') 813 814 def visit_desc_annotation(self, node: Element) -> None: 815 self.body.append(r'\sphinxbfcode{\sphinxupquote{') 816 817 def depart_desc_annotation(self, node: Element) -> None: 818 self.body.append('}}') 819 820 def visit_desc_content(self, node: Element) -> None: 821 if node.children and not isinstance(node.children[0], nodes.paragraph): 822 # avoid empty desc environment which causes a formatting bug 823 self.body.append('~') 824 825 def depart_desc_content(self, node: Element) -> None: 826 pass 827 828 def visit_seealso(self, node: Element) -> None: 829 self.body.append(BLANKLINE) 830 self.body.append('\\sphinxstrong{%s:}' % admonitionlabels['seealso'] + CR) 831 self.body.append('\\nopagebreak' + BLANKLINE) 832 833 def depart_seealso(self, node: Element) -> None: 834 self.body.append(BLANKLINE) 835 836 def visit_rubric(self, node: Element) -> None: 837 if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')): 838 raise nodes.SkipNode 839 self.body.append('\\subsubsection*{') 840 self.context.append('}' + CR) 841 self.in_title = 1 842 843 def depart_rubric(self, node: Element) -> None: 844 self.in_title = 0 845 self.body.append(self.context.pop()) 846 847 def visit_footnote(self, node: Element) -> None: 848 self.in_footnote += 1 849 label = cast(nodes.label, node[0]) 850 if 'auto' not in node: 851 self.body.append('\\sphinxstepexplicit ') 852 if self.in_parsed_literal: 853 self.body.append('\\begin{footnote}[%s]' % label.astext()) 854 else: 855 self.body.append('%' + CR) 856 self.body.append('\\begin{footnote}[%s]' % label.astext()) 857 if 'auto' not in node: 858 self.body.append('\\phantomsection' 859 '\\label{\\thesphinxscope.%s}%%' % label.astext() + CR) 860 self.body.append('\\sphinxAtStartFootnote' + CR) 861 862 def depart_footnote(self, node: Element) -> None: 863 if self.in_parsed_literal: 864 self.body.append('\\end{footnote}') 865 else: 866 self.body.append('%' + CR) 867 self.body.append('\\end{footnote}') 868 self.in_footnote -= 1 869 870 def visit_label(self, node: Element) -> None: 871 raise nodes.SkipNode 872 873 def visit_tabular_col_spec(self, node: Element) -> None: 874 self.next_table_colspec = node['spec'] 875 raise nodes.SkipNode 876 877 def visit_table(self, node: Element) -> None: 878 if len(self.tables) == 1: 879 if self.table.get_table_type() == 'longtable': 880 raise UnsupportedError( 881 '%s:%s: longtable does not support nesting a table.' % 882 (self.curfilestack[-1], node.line or '')) 883 else: 884 # change type of parent table to tabular 885 # see https://groups.google.com/d/msg/sphinx-users/7m3NeOBixeo/9LKP2B4WBQAJ 886 self.table.has_problematic = True 887 elif len(self.tables) > 2: 888 raise UnsupportedError( 889 '%s:%s: deeply nested tables are not implemented.' % 890 (self.curfilestack[-1], node.line or '')) 891 892 self.tables.append(Table(node)) 893 if self.next_table_colspec: 894 self.table.colspec = '{%s}' % self.next_table_colspec + CR 895 if 'colwidths-given' in node.get('classes', []): 896 logger.info(__('both tabularcolumns and :widths: option are given. ' 897 ':widths: is ignored.'), location=node) 898 self.next_table_colspec = None 899 900 def depart_table(self, node: Element) -> None: 901 labels = self.hypertarget_to(node) 902 table_type = self.table.get_table_type() 903 table = self.render(table_type + '.tex_t', 904 dict(table=self.table, labels=labels)) 905 self.body.append(BLANKLINE) 906 self.body.append(table) 907 self.body.append(CR) 908 909 self.tables.pop() 910 911 def visit_colspec(self, node: Element) -> None: 912 self.table.colcount += 1 913 if 'colwidth' in node: 914 self.table.colwidths.append(node['colwidth']) 915 if 'stub' in node: 916 self.table.stubs.append(self.table.colcount - 1) 917 918 def depart_colspec(self, node: Element) -> None: 919 pass 920 921 def visit_tgroup(self, node: Element) -> None: 922 pass 923 924 def depart_tgroup(self, node: Element) -> None: 925 pass 926 927 def visit_thead(self, node: Element) -> None: 928 # Redirect head output until header is finished. 929 self.pushbody(self.table.header) 930 931 def depart_thead(self, node: Element) -> None: 932 self.popbody() 933 934 def visit_tbody(self, node: Element) -> None: 935 # Redirect body output until table is finished. 936 self.pushbody(self.table.body) 937 938 def depart_tbody(self, node: Element) -> None: 939 self.popbody() 940 941 def visit_row(self, node: Element) -> None: 942 self.table.col = 0 943 944 # fill columns if the row starts with the bottom of multirow cell 945 while True: 946 cell = self.table.cell(self.table.row, self.table.col) 947 if cell is None: # not a bottom of multirow cell 948 break 949 else: # a bottom of multirow cell 950 self.table.col += cell.width 951 if cell.col: 952 self.body.append('&') 953 if cell.width == 1: 954 # insert suitable strut for equalizing row heights in given multirow 955 self.body.append('\\sphinxtablestrut{%d}' % cell.cell_id) 956 else: # use \multicolumn for wide multirow cell 957 self.body.append('\\multicolumn{%d}{|l|}' 958 '{\\sphinxtablestrut{%d}}' % 959 (cell.width, cell.cell_id)) 960 961 def depart_row(self, node: Element) -> None: 962 self.body.append('\\\\' + CR) 963 cells = [self.table.cell(self.table.row, i) for i in range(self.table.colcount)] 964 underlined = [cell.row + cell.height == self.table.row + 1 for cell in cells] 965 if all(underlined): 966 self.body.append('\\hline') 967 else: 968 i = 0 969 underlined.extend([False]) # sentinel 970 while i < len(underlined): 971 if underlined[i] is True: 972 j = underlined[i:].index(False) 973 self.body.append('\\cline{%d-%d}' % (i + 1, i + j)) 974 i += j 975 i += 1 976 self.table.row += 1 977 978 def visit_entry(self, node: Element) -> None: 979 if self.table.col > 0: 980 self.body.append('&') 981 self.table.add_cell(node.get('morerows', 0) + 1, node.get('morecols', 0) + 1) 982 cell = self.table.cell() 983 context = '' 984 if cell.width > 1: 985 if self.config.latex_use_latex_multicolumn: 986 if self.table.col == 0: 987 self.body.append('\\multicolumn{%d}{|l|}{%%' % cell.width + CR) 988 else: 989 self.body.append('\\multicolumn{%d}{l|}{%%' % cell.width + CR) 990 context = '}%' + CR 991 else: 992 self.body.append('\\sphinxstartmulticolumn{%d}%%' % cell.width + CR) 993 context = '\\sphinxstopmulticolumn' + CR 994 if cell.height > 1: 995 # \sphinxmultirow 2nd arg "cell_id" will serve as id for LaTeX macros as well 996 self.body.append('\\sphinxmultirow{%d}{%d}{%%' % (cell.height, cell.cell_id) + CR) 997 context = '}%' + CR + context 998 if cell.width > 1 or cell.height > 1: 999 self.body.append('\\begin{varwidth}[t]{\\sphinxcolwidth{%d}{%d}}' 1000 % (cell.width, self.table.colcount) + CR) 1001 context = ('\\par' + CR + '\\vskip-\\baselineskip' 1002 '\\vbox{\\hbox{\\strut}}\\end{varwidth}%' + CR + context) 1003 self.needs_linetrimming = 1 1004 if len(node.traverse(nodes.paragraph)) >= 2: 1005 self.table.has_oldproblematic = True 1006 if isinstance(node.parent.parent, nodes.thead) or (cell.col in self.table.stubs): 1007 if len(node) == 1 and isinstance(node[0], nodes.paragraph) and node.astext() == '': 1008 pass 1009 else: 1010 self.body.append('\\sphinxstyletheadfamily ') 1011 if self.needs_linetrimming: 1012 self.pushbody([]) 1013 self.context.append(context) 1014 1015 def depart_entry(self, node: Element) -> None: 1016 if self.needs_linetrimming: 1017 self.needs_linetrimming = 0 1018 body = self.popbody() 1019 1020 # Remove empty lines from top of merged cell 1021 while body and body[0] == CR: 1022 body.pop(0) 1023 self.body.extend(body) 1024 1025 self.body.append(self.context.pop()) 1026 1027 cell = self.table.cell() 1028 self.table.col += cell.width 1029 1030 # fill columns if next ones are a bottom of wide-multirow cell 1031 while True: 1032 nextcell = self.table.cell() 1033 if nextcell is None: # not a bottom of multirow cell 1034 break 1035 else: # a bottom part of multirow cell 1036 self.table.col += nextcell.width 1037 self.body.append('&') 1038 if nextcell.width == 1: 1039 # insert suitable strut for equalizing row heights in multirow 1040 # they also serve to clear colour panels which would hide the text 1041 self.body.append('\\sphinxtablestrut{%d}' % nextcell.cell_id) 1042 else: 1043 # use \multicolumn for wide multirow cell 1044 self.body.append('\\multicolumn{%d}{l|}' 1045 '{\\sphinxtablestrut{%d}}' % 1046 (nextcell.width, nextcell.cell_id)) 1047 1048 def visit_acks(self, node: Element) -> None: 1049 # this is a list in the source, but should be rendered as a 1050 # comma-separated list here 1051 bullet_list = cast(nodes.bullet_list, node[0]) 1052 list_items = cast(Iterable[nodes.list_item], bullet_list) 1053 self.body.append(BLANKLINE) 1054 self.body.append(', '.join(n.astext() for n in list_items) + '.') 1055 self.body.append(BLANKLINE) 1056 raise nodes.SkipNode 1057 1058 def visit_bullet_list(self, node: Element) -> None: 1059 if not self.compact_list: 1060 self.body.append('\\begin{itemize}' + CR) 1061 if self.table: 1062 self.table.has_problematic = True 1063 1064 def depart_bullet_list(self, node: Element) -> None: 1065 if not self.compact_list: 1066 self.body.append('\\end{itemize}' + CR) 1067 1068 def visit_enumerated_list(self, node: Element) -> None: 1069 def get_enumtype(node: Element) -> str: 1070 enumtype = node.get('enumtype', 'arabic') 1071 if 'alpha' in enumtype and 26 < node.get('start', 0) + len(node): 1072 # fallback to arabic if alphabet counter overflows 1073 enumtype = 'arabic' 1074 1075 return enumtype 1076 1077 def get_nested_level(node: Element) -> int: 1078 if node is None: 1079 return 0 1080 elif isinstance(node, nodes.enumerated_list): 1081 return get_nested_level(node.parent) + 1 1082 else: 1083 return get_nested_level(node.parent) 1084 1085 enum = "enum%s" % toRoman(get_nested_level(node)).lower() 1086 enumnext = "enum%s" % toRoman(get_nested_level(node) + 1).lower() 1087 style = ENUMERATE_LIST_STYLE.get(get_enumtype(node)) 1088 prefix = node.get('prefix', '') 1089 suffix = node.get('suffix', '.') 1090 1091 self.body.append('\\begin{enumerate}' + CR) 1092 self.body.append('\\sphinxsetlistlabels{%s}{%s}{%s}{%s}{%s}%%' % 1093 (style, enum, enumnext, prefix, suffix) + CR) 1094 if 'start' in node: 1095 self.body.append('\\setcounter{%s}{%d}' % (enum, node['start'] - 1) + CR) 1096 if self.table: 1097 self.table.has_problematic = True 1098 1099 def depart_enumerated_list(self, node: Element) -> None: 1100 self.body.append('\\end{enumerate}' + CR) 1101 1102 def visit_list_item(self, node: Element) -> None: 1103 # Append "{}" in case the next character is "[", which would break 1104 # LaTeX's list environment (no numbering and the "[" is not printed). 1105 self.body.append(r'\item {} ') 1106 1107 def depart_list_item(self, node: Element) -> None: 1108 self.body.append(CR) 1109 1110 def visit_definition_list(self, node: Element) -> None: 1111 self.body.append('\\begin{description}' + CR) 1112 if self.table: 1113 self.table.has_problematic = True 1114 1115 def depart_definition_list(self, node: Element) -> None: 1116 self.body.append('\\end{description}' + CR) 1117 1118 def visit_definition_list_item(self, node: Element) -> None: 1119 pass 1120 1121 def depart_definition_list_item(self, node: Element) -> None: 1122 pass 1123 1124 def visit_term(self, node: Element) -> None: 1125 self.in_term += 1 1126 ctx = '' 1127 if node.get('ids'): 1128 ctx = '\\phantomsection' 1129 for node_id in node['ids']: 1130 ctx += self.hypertarget(node_id, anchor=False) 1131 ctx += '}] \\leavevmode' 1132 self.body.append('\\item[{') 1133 self.context.append(ctx) 1134 1135 def depart_term(self, node: Element) -> None: 1136 self.body.append(self.context.pop()) 1137 self.in_term -= 1 1138 1139 def visit_classifier(self, node: Element) -> None: 1140 self.body.append('{[}') 1141 1142 def depart_classifier(self, node: Element) -> None: 1143 self.body.append('{]}') 1144 1145 def visit_definition(self, node: Element) -> None: 1146 pass 1147 1148 def depart_definition(self, node: Element) -> None: 1149 self.body.append(CR) 1150 1151 def visit_field_list(self, node: Element) -> None: 1152 self.body.append('\\begin{quote}\\begin{description}' + CR) 1153 if self.table: 1154 self.table.has_problematic = True 1155 1156 def depart_field_list(self, node: Element) -> None: 1157 self.body.append('\\end{description}\\end{quote}' + CR) 1158 1159 def visit_field(self, node: Element) -> None: 1160 pass 1161 1162 def depart_field(self, node: Element) -> None: 1163 pass 1164 1165 visit_field_name = visit_term 1166 depart_field_name = depart_term 1167 1168 visit_field_body = visit_definition 1169 depart_field_body = depart_definition 1170 1171 def visit_paragraph(self, node: Element) -> None: 1172 index = node.parent.index(node) 1173 if (index > 0 and isinstance(node.parent, nodes.compound) and 1174 not isinstance(node.parent[index - 1], nodes.paragraph) and 1175 not isinstance(node.parent[index - 1], nodes.compound)): 1176 # insert blank line, if the paragraph follows a non-paragraph node in a compound 1177 self.body.append('\\noindent' + CR) 1178 elif index == 1 and isinstance(node.parent, (nodes.footnote, footnotetext)): 1179 # don't insert blank line, if the paragraph is second child of a footnote 1180 # (first one is label node) 1181 pass 1182 else: 1183 # the \sphinxAtStartPar is to allow hyphenation of first word of 1184 # a paragraph in narrow contexts such as in a table cell 1185 # added as two items (cf. line trimming in depart_entry()) 1186 self.body.extend([CR, '\\sphinxAtStartPar' + CR]) 1187 1188 def depart_paragraph(self, node: Element) -> None: 1189 self.body.append(CR) 1190 1191 def visit_centered(self, node: Element) -> None: 1192 self.body.append(CR + '\\begin{center}') 1193 if self.table: 1194 self.table.has_problematic = True 1195 1196 def depart_centered(self, node: Element) -> None: 1197 self.body.append(CR + '\\end{center}') 1198 1199 def visit_hlist(self, node: Element) -> None: 1200 self.compact_list += 1 1201 ncolumns = node['ncolumns'] 1202 if self.compact_list > 1: 1203 self.body.append('\\setlength{\\multicolsep}{0pt}' + CR) 1204 self.body.append('\\begin{multicols}{' + ncolumns + '}\\raggedright' + CR) 1205 self.body.append('\\begin{itemize}\\setlength{\\itemsep}{0pt}' 1206 '\\setlength{\\parskip}{0pt}' + CR) 1207 if self.table: 1208 self.table.has_problematic = True 1209 1210 def depart_hlist(self, node: Element) -> None: 1211 self.compact_list -= 1 1212 self.body.append('\\end{itemize}\\raggedcolumns\\end{multicols}' + CR) 1213 1214 def visit_hlistcol(self, node: Element) -> None: 1215 pass 1216 1217 def depart_hlistcol(self, node: Element) -> None: 1218 # \columnbreak would guarantee same columns as in html ouput. But 1219 # some testing with long items showed that columns may be too uneven. 1220 # And in case only of short items, the automatic column breaks should 1221 # match the ones pre-computed by the hlist() directive. 1222 # self.body.append('\\columnbreak\n') 1223 pass 1224 1225 def latex_image_length(self, width_str: str, scale: int = 100) -> str: 1226 try: 1227 return rstdim_to_latexdim(width_str, scale) 1228 except ValueError: 1229 logger.warning(__('dimension unit %s is invalid. Ignored.'), width_str) 1230 return None 1231 1232 def is_inline(self, node: Element) -> bool: 1233 """Check whether a node represents an inline element.""" 1234 return isinstance(node.parent, nodes.TextElement) 1235 1236 def visit_image(self, node: Element) -> None: 1237 pre = [] # type: List[str] 1238 # in reverse order 1239 post = [] # type: List[str] 1240 include_graphics_options = [] 1241 has_hyperlink = isinstance(node.parent, nodes.reference) 1242 if has_hyperlink: 1243 is_inline = self.is_inline(node.parent) 1244 else: 1245 is_inline = self.is_inline(node) 1246 if 'width' in node: 1247 if 'scale' in node: 1248 w = self.latex_image_length(node['width'], node['scale']) 1249 else: 1250 w = self.latex_image_length(node['width']) 1251 if w: 1252 include_graphics_options.append('width=%s' % w) 1253 if 'height' in node: 1254 if 'scale' in node: 1255 h = self.latex_image_length(node['height'], node['scale']) 1256 else: 1257 h = self.latex_image_length(node['height']) 1258 if h: 1259 include_graphics_options.append('height=%s' % h) 1260 if 'scale' in node: 1261 if not include_graphics_options: 1262 # if no "width" nor "height", \sphinxincludegraphics will fit 1263 # to the available text width if oversized after rescaling. 1264 include_graphics_options.append('scale=%s' 1265 % (float(node['scale']) / 100.0)) 1266 if 'align' in node: 1267 align_prepost = { 1268 # By default latex aligns the top of an image. 1269 (1, 'top'): ('', ''), 1270 (1, 'middle'): ('\\raisebox{-0.5\\height}{', '}'), 1271 (1, 'bottom'): ('\\raisebox{-\\height}{', '}'), 1272 (0, 'center'): ('{\\hspace*{\\fill}', '\\hspace*{\\fill}}'), 1273 (0, 'default'): ('{\\hspace*{\\fill}', '\\hspace*{\\fill}}'), 1274 # These 2 don't exactly do the right thing. The image should 1275 # be floated alongside the paragraph. See 1276 # https://www.w3.org/TR/html4/struct/objects.html#adef-align-IMG 1277 (0, 'left'): ('{', '\\hspace*{\\fill}}'), 1278 (0, 'right'): ('{\\hspace*{\\fill}', '}'), 1279 } 1280 try: 1281 pre.append(align_prepost[is_inline, node['align']][0]) 1282 post.append(align_prepost[is_inline, node['align']][1]) 1283 except KeyError: 1284 pass 1285 if self.in_parsed_literal: 1286 pre.append('{\\sphinxunactivateextrasandspace ') 1287 post.append('}') 1288 if not is_inline and not has_hyperlink: 1289 pre.append(CR + '\\noindent') 1290 post.append(CR) 1291 pre.reverse() 1292 if node['uri'] in self.builder.images: 1293 uri = self.builder.images[node['uri']] 1294 else: 1295 # missing image! 1296 if self.ignore_missing_images: 1297 return 1298 uri = node['uri'] 1299 if uri.find('://') != -1: 1300 # ignore remote images 1301 return 1302 self.body.extend(pre) 1303 options = '' 1304 if include_graphics_options: 1305 options = '[%s]' % ','.join(include_graphics_options) 1306 base, ext = path.splitext(uri) 1307 if self.in_title and base: 1308 # Lowercase tokens forcely because some fncychap themes capitalize 1309 # the options of \sphinxincludegraphics unexpectly (ex. WIDTH=...). 1310 self.body.append('\\lowercase{\\sphinxincludegraphics%s}{{%s}%s}' % 1311 (options, base, ext)) 1312 else: 1313 self.body.append('\\sphinxincludegraphics%s{{%s}%s}' % 1314 (options, base, ext)) 1315 self.body.extend(post) 1316 1317 def depart_image(self, node: Element) -> None: 1318 pass 1319 1320 def visit_figure(self, node: Element) -> None: 1321 align = self.elements['figure_align'] 1322 if self.no_latex_floats: 1323 align = "H" 1324 if self.table: 1325 # TODO: support align option 1326 if 'width' in node: 1327 length = self.latex_image_length(node['width']) 1328 if length: 1329 self.body.append('\\begin{sphinxfigure-in-table}[%s]' % length + CR) 1330 self.body.append('\\centering' + CR) 1331 else: 1332 self.body.append('\\begin{sphinxfigure-in-table}' + CR) 1333 self.body.append('\\centering' + CR) 1334 if any(isinstance(child, nodes.caption) for child in node): 1335 self.body.append('\\capstart') 1336 self.context.append('\\end{sphinxfigure-in-table}\\relax' + CR) 1337 elif node.get('align', '') in ('left', 'right'): 1338 length = None 1339 if 'width' in node: 1340 length = self.latex_image_length(node['width']) 1341 elif isinstance(node[0], nodes.image) and 'width' in node[0]: 1342 length = self.latex_image_length(node[0]['width']) 1343 self.body.append(BLANKLINE) # Insert a blank line to prevent infinite loop 1344 # https://github.com/sphinx-doc/sphinx/issues/7059 1345 self.body.append('\\begin{wrapfigure}{%s}{%s}' % 1346 ('r' if node['align'] == 'right' else 'l', length or '0pt') + CR) 1347 self.body.append('\\centering') 1348 self.context.append('\\end{wrapfigure}' + CR) 1349 elif self.in_minipage: 1350 self.body.append(CR + '\\begin{center}') 1351 self.context.append('\\end{center}' + CR) 1352 else: 1353 self.body.append(CR + '\\begin{figure}[%s]' % align + CR) 1354 self.body.append('\\centering' + CR) 1355 if any(isinstance(child, nodes.caption) for child in node): 1356 self.body.append('\\capstart' + CR) 1357 self.context.append('\\end{figure}' + CR) 1358 1359 def depart_figure(self, node: Element) -> None: 1360 self.body.append(self.context.pop()) 1361 1362 def visit_caption(self, node: Element) -> None: 1363 self.in_caption += 1 1364 if isinstance(node.parent, captioned_literal_block): 1365 self.body.append('\\sphinxSetupCaptionForVerbatim{') 1366 elif self.in_minipage and isinstance(node.parent, nodes.figure): 1367 self.body.append('\\captionof{figure}{') 1368 elif self.table and node.parent.tagname == 'figure': 1369 self.body.append('\\sphinxfigcaption{') 1370 else: 1371 self.body.append('\\caption{') 1372 1373 def depart_caption(self, node: Element) -> None: 1374 self.body.append('}') 1375 if isinstance(node.parent, nodes.figure): 1376 labels = self.hypertarget_to(node.parent) 1377 self.body.append(labels) 1378 self.in_caption -= 1 1379 1380 def visit_legend(self, node: Element) -> None: 1381 self.body.append(CR + '\\begin{sphinxlegend}') 1382 1383 def depart_legend(self, node: Element) -> None: 1384 self.body.append('\\end{sphinxlegend}' + CR) 1385 1386 def visit_admonition(self, node: Element) -> None: 1387 self.body.append(CR + '\\begin{sphinxadmonition}{note}') 1388 self.no_latex_floats += 1 1389 1390 def depart_admonition(self, node: Element) -> None: 1391 self.body.append('\\end{sphinxadmonition}' + CR) 1392 self.no_latex_floats -= 1 1393 1394 def _visit_named_admonition(self, node: Element) -> None: 1395 label = admonitionlabels[node.tagname] 1396 self.body.append(CR + '\\begin{sphinxadmonition}{%s}{%s:}' % 1397 (node.tagname, label)) 1398 self.no_latex_floats += 1 1399 1400 def _depart_named_admonition(self, node: Element) -> None: 1401 self.body.append('\\end{sphinxadmonition}' + CR) 1402 self.no_latex_floats -= 1 1403 1404 visit_attention = _visit_named_admonition 1405 depart_attention = _depart_named_admonition 1406 visit_caution = _visit_named_admonition 1407 depart_caution = _depart_named_admonition 1408 visit_danger = _visit_named_admonition 1409 depart_danger = _depart_named_admonition 1410 visit_error = _visit_named_admonition 1411 depart_error = _depart_named_admonition 1412 visit_hint = _visit_named_admonition 1413 depart_hint = _depart_named_admonition 1414 visit_important = _visit_named_admonition 1415 depart_important = _depart_named_admonition 1416 visit_note = _visit_named_admonition 1417 depart_note = _depart_named_admonition 1418 visit_tip = _visit_named_admonition 1419 depart_tip = _depart_named_admonition 1420 visit_warning = _visit_named_admonition 1421 depart_warning = _depart_named_admonition 1422 1423 def visit_versionmodified(self, node: Element) -> None: 1424 pass 1425 1426 def depart_versionmodified(self, node: Element) -> None: 1427 pass 1428 1429 def visit_target(self, node: Element) -> None: 1430 def add_target(id: str) -> None: 1431 # indexing uses standard LaTeX index markup, so the targets 1432 # will be generated differently 1433 if id.startswith('index-'): 1434 return 1435 1436 # equations also need no extra blank line nor hypertarget 1437 # TODO: fix this dependency on mathbase extension internals 1438 if id.startswith('equation-'): 1439 return 1440 1441 # insert blank line, if the target follows a paragraph node 1442 index = node.parent.index(node) 1443 if index > 0 and isinstance(node.parent[index - 1], nodes.paragraph): 1444 self.body.append(CR) 1445 1446 # do not generate \phantomsection in \section{} 1447 anchor = not self.in_title 1448 self.body.append(self.hypertarget(id, anchor=anchor)) 1449 1450 # skip if visitor for next node supports hyperlink 1451 next_node = node # type: nodes.Node 1452 while isinstance(next_node, nodes.target): 1453 next_node = next_node.next_node(ascend=True) 1454 1455 domain = cast(StandardDomain, self.builder.env.get_domain('std')) 1456 if isinstance(next_node, HYPERLINK_SUPPORT_NODES): 1457 return 1458 elif domain.get_enumerable_node_type(next_node) and domain.get_numfig_title(next_node): 1459 return 1460 1461 if 'refuri' in node: 1462 return 1463 if 'anonymous' in node: 1464 return 1465 if node.get('refid'): 1466 prev_node = get_prev_node(node) 1467 if isinstance(prev_node, nodes.reference) and node['refid'] == prev_node['refid']: 1468 # a target for a hyperlink reference having alias 1469 pass 1470 else: 1471 add_target(node['refid']) 1472 for id in node['ids']: 1473 add_target(id) 1474 1475 def depart_target(self, node: Element) -> None: 1476 pass 1477 1478 def visit_attribution(self, node: Element) -> None: 1479 self.body.append(CR + '\\begin{flushright}' + CR) 1480 self.body.append('---') 1481 1482 def depart_attribution(self, node: Element) -> None: 1483 self.body.append(CR + '\\end{flushright}' + CR) 1484 1485 def visit_index(self, node: Element) -> None: 1486 def escape(value: str) -> str: 1487 value = self.encode(value) 1488 value = value.replace(r'\{', r'\sphinxleftcurlybrace{}') 1489 value = value.replace(r'\}', r'\sphinxrightcurlybrace{}') 1490 value = value.replace('"', '""') 1491 value = value.replace('@', '"@') 1492 value = value.replace('!', '"!') 1493 value = value.replace('|', r'\textbar{}') 1494 return value 1495 1496 def style(string: str) -> str: 1497 match = EXTRA_RE.match(string) 1498 if match: 1499 return match.expand(r'\\spxentry{\1}\\spxextra{\2}') 1500 else: 1501 return '\\spxentry{%s}' % string 1502 1503 if not node.get('inline', True): 1504 self.body.append(CR) 1505 entries = node['entries'] 1506 for type, string, tid, ismain, key_ in entries: 1507 m = '' 1508 if ismain: 1509 m = '|spxpagem' 1510 try: 1511 if type == 'single': 1512 try: 1513 p1, p2 = [escape(x) for x in split_into(2, 'single', string)] 1514 P1, P2 = style(p1), style(p2) 1515 self.body.append(r'\index{%s@%s!%s@%s%s}' % (p1, P1, p2, P2, m)) 1516 except ValueError: 1517 p = escape(split_into(1, 'single', string)[0]) 1518 P = style(p) 1519 self.body.append(r'\index{%s@%s%s}' % (p, P, m)) 1520 elif type == 'pair': 1521 p1, p2 = [escape(x) for x in split_into(2, 'pair', string)] 1522 P1, P2 = style(p1), style(p2) 1523 self.body.append(r'\index{%s@%s!%s@%s%s}\index{%s@%s!%s@%s%s}' % 1524 (p1, P1, p2, P2, m, p2, P2, p1, P1, m)) 1525 elif type == 'triple': 1526 p1, p2, p3 = [escape(x) for x in split_into(3, 'triple', string)] 1527 P1, P2, P3 = style(p1), style(p2), style(p3) 1528 self.body.append( 1529 r'\index{%s@%s!%s %s@%s %s%s}' 1530 r'\index{%s@%s!%s, %s@%s, %s%s}' 1531 r'\index{%s@%s!%s %s@%s %s%s}' % 1532 (p1, P1, p2, p3, P2, P3, m, 1533 p2, P2, p3, p1, P3, P1, m, 1534 p3, P3, p1, p2, P1, P2, m)) 1535 elif type == 'see': 1536 p1, p2 = [escape(x) for x in split_into(2, 'see', string)] 1537 P1 = style(p1) 1538 self.body.append(r'\index{%s@%s|see{%s}}' % (p1, P1, p2)) 1539 elif type == 'seealso': 1540 p1, p2 = [escape(x) for x in split_into(2, 'seealso', string)] 1541 P1 = style(p1) 1542 self.body.append(r'\index{%s@%s|see{%s}}' % (p1, P1, p2)) 1543 else: 1544 logger.warning(__('unknown index entry type %s found'), type) 1545 except ValueError as err: 1546 logger.warning(str(err)) 1547 if not node.get('inline', True): 1548 self.body.append('\\ignorespaces ') 1549 raise nodes.SkipNode 1550 1551 def visit_raw(self, node: Element) -> None: 1552 if not self.is_inline(node): 1553 self.body.append(CR) 1554 if 'latex' in node.get('format', '').split(): 1555 self.body.append(node.astext()) 1556 if not self.is_inline(node): 1557 self.body.append(CR) 1558 raise nodes.SkipNode 1559 1560 def visit_reference(self, node: Element) -> None: 1561 if not self.in_title: 1562 for id in node.get('ids'): 1563 anchor = not self.in_caption 1564 self.body += self.hypertarget(id, anchor=anchor) 1565 if not self.is_inline(node): 1566 self.body.append(CR) 1567 uri = node.get('refuri', '') 1568 if not uri and node.get('refid'): 1569 uri = '%' + self.curfilestack[-1] + '#' + node['refid'] 1570 if self.in_title or not uri: 1571 self.context.append('') 1572 elif uri.startswith('#'): 1573 # references to labels in the same document 1574 id = self.curfilestack[-1] + ':' + uri[1:] 1575 self.body.append(self.hyperlink(id)) 1576 self.body.append(r'\emph{') 1577 if self.config.latex_show_pagerefs and not \ 1578 self.in_production_list: 1579 self.context.append('}}} (%s)' % self.hyperpageref(id)) 1580 else: 1581 self.context.append('}}}') 1582 elif uri.startswith('%'): 1583 # references to documents or labels inside documents 1584 hashindex = uri.find('#') 1585 if hashindex == -1: 1586 # reference to the document 1587 id = uri[1:] + '::doc' 1588 else: 1589 # reference to a label 1590 id = uri[1:].replace('#', ':') 1591 self.body.append(self.hyperlink(id)) 1592 if (len(node) and 1593 isinstance(node[0], nodes.Element) and 1594 'std-term' in node[0].get('classes', [])): 1595 # don't add a pageref for glossary terms 1596 self.context.append('}}}') 1597 # mark up as termreference 1598 self.body.append(r'\sphinxtermref{') 1599 else: 1600 self.body.append(r'\sphinxcrossref{') 1601 if self.config.latex_show_pagerefs and not self.in_production_list: 1602 self.context.append('}}} (%s)' % self.hyperpageref(id)) 1603 else: 1604 self.context.append('}}}') 1605 else: 1606 if len(node) == 1 and uri == node[0]: 1607 if node.get('nolinkurl'): 1608 self.body.append('\\sphinxnolinkurl{%s}' % self.encode_uri(uri)) 1609 else: 1610 self.body.append('\\sphinxurl{%s}' % self.encode_uri(uri)) 1611 raise nodes.SkipNode 1612 else: 1613 self.body.append('\\sphinxhref{%s}{' % self.encode_uri(uri)) 1614 self.context.append('}') 1615 1616 def depart_reference(self, node: Element) -> None: 1617 self.body.append(self.context.pop()) 1618 if not self.is_inline(node): 1619 self.body.append(CR) 1620 1621 def visit_number_reference(self, node: Element) -> None: 1622 if node.get('refid'): 1623 id = self.curfilestack[-1] + ':' + node['refid'] 1624 else: 1625 id = node.get('refuri', '')[1:].replace('#', ':') 1626 1627 title = self.escape(node.get('title', '%s')).replace('\\%s', '%s') 1628 if '\\{name\\}' in title or '\\{number\\}' in title: 1629 # new style format (cf. "Fig.%{number}") 1630 title = title.replace('\\{name\\}', '{name}').replace('\\{number\\}', '{number}') 1631 text = escape_abbr(title).format(name='\\nameref{%s}' % self.idescape(id), 1632 number='\\ref{%s}' % self.idescape(id)) 1633 else: 1634 # old style format (cf. "Fig.%{number}") 1635 text = escape_abbr(title) % ('\\ref{%s}' % self.idescape(id)) 1636 hyperref = '\\hyperref[%s]{%s}' % (self.idescape(id), text) 1637 self.body.append(hyperref) 1638 1639 raise nodes.SkipNode 1640 1641 def visit_download_reference(self, node: Element) -> None: 1642 pass 1643 1644 def depart_download_reference(self, node: Element) -> None: 1645 pass 1646 1647 def visit_pending_xref(self, node: Element) -> None: 1648 pass 1649 1650 def depart_pending_xref(self, node: Element) -> None: 1651 pass 1652 1653 def visit_emphasis(self, node: Element) -> None: 1654 self.body.append(r'\sphinxstyleemphasis{') 1655 1656 def depart_emphasis(self, node: Element) -> None: 1657 self.body.append('}') 1658 1659 def visit_literal_emphasis(self, node: Element) -> None: 1660 self.body.append(r'\sphinxstyleliteralemphasis{\sphinxupquote{') 1661 1662 def depart_literal_emphasis(self, node: Element) -> None: 1663 self.body.append('}}') 1664 1665 def visit_strong(self, node: Element) -> None: 1666 self.body.append(r'\sphinxstylestrong{') 1667 1668 def depart_strong(self, node: Element) -> None: 1669 self.body.append('}') 1670 1671 def visit_literal_strong(self, node: Element) -> None: 1672 self.body.append(r'\sphinxstyleliteralstrong{\sphinxupquote{') 1673 1674 def depart_literal_strong(self, node: Element) -> None: 1675 self.body.append('}}') 1676 1677 def visit_abbreviation(self, node: Element) -> None: 1678 abbr = node.astext() 1679 self.body.append(r'\sphinxstyleabbreviation{') 1680 # spell out the explanation once 1681 if node.hasattr('explanation') and abbr not in self.handled_abbrs: 1682 self.context.append('} (%s)' % self.encode(node['explanation'])) 1683 self.handled_abbrs.add(abbr) 1684 else: 1685 self.context.append('}') 1686 1687 def depart_abbreviation(self, node: Element) -> None: 1688 self.body.append(self.context.pop()) 1689 1690 def visit_manpage(self, node: Element) -> None: 1691 return self.visit_literal_emphasis(node) 1692 1693 def depart_manpage(self, node: Element) -> None: 1694 return self.depart_literal_emphasis(node) 1695 1696 def visit_title_reference(self, node: Element) -> None: 1697 self.body.append(r'\sphinxtitleref{') 1698 1699 def depart_title_reference(self, node: Element) -> None: 1700 self.body.append('}') 1701 1702 def visit_thebibliography(self, node: Element) -> None: 1703 citations = cast(Iterable[nodes.citation], node) 1704 labels = (cast(nodes.label, citation[0]) for citation in citations) 1705 longest_label = max((label.astext() for label in labels), key=len) 1706 if len(longest_label) > MAX_CITATION_LABEL_LENGTH: 1707 # adjust max width of citation labels not to break the layout 1708 longest_label = longest_label[:MAX_CITATION_LABEL_LENGTH] 1709 1710 self.body.append(CR + '\\begin{sphinxthebibliography}{%s}' % 1711 self.encode(longest_label) + CR) 1712 1713 def depart_thebibliography(self, node: Element) -> None: 1714 self.body.append('\\end{sphinxthebibliography}' + CR) 1715 1716 def visit_citation(self, node: Element) -> None: 1717 label = cast(nodes.label, node[0]) 1718 self.body.append('\\bibitem[%s]{%s:%s}' % (self.encode(label.astext()), 1719 node['docname'], node['ids'][0])) 1720 1721 def depart_citation(self, node: Element) -> None: 1722 pass 1723 1724 def visit_citation_reference(self, node: Element) -> None: 1725 if self.in_title: 1726 pass 1727 else: 1728 self.body.append('\\sphinxcite{%s:%s}' % (node['docname'], node['refname'])) 1729 raise nodes.SkipNode 1730 1731 def depart_citation_reference(self, node: Element) -> None: 1732 pass 1733 1734 def visit_literal(self, node: Element) -> None: 1735 if self.in_title: 1736 self.body.append(r'\sphinxstyleliteralintitle{\sphinxupquote{') 1737 elif 'kbd' in node['classes']: 1738 self.body.append(r'\sphinxkeyboard{\sphinxupquote{') 1739 else: 1740 self.body.append(r'\sphinxcode{\sphinxupquote{') 1741 1742 def depart_literal(self, node: Element) -> None: 1743 self.body.append('}}') 1744 1745 def visit_footnote_reference(self, node: Element) -> None: 1746 raise nodes.SkipNode 1747 1748 def visit_footnotemark(self, node: Element) -> None: 1749 self.body.append('\\sphinxfootnotemark[') 1750 1751 def depart_footnotemark(self, node: Element) -> None: 1752 self.body.append(']') 1753 1754 def visit_footnotetext(self, node: Element) -> None: 1755 label = cast(nodes.label, node[0]) 1756 self.body.append('%' + CR) 1757 self.body.append('\\begin{footnotetext}[%s]' 1758 '\\phantomsection\\label{\\thesphinxscope.%s}%%' 1759 % (label.astext(), label.astext()) + CR) 1760 self.body.append('\\sphinxAtStartFootnote' + CR) 1761 1762 def depart_footnotetext(self, node: Element) -> None: 1763 # the \ignorespaces in particular for after table header use 1764 self.body.append('%' + CR) 1765 self.body.append('\\end{footnotetext}\\ignorespaces ') 1766 1767 def visit_captioned_literal_block(self, node: Element) -> None: 1768 pass 1769 1770 def depart_captioned_literal_block(self, node: Element) -> None: 1771 pass 1772 1773 def visit_literal_block(self, node: Element) -> None: 1774 if node.rawsource != node.astext(): 1775 # most probably a parsed-literal block -- don't highlight 1776 self.in_parsed_literal += 1 1777 self.body.append('\\begin{sphinxalltt}' + CR) 1778 else: 1779 labels = self.hypertarget_to(node) 1780 if isinstance(node.parent, captioned_literal_block): 1781 labels += self.hypertarget_to(node.parent) 1782 if labels and not self.in_footnote: 1783 self.body.append(CR + '\\def\\sphinxLiteralBlockLabel{' + labels + '}') 1784 1785 lang = node.get('language', 'default') 1786 linenos = node.get('linenos', False) 1787 highlight_args = node.get('highlight_args', {}) 1788 highlight_args['force'] = node.get('force', False) 1789 opts = self.config.highlight_options.get(lang, {}) 1790 1791 hlcode = self.highlighter.highlight_block( 1792 node.rawsource, lang, opts=opts, linenos=linenos, 1793 location=node, **highlight_args 1794 ) 1795 if self.in_footnote: 1796 self.body.append(CR + '\\sphinxSetupCodeBlockInFootnote') 1797 hlcode = hlcode.replace('\\begin{Verbatim}', 1798 '\\begin{sphinxVerbatim}') 1799 # if in table raise verbatim flag to avoid "tabulary" environment 1800 # and opt for sphinxVerbatimintable to handle caption & long lines 1801 elif self.table: 1802 self.table.has_problematic = True 1803 self.table.has_verbatim = True 1804 hlcode = hlcode.replace('\\begin{Verbatim}', 1805 '\\begin{sphinxVerbatimintable}') 1806 else: 1807 hlcode = hlcode.replace('\\begin{Verbatim}', 1808 '\\begin{sphinxVerbatim}') 1809 # get consistent trailer 1810 hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim} 1811 if self.table and not self.in_footnote: 1812 hlcode += '\\end{sphinxVerbatimintable}' 1813 else: 1814 hlcode += '\\end{sphinxVerbatim}' 1815 1816 hllines = str(highlight_args.get('hl_lines', []))[1:-1] 1817 if hllines: 1818 self.body.append(CR + '\\fvset{hllines={, %s,}}%%' % hllines) 1819 self.body.append(CR + hlcode + CR) 1820 if hllines: 1821 self.body.append('\\sphinxresetverbatimhllines' + CR) 1822 raise nodes.SkipNode 1823 1824 def depart_literal_block(self, node: Element) -> None: 1825 self.body.append(CR + '\\end{sphinxalltt}' + CR) 1826 self.in_parsed_literal -= 1 1827 visit_doctest_block = visit_literal_block 1828 depart_doctest_block = depart_literal_block 1829 1830 def visit_line(self, node: Element) -> None: 1831 self.body.append('\\item[] ') 1832 1833 def depart_line(self, node: Element) -> None: 1834 self.body.append(CR) 1835 1836 def visit_line_block(self, node: Element) -> None: 1837 if isinstance(node.parent, nodes.line_block): 1838 self.body.append('\\item[]' + CR) 1839 self.body.append('\\begin{DUlineblock}{\\DUlineblockindent}' + CR) 1840 else: 1841 self.body.append(CR + '\\begin{DUlineblock}{0em}' + CR) 1842 if self.table: 1843 self.table.has_problematic = True 1844 1845 def depart_line_block(self, node: Element) -> None: 1846 self.body.append('\\end{DUlineblock}' + CR) 1847 1848 def visit_block_quote(self, node: Element) -> None: 1849 # If the block quote contains a single object and that object 1850 # is a list, then generate a list not a block quote. 1851 # This lets us indent lists. 1852 done = 0 1853 if len(node.children) == 1: 1854 child = node.children[0] 1855 if isinstance(child, nodes.bullet_list) or \ 1856 isinstance(child, nodes.enumerated_list): 1857 done = 1 1858 if not done: 1859 self.body.append('\\begin{quote}' + CR) 1860 if self.table: 1861 self.table.has_problematic = True 1862 1863 def depart_block_quote(self, node: Element) -> None: 1864 done = 0 1865 if len(node.children) == 1: 1866 child = node.children[0] 1867 if isinstance(child, nodes.bullet_list) or \ 1868 isinstance(child, nodes.enumerated_list): 1869 done = 1 1870 if not done: 1871 self.body.append('\\end{quote}' + CR) 1872 1873 # option node handling copied from docutils' latex writer 1874 1875 def visit_option(self, node: Element) -> None: 1876 if self.context[-1]: 1877 # this is not the first option 1878 self.body.append(', ') 1879 1880 def depart_option(self, node: Element) -> None: 1881 # flag that the first option is done. 1882 self.context[-1] += 1 1883 1884 def visit_option_argument(self, node: Element) -> None: 1885 """The delimiter betweeen an option and its argument.""" 1886 self.body.append(node.get('delimiter', ' ')) 1887 1888 def depart_option_argument(self, node: Element) -> None: 1889 pass 1890 1891 def visit_option_group(self, node: Element) -> None: 1892 self.body.append('\\item [') 1893 # flag for first option 1894 self.context.append(0) 1895 1896 def depart_option_group(self, node: Element) -> None: 1897 self.context.pop() # the flag 1898 self.body.append('] ') 1899 1900 def visit_option_list(self, node: Element) -> None: 1901 self.body.append('\\begin{optionlist}{3cm}' + CR) 1902 if self.table: 1903 self.table.has_problematic = True 1904 1905 def depart_option_list(self, node: Element) -> None: 1906 self.body.append('\\end{optionlist}' + CR) 1907 1908 def visit_option_list_item(self, node: Element) -> None: 1909 pass 1910 1911 def depart_option_list_item(self, node: Element) -> None: 1912 pass 1913 1914 def visit_option_string(self, node: Element) -> None: 1915 ostring = node.astext() 1916 self.body.append(self.encode(ostring)) 1917 raise nodes.SkipNode 1918 1919 def visit_description(self, node: Element) -> None: 1920 self.body.append(' ') 1921 1922 def depart_description(self, node: Element) -> None: 1923 pass 1924 1925 def visit_superscript(self, node: Element) -> None: 1926 self.body.append('$^{\\text{') 1927 1928 def depart_superscript(self, node: Element) -> None: 1929 self.body.append('}}$') 1930 1931 def visit_subscript(self, node: Element) -> None: 1932 self.body.append('$_{\\text{') 1933 1934 def depart_subscript(self, node: Element) -> None: 1935 self.body.append('}}$') 1936 1937 def visit_inline(self, node: Element) -> None: 1938 classes = node.get('classes', []) 1939 if classes in [['menuselection']]: 1940 self.body.append(r'\sphinxmenuselection{') 1941 self.context.append('}') 1942 elif classes in [['guilabel']]: 1943 self.body.append(r'\sphinxguilabel{') 1944 self.context.append('}') 1945 elif classes in [['accelerator']]: 1946 self.body.append(r'\sphinxaccelerator{') 1947 self.context.append('}') 1948 elif classes and not self.in_title: 1949 self.body.append(r'\DUrole{%s}{' % ','.join(classes)) 1950 self.context.append('}') 1951 else: 1952 self.context.append('') 1953 1954 def depart_inline(self, node: Element) -> None: 1955 self.body.append(self.context.pop()) 1956 1957 def visit_generated(self, node: Element) -> None: 1958 pass 1959 1960 def depart_generated(self, node: Element) -> None: 1961 pass 1962 1963 def visit_compound(self, node: Element) -> None: 1964 pass 1965 1966 def depart_compound(self, node: Element) -> None: 1967 pass 1968 1969 def visit_container(self, node: Element) -> None: 1970 pass 1971 1972 def depart_container(self, node: Element) -> None: 1973 pass 1974 1975 def visit_decoration(self, node: Element) -> None: 1976 pass 1977 1978 def depart_decoration(self, node: Element) -> None: 1979 pass 1980 1981 # docutils-generated elements that we don't support 1982 1983 def visit_header(self, node: Element) -> None: 1984 raise nodes.SkipNode 1985 1986 def visit_footer(self, node: Element) -> None: 1987 raise nodes.SkipNode 1988 1989 def visit_docinfo(self, node: Element) -> None: 1990 raise nodes.SkipNode 1991 1992 # text handling 1993 1994 def encode(self, text: str) -> str: 1995 text = self.escape(text) 1996 if self.literal_whitespace: 1997 # Insert a blank before the newline, to avoid 1998 # ! LaTeX Error: There's no line here to end. 1999 text = text.replace(CR, '~\\\\' + CR).replace(' ', '~') 2000 return text 2001 2002 def encode_uri(self, text: str) -> str: 2003 # TODO: it is probably wrong that this uses texescape.escape() 2004 # this must be checked against hyperref package exact dealings 2005 # mainly, %, #, {, } and \ need escaping via a \ escape 2006 # in \href, the tilde is allowed and must be represented literally 2007 return self.encode(text).replace('\\textasciitilde{}', '~').\ 2008 replace('\\sphinxhyphen{}', '-').\ 2009 replace('\\textquotesingle{}', "'") 2010 2011 def visit_Text(self, node: Text) -> None: 2012 text = self.encode(node.astext()) 2013 self.body.append(text) 2014 2015 def depart_Text(self, node: Text) -> None: 2016 pass 2017 2018 def visit_comment(self, node: Element) -> None: 2019 raise nodes.SkipNode 2020 2021 def visit_meta(self, node: Element) -> None: 2022 # only valid for HTML 2023 raise nodes.SkipNode 2024 2025 def visit_system_message(self, node: Element) -> None: 2026 pass 2027 2028 def depart_system_message(self, node: Element) -> None: 2029 self.body.append(CR) 2030 2031 def visit_math(self, node: Element) -> None: 2032 if self.in_title: 2033 self.body.append(r'\protect\(%s\protect\)' % node.astext()) 2034 else: 2035 self.body.append(r'\(%s\)' % node.astext()) 2036 raise nodes.SkipNode 2037 2038 def visit_math_block(self, node: Element) -> None: 2039 if node.get('label'): 2040 label = "equation:%s:%s" % (node['docname'], node['label']) 2041 else: 2042 label = None 2043 2044 if node.get('nowrap'): 2045 if label: 2046 self.body.append(r'\label{%s}' % label) 2047 self.body.append(node.astext()) 2048 else: 2049 from sphinx.util.math import wrap_displaymath 2050 self.body.append(wrap_displaymath(node.astext(), label, 2051 self.config.math_number_all)) 2052 raise nodes.SkipNode 2053 2054 def visit_math_reference(self, node: Element) -> None: 2055 label = "equation:%s:%s" % (node['docname'], node['target']) 2056 eqref_format = self.config.math_eqref_format 2057 if eqref_format: 2058 try: 2059 ref = r'\ref{%s}' % label 2060 self.body.append(eqref_format.format(number=ref)) 2061 except KeyError as exc: 2062 logger.warning(__('Invalid math_eqref_format: %r'), exc, 2063 location=node) 2064 self.body.append(r'\eqref{%s}' % label) 2065 else: 2066 self.body.append(r'\eqref{%s}' % label) 2067 2068 def depart_math_reference(self, node: Element) -> None: 2069 pass 2070 2071 def unknown_visit(self, node: Node) -> None: 2072 raise NotImplementedError('Unknown node: ' + node.__class__.__name__) 2073 2074 # --------- METHODS FOR COMPATIBILITY -------------------------------------- 2075 2076 def collect_footnotes(self, node: Element) -> Dict[str, List[Union["collected_footnote", bool]]]: # NOQA 2077 def footnotes_under(n: Element) -> Iterator[nodes.footnote]: 2078 if isinstance(n, nodes.footnote): 2079 yield n 2080 else: 2081 for c in n.children: 2082 if isinstance(c, addnodes.start_of_file): 2083 continue 2084 elif isinstance(c, nodes.Element): 2085 yield from footnotes_under(c) 2086 2087 warnings.warn('LaTeXWriter.collected_footnote() is deprecated.', 2088 RemovedInSphinx40Warning, stacklevel=2) 2089 2090 fnotes = {} # type: Dict[str, List[Union[collected_footnote, bool]]] 2091 for fn in footnotes_under(node): 2092 label = cast(nodes.label, fn[0]) 2093 num = label.astext().strip() 2094 newnode = collected_footnote('', *fn.children, number=num) 2095 fnotes[num] = [newnode, False] 2096 return fnotes 2097 2098 @property 2099 def no_contractions(self) -> int: 2100 warnings.warn('LaTeXTranslator.no_contractions is deprecated.', 2101 RemovedInSphinx40Warning, stacklevel=2) 2102 return 0 2103 2104 def babel_defmacro(self, name: str, definition: str) -> str: 2105 warnings.warn('babel_defmacro() is deprecated.', 2106 RemovedInSphinx40Warning, stacklevel=2) 2107 2108 if self.elements['babel']: 2109 prefix = '\\addto\\extras%s{' % self.babel.get_language() 2110 suffix = '}' 2111 else: # babel is disabled (mainly for Japanese environment) 2112 prefix = '' 2113 suffix = '' 2114 2115 return ('%s\\def%s{%s}%s' % (prefix, name, definition, suffix) + CR) 2116 2117 def generate_numfig_format(self, builder: "LaTeXBuilder") -> str: 2118 warnings.warn('generate_numfig_format() is deprecated.', 2119 RemovedInSphinx40Warning, stacklevel=2) 2120 ret = [] # type: List[str] 2121 figure = self.config.numfig_format['figure'].split('%s', 1) 2122 if len(figure) == 1: 2123 ret.append('\\def\\fnum@figure{%s}' % self.escape(figure[0]).strip() + CR) 2124 else: 2125 definition = escape_abbr(self.escape(figure[0])) 2126 ret.append(self.babel_renewcommand('\\figurename', definition)) 2127 ret.append('\\makeatletter' + CR) 2128 ret.append('\\def\\fnum@figure{\\figurename\\thefigure{}%s}' % 2129 self.escape(figure[1]) + CR) 2130 ret.append('\\makeatother' + CR) 2131 2132 table = self.config.numfig_format['table'].split('%s', 1) 2133 if len(table) == 1: 2134 ret.append('\\def\\fnum@table{%s}' % self.escape(table[0]).strip() + CR) 2135 else: 2136 definition = escape_abbr(self.escape(table[0])) 2137 ret.append(self.babel_renewcommand('\\tablename', definition)) 2138 ret.append('\\makeatletter' + CR) 2139 ret.append('\\def\\fnum@table{\\tablename\\thetable{}%s}' % 2140 self.escape(table[1]) + CR) 2141 ret.append('\\makeatother' + CR) 2142 2143 codeblock = self.config.numfig_format['code-block'].split('%s', 1) 2144 if len(codeblock) == 1: 2145 pass # FIXME 2146 else: 2147 definition = self.escape(codeblock[0]).strip() 2148 ret.append(self.babel_renewcommand('\\literalblockname', definition)) 2149 if codeblock[1]: 2150 pass # FIXME 2151 2152 return ''.join(ret) 2153 2154 2155# Import old modules here for compatibility 2156from sphinx.builders.latex import constants # NOQA 2157from sphinx.builders.latex.util import ExtBabel # NOQA 2158 2159deprecated_alias('sphinx.writers.latex', 2160 { 2161 'ADDITIONAL_SETTINGS': constants.ADDITIONAL_SETTINGS, 2162 'DEFAULT_SETTINGS': constants.DEFAULT_SETTINGS, 2163 'LUALATEX_DEFAULT_FONTPKG': constants.LUALATEX_DEFAULT_FONTPKG, 2164 'PDFLATEX_DEFAULT_FONTPKG': constants.PDFLATEX_DEFAULT_FONTPKG, 2165 'SHORTHANDOFF': constants.SHORTHANDOFF, 2166 'XELATEX_DEFAULT_FONTPKG': constants.XELATEX_DEFAULT_FONTPKG, 2167 'XELATEX_GREEK_DEFAULT_FONTPKG': constants.XELATEX_GREEK_DEFAULT_FONTPKG, 2168 'ExtBabel': ExtBabel, 2169 }, 2170 RemovedInSphinx40Warning, 2171 { 2172 'ADDITIONAL_SETTINGS': 2173 'sphinx.builders.latex.constants.ADDITIONAL_SETTINGS', 2174 'DEFAULT_SETTINGS': 2175 'sphinx.builders.latex.constants.DEFAULT_SETTINGS', 2176 'LUALATEX_DEFAULT_FONTPKG': 2177 'sphinx.builders.latex.constants.LUALATEX_DEFAULT_FONTPKG', 2178 'PDFLATEX_DEFAULT_FONTPKG': 2179 'sphinx.builders.latex.constants.PDFLATEX_DEFAULT_FONTPKG', 2180 'SHORTHANDOFF': 2181 'sphinx.builders.latex.constants.SHORTHANDOFF', 2182 'XELATEX_DEFAULT_FONTPKG': 2183 'sphinx.builders.latex.constants.XELATEX_DEFAULT_FONTPKG', 2184 'XELATEX_GREEK_DEFAULT_FONTPKG': 2185 'sphinx.builders.latex.constants.XELATEX_GREEK_DEFAULT_FONTPKG', 2186 'ExtBabel': 'sphinx.builders.latex.util.ExtBabel', 2187 }) 2188 2189# FIXME: Workaround to avoid circular import 2190# refs: https://github.com/sphinx-doc/sphinx/issues/5433 2191from sphinx.builders.latex.nodes import ( # NOQA isort:skip 2192 HYPERLINK_SUPPORT_NODES, captioned_literal_block, footnotetext, 2193) 2194