Quick Search:

View

Revision:

Diff

Diff from 2030 to:

Annotations

Annotate by Age | Author | None
/fisheye/browse/selenium-rc/branches/documenthierarchy/clients/python/lib/docutils/writers/newlatex2e/__init__.py

Annotated File View

dfabulich
1106
1 # Author: Felix Wiemann
2 # Contact: Felix_Wiemann@ososo.de
3 # Revision: $Revision: 4242 $
4 # Date: $Date: 2006-01-06 00:28:53 +0100 (Fri, 06 Jan 2006) $
5 # Copyright: This module has been placed in the public domain.
6
7 """
8 LaTeX2e document tree Writer.
9 """
10
11 # Thanks to Engelbert Gruber and various contributors for the original
12 # LaTeX writer, some code and many ideas of which have been used for
13 # this writer.
14
15 __docformat__ = 'reStructuredText'
16
17
18 import re
19 import os.path
20 from types import ListType
21
22 import docutils
23 from docutils import nodes, writers, utils
24 from docutils.writers.newlatex2import unicode_map
25 from docutils.transforms import writer_aux
26
27
28 class Writer(writers.Writer):
29
30     supported = ('newlatex''newlatex2e')
31     """Formats this writer supports."""
32
33     default_stylesheet = 'base.tex'
34
35     default_stylesheet_path = utils.relative_path(
36         os.path.join(os.getcwd(), 'dummy'),
37         os.path.join(os.path.dirname(__file__), default_stylesheet))
38
39     settings_spec = (
40         'LaTeX-Specific Options',
41         'Note that this LaTeX writer is still EXPERIMENTAL. '
42         'You must specify the location of the tools/stylesheets/latex.tex '
43         'stylesheet file contained in the Docutils distribution tarball to '
44         'make the LaTeX output work.',
45         (('Specify a stylesheet file.  The path is used verbatim to include '
46           'the file.  Overrides --stylesheet-path.',
47           ['--stylesheet'],
48           {'default''''metavar''<file>',
49            'overrides''stylesheet_path'}),
50          ('Specify a stylesheet file, relative to the current working '
51           'directory.  Overrides --stylesheet.  Default: "%s"'
52           % default_stylesheet_path,
53           ['--stylesheet-path'],
54           {'metavar''<file>''overrides''stylesheet',
55            'default': default_stylesheet_path}),
56          ('Specify a user stylesheet file.  See --stylesheet.',
57           ['--user-stylesheet'],
58           {'default''''metavar''<file>',
59            'overrides''user_stylesheet_path'}),
60          ('Specify a user stylesheet file.  See --stylesheet-path.',
61           ['--user-stylesheet-path'],
62           {'metavar''<file>''overrides''user_stylesheet'})
63          ),)
64
65     settings_defaults = {
66         # Many Unicode characters are provided by unicode_map.py.
67         'output_encoding''ascii',
68         'output_encoding_error_handler''strict',
69         # Since we are using superscript footnotes, it is necessary to
70         # trim whitespace in front of footnote references.
71         'trim_footnote_reference_space'1,
72         # Currently unsupported:
73         'docinfo_xform'0,
74         # During development:
75         'traceback'1
76         }
77
78     relative_path_settings = ('stylesheet_path''user_stylesheet_path')
79
80     config_section = 'newlatex2e writer'
81     config_section_dependencies = ('writers',)
82
83     output = None
84     """Final translated form of `document`."""
85
86     def get_transforms(self):
87         return writers.Writer.get_transforms(self) + [writer_aux.Compound]
88
89     def __init__(self):
90         writers.Writer.__init__(self)
91         self.translator_class = LaTeXTranslator
92
93     def translate(self):
94         visitor = self.translator_class(self.document)
95         self.document.walkabout(visitor)
96         assert not visitor.context, 'context not empty: %s' % visitor.context
97         self.output = visitor.astext()
98         self.head = visitor.header
99         self.body = visitor.body
100
101
102 class LaTeXException(Exception):
103     """
104     Exception base class to for exceptions which influence the
105     automatic generation of LaTeX code.
106     """
107
108
109 class SkipAttrParentLaTeX(LaTeXException):
110     """
111     Do not generate ``\Dattr`` and ``\renewcommand{\Dparent}{...}`` for this
112     node.
113
114     To be raised from ``before_...`` methods.
115     """
116
117
118 class SkipParentLaTeX(LaTeXException):
119     """
120     Do not generate ``\renewcommand{\DNparent}{...}`` for this node.
121
122     To be raised from ``before_...`` methods.
123     """
124
125
126 class LaTeXTranslator(nodes.SparseNodeVisitor):
127
128     # Country code by a.schlock.
129     # Partly manually converted from iso and babel stuff.
130     iso639_to_babel = {
131         'no''norsk',     # added by hand
132         'gd''scottish',  # added by hand
133         'sl''slovenian',
134         'af''afrikaans',
135         'bg''bulgarian',
136         'br''breton',
137         'ca''catalan',
138         'cs''czech',
139         'cy''welsh',
140         'da''danish',
141         'fr''french',
142         # french, francais, canadien, acadian
143         'de''ngerman',
144         # ngerman, naustrian, german, germanb, austrian
145         'el''greek',
146         'en''english',
147         # english, USenglish, american, UKenglish, british, canadian
148         'eo''esperanto',
149         'es''spanish',
150         'et''estonian',
151         'eu''basque',
152         'fi''finnish',
153         'ga''irish',
154         'gl''galician',
155         'he''hebrew',
156         'hr''croatian',
157         'hu''hungarian',
158         'is''icelandic',
159         'it''italian',
160         'la''latin',
161         'nl''dutch',
162         'pl''polish',
163         'pt''portuguese',
164         'ro''romanian',
165         'ru''russian',
166         'sk''slovak',
167         'sr''serbian',
168         'sv''swedish',
169         'tr''turkish',
170         'uk''ukrainian'
171     }
172
173     # Start with left double quote.
174     left_quote = 1
175
176     def __init__(self, document):
177         nodes.NodeVisitor.__init__(self, document)
178         self.settings = document.settings
179         self.header = []
180         self.body = []
181         self.context = []
182         self.stylesheet_path = utils.get_stylesheet_reference(
183             self.settings, os.path.join(os.getcwd(), 'dummy'))
184         if self.stylesheet_path:
185             self.settings.record_dependencies.add(self.stylesheet_path)
186         # This ugly hack will be cleaned up when refactoring the
187         # stylesheet mess.
188         self.settings.stylesheet = self.settings.user_stylesheet
189         self.settings.stylesheet_path = self.settings.user_stylesheet_path
190         self.user_stylesheet_path = utils.get_stylesheet_reference(
191             self.settings, os.path.join(os.getcwd(), 'dummy'))
192         if self.user_stylesheet_path:
193             self.settings.record_dependencies.add(self.user_stylesheet_path)
194         self.write_header()
195
196     def write_header(self):
197         a = self.header.append
198         a('%% Generated by Docutils %s <http://docutils.sourceforge.net>.'
199           % docutils.__version__)
200         a('')
201         a('% Docutils settings:')
202         lang = self.settings.language_code or ''
203         a(r'\providecommand{\Dlanguageiso}{%s}' % lang)
204         a(r'\providecommand{\Dlanguagebabel}{%s}' % self.iso639_to_babel.get(
205             lang, self.iso639_to_babel.get(lang.split('_')[0], '')))
206         a('')
207         if self.user_stylesheet_path:
208             a('% User stylesheet:')
209             a(r'\input{%s}' % self.user_stylesheet_path)
210         a('% Docutils stylesheet:')
211         a(r'\input{%s}' % self.stylesheet_path)
212         a('')
213         a('% Default definitions for Docutils nodes:')
214         for node_name in nodes.node_class_names:
215             a(r'\providecommand{\DN%s}[1]{#1}' % node_name.replace('_'''))
216         a('')
217         a('% Auxiliary definitions:')
218         a(r'\providecommand{\Dsetattr}[2]{}')
219         a(r'\providecommand{\Dparent}{} % variable')
220         a(r'\providecommand{\Dattr}[5]{#5}')
221         a(r'\providecommand{\Dattrlen}{} % variable')
222         a(r'\providecommand{\Dtitleastext}{x} % variable')
223         a(r'\providecommand{\Dsinglebackref}{} % variable')
224         a(r'\providecommand{\Dmultiplebackrefs}{} % variable')
225         a(r'\providecommand{\Dparagraphindented}{false} % variable')
226         a('\n\n')
227
228     unicode_map = unicode_map.unicode_map # comprehensive Unicode map
229     # Fix problems with unimap.py.
230     unicode_map.update({
231         # We have AE or T1 encoding, so "``" etc. work.  The macros
232         # from unimap.py may *not* work.
233         u'\u201C''{``}',
234         u'\u201D'"{''}",
235         u'\u201E''{,,}',
236         })
237
238     character_map = {
239         '\\'r'{\textbackslash}',
240         '{'r'{\{}',
241         '}'r'{\}}',
242         '$'r'{\$}',
243         '&'r'{\&}',
244         '%'r'{\%}',
245         '#'r'{\#}',
246         '['r'{[}',
247         ']'r'{]}',
248         '-'r'{-}',
249         '`'r'{`}',
250         "'"r"{'}",
251         ','r'{,}',
252         '"'r'{"}',
253         '|'r'{\textbar}',
254         '<'r'{\textless}',
255         '>'r'{\textgreater}',
256         '^'r'{\textasciicircum}',
257         '~'r'{\textasciitilde}',
258         '_'r'{\Dtextunderscore}',
259         }
260     character_map.update(unicode_map)
261     #character_map.update(special_map)
262     
263     # `att_map` is for encoding attributes.  According to
264     # <http://www-h.eng.cam.ac.uk/help/tpl/textprocessing/teTeX/latex/latex2e-html/ltx-164.html>,
265     # the following characters are special: # $ % & ~ _ ^ \ { }
266     # These work without special treatment in macro parameters:
267     # $, &, ~, _, ^
268     att_map = {'#''\\#',
269                '%''\\%',
270                # We cannot do anything about backslashes.
271                '\\''',
272                '{''\\{',
273                '}''\\}',
274                # The quotation mark may be redefined by babel.
275                '"''"{}',
276                }
277     att_map.update(unicode_map)
278
279     def encode(self, text, attval=None):
280         """
281         Encode special characters in ``text`` and return it.
282
283         If attval is true, preserve as much as possible verbatim (used
284         in attribute value encoding).  If attval is 'width' or
285         'height', `text` is interpreted as a length value.
286         """
287         if attval in ('width''height'):
288             match = re.match(r'([0-9.]+)(\S*)$', text)
289             assert match, '%s="%s" must be a length' % (attval, text)
290             value, unit = match.groups()
291             if unit == '%':
292                 value = str(float(value) / 100)
293                 unit = r'\Drelativeunit'
294             elif unit in ('''px'):
295                 # If \Dpixelunit is "pt", this gives the same notion
296                 # of pixels as graphicx.
297                 value = str(float(value) * 0.75)
298                 unit = '\Dpixelunit'
299             return '%s%s' % (value, unit)
300         if attval:
301             get = self.att_map.get
302         else:
303             get = self.character_map.get
304         text = ''.join([get(c, c) for c in text])
305         if (self.literal_block or self.inline_literal) and not attval:
306             # NB: We can have inline literals within literal blocks.
307             # Shrink '\r\n'.
308             text = text.replace('\r\n''\n')
309             # Convert space.  If "{ }~~~~~" is wrapped (at the
310             # brace-enclosed space "{ }"), the following non-breaking
311             # spaces ("~~~~") do *not* wind up at the beginning of the
312             # next line.  Also note that, for some not-so-obvious
313             # reason, no hyphenation is done if the breaking space ("{
314             # }") comes *after* the non-breaking spaces.
315             if self.literal_block:
316                 # Replace newlines with real newlines.
317                 text = text.replace('\n''\mbox{}\\\\')
318                 replace_fn = self.encode_replace_for_literal_block_spaces
319             else:
320                 replace_fn = self.encode_replace_for_inline_literal_spaces
321             text = re.sub(r'\s+', replace_fn, text)
322             # Protect hyphens; if we don't, line breaks will be
323             # possible at the hyphens and even the \textnhtt macro
324             # from the hyphenat package won't change that.
325             text = text.replace('-'r'\mbox{-}')
326             text = text.replace("'"r'{\Dtextliteralsinglequote}')
327             return text
328         else:
329             if not attval:
330                 # Replace space with single protected space.
331                 text = re.sub(r'\s+''{ }', text)
332                 # Replace double quotes with macro calls.
333                 L = []
334                 for part in text.split(self.character_map['"']):
335                     if L:
336                         # Insert quote.
337                         L.append(self.left_quote and r'{\Dtextleftdblquote}'
338                                  or r'{\Dtextrightdblquote}')
339                         self.left_quote = not self.left_quote
340                     L.append(part)
341                 return ''.join(L)
342             else:
343                 return text
344
345     def encode_replace_for_literal_block_spaces(self, match):
346         return '~' * len(match.group())
347
348     def encode_replace_for_inline_literal_spaces(self, match):
349         return '{ }' + '~' * (len(match.group()) - 1)
350
351     def astext(self):
352         return '\n'.join(self.header) + (''.join(self.body))
353
354     def append(self, text, newline='%\n'):
355         """
356         Append text, stripping newlines, producing nice LaTeX code.
357         """
358         lines = ['  ' * self.indentation_level + line + newline
359                  for line in text.splitlines(0)]
360         self.body.append(''.join(lines))
361
362     def visit_Text(self, node):
363         self.append(self.encode(node.astext()))
364
365     def depart_Text(self, node):
366         pass
367
368     def is_indented(self, paragraph):
369         """Return true if `paragraph` should be first-line-indented."""
370         assert isinstance(paragraph, nodes.paragraph)
371         siblings = [n for n in paragraph.parent if
372                     self.is_visible(n) and not isinstance(n, nodes.Titular)]
373         index = siblings.index(paragraph)
374         if ('continued' in paragraph['classes'or
375             index > 0 and isinstance(siblings[index-1], nodes.transition)):
376             return 0
377         # Indent all but the first paragraphs.
378         return index > 0
379
380     def before_paragraph(self, node):
381         self.append(r'\renewcommand{\Dparagraphindented}{%s}'
382                     % (self.is_indented(node) and 'true' or 'false'))
383
384     def before_title(self, node):
385         self.append(r'\renewcommand{\Dtitleastext}{%s}'
386                     % self.encode(node.astext()))
387         self.append(r'\renewcommand{\Dhassubtitle}{%s}'
388                     % ((len(node.parent) > 2 and
389                         isinstance(node.parent[1], nodes.subtitle))
390                        and 'true' or 'false'))
391
392     def before_generated(self, node):
393         if 'sectnum' in node['classes']:
394             node[0] = node[0].strip()
395
396     literal_block = 0
397
398     def visit_literal_block(self, node):
399         self.literal_block = 1
400
401     def depart_literal_block(self, node):
402         self.literal_block = 0
403
404     visit_doctest_block = visit_literal_block
405     depart_doctest_block = depart_literal_block
406
407     inline_literal = 0
408
409     def visit_literal(self, node):
410         self.inline_literal += 1
411
412     def depart_literal(self, node):
413         self.inline_literal -= 1
414
415     def visit_comment(self, node):
416         self.append('\n'.join(['% ' + line for line
417                                in node.astext().splitlines(0)]), newline='\n')
418         raise nodes.SkipChildren
419
420     def before_topic(self, node):
421         if 'contents' in node['classes']:
422             for bullet_list in list(node.traverse(nodes.bullet_list)):
423                 p = bullet_list.parent
424                 if isinstance(p, nodes.list_item):
425                     p.parent.insert(p.parent.index(p) + 1, bullet_list)
426                     del p[1]
427             for paragraph in node.traverse(nodes.paragraph):
428                 paragraph.attributes.update(paragraph[0].attributes)
429                 paragraph[:] = paragraph[0]
430                 paragraph.parent['tocrefid'] = paragraph['refid']
431             node['contents'] = 1
432         else:
433             node['contents'] = 0
434
435     bullet_list_level = 0
436
437     def visit_bullet_list(self, node):
438         self.append(r'\Dsetbullet{\labelitem%s}' %
439                     ['i''ii''iii''iv'][min(self.bullet_list_level, 3)])
440         self.bullet_list_level += 1
441
442     def depart_bullet_list(self, node):
443         self.bullet_list_level -= 1
444
445     enum_styles = {'arabic''arabic''loweralpha''alph''upperalpha':
446                    'Alph''lowerroman''roman''upperroman''Roman'}
447
448     enum_counter = 0
449
450     def visit_enumerated_list(self, node):
451         # We create our own enumeration list environment.  This allows
452         # to set the style and starting value and unlimited nesting.
453         # Maybe this can be moved to the stylesheet?
454         self.enum_counter += 1
455         enum_prefix = self.encode(node['prefix'])
456         enum_suffix = self.encode(node['suffix'])
457         enum_type = '\\' + self.enum_styles.get(node['enumtype'], r'arabic')
458         start = node.get('start'1) - 1
459         counter = 'Denumcounter%d' % self.enum_counter
460         self.append(r'\Dmakeenumeratedlist{%s}{%s}{%s}{%s}{%s}{'
461                     % (enum_prefix, enum_type, enum_suffix, counter, start))
462                     # for Emacs: }
463
464     def depart_enumerated_list(self, node):
465         self.append('}')  # for Emacs: {
466
467     def before_list_item(self, node):
468         # XXX needs cleanup.
469         if (len(node) and (isinstance(node[-1], nodes.TextElement) or
470                            isinstance(node[-1], nodes.Text)) and
471             node.parent.index(node) == len(node.parent) - 1):
472             node['lastitem'] = 'true'
473
474     before_line = before_list_item
475
476     def before_raw(self, node):
477         if 'latex' in node.get('format''').split():
478             # We're inserting the text in before_raw and thus outside
479             # of \DN... and \Dattr in order to make grouping with
480             # curly brackets work.
481             self.append(node.astext())
482         raise nodes.SkipChildren
483
484     def process_backlinks(self, node, type):
485         self.append(r'\renewcommand{\Dsinglebackref}{}')
486         self.append(r'\renewcommand{\Dmultiplebackrefs}{}')
487         if len(node['backrefs']) > 1:
488             refs = []
489             for i in range(len(node['backrefs'])):
490                 refs.append(r'\Dmulti%sbacklink{%s}{%s}'
491                             % (type, node['backrefs'][i], i + 1))
492             self.append(r'\renewcommand{\Dmultiplebackrefs}{(%s){ }}'
493                         % ', '.join(refs))
494         elif len(node['backrefs']) == 1:
495             self.append(r'\renewcommand{\Dsinglebackref}{%s}'
496                         % node['backrefs'][0])
497
498     def visit_footnote(self, node):
499         self.process_backlinks(node, 'footnote')
500
501     def visit_citation(self, node):
502         self.process_backlinks(node, 'citation')
503
504     def before_table(self, node):
505         # A table contains exactly one tgroup.  See before_tgroup.
506         pass
507
508     def before_tgroup(self, node):
509         widths = []
510         total_width = 0
511         for i in range(int(node['cols'])):
512             assert isinstance(node[i], nodes.colspec)
513             widths.append(int(node[i]['colwidth']) + 1)
514             total_width += widths[-1]
515         del node[:len(widths)]
516         tablespec = '|'
517         for w in widths:
518             # 0.93 is probably wrong in many cases.  XXX Find a
519             # solution which works *always*.
520             tablespec += r'p{%s\textwidth}|' % (0.93 * w /
521                                                 max(total_width, 60))
522         self.append(r'\Dmaketable{%s}{' % tablespec)
523         self.context.append('}')
524         raise SkipAttrParentLaTeX
525
526     def depart_tgroup(self, node):
527         self.append(self.context.pop())
528
529     def before_row(self, node):
530         raise SkipAttrParentLaTeX
531
532     def before_thead(self, node):
533         raise SkipAttrParentLaTeX
534
535     def before_tbody(self, node):
536         raise SkipAttrParentLaTeX
537
538     def is_simply_entry(self, node):
539         return (len(node) == 1 and isinstance(node[0], nodes.paragraph) or
540                 len(node) == 0)
541
542     def before_entry(self, node):
543         is_leftmost = 0
544         if node.hasattr('morerows'):
545             self.document.reporter.severe('Rowspans are not supported.')
546             # Todo: Add empty cells below rowspanning cell and issue
547             # warning instead of severe.
548         if node.hasattr('morecols'):
549             # The author got a headache trying to implement
550             # multicolumn support.
551             if not self.is_simply_entry(node):
552                 self.document.reporter.severe(
553                     'Colspanning table cells may only contain one paragraph.')
554                 # Todo: Same as above.
555             # The number of columns this entry spans (as a string).
556             colspan = int(node['morecols']) + 1
557             del node['morecols']
558         else:
559             colspan = 1
560         # Macro to call.
561         macro_name = r'\Dcolspan'
562         if node.parent.index(node) == 0:
563             # Leftmost column.
564             macro_name += 'left'
565             is_leftmost = 1
566         if colspan > 1:
567             self.append('%s{%s}{' % (macro_name, colspan))
568             self.context.append('}')
569         else:
570             # Do not add a multicolumn with colspan 1 beacuse we need
571             # at least one non-multicolumn cell per column to get the
572             # desired column widths, and we can only do colspans with
573             # cells consisting of only one paragraph.
574             if not is_leftmost:
575                 self.append(r'\Dsubsequententry{')
576                 self.context.append('}')
577             else:
578                 self.context.append('')
579         if isinstance(node.parent.parent, nodes.thead):
580             node['tableheaderentry'] = 'true'
581
582         # Don't add \renewcommand{\Dparent}{...} because there must
583         # not be any non-expandable commands in front of \multicolumn.
584         raise SkipParentLaTeX
585
586     def depart_entry(self, node):
587         self.append(self.context.pop())
588
589     def before_substitution_definition(self, node):
590         raise nodes.SkipNode
591
592     indentation_level = 0
593
594     def node_name(self, node):
595         return node.__class__.__name__.replace('_''')
596
597     # Attribute propagation order.
598     attribute_order = ['align''classes''ids']
599
600     def attribute_cmp(self, a1, a2):
601         """
602         Compare attribute names `a1` and `a2`.  Used in
603         propagate_attributes to determine propagation order.
604
605         See built-in function `cmp` for return value.
606         """
607         if a1 in self.attribute_order and a2 in self.attribute_order:
608             return cmp(self.attribute_order.index(a1),
609                        self.attribute_order.index(a2))
610         if (a1 in self.attribute_order) != (a2 in self.attribute_order):
611             # Attributes not in self.attribute_order come last.
612             return a1 in self.attribute_order and -1 or 1
613         else:
614             return cmp(a1, a2)
615
616     def propagate_attributes(self, node):
617         # Propagate attributes using \Dattr macros.
618         node_name = self.node_name(node)
619         attlist = []
620         if isinstance(node, nodes.Element):
621             attlist = node.attlist()
622         attlist.sort(lambda pair1, pair2: self.attribute_cmp(pair1[0],
623                                                              pair2[0]))
624         # `numatts` may be greater than len(attlist) due to list
625         # attributes.
626         numatts = 0
627         pass_contents = self.pass_contents(node)
628         for key, value in attlist:
629             if isinstance(value, ListType):
630                 self.append(r'\renewcommand{\Dattrlen}{%s}' % len(value))
631                 for i in range(len(value)):
632                     self.append(r'\Dattr{%s}{%s}{%s}{%s}{' %
633                                 (i+1, key, self.encode(value[i], attval=key),
634                                  node_name))
635                     if not pass_contents:
636                         self.append('}')
637                 numatts += len(value)
638             else:
639                 self.append(r'\Dattr{}{%s}{%s}{%s}{' %
640                             (key, self.encode(unicode(value), attval=key),
641                              node_name))
642                 if not pass_contents:
643                     self.append('}')
644                 numatts += 1
645         if pass_contents:
646             self.context.append('}' * numatts)  # for Emacs: {
647         else:
648             self.context.append('')
649
650     def visit_docinfo(self, node):
651         raise NotImplementedError('Docinfo not yet implemented.')
652
653     def visit_document(self, node):
654         document = node
655         # Move IDs into TextElements.  This won't work for images.
656         # Need to review this.
657         for node in document.traverse(nodes.Element):
658             if node.has_key('ids'and not isinstance(node,
659                                                       nodes.TextElement):
660                 next_text_element = node.next_node(nodes.TextElement)
661                 if next_text_element:
662                     next_text_element['ids'].extend(node['ids'])
663                     node['ids'] = []
664
665     def pass_contents(self, node):
666         r"""
667         Return true if the node contents should be passed in
668         parameters of \DN... and \Dattr.
669         """
670         return not isinstance(node, (nodes.document, nodes.section))
671
672     def dispatch_visit(self, node):
673         skip_attr = skip_parent = 0
674         # TreePruningException to be propagated.
675         tree_pruning_exception = None
676         if hasattr(self, 'before_' + node.__class__.__name__):
677             try:
678                 getattr(self, 'before_' + node.__class__.__name__)(node)
679             except SkipParentLaTeX:
680                 skip_parent = 1
681             except SkipAttrParentLaTeX:
682                 skip_attr = 1
683                 skip_parent = 1
684             except nodes.SkipNode:
685                 raise
686             except (nodes.SkipChildren, nodes.SkipSiblings), instance:
687                 tree_pruning_exception = instance
688             except nodes.SkipDeparture:
689                 raise NotImplementedError(
690                     'SkipDeparture not usable in LaTeX writer')
691
692         if not isinstance(node, nodes.Text):
693             node_name = self.node_name(node)
694             # attribute_deleters will be appended to self.context.
695             attribute_deleters = []
696             if not skip_parent and not isinstance(node, nodes.document):
697                 self.append(r'\renewcommand{\Dparent}{%s}'
698                             % self.node_name(node.parent))
699                 for name, value in node.attlist():
700                     if not isinstance(value, ListType) and not ':' in name:
701                         macro = r'\DcurrentN%sA%s' % (node_name, name)
702                         self.append(r'\def%s{%s}' % (
703                             macro, self.encode(unicode(value), attval=name)))
704                         attribute_deleters.append(r'\let%s=\relax' % macro)
705             self.context.append('\n'.join(attribute_deleters))
706             if self.pass_contents(node):
707                 self.append(r'\DN%s{' % node_name)
708                 self.context.append('}')
709             else:
710                 self.append(r'\Dvisit%s' % node_name)
711                 self.context.append(r'\Ddepart%s' % node_name)
712             self.indentation_level += 1
713             if not skip_attr:
714                 self.propagate_attributes(node)
715             else:
716                 self.context.append('')
717
718         if (isinstance(node, nodes.TextElement) and
719             not isinstance(node.parent, nodes.TextElement)):
720             # Reset current quote to left.
721             self.left_quote = 1
722
723         # Call visit_... method.
724         try:
725             nodes.SparseNodeVisitor.dispatch_visit(self, node)
726         except LaTeXException:
727             raise NotImplementedError(
728                 'visit_... methods must not raise LaTeXExceptions')
729
730         if tree_pruning_exception:
731             # Propagate TreePruningException raised in before_... method.
732             raise tree_pruning_exception
733
734     def is_invisible(self, node):
735         # Return true if node is invisible or moved away in the LaTeX
736         # rendering.
737         return (not isinstance(node, nodes.Text) and
738                 (isinstance(node, nodes.Invisible) or
739                  isinstance(node, nodes.footnote) or
740                  isinstance(node, nodes.citation) or
741                  # Assume raw nodes to be invisible.
742                  isinstance(node, nodes.raw) or
743                  # Floating image or figure.
744                  node.get('align'in ('left''right')))
745
746     def is_visible(self, node):
747         return not self.is_invisible(node)
748
749     def needs_space(self, node):
750         """Two nodes for which `needs_space` is true need auxiliary space."""
751         # Return true if node is a visible block-level element.
752         return ((isinstance(node, nodes.Body) or
753                  isinstance(node, nodes.topic)) and
754                 not (self.is_invisible(node) or
755                      isinstance(node.parent, nodes.TextElement)))
756
757     def always_needs_space(self, node):
758         """
759         Always add space around nodes for which `always_needs_space()`
760         is true, regardless of whether the other node needs space as
761         well.  (E.g. transition next to section.)
762         """
763         return isinstance(node, nodes.transition)
764
765     def dispatch_departure(self, node):
766         # Call departure method.
767         nodes.SparseNodeVisitor.dispatch_departure(self, node)
768
769         if not isinstance(node, nodes.Text):
770             # Close attribute and node handler call (\DN...{...}).
771             self.indentation_level -= 1
772             self.append(self.context.pop() + self.context.pop())
773             # Delete \Dcurrent... attribute macros.
774             self.append(self.context.pop())
775             # Get next sibling.
776             next_node = node.next_node(
777                 ascend=0, siblings=1, descend=0,
778                 condition=self.is_visible)
779             # Insert space if necessary.
780             if  (self.needs_space(node) and self.needs_space(next_node) or
781                  self.always_needs_space(node) or
782                  self.always_needs_space(next_node)):
783                 if isinstance(node, nodes.paragraph) and isinstance(next_node, nodes.paragraph):
784                     # Space between paragraphs.
785                     self.append(r'\Dparagraphspace')
786                 else:
787                     # One of the elements is not a paragraph.
788                     self.append(r'\Dauxiliaryspace')