mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
2657 lines
99 KiB
Python
2657 lines
99 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
sphinx.writers.latex
|
|
~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Custom docutils writer for LaTeX.
|
|
|
|
Much of this code is adapted from Dave Kuhlman's "docpy" writer from his
|
|
docutils sandbox.
|
|
|
|
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
|
|
:license: BSD, see LICENSE for details.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
import warnings
|
|
from collections import defaultdict
|
|
from os import path
|
|
|
|
from docutils import nodes, writers
|
|
from docutils.writers.latex2e import Babel
|
|
from six import itervalues, text_type
|
|
|
|
from sphinx import addnodes
|
|
from sphinx import highlighting
|
|
from sphinx.deprecation import RemovedInSphinx30Warning
|
|
from sphinx.errors import SphinxError
|
|
from sphinx.locale import admonitionlabels, _, __
|
|
from sphinx.util import split_into, logging
|
|
from sphinx.util.i18n import format_date
|
|
from sphinx.util.nodes import clean_astext
|
|
from sphinx.util.template import LaTeXRenderer
|
|
from sphinx.util.texescape import tex_escape_map, tex_replace_map
|
|
|
|
try:
|
|
from docutils.utils.roman import toRoman
|
|
except ImportError:
|
|
# In Debain/Ubuntu, roman package is provided as roman, not as docutils.utils.roman
|
|
from roman import toRoman
|
|
|
|
if False:
|
|
# For type annotation
|
|
from typing import Any, Callable, Dict, Iterator, List, Pattern, Tuple, Set, Union # NOQA
|
|
from sphinx.builder import Builder # NOQA
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SHORTHANDOFF = r'''
|
|
\ifdefined\shorthandoff
|
|
\ifnum\catcode`\=\string=\active\shorthandoff{=}\fi
|
|
\ifnum\catcode`\"=\active\shorthandoff{"}\fi
|
|
\fi
|
|
'''
|
|
|
|
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',
|
|
}) # type: Dict[unicode, unicode]
|
|
|
|
DEFAULT_SETTINGS = {
|
|
'latex_engine': 'pdflatex',
|
|
'papersize': 'letterpaper',
|
|
'pointsize': '10pt',
|
|
'pxunit': '.75bp',
|
|
'classoptions': '',
|
|
'extraclassoptions': '',
|
|
'maxlistdepth': '',
|
|
'sphinxpkgoptions': '',
|
|
'sphinxsetup': '',
|
|
'fvset': '\\fvset{fontsize=\\small}',
|
|
'passoptionstopackages': '',
|
|
'geometry': '\\usepackage{geometry}',
|
|
'inputenc': '',
|
|
'utf8extra': '',
|
|
'cmappkg': '\\usepackage{cmap}',
|
|
'fontenc': '\\usepackage[T1]{fontenc}',
|
|
'amsmath': '\\usepackage{amsmath,amssymb,amstext}',
|
|
'multilingual': '',
|
|
'babel': '\\usepackage{babel}',
|
|
'polyglossia': '',
|
|
'fontpkg': '\\usepackage{times}',
|
|
'fncychap': '\\usepackage[Bjarne]{fncychap}',
|
|
'hyperref': ('% Include hyperref last.\n'
|
|
'\\usepackage{hyperref}\n'
|
|
'% Fix anchor placement for figures with captions.\n'
|
|
'\\usepackage{hypcap}% it must be loaded after hyperref.\n'
|
|
'% Set up styles of URL: it should be placed after hyperref.\n'
|
|
'\\urlstyle{same}'),
|
|
'usepackages': '',
|
|
'numfig_format': '',
|
|
'contentsname': '',
|
|
'preamble': '',
|
|
'title': '',
|
|
'date': '',
|
|
'release': '',
|
|
'author': '',
|
|
'logo': '\\vbox{}',
|
|
'releasename': '',
|
|
'makeindex': '\\makeindex',
|
|
'shorthandoff': '',
|
|
'maketitle': '\\sphinxmaketitle',
|
|
'tableofcontents': '\\sphinxtableofcontents',
|
|
'atendofbody': '',
|
|
'printindex': '\\printindex',
|
|
'transition': '\n\n\\bigskip\\hrule\\bigskip\n\n',
|
|
'figure_align': 'htbp',
|
|
'tocdepth': '',
|
|
'secnumdepth': '',
|
|
'pageautorefname': '',
|
|
'translatablestrings': '',
|
|
} # type: Dict[unicode, unicode]
|
|
|
|
ADDITIONAL_SETTINGS = {
|
|
'pdflatex': {
|
|
'inputenc': '\\usepackage[utf8]{inputenc}',
|
|
'utf8extra': ('\\ifdefined\\DeclareUnicodeCharacter\n'
|
|
'% support both utf8 and utf8x syntaxes\n'
|
|
'\\edef\\sphinxdqmaybe{'
|
|
'\\ifdefined\\DeclareUnicodeCharacterAsOptional'
|
|
'\\string"\\fi}\n'
|
|
' \\DeclareUnicodeCharacter{\\sphinxdqmaybe00A0}'
|
|
'{\\nobreakspace}\n'
|
|
' \\DeclareUnicodeCharacter{\\sphinxdqmaybe2500}'
|
|
'{\\sphinxunichar{2500}}\n'
|
|
' \\DeclareUnicodeCharacter{\\sphinxdqmaybe2502}'
|
|
'{\\sphinxunichar{2502}}\n'
|
|
' \\DeclareUnicodeCharacter{\\sphinxdqmaybe2514}'
|
|
'{\\sphinxunichar{2514}}\n'
|
|
' \\DeclareUnicodeCharacter{\\sphinxdqmaybe251C}'
|
|
'{\\sphinxunichar{251C}}\n'
|
|
' \\DeclareUnicodeCharacter{\\sphinxdqmaybe2572}'
|
|
'{\\textbackslash}\n'
|
|
'\\fi'),
|
|
},
|
|
'xelatex': {
|
|
'latex_engine': 'xelatex',
|
|
'polyglossia': '\\usepackage{polyglossia}',
|
|
'babel': '',
|
|
'fontenc': '\\usepackage{fontspec}',
|
|
'fontpkg': '',
|
|
'utf8extra': ('\\catcode`^^^^00a0\\active\\protected\\def^^^^00a0'
|
|
'{\\leavevmode\\nobreak\\ }'),
|
|
'fvset': '\\fvset{fontsize=auto}',
|
|
},
|
|
'lualatex': {
|
|
'latex_engine': 'lualatex',
|
|
'polyglossia': '\\usepackage{polyglossia}',
|
|
'babel': '',
|
|
'fontenc': '\\usepackage{fontspec}',
|
|
'fontpkg': '',
|
|
'utf8extra': ('\\catcode`^^^^00a0\\active\\protected\\def^^^^00a0'
|
|
'{\\leavevmode\\nobreak\\ }'),
|
|
'fvset': '\\fvset{fontsize=auto}',
|
|
},
|
|
'platex': {
|
|
'latex_engine': 'platex',
|
|
'babel': '',
|
|
'classoptions': ',dvipdfmx',
|
|
'fncychap': '',
|
|
'geometry': '\\usepackage[dvipdfm]{geometry}',
|
|
},
|
|
} # type: Dict[unicode, Dict[unicode, unicode]]
|
|
|
|
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 = {} # type: Dict
|
|
|
|
output = None
|
|
|
|
def __init__(self, builder):
|
|
# type: (Builder) -> None
|
|
writers.Writer.__init__(self)
|
|
self.builder = builder
|
|
|
|
def translate(self):
|
|
# type: () -> None
|
|
visitor = self.builder.create_translator(self.document, self.builder)
|
|
self.document.walkabout(visitor)
|
|
self.output = visitor.astext()
|
|
|
|
|
|
# Helper classes
|
|
|
|
class ExtBabel(Babel):
|
|
cyrillic_languages = ('bulgarian', 'kazakh', 'mongolian', 'russian', 'ukrainian')
|
|
|
|
def __init__(self, language_code, use_polyglossia=False):
|
|
# type: (unicode, bool) -> None
|
|
self.language_code = language_code
|
|
self.use_polyglossia = use_polyglossia
|
|
self.supported = True
|
|
super(ExtBabel, self).__init__(language_code or '')
|
|
|
|
def get_shorthandoff(self):
|
|
# type: () -> unicode
|
|
warnings.warn('ExtBabel.get_shorthandoff() is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return SHORTHANDOFF
|
|
|
|
def uses_cyrillic(self):
|
|
# type: () -> bool
|
|
return self.language in self.cyrillic_languages
|
|
|
|
def is_supported_language(self):
|
|
# type: () -> bool
|
|
return self.supported
|
|
|
|
def language_name(self, language_code):
|
|
# type: (unicode) -> unicode
|
|
language = super(ExtBabel, self).language_name(language_code)
|
|
if language == 'ngerman' and self.use_polyglossia:
|
|
# polyglossia calls new orthography (Neue Rechtschreibung) as
|
|
# german (with new spelling option).
|
|
return 'german'
|
|
elif not language:
|
|
self.supported = False
|
|
return 'english' # fallback to english
|
|
else:
|
|
return language
|
|
|
|
def get_mainlanguage_options(self):
|
|
# type: () -> unicode
|
|
"""Return options for polyglossia's ``\\setmainlanguage``."""
|
|
if self.use_polyglossia is False:
|
|
return None
|
|
elif self.language == 'german':
|
|
language = super(ExtBabel, self).language_name(self.language_code)
|
|
if language == 'ngerman':
|
|
return 'spelling=new'
|
|
else:
|
|
return 'spelling=old'
|
|
else:
|
|
return None
|
|
|
|
|
|
class Table(object):
|
|
"""A table data"""
|
|
|
|
def __init__(self, node):
|
|
# type: (nodes.table) -> None
|
|
self.header = [] # type: List[unicode]
|
|
self.body = [] # type: List[unicode]
|
|
self.align = node.get('align')
|
|
self.colcount = 0
|
|
self.colspec = None # type: unicode
|
|
self.colwidths = [] # type: List[int]
|
|
self.has_problematic = False
|
|
self.has_oldproblematic = False
|
|
self.has_verbatim = False
|
|
self.caption = None # type: List[unicode]
|
|
self.stubs = [] # type: List[int]
|
|
|
|
# current position
|
|
self.col = 0
|
|
self.row = 0
|
|
|
|
# for internal use
|
|
self.classes = node.get('classes', []) # type: List[unicode]
|
|
self.cells = defaultdict(int) # type: Dict[Tuple[int, int], int]
|
|
# it maps table location to cell_id
|
|
# (cell = rectangular area)
|
|
self.cell_id = 0 # last assigned cell_id
|
|
|
|
@property
|
|
def caption_footnotetexts(self):
|
|
# type: () -> List[unicode]
|
|
warnings.warn('table.caption_footnotetexts is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return []
|
|
|
|
@property
|
|
def header_footnotetexts(self):
|
|
# type: () -> List[unicode]
|
|
warnings.warn('table.header_footnotetexts is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return []
|
|
|
|
def is_longtable(self):
|
|
# type: () -> bool
|
|
"""True if and only if table uses longtable environment."""
|
|
return self.row > 30 or 'longtable' in self.classes
|
|
|
|
def get_table_type(self):
|
|
# type: () -> unicode
|
|
"""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):
|
|
# type: () -> unicode
|
|
"""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 ``sphinx.sty``.
|
|
"""
|
|
if self.colspec:
|
|
return self.colspec
|
|
elif self.colwidths and 'colwidths-given' in self.classes:
|
|
total = sum(self.colwidths)
|
|
colspecs = ['\\X{%d}{%d}' % (width, total) for width in self.colwidths]
|
|
return '{|%s|}\n' % '|'.join(colspecs)
|
|
elif self.has_problematic:
|
|
return '{|*{%d}{\\X{1}{%d}|}}\n' % (self.colcount, self.colcount)
|
|
elif self.get_table_type() == 'tabulary':
|
|
# sphinx.sty sets T to be J by default.
|
|
return '{|' + ('T|' * self.colcount) + '}\n'
|
|
elif self.has_oldproblematic:
|
|
return '{|*{%d}{\\X{1}{%d}|}}\n' % (self.colcount, self.colcount)
|
|
else:
|
|
return '{|' + ('l|' * self.colcount) + '}\n'
|
|
|
|
def add_cell(self, height, width):
|
|
# type: (int, 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=None, col=None):
|
|
# type: (int, int) -> TableCell
|
|
"""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(object):
|
|
"""A cell data of tables."""
|
|
|
|
def __init__(self, table, row, col):
|
|
# type: (Table, int, 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):
|
|
# type: () -> 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):
|
|
# type: () -> 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):
|
|
# type: (unicode) -> unicode
|
|
"""Adjust spacing after abbreviations."""
|
|
return re.sub(r'\.(?=\s|$)', r'.\@', text)
|
|
|
|
|
|
def rstdim_to_latexdim(width_str, scale = 100):
|
|
# type: (unicode, int) -> unicode
|
|
"""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 = "%s\\sphinxpxdimen" % amount
|
|
elif unit == 'pt':
|
|
res = '%sbp' % amount # convert to 'bp'
|
|
elif unit == "%":
|
|
res = "%.3f\\linewidth" % (float(amount) / 100.0)
|
|
else:
|
|
amount = float(amount) * scale / 100.0
|
|
if unit in ('', "px"):
|
|
res = "%.5f\\sphinxpxdimen" % amount
|
|
elif unit == 'pt':
|
|
res = '%.5fbp' % amount
|
|
elif unit == "%":
|
|
res = "%.5f\\linewidth" % (amount / 100.0)
|
|
else:
|
|
res = "%.5f%s" % (amount, unit)
|
|
return res
|
|
|
|
|
|
class LaTeXTranslator(nodes.NodeVisitor):
|
|
|
|
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
|
|
|
|
# sphinx specific document classes
|
|
docclasses = ('howto', 'manual')
|
|
|
|
def __init__(self, document, builder):
|
|
# type: (nodes.Node, Builder) -> None
|
|
nodes.NodeVisitor.__init__(self, document)
|
|
self.builder = builder
|
|
self.body = [] # type: List[unicode]
|
|
|
|
# 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.first_document = 1
|
|
self.this_is_the_title = 1
|
|
self.literal_whitespace = 0
|
|
self.no_contractions = 0
|
|
self.in_parsed_literal = 0
|
|
self.compact_list = 0
|
|
self.first_param = 0
|
|
|
|
# sort out some elements
|
|
self.elements = DEFAULT_SETTINGS.copy()
|
|
self.elements.update(ADDITIONAL_SETTINGS.get(builder.config.latex_engine, {}))
|
|
# for xelatex+French, don't use polyglossia
|
|
if self.elements['latex_engine'] == 'xelatex':
|
|
if builder.config.language:
|
|
if builder.config.language[:2] == 'fr':
|
|
self.elements.update({
|
|
'polyglossia': '',
|
|
'babel': '\\usepackage{babel}',
|
|
})
|
|
# allow the user to override them all
|
|
self.elements.update(builder.config.latex_elements)
|
|
|
|
# but some have other interface in config file
|
|
self.elements.update({
|
|
'wrapperclass': self.format_docclass(document.settings.docclass),
|
|
# if empty, the title is set to the first section title
|
|
'title': document.settings.title, # treat as a raw LaTeX code
|
|
'release': self.encode(builder.config.release),
|
|
'author': document.settings.author, # treat as a raw LaTeX code
|
|
'indexname': _('Index'),
|
|
'use_xindy': builder.config.latex_use_xindy,
|
|
})
|
|
if not self.elements['releasename'] and self.elements['release']:
|
|
self.elements.update({
|
|
'releasename': _('Release'),
|
|
})
|
|
|
|
# we assume LaTeX class provides \chapter command except in case
|
|
# of non-Japanese 'howto' case
|
|
self.sectionnames = LATEXSECTIONNAMES[:]
|
|
if document.settings.docclass == 'howto':
|
|
docclass = builder.config.latex_docclass.get('howto', 'article')
|
|
if docclass[0] == 'j': # Japanese class...
|
|
pass
|
|
else:
|
|
self.sectionnames.remove('chapter')
|
|
else:
|
|
docclass = builder.config.latex_docclass.get('manual', 'report')
|
|
self.elements['docclass'] = docclass
|
|
|
|
# determine top section level
|
|
self.top_sectionlevel = 1
|
|
if builder.config.latex_toplevel_sectioning:
|
|
try:
|
|
self.top_sectionlevel = \
|
|
self.sectionnames.index(builder.config.latex_toplevel_sectioning)
|
|
except ValueError:
|
|
logger.warning(__('unknown %r toplevel_sectioning for class %r') %
|
|
(builder.config.latex_toplevel_sectioning, docclass))
|
|
|
|
if builder.config.today:
|
|
self.elements['date'] = builder.config.today
|
|
else:
|
|
self.elements['date'] = format_date(builder.config.today_fmt or _('%b %d, %Y'),
|
|
language=builder.config.language)
|
|
|
|
if builder.config.numfig:
|
|
self.numfig_secnum_depth = builder.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
|
|
self.elements['sphinxpkgoptions'] += \
|
|
(',numfigreset=%s' % self.numfig_secnum_depth)
|
|
else:
|
|
self.elements['sphinxpkgoptions'] += ',nonumfigreset'
|
|
try:
|
|
if builder.config.math_numfig:
|
|
self.elements['sphinxpkgoptions'] += ',mathnumfig'
|
|
except AttributeError:
|
|
pass
|
|
|
|
if builder.config.latex_logo:
|
|
# no need for \\noindent here, used in flushright
|
|
self.elements['logo'] = '\\sphinxincludegraphics{%s}\\par' % \
|
|
path.basename(builder.config.latex_logo)
|
|
|
|
if (builder.config.language and builder.config.language != 'ja' and
|
|
'fncychap' not in builder.config.latex_elements):
|
|
# use Sonny style if any language specified
|
|
self.elements['fncychap'] = ('\\usepackage[Sonny]{fncychap}\n'
|
|
'\\ChNameVar{\\Large\\normalfont'
|
|
'\\sffamily}\n\\ChTitleVar{\\Large'
|
|
'\\normalfont\\sffamily}')
|
|
|
|
self.babel = ExtBabel(builder.config.language,
|
|
not self.elements['babel'])
|
|
if builder.config.language and 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'),
|
|
builder.config.language)
|
|
|
|
# set up multilingual module...
|
|
# 'babel' key is public and user setting must be obeyed
|
|
if self.elements['babel']:
|
|
self.elements['classoptions'] += ',' + self.babel.get_language()
|
|
# this branch is not taken for xelatex/lualatex if default settings
|
|
self.elements['multilingual'] = self.elements['babel']
|
|
if builder.config.language:
|
|
self.elements['shorthandoff'] = SHORTHANDOFF
|
|
|
|
# Times fonts don't work with Cyrillic languages
|
|
if self.babel.uses_cyrillic() \
|
|
and 'fontpkg' not in builder.config.latex_elements:
|
|
self.elements['fontpkg'] = ''
|
|
elif self.elements['polyglossia']:
|
|
self.elements['classoptions'] += ',' + self.babel.get_language()
|
|
options = self.babel.get_mainlanguage_options()
|
|
if options:
|
|
mainlanguage = r'\setmainlanguage[%s]{%s}' % (options,
|
|
self.babel.get_language())
|
|
else:
|
|
mainlanguage = r'\setmainlanguage{%s}' % self.babel.get_language()
|
|
|
|
self.elements['multilingual'] = '%s\n%s' % (self.elements['polyglossia'],
|
|
mainlanguage)
|
|
|
|
if getattr(builder, 'usepackages', None):
|
|
def declare_package(packagename, options=None):
|
|
# type:(unicode, unicode) -> unicode
|
|
if options:
|
|
return '\\usepackage[%s]{%s}' % (options, packagename)
|
|
else:
|
|
return '\\usepackage{%s}' % (packagename,)
|
|
usepackages = (declare_package(*p) for p in builder.usepackages)
|
|
self.elements['usepackages'] += "\n".join(usepackages)
|
|
|
|
minsecnumdepth = self.secnumdepth # 2 from legacy sphinx manual/howto
|
|
if 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 = document['tocdepth'] + 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'] = '\\setcounter{tocdepth}{%d}' % tocdepth
|
|
minsecnumdepth = max(minsecnumdepth, tocdepth)
|
|
|
|
if builder.config.numfig and (builder.config.numfig_secnum_depth > 0):
|
|
minsecnumdepth = max(minsecnumdepth, self.numfig_secnum_depth - 1)
|
|
|
|
if minsecnumdepth > self.secnumdepth:
|
|
self.elements['secnumdepth'] = '\\setcounter{secnumdepth}{%d}' %\
|
|
minsecnumdepth
|
|
|
|
if getattr(document.settings, 'contentsname', None):
|
|
self.elements['contentsname'] = \
|
|
self.babel_renewcommand('\\contentsname', document.settings.contentsname)
|
|
|
|
if self.elements['maxlistdepth']:
|
|
self.elements['sphinxpkgoptions'] += (',maxlistdepth=%s' %
|
|
self.elements['maxlistdepth'])
|
|
if self.elements['sphinxpkgoptions']:
|
|
self.elements['sphinxpkgoptions'] = ('[%s]' %
|
|
self.elements['sphinxpkgoptions'])
|
|
if self.elements['sphinxsetup']:
|
|
self.elements['sphinxsetup'] = ('\\sphinxsetup{%s}' %
|
|
self.elements['sphinxsetup'])
|
|
if self.elements['extraclassoptions']:
|
|
self.elements['classoptions'] += ',' + \
|
|
self.elements['extraclassoptions']
|
|
self.elements['translatablestrings'] = (
|
|
self.babel_renewcommand(
|
|
'\\literalblockcontinuedname', self.encode(_('continued from previous page'))
|
|
) +
|
|
self.babel_renewcommand(
|
|
'\\literalblockcontinuesname', self.encode(_('continues on next page'))
|
|
) +
|
|
self.babel_renewcommand(
|
|
'\\sphinxnonalphabeticalgroupname', self.encode(_('Non-alphabetical'))
|
|
) +
|
|
self.babel_renewcommand(
|
|
'\\sphinxsymbolsname', self.encode(_('Symbols'))
|
|
) +
|
|
self.babel_renewcommand(
|
|
'\\sphinxnumbersname', self.encode(_('Numbers'))
|
|
)
|
|
)
|
|
self.elements['pageautorefname'] = \
|
|
self.babel_defmacro('\\pageautorefname', self.encode(_('page')))
|
|
self.elements['numfig_format'] = self.generate_numfig_format(builder)
|
|
|
|
self.highlighter = highlighting.PygmentsBridge('latex', builder.config.pygments_style)
|
|
self.context = [] # type: List[Any]
|
|
self.descstack = [] # type: List[unicode]
|
|
self.table = None # type: Table
|
|
self.next_table_colspec = None # type: unicode
|
|
self.bodystack = [] # type: List[List[unicode]]
|
|
self.footnote_restricted = False
|
|
self.pending_footnotes = [] # type: List[nodes.footnote_reference]
|
|
self.curfilestack = [] # type: List[unicode]
|
|
self.handled_abbrs = set() # type: Set[unicode]
|
|
|
|
def pushbody(self, newbody):
|
|
# type: (List[unicode]) -> None
|
|
self.bodystack.append(self.body)
|
|
self.body = newbody
|
|
|
|
def popbody(self):
|
|
# type: () -> List[unicode]
|
|
body = self.body
|
|
self.body = self.bodystack.pop()
|
|
return body
|
|
|
|
def restrict_footnote(self, node):
|
|
# type: (nodes.Node) -> None
|
|
warnings.warn('LaTeXWriter.restrict_footnote() is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
|
|
if self.footnote_restricted is False:
|
|
self.footnote_restricted = node
|
|
self.pending_footnotes = []
|
|
|
|
def unrestrict_footnote(self, node):
|
|
# type: (nodes.Node) -> None
|
|
warnings.warn('LaTeXWriter.unrestrict_footnote() is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
|
|
if self.footnote_restricted == node:
|
|
self.footnote_restricted = False
|
|
for footnode in self.pending_footnotes:
|
|
footnode['footnotetext'] = True
|
|
footnode.walkabout(self)
|
|
self.pending_footnotes = []
|
|
|
|
def format_docclass(self, docclass):
|
|
# type: (unicode) -> unicode
|
|
""" prepends prefix to sphinx document classes
|
|
"""
|
|
if docclass in self.docclasses:
|
|
docclass = 'sphinx' + docclass
|
|
return docclass
|
|
|
|
def astext(self):
|
|
# type: () -> unicode
|
|
self.elements.update({
|
|
'body': u''.join(self.body),
|
|
'indices': self.generate_indices()
|
|
})
|
|
return self.render('latex.tex_t', self.elements)
|
|
|
|
def hypertarget(self, id, withdoc=True, anchor=True):
|
|
# type: (unicode, bool, bool) -> unicode
|
|
if withdoc:
|
|
id = self.curfilestack[-1] + ':' + id
|
|
return (anchor and '\\phantomsection' or '') + \
|
|
'\\label{%s}' % self.idescape(id)
|
|
|
|
def hypertarget_to(self, node, anchor=False):
|
|
# type: (nodes.Node, bool) -> unicode
|
|
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):
|
|
# type: (unicode) -> unicode
|
|
return '{\\hyperref[%s]{' % self.idescape(id)
|
|
|
|
def hyperpageref(self, id):
|
|
# type: (unicode) -> unicode
|
|
return '\\autopageref*{%s}' % self.idescape(id)
|
|
|
|
def idescape(self, id):
|
|
# type: (unicode) -> unicode
|
|
return '\\detokenize{%s}' % text_type(id).translate(tex_replace_map).\
|
|
encode('ascii', 'backslashreplace').decode('ascii').\
|
|
replace('\\', '_')
|
|
|
|
def babel_renewcommand(self, command, definition):
|
|
# type: (unicode, unicode) -> unicode
|
|
if self.elements['multilingual']:
|
|
prefix = '\\addto\\captions%s{' % self.babel.get_language()
|
|
suffix = '}'
|
|
else: # babel is disabled (mainly for Japanese environment)
|
|
prefix = ''
|
|
suffix = ''
|
|
|
|
return ('%s\\renewcommand{%s}{%s}%s\n' % (prefix, command, definition, suffix))
|
|
|
|
def babel_defmacro(self, name, definition):
|
|
# type: (unicode, unicode) -> unicode
|
|
if self.elements['babel']:
|
|
prefix = '\\addto\\extras%s{' % self.babel.get_language()
|
|
suffix = '}'
|
|
else: # babel is disabled (mainly for Japanese environment)
|
|
prefix = ''
|
|
suffix = ''
|
|
|
|
return ('%s\\def%s{%s}%s\n' % (prefix, name, definition, suffix))
|
|
|
|
def generate_numfig_format(self, builder):
|
|
# type: (Builder) -> unicode
|
|
ret = [] # type: List[unicode]
|
|
figure = self.builder.config.numfig_format['figure'].split('%s', 1)
|
|
if len(figure) == 1:
|
|
ret.append('\\def\\fnum@figure{%s}\n' %
|
|
text_type(figure[0]).strip().translate(tex_escape_map))
|
|
else:
|
|
definition = escape_abbr(text_type(figure[0]).translate(tex_escape_map))
|
|
ret.append(self.babel_renewcommand('\\figurename', definition))
|
|
ret.append('\\makeatletter\n')
|
|
ret.append('\\def\\fnum@figure{\\figurename\\thefigure{}%s}\n' %
|
|
text_type(figure[1]).translate(tex_escape_map))
|
|
ret.append('\\makeatother\n')
|
|
|
|
table = self.builder.config.numfig_format['table'].split('%s', 1)
|
|
if len(table) == 1:
|
|
ret.append('\\def\\fnum@table{%s}\n' %
|
|
text_type(table[0]).strip().translate(tex_escape_map))
|
|
else:
|
|
definition = escape_abbr(text_type(table[0]).translate(tex_escape_map))
|
|
ret.append(self.babel_renewcommand('\\tablename', definition))
|
|
ret.append('\\makeatletter\n')
|
|
ret.append('\\def\\fnum@table{\\tablename\\thetable{}%s}\n' %
|
|
text_type(table[1]).translate(tex_escape_map))
|
|
ret.append('\\makeatother\n')
|
|
|
|
codeblock = self.builder.config.numfig_format['code-block'].split('%s', 1)
|
|
if len(codeblock) == 1:
|
|
pass # FIXME
|
|
else:
|
|
definition = text_type(codeblock[0]).strip().translate(tex_escape_map)
|
|
ret.append(self.babel_renewcommand('\\literalblockname', definition))
|
|
if codeblock[1]:
|
|
pass # FIXME
|
|
|
|
return ''.join(ret)
|
|
|
|
def generate_indices(self):
|
|
# type: (Builder) -> unicode
|
|
def generate(content, collapsed):
|
|
# type: (List[Tuple[unicode, List[Tuple[unicode, unicode, unicode, unicode, unicode]]]], bool) -> None # NOQA
|
|
ret.append('\\begin{sphinxtheindex}\n')
|
|
ret.append('\\let\\bigletter\\sphinxstyleindexlettergroup\n')
|
|
for i, (letter, entries) in enumerate(content):
|
|
if i > 0:
|
|
ret.append('\\indexspace\n')
|
|
ret.append('\\bigletter{%s}\n' %
|
|
text_type(letter).translate(tex_escape_map))
|
|
for entry in entries:
|
|
if not entry[3]:
|
|
continue
|
|
ret.append('\\item\\relax\\sphinxstyleindexentry{%s}' %
|
|
self.encode(entry[0]))
|
|
if entry[4]:
|
|
# add "extra" info
|
|
ret.append('\\sphinxstyleindexextra{%s}' % self.encode(entry[4]))
|
|
ret.append('\\sphinxstyleindexpageref{%s:%s}\n' %
|
|
(entry[2], self.idescape(entry[3])))
|
|
ret.append('\\end{sphinxtheindex}\n')
|
|
|
|
ret = []
|
|
# latex_domain_indices can be False/True or a list of index names
|
|
indices_config = self.builder.config.latex_domain_indices
|
|
if indices_config:
|
|
for domain in itervalues(self.builder.env.domains):
|
|
for indexcls in domain.indices:
|
|
indexname = '%s-%s' % (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(u'\\renewcommand{\\indexname}{%s}\n' %
|
|
indexcls.localname)
|
|
generate(content, collapsed)
|
|
|
|
return ''.join(ret)
|
|
|
|
def render(self, template_name, variables):
|
|
# type: (unicode, Dict) -> unicode
|
|
for template_dir in self.builder.config.templates_path:
|
|
template = path.join(self.builder.confdir, template_dir,
|
|
template_name)
|
|
if path.exists(template):
|
|
return LaTeXRenderer().render(template, variables)
|
|
|
|
return LaTeXRenderer().render(template_name, variables)
|
|
|
|
def visit_document(self, node):
|
|
# type: (nodes.Node) -> 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(u'\n\\appendix\n')
|
|
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):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_start_of_file(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.curfilestack.append(node['docname'])
|
|
|
|
def collect_footnotes(self, node):
|
|
# type: (nodes.Node) -> Dict[unicode, List[Union[collected_footnote, bool]]]
|
|
def footnotes_under(n):
|
|
# type: (nodes.Node) -> Iterator[nodes.Node]
|
|
if isinstance(n, nodes.footnote):
|
|
yield n
|
|
else:
|
|
for c in n.children:
|
|
if isinstance(c, addnodes.start_of_file):
|
|
continue
|
|
for k in footnotes_under(c):
|
|
yield k
|
|
|
|
fnotes = {} # type: Dict[unicode, List[Union[collected_footnote, bool]]]
|
|
for fn in footnotes_under(node):
|
|
num = fn.children[0].astext().strip()
|
|
newnode = collected_footnote(*fn.children, number=num)
|
|
fnotes[num] = [newnode, False]
|
|
return fnotes
|
|
|
|
def depart_start_of_file(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.curfilestack.pop()
|
|
|
|
def visit_section(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if not self.this_is_the_title:
|
|
self.sectionlevel += 1
|
|
self.body.append('\n\n')
|
|
|
|
def depart_section(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.sectionlevel = max(self.sectionlevel - 1,
|
|
self.top_sectionlevel - 1)
|
|
|
|
def visit_problematic(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'{\color{red}\bfseries{}')
|
|
|
|
def depart_problematic(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}')
|
|
|
|
def visit_topic(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.in_minipage = 1
|
|
self.body.append('\n\\begin{sphinxShadowBox}\n')
|
|
|
|
def depart_topic(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.in_minipage = 0
|
|
self.body.append('\\end{sphinxShadowBox}\n')
|
|
visit_sidebar = visit_topic
|
|
depart_sidebar = depart_topic
|
|
|
|
def visit_glossary(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_glossary(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_productionlist(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\n\\begin{productionlist}\n')
|
|
self.in_production_list = 1
|
|
|
|
def depart_productionlist(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{productionlist}\n\n')
|
|
self.in_production_list = 0
|
|
|
|
def visit_production(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if node['tokenname']:
|
|
tn = node['tokenname']
|
|
self.body.append(self.hypertarget('grammar-token-' + tn))
|
|
self.body.append('\\production{%s}{' % self.encode(tn))
|
|
else:
|
|
self.body.append('\\productioncont{')
|
|
|
|
def depart_production(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}\n')
|
|
|
|
def visit_transition(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(self.elements['transition'])
|
|
|
|
def depart_transition(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_title(self, node):
|
|
# type: (nodes.Node) -> None
|
|
parent = node.parent
|
|
if isinstance(parent, addnodes.seealso):
|
|
# the environment already handles this
|
|
raise nodes.SkipNode
|
|
elif 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=(self.curfilestack[-1], node.line))
|
|
if not self.elements['title']:
|
|
# text needs to be escaped since it is inserted into
|
|
# the output literally
|
|
self.elements['title'] = node.astext().translate(tex_escape_map)
|
|
self.this_is_the_title = 0
|
|
raise nodes.SkipNode
|
|
else:
|
|
short = ''
|
|
if node.traverse(nodes.image):
|
|
short = ('[%s]' %
|
|
u' '.join(clean_astext(node).split()).translate(tex_escape_map))
|
|
|
|
try:
|
|
self.body.append(r'\%s%s{' % (self.sectionnames[self.sectionlevel], short))
|
|
except IndexError:
|
|
# just use "subparagraph", it's not numbered anyway
|
|
self.body.append(r'\%s%s{' % (self.sectionnames[-1], short))
|
|
self.context.append('}\n' + self.hypertarget_to(node.parent))
|
|
elif isinstance(parent, nodes.topic):
|
|
self.body.append(r'\sphinxstyletopictitle{')
|
|
self.context.append('}\n')
|
|
elif isinstance(parent, nodes.sidebar):
|
|
self.body.append(r'\sphinxstylesidebartitle{')
|
|
self.context.append('}\n')
|
|
elif isinstance(parent, nodes.Admonition):
|
|
self.body.append('{')
|
|
self.context.append('}\n')
|
|
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=(self.curfilestack[-1], node.line or ''))
|
|
self.body.append('\\sphinxstyleothertitle{')
|
|
self.context.append('}\n')
|
|
self.in_title = 1
|
|
|
|
def depart_title(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.in_title = 0
|
|
if isinstance(node.parent, nodes.table):
|
|
self.table.caption = self.popbody()
|
|
else:
|
|
self.body.append(self.context.pop())
|
|
|
|
def visit_subtitle(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if isinstance(node.parent, nodes.sidebar):
|
|
self.body.append('\\sphinxstylesidebarsubtitle{')
|
|
self.context.append('}\n')
|
|
else:
|
|
self.context.append('')
|
|
|
|
def depart_subtitle(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(self.context.pop())
|
|
|
|
def visit_desc(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\n\\begin{fulllineitems}\n')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_desc(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\\end{fulllineitems}\n\n')
|
|
|
|
def _visit_signature_line(self, node):
|
|
# type: (nodes.Node) -> None
|
|
for child in node:
|
|
if isinstance(child, addnodes.desc_parameterlist):
|
|
self.body.append(r'\pysiglinewithargsret{')
|
|
break
|
|
else:
|
|
self.body.append(r'\pysigline{')
|
|
|
|
def _depart_signature_line(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}')
|
|
|
|
def visit_desc_signature(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if node.parent['objtype'] != 'describe' and node['ids']:
|
|
hyper = self.hypertarget(node['ids'][0])
|
|
else:
|
|
hyper = ''
|
|
self.body.append(hyper)
|
|
if not node.get('is_multiline'):
|
|
self._visit_signature_line(node)
|
|
else:
|
|
self.body.append('%\n\\pysigstartmultiline\n')
|
|
|
|
def depart_desc_signature(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if not node.get('is_multiline'):
|
|
self._depart_signature_line(node)
|
|
else:
|
|
self.body.append('%\n\\pysigstopmultiline')
|
|
|
|
def visit_desc_signature_line(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self._visit_signature_line(node)
|
|
|
|
def depart_desc_signature_line(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self._depart_signature_line(node)
|
|
|
|
def visit_desc_addname(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxcode{\sphinxupquote{')
|
|
self.literal_whitespace += 1
|
|
|
|
def depart_desc_addname(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}}')
|
|
self.literal_whitespace -= 1
|
|
|
|
def visit_desc_type(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_desc_type(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_desc_returns(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'{ $\rightarrow$ ')
|
|
|
|
def depart_desc_returns(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'}')
|
|
|
|
def visit_desc_name(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxbfcode{\sphinxupquote{')
|
|
self.no_contractions += 1
|
|
self.literal_whitespace += 1
|
|
|
|
def depart_desc_name(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}}')
|
|
self.literal_whitespace -= 1
|
|
self.no_contractions -= 1
|
|
|
|
def visit_desc_parameterlist(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# close name, open parameterlist
|
|
self.body.append('}{')
|
|
self.first_param = 1
|
|
|
|
def depart_desc_parameterlist(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# close parameterlist, open return annotation
|
|
self.body.append('}{')
|
|
|
|
def visit_desc_parameter(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if not self.first_param:
|
|
self.body.append(', ')
|
|
else:
|
|
self.first_param = 0
|
|
if not node.hasattr('noemph'):
|
|
self.body.append(r'\emph{')
|
|
|
|
def depart_desc_parameter(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if not node.hasattr('noemph'):
|
|
self.body.append('}')
|
|
|
|
def visit_desc_optional(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxoptional{')
|
|
|
|
def depart_desc_optional(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}')
|
|
|
|
def visit_desc_annotation(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxbfcode{\sphinxupquote{')
|
|
|
|
def depart_desc_annotation(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}}')
|
|
|
|
def visit_desc_content(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if node.children and not isinstance(node.children[0], nodes.paragraph):
|
|
# avoid empty desc environment which causes a formatting bug
|
|
self.body.append('~')
|
|
|
|
def depart_desc_content(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_seealso(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(u'\n\n\\sphinxstrong{%s:}\n\n' % admonitionlabels['seealso'])
|
|
|
|
def depart_seealso(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append("\n\n")
|
|
|
|
def visit_rubric(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if len(node.children) == 1 and node.children[0].astext() in \
|
|
('Footnotes', _('Footnotes')):
|
|
raise nodes.SkipNode
|
|
self.body.append('\\subsubsection*{')
|
|
self.context.append('}\n')
|
|
self.in_title = 1
|
|
|
|
def depart_rubric(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.in_title = 0
|
|
self.body.append(self.context.pop())
|
|
|
|
def visit_footnote(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.in_footnote += 1
|
|
if self.in_parsed_literal:
|
|
self.body.append('\\begin{footnote}[%s]' % node[0].astext())
|
|
else:
|
|
self.body.append('%%\n\\begin{footnote}[%s]' % node[0].astext())
|
|
self.body.append('\\sphinxAtStartFootnote\n')
|
|
|
|
def depart_footnote(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if self.in_parsed_literal:
|
|
self.body.append('\\end{footnote}')
|
|
else:
|
|
self.body.append('%\n\\end{footnote}')
|
|
self.in_footnote -= 1
|
|
|
|
def visit_label(self, node):
|
|
# type: (nodes.Node) -> None
|
|
raise nodes.SkipNode
|
|
|
|
def visit_tabular_col_spec(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.next_table_colspec = node['spec']
|
|
raise nodes.SkipNode
|
|
|
|
def visit_table(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if self.table:
|
|
raise UnsupportedError(
|
|
'%s:%s: nested tables are not yet implemented.' %
|
|
(self.curfilestack[-1], node.line or ''))
|
|
self.table = Table(node)
|
|
if self.next_table_colspec:
|
|
self.table.colspec = '{%s}\n' % self.next_table_colspec
|
|
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):
|
|
# type: (nodes.Node) -> None
|
|
labels = self.hypertarget_to(node)
|
|
table_type = self.table.get_table_type()
|
|
table = self.render(table_type + '.tex_t',
|
|
dict(table=self.table, labels=labels))
|
|
self.body.append("\n\n")
|
|
self.body.append(table)
|
|
self.body.append("\n")
|
|
|
|
self.table = None
|
|
|
|
def visit_colspec(self, node):
|
|
# type: (nodes.Node) -> 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):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_tgroup(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_tgroup(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_thead(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# Redirect head output until header is finished.
|
|
self.pushbody(self.table.header)
|
|
|
|
def depart_thead(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.popbody()
|
|
|
|
def visit_tbody(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# Redirect body output until table is finished.
|
|
self.pushbody(self.table.body)
|
|
|
|
def depart_tbody(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.popbody()
|
|
|
|
def visit_row(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.table.col = 0
|
|
|
|
# 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
|
|
else: # 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('\\sphinxtablestrut{%d}' % cell.cell_id)
|
|
else: # use \multicolumn for wide multirow cell
|
|
self.body.append('\\multicolumn{%d}{|l|}'
|
|
'{\\sphinxtablestrut{%d}}' %
|
|
(cell.width, cell.cell_id))
|
|
|
|
def depart_row(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\\\\n')
|
|
cells = [self.table.cell(self.table.row, i) for i in range(self.table.colcount)]
|
|
underlined = [cell.row + cell.height == self.table.row + 1 for cell in cells]
|
|
if all(underlined):
|
|
self.body.append('\\hline')
|
|
else:
|
|
i = 0
|
|
underlined.extend([False]) # sentinel
|
|
while i < len(underlined):
|
|
if underlined[i] is True:
|
|
j = underlined[i:].index(False)
|
|
self.body.append('\\cline{%d-%d}' % (i + 1, i + j))
|
|
i += j
|
|
i += 1
|
|
self.table.row += 1
|
|
|
|
def visit_entry(self, node):
|
|
# type: (nodes.Node) -> 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()
|
|
context = ''
|
|
if cell.width > 1:
|
|
if self.builder.config.latex_use_latex_multicolumn:
|
|
if self.table.col == 0:
|
|
self.body.append('\\multicolumn{%d}{|l|}{%%\n' % cell.width)
|
|
else:
|
|
self.body.append('\\multicolumn{%d}{l|}{%%\n' % cell.width)
|
|
context = '}%\n'
|
|
else:
|
|
self.body.append('\\sphinxstartmulticolumn{%d}%%\n' % cell.width)
|
|
context = '\\sphinxstopmulticolumn\n'
|
|
if cell.height > 1:
|
|
# \sphinxmultirow 2nd arg "cell_id" will serve as id for LaTeX macros as well
|
|
self.body.append('\\sphinxmultirow{%d}{%d}{%%\n' % (cell.height, cell.cell_id))
|
|
context = '}%\n' + context
|
|
if cell.width > 1 or cell.height > 1:
|
|
self.body.append('\\begin{varwidth}[t]{\\sphinxcolwidth{%d}{%d}}\n'
|
|
% (cell.width, self.table.colcount))
|
|
context = ('\\par\n\\vskip-\\baselineskip'
|
|
'\\vbox{\\hbox{\\strut}}\\end{varwidth}%\n') + context
|
|
self.needs_linetrimming = 1
|
|
if len(node.traverse(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('\\sphinxstyletheadfamily ')
|
|
if self.needs_linetrimming:
|
|
self.pushbody([])
|
|
self.context.append(context)
|
|
|
|
def depart_entry(self, node):
|
|
# type: (nodes.Node) -> 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] == "\n":
|
|
body.pop(0)
|
|
self.body.extend(body)
|
|
|
|
self.body.append(self.context.pop())
|
|
|
|
cell = self.table.cell()
|
|
self.table.col += cell.width
|
|
|
|
# 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
|
|
else: # a bottom part of multirow cell
|
|
self.table.col += nextcell.width
|
|
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('\\sphinxtablestrut{%d}' % nextcell.cell_id)
|
|
else:
|
|
# use \multicolumn for wide multirow cell
|
|
self.body.append('\\multicolumn{%d}{l|}'
|
|
'{\\sphinxtablestrut{%d}}' %
|
|
(nextcell.width, nextcell.cell_id))
|
|
|
|
def visit_acks(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# this is a list in the source, but should be rendered as a
|
|
# comma-separated list here
|
|
self.body.append('\n\n')
|
|
self.body.append(', '.join(n.astext()
|
|
for n in node.children[0].children) + '.')
|
|
self.body.append('\n\n')
|
|
raise nodes.SkipNode
|
|
|
|
def visit_bullet_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if not self.compact_list:
|
|
self.body.append('\\begin{itemize}\n')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_bullet_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if not self.compact_list:
|
|
self.body.append('\\end{itemize}\n')
|
|
|
|
def visit_enumerated_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
def get_enumtype(node):
|
|
# type: (nodes.Node) -> unicode
|
|
enumtype = node.get('enumtype', 'arabic')
|
|
if 'alpha' in enumtype and 26 < node.get('start', 0) + len(node):
|
|
# fallback to arabic if alphabet counter overflows
|
|
enumtype = 'arabic'
|
|
|
|
return enumtype
|
|
|
|
def get_nested_level(node):
|
|
# type: (nodes.Node) -> 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('\\begin{enumerate}\n')
|
|
self.body.append('\\def\\the%s{%s{%s}}\n' % (enum, style, enum))
|
|
self.body.append('\\def\\label%s{%s\\the%s %s}\n' %
|
|
(enum, prefix, enum, suffix))
|
|
self.body.append('\\makeatletter\\def\\p@%s{\\p@%s %s\\the%s %s}\\makeatother\n' %
|
|
(enumnext, enum, prefix, enum, suffix))
|
|
if 'start' in node:
|
|
self.body.append('\\setcounter{%s}{%d}\n' % (enum, node['start'] - 1))
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_enumerated_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{enumerate}\n')
|
|
|
|
def visit_list_item(self, node):
|
|
# type: (nodes.Node) -> 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):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n')
|
|
|
|
def visit_definition_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\begin{description}\n')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_definition_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{description}\n')
|
|
|
|
def visit_definition_list_item(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_definition_list_item(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_term(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.in_term += 1
|
|
ctx = '' # type: unicode
|
|
if node.get('ids'):
|
|
ctx = '\\phantomsection'
|
|
for node_id in node['ids']:
|
|
ctx += self.hypertarget(node_id, anchor=False)
|
|
ctx += '}] \\leavevmode'
|
|
self.body.append('\\item[{')
|
|
self.context.append(ctx)
|
|
|
|
def depart_term(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(self.context.pop())
|
|
self.in_term -= 1
|
|
|
|
def visit_classifier(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('{[}')
|
|
|
|
def depart_classifier(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('{]}')
|
|
|
|
def visit_definition(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_definition(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n')
|
|
|
|
def visit_field_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\begin{quote}\\begin{description}\n')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_field_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{description}\\end{quote}\n')
|
|
|
|
def visit_field(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_field(self, node):
|
|
# type: (nodes.Node) -> 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):
|
|
# type: (nodes.Node) -> 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('\\noindent\n')
|
|
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:
|
|
self.body.append('\n')
|
|
|
|
def depart_paragraph(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n')
|
|
|
|
def visit_centered(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\\begin{center}')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_centered(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\\end{center}')
|
|
|
|
def visit_hlist(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# for now, we don't support a more compact list format
|
|
# don't add individual itemize environments, but one for all columns
|
|
self.compact_list += 1
|
|
self.body.append('\\begin{itemize}\\setlength{\\itemsep}{0pt}'
|
|
'\\setlength{\\parskip}{0pt}\n')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_hlist(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.compact_list -= 1
|
|
self.body.append('\\end{itemize}\n')
|
|
|
|
def visit_hlistcol(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_hlistcol(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def latex_image_length(self, width_str, scale = 100):
|
|
# type: (nodes.Node, int) -> unicode
|
|
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):
|
|
# type: (nodes.Node) -> bool
|
|
"""Check whether a node represents an inline element."""
|
|
return isinstance(node.parent, nodes.TextElement)
|
|
|
|
def visit_image(self, node):
|
|
# type: (nodes.Node) -> None
|
|
attrs = node.attributes
|
|
pre = [] # type: List[unicode]
|
|
# in reverse order
|
|
post = [] # type: List[unicode]
|
|
include_graphics_options = []
|
|
is_inline = self.is_inline(node)
|
|
if 'width' in attrs:
|
|
if 'scale' in attrs:
|
|
w = self.latex_image_length(attrs['width'], attrs['scale'])
|
|
else:
|
|
w = self.latex_image_length(attrs['width'])
|
|
if w:
|
|
include_graphics_options.append('width=%s' % w)
|
|
if 'height' in attrs:
|
|
if 'scale' in attrs:
|
|
h = self.latex_image_length(attrs['height'], attrs['scale'])
|
|
else:
|
|
h = self.latex_image_length(attrs['height'])
|
|
if h:
|
|
include_graphics_options.append('height=%s' % h)
|
|
if 'scale' in attrs:
|
|
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(attrs['scale']) / 100.0))
|
|
if 'align' in attrs:
|
|
align_prepost = {
|
|
# By default latex aligns the top of an image.
|
|
(1, 'top'): ('', ''),
|
|
(1, 'middle'): ('\\raisebox{-0.5\\height}{', '}'),
|
|
(1, 'bottom'): ('\\raisebox{-\\height}{', '}'),
|
|
(0, 'center'): ('{\\hspace*{\\fill}', '\\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'): ('{', '\\hspace*{\\fill}}'),
|
|
(0, 'right'): ('{\\hspace*{\\fill}', '}'),
|
|
}
|
|
try:
|
|
pre.append(align_prepost[is_inline, attrs['align']][0])
|
|
post.append(align_prepost[is_inline, attrs['align']][1])
|
|
except KeyError:
|
|
pass
|
|
if self.in_parsed_literal:
|
|
pre.append('{\\sphinxunactivateextrasandspace ')
|
|
post.append('}')
|
|
if not is_inline:
|
|
pre.append('\n\\noindent')
|
|
post.append('\n')
|
|
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 unexpectly (ex. WIDTH=...).
|
|
self.body.append('\\lowercase{\\sphinxincludegraphics%s}{{%s}%s}' %
|
|
(options, base, ext))
|
|
else:
|
|
self.body.append('\\sphinxincludegraphics%s{{%s}%s}' %
|
|
(options, base, ext))
|
|
self.body.extend(post)
|
|
|
|
def depart_image(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_figure(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if self.table:
|
|
# TODO: support align option
|
|
if 'width' in node:
|
|
length = self.latex_image_length(node['width'])
|
|
if length:
|
|
self.body.append('\\begin{sphinxfigure-in-table}[%s]\n'
|
|
'\\centering\n' % length)
|
|
else:
|
|
self.body.append('\\begin{sphinxfigure-in-table}\n\\centering\n')
|
|
if any(isinstance(child, nodes.caption) for child in node):
|
|
self.body.append('\\capstart')
|
|
self.context.append('\\end{sphinxfigure-in-table}\\relax\n')
|
|
elif node.get('align', '') in ('left', 'right'):
|
|
length = None
|
|
if 'width' in node:
|
|
length = self.latex_image_length(node['width'])
|
|
elif 'width' in node[0]:
|
|
length = self.latex_image_length(node[0]['width'])
|
|
self.body.append('\\begin{wrapfigure}{%s}{%s}\n\\centering' %
|
|
(node['align'] == 'right' and 'r' or 'l', length or '0pt'))
|
|
self.context.append('\\end{wrapfigure}\n')
|
|
elif self.in_minipage:
|
|
self.body.append('\n\\begin{center}')
|
|
self.context.append('\\end{center}\n')
|
|
else:
|
|
self.body.append('\n\\begin{figure}[%s]\n\\centering\n' %
|
|
self.elements['figure_align'])
|
|
if any(isinstance(child, nodes.caption) for child in node):
|
|
self.body.append('\\capstart\n')
|
|
self.context.append('\\end{figure}\n')
|
|
|
|
def depart_figure(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(self.context.pop())
|
|
|
|
def visit_caption(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.in_caption += 1
|
|
if isinstance(node.parent, captioned_literal_block):
|
|
self.body.append('\\sphinxSetupCaptionForVerbatim{')
|
|
elif self.in_minipage and isinstance(node.parent, nodes.figure):
|
|
self.body.append('\\captionof{figure}{')
|
|
elif self.table and node.parent.tagname == 'figure':
|
|
self.body.append('\\sphinxfigcaption{')
|
|
else:
|
|
self.body.append('\\caption{')
|
|
|
|
def depart_caption(self, node):
|
|
# type: (nodes.Node) -> 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):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\\begin{sphinxlegend}')
|
|
|
|
def depart_legend(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{sphinxlegend}\n')
|
|
|
|
def visit_admonition(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\\begin{sphinxadmonition}{note}')
|
|
|
|
def depart_admonition(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{sphinxadmonition}\n')
|
|
|
|
def _make_visit_admonition(name):
|
|
# type: (unicode) -> Callable[[LaTeXTranslator, nodes.Node], None]
|
|
def visit_admonition(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(u'\n\\begin{sphinxadmonition}{%s}{%s:}' %
|
|
(name, admonitionlabels[name]))
|
|
return visit_admonition
|
|
|
|
def _depart_named_admonition(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{sphinxadmonition}\n')
|
|
|
|
visit_attention = _make_visit_admonition('attention')
|
|
depart_attention = _depart_named_admonition
|
|
visit_caution = _make_visit_admonition('caution')
|
|
depart_caution = _depart_named_admonition
|
|
visit_danger = _make_visit_admonition('danger')
|
|
depart_danger = _depart_named_admonition
|
|
visit_error = _make_visit_admonition('error')
|
|
depart_error = _depart_named_admonition
|
|
visit_hint = _make_visit_admonition('hint')
|
|
depart_hint = _depart_named_admonition
|
|
visit_important = _make_visit_admonition('important')
|
|
depart_important = _depart_named_admonition
|
|
visit_note = _make_visit_admonition('note')
|
|
depart_note = _depart_named_admonition
|
|
visit_tip = _make_visit_admonition('tip')
|
|
depart_tip = _depart_named_admonition
|
|
visit_warning = _make_visit_admonition('warning')
|
|
depart_warning = _depart_named_admonition
|
|
|
|
def visit_versionmodified(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_versionmodified(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_target(self, node):
|
|
# type: (nodes.Node) -> None
|
|
def add_target(id):
|
|
# type: (unicode) -> 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('\n')
|
|
|
|
# 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
|
|
while isinstance(next_node, nodes.target):
|
|
next_node = next_node.next_node(ascend=True)
|
|
|
|
domain = self.builder.env.get_domain('std')
|
|
if isinstance(next_node, HYPERLINK_SUPPORT_NODES):
|
|
return
|
|
elif domain.get_enumerable_node_type(next_node) and domain.get_numfig_title(next_node):
|
|
return
|
|
|
|
if 'refuri' in node:
|
|
return
|
|
if node.get('refid'):
|
|
add_target(node['refid'])
|
|
for id in node['ids']:
|
|
add_target(id)
|
|
|
|
def depart_target(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_attribution(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\\begin{flushright}\n')
|
|
self.body.append('---')
|
|
|
|
def depart_attribution(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\\end{flushright}\n')
|
|
|
|
def visit_index(self, node, scre = None):
|
|
# type: (nodes.Node, None) -> None
|
|
def escape(value):
|
|
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('!', '"!')
|
|
return value
|
|
|
|
def style(string):
|
|
match = EXTRA_RE.match(string)
|
|
if match:
|
|
return match.expand(r'\\spxentry{\1}\\spxextra{\2}')
|
|
else:
|
|
return '\\spxentry{%s}' % string
|
|
|
|
if scre:
|
|
warnings.warn(('LaTeXTranslator.visit_index() optional argument '
|
|
'"scre" is deprecated. It is ignored.'),
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
if not node.get('inline', True):
|
|
self.body.append('\n')
|
|
entries = node['entries']
|
|
for type, string, tid, ismain, key_ in entries:
|
|
m = ''
|
|
if ismain:
|
|
m = '|spxpagem'
|
|
try:
|
|
if type == 'single':
|
|
try:
|
|
p1, p2 = [escape(x) for x in split_into(2, 'single', string)]
|
|
P1, P2 = style(p1), style(p2)
|
|
self.body.append(r'\index{%s@%s!%s@%s%s}' % (p1, P1, p2, P2, m))
|
|
except ValueError:
|
|
p = escape(split_into(1, 'single', string)[0])
|
|
P = style(p)
|
|
self.body.append(r'\index{%s@%s%s}' % (p, P, m))
|
|
elif type == 'pair':
|
|
p1, p2 = [escape(x) for x in split_into(2, 'pair', string)]
|
|
P1, P2 = style(p1), style(p2)
|
|
self.body.append(r'\index{%s@%s!%s@%s%s}\index{%s@%s!%s@%s%s}' %
|
|
(p1, P1, p2, P2, m, p2, P2, p1, P1, m))
|
|
elif type == 'triple':
|
|
p1, p2, p3 = [escape(x) for x in split_into(3, 'triple', string)]
|
|
P1, P2, P3 = style(p1), style(p2), style(p3)
|
|
self.body.append(
|
|
r'\index{%s@%s!%s %s@%s %s%s}'
|
|
r'\index{%s@%s!%s, %s@%s, %s%s}'
|
|
r'\index{%s@%s!%s %s@%s %s%s}' %
|
|
(p1, P1, p2, p3, P2, P3, m,
|
|
p2, P2, p3, p1, P3, P1, m,
|
|
p3, P3, p1, p2, P1, P2, m))
|
|
elif type == 'see':
|
|
p1, p2 = [escape(x) for x in split_into(2, 'see', string)]
|
|
P1 = style(p1)
|
|
self.body.append(r'\index{%s@%s|see{%s}}' % (p1, P1, p2))
|
|
elif type == 'seealso':
|
|
p1, p2 = [escape(x) for x in split_into(2, 'seealso', string)]
|
|
P1 = style(p1)
|
|
self.body.append(r'\index{%s@%s|see{%s}}' % (p1, P1, 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('\\ignorespaces ')
|
|
raise nodes.SkipNode
|
|
|
|
def visit_raw(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if not self.is_inline(node):
|
|
self.body.append('\n')
|
|
if 'latex' in node.get('format', '').split():
|
|
self.body.append(node.astext())
|
|
if not self.is_inline(node):
|
|
self.body.append('\n')
|
|
raise nodes.SkipNode
|
|
|
|
def visit_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if not self.in_title:
|
|
for id in node.get('ids'):
|
|
anchor = not self.in_caption
|
|
self.body += self.hypertarget(id, anchor=anchor)
|
|
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'\emph{')
|
|
if self.builder.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 hasattr(node[0], 'attributes') 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.builder.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('\\sphinxnolinkurl{%s}' % self.encode_uri(uri))
|
|
else:
|
|
self.body.append('\\sphinxurl{%s}' % self.encode_uri(uri))
|
|
raise nodes.SkipNode
|
|
else:
|
|
self.body.append('\\sphinxhref{%s}{' % self.encode_uri(uri))
|
|
self.context.append('}')
|
|
|
|
def depart_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(self.context.pop())
|
|
|
|
def visit_number_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if node.get('refid'):
|
|
id = self.curfilestack[-1] + ':' + node['refid']
|
|
else:
|
|
id = node.get('refuri', '')[1:].replace('#', ':')
|
|
|
|
title = node.get('title', '%s')
|
|
title = text_type(title).translate(tex_escape_map).replace('\\%s', '%s')
|
|
if '\\{name\\}' in title or '\\{number\\}' in title:
|
|
# new style format (cf. "Fig.%{number}")
|
|
title = title.replace('\\{name\\}', '{name}').replace('\\{number\\}', '{number}')
|
|
text = escape_abbr(title).format(name='\\nameref{%s}' % self.idescape(id),
|
|
number='\\ref{%s}' % self.idescape(id))
|
|
else:
|
|
# old style format (cf. "Fig.%{number}")
|
|
text = escape_abbr(title) % ('\\ref{%s}' % self.idescape(id))
|
|
hyperref = '\\hyperref[%s]{%s}' % (self.idescape(id), text)
|
|
self.body.append(hyperref)
|
|
|
|
raise nodes.SkipNode
|
|
|
|
def visit_download_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_download_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_pending_xref(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_pending_xref(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_emphasis(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxstyleemphasis{')
|
|
|
|
def depart_emphasis(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}')
|
|
|
|
def visit_literal_emphasis(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxstyleliteralemphasis{\sphinxupquote{')
|
|
self.no_contractions += 1
|
|
|
|
def depart_literal_emphasis(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}}')
|
|
self.no_contractions -= 1
|
|
|
|
def visit_strong(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxstylestrong{')
|
|
|
|
def depart_strong(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}')
|
|
|
|
def visit_literal_strong(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxstyleliteralstrong{\sphinxupquote{')
|
|
self.no_contractions += 1
|
|
|
|
def depart_literal_strong(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}}')
|
|
self.no_contractions -= 1
|
|
|
|
def visit_abbreviation(self, node):
|
|
# type: (nodes.Node) -> 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):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(self.context.pop())
|
|
|
|
def visit_manpage(self, node):
|
|
# type: (nodes.Node) -> Any
|
|
return self.visit_literal_emphasis(node)
|
|
|
|
def depart_manpage(self, node):
|
|
# type: (nodes.Node) -> Any
|
|
return self.depart_literal_emphasis(node)
|
|
|
|
def visit_title_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(r'\sphinxtitleref{')
|
|
|
|
def depart_title_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}')
|
|
|
|
def visit_thebibliography(self, node):
|
|
# type: (nodes.Node) -> None
|
|
longest_label = max((subnode[0].astext() for subnode in node), 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(u'\n\\begin{sphinxthebibliography}{%s}\n' %
|
|
self.encode(longest_label))
|
|
|
|
def depart_thebibliography(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(u'\\end{sphinxthebibliography}\n')
|
|
|
|
def visit_citation(self, node):
|
|
# type: (nodes.Node) -> None
|
|
label = node[0].astext()
|
|
self.body.append(u'\\bibitem[%s]{%s:%s}' %
|
|
(self.encode(label), node['docname'], node['ids'][0]))
|
|
|
|
def depart_citation(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_citation_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if self.in_title:
|
|
pass
|
|
else:
|
|
self.body.append('\\sphinxcite{%s:%s}' % (node['docname'], node['refname']))
|
|
raise nodes.SkipNode
|
|
|
|
def depart_citation_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_literal(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.no_contractions += 1
|
|
if self.in_title:
|
|
self.body.append(r'\sphinxstyleliteralintitle{\sphinxupquote{')
|
|
else:
|
|
self.body.append(r'\sphinxcode{\sphinxupquote{')
|
|
|
|
def depart_literal(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.no_contractions -= 1
|
|
self.body.append('}}')
|
|
|
|
def visit_footnote_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
raise nodes.SkipNode
|
|
|
|
def visit_footnotemark(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\sphinxfootnotemark[')
|
|
|
|
def depart_footnotemark(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(']')
|
|
|
|
def visit_footnotetext(self, node):
|
|
# type: (nodes.Node) -> None
|
|
number = node[0].astext()
|
|
self.body.append('%%\n\\begin{footnotetext}[%s]'
|
|
'\\sphinxAtStartFootnote\n' % number)
|
|
|
|
def depart_footnotetext(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# the \ignorespaces in particular for after table header use
|
|
self.body.append('%\n\\end{footnotetext}\\ignorespaces ')
|
|
|
|
def visit_captioned_literal_block(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_captioned_literal_block(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_literal_block(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if node.rawsource != node.astext():
|
|
# most probably a parsed-literal block -- don't highlight
|
|
self.in_parsed_literal += 1
|
|
self.body.append('\\begin{sphinxalltt}\n')
|
|
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('\n\\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_highlighting', False)
|
|
if lang is self.builder.config.highlight_language:
|
|
# only pass highlighter options for original language
|
|
opts = self.builder.config.highlight_options
|
|
else:
|
|
opts = {}
|
|
|
|
hlcode = self.highlighter.highlight_block(
|
|
node.rawsource, lang, opts=opts, linenos=linenos,
|
|
location=(self.curfilestack[-1], node.line), **highlight_args
|
|
)
|
|
# workaround for Unicode issue
|
|
hlcode = hlcode.replace(u'€', u'@texteuro[]')
|
|
if self.in_footnote:
|
|
self.body.append('\n\\sphinxSetupCodeBlockInFootnote')
|
|
hlcode = hlcode.replace('\\begin{Verbatim}',
|
|
'\\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('\\begin{Verbatim}',
|
|
'\\begin{sphinxVerbatimintable}')
|
|
else:
|
|
hlcode = hlcode.replace('\\begin{Verbatim}',
|
|
'\\begin{sphinxVerbatim}')
|
|
# get consistent trailer
|
|
hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim}
|
|
if self.table and not self.in_footnote:
|
|
hlcode += '\\end{sphinxVerbatimintable}'
|
|
else:
|
|
hlcode += '\\end{sphinxVerbatim}'
|
|
|
|
hllines = str(highlight_args.get('hl_lines', []))[1:-1]
|
|
if hllines:
|
|
self.body.append('\n\\fvset{hllines={, %s,}}%%' % hllines)
|
|
self.body.append('\n' + hlcode + '\n')
|
|
if hllines:
|
|
self.body.append('\\sphinxresetverbatimhllines\n')
|
|
raise nodes.SkipNode
|
|
|
|
def depart_literal_block(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n\\end{sphinxalltt}\n')
|
|
self.in_parsed_literal -= 1
|
|
visit_doctest_block = visit_literal_block
|
|
depart_doctest_block = depart_literal_block
|
|
|
|
def visit_line(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\item[] ')
|
|
|
|
def depart_line(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n')
|
|
|
|
def visit_line_block(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if isinstance(node.parent, nodes.line_block):
|
|
self.body.append('\\item[]\n'
|
|
'\\begin{DUlineblock}{\\DUlineblockindent}\n')
|
|
else:
|
|
self.body.append('\n\\begin{DUlineblock}{0em}\n')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_line_block(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{DUlineblock}\n')
|
|
|
|
def visit_block_quote(self, node):
|
|
# type: (nodes.Node) -> 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) or \
|
|
isinstance(child, nodes.enumerated_list):
|
|
done = 1
|
|
if not done:
|
|
self.body.append('\\begin{quote}\n')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_block_quote(self, node):
|
|
# type: (nodes.Node) -> None
|
|
done = 0
|
|
if len(node.children) == 1:
|
|
child = node.children[0]
|
|
if isinstance(child, nodes.bullet_list) or \
|
|
isinstance(child, nodes.enumerated_list):
|
|
done = 1
|
|
if not done:
|
|
self.body.append('\\end{quote}\n')
|
|
|
|
# option node handling copied from docutils' latex writer
|
|
|
|
def visit_option(self, node):
|
|
# type: (nodes.Node) -> None
|
|
if self.context[-1]:
|
|
# this is not the first option
|
|
self.body.append(', ')
|
|
|
|
def depart_option(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# flag that the first option is done.
|
|
self.context[-1] += 1
|
|
|
|
def visit_option_argument(self, node):
|
|
# type: (nodes.Node) -> None
|
|
"""The delimiter betweeen an option and its argument."""
|
|
self.body.append(node.get('delimiter', ' '))
|
|
|
|
def depart_option_argument(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_option_group(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\item [')
|
|
# flag for first option
|
|
self.context.append(0)
|
|
|
|
def depart_option_group(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.context.pop() # the flag
|
|
self.body.append('] ')
|
|
|
|
def visit_option_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\begin{optionlist}{3cm}\n')
|
|
if self.table:
|
|
self.table.has_problematic = True
|
|
|
|
def depart_option_list(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\\end{optionlist}\n')
|
|
|
|
def visit_option_list_item(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_option_list_item(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_option_string(self, node):
|
|
# type: (nodes.Node) -> None
|
|
ostring = node.astext()
|
|
self.no_contractions += 1
|
|
self.body.append(self.encode(ostring))
|
|
self.no_contractions -= 1
|
|
raise nodes.SkipNode
|
|
|
|
def visit_description(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(' ')
|
|
|
|
def depart_description(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_superscript(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('$^{\\text{')
|
|
|
|
def depart_superscript(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}}$')
|
|
|
|
def visit_subscript(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('$_{\\text{')
|
|
|
|
def depart_subscript(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('}}$')
|
|
|
|
def visit_inline(self, node):
|
|
# type: (nodes.Node) -> 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):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append(self.context.pop())
|
|
|
|
def visit_generated(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_generated(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_compound(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_compound(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_container(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_container(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_decoration(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_decoration(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
# docutils-generated elements that we don't support
|
|
|
|
def visit_header(self, node):
|
|
# type: (nodes.Node) -> None
|
|
raise nodes.SkipNode
|
|
|
|
def visit_footer(self, node):
|
|
# type: (nodes.Node) -> None
|
|
raise nodes.SkipNode
|
|
|
|
def visit_docinfo(self, node):
|
|
# type: (nodes.Node) -> None
|
|
raise nodes.SkipNode
|
|
|
|
# text handling
|
|
|
|
def encode(self, text):
|
|
# type: (unicode) -> unicode
|
|
text = text_type(text).translate(tex_escape_map)
|
|
if self.literal_whitespace:
|
|
# Insert a blank before the newline, to avoid
|
|
# ! LaTeX Error: There's no line here to end.
|
|
text = text.replace(u'\n', u'~\\\\\n').replace(u' ', u'~')
|
|
if self.no_contractions:
|
|
text = text.replace('--', u'-{-}')
|
|
text = text.replace("''", u"'{'}")
|
|
return text
|
|
|
|
def encode_uri(self, text):
|
|
# type: (unicode) -> unicode
|
|
# in \href, the tilde is allowed and must be represented literally
|
|
return self.encode(text).replace('\\textasciitilde{}', '~')
|
|
|
|
def visit_Text(self, node):
|
|
# type: (nodes.Node) -> None
|
|
text = self.encode(node.astext())
|
|
self.body.append(text)
|
|
|
|
def depart_Text(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def visit_comment(self, node):
|
|
# type: (nodes.Node) -> None
|
|
raise nodes.SkipNode
|
|
|
|
def visit_meta(self, node):
|
|
# type: (nodes.Node) -> None
|
|
# only valid for HTML
|
|
raise nodes.SkipNode
|
|
|
|
def visit_system_message(self, node):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def depart_system_message(self, node):
|
|
# type: (nodes.Node) -> None
|
|
self.body.append('\n')
|
|
|
|
def visit_math(self, node):
|
|
# type: (nodes.Node) -> 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):
|
|
# type: (nodes.Node) -> None
|
|
if node.get('label'):
|
|
label = "equation:%s:%s" % (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.builder.config.math_number_all))
|
|
raise nodes.SkipNode
|
|
|
|
def visit_math_reference(self, node):
|
|
# type: (nodes.Node) -> None
|
|
label = "equation:%s:%s" % (node['docname'], node['target'])
|
|
eqref_format = self.builder.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):
|
|
# type: (nodes.Node) -> None
|
|
pass
|
|
|
|
def unknown_visit(self, node):
|
|
# type: (nodes.Node) -> None
|
|
raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
|
|
|
|
# --------- METHODS FOR COMPATIBILITY --------------------------------------
|
|
|
|
@property
|
|
def footnotestack(self):
|
|
# type: () -> List[Dict[unicode, List[Union[collected_footnote, bool]]]]
|
|
warnings.warn('LaTeXWriter.footnotestack is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return []
|
|
|
|
@property
|
|
def bibitems(self):
|
|
# type: () -> List[List[unicode]]
|
|
warnings.warn('LaTeXTranslator.bibitems() is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return []
|
|
|
|
@property
|
|
def in_container_literal_block(self):
|
|
# type: () -> int
|
|
warnings.warn('LaTeXTranslator.in_container_literal_block is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return 0
|
|
|
|
@property
|
|
def next_section_ids(self):
|
|
# type: () -> Set[unicode]
|
|
warnings.warn('LaTeXTranslator.next_section_ids is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return set()
|
|
|
|
@property
|
|
def next_hyperlink_ids(self):
|
|
# type: () -> Dict
|
|
warnings.warn('LaTeXTranslator.next_hyperlink_ids is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return {}
|
|
|
|
def push_hyperlink_ids(self, figtype, ids):
|
|
# type: (unicode, Set[unicode]) -> None
|
|
warnings.warn('LaTeXTranslator.push_hyperlink_ids() is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
pass
|
|
|
|
def pop_hyperlink_ids(self, figtype):
|
|
# type: (unicode) -> Set[unicode]
|
|
warnings.warn('LaTeXTranslator.pop_hyperlink_ids() is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return set()
|
|
|
|
@property
|
|
def hlsettingstack(self):
|
|
# type: () -> List[List[Union[unicode, int]]]
|
|
warnings.warn('LaTeXTranslator.hlsettingstack is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
return [[self.builder.config.highlight_language, sys.maxsize]]
|
|
|
|
def check_latex_elements(self):
|
|
# type: () -> None
|
|
warnings.warn('check_latex_elements() is deprecated.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
|
|
for key in self.builder.config.latex_elements:
|
|
if key not in self.elements:
|
|
msg = __("Unknown configure key: latex_elements[%r] is ignored.")
|
|
logger.warning(msg % key)
|
|
|
|
|
|
# Import old modules here for compatibility
|
|
# They should be imported after `LaTeXTranslator` to avoid recursive import.
|
|
#
|
|
# refs: https://github.com/sphinx-doc/sphinx/issues/4889
|
|
from sphinx.builders.latex.transforms import URI_SCHEMES, ShowUrlsTransform # NOQA
|
|
|
|
# FIXME: Workaround to avoid circular import
|
|
# refs: https://github.com/sphinx-doc/sphinx/issues/5433
|
|
from sphinx.builders.latex.nodes import HYPERLINK_SUPPORT_NODES, captioned_literal_block, footnotetext # NOQA
|