Add <target> to one more testcase (see r8206).
[docutils.git] / sandbox / rst2beamer / rst2beamer.py
blob77907b28ab9d4141515a6f079bbb6aa60d4bcca6
1 #!/usr/bin/env python
2 # encoding: utf-8
3 """
4 A docutils script converting restructured text into Beamer-flavoured LaTeX.
6 Beamer is a LaTeX document class for presentations. Via this script, ReST can
7 be used to prepare slides. It can be called::
9 rst2beamer.py infile.txt > outfile.tex
11 where ``infile.txt`` contains the rst and ``outfile.tex`` contains the
12 Beamer-flavoured LaTeX.
14 See <http:www.agapow.net/software/rst2beamer> for more details.
16 """
17 # TODO: modifications for handout sections?
18 # TOOD: sections and subsections?
19 # TODO: convert document metadata to front page fields?
20 # TODO: toc-conversion?
21 # TODO: fix descriptions
22 # TODO: 'r2b' or 'beamer' as identifying prefix?
25 # This file has been modified by Ryan Krauss starting on 2009-03-25.
26 # Please contact him if it is broken: ryanwkrauss@gmail.com
28 __docformat__ = 'restructuredtext en'
29 __author__ = "Ryan Krauss <ryanwkrauss@gmail.com> & Paul-Michael Agapow <agapow@bbsrc.ac.uk>"
30 __version__ = "0.6.6"
33 ### IMPORTS ###
35 import re
36 import pdb
38 try:
39 locale.setlocale (locale.LC_ALL, '')
40 except:
41 pass
43 from docutils.core import publish_cmdline, default_description
44 from docutils.writers.latex2e import Writer as Latex2eWriter
45 from docutils.writers.latex2e import LaTeXTranslator, DocumentClass
46 from docutils import nodes
47 from docutils.nodes import fully_normalize_name as normalize_name
48 from docutils.parsers.rst import directives, Directive
49 from docutils import frontend
50 from docutils.writers.latex2e import PreambleCmds
51 ## CONSTANTS & DEFINES ###
53 SHOWNOTES_FALSE = 'false'
54 SHOWNOTES_TRUE = 'true'
55 SHOWNOTES_ONLY = 'only'
56 SHOWNOTES_LEFT = 'left'
57 SHOWNOTES_RIGHT = 'right'
58 SHOWNOTES_TOP = 'top'
59 SHOWNOTES_BOTTOM = 'bottom'
61 SHOWNOTES_OPTIONS = [
62 SHOWNOTES_FALSE,
63 SHOWNOTES_TRUE,
64 SHOWNOTES_ONLY,
65 SHOWNOTES_LEFT,
66 SHOWNOTES_RIGHT,
67 SHOWNOTES_TOP,
68 SHOWNOTES_BOTTOM,
71 HILITE_OPTIONS = {
72 'python': 'python',
73 'guess': 'guess',
74 'c++': 'cpp',
77 BEAMER_SPEC = (
78 'Beamer options',
79 'These are derived almost entirely from the LaTeX2e options',
80 tuple (
83 'Specify theme.',
84 ['--theme'],
85 {'default': 'Warsaw', }
88 'Overlay bulleted items. Put [<+-| alert@+>] at the end of '
89 '\\begin{itemize} so that Beamer creats an overlay for each '
90 'bulleted item and the presentation reveals one bullet at a time',
91 ['--overlaybullets'],
92 {'default': True, }
95 'Default for whether or not to pass the fragile option to '
96 'the beamber frames (slides).',
97 ['--fragile-default'],
98 {'default': True, }
102 'Center figures. All includegraphics statements will be put '
103 'inside center environments.',
104 ['--centerfigs'],
105 {'default': True, }
108 # TODO: this doesn't seem to do anything ...
109 'Specify document options. Multiple options can be given, '
110 'separated by commas. Default is "10pt,a4paper".',
111 ['--documentoptions'],
112 {'default': '', }
114 ## (
115 ## 'Attach author and date to the document title.',
116 ## ['--use-latex-docinfo'],
117 ## {'default': 1, 'action': 'store_true',
118 ## 'validator': frontend.validate_boolean}
119 ## ),
121 "Print embedded notes along with the slides. Possible "
122 "arguments include 'false' (don't show), 'only' (show "
123 "only notes), 'left', 'right', 'top', 'bottom' (show in "
124 "relation to the annotated slide).",
125 ['--shownotes'],
127 'action': "store",
128 'type': 'choice',
129 'dest': 'shownotes',
130 'choices': SHOWNOTES_OPTIONS,
131 'default': SHOWNOTES_FALSE,
134 # should the pygments highlighter be used for codeblocks?
136 "Use the Pygments syntax highlighter to color blocks of "
137 "code. Otherwise, they will be typeset as simple literal "
138 "text. Obviously Pygments must be installed or an error. "
139 "will be raised. ",
140 ['--codeblocks-use-pygments'],
142 'action': "store_true",
143 'dest': 'cb_use_pygments',
144 'default': False,
147 # replace tabs inside codeblocks?
149 "Replace the leading tabs in codeblocks with spaces.",
150 ['--codeblocks-replace-tabs'],
152 'action': 'store',
153 'type': int,
154 'dest': 'cb_replace_tabs',
155 'default': 0,
158 # what language the codeblock is if not specified
160 "The default language to hilight code blocks as. ",
161 ['--codeblocks-default-language'],
163 'action': 'store',
164 'type': 'choice',
165 'dest': 'cb_default_lang',
166 'choices': HILITE_OPTIONS.values(),
167 'default': 'guess',
170 ] + list (Latex2eWriter.settings_spec[2][2:])
174 BEAMER_DEFAULTS = {
175 'use_latex_toc': True,
176 'output_encoding': 'latin-1',
177 'documentclass': 'beamer',
178 'documentoptions': 't',#text is at the top of each slide rather than centered. Changing to 'c' centers the text on each slide (vertically)
181 BEAMER_DEFAULT_OVERRIDES = {'use_latex_docinfo': 1}
184 bool_strs = ['false','true','0','1']
185 bool_vals = [False, True, False, True]
186 bool_dict = dict (zip (bool_strs, bool_vals))
188 PreambleCmds.documenttitle = r"""
189 %% Document title
190 \title[%s]{%s}
191 \author[%s]{%s}
192 \date{%s}
193 \maketitle
196 docinfo_w_institute = r"""
197 %% Document title
198 \title[%s]{%s}
199 \author[%s]{%s}
200 \date{%s}
201 \institute{%s}
202 \maketitle
205 ### IMPLEMENTATION ###
207 ### UTILS
209 LEADING_SPACE_RE = re.compile ('^ +')
211 def adjust_indent_spaces (strn, orig_width=8, new_width=3):
213 Adjust the leading space on a string so as to change the indent width.
215 :Parameters:
216 strn
217 The source string to change.
218 orig_width
219 The expected width for an indent in the source string.
220 new_width
221 The new width to make an ident.
223 :Returns:
224 The original string re-indented.
226 That takes strings that may be indented by a set number of spaces (or its
227 multiple) and adjusts the indent for a new number of spaces. So if the
228 expected indent width is 8 and the desired ident width is 3, a string has
229 been indented by 16 spaces, will be changed to have a indent of 6.
231 For example::
233 >>> adjust_indent_spaces (' foo')
234 ' foo'
235 >>> adjust_indent_spaces (' foo', orig_width=2, new_width=1)
236 ' foo'
238 This is useful where meaningful indent must be preserved (i.e. passed
239 through) ReST, especially tabs when used in the literal environments. ReST
240 transforms tabs-as-indents to 8 spaces, which leads to excessively spaced
241 out text. This function can be used to adjust the indent step to a
242 reasonable size.
244 .. note::
246 Excess spaces (those above and beyond a multiple of the original
247 indent width) will be preserved. Only indenting spaces will be
248 handled. Mixing tabs and spaces is - as always - a bad idea.
251 ## Preconditions & preparation:
252 assert (1 <= orig_width)
253 assert (0 <= new_width)
254 if (orig_width == new_width):
255 return strn
256 ## Main:
257 match = LEADING_SPACE_RE.match (strn)
258 if (match):
259 indent_len = match.end() - match.start()
260 indent_cnt = indent_len / orig_width
261 indent_depth = indent_cnt * orig_width
262 strn = ' ' * indent_cnt * new_width + strn[indent_depth:]
263 return strn
266 def index (seq, f, fail=None):
268 Return the index of the first item in seq where f(item) is True.
270 :Parameters:
272 A sequence or iterable
274 A boolean function an element of `seq`, e.g. `lambda x: x==4`
275 fail
276 The value to return if no item is found in seq.
278 While this could be written in a neater fashion in Python 2.6, this method
279 maintains compatiability with earlier version.
281 for index in (i for i in xrange (len (seq)) if f (seq[i])):
282 return index
283 return fail
286 def node_has_class (node, classes):
288 Does the node have one of these classes?
290 :Parameters:
291 node
292 A docutils node
293 class
294 A class name or list of class names.
296 :Returns:
297 A boolean indicating membership.
299 A convenience function, largely for testing for the special class names
300 in containers.
302 ## Preconditions & preparation:
303 # wrap single name in list
304 if (not (issubclass (type (classes), list))):
305 classes = [classes]
306 ## Main:
307 for cname in classes:
308 if cname in node['classes']:
309 return True
310 return False
313 def node_lang_class (node):
315 Extract a language specification from a node class names.
317 :Parameters:
318 node
319 A docutils node
321 :Returns:
322 A string giving a language abbreviation (e.g. 'py') or None if no
323 langauge is found.
325 Some sourcecode containers can pass a (programming) language specification
326 by passing it via a classname like 'lang-py'. This function searches a
327 nodes classnames for those starting with 'lang-' and returns the trailing
328 portion. Note that if more than one classname matches, only the first is
329 seen.
331 ## Main:
332 for cname in node['classes']:
333 if (cname.startswith ('lang-')):
334 return cname[5:]
335 return None
338 def wrap_children_in_columns (par_node, children, width=None):
340 Replace this node's children with columns containing the passed children.
342 :Parameters:
343 par_node
344 The node whose children are to be replaced.
345 children
346 The new child nodes, to be wrapped in columns and added to the
347 parent.
348 width
349 The width to be assigned to the columns.
351 In constructing columns for beamer using either 'simplecolumns' approach,
352 we have to wrap the original elements in column nodes, giving them an
353 appropriate width. Note that this mutates parent node.
355 ## Preconditions & preparation:
356 # TODO: check for children and raise error if not?
357 width = width or 0.90
358 ## Main:
359 # calc width of child columns
360 child_cnt = len (children)
361 col_width = width / child_cnt
362 # set each element of content in a column and add to column set
363 new_children = []
364 for child in children:
365 col = column()
366 col.width = col_width
367 col.append (child)
368 new_children.append (col)
369 par_node.children = new_children
372 def has_sub_sections (node):
373 """Test whether or not a section node has children with the
374 tagname section. The function is going to be used to assess
375 whether or not a certain section is the lowest level. Sections
376 that have not sub-sections (i.e. no children with the tagname
377 section) are assumed to be Beamer slides"""
378 for child in node.children:
379 if child.tagname == 'section':
380 return True
381 return False
384 def string_to_bool (stringin, default=True):
386 Turn a commandline arguement string into a boolean value.
388 if type (stringin) == bool:
389 return stringin
390 temp = stringin.lower()
391 if temp not in bool_strs:
392 return default
393 else:
394 return bool_dict[temp]
397 def highlight_code (text, lang):
399 Syntax-highlight source code using Pygments.
401 :Parameters:
402 text
403 The code to be formatted.
404 lang
405 The language of the source code.
407 :Returns:
408 A LaTeX formatted representation of the source code.
411 ## Preconditions & preparation:
412 from pygments import highlight
413 from pygments.formatters import LatexFormatter
414 ## Main:
415 lexer = get_lexer (text, lang)
416 lexer.add_filter('whitespace', tabsize=3, tabs=' ')
417 return highlight (text, lexer, LatexFormatter(tabsize=3))
420 def get_lexer (text, lang):
422 Return the Pygments lexer for parsing this sourcecode.
424 :Parameters:
425 text
426 The sourcecode to be lexed for highlighting. This is analysed if
427 the language is 'guess'.
428 lang
429 An abbreviation for the programming langauge of the code. Can be
430 any 'name' accepted by Pygments, including 'none' (plain text) or
431 'guess' (analyse the passed code for clues).
433 :Returns:
434 A Pygments lexer.
437 # TODO: what if source has errors?
438 ## Preconditions & preparation:
439 from pygments.lexers import (get_lexer_by_name, TextLexer, guess_lexer)
440 ## Main:
441 if lang == 'guess':
442 try:
443 return guess_lexer (text)
444 except Exception:
445 return None
446 elif lang == 'none':
447 return TextLexer
448 else:
449 return get_lexer_by_name (lang)
453 ### NODES ###
454 # Special nodes for marking up beamer layout
456 class columnset (nodes.container):
458 A group of columns to display on one slide.
460 Named as per docutils standards.
462 # NOTE: a simple container, has no attributes.
465 class column (nodes.container):
467 A single column, grouping content.
469 Named as per docutils standards.
471 # TODO: should really init width in a c'tor
473 class beamer_note (nodes.container):
475 Annotations for a beamer presentation.
477 Named as per docutils standards and to distinguish it from core docutils
478 node type.
480 pass
483 ### DIRECTIVES
485 class CodeBlockDirective (Directive):
487 Directive for a code block with special highlighting or line numbering
488 settings.
490 Unabashedly borrowed from the Sphinx source.
492 has_content = True
493 required_arguments = 0
494 optional_arguments = 1
495 final_argument_whitespace = False
496 option_spec = {
497 'linenos': directives.flag,
500 def run (self):
501 # extract langauge from block or commandline
502 # we allow the langauge specification to be optional
503 try:
504 language = self.arguments[0]
505 except IndexError:
506 language = 'guess'
507 code = u'\n'.join (self.content)
508 literal = nodes.literal_block (code, code)
509 literal['classes'].append ('code-block')
510 literal['language'] = language
511 literal['linenos'] = 'linenos' in self.options
512 return [literal]
514 for name in ['code-block', 'sourcecode']:
515 directives.register_directive (name, CodeBlockDirective)
518 class SimpleColsDirective (Directive):
520 A directive that wraps all contained nodes in beamer columns.
522 Accept 'width' as an optional argument for total width of contained
523 columns.
525 required_arguments = 0
526 optional_arguments = 1
527 final_argument_whitespace = True
528 has_content = True
529 option_spec = {'width': float}
531 def run (self):
532 ## Preconditions:
533 self.assert_has_content()
534 # get width
535 width = self.options.get ('width', 0.9)
536 if (width <= 0.0) or (1.0 < width):
537 raise self.error ("columnset width '%f' must be between 0.0 and 1.0" % width)
538 ## Main:
539 # parse content of columnset
540 dummy = nodes.Element()
541 self.state.nested_parse (self.content, self.content_offset,
542 dummy)
543 # make columnset
544 text = '\n'.join (self.content)
545 cset = columnset (text)
546 # wrap children in columns & set widths
547 wrap_children_in_columns (cset, dummy.children, width)
548 ## Postconditions & return:
549 return [cset]
551 for name in ['r2b-simplecolumns', 'r2b_simplecolumns']:
552 directives.register_directive (name, SimpleColsDirective)
555 class ColumnSetDirective (Directive):
557 A directive that encloses explicit columns in a 'columns' environment.
559 Within this, columns are explcitly set with the column directive. There is
560 a single optional argument 'width' to determine the total width of
561 columns on the page, expressed as a fraction of textwidth. If no width is
562 given, it defaults to 0.90.
564 Contained columns may have an assigned width. If not, the remaining width
565 is divided amongst them. Contained columns can 'overassign' width,
566 provided all column widths are defined.
569 required_arguments = 0
570 optional_arguments = 1
571 final_argument_whitespace = True
572 has_content = True
573 option_spec = {'width': float}
575 def run (self):
576 ## Preconditions:
577 self.assert_has_content()
578 # get and check width of column set
579 width = self.options.get ('width', 0.9)
580 if ((width <= 0.0) or (1.0 < width)):
581 raise self.error ( \
582 "columnset width '%f' must be between 0.0 and 1.0" % width)
583 ## Main:
584 # make columnset
585 text = '\n'.join (self.content)
586 cset = columnset (text)
587 # parse content of columnset
588 self.state.nested_parse (self.content, self.content_offset, cset)
589 # survey widths
590 used_width = 0.0
591 unsized_cols = []
592 for child in cset:
593 child_width = getattr (child, 'width', None)
594 if (child_width):
595 used_width += child_width
596 else:
597 unsized_cols.append (child)
599 if (1.0 < used_width):
600 raise self.error ( \
601 "cumulative column width '%f' exceeds 1.0" % used_width)
602 # set unsized widths
603 if (unsized_cols):
604 excess_width = width - used_width
605 if (excess_width <= 0.0):
606 raise self.error ( \
607 "no room for unsized columns '%f'" % excess_width)
608 col_width = excess_width / len (unsized_cols)
609 for child in unsized_cols:
610 child.width = col_width
611 elif (width < used_width):
612 # TODO: should post a warning?
613 pass
614 ## Postconditions & return:
615 return [cset]
617 for name in ['r2b-columnset', 'r2b_columnset']:
618 directives.register_directive (name, ColumnSetDirective)
621 class ColumnDirective (Directive):
623 A directive to explicitly create an individual column.
625 This can only be used within the columnset directive. It can takes a
626 single optional argument 'width' to determine the column width on page.
627 If no width is given, it is recorded as None and should be later assigned
628 by the enclosing columnset.
630 required_arguments = 0
631 optional_arguments = 1
632 final_argument_whitespace = True
633 has_content = True
634 option_spec = {'width': float}
636 def run (self):
637 ## Preconditions:
638 self.assert_has_content()
639 # get width
640 width = self.options.get ('width', None)
641 if (width is not None):
642 if (width <= 0.0) or (1.0 < width):
643 raise self.error ("columnset width '%f' must be between 0.0 and 1.0" % width)
644 ## Main:
645 # make columnset
646 text = '\n'.join (self.content)
647 col = column (text)
648 col.width = width
649 # parse content of column
650 self.state.nested_parse (self.content, self.content_offset, col)
651 # adjust widths
652 ## Postconditions & return:
653 return [col]
655 for name in ['r2b-column', 'r2b_column']:
656 directives.register_directive (name, ColumnDirective)
659 class NoteDirective (Directive):
661 A directive to include notes within a beamer presentation.
664 required_arguments = 0
665 optional_arguments = 0
666 final_argument_whitespace = True
667 has_content = True
668 option_spec = {}
670 def run (self):
671 ## Preconditions:
672 self.assert_has_content()
673 ## Main:
674 ## Preconditions:
675 # make columnset
676 text = '\n'.join (self.content)
677 note_node = beamer_note (text)
678 # parse content of note
679 self.state.nested_parse (self.content, self.content_offset, note_node)
680 ## Postconditions & return:
681 return [note_node]
683 for name in ['r2b-note', 'r2b_note']:
684 directives.register_directive (name, NoteDirective)
687 class beamer_section (Directive):
689 required_arguments = 1
690 optional_arguments = 0
691 final_argument_whitespace = True
692 has_content = True
694 def run (self):
695 title = self.arguments[0]
697 section_text = '\\section{%s}' % title
698 text_node = nodes.Text (title)
699 text_nodes = [text_node]
700 title_node = nodes.title (title, '', *text_nodes)
701 name = normalize_name (title_node.astext())
703 section_node = nodes.section(rawsource=self.block_text)
704 section_node['names'].append(name)
705 section_node += title_node
706 messages = []
707 title_messages = []
708 section_node += messages
709 section_node += title_messages
710 section_node.tagname = 'beamer_section'
711 return [section_node]
713 for name in ['beamer_section', 'r2b-section', 'r2b_section']:
714 directives.register_directive (name, beamer_section)
717 ### WRITER
719 class BeamerTranslator (LaTeXTranslator):
721 A converter for docutils elements to beamer-flavoured latex.
724 def __init__ (self, document):
725 LaTeXTranslator.__init__ (self, document)
727 self.organization = None#used for Beamer title and possibly
728 #header/footer. Set from docinfo
729 # record the the settings for codeblocks
730 self.cb_use_pygments = document.settings.cb_use_pygments
731 self.cb_replace_tabs = document.settings.cb_replace_tabs
732 self.cb_default_lang = document.settings.cb_default_lang
734 self.head_prefix = [x for x in self.head_prefix
735 if ('{typearea}' not in x)]
736 #hyperref_posn = [i for i in range (len (self.head_prefix))
737 # if ('{hyperref}' in self.head_prefix[i])]
738 hyperref_posn = index (self.head_prefix,
739 lambda x: '{hyperref}\n' in x)
740 if (hyperref_posn is None):
741 self.head_prefix.extend ([
742 '\\usepackage{hyperref}\n'
745 #self.head_prefix[hyperref_posn[0]] = '\\usepackage{hyperref}\n'
746 self.head_prefix.extend ([
747 '\\definecolor{rrblitbackground}{rgb}{0.55, 0.3, 0.1}\n',
748 '\\newenvironment{rtbliteral}{\n',
749 '\\begin{ttfamily}\n',
750 '\\color{rrblitbackground}\n',
751 '}{\n',
752 '\\end{ttfamily}\n',
753 '}\n',
756 if (self.cb_use_pygments):
757 #from pygments.formatters import LatexFormatter
758 #fmtr = LatexFormatter()
759 self.head_prefix.extend ([
760 '\\usepackage{fancyvrb}\n',
761 '\\usepackage{color}\n',
762 #LatexFormatter().get_style_defs(),
765 # set appropriate header options for theming
766 theme = document.settings.theme
767 if theme:
768 self.head_prefix.append ('\\usetheme{%s}\n' % theme)
770 # set appropriate header options for note display
771 shownotes = document.settings.shownotes
772 if shownotes == SHOWNOTES_TRUE:
773 shownotes = SHOWNOTES_RIGHT
774 use_pgfpages = True
775 if (shownotes == SHOWNOTES_FALSE):
776 option_str = 'hide notes'
777 use_pgfpages = False
778 elif (shownotes == SHOWNOTES_ONLY):
779 option_str = 'show only notes'
780 else:
781 if (shownotes == SHOWNOTES_LEFT):
782 notes_posn = 'left'
783 elif (shownotes in SHOWNOTES_RIGHT):
784 notes_posn = 'right'
785 elif (shownotes == SHOWNOTES_TOP):
786 notes_posn = 'top'
787 elif (shownotes == SHOWNOTES_BOTTOM):
788 notes_posn = 'bottom'
789 else:
790 # TODO: better error handling
791 assert False, "unrecognised option for shownotes '%s'" % shownotes
792 option_str = 'show notes on second screen=%s' % notes_posn
793 if use_pgfpages:
794 self.head_prefix.append ('\\usepackage{pgfpages}\n')
795 self.head_prefix.append ('\\setbeameroption{%s}\n' % option_str)
797 if (self.cb_use_pygments):
798 from pygments.formatters import LatexFormatter
799 fmtr = LatexFormatter()
800 self.head_prefix.extend ([
801 LatexFormatter().get_style_defs(),
804 self.overlay_bullets = string_to_bool (document.settings.overlaybullets, False)
805 self.fragile_default = string_to_bool (document.settings.fragile_default, True)
806 #using a False default because
807 #True is the actual default. If you are trying to pass in a value
808 #and I can't determine what you really meant, I am assuming you
809 #want something other than the actual default.
810 self.centerfigs = string_to_bool(document.settings.centerfigs, False)#same reasoning as above
811 self.in_columnset = False
812 self.in_column = False
813 self.in_note = False
814 self.frame_level = 0
816 # this fixes the hardcoded section titles in docutils 0.4
817 self.d_class = DocumentClass ('article')
820 def depart_document(self, node):
821 # Complete header with information gained from walkabout
822 # a) conditional requirements (before style sheet)
823 self.requirements = self.requirements.sortedvalues()
824 # b) coditional fallback definitions (after style sheet)
825 self.fallbacks = self.fallbacks.sortedvalues()
826 # c) PDF properties
827 self.pdfsetup.append(PreambleCmds.linking %
828 ('colorlinks=true,linkcolor=%s,urlcolor=%s' %
829 (self.hyperlink_color,
830 self.hyperlink_color)))
831 if self.pdfauthor:
832 authors = self.author_separator.join(self.pdfauthor)
833 self.pdfinfo.append(' pdfauthor={%s}' % authors)
834 if self.pdfinfo:
835 self.pdfsetup += [r'\hypersetup{'] + self.pdfinfo + ['}']
836 # Complete body
837 # a) document title (part 'body_prefix'):
838 # NOTE: Docutils puts author/date into docinfo, so normally
839 # we do not want LaTeX author/date handling (via \maketitle).
840 # To deactivate it, we add \title, \author, \date,
841 # even if the arguments are empty strings.
842 if self.title or self.author_stack or self.date:
843 authors = ['\\\\\n'.join(author_entry)
844 for author_entry in self.author_stack]
845 title = [''.join(self.title)] + self.title_labels
846 shorttitle = ''.join(self.title)
847 shortauthor = ''.join(self.pdfauthor)
849 if self.subtitle:
850 title += [r'\\ % subtitle',
851 r'\large{%s}' % ''.join(self.subtitle)
852 ] + self.subtitle_labels
853 docinfo_list = [shorttitle,
854 '%\n '.join(title),
855 shortauthor,
856 ' \\and\n'.join(authors),
857 ', '.join(self.date)]
858 if self.organization is None:
859 docinfo_str = PreambleCmds.documenttitle % tuple(docinfo_list)
860 else:
861 docinfo_list.append(self.organization)
862 docinfo_str = docinfo_w_institute % tuple(docinfo_list)
863 self.body_pre_docinfo.append(docinfo_str)
864 # b) bibliography
865 # TODO insertion point of bibliography should be configurable.
866 if self._use_latex_citations and len(self._bibitems)>0:
867 if not self.bibtex:
868 widest_label = ''
869 for bi in self._bibitems:
870 if len(widest_label)<len(bi[0]):
871 widest_label = bi[0]
872 self.out.append('\n\\begin{thebibliography}{%s}\n' %
873 widest_label)
874 for bi in self._bibitems:
875 # cite_key: underscores must not be escaped
876 cite_key = bi[0].replace(r'\_','_')
877 self.out.append('\\bibitem[%s]{%s}{%s}\n' %
878 (bi[0], cite_key, bi[1]))
879 self.out.append('\\end{thebibliography}\n')
880 else:
881 self.out.append('\n\\bibliographystyle{%s}\n' %
882 self.bibtex[0])
883 self.out.append('\\bibliography{%s}\n' % self.bibtex[1])
884 # c) make sure to generate a toc file if needed for local contents:
885 if 'minitoc' in self.requirements and not self.has_latex_toc:
886 self.out.append('\n\\faketableofcontents % for local ToCs\n')
890 def visit_docinfo_item(self, node, name):
891 if name == 'author':
892 self.pdfauthor.append(self.attval(node.astext()))
893 if self.use_latex_docinfo:
894 if name in ('author', 'contact', 'address'):
895 # We attach these to the last author. If any of them precedes
896 # the first author, put them in a separate "author" group
897 # (in lack of better semantics).
898 if name == 'author' or not self.author_stack:
899 self.author_stack.append([])
900 if name == 'address': # newlines are meaningful
901 self.insert_newline = 1
902 text = self.encode(node.astext())
903 self.insert_newline = False
904 else:
905 text = self.attval(node.astext())
906 self.author_stack[-1].append(text)
907 raise nodes.SkipNode
908 elif name == 'date':
909 self.date.append(self.attval(node.astext()))
910 raise nodes.SkipNode
911 elif name == 'organization':
912 self.organization = node.astext()
913 raise nodes.SkipNode
915 self.out.append('\\textbf{%s}: &\n\t' % self.language_label(name))
916 if name == 'address':
917 self.insert_newline = 1
918 self.out.append('{\\raggedright\n')
919 self.context.append(' } \\\\\n')
920 else:
921 self.context.append(' \\\\\n')
922 #LaTeXTranslator.visit_docinfo_item(self, node, name)
925 def latex_image_length(self, width_str):
926 match = re.match('(\d*\.?\d*)\s*(\S*)', width_str)
927 if not match:
928 # fallback
929 return width_str
930 res = width_str
931 amount, unit = match.groups()[:2]
932 if unit == "px":
933 # LaTeX does not know pixels but points
934 res = "%spt" % amount
935 elif unit == "%":
936 res = "%.3f\\linewidth" % (float(amount) / 100.0)
937 return res
940 def visit_image(self, node):
941 attrs = node.attributes
942 if not 'align' in attrs and self.centerfigs:
943 attrs['align'] = 'center'
944 if ('height' not in attrs) and ('width' not in attrs):
945 attrs['height'] = '0.75\\textheight'
946 LaTeXTranslator.visit_image(self, node)
948 ## #Old approach
949 ## if self.centerfigs:
950 ## self.out.append('\\begin{center}\n')
951 ## attrs = node.attributes
952 ## # Add image URI to dependency list, assuming that it's
953 ## # referring to a local file.
954 ## self.settings.record_dependencies.add(attrs['uri'])
955 ## pre = [] # in reverse order
956 ## post = []
957 ## include_graphics_options = []
958 ## inline = isinstance(node.parent, nodes.TextElement)
959 ## if 'scale' in attrs:
960 ## # Could also be done with ``scale`` option to
961 ## # ``\includegraphics``; doing it this way for consistency.
962 ## pre.append('\\scalebox{%f}{' % (attrs['scale'] / 100.0,))
963 ## post.append('}')
964 ## if 'width' in attrs:
965 ## include_graphics_options.append('width=%s' % (
966 ## self.latex_image_length(attrs['width']), ))
967 ## if 'height' in attrs:
968 ## include_graphics_options.append('height=%s' % (
969 ## self.latex_image_length(attrs['height']), ))
970 ## if ('height' not in attrs) and ('width' not in attrs):
971 ## include_graphics_options.append('height=0.75\\textheight')
973 ## if 'align' in attrs:
974 ## align_prepost = {
975 ## # By default latex aligns the bottom of an image.
976 ## (1, 'bottom'): ('', ''),
977 ## (1, 'middle'): ('\\raisebox{-0.5\\height}{', '}'),
978 ## (1, 'top'): ('\\raisebox{-\\height}{', '}'),
979 ## (0, 'center'): ('{\\hfill', '\\hfill}'),
980 ## # These 2 don't exactly do the right thing. The image should
981 ## # be floated alongside the paragraph. See
982 ## # http://www.w3.org/TR/html4/struct/objects.html#adef-align-IMG
983 ## (0, 'left'): ('{', '\\hfill}'),
984 ## (0, 'right'): ('{\\hfill', '}'),}
985 ## try:
986 ## pre.append(align_prepost[inline, attrs['align']][0])
987 ## post.append(align_prepost[inline, attrs['align']][1])
988 ## except KeyError:
989 ## pass # XXX complain here?
990 ## if not inline:
991 ## pre.append('\n')
992 ## post.append('\n')
993 ## pre.reverse()
994 ## self.out.extend( pre )
995 ## options = ''
996 ## if len(include_graphics_options)>0:
997 ## options = '[%s]' % (','.join(include_graphics_options))
998 ## self.out.append( '\\includegraphics%s{%s}' % (
999 ## options, attrs['uri'] ) )
1000 ## self.out.extend( post )
1003 ## def depart_image(self, node):
1004 ## #This goes with the old approach above
1005 ## if self.centerfigs:
1006 ## self.out.append('\\end{center}\n')
1009 ## def visit_Text (self, node):
1010 ## self.out.append(self.encode(node.astext()))
1012 def depart_Text(self, node):
1013 pass
1016 def node_fragile_check(self, node):
1017 """Check whether or not a slide should be marked as fragile.
1018 If the slide has class attributes of fragile or notfragile,
1019 then the document default is overriden."""
1020 if 'notfragile' in node.attributes['classes']:
1021 return False
1022 elif 'fragile' in node.attributes['classes']:
1023 return True
1024 else:
1025 return self.fragile_default
1028 def begin_frametag (self, node):
1029 bf_str = '\n\\begin{frame}'
1030 if self.node_fragile_check(node):
1031 bf_str += '[fragile]'
1032 bf_str += '\n'
1033 return bf_str
1036 def end_frametag (self):
1037 return '\\end{frame}\n'
1039 def visit_section (self, node):
1040 if has_sub_sections (node):
1041 temp = self.section_level + 1
1042 if temp > self.frame_level:
1043 self.frame_level = temp
1044 else:
1045 self.out.append (self.begin_frametag(node))
1046 LaTeXTranslator.visit_section (self, node)
1049 def bookmark (self, node):
1050 """I think beamer alread handles bookmarks well, so I
1051 don't want duplicates."""
1052 return ''
1054 def depart_section (self, node):
1055 # Remove counter for potential subsections:
1056 LaTeXTranslator.depart_section (self, node)
1057 if (self.section_level == self.frame_level):#0
1058 self.out.append (self.end_frametag())
1061 def visit_title (self, node):
1062 if node.astext() == 'dummy':
1063 raise nodes.SkipNode
1064 if (self.section_level == self.frame_level+1):#1
1065 self.out.append ('\\frametitle{%s}\n\n' % \
1066 self.encode(node.astext()))
1067 raise nodes.SkipNode
1068 else:
1069 LaTeXTranslator.visit_title (self, node)
1071 def depart_title (self, node):
1072 if (self.section_level != self.frame_level+1):#1
1073 LaTeXTranslator.depart_title (self, node)
1076 def visit_literal_block (self, node):
1077 # FIX: the purpose of this method is unclear, but it causes parsed
1078 # literals in docutils 0.6 to lose indenting. Thus we've solve the
1079 # problem be just getting rid of it. [PMA 20091020]
1080 # TODO: replace leading tabs like in codeblocks?
1081 if (node_has_class (node, 'code-block') and self.cb_use_pygments):
1082 self.visit_codeblock (node)
1083 else:
1084 self.out.append ('\\setbeamerfont{quote}{parent={}}\n')
1085 LaTeXTranslator.visit_literal_block (self, node)
1087 def depart_literal_block (self, node):
1088 # FIX: see `visit_literal_block`
1089 if (node_has_class (node, 'code-block') and self.cb_use_pygments):
1090 self.visit_codeblock (node)
1091 else:
1092 LaTeXTranslator.depart_literal_block (self, node)
1093 self.out.append ( '\\setbeamerfont{quote}{parent=quotation}\n' )
1095 def visit_codeblock (self, node):
1096 # was langauge argument defined on node?
1097 lang = node.get ('language', None)
1098 # otherwise, was it defined in node classes?
1099 if (lang is None):
1100 lang = node_lang_class (node)
1101 # otherwise, use commandline argument or default
1102 if lang is None:
1103 lang = self.cb_default_lang
1104 # replace tabs if required
1105 srccode = node.rawsource
1106 if (self.cb_replace_tabs):
1107 srccode = '\n'.join (adjust_indent_spaces (x,
1108 new_width=self.cb_replace_tabs) for x in srccode.split ('\n'))
1109 # hilight the code
1110 hilite_code = highlight_code (srccode, lang)
1111 self.out.append ('\n' + hilite_code + '\n')
1112 raise nodes.SkipNode
1114 def depart_codeblock (self, node):
1115 pass
1117 def visit_bullet_list (self, node):
1118 # NOTE: required by the loss of 'topic_classes' in docutils 0.6
1119 # TODO: so what replaces it?
1120 if (hasattr (self, 'topic_classes') and
1121 ('contents' in self.topic_classes)):
1122 if self.use_latex_toc:
1123 raise nodes.SkipNode
1124 self.out.append( '\\begin{list}{}{}\n' )
1125 else:
1126 begin_str = '\\begin{itemize}'
1127 if self.node_overlay_check(node):
1128 begin_str += '[<+-| alert@+>]'
1129 begin_str += '\n'
1130 self.out.append (begin_str)
1133 def node_overlay_check(self, node):
1134 """Assuming that the bullet or enumerated list is the child of
1135 a slide, check to see if the slide has either nooverlay or
1136 overlay in its classes. If not, default to the commandline
1137 specification for overlaybullets."""
1138 if 'nooverlay' in node.parent.attributes['classes']:
1139 return False
1140 elif 'overlay' in node.parent.attributes['classes']:
1141 return True
1142 else:
1143 return self.overlay_bullets
1146 def depart_bullet_list (self, node):
1147 # NOTE: see `visit_bullet_list`
1148 if (hasattr (self, 'topic_classes') and
1149 ('contents' in self.topic_classes)):
1150 self.out.append( '\\end{list}\n' )
1151 else:
1152 self.out.append( '\\end{itemize}\n' )
1154 ## def latex_image_length(self, width_str):
1155 ## if ('\\textheight' in width_str) or ('\\textwidth' in width_str):
1156 ## return width_str
1157 ## else:
1158 ## return LaTeXTranslator.latex_image_length(self, width_str)
1160 def visit_enumerated_list (self, node):
1161 #LaTeXTranslator has a very complicated
1162 #visit_enumerated_list that throws out much of what latex
1163 #does to handle them for us. I am going back to relying
1164 #on latex.
1165 if ('contents' in getattr (self, 'topic_classes', [])):
1166 if self.use_latex_toc:
1167 raise nodes.SkipNode
1168 self.out.append( '\\begin{list}{}{}\n' )
1169 else:
1170 begin_str = '\\begin{enumerate}'
1171 if self.node_overlay_check(node):
1172 begin_str += '[<+-| alert@+>]'
1173 begin_str += '\n'
1174 self.out.append(begin_str)
1175 if node.has_key('start'):
1176 self.out.append('\\addtocounter{enumi}{%d}\n' \
1177 % (node['start']-1))
1180 def depart_enumerated_list (self, node):
1181 if ('contents' in getattr (self, 'topic_classes', [])):
1182 self.out.append ('\\end{list}\n')
1183 else:
1184 self.out.append ('\\end{enumerate}\n' )
1187 ## def astext (self):
1188 ## if self.pdfinfo is not None and self.pdfauthor:
1189 ## self.pdfinfo.append ('pdfauthor={%s}' % self.pdfauthor)
1190 ## if self.pdfinfo:
1191 ## pdfinfo = '\\hypersetup{\n' + ',\n'.join (self.pdfinfo) + '\n}\n'
1192 ## else:
1193 ## pdfinfo = ''
1194 ## head = '\\title{%s}\n' % self.title
1195 ## if self.auth_stack:
1196 ## auth_head = '\\author{%s}\n' % ' \\and\n'.join (\
1197 ## ['~\\\\\n'.join (auth_lines) for auth_lines in self.auth_stack])
1198 ## head += auth_head
1199 ## if self.date:
1200 ## date_head = '\\date{%s}\n' % self.date
1201 ## head += date_head
1202 ## return ''.join (self.head_prefix + [head] + self.head + [pdfinfo]
1203 ## + self.out_prefix + self.out + self.out_suffix)
1206 ## def visit_docinfo (self, node):
1207 ## """
1208 ## Docinfo is ignored for Beamer documents.
1209 ## """
1210 ## pass
1212 ## def depart_docinfo (self, node):
1213 ## # see visit_docinfo
1214 ## pass
1216 def visit_columnset (self, node):
1217 assert not self.in_columnset, \
1218 "already in column set, which cannot be nested"
1219 self.in_columnset = True
1220 self.out.append ('\\begin{columns}[T]\n')
1222 def depart_columnset (self, node):
1223 assert self.in_columnset, "not in column set"
1224 self.in_columnset = False
1225 self.out.append ('\\end{columns}\n')
1227 def visit_column (self, node):
1228 assert not self.in_column, "already in column, which cannot be nested"
1229 self.in_column = True
1230 self.out.append ('\\column{%.2f\\textwidth}\n' % node.width)
1232 def depart_column (self, node):
1233 self.in_column = False
1234 self.out.append ('\n')
1236 def visit_beamer_note (self, node):
1237 assert not self.in_note, "already in note, which cannot be nested"
1238 self.in_note = True
1239 self.out.append ('\\note{\n')
1241 def depart_beamer_note (self, node):
1242 self.in_note = False
1243 self.out.append ('}\n')
1245 def visit_container (self, node):
1247 Handle containers with 'special' names, ignore the rest.
1249 # NOTE: theres something wierd here where ReST seems to translate
1250 # underscores in container identifiers into hyphens. So for the
1251 # moment we'll allow both.
1252 if (node_has_class (node, 'r2b-simplecolumns')):
1253 self.visit_columnset (node)
1254 wrap_children_in_columns (node, node.children)
1255 elif (node_has_class (node, 'r2b-note')):
1256 self.visit_beamer_note (node)
1257 else:
1258 # currently the LaTeXTranslator does nothing, but just in case
1259 LaTeXTranslator.visit_container (self, node)
1261 def depart_container (self, node):
1262 if (node_has_class (node, 'r2b-simplecolumns')):
1263 self.depart_columnset (node)
1264 elif (node_has_class (node, 'r2b-note')):
1265 self.depart_beamer_note (node)
1266 else:
1267 # currently the LaTeXTranslator does nothing, but just in case
1268 LaTeXTranslator.depart_container (self, node)
1271 class BeamerWriter (Latex2eWriter):
1273 A docutils writer that produces Beamer-flavoured LaTeX.
1275 settings_spec = BEAMER_SPEC
1276 settings_default_overrides = BEAMER_DEFAULT_OVERRIDES
1277 def __init__(self):
1278 self.settings_defaults.update(BEAMER_DEFAULTS)
1279 Latex2eWriter.__init__(self)
1280 self.translator_class = BeamerTranslator
1283 ### TEST & DEBUG ###
1284 # TODO: should really move to a test file or dir
1286 def test_with_file (fpath, args=[]):
1288 Call rst2beamer on the given file with the given args.
1290 During development, it's handy to be able to easily call the writer from
1291 within Python. This is a convenience function that wraps the docutils
1292 functions to do so.
1294 return publish_cmdline (writer=BeamerWriter(), argv=args+[fpath])
1297 ### MAIN ###
1299 def main ():
1300 description = (
1301 "Generates Beamer-flavoured LaTeX for PDF-based presentations." +
1302 default_description)
1303 publish_cmdline (writer=BeamerWriter(), description=description)
1306 if __name__ == '__main__':
1307 main()
1310 ### END ###