summaryrefslogtreecommitdiff
path: root/docutils/writers/newlatex2e/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'docutils/writers/newlatex2e/__init__.py')
-rw-r--r--docutils/writers/newlatex2e/__init__.py788
1 files changed, 788 insertions, 0 deletions
diff --git a/docutils/writers/newlatex2e/__init__.py b/docutils/writers/newlatex2e/__init__.py
new file mode 100644
index 000000000..e46866d0d
--- /dev/null
+++ b/docutils/writers/newlatex2e/__init__.py
@@ -0,0 +1,788 @@
+# Author: Felix Wiemann
+# Contact: Felix_Wiemann@ososo.de
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+LaTeX2e document tree Writer.
+"""
+
+# Thanks to Engelbert Gruber and various contributors for the original
+# LaTeX writer, some code and many ideas of which have been used for
+# this writer.
+
+__docformat__ = 'reStructuredText'
+
+
+import re
+import os.path
+from types import ListType
+
+import docutils
+from docutils import nodes, writers, utils
+from docutils.writers.newlatex2e import unicode_map
+from docutils.transforms import writer_aux
+
+
+class Writer(writers.Writer):
+
+ supported = ('newlatex', 'newlatex2e')
+ """Formats this writer supports."""
+
+ default_stylesheet = 'base.tex'
+
+ default_stylesheet_path = utils.relative_path(
+ os.path.join(os.getcwd(), 'dummy'),
+ os.path.join(os.path.dirname(__file__), default_stylesheet))
+
+ settings_spec = (
+ 'LaTeX-Specific Options',
+ 'Note that this LaTeX writer is still EXPERIMENTAL. '
+ 'You must specify the location of the tools/stylesheets/latex.tex '
+ 'stylesheet file contained in the Docutils distribution tarball to '
+ 'make the LaTeX output work.',
+ (('Specify a stylesheet file. The path is used verbatim to include '
+ 'the file. Overrides --stylesheet-path.',
+ ['--stylesheet'],
+ {'default': '', 'metavar': '<file>',
+ 'overrides': 'stylesheet_path'}),
+ ('Specify a stylesheet file, relative to the current working '
+ 'directory. Overrides --stylesheet. Default: "%s"'
+ % default_stylesheet_path,
+ ['--stylesheet-path'],
+ {'metavar': '<file>', 'overrides': 'stylesheet',
+ 'default': default_stylesheet_path}),
+ ('Specify a user stylesheet file. See --stylesheet.',
+ ['--user-stylesheet'],
+ {'default': '', 'metavar': '<file>',
+ 'overrides': 'user_stylesheet_path'}),
+ ('Specify a user stylesheet file. See --stylesheet-path.',
+ ['--user-stylesheet-path'],
+ {'metavar': '<file>', 'overrides': 'user_stylesheet'})
+ ),)
+
+ settings_defaults = {
+ # Many Unicode characters are provided by unicode_map.py.
+ 'output_encoding': 'ascii',
+ 'output_encoding_error_handler': 'strict',
+ # Since we are using superscript footnotes, it is necessary to
+ # trim whitespace in front of footnote references.
+ 'trim_footnote_reference_space': 1,
+ # Currently unsupported:
+ 'docinfo_xform': 0,
+ # During development:
+ 'traceback': 1
+ }
+
+ relative_path_settings = ('stylesheet_path', 'user_stylesheet_path')
+
+ config_section = 'newlatex2e writer'
+ config_section_dependencies = ('writers',)
+
+ output = None
+ """Final translated form of `document`."""
+
+ def get_transforms(self):
+ return writers.Writer.get_transforms(self) + [writer_aux.Compound]
+
+ def __init__(self):
+ writers.Writer.__init__(self)
+ self.translator_class = LaTeXTranslator
+
+ def translate(self):
+ visitor = self.translator_class(self.document)
+ self.document.walkabout(visitor)
+ assert not visitor.context, 'context not empty: %s' % visitor.context
+ self.output = visitor.astext()
+ self.head = visitor.header
+ self.body = visitor.body
+
+
+class LaTeXException(Exception):
+ """
+ Exception base class to for exceptions which influence the
+ automatic generation of LaTeX code.
+ """
+
+
+class SkipAttrParentLaTeX(LaTeXException):
+ """
+ Do not generate ``\Dattr`` and ``\renewcommand{\Dparent}{...}`` for this
+ node.
+
+ To be raised from ``before_...`` methods.
+ """
+
+
+class SkipParentLaTeX(LaTeXException):
+ """
+ Do not generate ``\renewcommand{\DNparent}{...}`` for this node.
+
+ To be raised from ``before_...`` methods.
+ """
+
+
+class LaTeXTranslator(nodes.SparseNodeVisitor):
+
+ # Country code by a.schlock.
+ # Partly manually converted from iso and babel stuff.
+ iso639_to_babel = {
+ 'no': 'norsk', # added by hand
+ 'gd': 'scottish', # added by hand
+ 'sl': 'slovenian',
+ 'af': 'afrikaans',
+ 'bg': 'bulgarian',
+ 'br': 'breton',
+ 'ca': 'catalan',
+ 'cs': 'czech',
+ 'cy': 'welsh',
+ 'da': 'danish',
+ 'fr': 'french',
+ # french, francais, canadien, acadian
+ 'de': 'ngerman',
+ # ngerman, naustrian, german, germanb, austrian
+ 'el': 'greek',
+ 'en': 'english',
+ # english, USenglish, american, UKenglish, british, canadian
+ 'eo': 'esperanto',
+ 'es': 'spanish',
+ 'et': 'estonian',
+ 'eu': 'basque',
+ 'fi': 'finnish',
+ 'ga': 'irish',
+ 'gl': 'galician',
+ 'he': 'hebrew',
+ 'hr': 'croatian',
+ 'hu': 'hungarian',
+ 'is': 'icelandic',
+ 'it': 'italian',
+ 'la': 'latin',
+ 'nl': 'dutch',
+ 'pl': 'polish',
+ 'pt': 'portuguese',
+ 'ro': 'romanian',
+ 'ru': 'russian',
+ 'sk': 'slovak',
+ 'sr': 'serbian',
+ 'sv': 'swedish',
+ 'tr': 'turkish',
+ 'uk': 'ukrainian'
+ }
+
+ # Start with left double quote.
+ left_quote = 1
+
+ def __init__(self, document):
+ nodes.NodeVisitor.__init__(self, document)
+ self.settings = document.settings
+ self.header = []
+ self.body = []
+ self.context = []
+ self.stylesheet_path = utils.get_stylesheet_reference(
+ self.settings, os.path.join(os.getcwd(), 'dummy'))
+ if self.stylesheet_path:
+ self.settings.record_dependencies.add(self.stylesheet_path)
+ # This ugly hack will be cleaned up when refactoring the
+ # stylesheet mess.
+ self.settings.stylesheet = self.settings.user_stylesheet
+ self.settings.stylesheet_path = self.settings.user_stylesheet_path
+ self.user_stylesheet_path = utils.get_stylesheet_reference(
+ self.settings, os.path.join(os.getcwd(), 'dummy'))
+ if self.user_stylesheet_path:
+ self.settings.record_dependencies.add(self.user_stylesheet_path)
+ self.write_header()
+
+ def write_header(self):
+ a = self.header.append
+ a('%% Generated by Docutils %s <http://docutils.sourceforge.net>.'
+ % docutils.__version__)
+ a('')
+ a('% Docutils settings:')
+ lang = self.settings.language_code or ''
+ a(r'\providecommand{\Dlanguageiso}{%s}' % lang)
+ a(r'\providecommand{\Dlanguagebabel}{%s}' % self.iso639_to_babel.get(
+ lang, self.iso639_to_babel.get(lang.split('_')[0], '')))
+ a('')
+ if self.user_stylesheet_path:
+ a('% User stylesheet:')
+ a(r'\input{%s}' % self.user_stylesheet_path)
+ a('% Docutils stylesheet:')
+ a(r'\input{%s}' % self.stylesheet_path)
+ a('')
+ a('% Default definitions for Docutils nodes:')
+ for node_name in nodes.node_class_names:
+ a(r'\providecommand{\DN%s}[1]{#1}' % node_name.replace('_', ''))
+ a('')
+ a('% Auxiliary definitions:')
+ a(r'\providecommand{\Dsetattr}[2]{}')
+ a(r'\providecommand{\Dparent}{} % variable')
+ a(r'\providecommand{\Dattr}[5]{#5}')
+ a(r'\providecommand{\Dattrlen}{} % variable')
+ a(r'\providecommand{\Dtitleastext}{x} % variable')
+ a(r'\providecommand{\Dsinglebackref}{} % variable')
+ a(r'\providecommand{\Dmultiplebackrefs}{} % variable')
+ a(r'\providecommand{\Dparagraphindented}{false} % variable')
+ a('\n\n')
+
+ unicode_map = unicode_map.unicode_map # comprehensive Unicode map
+ # Fix problems with unimap.py.
+ unicode_map.update({
+ # We have AE or T1 encoding, so "``" etc. work. The macros
+ # from unimap.py may *not* work.
+ u'\u201C': '{``}',
+ u'\u201D': "{''}",
+ u'\u201E': '{,,}',
+ })
+
+ character_map = {
+ '\\': r'{\textbackslash}',
+ '{': r'{\{}',
+ '}': r'{\}}',
+ '$': r'{\$}',
+ '&': r'{\&}',
+ '%': r'{\%}',
+ '#': r'{\#}',
+ '[': r'{[}',
+ ']': r'{]}',
+ '-': r'{-}',
+ '`': r'{`}',
+ "'": r"{'}",
+ ',': r'{,}',
+ '"': r'{"}',
+ '|': r'{\textbar}',
+ '<': r'{\textless}',
+ '>': r'{\textgreater}',
+ '^': r'{\textasciicircum}',
+ '~': r'{\textasciitilde}',
+ '_': r'{\Dtextunderscore}',
+ }
+ character_map.update(unicode_map)
+ #character_map.update(special_map)
+
+ # `att_map` is for encoding attributes. According to
+ # <http://www-h.eng.cam.ac.uk/help/tpl/textprocessing/teTeX/latex/latex2e-html/ltx-164.html>,
+ # the following characters are special: # $ % & ~ _ ^ \ { }
+ # These work without special treatment in macro parameters:
+ # $, &, ~, _, ^
+ att_map = {'#': '\\#',
+ '%': '\\%',
+ # We cannot do anything about backslashes.
+ '\\': '',
+ '{': '\\{',
+ '}': '\\}',
+ # The quotation mark may be redefined by babel.
+ '"': '"{}',
+ }
+ att_map.update(unicode_map)
+
+ def encode(self, text, attval=None):
+ """
+ Encode special characters in ``text`` and return it.
+
+ If attval is true, preserve as much as possible verbatim (used
+ in attribute value encoding). If attval is 'width' or
+ 'height', `text` is interpreted as a length value.
+ """
+ if attval in ('width', 'height'):
+ match = re.match(r'([0-9.]+)(\S*)$', text)
+ assert match, '%s="%s" must be a length' % (attval, text)
+ value, unit = match.groups()
+ if unit == '%':
+ value = str(float(value) / 100)
+ unit = r'\Drelativeunit'
+ elif unit in ('', 'px'):
+ # If \Dpixelunit is "pt", this gives the same notion
+ # of pixels as graphicx.
+ value = str(float(value) * 0.75)
+ unit = '\Dpixelunit'
+ return '%s%s' % (value, unit)
+ if attval:
+ get = self.att_map.get
+ else:
+ get = self.character_map.get
+ text = ''.join([get(c, c) for c in text])
+ if (self.literal_block or self.inline_literal) and not attval:
+ # NB: We can have inline literals within literal blocks.
+ # Shrink '\r\n'.
+ text = text.replace('\r\n', '\n')
+ # Convert space. If "{ }~~~~~" is wrapped (at the
+ # brace-enclosed space "{ }"), the following non-breaking
+ # spaces ("~~~~") do *not* wind up at the beginning of the
+ # next line. Also note that, for some not-so-obvious
+ # reason, no hyphenation is done if the breaking space ("{
+ # }") comes *after* the non-breaking spaces.
+ if self.literal_block:
+ # Replace newlines with real newlines.
+ text = text.replace('\n', '\mbox{}\\\\')
+ replace_fn = self.encode_replace_for_literal_block_spaces
+ else:
+ replace_fn = self.encode_replace_for_inline_literal_spaces
+ text = re.sub(r'\s+', replace_fn, text)
+ # Protect hyphens; if we don't, line breaks will be
+ # possible at the hyphens and even the \textnhtt macro
+ # from the hyphenat package won't change that.
+ text = text.replace('-', r'\mbox{-}')
+ text = text.replace("'", r'{\Dtextliteralsinglequote}')
+ return text
+ else:
+ if not attval:
+ # Replace space with single protected space.
+ text = re.sub(r'\s+', '{ }', text)
+ # Replace double quotes with macro calls.
+ L = []
+ for part in text.split(self.character_map['"']):
+ if L:
+ # Insert quote.
+ L.append(self.left_quote and r'{\Dtextleftdblquote}'
+ or r'{\Dtextrightdblquote}')
+ self.left_quote = not self.left_quote
+ L.append(part)
+ return ''.join(L)
+ else:
+ return text
+
+ def encode_replace_for_literal_block_spaces(self, match):
+ return '~' * len(match.group())
+
+ def encode_replace_for_inline_literal_spaces(self, match):
+ return '{ }' + '~' * (len(match.group()) - 1)
+
+ def astext(self):
+ return '\n'.join(self.header) + (''.join(self.body))
+
+ def append(self, text, newline='%\n'):
+ """
+ Append text, stripping newlines, producing nice LaTeX code.
+ """
+ lines = [' ' * self.indentation_level + line + newline
+ for line in text.splitlines(0)]
+ self.body.append(''.join(lines))
+
+ def visit_Text(self, node):
+ self.append(self.encode(node.astext()))
+
+ def depart_Text(self, node):
+ pass
+
+ def is_indented(self, paragraph):
+ """Return true if `paragraph` should be first-line-indented."""
+ assert isinstance(paragraph, nodes.paragraph)
+ siblings = [n for n in paragraph.parent if
+ self.is_visible(n) and not isinstance(n, nodes.Titular)]
+ index = siblings.index(paragraph)
+ if ('continued' in paragraph['classes'] or
+ index > 0 and isinstance(siblings[index-1], nodes.transition)):
+ return 0
+ # Indent all but the first paragraphs.
+ return index > 0
+
+ def before_paragraph(self, node):
+ self.append(r'\renewcommand{\Dparagraphindented}{%s}'
+ % (self.is_indented(node) and 'true' or 'false'))
+
+ def before_title(self, node):
+ self.append(r'\renewcommand{\Dtitleastext}{%s}'
+ % self.encode(node.astext()))
+ self.append(r'\renewcommand{\Dhassubtitle}{%s}'
+ % ((len(node.parent) > 2 and
+ isinstance(node.parent[1], nodes.subtitle))
+ and 'true' or 'false'))
+
+ def before_generated(self, node):
+ if 'sectnum' in node['classes']:
+ node[0] = node[0].strip()
+
+ literal_block = 0
+
+ def visit_literal_block(self, node):
+ self.literal_block = 1
+
+ def depart_literal_block(self, node):
+ self.literal_block = 0
+
+ visit_doctest_block = visit_literal_block
+ depart_doctest_block = depart_literal_block
+
+ inline_literal = 0
+
+ def visit_literal(self, node):
+ self.inline_literal += 1
+
+ def depart_literal(self, node):
+ self.inline_literal -= 1
+
+ def visit_comment(self, node):
+ self.append('\n'.join(['% ' + line for line
+ in node.astext().splitlines(0)]), newline='\n')
+ raise nodes.SkipChildren
+
+ def before_topic(self, node):
+ if 'contents' in node['classes']:
+ for bullet_list in list(node.traverse(nodes.bullet_list)):
+ p = bullet_list.parent
+ if isinstance(p, nodes.list_item):
+ p.parent.insert(p.parent.index(p) + 1, bullet_list)
+ del p[1]
+ for paragraph in node.traverse(nodes.paragraph):
+ paragraph.attributes.update(paragraph[0].attributes)
+ paragraph[:] = paragraph[0]
+ paragraph.parent['tocrefid'] = paragraph['refid']
+ node['contents'] = 1
+ else:
+ node['contents'] = 0
+
+ bullet_list_level = 0
+
+ def visit_bullet_list(self, node):
+ self.append(r'\Dsetbullet{\labelitem%s}' %
+ ['i', 'ii', 'iii', 'iv'][min(self.bullet_list_level, 3)])
+ self.bullet_list_level += 1
+
+ def depart_bullet_list(self, node):
+ self.bullet_list_level -= 1
+
+ enum_styles = {'arabic': 'arabic', 'loweralpha': 'alph', 'upperalpha':
+ 'Alph', 'lowerroman': 'roman', 'upperroman': 'Roman'}
+
+ enum_counter = 0
+
+ def visit_enumerated_list(self, node):
+ # We create our own enumeration list environment. This allows
+ # to set the style and starting value and unlimited nesting.
+ # Maybe this can be moved to the stylesheet?
+ self.enum_counter += 1
+ enum_prefix = self.encode(node['prefix'])
+ enum_suffix = self.encode(node['suffix'])
+ enum_type = '\\' + self.enum_styles.get(node['enumtype'], r'arabic')
+ start = node.get('start', 1) - 1
+ counter = 'Denumcounter%d' % self.enum_counter
+ self.append(r'\Dmakeenumeratedlist{%s}{%s}{%s}{%s}{%s}{'
+ % (enum_prefix, enum_type, enum_suffix, counter, start))
+ # for Emacs: }
+
+ def depart_enumerated_list(self, node):
+ self.append('}') # for Emacs: {
+
+ def before_list_item(self, node):
+ # XXX needs cleanup.
+ if (len(node) and (isinstance(node[-1], nodes.TextElement) or
+ isinstance(node[-1], nodes.Text)) and
+ node.parent.index(node) == len(node.parent) - 1):
+ node['lastitem'] = 'true'
+
+ before_line = before_list_item
+
+ def before_raw(self, node):
+ if 'latex' in node.get('format', '').split():
+ # We're inserting the text in before_raw and thus outside
+ # of \DN... and \Dattr in order to make grouping with
+ # curly brackets work.
+ self.append(node.astext())
+ raise nodes.SkipChildren
+
+ def process_backlinks(self, node, type):
+ self.append(r'\renewcommand{\Dsinglebackref}{}')
+ self.append(r'\renewcommand{\Dmultiplebackrefs}{}')
+ if len(node['backrefs']) > 1:
+ refs = []
+ for i in range(len(node['backrefs'])):
+ refs.append(r'\Dmulti%sbacklink{%s}{%s}'
+ % (type, node['backrefs'][i], i + 1))
+ self.append(r'\renewcommand{\Dmultiplebackrefs}{(%s){ }}'
+ % ', '.join(refs))
+ elif len(node['backrefs']) == 1:
+ self.append(r'\renewcommand{\Dsinglebackref}{%s}'
+ % node['backrefs'][0])
+
+ def visit_footnote(self, node):
+ self.process_backlinks(node, 'footnote')
+
+ def visit_citation(self, node):
+ self.process_backlinks(node, 'citation')
+
+ def before_table(self, node):
+ # A table contains exactly one tgroup. See before_tgroup.
+ pass
+
+ def before_tgroup(self, node):
+ widths = []
+ total_width = 0
+ for i in range(int(node['cols'])):
+ assert isinstance(node[i], nodes.colspec)
+ widths.append(int(node[i]['colwidth']) + 1)
+ total_width += widths[-1]
+ del node[:len(widths)]
+ tablespec = '|'
+ for w in widths:
+ # 0.93 is probably wrong in many cases. XXX Find a
+ # solution which works *always*.
+ tablespec += r'p{%s\textwidth}|' % (0.93 * w /
+ max(total_width, 60))
+ self.append(r'\Dmaketable{%s}{' % tablespec)
+ self.context.append('}')
+ raise SkipAttrParentLaTeX
+
+ def depart_tgroup(self, node):
+ self.append(self.context.pop())
+
+ def before_row(self, node):
+ raise SkipAttrParentLaTeX
+
+ def before_thead(self, node):
+ raise SkipAttrParentLaTeX
+
+ def before_tbody(self, node):
+ raise SkipAttrParentLaTeX
+
+ def is_simply_entry(self, node):
+ return (len(node) == 1 and isinstance(node[0], nodes.paragraph) or
+ len(node) == 0)
+
+ def before_entry(self, node):
+ is_leftmost = 0
+ if node.hasattr('morerows'):
+ self.document.reporter.severe('Rowspans are not supported.')
+ # Todo: Add empty cells below rowspanning cell and issue
+ # warning instead of severe.
+ if node.hasattr('morecols'):
+ # The author got a headache trying to implement
+ # multicolumn support.
+ if not self.is_simply_entry(node):
+ self.document.reporter.severe(
+ 'Colspanning table cells may only contain one paragraph.')
+ # Todo: Same as above.
+ # The number of columns this entry spans (as a string).
+ colspan = int(node['morecols']) + 1
+ del node['morecols']
+ else:
+ colspan = 1
+ # Macro to call.
+ macro_name = r'\Dcolspan'
+ if node.parent.index(node) == 0:
+ # Leftmost column.
+ macro_name += 'left'
+ is_leftmost = 1
+ if colspan > 1:
+ self.append('%s{%s}{' % (macro_name, colspan))
+ self.context.append('}')
+ else:
+ # Do not add a multicolumn with colspan 1 beacuse we need
+ # at least one non-multicolumn cell per column to get the
+ # desired column widths, and we can only do colspans with
+ # cells consisting of only one paragraph.
+ if not is_leftmost:
+ self.append(r'\Dsubsequententry{')
+ self.context.append('}')
+ else:
+ self.context.append('')
+ if isinstance(node.parent.parent, nodes.thead):
+ node['tableheaderentry'] = 'true'
+
+ # Don't add \renewcommand{\Dparent}{...} because there must
+ # not be any non-expandable commands in front of \multicolumn.
+ raise SkipParentLaTeX
+
+ def depart_entry(self, node):
+ self.append(self.context.pop())
+
+ def before_substitution_definition(self, node):
+ raise nodes.SkipNode
+
+ indentation_level = 0
+
+ def node_name(self, node):
+ return node.__class__.__name__.replace('_', '')
+
+ # Attribute propagation order.
+ attribute_order = ['align', 'classes', 'ids']
+
+ def attribute_cmp(self, a1, a2):
+ """
+ Compare attribute names `a1` and `a2`. Used in
+ propagate_attributes to determine propagation order.
+
+ See built-in function `cmp` for return value.
+ """
+ if a1 in self.attribute_order and a2 in self.attribute_order:
+ return cmp(self.attribute_order.index(a1),
+ self.attribute_order.index(a2))
+ if (a1 in self.attribute_order) != (a2 in self.attribute_order):
+ # Attributes not in self.attribute_order come last.
+ return a1 in self.attribute_order and -1 or 1
+ else:
+ return cmp(a1, a2)
+
+ def propagate_attributes(self, node):
+ # Propagate attributes using \Dattr macros.
+ node_name = self.node_name(node)
+ attlist = []
+ if isinstance(node, nodes.Element):
+ attlist = node.attlist()
+ attlist.sort(lambda pair1, pair2: self.attribute_cmp(pair1[0],
+ pair2[0]))
+ # `numatts` may be greater than len(attlist) due to list
+ # attributes.
+ numatts = 0
+ pass_contents = self.pass_contents(node)
+ for key, value in attlist:
+ if isinstance(value, ListType):
+ self.append(r'\renewcommand{\Dattrlen}{%s}' % len(value))
+ for i in range(len(value)):
+ self.append(r'\Dattr{%s}{%s}{%s}{%s}{' %
+ (i+1, key, self.encode(value[i], attval=key),
+ node_name))
+ if not pass_contents:
+ self.append('}')
+ numatts += len(value)
+ else:
+ self.append(r'\Dattr{}{%s}{%s}{%s}{' %
+ (key, self.encode(unicode(value), attval=key),
+ node_name))
+ if not pass_contents:
+ self.append('}')
+ numatts += 1
+ if pass_contents:
+ self.context.append('}' * numatts) # for Emacs: {
+ else:
+ self.context.append('')
+
+ def visit_docinfo(self, node):
+ raise NotImplementedError('Docinfo not yet implemented.')
+
+ def visit_document(self, node):
+ document = node
+ # Move IDs into TextElements. This won't work for images.
+ # Need to review this.
+ for node in document.traverse(nodes.Element):
+ if node.has_key('ids') and not isinstance(node,
+ nodes.TextElement):
+ next_text_element = node.next_node(nodes.TextElement)
+ if next_text_element:
+ next_text_element['ids'].extend(node['ids'])
+ node['ids'] = []
+
+ def pass_contents(self, node):
+ r"""
+ Return true if the node contents should be passed in
+ parameters of \DN... and \Dattr.
+ """
+ return not isinstance(node, (nodes.document, nodes.section))
+
+ def dispatch_visit(self, node):
+ skip_attr = skip_parent = 0
+ # TreePruningException to be propagated.
+ tree_pruning_exception = None
+ if hasattr(self, 'before_' + node.__class__.__name__):
+ try:
+ getattr(self, 'before_' + node.__class__.__name__)(node)
+ except SkipParentLaTeX:
+ skip_parent = 1
+ except SkipAttrParentLaTeX:
+ skip_attr = 1
+ skip_parent = 1
+ except nodes.SkipNode:
+ raise
+ except (nodes.SkipChildren, nodes.SkipSiblings), instance:
+ tree_pruning_exception = instance
+ except nodes.SkipDeparture:
+ raise NotImplementedError(
+ 'SkipDeparture not usable in LaTeX writer')
+
+ if not isinstance(node, nodes.Text):
+ node_name = self.node_name(node)
+ # attribute_deleters will be appended to self.context.
+ attribute_deleters = []
+ if not skip_parent and not isinstance(node, nodes.document):
+ self.append(r'\renewcommand{\Dparent}{%s}'
+ % self.node_name(node.parent))
+ for name, value in node.attlist():
+ if not isinstance(value, ListType) and not ':' in name:
+ macro = r'\DcurrentN%sA%s' % (node_name, name)
+ self.append(r'\def%s{%s}' % (
+ macro, self.encode(unicode(value), attval=name)))
+ attribute_deleters.append(r'\let%s=\relax' % macro)
+ self.context.append('\n'.join(attribute_deleters))
+ if self.pass_contents(node):
+ self.append(r'\DN%s{' % node_name)
+ self.context.append('}')
+ else:
+ self.append(r'\Dvisit%s' % node_name)
+ self.context.append(r'\Ddepart%s' % node_name)
+ self.indentation_level += 1
+ if not skip_attr:
+ self.propagate_attributes(node)
+ else:
+ self.context.append('')
+
+ if (isinstance(node, nodes.TextElement) and
+ not isinstance(node.parent, nodes.TextElement)):
+ # Reset current quote to left.
+ self.left_quote = 1
+
+ # Call visit_... method.
+ try:
+ nodes.SparseNodeVisitor.dispatch_visit(self, node)
+ except LaTeXException:
+ raise NotImplementedError(
+ 'visit_... methods must not raise LaTeXExceptions')
+
+ if tree_pruning_exception:
+ # Propagate TreePruningException raised in before_... method.
+ raise tree_pruning_exception
+
+ def is_invisible(self, node):
+ # Return true if node is invisible or moved away in the LaTeX
+ # rendering.
+ return (not isinstance(node, nodes.Text) and
+ (isinstance(node, nodes.Invisible) or
+ isinstance(node, nodes.footnote) or
+ isinstance(node, nodes.citation) or
+ # Assume raw nodes to be invisible.
+ isinstance(node, nodes.raw) or
+ # Floating image or figure.
+ node.get('align') in ('left', 'right')))
+
+ def is_visible(self, node):
+ return not self.is_invisible(node)
+
+ def needs_space(self, node):
+ """Two nodes for which `needs_space` is true need auxiliary space."""
+ # Return true if node is a visible block-level element.
+ return ((isinstance(node, nodes.Body) or
+ isinstance(node, nodes.topic)) and
+ not (self.is_invisible(node) or
+ isinstance(node.parent, nodes.TextElement)))
+
+ def always_needs_space(self, node):
+ """
+ Always add space around nodes for which `always_needs_space()`
+ is true, regardless of whether the other node needs space as
+ well. (E.g. transition next to section.)
+ """
+ return isinstance(node, nodes.transition)
+
+ def dispatch_departure(self, node):
+ # Call departure method.
+ nodes.SparseNodeVisitor.dispatch_departure(self, node)
+
+ if not isinstance(node, nodes.Text):
+ # Close attribute and node handler call (\DN...{...}).
+ self.indentation_level -= 1
+ self.append(self.context.pop() + self.context.pop())
+ # Delete \Dcurrent... attribute macros.
+ self.append(self.context.pop())
+ # Get next sibling.
+ next_node = node.next_node(
+ ascend=0, siblings=1, descend=0,
+ condition=self.is_visible)
+ # Insert space if necessary.
+ if (self.needs_space(node) and self.needs_space(next_node) or
+ self.always_needs_space(node) or
+ self.always_needs_space(next_node)):
+ if isinstance(node, nodes.paragraph) and isinstance(next_node, nodes.paragraph):
+ # Space between paragraphs.
+ self.append(r'\Dparagraphspace')
+ else:
+ # One of the elements is not a paragraph.
+ self.append(r'\Dauxiliaryspace')