Last active
August 29, 2015 14:05
-
-
Save keik/2dd7276b4de801488454 to your computer and use it in GitHub Desktop.
sphinx.writer.latex -- fix double footnote in tabulary
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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-2014 by the Sphinx team, see AUTHORS. | |
:license: BSD, see LICENSE for details. | |
""" | |
import re | |
import sys | |
from os import path | |
from six import itervalues, text_type | |
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, _ | |
from sphinx.util import split_into | |
from sphinx.util.osutil import ustrftime | |
from sphinx.util.texescape import tex_escape_map, tex_replace_map | |
from sphinx.util.smartypants import educate_quotes_latex | |
import os, code, traceback, signal | |
def debug(sig, frame): | |
"""Interrupt running process, and provide a python prompt for | |
interactive debugging.""" | |
d={'_frame':frame} # Allow access to frame object. | |
d.update(frame.f_globals) # Unless shadowed by global | |
d.update(frame.f_locals) | |
i = code.InteractiveConsole(d) | |
message = "Signal recieved : entering python shell.\nTraceback:\n" | |
message += ''.join(traceback.format_stack(frame)) | |
i.interact(message) | |
def listen(): | |
signal.signal(signal.SIGUSR1, debug) # Register handler | |
HEADER = r'''%% Generated by Sphinx. | |
\def\sphinxdocclass{%(docclass)s} | |
\documentclass[%(papersize)s,%(pointsize)s%(classoptions)s]{%(wrapperclass)s} | |
%(inputenc)s | |
%(utf8extra)s | |
%(cmappkg)s | |
%(fontenc)s | |
%(babel)s | |
%(fontpkg)s | |
%(fncychap)s | |
%(longtable)s | |
\usepackage{sphinx} | |
\usepackage{multirow} | |
\usepackage{chngcntr} | |
\counterwithout{footnote}{chapter} | |
%(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 | |
self.translator_class = ( | |
self.builder.translator_class or LaTeXTranslator) | |
def translate(self): | |
visitor = self.translator_class(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', 'ngerman', 'sl', 'slovene', 'pt', 'portuges', | |
'es', 'spanish', 'nl', 'dutch', 'pl', 'polish', 'it', | |
'italian'): | |
return '\\shorthandoff{"}' | |
return '' | |
def uses_cyrillic(self): | |
shortlang = self.language.split('_')[0] | |
return shortlang in ('bg','bulgarian', 'kk','kazakh', | |
'mn','mongolian', 'ru','russian', | |
'uk','ukrainian') | |
# 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': '', | |
'extraclassoptions': '', | |
'inputenc': '\\usepackage[utf8]{inputenc}', | |
'utf8extra': '\\DeclareUnicodeCharacter{00A0}{\\nobreakspace}', | |
'cmappkg': '\\usepackage{cmap}', | |
'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', | |
'transition': '\n\n\\bigskip\\hrule{}\\bigskip\n\n', | |
'figure_align': 'htbp', | |
} | |
# sphinx specific document classes | |
docclasses = ('howto', 'manual') | |
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': self.format_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, | |
'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}' | |
# Times fonts don't work with Cyrillic languages | |
if babel.uses_cyrillic(): | |
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'] = '' | |
# disable fncychap in Japanese documents | |
self.elements['fncychap'] = '' | |
else: | |
self.elements['classoptions'] += ',english' | |
# allow the user to override them all | |
self.elements.update(builder.config.latex_elements) | |
if self.elements['extraclassoptions']: | |
self.elements['classoptions'] += ',' + \ | |
self.elements['extraclassoptions'] | |
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.maxsize]] | |
self.footnotestack = [] | |
self.footnotecounter = 0 | |
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.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 format_docclass(self, docclass): | |
""" prepends prefix to sphinx document classes | |
""" | |
if docclass in self.docclasses: | |
docclass = 'sphinx' + docclass | |
return docclass | |
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 text_type(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' % | |
text_type(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 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 | |
# 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[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{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.footnotecounter += len(self.footnotestack[-1]) | |
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(self.elements['transition']) | |
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.no_contractions += 1 | |
self.literal_whitespace += 1 | |
def depart_desc_name(self, node): | |
self.body.append('}') | |
self.literal_whitespace -= 1 | |
self.no_contractions -= 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_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] | |
self.bibitems[-1][3] = node.parent['ids'][0] | |
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'{{\textsf{\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(r'\hline \multicolumn{%s}{|r|}{{\textsf{%s}}} \\ \hline' | |
% (self.table.colcount, | |
_('Continued on next page'))) | |
self.body.append('\n\\endfoot\n\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') | |
self.body.append('\\hline') | |
self.table.rowcount += 1 | |
def visit_entry(self, node): | |
if self.table.col > 0: | |
self.body.append(' & ') | |
elif self.remember_multirow.get(1, 0) > 1: | |
self.remember_multirow[1] -= 1 | |
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('\\textsf{\\relax ') | |
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}[%s]%s\n' % ( | |
self.elements['figure_align'], 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): | |
pass | |
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 = [self.encode(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 = [self.encode(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 = [self.encode(x) for x in split_into(2, 'see', string)] | |
self.body.append(r'\index{%s|see{%s}}' % (p1, p2)) | |
elif type == 'seealso': | |
p1, p2 = [self.encode(x) for x in 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 as 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): | |
for id in node.get('ids'): | |
self.body += self.hypertarget(id, anchor=True) | |
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_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('}}') | |
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_literal_strong(self, node): | |
self.body.append(r'\textbf{\texttt{') | |
self.no_contractions += 1 | |
def depart_literal_strong(self, node): | |
self.body.append('}}') | |
self.no_contractions -= 1 | |
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 | |
# bibitem: [citelabel, citetext, docname, citeid] | |
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): | |
# 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]' % (int(num) + self.footnotecounter)) | |
else: | |
if self.in_caption: | |
raise UnsupportedError('%s:%s: footnotes in float captions ' | |
'are not supported by LaTeX' % | |
(self.curfilestack[-1], node.line)) | |
if self.table: | |
if self.table.longtable: | |
pass | |
elif self.table.has_verbatim: | |
pass | |
elif self.table.has_problematic and not self.table.colspec: | |
pass | |
else: | |
self.body.append('\\ifx\\[${\\footnotesize \\the\\value{footnote}}\\else') | |
footnode.walkabout(self) | |
raise nodes.SkipChildren | |
def depart_footnote_reference(self, node): | |
# listen() | |
# os.kill(os.getpid(), signal.SIGUSR1) | |
num = node.astext().strip() | |
print 'ids: %s, refid: %s, num: %s, used: %s, counter: %s' % (node.get('ids'), node.get('refid'), num, self.footnotestack[-1][num][1], self.footnotecounter) | |
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: | |
pass | |
else: | |
if self.table: | |
if self.table.longtable: | |
pass | |
elif self.table.has_verbatim: | |
pass | |
elif self.table.has_problematic and not self.table.colspec: | |
pass | |
else: | |
self.body.append('\\fi') | |
self.footnotestack[-1][num][1] = True | |
def visit_literal_block(self, node): | |
if self.in_footnote: | |
raise UnsupportedError('%s:%s: literal blocks in footnotes are ' | |
'not supported by LaTeX' % | |
(self.curfilestack[-1], node.line)) | |
if node.rawsource != node.astext(): | |
# most probably a parsed-literal block -- don't highlight | |
self.body.append('\\begin{alltt}\n') | |
else: | |
code = node.astext().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'] | |
filename = node.get('filename') | |
if filename: | |
self.body.append('\n{\\colorbox[rgb]{0.9,0.9,0.9}' | |
'{\\makebox[\\textwidth][l]' | |
'{\\small\\texttt{%s}}}}\n' % (filename,)) | |
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 '')) | |
raise nodes.SkipNode | |
def depart_literal_block(self, node): | |
self.body.append('\n\\end{alltt}\n') | |
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 = 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): | |
# in \href, the tilde is allowed and must be represented literally | |
return self.encode(text).replace('\\textasciitilde{}', '~') | |
def visit_Text(self, node): | |
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 visit_math(self, node): | |
self.builder.warn('using "math" markup without a Sphinx math extension ' | |
'active, please use one of the math extensions ' | |
'described at http://sphinx-doc.org/ext/math.html', | |
(self.curfilestack[-1], node.line)) | |
raise nodes.SkipNode | |
visit_math_block = visit_math | |
def unknown_visit(self, node): | |
raise NotImplementedError('Unknown node: ' + node.__class__.__name__) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment