sphinx/sphinx/writers/latex.py
2018-07-16 21:03:32 +09:00

2584 lines
98 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-2018 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import re
import sys
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.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__)
BEGIN_DOC = r'''
\begin{document}
%(shorthandoff)s
%(maketitle)s
%(tableofcontents)s
'''
LATEXSECTIONNAMES = ["part", "chapter", "section", "subsection",
"subsubsection", "paragraph", "subparagraph"]
HYPERLINK_SUPPORT_NODES = (
nodes.figure,
nodes.literal_block,
nodes.table,
nodes.section,
)
DEFAULT_SETTINGS = {
'latex_engine': 'pdflatex',
'papersize': 'letterpaper',
'pointsize': '10pt',
'pxunit': '.75bp',
'classoptions': '',
'extraclassoptions': '',
'maxlistdepth': '',
'sphinxpkgoptions': '',
'sphinxsetup': '',
'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': '\\maketitle',
'tableofcontents': '\\sphinxtableofcontents',
'atendofbody': '',
'printindex': '\\printindex',
'transition': '\n\n\\bigskip\\hrule\\bigskip\n\n',
'figure_align': 'htbp',
'tocdepth': '',
'secnumdepth': '',
'pageautorefname': '',
'literalblockpto': '',
} # type: Dict[unicode, unicode]
ADDITIONAL_SETTINGS = {
'pdflatex': {
'inputenc': '\\usepackage[utf8]{inputenc}',
'utf8extra': ('\\ifdefined\\DeclareUnicodeCharacter\n'
' \\ifdefined\\DeclareUnicodeCharacterAsOptional\n'
' \\DeclareUnicodeCharacter{"00A0}{\\nobreakspace}\n'
' \\DeclareUnicodeCharacter{"2500}{\\sphinxunichar{2500}}\n'
' \\DeclareUnicodeCharacter{"2502}{\\sphinxunichar{2502}}\n'
' \\DeclareUnicodeCharacter{"2514}{\\sphinxunichar{2514}}\n'
' \\DeclareUnicodeCharacter{"251C}{\\sphinxunichar{251C}}\n'
' \\DeclareUnicodeCharacter{"2572}{\\textbackslash}\n'
' \\else\n'
' \\DeclareUnicodeCharacter{00A0}{\\nobreakspace}\n'
' \\DeclareUnicodeCharacter{2500}{\\sphinxunichar{2500}}\n'
' \\DeclareUnicodeCharacter{2502}{\\sphinxunichar{2502}}\n'
' \\DeclareUnicodeCharacter{2514}{\\sphinxunichar{2514}}\n'
' \\DeclareUnicodeCharacter{251C}{\\sphinxunichar{251C}}\n'
' \\DeclareUnicodeCharacter{2572}{\\textbackslash}\n'
' \\fi\n'
'\\fi'),
},
'xelatex': {
'latex_engine': 'xelatex',
'polyglossia': '\\usepackage{polyglossia}',
'babel': '',
'fontenc': '\\usepackage{fontspec}',
'fontpkg': '',
'utf8extra': ('\\catcode`^^^^00a0\\active\\protected\\def^^^^00a0'
'{\\leavevmode\\nobreak\\ }'),
},
'lualatex': {
'latex_engine': 'lualatex',
'polyglossia': '\\usepackage{polyglossia}',
'babel': '',
'fontenc': '\\usepackage{fontspec}',
'fontpkg': '',
'utf8extra': ('\\catcode`^^^^00a0\\active\\protected\\def^^^^00a0'
'{\\leavevmode\\nobreak\\ }'),
},
'platex': {
'latex_engine': 'platex',
'geometry': '\\usepackage[dvipdfm]{geometry}',
},
} # type: Dict[unicode, Dict[unicode, unicode]]
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
transform = ShowUrlsTransform(self.document)
transform.apply()
visitor = self.builder.create_translator(self.document, self.builder)
self.document.walkabout(visitor)
self.output = visitor.astext()
# Helper classes
class ExtBabel(Babel):
def __init__(self, language_code, use_polyglossia=False):
# type: (unicode, bool) -> None
super(ExtBabel, self).__init__(language_code or '')
self.language_code = language_code
self.use_polyglossia = use_polyglossia
def get_shorthandoff(self):
# type: () -> unicode
return ('\\ifdefined\\shorthandoff\n'
' \\ifnum\\catcode`\\=\\string=\\active\\shorthandoff{=}\\fi\n'
' \\ifnum\\catcode`\\"=\\active\\shorthandoff{"}\\fi\n'
'\\fi')
def uses_cyrillic(self):
# type: () -> bool
shortlang = self.language.split('_')[0]
return shortlang in ('bg', 'bulgarian', 'kk', 'kazakh',
'mn', 'mongolian', 'ru', 'russian',
'uk', 'ukrainian')
def is_supported_language(self):
# type: () -> bool
return bool(super(ExtBabel, self).get_language())
def get_language(self):
# type: () -> unicode
language = super(ExtBabel, self).get_language()
if language == 'ngerman' and self.use_polyglossia:
# polyglossia calls new orthography (Neue Rechtschreibung) as
# german (with new spelling option).
return 'german'
elif not language:
return 'english' # fallback to english
else:
return language
def get_mainlanguage_options(self):
# type: () -> unicode
"""Return options for polyglossia's ``\\setmainlanguage``."""
language = super(ExtBabel, self).get_language()
if self.use_polyglossia is False:
return None
elif language == 'ngerman':
return 'spelling=new'
elif language == 'german':
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.caption_footnotetexts = [] # type: List[unicode]
self.header_footnotetexts = [] # 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
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):
# type: (unicode) -> 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]
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)
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_container_literal_block = 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.check_latex_elements()
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'),
})
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'), # type: ignore # NOQA
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 '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)
# simply use babel.get_language() always, as get_language() returns
# 'english' even if language is invalid or empty
self.elements['classoptions'] += ',' + self.babel.get_language()
# set up multilingual module...
# 'babel' key is public and user setting must be obeyed
if self.elements['babel']:
# this branch is not taken for xelatex/lualatex if default settings
self.elements['multilingual'] = self.elements['babel']
if builder.config.language:
self.elements['shorthandoff'] = self.babel.get_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'] = ''
# pTeX (Japanese TeX) for support
if builder.config.language == 'ja':
# use dvipdfmx as default class option in Japanese
self.elements['classoptions'] = ',dvipdfmx'
# disable babel which has not publishing quality in Japanese
self.elements['babel'] = ''
self.elements['shorthandoff'] = ''
self.elements['multilingual'] = ''
# disable fncychap in Japanese documents
self.elements['fncychap'] = ''
elif self.elements['polyglossia']:
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['literalblockpto'] = (
self.babel_renewcommand(
'\\literalblockcontinuedname', self.encode(_('continued from previous page'))
) +
self.babel_renewcommand(
'\\literalblockcontinuesname', self.encode(_('continues on next page'))
)
)
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, builder.config.trim_doctest_flags)
self.context = [] # type: List[Any]
self.descstack = [] # type: List[unicode]
self.bibitems = [] # type: List[List[unicode]]
self.table = None # type: Table
self.next_table_colspec = None # type: unicode
# stack of [language, linenothreshold] settings per file
# the first item here is the default and must not be changed
# the second item is the default for the master file and can be changed
# by .. highlight:: directive in the master file
self.hlsettingstack = 2 * [[builder.config.highlight_language,
sys.maxsize]]
self.bodystack = [] # type: List[List[unicode]]
self.footnotestack = [] # type: List[Dict[unicode, List[Union[collected_footnote, bool]]]] # NOQA
self.footnote_restricted = False
self.pending_footnotes = [] # type: List[nodes.footnote_reference]
self.curfilestack = [] # type: List[unicode]
self.handled_abbrs = set() # type: Set[unicode]
self.next_hyperlink_ids = {} # type: Dict[unicode, Set[unicode]]
self.next_section_ids = 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 push_hyperlink_ids(self, figtype, ids):
# type: (unicode, Set[unicode]) -> None
hyperlink_ids = self.next_hyperlink_ids.setdefault(figtype, set())
hyperlink_ids.update(ids)
def pop_hyperlink_ids(self, figtype):
# type: (unicode) -> Set[unicode]
return self.next_hyperlink_ids.pop(figtype, set())
def check_latex_elements(self):
# type: () -> None
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)
def restrict_footnote(self, node):
# type: (nodes.Node) -> None
if self.footnote_restricted is False:
self.footnote_restricted = node
self.pending_footnotes = []
def unrestrict_footnote(self, node):
# type: (nodes.Node) -> None
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 = text_type(figure[0]).strip().translate(tex_escape_map)
ret.append(self.babel_renewcommand('\\figurename', definition))
if figure[1]:
ret.append('\\makeatletter\n')
ret.append('\\def\\fnum@figure{\\figurename\\thefigure%s}\n' %
text_type(figure[1]).strip().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 = text_type(table[0]).strip().translate(tex_escape_map)
ret.append(self.babel_renewcommand('\\tablename', definition))
if table[1]:
ret.append('\\makeatletter\n')
ret.append('\\def\\fnum@table{\\tablename\\thetable%s}\n' %
text_type(table[1]).strip().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('\\def\\bigletter#1{{\\Large\\sffamily#1}'
'\\nopagebreak\\vspace{1mm}}\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 {\\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.footnotestack.append(self.collect_footnotes(node))
self.curfilestack.append(node.get('docname', ''))
if self.first_document == 1:
# the first document is all the regular content ...
self.body.append(BEGIN_DOC % self.elements)
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
if self.bibitems:
widest_label = "" # type: unicode
for bi in self.bibitems:
if len(widest_label) < len(bi[0]):
widest_label = bi[0]
self.body.append(u'\n\\begin{sphinxthebibliography}{%s}\n' % widest_label)
for bi in self.bibitems:
target = self.hypertarget(bi[2] + ':' + bi[3],
withdoc=False)
self.body.append(u'\\bibitem[%s]{%s}{%s %s}\n' %
(self.encode(bi[0]), self.idescape(bi[0]),
target, bi[1]))
self.body.append(u'\\end{sphinxthebibliography}\n')
self.bibitems = []
def visit_start_of_file(self, node):
# type: (nodes.Node) -> None
# collect new footnotes
self.footnotestack.append(self.collect_footnotes(node))
# also add a document target
self.next_section_ids.add(':doc')
self.curfilestack.append(node['docname'])
# use default highlight settings for new file
self.hlsettingstack.append(self.hlsettingstack[0])
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.footnotestack.pop()
self.curfilestack.pop()
self.hlsettingstack.pop()
def visit_highlightlang(self, node):
# type: (nodes.Node) -> None
self.hlsettingstack[-1] = [node['lang'], node['linenothreshold']]
raise nodes.SkipNode
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))
self.restrict_footnote(node)
if self.next_section_ids:
for id in self.next_section_ids:
self.context[-1] += self.hypertarget(id, anchor=False)
self.next_section_ids.clear()
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([])
self.restrict_footnote(node)
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()
# temporary buffer for footnotes from caption
self.pushbody([])
self.unrestrict_footnote(node)
# the footnote texts from caption
self.table.caption_footnotetexts = self.popbody()
else:
self.body.append(self.context.pop())
self.unrestrict_footnote(node)
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('\\paragraph{')
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
raise nodes.SkipNode
def visit_collected_footnote(self, node):
# type: (nodes.Node) -> None
self.in_footnote += 1
if 'footnotetext' in node:
self.body.append('%%\n\\begin{footnotetext}[%s]'
'\\sphinxAtStartFootnote\n' % node['number'])
else:
if self.in_parsed_literal:
self.body.append('\\begin{footnote}[%s]' % node['number'])
else:
self.body.append('%%\n\\begin{footnote}[%s]' % node['number'])
self.body.append('\\sphinxAtStartFootnote\n')
def depart_collected_footnote(self, node):
# type: (nodes.Node) -> None
if 'footnotetext' in node:
# the \ignorespaces in particular for after table header use
self.body.append('%\n\\end{footnotetext}\\ignorespaces ')
else:
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
if isinstance(node.parent, nodes.citation):
self.bibitems[-1][0] = node.astext()
self.bibitems[-1][2] = self.curfilestack[-1]
self.bibitems[-1][3] = node.parent['ids'][0]
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)
# footnotes in longtable header must be restricted
self.restrict_footnote(node)
def depart_thead(self, node):
# type: (nodes.Node) -> None
self.popbody()
# temporary buffer for footnotes from table header
self.pushbody([])
self.unrestrict_footnote(node)
# the footnote texts from header
self.table.header_footnotetexts = self.popbody()
def visit_tbody(self, node):
# type: (nodes.Node) -> None
# Redirect body output until table is finished.
self.pushbody(self.table.body)
# insert footnotetexts from header at start of body (due to longtable)
# those from caption are handled by templates (to allow caption at foot)
self.body.extend(self.table.header_footnotetexts)
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_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)
self.body.append('\\begin{enumerate}\n')
if 'start' in node:
enum_depth = "enum%s" % toRoman(get_nested_level(node)).lower()
self.body.append('\\setcounter{%s}{%d}\n' % (enum_depth, 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.restrict_footnote(node)
self.context.append(ctx)
def depart_term(self, node):
# type: (nodes.Node) -> None
self.body.append(self.context.pop())
self.unrestrict_footnote(node)
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 == 0 and isinstance(node.parent, nodes.footnote):
# don't insert blank line, if the paragraph is first child of a footnote
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):
# type: (nodes.Node) -> unicode
try:
return rstdim_to_latexdim(width_str)
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:
w = self.latex_image_length(attrs['width'])
if w:
include_graphics_options.append('width=%s' % w)
if 'height' in attrs:
h = self.latex_image_length(attrs['height'])
if h:
include_graphics_options.append('height=%s' % h)
if 'scale' in attrs:
if include_graphics_options:
# unfortunately passing "height=1cm,scale=2.0" to \includegraphics
# does not result in a height of 2cm. We must scale afterwards.
pre.append('\\scalebox{%f}{' % (attrs['scale'] / 100.0,))
post.append('}')
else:
# 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
# http://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
labels = self.hypertarget_to(node)
self.restrict_footnote(node)
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(labels + '\\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(labels + '\\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(labels + '\\end{figure}\n')
def depart_figure(self, node):
# type: (nodes.Node) -> None
self.body.append(self.context.pop())
self.unrestrict_footnote(node)
def visit_caption(self, node):
# type: (nodes.Node) -> None
self.in_caption += 1
self.restrict_footnote(node)
if self.in_container_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('}')
self.in_caption -= 1
self.unrestrict_footnote(node)
def visit_legend(self, node):
self.body.append('\n\\begin{sphinxlegend}')
def depart_legend(self, node):
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.next_node(ascend=True)
if isinstance(next_node, HYPERLINK_SUPPORT_NODES):
return
# postpone the labels until after the sectioning command
parindex = node.parent.index(node)
try:
try:
next = node.parent[parindex + 1]
except IndexError:
# last node in parent, look at next after parent
# (for section of equal level) if it exists
if node.parent.parent is not None:
next = node.parent.parent[
node.parent.parent.index(node.parent)]
else:
raise
domain = self.builder.env.get_domain('std')
figtype = domain.get_figtype(next)
if figtype and domain.get_numfig_title(next):
ids = set()
# labels for figures go in the figure body, not before
if node.get('refid'):
ids.add(node['refid'])
ids.update(node['ids'])
self.push_hyperlink_ids(figtype, ids)
return
except IndexError:
pass
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=re.compile(r';\s*')):
# type: (nodes.Node, Pattern) -> 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
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 = '|textbf'
try:
if type == 'single':
p = scre.sub('!', escape(string))
self.body.append(r'\index{%s%s}' % (p, m))
elif type == 'pair':
p1, p2 = [escape(x) for x in split_into(2, 'pair', string)]
self.body.append(r'\index{%s!%s%s}\index{%s!%s%s}' %
(p1, p2, m, p2, p1, m))
elif type == 'triple':
p1, p2, p3 = [escape(x) for x in split_into(3, 'triple', string)]
self.body.append(
r'\index{%s!%s %s%s}\index{%s!%s, %s%s}'
r'\index{%s!%s %s%s}' %
(p1, p2, p3, m, p2, p3, p1, m, p3, p1, p2, m))
elif type == 'see':
p1, p2 = [escape(x) for x in split_into(2, 'see', string)]
self.body.append(r'\index{%s|see{%s}}' % (p1, p2))
elif type == 'seealso':
p1, p2 = [escape(x) for x in split_into(2, 'seealso', string)]
self.body.append(r'\index{%s|see{%s}}' % (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_citation(self, node):
# type: (nodes.Node) -> None
# TODO maybe use cite bibitems
# bibitem: [citelabel, citetext, docname, citeid]
self.bibitems.append(['', '', '', ''])
self.context.append(len(self.body))
def depart_citation(self, node):
# type: (nodes.Node) -> None
size = self.context.pop()
text = ''.join(self.body[size:])
del self.body[size:]
self.bibitems[-1][1] = text
def visit_citation_reference(self, node):
# type: (nodes.Node) -> None
# This is currently never encountered, since citation_reference nodes
# are already replaced by pending_xref nodes in the environment.
self.body.append('\\cite{%s}' % self.idescape(node.astext()))
raise nodes.SkipNode
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
num = node.astext().strip()
try:
footnode, used = self.footnotestack[-1][num]
except (KeyError, IndexError):
raise nodes.SkipNode
# if a footnote has been inserted once, it shouldn't be repeated
# by the next reference
if used:
self.body.append('\\sphinxfootnotemark[%s]' % num)
elif self.footnote_restricted:
self.footnotestack[-1][num][1] = True
self.body.append('\\sphinxfootnotemark[%s]' % num)
self.pending_footnotes.append(footnode)
else:
self.footnotestack[-1][num][1] = True
footnode.walkabout(self) # type: ignore
raise nodes.SkipChildren
def depart_footnote_reference(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)
# LaTeX code will insert \phantomsection prior to \label
if labels and not self.in_footnote:
self.body.append('\n\\def\\sphinxLiteralBlockLabel{' + labels + '}')
code = node.astext()
lang = self.hlsettingstack[-1][0]
linenos = code.count('\n') >= self.hlsettingstack[-1][1] - 1
highlight_args = node.get('highlight_args', {})
hllines = '\\fvset{hllines={, %s,}}%%' %\
str(highlight_args.get('hl_lines', []))[1:-1]
if 'language' in node:
# code-block directives
lang = node['language']
highlight_args['force'] = True
if 'linenos' in node:
linenos = node['linenos']
if lang is self.hlsettingstack[0][0]:
# only pass highlighter options for original language
opts = self.builder.config.highlight_options
else:
opts = {}
hlcode = self.highlighter.highlight_block(
code, 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}'
self.body.append('\n' + hllines + '\n' + hlcode + '\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_substitution_definition(self, node):
# type: (nodes.Node) -> None
raise nodes.SkipNode
def visit_substitution_reference(self, node):
# type: (nodes.Node) -> None
raise nodes.SkipNode
def visit_inline(self, node):
# type: (nodes.Node) -> None
classes = node.get('classes', [])
if classes in [['menuselection'], ['guilabel']]:
self.body.append(r'\sphinxmenuselection{')
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
if node.get('literal_block'):
self.in_container_literal_block += 1
ids = '' # type: unicode
for id in self.pop_hyperlink_ids('code-block'):
ids += self.hypertarget(id, anchor=False)
if node['ids']:
# suppress with anchor=False \phantomsection insertion
ids += self.hypertarget(node['ids'][0], anchor=False)
# define label for use in caption.
if ids:
self.body.append('\n\\def\\sphinxLiteralBlockLabel{' + ids + '}\n')
def depart_container(self, node):
# type: (nodes.Node) -> None
if node.get('literal_block'):
self.in_container_literal_block -= 1
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
logger.warning('using "math" markup without a Sphinx math extension '
'active, please use one of the math extensions '
'described at http://sphinx-doc.org/en/master/ext/math.html',
location=(self.curfilestack[-1], node.line))
raise nodes.SkipNode
visit_math_block = visit_math
def unknown_visit(self, node):
# type: (nodes.Node) -> None
raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
# 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