3 :Contact: Felix_Wiemann@ososo.de
6 :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
26 class Writer(writers
.Writer
):
28 supported
= ('newlatex', 'newlatex2e')
29 """Formats this writer supports."""
32 'LaTeX-Specific Options',
33 'The LaTeX "--output-encoding" default is "latin-1:strict". '
34 'Note that this LaTeX writer is still EXPERIMENTAL.',
35 (('Specify a stylesheet file. The path is used verbatim to include '
36 'the file. Overrides --stylesheet-path.',
38 {'default': '', 'metavar': '<file>',
39 'overrides': 'stylesheet_path'}),
40 ('Specify a stylesheet file, relative to the current working '
41 'directory. Overrides --stylesheet.',
42 ['--stylesheet-path'],
43 {'metavar': '<file>', 'overrides': 'stylesheet'}),
44 ('Specify a uesr stylesheet file. See --stylesheet.',
45 ['--user-stylesheet'],
46 {'default': '', 'metavar': '<file>',
47 'overrides': 'user_stylesheet_path'}),
48 ('Specify a user stylesheet file. See --stylesheet-path.',
49 ['--user-stylesheet-path'],
50 {'metavar': '<file>', 'overrides': 'user_stylesheet'})
53 settings_defaults
= {'output_encoding': 'latin-1',
54 'trim_footnote_reference_space': 1,
55 # Currently unsupported:
60 relative_path_settings
= ('stylesheet_path',)
62 config_section
= 'newlatex2e writer'
63 config_section_dependencies
= ('writers',)
66 """Final translated form of `document`."""
69 writers
.Writer
.__init
__(self
)
70 self
.translator_class
= LaTeXTranslator
73 visitor
= self
.translator_class(self
.document
)
74 self
.document
.walkabout(visitor
)
75 self
.output
= visitor
.astext()
76 self
.head
= visitor
.header
77 self
.body
= visitor
.body
81 """Language specifics for LaTeX."""
82 # country code by a.schlock.
83 # partly manually converted from iso and babel stuff, dialects and some
85 'no': 'norsk', # added by hand ( forget about nynorsk?)
86 'gd': 'scottish', # added by hand
87 'hu': 'magyar', # added by hand
88 'pt': 'portuguese',# added by hand
98 # french, francais, canadien, acadian
99 'de': 'ngerman', # rather than german
100 # ngerman, naustrian, german, germanb, austrian
103 # english, USenglish, american, UKenglish, british, canadian
129 def __init__(self
, lang
):
132 def get_language(self
):
133 if self
._ISO
639_TO
_BABEL
.has_key(self
.language
):
134 return self
._ISO
639_TO
_BABEL
[self
.language
]
137 l
= self
.language
.split("_")[0]
138 if self
._ISO
639_TO
_BABEL
.has_key(l
):
139 return self
._ISO
639_TO
_BABEL
[l
]
143 class LaTeXException(Exception):
145 Exception base class to for exceptions which influence the
146 automatic generation of LaTeX code.
150 class SkipAttrParentLaTeX(LaTeXException
):
152 Do not generate \Dattr and \renewcommand{\Dparent}{...} for this
155 To be raised from before_... methods.
159 class SkipParentLaTeX(LaTeXException
):
161 Do not generate \renewcommand{\DNparent}{...} for this node.
163 To be raised from before_... methods.
167 class LaTeXTranslator(nodes
.SparseNodeVisitor
):
169 # Start with left double quote.
172 def __init__(self
, document
):
173 nodes
.NodeVisitor
.__init
__(self
, document
)
174 self
.settings
= document
.settings
178 self
.stylesheet_path
= utils
.get_stylesheet_reference(
179 self
.settings
, os
.path
.join(os
.getcwd(), 'dummy'))
180 if self
.stylesheet_path
:
181 self
.settings
.record_dependencies
.add(self
.stylesheet_path
)
182 # This ugly hack will be cleaned up when refactoring the
184 self
.settings
.stylesheet
= self
.settings
.user_stylesheet
185 self
.settings
.stylesheet_path
= self
.settings
.user_stylesheet_path
186 self
.user_stylesheet_path
= utils
.get_stylesheet_reference(
187 self
.settings
, os
.path
.join(os
.getcwd(), 'dummy'))
188 if self
.user_stylesheet_path
:
189 self
.settings
.record_dependencies
.add(self
.user_stylesheet_path
)
191 for key
, value
in self
.character_map
.items():
192 self
.character_map
[key
] = '{%s}' % value
194 def write_header(self
):
195 a
= self
.header
.append
196 a('%% Generated by Docutils %s <http://docutils.sourceforge.net>.\n'
197 % docutils
.__version
__)
198 if self
.user_stylesheet_path
:
199 a('% User stylesheet:')
200 a(r
'\input{%s}' % self
.user_stylesheet_path
)
201 a('% Docutils stylesheet:')
202 a(r
'\input{%s}' % self
.stylesheet_path
)
204 a('% Definitions for Docutils Nodes:')
205 for node_name
in nodes
.node_class_names
:
206 a(r
'\providecommand{\DN%s}[1]{#1}' % node_name
.replace('_', ''))
208 a('% Auxiliary definitions:')
209 a(r
'\providecommand{\Dsetattr}[2]{}')
210 a(r
'\providecommand{\Dparent}{} % variable')
211 a(r
'\providecommand{\Dattr}[5]{#5}')
212 a(r
'\providecommand{\Dattrlen}{} % variable')
213 a(r
'\providecommand{\Dtitleastext}{x}')
214 a(r
'\providecommand{\Dsinglebackref}{} % variable')
215 a(r
'\providecommand{\Dmultiplebackrefs}{} % variable')
218 def to_latex_encoding(self
,docutils_encoding
):
220 Translate docutils encoding name into latex's.
222 Default fallback method is remove "-" and "_" chars from
225 tr
= { "iso-8859-1": "latin1", # west european
226 "iso-8859-2": "latin2", # east european
227 "iso-8859-3": "latin3", # esperanto, maltese
228 "iso-8859-4": "latin4", # north european,scandinavian, baltic
229 "iso-8859-5": "iso88595", # cyrillic (ISO)
230 "iso-8859-9": "latin5", # turkish
231 "iso-8859-15": "latin9", # latin9, update to latin1.
232 "mac_cyrillic": "maccyr", # cyrillic (on Mac)
233 "windows-1251": "cp1251", # cyrillic (on Windows)
234 "koi8-r": "koi8-r", # cyrillic (Russian)
235 "koi8-u": "koi8-u", # cyrillic (Ukrainian)
236 "windows-1250": "cp1250", #
237 "windows-1252": "cp1252", #
238 "us-ascii": "ascii", # ASCII (US)
239 # unmatched encodings
241 #"": "ansinew", # windows 3.1 ansi
242 #"": "ascii", # ASCII encoding for the range 32--127.
243 #"": "cp437", # dos latine us
244 #"": "cp850", # dos latin 1
245 #"": "cp852", # dos latin 2
248 #"iso-8859-6": "" # arabic
249 #"iso-8859-7": "" # greek
250 #"iso-8859-8": "" # hebrew
251 #"iso-8859-10": "" # latin6, more complete iso-8859-4
253 if tr
.has_key(docutils_encoding
.lower()):
254 return tr
[docutils_encoding
.lower()]
255 return docutils_encoding
.translate(string
.maketrans("",""),"_-").lower()
257 def language_label(self
, docutil_label
):
258 return self
.language
.labels
[docutil_label
]
260 # To do: Use unimap.py from TeXML instead. Have to deal with
261 # legal cruft before, because it's LPGL.
262 character_map_string
= r
"""
285 #special_map = {'\n': ' ', '\r': ' ', '\t': ' ', '\v': ' ', '\f': ' '}
298 u
'\u2020': '{\\dag}',
299 u
'\u2021': '{\\ddag}',
300 u
'\u2026': '{\\dots}',
301 u
'\u2122': '{\\texttrademark}',
302 u
'\u21d4': '{$\\Leftrightarrow$}',
306 for pair
in character_map_string
.strip().split('\n'):
307 char
, replacement
= pair
.split()
308 character_map
[char
] = replacement
309 character_map
.update(unicode_map
)
310 #character_map.update(special_map)
312 def encode(self
, text
, attval
=0):
314 Encode special characters in ``text`` and return it.
316 If attval is true, preserve as much as possible verbatim (used in
317 attribute value encoding).
320 get
= self
.character_map
.get
323 # <http://www-h.eng.cam.ac.uk/help/tpl/textprocessing/teTeX/latex/latex2e-html/ltx-164.html>,
324 # the following characters are special: # $ % & ~ _ ^ \ { }
325 # These work without special treatment in macro parameters:
329 # We cannot do anything about backslashes.
333 text
= ''.join([get(c
, c
) for c
in text
])
334 if (self
.literal_block
or self
.inline_literal
) and not attval
:
335 # NB: We can have inline literals within literal blocks.
337 text
= text
.replace('\r\n', '\n')
338 # Convert space. If "{ }~~~~~" is wrapped (at the
339 # brace-enclosed space "{ }"), the following non-breaking
340 # spaces ("~~~~") do *not* wind up at the beginning of the
341 # next line. Also note that, for some not-so-obvious
342 # reason, no hyphenation is done if the breaking space ("{
343 # }") comes *after* the non-breaking spaces.
344 if self
.literal_block
:
345 # Replace newlines with real newlines.
346 text
= text
.replace('\n', '\mbox{}\\\\')
350 text
= re
.sub(r
'\s+', lambda m
: firstspace
+
351 '~' * (len(m
.group()) - 1), text
)
352 # Protect hyphens; if we don't, line breaks will be
353 # possible at the hyphens and even the \textnhtt macro
354 # from the hyphenat package won't change that.
355 text
= text
.replace('-', r
'\mbox{-}')
356 text
= text
.replace("'", r
'{\Dtextliteralsinglequote}')
360 # Replace space with single protected space.
361 text
= re
.sub(r
'\s+', '{ }', text
)
362 # Replace double quotes with macro calls.
364 for part
in text
.split('"'):
367 L
.append(self
.left_quote
and r
'\Dtextleftdblquote' or
368 r
'\Dtextrightdblquote')
369 self
.left_quote
= not self
.left_quote
376 return '\n'.join(self
.header
) + (''.join(self
.body
))
378 def append(self
, text
, newline
='%\n'):
380 Append text, stripping newlines, producing nice LaTeX code.
382 lines
= [' ' * self
.indentation_level
+ line
+ newline
383 for line
in text
.splitlines(0)]
384 self
.body
.append(''.join(lines
))
386 def visit_Text(self
, node
):
387 self
.append(self
.encode(node
.astext()))
389 def depart_Text(self
, node
):
392 def before_title(self
, node
):
393 self
.append(r
'\renewcommand{\Dtitleastext}{%s}'
394 % self
.encode(node
.astext()))
395 self
.append(r
'\renewcommand{\Dhassubtitle}{%s}'
396 % ((len(node
.parent
) > 2 and
397 isinstance(node
.parent
[1], nodes
.subtitle
))
398 and 'true' or 'false'))
402 def visit_literal_block(self
, node
):
403 self
.literal_block
= 1
405 def depart_literal_block(self
, node
):
406 self
.literal_block
= 0
408 visit_doctest_block
= visit_literal_block
409 depart_doctest_block
= depart_literal_block
413 def visit_literal(self
, node
):
414 self
.inline_literal
+= 1
416 def depart_literal(self
, node
):
417 self
.inline_literal
-= 1
419 def visit_comment(self
, node
):
420 self
.append('\n'.join(['% ' + line
for line
421 in node
.astext().splitlines(0)]), newline
='\n')
422 raise nodes
.SkipChildren
424 bullet_list_level
= 0
426 def visit_bullet_list(self
, node
):
427 self
.append(r
'\Dsetbullet{\labelitem%s}' %
428 ['i', 'ii', 'iii', 'iv'][min(self
.bullet_list_level
, 3)])
429 self
.bullet_list_level
+= 1
431 def depart_bullet_list(self
, node
):
432 self
.bullet_list_level
-= 1
434 enum_styles
= {'arabic': 'arabic', 'loweralpha': 'alph', 'upperalpha':
435 'Alph', 'lowerroman': 'roman', 'upperroman': 'Roman'}
439 def visit_enumerated_list(self
, node
):
440 # We create our own enumeration list environment. This allows
441 # to set the style and starting value and unlimited nesting.
442 # Maybe this can be moved to the stylesheet?
443 self
.enum_counter
+= 1
444 enum_prefix
= self
.encode(node
['prefix'])
445 enum_suffix
= self
.encode(node
['suffix'])
446 enum_type
= '\\' + self
.enum_styles
.get(node
['enumtype'], r
'arabic')
447 start
= node
.get('start', 1) - 1
448 counter
= 'Denumcounter%d' % self
.enum_counter
449 self
.append(r
'\Dmakeenumeratedlist{%s}{%s}{%s}{%s}{%s}{'
450 % (enum_prefix
, enum_type
, enum_suffix
, counter
, start
))
453 def depart_enumerated_list(self
, node
):
454 self
.append('}') # for Emacs: {
456 def before_list_item(self
, node
):
458 if (len(node
) and (isinstance(node
[-1], nodes
.TextElement
) or
459 isinstance(node
[-1], nodes
.Text
)) and
460 node
.parent
.index(node
) == len(node
.parent
) - 1):
461 node
['lastitem'] = 'true'
463 before_line
= before_list_item
465 def visit_raw(self
, node
):
466 if 'latex' in node
.get('format', '').split():
467 self
.append(node
.astext())
468 raise nodes
.SkipChildren
470 def process_backlinks(self
, node
, type):
471 self
.append(r
'\renewcommand{\Dsinglebackref}{}')
472 self
.append(r
'\renewcommand{\Dmultiplebackrefs}{}')
473 if len(node
['backrefs']) > 1:
475 for i
in range(len(node
['backrefs'])):
476 refs
.append(r
'\Dmulti%sbacklink{%s}{%s}'
477 % (type, node
['backrefs'][i
], i
+ 1))
478 self
.append(r
'\renewcommand{\Dmultiplebackrefs}{(%s){ }}'
480 elif len(node
['backrefs']) == 1:
481 self
.append(r
'\renewcommand{\Dsinglebackref}{%s}'
482 % node
['backrefs'][0])
484 def visit_footnote(self
, node
):
485 self
.process_backlinks(node
, 'footnote')
487 def visit_citation(self
, node
):
488 self
.process_backlinks(node
, 'citation')
490 def visit_table(self
, node
):
491 # Everything's handled in tgroup.
494 def before_table(self
, node
):
495 # A tables contains exactly one tgroup. See before_tgroup.
498 def before_tgroup(self
, node
):
501 for i
in range(int(node
['cols'])):
502 assert isinstance(node
[i
], nodes
.colspec
)
503 widths
.append(int(node
[i
]['colwidth']) + 1)
504 total_width
+= widths
[-1]
505 del node
[:len(widths
)]
508 tablespec
+= r
'p{%s\linewidth}|' % (0.93 * w
/
509 max(total_width
, 60))
510 self
.append(r
'\Dmaketable{%s}{' % tablespec
)
511 self
.context
.append('}')
512 raise SkipAttrParentLaTeX
514 def depart_tgroup(self
, node
):
515 self
.append(self
.context
.pop())
517 def before_row(self
, node
):
518 raise SkipAttrParentLaTeX
520 def before_thead(self
, node
):
521 raise SkipAttrParentLaTeX
523 def before_tbody(self
, node
):
524 raise SkipAttrParentLaTeX
526 def is_simply_entry(self
, node
):
527 return (len(node
) == 1 and isinstance(node
[0], nodes
.paragraph
) or
530 def before_entry(self
, node
):
532 if node
.hasattr('morerows'):
533 self
.document
.reporter
.severe('Rowspans are not supported.')
534 # Todo: Add empty cells below rowspanning cell and issue
535 # warning instead of severe.
536 if node
.hasattr('morecols'):
537 # The author got a headache trying to implement
538 # multicolumn support.
539 if not self
.is_simply_entry(node
):
540 self
.document
.reporter
.severe(
541 'Colspanning table cells may only contain one paragraph.')
542 # Todo: Same as above.
543 # The number of columns this entry spans (as a string).
544 colspan
= int(node
['morecols']) + 1
549 macro_name
= r
'\Dcolspan'
550 if node
.parent
.index(node
) == 0:
555 self
.append('%s{%s}{' % (macro_name
, colspan
))
556 self
.context
.append('}')
558 # Do not add a multicolumn with colspan 1 beacuse we need
559 # at least one non-multicolumn cell per column to get the
560 # desired column widths, and we can only do colspans with
561 # cells consisting of only one paragraph.
563 self
.append(r
'\Dsubsequententry{')
564 self
.context
.append('}')
566 self
.context
.append('')
567 if isinstance(node
.parent
.parent
, nodes
.thead
):
568 node
['tableheaderentry'] = 'true'
570 # Don't add \renewcommand{\Dparent}{...} because there may not
571 # be any non-expandable commands in front of \multicolumn.
572 raise SkipParentLaTeX
574 def depart_entry(self
, node
):
575 self
.append(self
.context
.pop())
577 def before_substitution_definition(self
, node
):
580 indentation_level
= 0
582 def node_name(self
, node
):
583 return node
.__class
__.__name
__.replace('_', '')
585 def propagate_attributes(self
, node
):
586 # Propagate attributes using \Dattr macros.
587 node_name
= self
.node_name(node
)
589 if isinstance(node
, nodes
.Element
):
590 attlist
= node
.attlist()
592 pass_contents
= self
.pass_contents(node
)
593 for key
, value
in attlist
:
594 if isinstance(value
, ListType
):
595 self
.append(r
'\renewcommand{\Dattrlen}{%s}' % len(value
))
596 for i
in range(len(value
)):
597 self
.append(r
'\Dattr{%s}{%s}{%s}{%s}{' %
598 (i
+1, key
, self
.encode(value
[i
], attval
=1),
600 if not pass_contents
:
602 numatts
+= len(value
)
604 self
.append(r
'\Dattr{}{%s}{%s}{%s}{' %
605 (key
, self
.encode(unicode(value
), attval
=1),
607 if not pass_contents
:
611 self
.context
.append('}' * numatts
) # for Emacs: {
613 self
.context
.append('')
615 def visit_docinfo(self
, node
):
616 raise NotImplementedError('Docinfo not yet implemented.')
618 def visit_document(self
, node
):
620 # Move IDs into TextElements. This won't work for images.
621 # Need to review this.
622 for node
in document
.traverse(lambda n
: isinstance(n
, nodes
.Element
)):
623 if node
.has_key('ids') and not isinstance(node
,
625 next_text_element
= node
.next_node(
626 lambda n
: isinstance(n
, nodes
.TextElement
))
627 if next_text_element
:
628 next_text_element
['ids'].extend(node
['ids'])
631 def pass_contents(self
, node
):
633 Return true if the node contents should be passed in
634 parameters of \DN... and \Dattr.
636 return not isinstance(node
, (nodes
.document
, nodes
.section
))
638 def dispatch_visit(self
, node
):
639 skip_attr
= skip_parent
= 0
640 # TreePruningException to be propagated.
641 tree_pruning_exception
= None
642 if hasattr(self
, 'before_' + node
.__class
__.__name
__):
644 getattr(self
, 'before_' + node
.__class
__.__name
__)(node
)
645 except SkipParentLaTeX
:
647 except SkipAttrParentLaTeX
:
650 except nodes
.SkipNode
:
652 except (nodes
.SkipChildren
, nodes
.SkipSiblings
), instance
:
653 tree_pruning_exception
= instance
654 except nodes
.SkipDeparture
:
655 raise NotImplementedError(
656 'SkipDeparture not usable in LaTeX writer')
658 if not isinstance(node
, nodes
.Text
):
659 node_name
= self
.node_name(node
)
660 if not skip_parent
and not isinstance(node
, nodes
.document
):
661 self
.append(r
'\renewcommand{\Dparent}{%s}'
662 % self
.node_name(node
.parent
))
663 if self
.pass_contents(node
):
664 self
.append(r
'\DN%s{' % node_name
)
665 self
.context
.append('}')
667 self
.append(r
'\Dvisit%s' % node_name
)
668 self
.context
.append(r
'\Ddepart%s' % node_name
)
669 self
.indentation_level
+= 1
671 self
.propagate_attributes(node
)
673 self
.context
.append('')
675 if (isinstance(node
, nodes
.TextElement
) and
676 not isinstance(node
.parent
, nodes
.TextElement
)):
677 # Reset current quote to left.
680 # Call visit_... method.
682 nodes
.SparseNodeVisitor
.dispatch_visit(self
, node
)
683 except LaTeXException
:
684 raise NotImplementedError(
685 'visit_... methods must not raise LaTeXExceptions')
687 if tree_pruning_exception
:
688 # Propagate TreePruningException raised in before_... method.
689 raise tree_pruning_exception
691 def is_invisible(self
, node
):
692 # Return true if node is invisible or moved away in the LaTeX
694 return (isinstance(node
, nodes
.Invisible
) or
695 isinstance(node
, nodes
.footnote
) or
696 isinstance(node
, nodes
.citation
))
698 def needs_space(self
, node
):
699 # Return true if node is a visible block-level element.
700 return ((isinstance(node
, nodes
.Body
) or
701 isinstance(node
, nodes
.topic
) or
702 #isinstance(node, nodes.rubric) or
703 isinstance(node
, nodes
.transition
) or
704 isinstance(node
, nodes
.caption
) or
705 isinstance(node
, nodes
.legend
)) and
706 not (self
.is_invisible(node
) or
707 isinstance(node
.parent
, nodes
.TextElement
)))
709 def dispatch_departure(self
, node
):
710 # Call departure method.
711 nodes
.SparseNodeVisitor
.dispatch_departure(self
, node
)
713 if not isinstance(node
, nodes
.Text
):
714 # Close attribute and node handler call (\DN...{...}).
715 self
.indentation_level
-= 1
716 self
.append(self
.context
.pop() + self
.context
.pop())
719 if self
.needs_space(node
):
721 next_node
= node
.next_node(
722 ascend
=0, siblings
=1, descend
=0,
723 condition
=lambda n
: not self
.is_invisible(n
))
724 if self
.needs_space(next_node
):
726 if isinstance(next_node
, nodes
.paragraph
):
727 if isinstance(node
, nodes
.paragraph
):
728 # Space between paragraphs.
729 self
.append(r
'\Dparagraphspace')
731 # Space in front of a paragraph.
732 self
.append(r
'\Dauxiliaryparspace')
734 # Space in front of something else than a paragraph.
735 self
.append(r
'\Dauxiliaryspace')