mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
1181 lines
41 KiB
Python
1181 lines
41 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
sphinx.latexwriter
|
||
~~~~~~~~~~~~~~~~~~
|
||
|
||
Custom docutils writer for LaTeX.
|
||
|
||
Much of this code is adapted from Dave Kuhlman's "docpy" writer from his
|
||
docutils sandbox.
|
||
|
||
:copyright: 2007-2008 by Georg Brandl, Dave Kuhlman.
|
||
:license: BSD.
|
||
"""
|
||
|
||
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.locale import admonitionlabels, versionlabels
|
||
from sphinx.util import ustrftime
|
||
from sphinx.util.texescape import tex_escape_map
|
||
from sphinx.util.smartypants import educateQuotesLatex
|
||
|
||
HEADER = r'''%% Generated by Sphinx.
|
||
\documentclass[%(papersize)s,%(pointsize)s%(classoptions)s]{%(docclass)s}
|
||
%(inputenc)s
|
||
%(fontenc)s
|
||
%(babel)s
|
||
%(fontpkg)s
|
||
%(fncychap)s
|
||
\usepackage{sphinx}
|
||
%(preamble)s
|
||
|
||
\title{%(title)s}
|
||
\date{%(date)s}
|
||
\release{%(release)s}
|
||
\author{%(author)s}
|
||
\newcommand{\sphinxlogo}{%(logo)s}
|
||
\renewcommand{\releasename}{%(releasename)s}
|
||
%(makeindex)s
|
||
%(makemodindex)s
|
||
'''
|
||
|
||
BEGIN_DOC = r'''
|
||
\begin{document}
|
||
%(shorthandoff)s
|
||
%(maketitle)s
|
||
%(tableofcontents)s
|
||
'''
|
||
|
||
FOOTER = r'''
|
||
%(footer)s
|
||
\renewcommand{\indexname}{%(modindexname)s}
|
||
%(printmodindex)s
|
||
\renewcommand{\indexname}{%(indexname)s}
|
||
%(printindex)s
|
||
\end{document}
|
||
'''
|
||
|
||
|
||
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'):
|
||
return '\\shorthandoff{"}'
|
||
return ''
|
||
|
||
_ISO639_TO_BABEL = Babel._ISO639_TO_BABEL.copy()
|
||
_ISO639_TO_BABEL['sl'] = 'slovene'
|
||
|
||
|
||
class Table(object):
|
||
def __init__(self):
|
||
self.col = 0
|
||
self.colcount = 0
|
||
self.colspec = None
|
||
self.had_head = False
|
||
self.has_verbatim = False
|
||
self.caption = None
|
||
|
||
|
||
class Desc(object):
|
||
def __init__(self, node):
|
||
self.env = LaTeXTranslator.desc_map.get(node['desctype'], 'describe')
|
||
self.type = self.cls = self.name = self.params = self.annotation = ''
|
||
self.count = 0
|
||
|
||
|
||
class LaTeXTranslator(nodes.NodeVisitor):
|
||
sectionnames = ["part", "chapter", "section", "subsection",
|
||
"subsubsection", "paragraph", "subparagraph"]
|
||
|
||
ignore_missing_images = False
|
||
|
||
default_elements = {
|
||
'docclass': 'manual',
|
||
'papersize': 'letterpaper',
|
||
'pointsize': '10pt',
|
||
'classoptions': '',
|
||
'inputenc': '\\usepackage[utf8]{inputenc}',
|
||
'fontenc': '\\usepackage[T1]{fontenc}',
|
||
'babel': '\\usepackage{babel}',
|
||
'fontpkg': '\\usepackage{times}',
|
||
'fncychap': '\\usepackage[Bjarne]{fncychap}',
|
||
'preamble': '',
|
||
'title': '',
|
||
'date': '',
|
||
'release': '',
|
||
'author': '',
|
||
'logo': '',
|
||
'releasename': 'Release',
|
||
'makeindex': '\\makeindex',
|
||
'makemodindex': '\\makemodindex',
|
||
'shorthandoff': '',
|
||
'maketitle': '\\maketitle',
|
||
'tableofcontents': '\\tableofcontents',
|
||
'footer': '',
|
||
'printmodindex': '\\printmodindex',
|
||
'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({
|
||
'docclass': 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,
|
||
'date': ustrftime(builder.config.today_fmt or _('%B %d, %Y')),
|
||
'release': builder.config.release,
|
||
'author': document.settings.author,
|
||
'releasename': _('Release'),
|
||
'preamble': builder.config.latex_preamble,
|
||
'modindexname': _('Module Index'),
|
||
'indexname': _('Index'),
|
||
})
|
||
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}'
|
||
else:
|
||
self.elements['classoptions'] += ',english'
|
||
if not builder.config.latex_use_modindex:
|
||
self.elements['makemodindex'] = ''
|
||
self.elements['printmodindex'] = ''
|
||
# allow the user to override them all
|
||
self.elements.update(builder.config.latex_elements)
|
||
|
||
self.highlighter = highlighting.PygmentsBridge(
|
||
'latex', builder.config.pygments_style)
|
||
self.context = []
|
||
self.descstack = []
|
||
self.bibitems = []
|
||
self.table = None
|
||
self.next_table_colspec = None
|
||
self.highlightlang = builder.config.highlight_language
|
||
self.highlightlinenothreshold = sys.maxint
|
||
self.written_ids = set()
|
||
self.footnotestack = []
|
||
if self.elements['docclass'] == 'manual':
|
||
if builder.config.latex_use_parts:
|
||
self.top_sectionlevel = 0
|
||
else:
|
||
self.top_sectionlevel = 1
|
||
else:
|
||
self.top_sectionlevel = 2
|
||
self.next_section_target = None
|
||
# flags
|
||
self.verbatim = None
|
||
self.in_title = 0
|
||
self.in_production_list = 0
|
||
self.first_document = 1
|
||
self.this_is_the_title = 1
|
||
self.literal_whitespace = 0
|
||
|
||
def astext(self):
|
||
return (HEADER % self.elements + self.highlighter.get_stylesheet() +
|
||
u''.join(self.body) + FOOTER % self.elements)
|
||
|
||
def visit_document(self, node):
|
||
self.footnotestack.append(self.collect_footnotes(node))
|
||
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('\n\\appendix\n')
|
||
self.first_document = -1
|
||
# "- 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('\n\\begin{thebibliography}{%s}\n' % widest_label)
|
||
for bi in self.bibitems:
|
||
# cite_key: underscores must not be escaped
|
||
cite_key = bi[0].replace(r"\_", "_")
|
||
self.body.append('\\bibitem[%s]{%s}{%s}\n' % (bi[0], cite_key, bi[1]))
|
||
self.body.append('\\end{thebibliography}\n')
|
||
self.bibitems = []
|
||
|
||
def visit_start_of_file(self, node):
|
||
# This marks the begin of a new file; therefore the current module and
|
||
# class must be reset
|
||
self.body.append('\n\\resetcurrentobjects\n')
|
||
# and also, new footnotes
|
||
self.footnotestack.append(self.collect_footnotes(node))
|
||
|
||
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] = fn
|
||
fn.parent.remove(fn)
|
||
return fnotes
|
||
|
||
def depart_start_of_file(self, node):
|
||
self.footnotestack.pop()
|
||
|
||
def visit_highlightlang(self, node):
|
||
self.highlightlang = node['lang']
|
||
self.highlightlinenothreshold = 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 self.next_section_target:
|
||
self.body.append(r'\hypertarget{%s}{}' % self.next_section_target)
|
||
self.next_section_target = None
|
||
#if node.get('ids'):
|
||
# for id in node['ids']:
|
||
# if id not in self.written_ids:
|
||
# self.body.append(r'\hypertarget{%s}{}' % id)
|
||
# self.written_ids.add(id)
|
||
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\\textwidth}\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']:
|
||
self.body.append('\\production{%s}{' % self.encode(node['tokenname']))
|
||
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')
|
||
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:
|
||
from sphinx.application import SphinxError
|
||
raise SphinxError('too many nesting section levels for LaTeX, '
|
||
'at heading: %s' % node.astext())
|
||
self.context.append('}\n')
|
||
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.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())
|
||
|
||
desc_map = {
|
||
'function' : 'funcdesc',
|
||
'class': 'classdesc',
|
||
'method': 'methoddesc',
|
||
'staticmethod': 'staticmethoddesc',
|
||
'exception': 'excdesc',
|
||
'data': 'datadesc',
|
||
'attribute': 'memberdesc',
|
||
'opcode': 'opcodedesc',
|
||
|
||
'cfunction': 'cfuncdesc',
|
||
'cmember': 'cmemberdesc',
|
||
'cmacro': 'csimplemacrodesc',
|
||
'ctype': 'ctypedesc',
|
||
'cvar': 'cvardesc',
|
||
|
||
'describe': 'describe',
|
||
# and all others are 'describe' too
|
||
}
|
||
|
||
def visit_desc(self, node):
|
||
self.descstack.append(Desc(node))
|
||
def depart_desc(self, node):
|
||
d = self.descstack.pop()
|
||
self.body.append("\\end{%s}\n" % d.env)
|
||
|
||
def visit_desc_signature(self, node):
|
||
d = self.descstack[-1]
|
||
# reset these for every signature
|
||
d.type = d.cls = d.name = d.params = ''
|
||
def depart_desc_signature(self, node):
|
||
d = self.descstack[-1]
|
||
d.cls = d.cls.rstrip('.')
|
||
if node.parent['desctype'] != 'describe' and node['ids']:
|
||
hyper = '\\hypertarget{%s}{}' % node['ids'][0]
|
||
else:
|
||
hyper = ''
|
||
if d.count == 0:
|
||
t1 = "\n\n%s\\begin{%s}" % (hyper, d.env)
|
||
else:
|
||
t1 = "\n%s\\%sline" % (hyper, d.env[:-4])
|
||
d.count += 1
|
||
if d.env in ('funcdesc', 'classdesc', 'excclassdesc'):
|
||
t2 = "{%s}{%s}" % (d.name, d.params)
|
||
elif d.env in ('datadesc', 'excdesc', 'csimplemacrodesc'):
|
||
t2 = "{%s}" % (d.name)
|
||
elif d.env in ('methoddesc', 'staticmethoddesc'):
|
||
if d.cls:
|
||
t2 = "[%s]{%s}{%s}" % (d.cls, d.name, d.params)
|
||
else:
|
||
t2 = "{%s}{%s}" % (d.name, d.params)
|
||
elif d.env == 'memberdesc':
|
||
if d.cls:
|
||
t2 = "[%s]{%s}" % (d.cls, d.name)
|
||
else:
|
||
t2 = "{%s}" % d.name
|
||
elif d.env == 'cfuncdesc':
|
||
if d.cls:
|
||
# C++ class names
|
||
d.name = '%s::%s' % (d.cls, d.name)
|
||
t2 = "{%s}{%s}{%s}" % (d.type, d.name, d.params)
|
||
elif d.env == 'cmemberdesc':
|
||
try:
|
||
type, container = d.type.rsplit(' ', 1)
|
||
container = container.rstrip('.')
|
||
except ValueError:
|
||
container = ''
|
||
type = d.type
|
||
t2 = "{%s}{%s}{%s}" % (container, type, d.name)
|
||
elif d.env == 'cvardesc':
|
||
t2 = "{%s}{%s}" % (d.type, d.name)
|
||
elif d.env == 'ctypedesc':
|
||
t2 = "{%s}" % (d.name)
|
||
elif d.env == 'opcodedesc':
|
||
t2 = "{%s}{%s}" % (d.name, d.params)
|
||
elif d.env == 'describe':
|
||
t2 = "{%s}" % d.name
|
||
self.body.append(t1 + t2)
|
||
|
||
def visit_desc_type(self, node):
|
||
d = self.descstack[-1]
|
||
if d.env == 'describe':
|
||
d.name += self.encode(node.astext())
|
||
else:
|
||
self.descstack[-1].type = self.encode(node.astext().strip())
|
||
raise nodes.SkipNode
|
||
|
||
def visit_desc_name(self, node):
|
||
d = self.descstack[-1]
|
||
if d.env == 'describe':
|
||
d.name += self.encode(node.astext())
|
||
else:
|
||
self.descstack[-1].name = self.encode(node.astext().strip())
|
||
raise nodes.SkipNode
|
||
|
||
def visit_desc_addname(self, node):
|
||
d = self.descstack[-1]
|
||
if d.env == 'describe':
|
||
d.name += self.encode(node.astext())
|
||
else:
|
||
self.descstack[-1].cls = self.encode(node.astext().strip())
|
||
raise nodes.SkipNode
|
||
|
||
def visit_desc_parameterlist(self, node):
|
||
d = self.descstack[-1]
|
||
if d.env == 'describe':
|
||
d.name += self.encode(node.astext())
|
||
else:
|
||
self.descstack[-1].params = self.encode(node.astext().strip())
|
||
raise nodes.SkipNode
|
||
|
||
def visit_desc_annotation(self, node):
|
||
d = self.descstack[-1]
|
||
if d.env == 'describe':
|
||
d.name += self.encode(node.astext())
|
||
else:
|
||
self.descstack[-1].annotation = self.encode(node.astext().strip())
|
||
raise nodes.SkipNode
|
||
|
||
def visit_refcount(self, node):
|
||
self.body.append("\\emph{")
|
||
def depart_refcount(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_seealso(self, node):
|
||
self.body.append("\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() == '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):
|
||
pass
|
||
def depart_footnote(self, node):
|
||
pass
|
||
|
||
def visit_label(self, node):
|
||
if isinstance(node.parent, nodes.citation):
|
||
self.bibitems[-1][0] = node.astext()
|
||
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 NotImplementedError('Nested tables are not supported.')
|
||
self.table = Table()
|
||
self.tablebody = []
|
||
# Redirect body output until table is finished.
|
||
self._body = self.body
|
||
self.body = self.tablebody
|
||
def depart_table(self, node):
|
||
self.body = self._body
|
||
if self.table.caption is not None:
|
||
self.body.append('\n\\begin{threeparttable}\n'
|
||
'\\caption{%s}\n' % self.table.caption)
|
||
if self.table.has_verbatim:
|
||
self.body.append('\n\\begin{tabular}')
|
||
else:
|
||
self.body.append('\n\\begin{tabulary}{\\textwidth}')
|
||
if self.table.colspec:
|
||
self.body.append(self.table.colspec)
|
||
else:
|
||
if self.table.has_verbatim:
|
||
colwidth = 0.95 / self.table.colcount
|
||
colspec = ('p{%.3f\\textwidth}|' % colwidth) * self.table.colcount
|
||
self.body.append('{|' + colspec + '}\n')
|
||
else:
|
||
self.body.append('{|' + ('L|' * self.table.colcount) + '}\n')
|
||
self.body.extend(self.tablebody)
|
||
if self.table.has_verbatim:
|
||
self.body.append('\\end{tabular}\n\n')
|
||
else:
|
||
self.body.append('\\end{tabulary}\n\n')
|
||
if 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):
|
||
if self.next_table_colspec:
|
||
self.table.colspec = '{%s}\n' % self.next_table_colspec
|
||
self.next_table_colspec = None
|
||
self.body.append('\\hline\n')
|
||
self.table.had_head = True
|
||
def depart_thead(self, node):
|
||
self.body.append('\\hline\n')
|
||
|
||
def visit_tbody(self, node):
|
||
if not self.table.had_head:
|
||
self.visit_thead(node)
|
||
def depart_tbody(self, node):
|
||
self.body.append('\\hline\n')
|
||
|
||
def visit_row(self, node):
|
||
self.table.col = 0
|
||
def depart_row(self, node):
|
||
self.body.append('\\\\\n')
|
||
|
||
def visit_entry(self, node):
|
||
if node.has_key('morerows') or node.has_key('morecols'):
|
||
raise NotImplementedError('Column or row spanning cells are '
|
||
'not implemented.')
|
||
if self.table.col > 0:
|
||
self.body.append(' & ')
|
||
self.table.col += 1
|
||
if isinstance(node.parent.parent, nodes.thead):
|
||
self.body.append('\\textbf{')
|
||
self.context.append('}')
|
||
else:
|
||
self.context.append('')
|
||
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):
|
||
self.body.append('\\begin{itemize}\n' )
|
||
def depart_bullet_list(self, node):
|
||
self.body.append('\\end{itemize}\n' )
|
||
|
||
def visit_enumerated_list(self, node):
|
||
self.body.append('\\begin{enumerate}\n' )
|
||
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')
|
||
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 = ']'
|
||
if node.has_key('ids') and node['ids']:
|
||
ctx += '\\hypertarget{%s}{}' % node['ids'][0]
|
||
self.body.append('\\item[')
|
||
self.context.append(ctx)
|
||
def depart_term(self, node):
|
||
self.body.append(self.context.pop())
|
||
|
||
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')
|
||
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{centering}')
|
||
def depart_centered(self, node):
|
||
self.body.append('\n\\end{centering}')
|
||
|
||
def visit_module(self, node):
|
||
modname = node['modname']
|
||
self.body.append('\n\\declaremodule[%s]{}{%s}' % (modname.replace('_', ''),
|
||
self.encode(modname)))
|
||
self.body.append('\n\\modulesynopsis{%s}' % self.encode(node['synopsis']))
|
||
if node.has_key('platform'):
|
||
self.body.append('\\platform{%s}' % self.encode(node['platform']))
|
||
def depart_module(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 visit_image(self, node):
|
||
attrs = node.attributes
|
||
pre = [] # in reverse order
|
||
post = []
|
||
include_graphics_options = []
|
||
inline = isinstance(node.parent, nodes.TextElement)
|
||
if attrs.has_key('scale'):
|
||
# 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 attrs.has_key('width'):
|
||
w = self.latex_image_length(attrs['width'])
|
||
if w:
|
||
include_graphics_options.append('width=%s' % w)
|
||
if attrs.has_key('height'):
|
||
h = self.latex_image_length(attrs['height'])
|
||
include_graphics_options.append('height=%s' % h)
|
||
if attrs.has_key('align'):
|
||
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[inline, attrs['align']][0])
|
||
post.append(align_prepost[inline, attrs['align']][1])
|
||
except KeyError:
|
||
pass # XXX complain here?
|
||
if not 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):
|
||
if (not node.attributes.has_key('align') 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)
|
||
self.context.append('%s\\end{figure}\n' % align_end)
|
||
def depart_figure(self, node):
|
||
self.body.append(self.context.pop())
|
||
|
||
def visit_caption(self, node):
|
||
self.body.append('\\caption{')
|
||
def depart_caption(self, node):
|
||
self.body.append('}')
|
||
|
||
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('\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 not id.startswith('index-'):
|
||
self.body.append(r'\hypertarget{%s}{}' % id)
|
||
|
||
if node.has_key('refid') and node['refid'] not in self.written_ids:
|
||
parindex = node.parent.index(node)
|
||
try:
|
||
next = node.parent[parindex+1]
|
||
if isinstance(next, nodes.section):
|
||
self.next_section_target = node['refid']
|
||
return
|
||
except IndexError:
|
||
pass
|
||
add_target(node['refid'])
|
||
self.written_ids.add(node['refid'])
|
||
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*')):
|
||
entries = node['entries']
|
||
for type, string, tid, _ in entries:
|
||
if type == 'single':
|
||
self.body.append(r'\index{%s}' % scre.sub('!', self.encode(string)))
|
||
elif type == 'pair':
|
||
parts = tuple(self.encode(x.strip()) for x in string.split(';', 1))
|
||
self.body.append(r'\indexii{%s}{%s}' % parts)
|
||
elif type == 'triple':
|
||
parts = tuple(self.encode(x.strip()) for x in string.split(';', 2))
|
||
self.body.append(r'\indexiii{%s}{%s}{%s}' % parts)
|
||
else:
|
||
self.builder.warn('unknown index entry type %s found' % type)
|
||
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 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))
|
||
self.context.append('}')
|
||
elif uri.startswith('#'):
|
||
self.body.append('\\hyperlink{%s}{' % uri[1:])
|
||
self.context.append('}')
|
||
elif uri.startswith('@token'):
|
||
if self.in_production_list:
|
||
self.body.append('\\token{')
|
||
else:
|
||
self.body.append('\\grammartoken{')
|
||
self.context.append('}')
|
||
else:
|
||
self.builder.warn('unusable reference target found: %s' % uri)
|
||
self.context.append('')
|
||
def depart_reference(self, node):
|
||
self.body.append(self.context.pop())
|
||
|
||
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{')
|
||
def depart_literal_emphasis(self, node):
|
||
self.body.append('}}')
|
||
|
||
def visit_strong(self, node):
|
||
self.body.append(r'\textbf{')
|
||
def depart_strong(self, node):
|
||
self.body.append('}')
|
||
|
||
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(['', ''])
|
||
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):
|
||
citeid = node.astext()
|
||
self.body.append('\\cite{%s}' % citeid)
|
||
raise nodes.SkipNode
|
||
|
||
def visit_literal(self, node):
|
||
content = self.encode(node.astext().strip())
|
||
if self.in_title:
|
||
self.body.append(r'\texttt{%s}' % content)
|
||
elif node.has_key('role') and node['role'] == 'samp':
|
||
self.body.append(r'\samp{%s}' % content)
|
||
else:
|
||
self.body.append(r'\code{%s}' % content)
|
||
raise nodes.SkipNode
|
||
|
||
def visit_footnote_reference(self, node):
|
||
num = node.astext().strip()
|
||
try:
|
||
fn = self.footnotestack[-1][num]
|
||
except (KeyError, IndexError):
|
||
raise nodes.SkipNode
|
||
self.body.append('\\footnote{')
|
||
fn.walkabout(self)
|
||
raise nodes.SkipChildren
|
||
def depart_footnote_reference(self, node):
|
||
self.body.append('}')
|
||
|
||
def visit_literal_block(self, node):
|
||
self.verbatim = ''
|
||
def depart_literal_block(self, node):
|
||
code = self.verbatim.rstrip('\n')
|
||
lang = self.highlightlang
|
||
linenos = code.count('\n') >= self.highlightlinenothreshold - 1
|
||
if node.has_key('language'):
|
||
# code-block directives
|
||
lang = node['language']
|
||
if node.has_key('linenos'):
|
||
linenos = node['linenos']
|
||
hlcode = self.highlighter.highlight_block(code, lang, linenos)
|
||
# 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_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_block(self, node):
|
||
"""line-block:
|
||
* whitespace (including linebreaks) is significant
|
||
* inline markup is supported.
|
||
* serif typeface
|
||
"""
|
||
self.body.append('{\\raggedright{}')
|
||
self.literal_whitespace = 1
|
||
def depart_line_block(self, node):
|
||
self.literal_whitespace = 0
|
||
# remove the last \\
|
||
del self.body[-1]
|
||
self.body.append('}\n')
|
||
|
||
def visit_line(self, node):
|
||
self._line_start = len(self.body)
|
||
def depart_line(self, node):
|
||
if self._line_start == len(self.body):
|
||
# no output in this line -- add a nonbreaking space, else the
|
||
# \\ command will give an error
|
||
self.body.append('~')
|
||
if self.table is not None:
|
||
self.body.append('\\newline\n')
|
||
else:
|
||
self.body.append('\\\\\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')
|
||
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')
|
||
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.body.append(self.encode(ostring.replace('--', u'--')))
|
||
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_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
|
||
|
||
# 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'~')
|
||
return text
|
||
|
||
def visit_Text(self, node):
|
||
if self.verbatim is not None:
|
||
self.verbatim += node.astext()
|
||
else:
|
||
text = self.encode(node.astext())
|
||
self.body.append(educateQuotesLatex(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__)
|