"""Custom docutils writer for LaTeX.

Much of this code is adapted from Dave Kuhlman's "docpy" writer from his
docutils sandbox.
"""

from __future__ import annotations

import re
from collections import defaultdict
from collections.abc import Iterable
from os import path
from typing import TYPE_CHECKING, Any, ClassVar, cast

from docutils import nodes, writers

from sphinx import addnodes, highlighting
from sphinx.errors import SphinxError
from sphinx.locale import _, __, admonitionlabels
from sphinx.util import logging, texescape
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.index_entries import split_index_msg
from sphinx.util.nodes import clean_astext, get_prev_node
from sphinx.util.template import LaTeXRenderer
from sphinx.util.texescape import tex_replace_map

try:
    from docutils.utils.roman import toRoman
except ImportError:
    # In Debian/Ubuntu, roman package is provided as roman, not as docutils.utils.roman
    from roman import toRoman  # type: ignore[no-redef, import-not-found]

if TYPE_CHECKING:
    from docutils.nodes import Element, Node, Text

    from sphinx.builders.latex import LaTeXBuilder
    from sphinx.builders.latex.theming import Theme
    from sphinx.domains import IndexEntry


logger = logging.getLogger(__name__)

MAX_CITATION_LABEL_LENGTH = 8
LATEXSECTIONNAMES = [
    'part',
    'chapter',
    'section',
    'subsection',
    'subsubsection',
    'paragraph',
    'subparagraph',
]
ENUMERATE_LIST_STYLE = defaultdict(
    lambda: r'\arabic',
    {
        'arabic': r'\arabic',
        'loweralpha': r'\alph',
        'upperalpha': r'\Alph',
        'lowerroman': r'\roman',
        'upperroman': r'\Roman',
    },
)

CR = '\n'
BLANKLINE = '\n\n'
EXTRA_RE = re.compile(r'^(.*\S)\s+\(([^()]*)\)\s*$')


class collected_footnote(nodes.footnote):
    """Footnotes that are collected are assigned this class."""


class UnsupportedError(SphinxError):
    category = 'Markup is unsupported in LaTeX'


class LaTeXWriter(writers.Writer):  # type: ignore[type-arg]
    supported = ('sphinxlatex',)

    settings_spec = (
        'LaTeX writer options',
        '',
        (
            ('Document name', ['--docname'], {'default': ''}),
            ('Document class', ['--docclass'], {'default': 'manual'}),
            ('Author', ['--author'], {'default': ''}),
        ),
    )
    settings_defaults: ClassVar[dict[str, Any]] = {}

    theme: Theme

    def __init__(self, builder: LaTeXBuilder) -> None:
        super().__init__()
        self.builder = builder

    def translate(self) -> None:
        assert isinstance(self.document, nodes.document)
        visitor = self.builder.create_translator(
            self.document, self.builder, self.theme
        )
        self.document.walkabout(visitor)
        self.output = cast(LaTeXTranslator, visitor).astext()


# Helper classes


class Table:
    """A table data"""

    def __init__(self, node: Element) -> None:
        self.header: list[str] = []
        self.body: list[str] = []
        self.align = node.get('align', 'default')
        self.classes: list[str] = node.get('classes', [])
        self.styles: list[str] = []
        if 'standard' in self.classes:
            self.styles.append('standard')
        elif 'borderless' in self.classes:
            self.styles.append('borderless')
        elif 'booktabs' in self.classes:
            self.styles.append('booktabs')
        if 'nocolorrows' in self.classes:
            self.styles.append('nocolorrows')
        elif 'colorrows' in self.classes:
            self.styles.append('colorrows')
        self.colcount = 0
        self.colspec: str = ''
        if 'booktabs' in self.styles or 'borderless' in self.styles:
            self.colsep: str | None = ''
        elif 'standard' in self.styles:
            self.colsep = '|'
        else:
            self.colsep = None
        self.colwidths: list[int] = []
        self.has_problematic = False
        self.has_oldproblematic = False
        self.has_verbatim = False
        self.caption: list[str] = []
        self.stubs: list[int] = []

        # current position
        self.col = 0
        self.row = 0

        # A dict mapping a table location to a cell_id (cell = rectangular area)
        self.cells: dict[tuple[int, int], int] = defaultdict(int)
        self.cell_id = 0  # last assigned cell_id

    def is_longtable(self) -> bool:
        """True if and only if table uses longtable environment."""
        return self.row > 30 or 'longtable' in self.classes

    def get_table_type(self) -> str:
        """Returns the LaTeX environment name for the table.

        The class currently supports:

        * longtable
        * tabular
        * tabulary
        """
        if self.is_longtable():
            return 'longtable'
        elif self.has_verbatim:
            return 'tabular'
        elif self.colspec:
            return 'tabulary'
        elif self.has_problematic or (
            self.colwidths and 'colwidths-given' in self.classes
        ):
            return 'tabular'
        else:
            return 'tabulary'

    def get_colspec(self) -> str:
        r"""Returns a column spec of table.

        This is what LaTeX calls the 'preamble argument' of the used table environment.

        .. note::

           The ``\\X`` and ``T`` column type specifiers are defined in
           ``sphinxlatextables.sty``.
        """
        if self.colspec:
            return self.colspec

        _colsep = self.colsep
        assert _colsep is not None
        if self.colwidths and 'colwidths-given' in self.classes:
            total = sum(self.colwidths)
            colspecs = [r'\X{%d}{%d}' % (width, total) for width in self.colwidths]
            return f'{{{_colsep}{_colsep.join(colspecs)}{_colsep}}}' + CR
        elif self.has_problematic:
            return (
                r'{%s*{%d}{\X{1}{%d}%s}}'
                % (_colsep, self.colcount, self.colcount, _colsep)
                + CR
            )
        elif self.get_table_type() == 'tabulary':
            # sphinx.sty sets T to be J by default.
            return '{' + _colsep + (('T' + _colsep) * self.colcount) + '}' + CR
        elif self.has_oldproblematic:
            return (
                r'{%s*{%d}{\X{1}{%d}%s}}'
                % (_colsep, self.colcount, self.colcount, _colsep)
                + CR
            )
        else:
            return '{' + _colsep + (('l' + _colsep) * self.colcount) + '}' + CR

    def add_cell(self, height: int, width: int) -> None:
        """Adds a new cell to a table.

        It will be located at current position: (``self.row``, ``self.col``).
        """
        self.cell_id += 1
        for col in range(width):
            for row in range(height):
                assert self.cells[(self.row + row, self.col + col)] == 0
                self.cells[(self.row + row, self.col + col)] = self.cell_id

    def cell(
        self,
        row: int | None = None,
        col: int | None = None,
    ) -> TableCell | None:
        """Returns a cell object (i.e. rectangular area) containing given position.

        If no option arguments: ``row`` or ``col`` are given, the current position;
        ``self.row`` and ``self.col`` are used to get a cell object by default.
        """
        try:
            if row is None:
                row = self.row
            if col is None:
                col = self.col
            return TableCell(self, row, col)
        except IndexError:
            return None


class TableCell:
    """Data of a cell in a table."""

    def __init__(self, table: Table, row: int, col: int) -> None:
        if table.cells[(row, col)] == 0:
            raise IndexError

        self.table = table
        self.cell_id = table.cells[(row, col)]
        self.row = row
        self.col = col

        # adjust position for multirow/multicol cell
        while table.cells[(self.row - 1, self.col)] == self.cell_id:
            self.row -= 1
        while table.cells[(self.row, self.col - 1)] == self.cell_id:
            self.col -= 1

    @property
    def width(self) -> int:
        """Returns the cell width."""
        width = 0
        while self.table.cells[(self.row, self.col + width)] == self.cell_id:
            width += 1
        return width

    @property
    def height(self) -> int:
        """Returns the cell height."""
        height = 0
        while self.table.cells[(self.row + height, self.col)] == self.cell_id:
            height += 1
        return height


def escape_abbr(text: str) -> str:
    """Adjust spacing after abbreviations."""
    return re.sub(r'\.(?=\s|$)', r'.\@', text)


def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
    """Convert `width_str` with rst length to LaTeX length."""
    match = re.match(r'^(\d*\.?\d*)\s*(\S*)$', width_str)
    if not match:
        raise ValueError
    res = width_str
    amount, unit = match.groups()[:2]
    if scale == 100:
        float(amount)  # validate amount is float
        if unit in ('', 'px'):
            res = r'%s\sphinxpxdimen' % amount
        elif unit == 'pt':
            res = '%sbp' % amount  # convert to 'bp'
        elif unit == '%':
            res = r'%.3f\linewidth' % (float(amount) / 100.0)
    else:
        amount_float = float(amount) * scale / 100.0
        if unit in ('', 'px'):
            res = r'%.5f\sphinxpxdimen' % amount_float
        elif unit == 'pt':
            res = '%.5fbp' % amount_float
        elif unit == '%':
            res = r'%.5f\linewidth' % (amount_float / 100.0)
        else:
            res = f'{amount_float:.5f}{unit}'
    return res


class LaTeXTranslator(SphinxTranslator):
    builder: LaTeXBuilder

    secnumdepth = 2  # legacy sphinxhowto.cls uses this, whereas article.cls
    # default is originally 3. For book/report, 2 is already LaTeX default.
    ignore_missing_images = False

    def __init__(
        self, document: nodes.document, builder: LaTeXBuilder, theme: Theme
    ) -> None:
        super().__init__(document, builder)
        self.body: list[str] = []
        self.theme = theme

        # flags
        self.in_title = 0
        self.in_production_list = 0
        self.in_footnote = 0
        self.in_caption = 0
        self.in_term = 0
        self.needs_linetrimming = 0
        self.in_minipage = 0
        # only used by figure inside an admonition
        self.no_latex_floats = 0
        self.first_document = 1
        self.this_is_the_title = 1
        self.literal_whitespace = 0
        self.in_parsed_literal = 0
        self.compact_list = 0
        self.first_param = 0
        self.in_desc_signature = False

        sphinxpkgoptions = []

        # sort out some elements
        self.elements = self.builder.context.copy()

        # initial section names
        self.sectionnames = LATEXSECTIONNAMES.copy()
        if self.theme.toplevel_sectioning == 'section':
            self.sectionnames.remove('chapter')

        # determine top section level
        self.top_sectionlevel = 1
        if self.config.latex_toplevel_sectioning:
            try:
                self.top_sectionlevel = self.sectionnames.index(
                    self.config.latex_toplevel_sectioning
                )
            except ValueError:
                logger.warning(
                    __('unknown %r toplevel_sectioning for class %r'),
                    self.config.latex_toplevel_sectioning,
                    self.theme.docclass,
                )

        if self.config.numfig:
            self.numfig_secnum_depth = self.config.numfig_secnum_depth
            if self.numfig_secnum_depth > 0:  # default is 1
                # numfig_secnum_depth as passed to sphinx.sty indices same names as in
                # LATEXSECTIONNAMES but with -1 for part, 0 for chapter, 1 for section...
                if (
                    len(self.sectionnames) < len(LATEXSECTIONNAMES)
                    and self.top_sectionlevel > 0
                ):
                    self.numfig_secnum_depth += self.top_sectionlevel
                else:
                    self.numfig_secnum_depth += self.top_sectionlevel - 1
                # this (minus one) will serve as minimum to LaTeX's secnumdepth
                self.numfig_secnum_depth = min(
                    self.numfig_secnum_depth, len(LATEXSECTIONNAMES) - 1
                )
                # if passed key value is < 1 LaTeX will act as if 0; see sphinx.sty
                sphinxpkgoptions.append('numfigreset=%s' % self.numfig_secnum_depth)
            else:
                sphinxpkgoptions.append('nonumfigreset')

        if self.config.numfig and self.config.math_numfig:
            sphinxpkgoptions.extend([
                'mathnumfig',
                'mathnumsep={%s}' % self.config.math_numsep,
            ])

        if (
            self.config.language not in {'en', 'ja'}
            and 'fncychap' not in self.config.latex_elements
        ):
            # use Sonny style if any language specified (except English)
            self.elements['fncychap'] = (
                r'\usepackage[Sonny]{fncychap}'
                + CR
                + r'\ChNameVar{\Large\normalfont\sffamily}'
                + CR
                + r'\ChTitleVar{\Large\normalfont\sffamily}'
            )

        self.babel = self.builder.babel
        if not self.babel.is_supported_language():
            # emit warning if specified language is invalid
            # (only emitting, nothing changed to processing)
            logger.warning(
                __('no Babel option known for language %r'), self.config.language
            )

        minsecnumdepth = self.secnumdepth  # 2 from legacy sphinx manual/howto
        if self.document.get('tocdepth'):
            # reduce tocdepth if `part` or `chapter` is used for top_sectionlevel
            #   tocdepth = -1: show only parts
            #   tocdepth =  0: show parts and chapters
            #   tocdepth =  1: show parts, chapters and sections
            #   tocdepth =  2: show parts, chapters, sections and subsections
            #   ...
            tocdepth = self.document.get('tocdepth', 999) + self.top_sectionlevel - 2
            if (
                len(self.sectionnames) < len(LATEXSECTIONNAMES)
                and self.top_sectionlevel > 0
            ):
                tocdepth += 1  # because top_sectionlevel is shifted by -1
            if tocdepth > len(LATEXSECTIONNAMES) - 2:  # default is 5 <-> subparagraph
                logger.warning(__('too large :maxdepth:, ignored.'))
                tocdepth = len(LATEXSECTIONNAMES) - 2

            self.elements['tocdepth'] = r'\setcounter{tocdepth}{%d}' % tocdepth
            minsecnumdepth = max(minsecnumdepth, tocdepth)

        if self.config.numfig and (self.config.numfig_secnum_depth > 0):
            minsecnumdepth = max(minsecnumdepth, self.numfig_secnum_depth - 1)

        if minsecnumdepth > self.secnumdepth:
            self.elements['secnumdepth'] = (
                r'\setcounter{secnumdepth}{%d}' % minsecnumdepth
            )

        contentsname = document.get('contentsname')
        if contentsname:
            self.elements['contentsname'] = self.babel_renewcommand(
                r'\contentsname', contentsname
            )

        if self.elements['maxlistdepth']:
            sphinxpkgoptions.append('maxlistdepth=%s' % self.elements['maxlistdepth'])
        if sphinxpkgoptions:
            self.elements['sphinxpkgoptions'] = '[,%s]' % ','.join(sphinxpkgoptions)
        if self.elements['sphinxsetup']:
            self.elements['sphinxsetup'] = (
                r'\sphinxsetup{%s}' % self.elements['sphinxsetup']
            )
        if self.elements['extraclassoptions']:
            self.elements['classoptions'] += ',' + self.elements['extraclassoptions']

        self.highlighter = highlighting.PygmentsBridge(
            'latex', self.config.pygments_style, latex_engine=self.config.latex_engine
        )
        self.context: list[Any] = []
        self.descstack: list[str] = []
        self.tables: list[Table] = []
        self.next_table_colspec: str | None = None
        self.bodystack: list[list[str]] = []
        self.footnote_restricted: Element | None = None
        self.pending_footnotes: list[nodes.footnote_reference] = []
        self.curfilestack: list[str] = []
        self.handled_abbrs: set[str] = set()

    def pushbody(self, newbody: list[str]) -> None:
        self.bodystack.append(self.body)
        self.body = newbody

    def popbody(self) -> list[str]:
        body = self.body
        self.body = self.bodystack.pop()
        return body

    def astext(self) -> str:
        self.elements.update({
            'body': ''.join(self.body),
            'indices': self.generate_indices(),
        })
        return self.render('latex.tex.jinja', self.elements)

    def hypertarget(self, id: str, withdoc: bool = True, anchor: bool = True) -> str:
        if withdoc:
            id = self.curfilestack[-1] + ':' + id
        escaped_id = self.idescape(id)
        return (r'\phantomsection' if anchor else '') + r'\label{%s}' % escaped_id

    def hypertarget_to(self, node: Element, anchor: bool = False) -> str:
        labels = ''.join(
            self.hypertarget(node_id, anchor=False) for node_id in node['ids']
        )
        if anchor:
            return r'\phantomsection' + labels
        else:
            return labels

    def hyperlink(self, id: str) -> str:
        return r'{\hyperref[%s]{' % self.idescape(id)

    def hyperpageref(self, id: str) -> str:
        return r'\autopageref*{%s}' % self.idescape(id)

    def escape(self, s: str) -> str:
        return texescape.escape(s, self.config.latex_engine)

    def idescape(self, id: str) -> str:
        id = str(id).translate(tex_replace_map)
        id = id.encode('ascii', 'backslashreplace').decode('ascii')
        return r'\detokenize{%s}' % id.replace('\\', '_')

    def babel_renewcommand(self, command: str, definition: str) -> str:
        if self.elements['multilingual']:
            prefix = r'\addto\captions%s{' % self.babel.get_language()
            suffix = '}'
        else:  # babel is disabled (mainly for Japanese environment)
            prefix = ''
            suffix = ''

        return rf'{prefix}\renewcommand{{{command}}}{{{definition}}}{suffix}' + CR

    def generate_indices(self) -> str:
        def generate(
            content: list[tuple[str, list[IndexEntry]]], collapsed: bool
        ) -> None:
            ret.append(r'\begin{sphinxtheindex}' + CR)
            ret.append(r'\let\bigletter\sphinxstyleindexlettergroup' + CR)
            for i, (letter, entries) in enumerate(content):
                if i > 0:
                    ret.append(r'\indexspace' + CR)
                ret.append(r'\bigletter{%s}' % self.escape(letter) + CR)
                for entry in entries:
                    if not entry[3]:
                        continue
                    ret.append(
                        r'\item\relax\sphinxstyleindexentry{%s}' % self.encode(entry[0])
                    )
                    if entry[4]:
                        # add "extra" info
                        ret.append(
                            r'\sphinxstyleindexextra{%s}' % self.encode(entry[4])
                        )
                    ret.append(
                        r'\sphinxstyleindexpageref{%s:%s}'
                        % (entry[2], self.idescape(entry[3]))
                        + CR
                    )
            ret.append(r'\end{sphinxtheindex}' + CR)

        ret = []
        # latex_domain_indices can be False/True or a list of index names
        if indices_config := self.config.latex_domain_indices:
            if not isinstance(indices_config, bool):
                check_names = True
                indices_config = frozenset(indices_config)
            else:
                check_names = False
            for domain in self.builder.env.domains.sorted():
                for index_cls in domain.indices:
                    index_name = f'{domain.name}-{index_cls.name}'
                    if check_names and index_name not in indices_config:
                        continue
                    content, collapsed = index_cls(domain).generate(
                        self.builder.docnames
                    )
                    if content:
                        ret.append(
                            r'\renewcommand{\indexname}{%s}' % index_cls.localname + CR
                        )
                        generate(content, collapsed)

        return ''.join(ret)

    def render(self, template_name: str, variables: dict[str, Any]) -> str:
        renderer = LaTeXRenderer(latex_engine=self.config.latex_engine)
        for template_dir in self.config.templates_path:
            template = path.join(self.builder.confdir, template_dir, template_name)
            if path.exists(template):
                return renderer.render(template, variables)
            elif template.endswith('.jinja'):
                legacy_template = template.removesuffix('.jinja') + '_t'
                if path.exists(legacy_template):
                    logger.warning(
                        __('template %s not found; loading from legacy %s instead'),
                        template_name,
                        legacy_template,
                    )
                    return renderer.render(legacy_template, variables)

        return renderer.render(template_name, variables)

    @property
    def table(self) -> Table | None:
        """Get current table."""
        if self.tables:
            return self.tables[-1]
        else:
            return None

    def visit_document(self, node: Element) -> None:
        self.curfilestack.append(node.get('docname', ''))
        if self.first_document == 1:
            # the first document is all the regular content ...
            self.first_document = 0
        elif self.first_document == 0:
            # ... and all others are the appendices
            self.body.append(CR + r'\appendix' + CR)
            self.first_document = -1
        if 'docname' in node:
            self.body.append(self.hypertarget(':doc'))
        # "- 1" because the level is increased before the title is visited
        self.sectionlevel = self.top_sectionlevel - 1

    def depart_document(self, node: Element) -> None:
        pass

    def visit_start_of_file(self, node: Element) -> None:
        self.curfilestack.append(node['docname'])
        self.body.append(CR + r'\sphinxstepscope' + CR)

    def depart_start_of_file(self, node: Element) -> None:
        self.curfilestack.pop()

    def visit_section(self, node: Element) -> None:
        if not self.this_is_the_title:
            self.sectionlevel += 1
        self.body.append(BLANKLINE)

    def depart_section(self, node: Element) -> None:
        self.sectionlevel = max(self.sectionlevel - 1, self.top_sectionlevel - 1)

    def visit_problematic(self, node: Element) -> None:
        self.body.append(r'{\color{red}\bfseries{}')

    def depart_problematic(self, node: Element) -> None:
        self.body.append('}')

    def visit_topic(self, node: Element) -> None:
        self.in_minipage += 1
        if 'contents' in node.get('classes', []):
            self.body.append(CR + r'\begin{sphinxcontents}' + CR)
            self.context.append(r'\end{sphinxcontents}' + CR)
        else:
            self.body.append(CR + r'\begin{sphinxtopic}' + CR)
            self.context.append(r'\end{sphinxtopic}' + CR)

    def depart_topic(self, node: Element) -> None:
        self.in_minipage -= 1
        self.body.append(self.context.pop())

    def visit_sidebar(self, node: Element) -> None:
        self.in_minipage += 1
        self.body.append(CR + r'\begin{sphinxsidebar}' + CR)
        self.context.append(r'\end{sphinxsidebar}' + CR)

    depart_sidebar = depart_topic

    def visit_glossary(self, node: Element) -> None:
        pass

    def depart_glossary(self, node: Element) -> None:
        pass

    def visit_productionlist(self, node: Element) -> None:
        self.body.append(BLANKLINE)
        self.body.append(r'\begin{productionlist}' + CR)
        self.in_production_list = 1

    def depart_productionlist(self, node: Element) -> None:
        self.body.append(r'\end{productionlist}' + BLANKLINE)
        self.in_production_list = 0

    def visit_production(self, node: Element) -> None:
        if node['tokenname']:
            tn = node['tokenname']
            self.body.append(self.hypertarget('grammar-token-' + tn))
            self.body.append(r'\production{%s}{' % self.encode(tn))
        else:
            self.body.append(r'\productioncont{')

    def depart_production(self, node: Element) -> None:
        self.body.append('}' + CR)

    def visit_transition(self, node: Element) -> None:
        self.body.append(self.elements['transition'])

    def depart_transition(self, node: Element) -> None:
        pass

    def visit_title(self, node: Element) -> None:
        parent = node.parent
        if isinstance(parent, addnodes.seealso):
            # the environment already handles this
            raise nodes.SkipNode
        if isinstance(parent, nodes.section):
            if self.this_is_the_title:
                if (
                    len(node.children) != 1
                    and not isinstance(node.children[0], nodes.Text)
                ):  # fmt: skip
                    logger.warning(
                        __('document title is not a single Text node'), location=node
                    )
                if not self.elements['title']:
                    # text needs to be escaped since it is inserted into
                    # the output literally
                    self.elements['title'] = self.escape(node.astext())
                self.this_is_the_title = 0
                raise nodes.SkipNode
            short = ''
            if any(node.findall(nodes.image)):
                short = '[%s]' % self.escape(' '.join(clean_astext(node).split()))

            try:
                self.body.append(rf'\{self.sectionnames[self.sectionlevel]}{short}{{')
            except IndexError:
                # just use "subparagraph", it's not numbered anyway
                self.body.append(rf'\{self.sectionnames[-1]}{short}{{')
            self.context.append('}' + CR + self.hypertarget_to(node.parent))
        elif isinstance(parent, nodes.topic):
            if 'contents' in parent.get('classes', []):
                self.body.append(r'\sphinxstylecontentstitle{')
            else:
                self.body.append(r'\sphinxstyletopictitle{')
            self.context.append('}' + CR)
        elif isinstance(parent, nodes.sidebar):
            self.body.append(r'\sphinxstylesidebartitle{')
            self.context.append('}' + CR)
        elif isinstance(parent, nodes.Admonition):
            self.body.append('{')
            self.context.append('}' + CR)
        elif isinstance(parent, nodes.table):
            # Redirect body output until title is finished.
            self.pushbody([])
        else:
            logger.warning(
                __(
                    'encountered title node not in section, topic, table, '
                    'admonition or sidebar'
                ),
                location=node,
            )
            self.body.append(r'\sphinxstyleothertitle{')
            self.context.append('}' + CR)
        self.in_title = 1

    def depart_title(self, node: Element) -> None:
        self.in_title = 0
        if isinstance(node.parent, nodes.table):
            assert self.table is not None
            self.table.caption = self.popbody()
        else:
            self.body.append(self.context.pop())

    def visit_subtitle(self, node: Element) -> None:
        if isinstance(node.parent, nodes.sidebar):
            self.body.append(r'\sphinxstylesidebarsubtitle{')
            self.context.append('}' + CR)
        else:
            self.context.append('')

    def depart_subtitle(self, node: Element) -> None:
        self.body.append(self.context.pop())

    #############################################################
    # Domain-specific object descriptions
    #############################################################

    # Top-level nodes for descriptions
    ##################################

    def visit_desc(self, node: Element) -> None:
        if self.config.latex_show_urls == 'footnote':
            self.body.append(BLANKLINE)
            self.body.append(r'\begin{savenotes}\begin{fulllineitems}' + CR)
        else:
            self.body.append(BLANKLINE)
            self.body.append(r'\begin{fulllineitems}' + CR)
        if self.table:
            self.table.has_problematic = True

    def depart_desc(self, node: Element) -> None:
        if self.in_desc_signature:
            self.body.append(CR + r'\pysigstopsignatures')
            self.in_desc_signature = False
        if self.config.latex_show_urls == 'footnote':
            self.body.append(CR + r'\end{fulllineitems}\end{savenotes}' + BLANKLINE)
        else:
            self.body.append(CR + r'\end{fulllineitems}' + BLANKLINE)

    def _visit_signature_line(self, node: Element) -> None:
        def next_sibling(e: Node) -> Node | None:
            try:
                return e.parent[e.parent.index(e) + 1]
            except (AttributeError, IndexError):
                return None

        def has_multi_line(e: Element) -> bool:
            return e.get('multi_line_parameter_list')

        self.has_tp_list = False
        self.orphan_tp_list = False

        for child in node:
            if isinstance(child, addnodes.desc_type_parameter_list):
                self.has_tp_list = True
                multi_tp_list = has_multi_line(child)
                arglist = next_sibling(child)
                if isinstance(arglist, addnodes.desc_parameterlist):
                    # tp_list + arglist: \macro{name}{tp_list}{arglist}{retann}
                    multi_arglist = has_multi_line(arglist)
                else:
                    # orphan tp_list:    \macro{name}{tp_list}{}{retann}
                    # see: https://github.com/sphinx-doc/sphinx/issues/12543
                    self.orphan_tp_list = True
                    multi_arglist = False

                if multi_tp_list:
                    if multi_arglist:
                        self.body.append(
                            CR
                            + r'\pysigwithonelineperargwithonelinepertparg'
                            + CR
                            + '{'
                        )
                    else:
                        self.body.append(
                            CR + r'\pysiglinewithargsretwithonelinepertparg' + CR + '{'
                        )
                else:
                    if multi_arglist:
                        self.body.append(
                            CR + r'\pysigwithonelineperargwithtypelist' + CR + '{'
                        )
                    else:
                        self.body.append(
                            CR + r'\pysiglinewithargsretwithtypelist' + CR + '{'
                        )
                break

            if isinstance(child, addnodes.desc_parameterlist):
                # arglist only: \macro{name}{arglist}{retann}
                if has_multi_line(child):
                    self.body.append(CR + r'\pysigwithonelineperarg' + CR + '{')
                else:
                    self.body.append(CR + r'\pysiglinewithargsret' + CR + '{')
                break
        else:
            # no tp_list, no arglist: \macro{name}
            self.body.append(CR + r'\pysigline' + CR + '{')

    def _depart_signature_line(self, node: Element) -> None:
        self.body.append('}')

    def visit_desc_signature(self, node: Element) -> None:
        hyper = ''
        if node.parent['objtype'] != 'describe' and node['ids']:
            for id in node['ids']:
                hyper += self.hypertarget(id)
        self.body.append(hyper)
        if not self.in_desc_signature:
            self.in_desc_signature = True
            self.body.append(CR + r'\pysigstartsignatures')
        if not node.get('is_multiline'):
            self._visit_signature_line(node)
        else:
            self.body.append(CR + r'\pysigstartmultiline')

    def depart_desc_signature(self, node: Element) -> None:
        if not node.get('is_multiline'):
            self._depart_signature_line(node)
        else:
            self.body.append(CR + r'\pysigstopmultiline')

    def visit_desc_signature_line(self, node: Element) -> None:
        self._visit_signature_line(node)

    def depart_desc_signature_line(self, node: Element) -> None:
        self._depart_signature_line(node)

    def visit_desc_content(self, node: Element) -> None:
        assert self.in_desc_signature
        self.body.append(CR + r'\pysigstopsignatures')
        self.in_desc_signature = False

    def depart_desc_content(self, node: Element) -> None:
        pass

    def visit_desc_inline(self, node: Element) -> None:
        self.body.append(r'\sphinxcode{\sphinxupquote{')

    def depart_desc_inline(self, node: Element) -> None:
        self.body.append('}}')

    # Nodes for high-level structure in signatures
    ##############################################

    def visit_desc_name(self, node: Element) -> None:
        self.body.append(r'\sphinxbfcode{\sphinxupquote{')
        self.literal_whitespace += 1

    def depart_desc_name(self, node: Element) -> None:
        self.body.append('}}')
        self.literal_whitespace -= 1

    def visit_desc_addname(self, node: Element) -> None:
        self.body.append(r'\sphinxcode{\sphinxupquote{')
        self.literal_whitespace += 1

    def depart_desc_addname(self, node: Element) -> None:
        self.body.append('}}')
        self.literal_whitespace -= 1

    def visit_desc_type(self, node: Element) -> None:
        pass

    def depart_desc_type(self, node: Element) -> None:
        pass

    def visit_desc_returns(self, node: Element) -> None:
        self.body.append(r'{ $\rightarrow$ ')

    def depart_desc_returns(self, node: Element) -> None:
        self.body.append(r'}')

    def _visit_sig_parameter_list(
        self, node: Element, parameter_group: type[Element]
    ) -> None:
        """Visit a signature parameters or type parameters list.

        The *parameter_group* value is the type of a child node acting as a required parameter
        or as a set of contiguous optional parameters.

        The caller is responsible for closing adding surrounding LaTeX macro argument start
        and stop tokens.
        """
        self.is_first_param = True
        self.optional_param_level = 0
        self.params_left_at_level = 0
        self.param_group_index = 0
        # Counts as what we call a parameter group either a required parameter, or a
        # set of contiguous optional ones.
        self.list_is_required_param = [
            isinstance(c, parameter_group) for c in node.children
        ]
        # How many required parameters are left.
        self.required_params_left = sum(self.list_is_required_param)
        self.param_separator = r'\sphinxparamcomma '
        self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)

    def visit_desc_parameterlist(self, node: Element) -> None:
        if self.has_tp_list:
            if self.orphan_tp_list:
                # close type parameters list (#2)
                self.body.append('}' + CR + '{')
                # empty parameters list argument (#3)
                return
        else:
            # close name argument (#1), open parameters list argument (#2)
            self.body.append('}' + CR + '{')
        self._visit_sig_parameter_list(node, addnodes.desc_parameter)

    def depart_desc_parameterlist(self, node: Element) -> None:
        # close parameterlist, open return annotation
        assert not self.orphan_tp_list
        self.body.append('}' + CR + '{')

    def visit_desc_type_parameter_list(self, node: Element) -> None:
        # close name argument (#1), open type parameters list argument (#2)
        self.body.append('}' + CR + '{')
        self._visit_sig_parameter_list(node, addnodes.desc_type_parameter)

    def depart_desc_type_parameter_list(self, node: Element) -> None:
        if self.orphan_tp_list:
            # this node next sibling isn't a desc_parameterlist, there are no parameters:
            # close the type list, output an empty parameter list, open return annotation.
            self.body.append('}' + CR + '{}' + CR + '{')
        else:
            # close type parameters list, open parameters list argument (#3)
            self.body.append('}' + CR + '{')

    def _visit_sig_parameter(self, node: Element, parameter_macro: str) -> None:
        if self.is_first_param:
            self.is_first_param = False
        elif not self.multi_line_parameter_list and not self.required_params_left:
            self.body.append(self.param_separator)
        if self.optional_param_level == 0:
            self.required_params_left -= 1
        else:
            self.params_left_at_level -= 1
        if not node.hasattr('noemph'):
            self.body.append(parameter_macro)

    def _depart_sig_parameter(self, node: Element) -> None:
        if not node.hasattr('noemph'):
            self.body.append('}')
        is_required = self.list_is_required_param[self.param_group_index]
        if self.multi_line_parameter_list:
            len_lirp = len(self.list_is_required_param)
            is_last_group = self.param_group_index + 1 == len_lirp
            next_is_required = (
                not is_last_group
                and self.list_is_required_param[self.param_group_index + 1]
            )
            opt_param_left_at_level = self.params_left_at_level > 0
            if (
                opt_param_left_at_level
                or is_required
                and (is_last_group or next_is_required)
            ):
                self.body.append(self.param_separator)

        elif self.required_params_left:
            self.body.append(self.param_separator)

        if is_required:
            self.param_group_index += 1

    def visit_desc_parameter(self, node: Element) -> None:
        self._visit_sig_parameter(node, r'\sphinxparam{')

    def depart_desc_parameter(self, node: Element) -> None:
        self._depart_sig_parameter(node)

    def visit_desc_type_parameter(self, node: Element) -> None:
        self._visit_sig_parameter(node, r'\sphinxtypeparam{')

    def depart_desc_type_parameter(self, node: Element) -> None:
        self._depart_sig_parameter(node)

    def visit_desc_optional(self, node: Element) -> None:
        self.params_left_at_level = sum(
            isinstance(c, addnodes.desc_parameter) for c in node.children
        )
        self.optional_param_level += 1
        self.max_optional_param_level = self.optional_param_level
        if self.multi_line_parameter_list:
            if self.is_first_param:
                self.body.append(r'\sphinxoptional{')
            elif self.required_params_left:
                self.body.append(self.param_separator)
                self.body.append(r'\sphinxoptional{')
            else:
                self.body.append(r'\sphinxoptional{')
                self.body.append(self.param_separator)
        else:
            self.body.append(r'\sphinxoptional{')

    def depart_desc_optional(self, node: Element) -> None:
        self.optional_param_level -= 1
        if self.multi_line_parameter_list:
            # If it's the first time we go down one level, add the separator before the
            # bracket.
            if self.optional_param_level == self.max_optional_param_level - 1:
                self.body.append(self.param_separator)
        self.body.append('}')
        if self.optional_param_level == 0:
            self.param_group_index += 1

    def visit_desc_annotation(self, node: Element) -> None:
        self.body.append(r'\sphinxbfcode{\sphinxupquote{')

    def depart_desc_annotation(self, node: Element) -> None:
        self.body.append('}}')

    ##############################################

    def visit_seealso(self, node: Element) -> None:
        self.body.append(BLANKLINE)
        self.body.append(
            r'\begin{sphinxseealso}{%s:}' % admonitionlabels['seealso'] + CR
        )
        self.no_latex_floats += 1
        if self.table:
            self.table.has_problematic = True

    def depart_seealso(self, node: Element) -> None:
        self.body.append(BLANKLINE)
        self.body.append(r'\end{sphinxseealso}')
        self.body.append(BLANKLINE)
        self.no_latex_floats -= 1

    def visit_rubric(self, node: nodes.rubric) -> None:
        if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
            raise nodes.SkipNode
        tag = 'subsubsection'
        if 'heading-level' in node:
            level = node['heading-level']
            try:
                tag = self.sectionnames[self.top_sectionlevel - 1 + level]
            except Exception:
                logger.warning(
                    __('unsupported rubric heading level: %s'),
                    level,
                    type='latex',
                    location=node,
                )

        self.body.append(rf'\{tag}*{{')
        self.context.append('}' + CR)
        self.in_title = 1

    def depart_rubric(self, node: nodes.rubric) -> None:
        self.in_title = 0
        self.body.append(self.context.pop())

    def visit_footnote(self, node: Element) -> None:
        self.in_footnote += 1
        label = cast(nodes.label, node[0])
        if self.in_parsed_literal:
            self.body.append(r'\begin{footnote}[%s]' % label.astext())
        else:
            self.body.append('%' + CR)
            self.body.append(r'\begin{footnote}[%s]' % label.astext())
        if 'referred' in node:
            # TODO: in future maybe output a latex macro with backrefs here
            pass
        self.body.append(r'\sphinxAtStartFootnote' + CR)

    def depart_footnote(self, node: Element) -> None:
        if self.in_parsed_literal:
            self.body.append(r'\end{footnote}')
        else:
            self.body.append('%' + CR)
            self.body.append(r'\end{footnote}')
        self.in_footnote -= 1

    def visit_label(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_tabular_col_spec(self, node: Element) -> None:
        self.next_table_colspec = node['spec']
        raise nodes.SkipNode

    def visit_table(self, node: Element) -> None:
        if len(self.tables) == 1:
            assert self.table is not None
            if self.table.get_table_type() == 'longtable':
                raise UnsupportedError(
                    '%s:%s: longtable does not support nesting a table.'
                    % (self.curfilestack[-1], node.line or '')
                )
            # change type of parent table to tabular
            # see https://groups.google.com/d/msg/sphinx-users/7m3NeOBixeo/9LKP2B4WBQAJ
            self.table.has_problematic = True
        elif len(self.tables) > 2:
            raise UnsupportedError(
                '%s:%s: deeply nested tables are not implemented.'
                % (self.curfilestack[-1], node.line or '')
            )

        table = Table(node)
        self.tables.append(table)
        if table.colsep is None:
            table.colsep = '|' * (
                'booktabs' not in self.builder.config.latex_table_style
                and 'borderless' not in self.builder.config.latex_table_style
            )
        if self.next_table_colspec:
            table.colspec = '{%s}' % self.next_table_colspec + CR
            if '|' in table.colspec:
                table.styles.append('vlines')
                table.colsep = '|'
            else:
                table.styles.append('novlines')
                table.colsep = ''
            if 'colwidths-given' in node.get('classes', []):
                logger.info(
                    __(
                        'both tabularcolumns and :widths: option are given. '
                        ':widths: is ignored.'
                    ),
                    location=node,
                )
        self.next_table_colspec = None

    def depart_table(self, node: Element) -> None:
        assert self.table is not None
        labels = self.hypertarget_to(node)
        table_type = self.table.get_table_type()
        table = self.render(
            table_type + '.tex.jinja', {'table': self.table, 'labels': labels}
        )
        self.body.append(BLANKLINE)
        self.body.append(table)
        self.body.append(CR)

        self.tables.pop()

    def visit_colspec(self, node: Element) -> None:
        assert self.table is not None
        self.table.colcount += 1
        if 'colwidth' in node:
            self.table.colwidths.append(node['colwidth'])
        if 'stub' in node:
            self.table.stubs.append(self.table.colcount - 1)

    def depart_colspec(self, node: Element) -> None:
        pass

    def visit_tgroup(self, node: Element) -> None:
        pass

    def depart_tgroup(self, node: Element) -> None:
        pass

    def visit_thead(self, node: Element) -> None:
        assert self.table is not None
        # Redirect head output until header is finished.
        self.pushbody(self.table.header)

    def depart_thead(self, node: Element) -> None:
        if self.body and self.body[-1] == r'\sphinxhline':
            self.body.pop()
        self.popbody()

    def visit_tbody(self, node: Element) -> None:
        assert self.table is not None
        # Redirect body output until table is finished.
        self.pushbody(self.table.body)

    def depart_tbody(self, node: Element) -> None:
        if self.body and self.body[-1] == r'\sphinxhline':
            self.body.pop()
        self.popbody()

    def visit_row(self, node: Element) -> None:
        assert self.table is not None
        self.table.col = 0
        _colsep = self.table.colsep
        # fill columns if the row starts with the bottom of multirow cell
        while True:
            cell = self.table.cell(self.table.row, self.table.col)
            if cell is None:  # not a bottom of multirow cell
                break
            # a bottom of multirow cell
            self.table.col += cell.width
            if cell.col:
                self.body.append('&')
            if cell.width == 1:
                # insert suitable strut for equalizing row heights in given multirow
                self.body.append(r'\sphinxtablestrut{%d}' % cell.cell_id)
            else:  # use \multicolumn for wide multirow cell
                self.body.append(
                    r'\multicolumn{%d}{%sl%s}{\sphinxtablestrut{%d}}'
                    % (cell.width, _colsep, _colsep, cell.cell_id)
                )

    def depart_row(self, node: Element) -> None:
        assert self.table is not None
        self.body.append(r'\\' + CR)
        cells = [self.table.cell(self.table.row, i) for i in range(self.table.colcount)]
        underlined = [
            cell.row + cell.height == self.table.row + 1  # type: ignore[union-attr]
            for cell in cells
        ]
        if all(underlined):
            self.body.append(r'\sphinxhline')
        else:
            i = 0
            underlined.extend([False])  # sentinel
            if underlined[0] is False:
                i = 1
                while i < self.table.colcount and underlined[i] is False:
                    if cells[i - 1].cell_id != cells[i].cell_id:  # type: ignore[union-attr]
                        self.body.append(r'\sphinxvlinecrossing{%d}' % i)
                    i += 1
            while i < self.table.colcount:
                # each time here underlined[i] is True
                j = underlined[i:].index(False)
                self.body.append(r'\sphinxcline{%d-%d}' % (i + 1, i + j))
                i += j
                i += 1
                while i < self.table.colcount and underlined[i] is False:
                    if cells[i - 1].cell_id != cells[i].cell_id:  # type: ignore[union-attr]
                        self.body.append(r'\sphinxvlinecrossing{%d}' % i)
                    i += 1
            self.body.append(r'\sphinxfixclines{%d}' % self.table.colcount)
        self.table.row += 1

    def visit_entry(self, node: Element) -> None:
        assert self.table is not None
        if self.table.col > 0:
            self.body.append('&')
        self.table.add_cell(node.get('morerows', 0) + 1, node.get('morecols', 0) + 1)
        cell = self.table.cell()
        assert cell is not None
        context = ''
        _colsep = self.table.colsep
        if cell.width > 1:
            if self.config.latex_use_latex_multicolumn:
                if self.table.col == 0:
                    self.body.append(
                        r'\multicolumn{%d}{%sl%s}{%%' % (cell.width, _colsep, _colsep)
                        + CR
                    )
                else:
                    self.body.append(
                        r'\multicolumn{%d}{l%s}{%%' % (cell.width, _colsep) + CR
                    )
                context = '}%' + CR
            else:
                self.body.append(r'\sphinxstartmulticolumn{%d}%%' % cell.width + CR)
                context = r'\sphinxstopmulticolumn' + CR
        if cell.height > 1:
            # \sphinxmultirow 2nd arg "cell_id" will serve as id for LaTeX macros as well
            self.body.append(
                r'\sphinxmultirow{%d}{%d}{%%' % (cell.height, cell.cell_id) + CR
            )
            context = '}%' + CR + context
        if cell.width > 1 or cell.height > 1:
            self.body.append(
                r'\begin{varwidth}[t]{\sphinxcolwidth{%d}{%d}}'
                % (cell.width, self.table.colcount)
                + CR
            )
            context = (
                r'\par' + CR + r'\vskip-\baselineskip'
                r'\vbox{\hbox{\strut}}\end{varwidth}%' + CR + context
            )
            self.needs_linetrimming = 1
        if len(list(node.findall(nodes.paragraph))) >= 2:
            self.table.has_oldproblematic = True
        if (
            isinstance(node.parent.parent, nodes.thead)
            or (cell.col in self.table.stubs)
        ):  # fmt: skip
            if (
                len(node) == 1
                and isinstance(node[0], nodes.paragraph)
                and node.astext() == ''
            ):
                pass
            else:
                self.body.append(r'\sphinxstyletheadfamily ')
        if self.needs_linetrimming:
            self.pushbody([])
        self.context.append(context)

    def depart_entry(self, node: Element) -> None:
        if self.needs_linetrimming:
            self.needs_linetrimming = 0
            body = self.popbody()

            # Remove empty lines from top of merged cell
            while body and body[0] == CR:
                body.pop(0)
            self.body.extend(body)

        self.body.append(self.context.pop())

        assert self.table is not None
        cell = self.table.cell()
        assert cell is not None
        self.table.col += cell.width
        _colsep = self.table.colsep

        # fill columns if next ones are a bottom of wide-multirow cell
        while True:
            nextcell = self.table.cell()
            if nextcell is None:  # not a bottom of multirow cell
                break
            # a bottom part of multirow cell
            self.body.append('&')
            if nextcell.width == 1:
                # insert suitable strut for equalizing row heights in multirow
                # they also serve to clear colour panels which would hide the text
                self.body.append(r'\sphinxtablestrut{%d}' % nextcell.cell_id)
            else:
                # use \multicolumn for not first row of wide multirow cell
                self.body.append(
                    r'\multicolumn{%d}{l%s}{\sphinxtablestrut{%d}}'
                    % (nextcell.width, _colsep, nextcell.cell_id)
                )
            self.table.col += nextcell.width

    def visit_acks(self, node: Element) -> None:
        # this is a list in the source, but should be rendered as a
        # comma-separated list here
        bullet_list = cast(nodes.bullet_list, node[0])
        list_items = cast(Iterable[nodes.list_item], bullet_list)
        self.body.append(BLANKLINE)
        self.body.append(', '.join(n.astext() for n in list_items) + '.')
        self.body.append(BLANKLINE)
        raise nodes.SkipNode

    def visit_bullet_list(self, node: Element) -> None:
        if not self.compact_list:
            self.body.append(r'\begin{itemize}' + CR)
        if self.table:
            self.table.has_problematic = True

    def depart_bullet_list(self, node: Element) -> None:
        if not self.compact_list:
            self.body.append(r'\end{itemize}' + CR)

    def visit_enumerated_list(self, node: Element) -> None:
        def get_enumtype(node: Element) -> str:
            enumtype = node.get('enumtype', 'arabic')
            if 'alpha' in enumtype and (node.get('start', 0) + len(node)) > 26:
                # fallback to arabic if alphabet counter overflows
                enumtype = 'arabic'

            return enumtype

        def get_nested_level(node: Element) -> int:
            if node is None:
                return 0
            elif isinstance(node, nodes.enumerated_list):
                return get_nested_level(node.parent) + 1
            else:
                return get_nested_level(node.parent)

        enum = 'enum%s' % toRoman(get_nested_level(node)).lower()
        enumnext = 'enum%s' % toRoman(get_nested_level(node) + 1).lower()
        style = ENUMERATE_LIST_STYLE.get(get_enumtype(node))
        prefix = node.get('prefix', '')
        suffix = node.get('suffix', '.')

        self.body.append(r'\begin{enumerate}' + CR)
        self.body.append(
            r'\sphinxsetlistlabels{%s}{%s}{%s}{%s}{%s}%%'
            % (style, enum, enumnext, prefix, suffix)
            + CR
        )
        if 'start' in node:
            self.body.append(r'\setcounter{%s}{%d}' % (enum, node['start'] - 1) + CR)
        if self.table:
            self.table.has_problematic = True

    def depart_enumerated_list(self, node: Element) -> None:
        self.body.append(r'\end{enumerate}' + CR)

    def visit_list_item(self, node: Element) -> None:
        # Append "{}" in case the next character is "[", which would break
        # LaTeX's list environment (no numbering and the "[" is not printed).
        self.body.append(r'\item {} ')

    def depart_list_item(self, node: Element) -> None:
        self.body.append(CR)

    def visit_definition_list(self, node: Element) -> None:
        self.body.append(r'\begin{description}' + CR)
        if self.table:
            self.table.has_problematic = True

    def depart_definition_list(self, node: Element) -> None:
        self.body.append(r'\end{description}' + CR)

    def visit_definition_list_item(self, node: Element) -> None:
        pass

    def depart_definition_list_item(self, node: Element) -> None:
        pass

    def visit_term(self, node: Element) -> None:
        self.in_term += 1
        ctx = ''
        if node.get('ids'):
            ctx = r'\phantomsection'
            for node_id in node['ids']:
                ctx += self.hypertarget(node_id, anchor=False)
        ctx += r'}'
        self.body.append(r'\sphinxlineitem{')
        self.context.append(ctx)

    def depart_term(self, node: Element) -> None:
        self.body.append(self.context.pop())
        self.in_term -= 1

    def visit_classifier(self, node: Element) -> None:
        self.body.append('{[}')

    def depart_classifier(self, node: Element) -> None:
        self.body.append('{]}')

    def visit_definition(self, node: Element) -> None:
        pass

    def depart_definition(self, node: Element) -> None:
        self.body.append(CR)

    def visit_field_list(self, node: Element) -> None:
        self.body.append(r'\begin{quote}\begin{description}' + CR)
        if self.table:
            self.table.has_problematic = True

    def depart_field_list(self, node: Element) -> None:
        self.body.append(r'\end{description}\end{quote}' + CR)

    def visit_field(self, node: Element) -> None:
        pass

    def depart_field(self, node: Element) -> None:
        pass

    visit_field_name = visit_term
    depart_field_name = depart_term

    visit_field_body = visit_definition
    depart_field_body = depart_definition

    def visit_paragraph(self, node: Element) -> None:
        index = node.parent.index(node)
        if (
            index > 0
            and isinstance(node.parent, nodes.compound)
            and not isinstance(node.parent[index - 1], nodes.paragraph)
            and not isinstance(node.parent[index - 1], nodes.compound)
        ):
            # insert blank line, if the paragraph follows a non-paragraph node in a compound
            self.body.append(r'\noindent' + CR)
        elif index == 1 and isinstance(node.parent, nodes.footnote | footnotetext):
            # don't insert blank line, if the paragraph is second child of a footnote
            # (first one is label node)
            pass
        else:
            # the \sphinxAtStartPar is to allow hyphenation of first word of
            # a paragraph in narrow contexts such as in a table cell
            # added as two items (cf. line trimming in depart_entry())
            self.body.extend([CR, r'\sphinxAtStartPar' + CR])

    def depart_paragraph(self, node: Element) -> None:
        self.body.append(CR)

    def visit_centered(self, node: Element) -> None:
        self.body.append(CR + r'\begin{center}')
        if self.table:
            self.table.has_problematic = True

    def depart_centered(self, node: Element) -> None:
        self.body.append(CR + r'\end{center}')

    def visit_hlist(self, node: Element) -> None:
        self.compact_list += 1
        ncolumns = node['ncolumns']
        if self.compact_list > 1:
            self.body.append(r'\setlength{\multicolsep}{0pt}' + CR)
        self.body.append(r'\begin{multicols}{' + ncolumns + r'}\raggedright' + CR)
        self.body.append(
            r'\begin{itemize}\setlength{\itemsep}{0pt}'
            r'\setlength{\parskip}{0pt}' + CR
        )
        if self.table:
            self.table.has_problematic = True

    def depart_hlist(self, node: Element) -> None:
        self.compact_list -= 1
        self.body.append(r'\end{itemize}\raggedcolumns\end{multicols}' + CR)

    def visit_hlistcol(self, node: Element) -> None:
        pass

    def depart_hlistcol(self, node: Element) -> None:
        # \columnbreak would guarantee same columns as in html output.  But
        # some testing with long items showed that columns may be too uneven.
        # And in case only of short items, the automatic column breaks should
        # match the ones pre-computed by the hlist() directive.
        # self.body.append(r'\columnbreak\n')
        pass

    def latex_image_length(self, width_str: str, scale: int = 100) -> str | None:
        try:
            return rstdim_to_latexdim(width_str, scale)
        except ValueError:
            logger.warning(__('dimension unit %s is invalid. Ignored.'), width_str)
            return None

    def is_inline(self, node: Element) -> bool:
        """Check whether a node represents an inline element."""
        return isinstance(node.parent, nodes.TextElement)

    def visit_image(self, node: Element) -> None:
        pre: list[str] = []  # in reverse order
        post: list[str] = []
        include_graphics_options = []
        has_hyperlink = isinstance(node.parent, nodes.reference)
        if has_hyperlink:
            is_inline = self.is_inline(node.parent)
        else:
            is_inline = self.is_inline(node)
        if 'width' in node:
            if 'scale' in node:
                w = self.latex_image_length(node['width'], node['scale'])
            else:
                w = self.latex_image_length(node['width'])
            if w:
                include_graphics_options.append('width=%s' % w)
        if 'height' in node:
            if 'scale' in node:
                h = self.latex_image_length(node['height'], node['scale'])
            else:
                h = self.latex_image_length(node['height'])
            if h:
                include_graphics_options.append('height=%s' % h)
        if 'scale' in node:
            if not include_graphics_options:
                # if no "width" nor "height", \sphinxincludegraphics will fit
                # to the available text width if oversized after rescaling.
                include_graphics_options.append(
                    'scale=%s' % (float(node['scale']) / 100.0)
                )
        if 'align' in node:
            align_prepost = {
                # By default latex aligns the top of an image.
                (1, 'top'): ('', ''),
                (1, 'middle'): (r'\raisebox{-0.5\height}{', '}'),
                (1, 'bottom'): (r'\raisebox{-\height}{', '}'),
                (0, 'center'): (r'{\hspace*{\fill}', r'\hspace*{\fill}}'),
                # These 2 don't exactly do the right thing.  The image should
                # be floated alongside the paragraph.  See
                # https://www.w3.org/TR/html4/struct/objects.html#adef-align-IMG
                (0, 'left'): ('{', r'\hspace*{\fill}}'),
                (0, 'right'): (r'{\hspace*{\fill}', '}'),
            }
            try:
                pre.append(align_prepost[is_inline, node['align']][0])
                post.append(align_prepost[is_inline, node['align']][1])
            except KeyError:
                pass
        if self.in_parsed_literal:
            pre.append(r'{\sphinxunactivateextrasandspace ')
            post.append('}')
        if not is_inline and not has_hyperlink:
            pre.append(CR + r'\noindent')
            post.append(CR)
        pre.reverse()
        if node['uri'] in self.builder.images:
            uri = self.builder.images[node['uri']]
        else:
            # missing image!
            if self.ignore_missing_images:
                return
            uri = node['uri']
        if uri.find('://') != -1:
            # ignore remote images
            return
        self.body.extend(pre)
        options = ''
        if include_graphics_options:
            options = '[%s]' % ','.join(include_graphics_options)
        base, ext = path.splitext(uri)

        if self.in_title and base:
            # Lowercase tokens forcely because some fncychap themes capitalize
            # the options of \sphinxincludegraphics unexpectedly (ex. WIDTH=...).
            cmd = rf'\lowercase{{\sphinxincludegraphics{options}}}{{{{{base}}}{ext}}}'
        else:
            cmd = rf'\sphinxincludegraphics{options}{{{{{base}}}{ext}}}'
        # escape filepath for includegraphics, https://tex.stackexchange.com/a/202714/41112
        if '#' in base:
            cmd = r'{\catcode`\#=12' + cmd + '}'
        self.body.append(cmd)
        self.body.extend(post)

    def depart_image(self, node: Element) -> None:
        pass

    def visit_figure(self, node: Element) -> None:
        align = self.elements['figure_align']
        if self.no_latex_floats:
            align = 'H'
        if self.table:
            # Blank line is needed if text precedes
            self.body.append(BLANKLINE)
            # TODO: support align option
            if 'width' in node:
                length = self.latex_image_length(node['width'])
                if length:
                    self.body.append(r'\begin{sphinxfigure-in-table}[%s]' % length + CR)
                    self.body.append(r'\centering' + CR)
            else:
                self.body.append(r'\begin{sphinxfigure-in-table}' + CR)
                self.body.append(r'\centering' + CR)
            if any(isinstance(child, nodes.caption) for child in node):
                self.body.append(r'\capstart')
            self.context.append(r'\end{sphinxfigure-in-table}\relax' + CR)
        elif node.get('align', '') in ('left', 'right'):
            length = None
            if 'width' in node:
                length = self.latex_image_length(node['width'])
            elif isinstance(node[0], nodes.image) and 'width' in node[0]:
                length = self.latex_image_length(node[0]['width'])
            # Insert a blank line to prevent an infinite loop
            # https://github.com/sphinx-doc/sphinx/issues/7059
            self.body.append(BLANKLINE)
            self.body.append(
                r'\begin{wrapfigure}{%s}{%s}'
                % ('r' if node['align'] == 'right' else 'l', length or '0pt')
                + CR
            )
            self.body.append(r'\centering')
            self.context.append(
                r'\end{wrapfigure}'
                + BLANKLINE
                + r'\mbox{}\par\vskip-\dimexpr\baselineskip+\parskip\relax'
                + CR
            )  # avoid disappearance if no text next issues/11079
        elif self.in_minipage:
            self.body.append(CR + r'\begin{center}')
            self.context.append(r'\end{center}' + CR)
        else:
            self.body.append(CR + r'\begin{figure}[%s]' % align + CR)
            self.body.append(r'\centering' + CR)
            if any(isinstance(child, nodes.caption) for child in node):
                self.body.append(r'\capstart' + CR)
            self.context.append(r'\end{figure}' + CR)

    def depart_figure(self, node: Element) -> None:
        self.body.append(self.context.pop())

    def visit_caption(self, node: Element) -> None:
        self.in_caption += 1
        if isinstance(node.parent, captioned_literal_block):
            self.body.append(r'\sphinxSetupCaptionForVerbatim{')
        elif self.in_minipage and isinstance(node.parent, nodes.figure):
            self.body.append(r'\captionof{figure}{')
        elif self.table and node.parent.tagname == 'figure':
            self.body.append(r'\sphinxfigcaption{')
        else:
            self.body.append(r'\caption{')

    def depart_caption(self, node: Element) -> None:
        self.body.append('}')
        if isinstance(node.parent, nodes.figure):
            labels = self.hypertarget_to(node.parent)
            self.body.append(labels)
        self.in_caption -= 1

    def visit_legend(self, node: Element) -> None:
        self.body.append(CR + r'\begin{sphinxlegend}')

    def depart_legend(self, node: Element) -> None:
        self.body.append(r'\end{sphinxlegend}' + CR)

    def visit_admonition(self, node: Element) -> None:
        self.body.append(CR + r'\begin{sphinxadmonition}{note}')
        self.no_latex_floats += 1
        if self.table:
            self.table.has_problematic = True

    def depart_admonition(self, node: Element) -> None:
        self.body.append(r'\end{sphinxadmonition}' + CR)
        self.no_latex_floats -= 1

    def _visit_named_admonition(self, node: Element) -> None:
        label = admonitionlabels[node.tagname]
        self.body.append(
            CR + r'\begin{sphinxadmonition}{%s}{%s:}' % (node.tagname, label)
        )
        self.no_latex_floats += 1
        if self.table:
            self.table.has_problematic = True

    def _depart_named_admonition(self, node: Element) -> None:
        self.body.append(r'\end{sphinxadmonition}' + CR)
        self.no_latex_floats -= 1

    visit_attention = _visit_named_admonition
    depart_attention = _depart_named_admonition
    visit_caution = _visit_named_admonition
    depart_caution = _depart_named_admonition
    visit_danger = _visit_named_admonition
    depart_danger = _depart_named_admonition
    visit_error = _visit_named_admonition
    depart_error = _depart_named_admonition
    visit_hint = _visit_named_admonition
    depart_hint = _depart_named_admonition
    visit_important = _visit_named_admonition
    depart_important = _depart_named_admonition
    visit_note = _visit_named_admonition
    depart_note = _depart_named_admonition
    visit_tip = _visit_named_admonition
    depart_tip = _depart_named_admonition
    visit_warning = _visit_named_admonition
    depart_warning = _depart_named_admonition

    def visit_versionmodified(self, node: Element) -> None:
        pass

    def depart_versionmodified(self, node: Element) -> None:
        pass

    def visit_target(self, node: Element) -> None:
        def add_target(id: str) -> None:
            # indexing uses standard LaTeX index markup, so the targets
            # will be generated differently
            if id.startswith('index-'):
                return

            # equations also need no extra blank line nor hypertarget
            # TODO: fix this dependency on mathbase extension internals
            if id.startswith('equation-'):
                return

            # insert blank line, if the target follows a paragraph node
            index = node.parent.index(node)
            if index > 0 and isinstance(node.parent[index - 1], nodes.paragraph):
                self.body.append(CR)

            # do not generate \phantomsection in \section{}
            anchor = not self.in_title
            self.body.append(self.hypertarget(id, anchor=anchor))

        # skip if visitor for next node supports hyperlink
        next_node: Node = node
        while isinstance(next_node, nodes.target):
            next_node = next_node.next_node(ascend=True)

        domain = self.builder.env.domains.standard_domain
        if isinstance(next_node, HYPERLINK_SUPPORT_NODES):
            return
        if (
            domain.get_enumerable_node_type(next_node)
            and domain.get_numfig_title(next_node)
        ):  # fmt: skip
            return

        if 'refuri' in node:
            return
        if 'anonymous' in node:
            return
        if node.get('refid'):
            prev_node = get_prev_node(node)
            if (
                isinstance(prev_node, nodes.reference)
                and node['refid'] == prev_node['refid']
            ):
                # a target for a hyperlink reference having alias
                pass
            else:
                add_target(node['refid'])
        # Temporary fix for https://github.com/sphinx-doc/sphinx/issues/11093
        # TODO: investigate if a more elegant solution exists (see comments of #11093)
        if node.get('ismod', False):
            # Detect if the previous nodes are label targets. If so, remove
            # the refid thereof from node['ids'] to avoid duplicated ids.
            def has_dup_label(sib: Node | None) -> bool:
                return isinstance(sib, nodes.target) and sib.get('refid') in node['ids']

            prev = get_prev_node(node)
            if has_dup_label(prev):
                ids = node['ids'][:]  # copy to avoid side-effects
                while has_dup_label(prev):
                    ids.remove(prev['refid'])  # type: ignore[index]
                    prev = get_prev_node(prev)  # type: ignore[arg-type]
            else:
                ids = iter(node['ids'])  # read-only iterator
        else:
            ids = iter(node['ids'])  # read-only iterator

        for id in ids:
            add_target(id)

    def depart_target(self, node: Element) -> None:
        pass

    def visit_attribution(self, node: Element) -> None:
        self.body.append(CR + r'\begin{flushright}' + CR)
        self.body.append('---')

    def depart_attribution(self, node: Element) -> None:
        self.body.append(CR + r'\end{flushright}' + CR)

    def visit_index(self, node: Element) -> None:
        def escape(value: str) -> str:
            value = self.encode(value)
            value = value.replace(r'\{', r'\sphinxleftcurlybrace{}')
            value = value.replace(r'\}', r'\sphinxrightcurlybrace{}')
            value = value.replace('"', '""')
            value = value.replace('@', '"@')
            value = value.replace('!', '"!')
            value = value.replace('|', r'\textbar{}')
            return value

        def style(string: str) -> str:
            match = EXTRA_RE.match(string)
            if match:
                return match.expand(r'\\spxentry{\1}\\spxextra{\2}')
            else:
                return r'\spxentry{%s}' % string

        if not node.get('inline', True):
            self.body.append(CR)
        entries = node['entries']
        for type, string, _tid, ismain, _key in entries:
            m = ''
            if ismain:
                m = '|spxpagem'
            try:
                parts = tuple(map(escape, split_index_msg(type, string)))
                styled = tuple(map(style, parts))
                if type == 'single':
                    try:
                        p1, p2 = parts
                        P1, P2 = styled
                        self.body.append(rf'\index{{{p1}@{P1}!{p2}@{P2}{m}}}')
                    except ValueError:
                        (p,) = parts
                        (P,) = styled
                        self.body.append(rf'\index{{{p}@{P}{m}}}')
                elif type == 'pair':
                    p1, p2 = parts
                    P1, P2 = styled
                    self.body.append(
                        rf'\index{{{p1}@{P1}!{p2}@{P2}{m}}}'
                        rf'\index{{{p2}@{P2}!{p1}@{P1}{m}}}'
                    )
                elif type == 'triple':
                    p1, p2, p3 = parts
                    P1, P2, P3 = styled
                    self.body.append(
                        rf'\index{{{p1}@{P1}!{p2} {p3}@{P2} {P3}{m}}}'
                        rf'\index{{{p2}@{P2}!{p3}, {p1}@{P3}, {P1}{m}}}'
                        rf'\index{{{p3}@{P3}!{p1} {p2}@{P1} {P2}{m}}}'
                    )
                elif type in {'see', 'seealso'}:
                    p1, p2 = parts
                    P1, _P2 = styled
                    self.body.append(rf'\index{{{p1}@{P1}|see{{{p2}}}}}')
                else:
                    logger.warning(__('unknown index entry type %s found'), type)
            except ValueError as err:
                logger.warning(str(err))
        if not node.get('inline', True):
            self.body.append(r'\ignorespaces ')
        raise nodes.SkipNode

    def visit_raw(self, node: Element) -> None:
        if not self.is_inline(node):
            self.body.append(CR)
        if 'latex' in node.get('format', '').split():
            self.body.append(node.astext())
        if not self.is_inline(node):
            self.body.append(CR)
        raise nodes.SkipNode

    def visit_reference(self, node: Element) -> None:
        if not self.in_title:
            for id in node.get('ids'):
                anchor = not self.in_caption
                self.body += self.hypertarget(id, anchor=anchor)
        if not self.is_inline(node):
            self.body.append(CR)
        uri = node.get('refuri', '')
        if not uri and node.get('refid'):
            uri = '%' + self.curfilestack[-1] + '#' + node['refid']
        if self.in_title or not uri:
            self.context.append('')
        elif uri.startswith('#'):
            # references to labels in the same document
            id = self.curfilestack[-1] + ':' + uri[1:]
            self.body.append(self.hyperlink(id))
            self.body.append(r'\sphinxsamedocref{')
            if self.config.latex_show_pagerefs and not self.in_production_list:
                self.context.append('}}} (%s)' % self.hyperpageref(id))
            else:
                self.context.append('}}}')
        elif uri.startswith('%'):
            # references to documents or labels inside documents
            hashindex = uri.find('#')
            if hashindex == -1:
                # reference to the document
                id = uri[1:] + '::doc'
            else:
                # reference to a label
                id = uri[1:].replace('#', ':')
            self.body.append(self.hyperlink(id))
            if (
                len(node)
                and isinstance(node[0], nodes.Element)
                and 'std-term' in node[0].get('classes', [])
            ):
                # don't add a pageref for glossary terms
                self.context.append('}}}')
                # mark up as termreference
                self.body.append(r'\sphinxtermref{')
            else:
                self.body.append(r'\sphinxcrossref{')
                if self.config.latex_show_pagerefs and not self.in_production_list:
                    self.context.append('}}} (%s)' % self.hyperpageref(id))
                else:
                    self.context.append('}}}')
        else:
            if len(node) == 1 and uri == node[0]:
                if node.get('nolinkurl'):
                    self.body.append(r'\sphinxnolinkurl{%s}' % self.encode_uri(uri))
                else:
                    self.body.append(r'\sphinxurl{%s}' % self.encode_uri(uri))
                raise nodes.SkipNode
            else:
                self.body.append(r'\sphinxhref{%s}{' % self.encode_uri(uri))
                self.context.append('}')

    def depart_reference(self, node: Element) -> None:
        self.body.append(self.context.pop())
        if not self.is_inline(node):
            self.body.append(CR)

    def visit_number_reference(self, node: Element) -> None:
        if node.get('refid'):
            id = self.curfilestack[-1] + ':' + node['refid']
        else:
            id = node.get('refuri', '')[1:].replace('#', ':')

        title = self.escape(node.get('title', '%s')).replace(r'\%s', '%s')
        if r'\{name\}' in title or r'\{number\}' in title:
            # new style format (cf. "Fig.%{number}")
            title = title.replace(r'\{name\}', '{name}').replace(
                r'\{number\}', '{number}'
            )
            text = escape_abbr(title).format(
                name=r'\nameref{%s}' % self.idescape(id),
                number=r'\ref{%s}' % self.idescape(id),
            )
        else:
            # old style format (cf. "Fig.%{number}")
            text = escape_abbr(title) % (r'\ref{%s}' % self.idescape(id))
        hyperref = rf'\hyperref[{self.idescape(id)}]{{{text}}}'
        self.body.append(hyperref)

        raise nodes.SkipNode

    def visit_download_reference(self, node: Element) -> None:
        pass

    def depart_download_reference(self, node: Element) -> None:
        pass

    def visit_pending_xref(self, node: Element) -> None:
        pass

    def depart_pending_xref(self, node: Element) -> None:
        pass

    def visit_emphasis(self, node: Element) -> None:
        self.body.append(r'\sphinxstyleemphasis{')

    def depart_emphasis(self, node: Element) -> None:
        self.body.append('}')

    def visit_literal_emphasis(self, node: Element) -> None:
        self.body.append(r'\sphinxstyleliteralemphasis{\sphinxupquote{')

    def depart_literal_emphasis(self, node: Element) -> None:
        self.body.append('}}')

    def visit_strong(self, node: Element) -> None:
        self.body.append(r'\sphinxstylestrong{')

    def depart_strong(self, node: Element) -> None:
        self.body.append('}')

    def visit_literal_strong(self, node: Element) -> None:
        self.body.append(r'\sphinxstyleliteralstrong{\sphinxupquote{')

    def depart_literal_strong(self, node: Element) -> None:
        self.body.append('}}')

    def visit_abbreviation(self, node: Element) -> None:
        abbr = node.astext()
        self.body.append(r'\sphinxstyleabbreviation{')
        # spell out the explanation once
        if node.hasattr('explanation') and abbr not in self.handled_abbrs:
            self.context.append('} (%s)' % self.encode(node['explanation']))
            self.handled_abbrs.add(abbr)
        else:
            self.context.append('}')

    def depart_abbreviation(self, node: Element) -> None:
        self.body.append(self.context.pop())

    def visit_manpage(self, node: Element) -> None:
        return self.visit_literal_emphasis(node)

    def depart_manpage(self, node: Element) -> None:
        return self.depart_literal_emphasis(node)

    def visit_title_reference(self, node: Element) -> None:
        self.body.append(r'\sphinxtitleref{')

    def depart_title_reference(self, node: Element) -> None:
        self.body.append('}')

    def visit_thebibliography(self, node: Element) -> None:
        citations = cast(Iterable[nodes.citation], node)
        labels = (cast(nodes.label, citation[0]) for citation in citations)
        longest_label = max((label.astext() for label in labels), key=len)
        if len(longest_label) > MAX_CITATION_LABEL_LENGTH:
            # adjust max width of citation labels not to break the layout
            longest_label = longest_label[:MAX_CITATION_LABEL_LENGTH]

        self.body.append(
            CR + r'\begin{sphinxthebibliography}{%s}' % self.encode(longest_label) + CR
        )

    def depart_thebibliography(self, node: Element) -> None:
        self.body.append(r'\end{sphinxthebibliography}' + CR)

    def visit_citation(self, node: Element) -> None:
        label = cast(nodes.label, node[0])
        self.body.append(
            rf'\bibitem[{self.encode(label.astext())}]'
            rf'{{{node["docname"]}:{node["ids"][0]}}}'
        )

    def depart_citation(self, node: Element) -> None:
        pass

    def visit_citation_reference(self, node: Element) -> None:
        if self.in_title:
            pass
        else:
            self.body.append(rf'\sphinxcite{{{node["docname"]}:{node["refname"]}}}')
            raise nodes.SkipNode

    def depart_citation_reference(self, node: Element) -> None:
        pass

    def visit_literal(self, node: Element) -> None:
        if self.in_title:
            self.body.append(r'\sphinxstyleliteralintitle{\sphinxupquote{')
            return
        elif 'kbd' in node['classes']:
            self.body.append(r'\sphinxkeyboard{\sphinxupquote{')
            return
        lang = node.get('language', None)
        if 'code' not in node['classes'] or not lang:
            self.body.append(r'\sphinxcode{\sphinxupquote{')
            return

        opts = self.config.highlight_options.get(lang, {})
        hlcode = self.highlighter.highlight_block(
            node.astext(), lang, opts=opts, location=node, nowrap=True
        )
        self.body.append(
            r'\sphinxcode{\sphinxupquote{%' + CR + hlcode.rstrip() + '%' + CR + '}}'
        )
        raise nodes.SkipNode

    def depart_literal(self, node: Element) -> None:
        self.body.append('}}')

    def visit_footnote_reference(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_footnotemark(self, node: Element) -> None:
        self.body.append(r'\sphinxfootnotemark[')

    def depart_footnotemark(self, node: Element) -> None:
        self.body.append(']')

    def visit_footnotetext(self, node: Element) -> None:
        label = cast(nodes.label, node[0])
        self.body.append('%' + CR)
        self.body.append(r'\begin{footnotetext}[%s]' % label.astext())
        self.body.append(r'\sphinxAtStartFootnote' + CR)

    def depart_footnotetext(self, node: Element) -> None:
        # the \ignorespaces in particular for after table header use
        self.body.append('%' + CR)
        self.body.append(r'\end{footnotetext}\ignorespaces ')

    def visit_captioned_literal_block(self, node: Element) -> None:
        pass

    def depart_captioned_literal_block(self, node: Element) -> None:
        pass

    def visit_literal_block(self, node: Element) -> None:
        if node.rawsource != node.astext():
            # most probably a parsed-literal block -- don't highlight
            self.in_parsed_literal += 1
            self.body.append(r'\begin{sphinxalltt}' + CR)
        else:
            labels = self.hypertarget_to(node)
            if isinstance(node.parent, captioned_literal_block):
                labels += self.hypertarget_to(node.parent)
            if labels and not self.in_footnote:
                self.body.append(CR + r'\def\sphinxLiteralBlockLabel{' + labels + '}')

            lang = node.get('language', 'default')
            linenos = node.get('linenos', False)
            highlight_args = node.get('highlight_args', {})
            highlight_args['force'] = node.get('force', False)
            opts = self.config.highlight_options.get(lang, {})

            hlcode = self.highlighter.highlight_block(
                node.rawsource,
                lang,
                opts=opts,
                linenos=linenos,
                location=node,
                **highlight_args,
            )
            if self.in_footnote:
                self.body.append(CR + r'\sphinxSetupCodeBlockInFootnote')
                hlcode = hlcode.replace(r'\begin{Verbatim}', r'\begin{sphinxVerbatim}')
            # if in table raise verbatim flag to avoid "tabulary" environment
            # and opt for sphinxVerbatimintable to handle caption & long lines
            elif self.table:
                self.table.has_problematic = True
                self.table.has_verbatim = True
                hlcode = hlcode.replace(
                    r'\begin{Verbatim}', r'\begin{sphinxVerbatimintable}'
                )
            else:
                hlcode = hlcode.replace(r'\begin{Verbatim}', r'\begin{sphinxVerbatim}')
            # get consistent trailer
            hlcode = hlcode.rstrip()[:-14]  # strip \end{Verbatim}
            if self.table and not self.in_footnote:
                hlcode += r'\end{sphinxVerbatimintable}'
            else:
                hlcode += r'\end{sphinxVerbatim}'

            hllines = str(highlight_args.get('hl_lines', []))[1:-1]
            if hllines:
                self.body.append(CR + r'\fvset{hllines={, %s,}}%%' % hllines)
            self.body.append(CR + hlcode + CR)
            if hllines:
                self.body.append(r'\sphinxresetverbatimhllines' + CR)
            raise nodes.SkipNode

    def depart_literal_block(self, node: Element) -> None:
        self.body.append(CR + r'\end{sphinxalltt}' + CR)
        self.in_parsed_literal -= 1

    visit_doctest_block = visit_literal_block
    depart_doctest_block = depart_literal_block

    def visit_line(self, node: Element) -> None:
        self.body.append(r'\item[] ')

    def depart_line(self, node: Element) -> None:
        self.body.append(CR)

    def visit_line_block(self, node: Element) -> None:
        if isinstance(node.parent, nodes.line_block):
            self.body.append(r'\item[]' + CR)
            self.body.append(r'\begin{DUlineblock}{\DUlineblockindent}' + CR)
        else:
            self.body.append(CR + r'\begin{DUlineblock}{0em}' + CR)
        if self.table:
            self.table.has_problematic = True

    def depart_line_block(self, node: Element) -> None:
        self.body.append(r'\end{DUlineblock}' + CR)

    def visit_block_quote(self, node: Element) -> None:
        # If the block quote contains a single object and that object
        # is a list, then generate a list not a block quote.
        # This lets us indent lists.
        done = 0
        if len(node.children) == 1:
            child = node.children[0]
            if isinstance(child, nodes.bullet_list | nodes.enumerated_list):
                done = 1
        if not done:
            self.body.append(r'\begin{quote}' + CR)
            if self.table:
                self.table.has_problematic = True

    def depart_block_quote(self, node: Element) -> None:
        done = 0
        if len(node.children) == 1:
            child = node.children[0]
            if isinstance(child, nodes.bullet_list | nodes.enumerated_list):
                done = 1
        if not done:
            self.body.append(r'\end{quote}' + CR)

    # option node handling copied from docutils' latex writer

    def visit_option(self, node: Element) -> None:
        if self.context[-1]:
            # this is not the first option
            self.body.append(', ')

    def depart_option(self, node: Element) -> None:
        # flag that the first option is done.
        self.context[-1] += 1

    def visit_option_argument(self, node: Element) -> None:
        """The delimiter between an option and its argument."""
        self.body.append(node.get('delimiter', ' '))

    def depart_option_argument(self, node: Element) -> None:
        pass

    def visit_option_group(self, node: Element) -> None:
        self.body.append(r'\item [')
        # flag for first option
        self.context.append(0)

    def depart_option_group(self, node: Element) -> None:
        self.context.pop()  # the flag
        self.body.append('] ')

    def visit_option_list(self, node: Element) -> None:
        self.body.append(r'\begin{optionlist}{3cm}' + CR)
        if self.table:
            self.table.has_problematic = True

    def depart_option_list(self, node: Element) -> None:
        self.body.append(r'\end{optionlist}' + CR)

    def visit_option_list_item(self, node: Element) -> None:
        pass

    def depart_option_list_item(self, node: Element) -> None:
        pass

    def visit_option_string(self, node: Element) -> None:
        ostring = node.astext()
        self.body.append(self.encode(ostring))
        raise nodes.SkipNode

    def visit_description(self, node: Element) -> None:
        self.body.append(' ')

    def depart_description(self, node: Element) -> None:
        pass

    def visit_superscript(self, node: Element) -> None:
        self.body.append(r'$^{\text{')

    def depart_superscript(self, node: Element) -> None:
        self.body.append('}}$')

    def visit_subscript(self, node: Element) -> None:
        self.body.append(r'$_{\text{')

    def depart_subscript(self, node: Element) -> None:
        self.body.append('}}$')

    def visit_inline(self, node: Element) -> None:
        classes = node.get('classes', [])  # type: ignore[var-annotated]
        if classes == ['menuselection']:
            self.body.append(r'\sphinxmenuselection{')
            self.context.append('}')
        elif classes == ['guilabel']:
            self.body.append(r'\sphinxguilabel{')
            self.context.append('}')
        elif classes == ['accelerator']:
            self.body.append(r'\sphinxaccelerator{')
            self.context.append('}')
        elif classes and not self.in_title:
            self.body.append(r'\DUrole{' + r'}{\DUrole{'.join(classes) + '}{')
            self.context.append('}' * len(classes))
        else:
            self.context.append('')

    def depart_inline(self, node: Element) -> None:
        self.body.append(self.context.pop())

    def visit_generated(self, node: Element) -> None:
        pass

    def depart_generated(self, node: Element) -> None:
        pass

    def visit_compound(self, node: Element) -> None:
        pass

    def depart_compound(self, node: Element) -> None:
        pass

    def visit_container(self, node: Element) -> None:
        classes = node.get('classes', [])  # type: ignore[var-annotated]
        for c in classes:
            self.body.append('\n\\begin{sphinxuseclass}{%s}' % c)

    def depart_container(self, node: Element) -> None:
        classes = node.get('classes', [])  # type: ignore[var-annotated]
        for _c in classes:
            self.body.append('\n\\end{sphinxuseclass}')

    def visit_decoration(self, node: Element) -> None:
        pass

    def depart_decoration(self, node: Element) -> None:
        pass

    # docutils-generated elements that we don't support

    def visit_header(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_footer(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_docinfo(self, node: Element) -> None:
        raise nodes.SkipNode

    # text handling

    def encode(self, text: str) -> str:
        text = self.escape(text)
        if self.literal_whitespace:
            # Insert a blank before the newline, to avoid
            # ! LaTeX Error: There's no line here to end.
            text = text.replace(CR, r'~\\' + CR).replace(' ', '~')
        return text

    def encode_uri(self, text: str) -> str:
        # TODO: it is probably wrong that this uses texescape.escape()
        #       this must be checked against hyperref package exact dealings
        #       mainly, %, #, {, } and \ need escaping via a \ escape
        # in \href, the tilde is allowed and must be represented literally
        return (
            self.encode(text)
            .replace(r'\textasciitilde{}', '~')
            .replace(r'\sphinxhyphen{}', '-')
            .replace(r'\textquotesingle{}', "'")
        )

    def visit_Text(self, node: Text) -> None:
        text = self.encode(node.astext())
        self.body.append(text)

    def depart_Text(self, node: Text) -> None:
        pass

    def visit_comment(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_meta(self, node: Element) -> None:
        # only valid for HTML
        raise nodes.SkipNode

    def visit_system_message(self, node: Element) -> None:
        pass

    def depart_system_message(self, node: Element) -> None:
        self.body.append(CR)

    def visit_math(self, node: Element) -> None:
        if self.in_title:
            self.body.append(r'\protect\(%s\protect\)' % node.astext())
        else:
            self.body.append(r'\(%s\)' % node.astext())
        raise nodes.SkipNode

    def visit_math_block(self, node: Element) -> None:
        if node.get('label'):
            label = f"equation:{node['docname']}:{node['label']}"
        else:
            label = None

        if node.get('nowrap'):
            if label:
                self.body.append(r'\label{%s}' % label)
            self.body.append(node.astext())
        else:
            from sphinx.util.math import wrap_displaymath

            self.body.append(
                wrap_displaymath(node.astext(), label, self.config.math_number_all)
            )
        raise nodes.SkipNode

    def visit_math_reference(self, node: Element) -> None:
        label = f"equation:{node['docname']}:{node['target']}"
        eqref_format = self.config.math_eqref_format
        if eqref_format:
            try:
                ref = r'\ref{%s}' % label
                self.body.append(eqref_format.format(number=ref))
            except KeyError as exc:
                logger.warning(__('Invalid math_eqref_format: %r'), exc, location=node)
                self.body.append(r'\eqref{%s}' % label)
        else:
            self.body.append(r'\eqref{%s}' % label)

    def depart_math_reference(self, node: Element) -> None:
        pass


# FIXME: Workaround to avoid circular import
# refs: https://github.com/sphinx-doc/sphinx/issues/5433
from sphinx.builders.latex.nodes import (  # NoQA: E402  # isort:skip
    HYPERLINK_SUPPORT_NODES,
    captioned_literal_block,
    footnotetext,
)
