2267 lines
89 KiB
Python
2267 lines
89 KiB
Python
|
"""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, cast
|
||
|
|
||
|
from docutils import nodes, writers
|
||
|
|
||
|
from sphinx import addnodes, highlighting
|
||
|
from sphinx.domains.std import StandardDomain
|
||
|
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]
|
||
|
|
||
|
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):
|
||
|
|
||
|
supported = ('sphinxlatex',)
|
||
|
|
||
|
settings_spec = ('LaTeX writer options', '', (
|
||
|
('Document name', ['--docname'], {'default': ''}),
|
||
|
('Document class', ['--docclass'], {'default': 'manual'}),
|
||
|
('Author', ['--author'], {'default': ''}),
|
||
|
))
|
||
|
settings_defaults: dict[str, Any] = {}
|
||
|
|
||
|
theme: Theme
|
||
|
|
||
|
def __init__(self, builder: LaTeXBuilder) -> None:
|
||
|
super().__init__()
|
||
|
self.builder = builder
|
||
|
|
||
|
def translate(self) -> None:
|
||
|
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:
|
||
|
"""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
|
||
|
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[:]
|
||
|
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.append('mathnumfig')
|
||
|
|
||
|
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_t', self.elements)
|
||
|
|
||
|
def hypertarget(self, id: str, withdoc: bool = True, anchor: bool = True) -> str:
|
||
|
if withdoc:
|
||
|
id = self.curfilestack[-1] + ':' + id
|
||
|
return (r'\phantomsection' if anchor else '') + r'\label{%s}' % self.idescape(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:
|
||
|
return r'\detokenize{%s}' % str(id).translate(tex_replace_map).\
|
||
|
encode('ascii', 'backslashreplace').decode('ascii').\
|
||
|
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 fr'{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
|
||
|
indices_config = self.config.latex_domain_indices
|
||
|
if indices_config:
|
||
|
for domain in self.builder.env.domains.values():
|
||
|
for indexcls in domain.indices:
|
||
|
indexname = f'{domain.name}-{indexcls.name}'
|
||
|
if isinstance(indices_config, list):
|
||
|
if indexname not in indices_config:
|
||
|
continue
|
||
|
content, collapsed = indexcls(domain).generate(
|
||
|
self.builder.docnames)
|
||
|
if not content:
|
||
|
continue
|
||
|
ret.append(r'\renewcommand{\indexname}{%s}' % indexcls.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)
|
||
|
|
||
|
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
|
||
|
self.body.append(CR + r'\begin{sphinxShadowBox}' + CR)
|
||
|
|
||
|
def depart_topic(self, node: Element) -> None:
|
||
|
self.in_minipage = 0
|
||
|
self.body.append(r'\end{sphinxShadowBox}' + CR)
|
||
|
visit_sidebar = visit_topic
|
||
|
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):
|
||
|
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(fr'\{self.sectionnames[self.sectionlevel]}{short}{{')
|
||
|
except IndexError:
|
||
|
# just use "subparagraph", it's not numbered anyway
|
||
|
self.body.append(fr'\{self.sectionnames[-1]}{short}{{')
|
||
|
self.context.append('}' + CR + self.hypertarget_to(node.parent))
|
||
|
elif isinstance(parent, nodes.topic):
|
||
|
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
|
||
|
|
||
|
for child in node:
|
||
|
if isinstance(child, addnodes.desc_type_parameter_list):
|
||
|
self.has_tp_list = True
|
||
|
# recall that return annotations must follow an argument list,
|
||
|
# so signatures of the form "foo[tp_list] -> retann" will not
|
||
|
# be encountered (if they should, the `domains.python.py_sig_re`
|
||
|
# pattern must be modified accordingly)
|
||
|
arglist = next_sibling(child)
|
||
|
assert isinstance(arglist, addnodes.desc_parameterlist)
|
||
|
# tp_list + arglist: \macro{name}{tp_list}{arglist}{return}
|
||
|
multi_tp_list = has_multi_line(child)
|
||
|
multi_arglist = has_multi_line(arglist)
|
||
|
|
||
|
if multi_tp_list:
|
||
|
if multi_arglist:
|
||
|
self.body.append(CR + r'\pysigwithonelineperargwithonelinepertparg{')
|
||
|
else:
|
||
|
self.body.append(CR + r'\pysiglinewithargsretwithonelinepertparg{')
|
||
|
else:
|
||
|
if multi_arglist:
|
||
|
self.body.append(CR + r'\pysigwithonelineperargwithtypelist{')
|
||
|
else:
|
||
|
self.body.append(CR + r'\pysiglinewithargsretwithtypelist{')
|
||
|
break
|
||
|
|
||
|
if isinstance(child, addnodes.desc_parameterlist):
|
||
|
# arglist only: \macro{name}{arglist}{return}
|
||
|
if has_multi_line(child):
|
||
|
self.body.append(CR + r'\pysigwithonelineperarg{')
|
||
|
else:
|
||
|
self.body.append(CR + r'\pysiglinewithargsret{')
|
||
|
break
|
||
|
else:
|
||
|
# no tp_list, no arglist: \macro{name}
|
||
|
self.body.append(CR + r'\pysigline{')
|
||
|
|
||
|
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 not self.has_tp_list:
|
||
|
# close name argument (#1), open parameters list argument (#2)
|
||
|
self.body.append('}{')
|
||
|
self._visit_sig_parameter_list(node, addnodes.desc_parameter)
|
||
|
|
||
|
def depart_desc_parameterlist(self, node: Element) -> None:
|
||
|
# close parameterlist, open return annotation
|
||
|
self.body.append('}{')
|
||
|
|
||
|
def visit_desc_type_parameter_list(self, node: Element) -> None:
|
||
|
# close name argument (#1), open type parameters list argument (#2)
|
||
|
self.body.append('}{')
|
||
|
self._visit_sig_parameter_list(node, addnodes.desc_type_parameter)
|
||
|
|
||
|
def depart_desc_type_parameter_list(self, node: Element) -> None:
|
||
|
# close type parameters list, open parameters list argument (#3)
|
||
|
self.body.append('}{')
|
||
|
|
||
|
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:
|
||
|
is_last_group = self.param_group_index + 1 == len(self.list_is_required_param)
|
||
|
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)
|
||
|
|
||
|
def depart_seealso(self, node: Element) -> None:
|
||
|
self.body.append(BLANKLINE)
|
||
|
self.body.append(r'\end{sphinxseealso}')
|
||
|
self.body.append(BLANKLINE)
|
||
|
|
||
|
def visit_rubric(self, node: Element) -> None:
|
||
|
if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
|
||
|
raise nodes.SkipNode
|
||
|
self.body.append(r'\subsubsection*{')
|
||
|
self.context.append('}' + CR)
|
||
|
self.in_title = 1
|
||
|
|
||
|
def depart_rubric(self, node: Element) -> 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_t',
|
||
|
{'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):
|
||
|
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 = fr'\lowercase{{\sphinxincludegraphics{options}}}{{{{{base}}}{ext}}}'
|
||
|
else:
|
||
|
cmd = fr'\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:
|
||
|
# 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
|
||
|
|
||
|
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
|
||
|
|
||
|
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 = cast(StandardDomain, self.builder.env.get_domain('std'))
|
||
|
if isinstance(next_node, HYPERLINK_SUPPORT_NODES):
|
||
|
return
|
||
|
if domain.get_enumerable_node_type(next_node) and domain.get_numfig_title(next_node):
|
||
|
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(fr'\index{{{p1}@{P1}!{p2}@{P2}{m}}}')
|
||
|
except ValueError:
|
||
|
p, = parts
|
||
|
P, = styled
|
||
|
self.body.append(fr'\index{{{p}@{P}{m}}}')
|
||
|
elif type == 'pair':
|
||
|
p1, p2 = parts
|
||
|
P1, P2 = styled
|
||
|
self.body.append(fr'\index{{{p1}@{P1}!{p2}@{P2}{m}}}'
|
||
|
fr'\index{{{p2}@{P2}!{p1}@{P1}{m}}}')
|
||
|
elif type == 'triple':
|
||
|
p1, p2, p3 = parts
|
||
|
P1, P2, P3 = styled
|
||
|
self.body.append(
|
||
|
fr'\index{{{p1}@{P1}!{p2} {p3}@{P2} {P3}{m}}}'
|
||
|
fr'\index{{{p2}@{P2}!{p3}, {p1}@{P3}, {P1}{m}}}'
|
||
|
fr'\index{{{p3}@{P3}!{p1} {p2}@{P1} {P2}{m}}}')
|
||
|
elif type in {'see', 'seealso'}:
|
||
|
p1, p2 = parts
|
||
|
P1, _P2 = styled
|
||
|
self.body.append(fr'\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 = fr'\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(fr'\bibitem[{self.encode(label.astext())}]'
|
||
|
fr'{{{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(fr'\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', [])
|
||
|
if classes in [['menuselection']]:
|
||
|
self.body.append(r'\sphinxmenuselection{')
|
||
|
self.context.append('}')
|
||
|
elif classes in [['guilabel']]:
|
||
|
self.body.append(r'\sphinxguilabel{')
|
||
|
self.context.append('}')
|
||
|
elif classes in [['accelerator']]:
|
||
|
self.body.append(r'\sphinxaccelerator{')
|
||
|
self.context.append('}')
|
||
|
elif classes and not self.in_title:
|
||
|
self.body.append(r'\DUrole{%s}{' % ','.join(classes))
|
||
|
self.context.append('}')
|
||
|
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', [])
|
||
|
for c in classes:
|
||
|
self.body.append('\n\\begin{sphinxuseclass}{%s}' % c)
|
||
|
|
||
|
def depart_container(self, node: Element) -> None:
|
||
|
classes = node.get('classes', [])
|
||
|
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,
|
||
|
)
|