Files
sphinx/sphinx/writers/latex.py

1512 lines
55 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-2011 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import re
import sys
from os import path
from docutils import nodes, writers
from docutils.writers.latex2e import Babel
from sphinx import addnodes
from sphinx import highlighting
from sphinx.errors import SphinxError
from sphinx.locale import admonitionlabels, versionlabels, _
from sphinx.util import split_into
from sphinx.util.osutil import ustrftime
from sphinx.util.pycompat import any
from sphinx.util.texescape import tex_escape_map, tex_replace_map
from sphinx.util.smartypants import educate_quotes_latex
HEADER = r'''%% Generated by Sphinx.
\def\sphinxdocclass{%(docclass)s}
\documentclass[%(papersize)s,%(pointsize)s%(classoptions)s]{%(wrapperclass)s}
%(inputenc)s
%(utf8extra)s
%(fontenc)s
%(babel)s
%(fontpkg)s
%(fncychap)s
%(longtable)s
\usepackage{sphinx}
\usepackage{multirow}
%(preamble)s
\title{%(title)s}
\date{%(date)s}
\release{%(release)s}
\author{%(author)s}
\newcommand{\sphinxlogo}{%(logo)s}
\renewcommand{\releasename}{%(releasename)s}
%(makeindex)s
'''
BEGIN_DOC = r'''
\begin{document}
%(shorthandoff)s
%(maketitle)s
%(tableofcontents)s
'''
FOOTER = r'''
\renewcommand{\indexname}{%(indexname)s}
%(printindex)s
\end{document}
'''
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 = {}
output = None
def __init__(self, builder):
writers.Writer.__init__(self)
self.builder = builder
def translate(self):
visitor = LaTeXTranslator(self.document, self.builder)
self.document.walkabout(visitor)
self.output = visitor.astext()
# Helper classes
class ExtBabel(Babel):
def get_shorthandoff(self):
shortlang = self.language.split('_')[0]
if shortlang in ('de', 'sl', 'pt', 'es', 'nl', 'pl', 'it'):
return '\\shorthandoff{"}'
return ''
# in latest trunk, the attribute is called Babel.language_codes and already
# includes Slovene
if hasattr(Babel, '_ISO639_TO_BABEL'):
Babel._ISO639_TO_BABEL['sl'] = 'slovene'
class Table(object):
def __init__(self):
self.col = 0
self.colcount = 0
self.colspec = None
self.rowcount = 0
self.had_head = False
self.has_problematic = False
self.has_verbatim = False
self.caption = None
self.longtable = False
class LaTeXTranslator(nodes.NodeVisitor):
sectionnames = ["part", "chapter", "section", "subsection",
"subsubsection", "paragraph", "subparagraph"]
ignore_missing_images = False
default_elements = {
'papersize': 'letterpaper',
'pointsize': '10pt',
'classoptions': '',
'inputenc': '\\usepackage[utf8]{inputenc}',
'utf8extra': '\\DeclareUnicodeCharacter{00A0}{\\nobreakspace}',
'fontenc': '\\usepackage[T1]{fontenc}',
'babel': '\\usepackage{babel}',
'fontpkg': '\\usepackage{times}',
'fncychap': '\\usepackage[Bjarne]{fncychap}',
'longtable': '\\usepackage{longtable}',
'preamble': '',
'title': '',
'date': '',
'release': '',
'author': '',
'logo': '',
'releasename': 'Release',
'makeindex': '\\makeindex',
'shorthandoff': '',
'maketitle': '\\maketitle',
'tableofcontents': '\\tableofcontents',
'footer': '',
'printindex': '\\printindex',
}
def __init__(self, document, builder):
nodes.NodeVisitor.__init__(self, document)
self.builder = builder
self.body = []
# sort out some elements
papersize = builder.config.latex_paper_size + 'paper'
if papersize == 'paper': # e.g. command line "-D latex_paper_size="
papersize = 'letterpaper'
self.elements = self.default_elements.copy()
self.elements.update({
'wrapperclass': 'sphinx' + document.settings.docclass,
'papersize': papersize,
'pointsize': builder.config.latex_font_size,
# if empty, the title is set to the first section title
'title': document.settings.title,
'release': builder.config.release,
'author': document.settings.author,
'releasename': _('Release'),
'preamble': builder.config.latex_preamble,
'indexname': _('Index'),
})
if document.settings.docclass == 'howto':
docclass = builder.config.latex_docclass.get('howto', 'article')
else:
docclass = builder.config.latex_docclass.get('manual', 'report')
self.elements['docclass'] = docclass
if builder.config.today:
self.elements['date'] = builder.config.today
else:
self.elements['date'] = ustrftime(builder.config.today_fmt
or _('%B %d, %Y'))
if builder.config.latex_logo:
self.elements['logo'] = '\\includegraphics{%s}\\par' % \
path.basename(builder.config.latex_logo)
if builder.config.language:
babel = ExtBabel(builder.config.language)
lang = babel.get_language()
if lang:
self.elements['classoptions'] += ',' + babel.get_language()
else:
self.builder.warn('no Babel option known for language %r' %
builder.config.language)
self.elements['shorthandoff'] = babel.get_shorthandoff()
self.elements['fncychap'] = '\\usepackage[Sonny]{fncychap}'
# pTeX (Japanese TeX) for support
if builder.config.language == 'ja':
self.elements['classoptions'] = ',dvipdfm'
# found elements of babel, but this should be above sphinx.sty.
# because pTeX (Japanese TeX) cannot handle this count.
self.elements['babel'] = r'\newcount\pdfoutput\pdfoutput=0'
# to make the pdf with correct encoded hyperref bookmarks
self.elements['preamble'] += \
r'\AtBeginDvi{\special{pdf:tounicode EUC-UCS2}}'
else:
self.elements['classoptions'] += ',english'
# allow the user to override them all
self.elements.update(builder.config.latex_elements)
self.highlighter = highlighting.PygmentsBridge('latex',
builder.config.pygments_style, builder.config.trim_doctest_flags)
self.context = []
self.descstack = []
self.bibitems = []
self.table = None
self.next_table_colspec = None
# 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.maxint]]
self.footnotestack = []
self.curfilestack = []
self.handled_abbrs = set()
if document.settings.docclass == 'howto':
self.top_sectionlevel = 2
else:
if builder.config.latex_use_parts:
self.top_sectionlevel = 0
else:
self.top_sectionlevel = 1
self.next_section_ids = set()
self.next_figure_ids = set()
self.next_table_ids = set()
# flags
self.verbatim = None
self.in_title = 0
self.in_production_list = 0
self.in_footnote = 0
self.in_caption = 0
self.first_document = 1
self.this_is_the_title = 1
self.literal_whitespace = 0
self.no_contractions = 0
self.compact_list = 0
self.first_param = 0
self.previous_spanning_row = 0
self.previous_spanning_column = 0
self.remember_multirow = {}
def astext(self):
return (HEADER % self.elements +
self.highlighter.get_stylesheet() +
u''.join(self.body) +
'\n' + self.elements['footer'] + '\n' +
self.generate_indices() +
FOOTER % self.elements)
def hypertarget(self, id, withdoc=True, anchor=True):
if withdoc:
id = self.curfilestack[-1] + ':' + id
return (anchor and '\\phantomsection' or '') + \
'\\label{%s}' % self.idescape(id)
def hyperlink(self, id):
return '{\\hyperref[%s]{' % self.idescape(id)
def hyperpageref(self, id):
return '\\autopageref*{%s}' % self.idescape(id)
def idescape(self, id):
return unicode(id).translate(tex_replace_map).\
encode('ascii', 'backslashreplace').decode('ascii').\
replace('\\', '_')
def generate_indices(self):
def generate(content, collapsed):
ret.append('\\begin{theindex}\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' %
letter.translate(tex_escape_map))
for entry in entries:
if not entry[3]:
continue
ret.append('\\item {\\texttt{%s}}' % self.encode(entry[0]))
if entry[4]:
# add "extra" info
ret.append(' \\emph{(%s)}' % self.encode(entry[4]))
ret.append(', \\pageref{%s:%s}\n' %
(entry[2], self.idescape(entry[3])))
ret.append('\\end{theindex}\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 self.builder.env.domains.itervalues():
for indexcls in domain.indices:
indexname = '%s-%s' % (domain.name, indexcls.name)
if isinstance(indices_config, list):
if indexname not in indices_config:
continue
# deprecated config value
if indexname == 'py-modindex' and \
not self.builder.config.latex_use_modindex:
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 visit_document(self, node):
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):
if self.bibitems:
widest_label = ""
for bi in self.bibitems:
if len(widest_label) < len(bi[0]):
widest_label = bi[0]
self.body.append(u'\n\\begin{thebibliography}{%s}\n' % widest_label)
for bi in self.bibitems:
target = self.hypertarget(bi[2] + ':' + bi[0].lower(),
withdoc=False)
self.body.append(u'\\bibitem[%s]{%s}{%s %s}\n' %
(bi[0], self.idescape(bi[0]), target, bi[1]))
self.body.append(u'\\end{thebibliography}\n')
self.bibitems = []
def visit_start_of_file(self, node):
# 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):
fnotes = {}
def footnotes_under(n):
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
for fn in footnotes_under(node):
num = fn.children[0].astext().strip()
fnotes[num] = [collected_footnote(*fn.children), False]
return fnotes
def depart_start_of_file(self, node):
self.footnotestack.pop()
self.curfilestack.pop()
self.hlsettingstack.pop()
def visit_highlightlang(self, node):
self.hlsettingstack[-1] = [node['lang'], node['linenothreshold']]
raise nodes.SkipNode
def visit_section(self, node):
if not self.this_is_the_title:
self.sectionlevel += 1
self.body.append('\n\n')
if node.get('ids'):
self.next_section_ids.update(node['ids'])
def depart_section(self, node):
self.sectionlevel = max(self.sectionlevel - 1,
self.top_sectionlevel - 1)
def visit_problematic(self, node):
self.body.append(r'{\color{red}\bfseries{}')
def depart_problematic(self, node):
self.body.append('}')
def visit_topic(self, node):
self.body.append('\\setbox0\\vbox{\n'
'\\begin{minipage}{0.95\\linewidth}\n')
def depart_topic(self, node):
self.body.append('\\end{minipage}}\n'
'\\begin{center}\\setlength{\\fboxsep}{5pt}'
'\\shadowbox{\\box0}\\end{center}\n')
visit_sidebar = visit_topic
depart_sidebar = depart_topic
def visit_glossary(self, node):
pass
def depart_glossary(self, node):
pass
def visit_productionlist(self, node):
self.body.append('\n\n\\begin{productionlist}\n')
self.in_production_list = 1
def depart_productionlist(self, node):
self.body.append('\\end{productionlist}\n\n')
self.in_production_list = 0
def visit_production(self, node):
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):
self.body.append('}\n')
def visit_transition(self, node):
self.body.append('\n\n\\bigskip\\hrule{}\\bigskip\n\n')
def depart_transition(self, node):
pass
def visit_title(self, node):
parent = node.parent
if isinstance(parent, addnodes.seealso):
# the environment already handles this
raise nodes.SkipNode
elif self.this_is_the_title:
if len(node.children) != 1 and not isinstance(node.children[0],
nodes.Text):
self.builder.warn('document title is not a single Text node',
(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
elif isinstance(parent, nodes.section):
try:
self.body.append(r'\%s{' % self.sectionnames[self.sectionlevel])
except IndexError:
# just use "subparagraph", it's not numbered anyway
self.body.append(r'\%s{' % self.sectionnames[-1])
self.context.append('}\n')
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, nodes.sidebar)):
self.body.append(r'\textbf{')
self.context.append('}\n\n\medskip\n\n')
elif isinstance(parent, nodes.Admonition):
self.body.append('{')
self.context.append('}\n')
elif isinstance(parent, nodes.table):
self.table.caption = self.encode(node.astext())
raise nodes.SkipNode
else:
self.builder.warn(
'encountered title node not in section, topic, table, '
'admonition or sidebar',
(self.curfilestack[-1], node.line or ''))
self.body.append('\\textbf{')
self.context.append('}\n')
self.in_title = 1
def depart_title(self, node):
self.in_title = 0
self.body.append(self.context.pop())
def visit_subtitle(self, node):
if isinstance(node.parent, nodes.sidebar):
self.body.append('~\\\\\n\\textbf{')
self.context.append('}\n\\smallskip\n')
else:
self.context.append('')
def depart_subtitle(self, node):
self.body.append(self.context.pop())
def visit_desc(self, node):
self.body.append('\n\n\\begin{fulllineitems}\n')
if self.table:
self.table.has_problematic = True
def depart_desc(self, node):
self.body.append('\n\\end{fulllineitems}\n\n')
def visit_desc_signature(self, node):
if node.parent['objtype'] != 'describe' and node['ids']:
hyper = self.hypertarget(node['ids'][0])
else:
hyper = ''
self.body.append(hyper)
for child in node:
if isinstance(child, addnodes.desc_parameterlist):
self.body.append(r'\pysiglinewithargsret{')
break
else:
self.body.append(r'\pysigline{')
def depart_desc_signature(self, node):
self.body.append('}')
def visit_desc_addname(self, node):
self.body.append(r'\code{')
self.literal_whitespace += 1
def depart_desc_addname(self, node):
self.body.append('}')
self.literal_whitespace -= 1
def visit_desc_type(self, node):
pass
def depart_desc_type(self, node):
pass
def visit_desc_returns(self, node):
self.body.append(r'{ $\rightarrow$ ')
def depart_desc_returns(self, node):
self.body.append(r'}')
def visit_desc_name(self, node):
self.body.append(r'\bfcode{')
self.literal_whitespace += 1
def depart_desc_name(self, node):
self.body.append('}')
self.literal_whitespace -= 1
def visit_desc_parameterlist(self, node):
# close name, open parameterlist
self.body.append('}{')
self.first_param = 1
def depart_desc_parameterlist(self, node):
# close parameterlist, open return annotation
self.body.append('}{')
def visit_desc_parameter(self, node):
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):
if not node.hasattr('noemph'):
self.body.append('}')
def visit_desc_optional(self, node):
self.body.append(r'\optional{')
def depart_desc_optional(self, node):
self.body.append('}')
def visit_desc_annotation(self, node):
self.body.append(r'\strong{')
def depart_desc_annotation(self, node):
self.body.append('}')
def visit_desc_content(self, node):
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):
pass
def visit_refcount(self, node):
self.body.append("\\emph{")
def depart_refcount(self, node):
self.body.append("}\\\\")
def visit_seealso(self, node):
self.body.append(u'\n\n\\strong{%s:}\n\n' % admonitionlabels['seealso'])
def depart_seealso(self, node):
self.body.append("\n\n")
def visit_rubric(self, node):
if len(node.children) == 1 and node.children[0].astext() in \
('Footnotes', _('Footnotes')):
raise nodes.SkipNode
self.body.append('\\paragraph{')
self.context.append('}\n')
def depart_rubric(self, node):
self.body.append(self.context.pop())
def visit_footnote(self, node):
raise nodes.SkipNode
def visit_collected_footnote(self, node):
self.in_footnote += 1
self.body.append('\\footnote{')
def depart_collected_footnote(self, node):
self.body.append('}')
self.in_footnote -= 1
def visit_label(self, node):
if isinstance(node.parent, nodes.citation):
self.bibitems[-1][0] = node.astext()
self.bibitems[-1][2] = self.curfilestack[-1]
raise nodes.SkipNode
def visit_tabular_col_spec(self, node):
self.next_table_colspec = node['spec']
raise nodes.SkipNode
def visit_table(self, node):
if self.table:
raise UnsupportedError(
'%s:%s: nested tables are not yet implemented.' %
(self.curfilestack[-1], node.line or ''))
self.table = Table()
self.table.longtable = 'longtable' in node['classes']
self.tablebody = []
self.tableheaders = []
# Redirect body output until table is finished.
self._body = self.body
self.body = self.tablebody
def depart_table(self, node):
if self.table.rowcount > 30:
self.table.longtable = True
self.body = self._body
if not self.table.longtable and self.table.caption is not None:
self.body.append(u'\n\n\\begin{threeparttable}\n'
u'\\capstart\\caption{%s}\n' % self.table.caption)
if self.table.longtable:
self.body.append('\n\\begin{longtable}')
endmacro = '\\end{longtable}\n\n'
elif self.table.has_verbatim:
self.body.append('\n\\begin{tabular}')
endmacro = '\\end{tabular}\n\n'
elif self.table.has_problematic and not self.table.colspec:
# if the user has given us tabularcolumns, accept them and use
# tabulary nevertheless
self.body.append('\n\\begin{tabular}')
endmacro = '\\end{tabular}\n\n'
else:
self.body.append('\n\\begin{tabulary}{\\linewidth}')
endmacro = '\\end{tabulary}\n\n'
if self.table.colspec:
self.body.append(self.table.colspec)
else:
if self.table.has_problematic:
colwidth = 0.95 / self.table.colcount
colspec = ('p{%.3f\\linewidth}|' % colwidth) * \
self.table.colcount
self.body.append('{|' + colspec + '}\n')
elif self.table.longtable:
self.body.append('{|' + ('l|' * self.table.colcount) + '}\n')
else:
self.body.append('{|' + ('L|' * self.table.colcount) + '}\n')
if self.table.longtable and self.table.caption is not None:
self.body.append(u'\\caption{%s} \\\\\n' % self.table.caption)
if self.table.caption is not None:
for id in self.next_table_ids:
self.body.append(self.hypertarget(id, anchor=False))
self.next_table_ids.clear()
if self.table.longtable:
self.body.append('\\hline\n')
self.body.extend(self.tableheaders)
self.body.append('\\endfirsthead\n\n')
self.body.append('\\multicolumn{%s}{c}%%\n' % self.table.colcount)
self.body.append(r'{{\bfseries \tablename\ \thetable{} -- %s}} \\'
% _('continued from previous page'))
self.body.append('\n\\hline\n')
self.body.extend(self.tableheaders)
self.body.append('\\endhead\n\n')
self.body.append(ur'\hline \multicolumn{%s}{|r|}{{%s}} \\ \hline'
% (self.table.colcount,
_('Continued on next page')))
self.body.append('\n\\endfoot\n\n')
self.body.append('\\hline\n')
self.body.append('\\endlastfoot\n\n')
else:
self.body.append('\\hline\n')
self.body.extend(self.tableheaders)
self.body.extend(self.tablebody)
self.body.append(endmacro)
if not self.table.longtable and self.table.caption is not None:
self.body.append('\\end{threeparttable}\n\n')
self.table = None
self.tablebody = None
def visit_colspec(self, node):
self.table.colcount += 1
def depart_colspec(self, node):
pass
def visit_tgroup(self, node):
pass
def depart_tgroup(self, node):
pass
def visit_thead(self, node):
self.table.had_head = True
if self.next_table_colspec:
self.table.colspec = '{%s}\n' % self.next_table_colspec
self.next_table_colspec = None
# Redirect head output until header is finished. see visit_tbody.
self.body = self.tableheaders
def depart_thead(self, node):
pass
def visit_tbody(self, node):
if not self.table.had_head:
self.visit_thead(node)
self.body = self.tablebody
def depart_tbody(self, node):
pass
def visit_row(self, node):
self.table.col = 0
def depart_row(self, node):
if self.previous_spanning_row == 1:
self.previous_spanning_row = 0
self.body.append('\\\\\n')
else:
self.body.append('\\\\\\hline\n')
self.table.rowcount += 1
def visit_entry(self, node):
if self.remember_multirow.get(0, 0) > 1:
self.body.append(' & ')
if self.table.col > 0:
self.body.append(' & ')
self.table.col += 1
context = ''
if 'morerows' in node:
self.body.append(' \multirow{')
self.previous_spanning_row = 1
self.body.append(str(node.get('morerows') + 1))
self.body.append('}{*}{')
context += '}'
self.remember_multirow[self.table.col] = node.get('morerows') + 1
if 'morecols' in node:
self.body.append(' \multicolumn{')
self.body.append(str(node.get('morecols') + 1))
if self.table.col == 1:
self.body.append('}{|l|}{')
else:
self.body.append('}{l|}{')
context += '}'
if isinstance(node.parent.parent, nodes.thead):
self.body.append('\\textbf{')
context += '}'
if self.remember_multirow.get(self.table.col + 1, 0) > 1:
self.remember_multirow[self.table.col + 1] -= 1
context += ' & '
self.context.append(context)
def depart_entry(self, node):
self.body.append(self.context.pop()) # header
def visit_acks(self, node):
# 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):
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):
if not self.compact_list:
self.body.append('\\end{itemize}\n' )
def visit_enumerated_list(self, node):
self.body.append('\\begin{enumerate}\n' )
if 'start' in node:
self.body.append('\\setcounter{enumi}{%d}\n' % (node['start'] - 1))
if self.table:
self.table.has_problematic = True
def depart_enumerated_list(self, node):
self.body.append('\\end{enumerate}\n' )
def visit_list_item(self, node):
# 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):
self.body.append('\n')
def visit_definition_list(self, node):
self.body.append('\\begin{description}\n')
if self.table:
self.table.has_problematic = True
def depart_definition_list(self, node):
self.body.append('\\end{description}\n')
def visit_definition_list_item(self, node):
pass
def depart_definition_list_item(self, node):
pass
def visit_term(self, node):
ctx = '}] \\leavevmode'
if node.get('ids'):
ctx += self.hypertarget(node['ids'][0])
self.body.append('\\item[{')
self.context.append(ctx)
def depart_term(self, node):
self.body.append(self.context.pop())
def visit_termsep(self, node):
self.body.append(', ')
raise nodes.SkipNode
def visit_classifier(self, node):
self.body.append('{[}')
def depart_classifier(self, node):
self.body.append('{]}')
def visit_definition(self, node):
pass
def depart_definition(self, node):
self.body.append('\n')
def visit_field_list(self, node):
self.body.append('\\begin{quote}\\begin{description}\n')
if self.table:
self.table.has_problematic = True
def depart_field_list(self, node):
self.body.append('\\end{description}\\end{quote}\n')
def visit_field(self, node):
pass
def depart_field(self, node):
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):
self.body.append('\n')
def depart_paragraph(self, node):
self.body.append('\n')
def visit_centered(self, node):
self.body.append('\n\\begin{center}')
if self.table:
self.table.has_problematic = True
def depart_centered(self, node):
self.body.append('\n\\end{center}')
def visit_hlist(self, node):
# 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):
self.compact_list -= 1
self.body.append('\\end{itemize}\n')
def visit_hlistcol(self, node):
pass
def depart_hlistcol(self, node):
pass
def latex_image_length(self, width_str):
match = re.match('(\d*\.?\d*)\s*(\S*)', width_str)
if not match:
# fallback
return width_str
res = width_str
amount, unit = match.groups()[:2]
if not unit or unit == "px":
# pixels: let LaTeX alone
return None
elif unit == "%":
res = "%.3f\\linewidth" % (float(amount) / 100.0)
return res
def is_inline(self, node):
"""Check whether a node represents an inline element."""
return isinstance(node.parent, nodes.TextElement)
def visit_image(self, node):
attrs = node.attributes
pre = [] # in reverse order
post = []
include_graphics_options = []
is_inline = self.is_inline(node)
if 'scale' in attrs:
# Could also be done with ``scale`` option to
# ``\includegraphics``; doing it this way for consistency.
pre.append('\\scalebox{%f}{' % (attrs['scale'] / 100.0,))
post.append('}')
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 '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'): ('{\\hfill', '\\hfill}'),
# 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'): ('{', '\\hfill}'),
(0, 'right'): ('{\\hfill', '}'),}
try:
pre.append(align_prepost[is_inline, attrs['align']][0])
post.append(align_prepost[is_inline, attrs['align']][1])
except KeyError:
pass
if not is_inline:
pre.append('\n')
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)
self.body.append('\\includegraphics%s{%s}' % (options, uri))
self.body.extend(post)
def depart_image(self, node):
pass
def visit_figure(self, node):
ids = ''
for id in self.next_figure_ids:
ids += self.hypertarget(id, anchor=False)
self.next_figure_ids.clear()
if 'width' in node and node.get('align', '') in ('left', 'right'):
self.body.append('\\begin{wrapfigure}{%s}{%s}\n\\centering' %
(node['align'] == 'right' and 'r' or 'l',
node['width']))
self.context.append(ids + '\\end{wrapfigure}\n')
else:
if (not 'align' in node.attributes or
node.attributes['align'] == 'center'):
# centering does not add vertical space like center.
align = '\n\\centering'
align_end = ''
else:
# TODO non vertical space for other alignments.
align = '\\begin{flush%s}' % node.attributes['align']
align_end = '\\end{flush%s}' % node.attributes['align']
self.body.append('\\begin{figure}[htbp]%s\n' % align)
if any(isinstance(child, nodes.caption) for child in node):
self.body.append('\\capstart\n')
self.context.append(ids + align_end + '\\end{figure}\n')
def depart_figure(self, node):
self.body.append(self.context.pop())
def visit_caption(self, node):
self.in_caption += 1
self.body.append('\\caption{')
def depart_caption(self, node):
self.body.append('}')
self.in_caption -= 1
def visit_legend(self, node):
self.body.append('{\\small ')
def depart_legend(self, node):
self.body.append('}')
def visit_admonition(self, node):
self.body.append('\n\\begin{notice}{note}')
def depart_admonition(self, node):
self.body.append('\\end{notice}\n')
def _make_visit_admonition(name):
def visit_admonition(self, node):
self.body.append(u'\n\\begin{notice}{%s}{%s:}' %
(name, admonitionlabels[name]))
return visit_admonition
def _depart_named_admonition(self, node):
self.body.append('\\end{notice}\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):
intro = versionlabels[node['type']] % node['version']
if node.children:
intro += ': '
else:
intro += '.'
self.body.append(intro)
def depart_versionmodified(self, node):
pass
def visit_target(self, node):
def add_target(id):
# indexing uses standard LaTeX index markup, so the targets
# will be generated differently
if id.startswith('index-'):
return
# do not generate \phantomsection in \section{}
anchor = not self.in_title
self.body.append(self.hypertarget(id, anchor=anchor))
# 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
if isinstance(next, nodes.section):
if node.get('refid'):
self.next_section_ids.add(node['refid'])
self.next_section_ids.update(node['ids'])
return
elif isinstance(next, nodes.figure):
# labels for figures go in the figure body, not before
if node.get('refid'):
self.next_figure_ids.add(node['refid'])
self.next_figure_ids.update(node['ids'])
return
elif isinstance(next, nodes.table):
# same for tables, but only if they have a caption
for n in node:
if isinstance(n, nodes.title):
if node.get('refid'):
self.next_table_ids.add(node['refid'])
self.next_table_ids.update(node['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):
pass
def visit_attribution(self, node):
self.body.append('\n\\begin{flushright}\n')
self.body.append('---')
def depart_attribution(self, node):
self.body.append('\n\\end{flushright}\n')
def visit_index(self, node, scre=re.compile(r';\s*')):
if not node.get('inline', True):
self.body.append('\n')
entries = node['entries']
for type, string, tid, ismain in entries:
m = ''
if ismain:
m = '|textbf'
try:
if type == 'single':
p = scre.sub('!', self.encode(string))
self.body.append(r'\index{%s%s}' % (p, m))
elif type == 'pair':
p1, p2 = map(self.encode, 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 = map(self.encode,
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 = map(self.encode, split_into(2, 'see', string))
self.body.append(r'\index{%s|see{%s}}' % (p1, p2))
elif type == 'seealso':
p1, p2 = map(self.encode, split_into(2, 'seealso', string))
self.body.append(r'\index{%s|see{%s}}' % (p1, p2))
else:
self.builder.warn(
'unknown index entry type %s found' % type)
except ValueError, err:
self.builder.warn(str(err))
raise nodes.SkipNode
def visit_raw(self, node):
if 'latex' in node.get('format', '').split():
self.body.append(node.astext())
raise nodes.SkipNode
def visit_reference(self, node):
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('mailto:') or uri.startswith('http:') or \
uri.startswith('https:') or uri.startswith('ftp:'):
self.body.append('\\href{%s}{' % self.encode_uri(uri))
# if configured, put the URL after the link
show_urls = self.builder.config.latex_show_urls
if node.astext() != uri and show_urls and show_urls != 'no':
if uri.startswith('mailto:'):
uri = uri[7:]
if show_urls == 'footnote' and not \
(self.in_footnote or self.in_caption):
# obviously, footnotes in footnotes are not going to work
self.context.append(
r'}\footnote{%s}' % self.encode_uri(uri))
else: # all other true values (b/w compat)
self.context.append('} (%s)' % self.encode_uri(uri))
else:
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))
if self.builder.config.latex_show_pagerefs and not \
self.in_productionlist:
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('}}')
else:
if self.builder.config.latex_show_pagerefs:
self.context.append('}} (%s)' % self.hyperpageref(id))
else:
self.context.append('}}')
else:
self.builder.warn('unusable reference target found: %s' % uri,
(self.curfilestack[-1], node.line))
self.context.append('')
def depart_reference(self, node):
self.body.append(self.context.pop())
def visit_download_reference(self, node):
pass
def depart_download_reference(self, node):
pass
def visit_pending_xref(self, node):
pass
def depart_pending_xref(self, node):
pass
def visit_emphasis(self, node):
self.body.append(r'\emph{')
def depart_emphasis(self, node):
self.body.append('}')
def visit_literal_emphasis(self, node):
self.body.append(r'\emph{\texttt{')
self.no_contractions += 1
def depart_literal_emphasis(self, node):
self.body.append('}}')
self.no_contractions -= 1
def visit_strong(self, node):
self.body.append(r'\textbf{')
def depart_strong(self, node):
self.body.append('}')
def visit_abbreviation(self, node):
abbr = node.astext()
self.body.append(r'\textsc{')
# 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):
self.body.append(self.context.pop())
def visit_title_reference(self, node):
self.body.append(r'\emph{')
def depart_title_reference(self, node):
self.body.append('}')
def visit_citation(self, node):
# TODO maybe use cite bibitems
self.bibitems.append(['', '', '']) # [citeid, citetext, docname]
self.context.append(len(self.body))
def depart_citation(self, node):
size = self.context.pop()
text = ''.join(self.body[size:])
del self.body[size:]
self.bibitems[-1][1] = text
def visit_citation_reference(self, node):
# 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):
self.no_contractions += 1
if self.in_title:
self.body.append(r'\texttt{')
else:
self.body.append(r'\code{')
def depart_literal(self, node):
self.no_contractions -= 1
self.body.append('}')
def visit_footnote_reference(self, node):
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('\\footnotemark[%s]' % num)
else:
if self.in_caption:
raise UnsupportedError('%s:%s: footnotes in float captions '
'are not supported by LaTeX' %
(self.curfilestack[-1], node.line))
footnode.walkabout(self)
self.footnotestack[-1][num][1] = True
raise nodes.SkipChildren
def depart_footnote_reference(self, node):
pass
def visit_literal_block(self, node):
self.verbatim = ''
def depart_literal_block(self, node):
code = self.verbatim.rstrip('\n')
lang = self.hlsettingstack[-1][0]
linenos = code.count('\n') >= self.hlsettingstack[-1][1] - 1
highlight_args = node.get('highlight_args', {})
if 'language' in node:
# code-block directives
lang = node['language']
highlight_args['force'] = True
if 'linenos' in node:
linenos = node['linenos']
def warner(msg):
self.builder.warn(msg, (self.curfilestack[-1], node.line))
hlcode = self.highlighter.highlight_block(code, lang, warn=warner,
linenos=linenos, **highlight_args)
# workaround for Unicode issue
hlcode = hlcode.replace(u'', u'@texteuro[]')
# must use original Verbatim environment and "tabular" environment
if self.table:
hlcode = hlcode.replace('\\begin{Verbatim}',
'\\begin{OriginalVerbatim}')
self.table.has_problematic = True
self.table.has_verbatim = True
# get consistent trailer
hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim}
hlcode = hlcode.rstrip() + '\n'
self.body.append('\n' + hlcode + '\\end{%sVerbatim}\n' %
(self.table and 'Original' or ''))
self.verbatim = None
visit_doctest_block = visit_literal_block
depart_doctest_block = depart_literal_block
def visit_line(self, node):
self.body.append('\item[] ')
def depart_line(self, node):
self.body.append('\n')
def visit_line_block(self, node):
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):
self.body.append('\\end{DUlineblock}\n')
def visit_block_quote(self, node):
# 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):
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):
if self.context[-1]:
# this is not the first option
self.body.append(', ')
def depart_option(self, node):
# flag that the first option is done.
self.context[-1] += 1
def visit_option_argument(self, node):
"""The delimiter betweeen an option and its argument."""
self.body.append(node.get('delimiter', ' '))
def depart_option_argument(self, node):
pass
def visit_option_group(self, node):
self.body.append('\\item [')
# flag for first option
self.context.append(0)
def depart_option_group(self, node):
self.context.pop() # the flag
self.body.append('] ')
def visit_option_list(self, node):
self.body.append('\\begin{optionlist}{3cm}\n')
if self.table:
self.table.has_problematic = True
def depart_option_list(self, node):
self.body.append('\\end{optionlist}\n')
def visit_option_list_item(self, node):
pass
def depart_option_list_item(self, node):
pass
def visit_option_string(self, node):
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):
self.body.append(' ')
def depart_description(self, node):
pass
def visit_superscript(self, node):
self.body.append('$^{\\text{')
def depart_superscript(self, node):
self.body.append('}}$')
def visit_subscript(self, node):
self.body.append('$_{\\text{')
def depart_subscript(self, node):
self.body.append('}}$')
def visit_substitution_definition(self, node):
raise nodes.SkipNode
def visit_substitution_reference(self, node):
raise nodes.SkipNode
def visit_inline(self, node):
classes = node.get('classes', [])
self.body.append(r'\DUspan{%s}{' % ','.join(classes))
def depart_inline(self, node):
self.body.append('}')
def visit_generated(self, node):
pass
def depart_generated(self, node):
pass
def visit_compound(self, node):
pass
def depart_compound(self, node):
pass
def visit_container(self, node):
pass
def depart_container(self, node):
pass
def visit_decoration(self, node):
pass
def depart_decoration(self, node):
pass
# docutils-generated elements that we don't support
def visit_header(self, node):
raise nodes.SkipNode
def visit_footer(self, node):
raise nodes.SkipNode
def visit_docinfo(self, node):
raise nodes.SkipNode
# text handling
def encode(self, text):
text = unicode(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'-{-}')
return text
def encode_uri(self, text):
# in \href, the tilde is allowed and must be represented literally
return self.encode(text).replace('\\textasciitilde{}', '~')
def visit_Text(self, node):
if self.verbatim is not None:
self.verbatim += node.astext()
else:
text = self.encode(node.astext())
if not self.no_contractions:
text = educate_quotes_latex(text)
self.body.append(text)
def depart_Text(self, node):
pass
def visit_comment(self, node):
raise nodes.SkipNode
def visit_meta(self, node):
# only valid for HTML
raise nodes.SkipNode
def visit_system_message(self, node):
pass
def depart_system_message(self, node):
self.body.append('\n')
def unknown_visit(self, node):
raise NotImplementedError('Unknown node: ' + node.__class__.__name__)