~ubuntu-branches/ubuntu/karmic/python-docutils/karmic

« back to all changes in this revision

Viewing changes to docutils/writers/newlatex2e/__init__.py

  • Committer: Bazaar Package Importer
  • Author(s): martin f. krafft
  • Date: 2006-07-10 11:45:05 UTC
  • mfrom: (2.1.4 edgy)
  • Revision ID: james.westby@ubuntu.com-20060710114505-otkhqcslevewxmz5
Tags: 0.4-3
Added build dependency on python-central (closes: #377580).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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.newlatex2e import 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')