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.
8
LaTeX2e document tree Writer.
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
15
__docformat__ = 'reStructuredText'
20
from types import ListType
23
from docutils import nodes, writers, utils
24
from docutils.writers.newlatex2e import unicode_map
25
from docutils.transforms import writer_aux
28
class Writer(writers.Writer):
30
supported = ('newlatex', 'newlatex2e')
31
"""Formats this writer supports."""
33
default_stylesheet = 'base.tex'
35
default_stylesheet_path = utils.relative_path(
36
os.path.join(os.getcwd(), 'dummy'),
37
os.path.join(os.path.dirname(__file__), default_stylesheet))
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.',
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'})
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:
78
relative_path_settings = ('stylesheet_path', 'user_stylesheet_path')
80
config_section = 'newlatex2e writer'
81
config_section_dependencies = ('writers',)
84
"""Final translated form of `document`."""
86
def get_transforms(self):
87
return writers.Writer.get_transforms(self) + [writer_aux.Compound]
90
writers.Writer.__init__(self)
91
self.translator_class = LaTeXTranslator
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
102
class LaTeXException(Exception):
104
Exception base class to for exceptions which influence the
105
automatic generation of LaTeX code.
109
class SkipAttrParentLaTeX(LaTeXException):
111
Do not generate ``\Dattr`` and ``\renewcommand{\Dparent}{...}`` for this
114
To be raised from ``before_...`` methods.
118
class SkipParentLaTeX(LaTeXException):
120
Do not generate ``\renewcommand{\DNparent}{...}`` for this node.
122
To be raised from ``before_...`` methods.
126
class LaTeXTranslator(nodes.SparseNodeVisitor):
128
# Country code by a.schlock.
129
# Partly manually converted from iso and babel stuff.
131
'no': 'norsk', # added by hand
132
'gd': 'scottish', # added by hand
142
# french, francais, canadien, acadian
144
# ngerman, naustrian, german, germanb, austrian
147
# english, USenglish, american, UKenglish, british, canadian
173
# Start with left double quote.
176
def __init__(self, document):
177
nodes.NodeVisitor.__init__(self, document)
178
self.settings = document.settings
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
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)
196
def write_header(self):
197
a = self.header.append
198
a('%% Generated by Docutils %s <http://docutils.sourceforge.net>.'
199
% docutils.__version__)
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], '')))
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)
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('_', ''))
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')
228
unicode_map = unicode_map.unicode_map # comprehensive Unicode map
229
# Fix problems with unimap.py.
231
# We have AE or T1 encoding, so "``" etc. work. The macros
232
# from unimap.py may *not* work.
239
'\\': r'{\textbackslash}',
255
'>': r'{\textgreater}',
256
'^': r'{\textasciicircum}',
257
'~': r'{\textasciitilde}',
258
'_': r'{\Dtextunderscore}',
260
character_map.update(unicode_map)
261
#character_map.update(special_map)
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:
268
att_map = {'#': '\\#',
270
# We cannot do anything about backslashes.
274
# The quotation mark may be redefined by babel.
277
att_map.update(unicode_map)
279
def encode(self, text, attval=None):
281
Encode special characters in ``text`` and return it.
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.
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()
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)
299
return '%s%s' % (value, unit)
301
get = self.att_map.get
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.
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
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}')
330
# Replace space with single protected space.
331
text = re.sub(r'\s+', '{ }', text)
332
# Replace double quotes with macro calls.
334
for part in text.split(self.character_map['"']):
337
L.append(self.left_quote and r'{\Dtextleftdblquote}'
338
or r'{\Dtextrightdblquote}')
339
self.left_quote = not self.left_quote
345
def encode_replace_for_literal_block_spaces(self, match):
346
return '~' * len(match.group())
348
def encode_replace_for_inline_literal_spaces(self, match):
349
return '{ }' + '~' * (len(match.group()) - 1)
352
return '\n'.join(self.header) + (''.join(self.body))
354
def append(self, text, newline='%\n'):
356
Append text, stripping newlines, producing nice LaTeX code.
358
lines = [' ' * self.indentation_level + line + newline
359
for line in text.splitlines(0)]
360
self.body.append(''.join(lines))
362
def visit_Text(self, node):
363
self.append(self.encode(node.astext()))
365
def depart_Text(self, node):
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)):
377
# Indent all but the first paragraphs.
380
def before_paragraph(self, node):
381
self.append(r'\renewcommand{\Dparagraphindented}{%s}'
382
% (self.is_indented(node) and 'true' or 'false'))
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'))
392
def before_generated(self, node):
393
if 'sectnum' in node['classes']:
394
node[0] = node[0].strip()
398
def visit_literal_block(self, node):
399
self.literal_block = 1
401
def depart_literal_block(self, node):
402
self.literal_block = 0
404
visit_doctest_block = visit_literal_block
405
depart_doctest_block = depart_literal_block
409
def visit_literal(self, node):
410
self.inline_literal += 1
412
def depart_literal(self, node):
413
self.inline_literal -= 1
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
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)
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']
435
bullet_list_level = 0
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
442
def depart_bullet_list(self, node):
443
self.bullet_list_level -= 1
445
enum_styles = {'arabic': 'arabic', 'loweralpha': 'alph', 'upperalpha':
446
'Alph', 'lowerroman': 'roman', 'upperroman': 'Roman'}
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))
464
def depart_enumerated_list(self, node):
465
self.append('}') # for Emacs: {
467
def before_list_item(self, node):
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'
474
before_line = before_list_item
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
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:
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){ }}'
494
elif len(node['backrefs']) == 1:
495
self.append(r'\renewcommand{\Dsinglebackref}{%s}'
496
% node['backrefs'][0])
498
def visit_footnote(self, node):
499
self.process_backlinks(node, 'footnote')
501
def visit_citation(self, node):
502
self.process_backlinks(node, 'citation')
504
def before_table(self, node):
505
# A table contains exactly one tgroup. See before_tgroup.
508
def before_tgroup(self, node):
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)]
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
526
def depart_tgroup(self, node):
527
self.append(self.context.pop())
529
def before_row(self, node):
530
raise SkipAttrParentLaTeX
532
def before_thead(self, node):
533
raise SkipAttrParentLaTeX
535
def before_tbody(self, node):
536
raise SkipAttrParentLaTeX
538
def is_simply_entry(self, node):
539
return (len(node) == 1 and isinstance(node[0], nodes.paragraph) or
542
def before_entry(self, node):
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
561
macro_name = r'\Dcolspan'
562
if node.parent.index(node) == 0:
567
self.append('%s{%s}{' % (macro_name, colspan))
568
self.context.append('}')
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.
575
self.append(r'\Dsubsequententry{')
576
self.context.append('}')
578
self.context.append('')
579
if isinstance(node.parent.parent, nodes.thead):
580
node['tableheaderentry'] = 'true'
582
# Don't add \renewcommand{\Dparent}{...} because there must
583
# not be any non-expandable commands in front of \multicolumn.
584
raise SkipParentLaTeX
586
def depart_entry(self, node):
587
self.append(self.context.pop())
589
def before_substitution_definition(self, node):
592
indentation_level = 0
594
def node_name(self, node):
595
return node.__class__.__name__.replace('_', '')
597
# Attribute propagation order.
598
attribute_order = ['align', 'classes', 'ids']
600
def attribute_cmp(self, a1, a2):
602
Compare attribute names `a1` and `a2`. Used in
603
propagate_attributes to determine propagation order.
605
See built-in function `cmp` for return value.
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
616
def propagate_attributes(self, node):
617
# Propagate attributes using \Dattr macros.
618
node_name = self.node_name(node)
620
if isinstance(node, nodes.Element):
621
attlist = node.attlist()
622
attlist.sort(lambda pair1, pair2: self.attribute_cmp(pair1[0],
624
# `numatts` may be greater than len(attlist) due to list
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),
635
if not pass_contents:
637
numatts += len(value)
639
self.append(r'\Dattr{}{%s}{%s}{%s}{' %
640
(key, self.encode(unicode(value), attval=key),
642
if not pass_contents:
646
self.context.append('}' * numatts) # for Emacs: {
648
self.context.append('')
650
def visit_docinfo(self, node):
651
raise NotImplementedError('Docinfo not yet implemented.')
653
def visit_document(self, 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,
660
next_text_element = node.next_node(nodes.TextElement)
661
if next_text_element:
662
next_text_element['ids'].extend(node['ids'])
665
def pass_contents(self, node):
667
Return true if the node contents should be passed in
668
parameters of \DN... and \Dattr.
670
return not isinstance(node, (nodes.document, nodes.section))
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__):
678
getattr(self, 'before_' + node.__class__.__name__)(node)
679
except SkipParentLaTeX:
681
except SkipAttrParentLaTeX:
684
except nodes.SkipNode:
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')
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('}')
710
self.append(r'\Dvisit%s' % node_name)
711
self.context.append(r'\Ddepart%s' % node_name)
712
self.indentation_level += 1
714
self.propagate_attributes(node)
716
self.context.append('')
718
if (isinstance(node, nodes.TextElement) and
719
not isinstance(node.parent, nodes.TextElement)):
720
# Reset current quote to left.
723
# Call visit_... method.
725
nodes.SparseNodeVisitor.dispatch_visit(self, node)
726
except LaTeXException:
727
raise NotImplementedError(
728
'visit_... methods must not raise LaTeXExceptions')
730
if tree_pruning_exception:
731
# Propagate TreePruningException raised in before_... method.
732
raise tree_pruning_exception
734
def is_invisible(self, node):
735
# Return true if node is invisible or moved away in the LaTeX
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')))
746
def is_visible(self, node):
747
return not self.is_invisible(node)
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)))
757
def always_needs_space(self, node):
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.)
763
return isinstance(node, nodes.transition)
765
def dispatch_departure(self, node):
766
# Call departure method.
767
nodes.SparseNodeVisitor.dispatch_departure(self, node)
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())
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')
787
# One of the elements is not a paragraph.
788
self.append(r'\Dauxiliaryspace')