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