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.
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>"
39 locale.setlocale (locale.LC_ALL, '')
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'
59 SHOWNOTES_BOTTOM = 'bottom'
79 'These are derived almost entirely from the LaTeX2e options',
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',
95 'Default for whether or not to pass the fragile option to '
96 'the beamber frames (slides).',
97 ['--fragile-default'],
102 'Center figures. All includegraphics statements will be put '
103 'inside center environments.',
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'],
115 ## 'Attach author and date to the document title.',
116 ## ['--use-latex-docinfo'],
117 ## {'default': 1, 'action': 'store_true',
118 ## 'validator': frontend.validate_boolean}
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).",
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. "
140 ['--codeblocks-use-pygments'],
142 'action': "store_true",
143 'dest': 'cb_use_pygments',
147 # replace tabs inside codeblocks?
149 "Replace the leading tabs in codeblocks with spaces.",
150 ['--codeblocks-replace-tabs'],
154 'dest': 'cb_replace_tabs',
158 # what language the codeblock is if not specified
160 "The default language to hilight code blocks as. ",
161 ['--codeblocks-default-language'],
165 'dest': 'cb_default_lang',
166 'choices': HILITE_OPTIONS.values(),
170 ] + list (Latex2eWriter.settings_spec[2][2:])
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"""
196 docinfo_w_institute = r"""
205 ### IMPLEMENTATION ###
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.
217 The source string to change.
219 The expected width for an indent in the source string.
221 The new width to make an ident.
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.
233 >>> adjust_indent_spaces (' foo')
235 >>> adjust_indent_spaces (' foo', orig_width=2, new_width=1)
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
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):
257 match = LEADING_SPACE_RE.match (strn)
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:]
266 def index (seq, f, fail=None):
268 Return the index of the first item in seq where f(item) is True.
272 A sequence or iterable
274 A boolean function an element of `seq`, e.g. `lambda x: x==4`
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])):
286 def node_has_class (node, classes):
288 Does the node have one of these classes?
294 A class name or list of class names.
297 A boolean indicating membership.
299 A convenience function, largely for testing for the special class names
302 ## Preconditions & preparation:
303 # wrap single name in list
304 if (not (issubclass (type (classes), list))):
307 for cname in classes:
308 if cname in node['classes']:
313 def node_lang_class (node):
315 Extract a language specification from a node class names.
322 A string giving a language abbreviation (e.g. 'py') or None if no
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
332 for cname in node['classes']:
333 if (cname.startswith ('lang-')):
338 def wrap_children_in_columns (par_node, children, width=None):
340 Replace this node's children with columns containing the passed children.
344 The node whose children are to be replaced.
346 The new child nodes, to be wrapped in columns and added to the
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
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
364 for child in children:
366 col.width = col_width
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':
384 def string_to_bool (stringin, default=True):
386 Turn a commandline arguement string into a boolean value.
388 if type (stringin) == bool:
390 temp = stringin.lower()
391 if temp not in bool_strs:
394 return bool_dict[temp]
397 def highlight_code (text, lang):
399 Syntax-highlight source code using Pygments.
403 The code to be formatted.
405 The language of the source code.
408 A LaTeX formatted representation of the source code.
411 ## Preconditions & preparation:
412 from pygments import highlight
413 from pygments.formatters import LatexFormatter
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.
426 The sourcecode to be lexed for highlighting. This is analysed if
427 the language is 'guess'.
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).
437 # TODO: what if source has errors?
438 ## Preconditions & preparation:
439 from pygments.lexers import (get_lexer_by_name, TextLexer, guess_lexer)
443 return guess_lexer (text)
449 return get_lexer_by_name (lang)
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
485 class CodeBlockDirective (Directive):
487 Directive for a code block with special highlighting or line numbering
490 Unabashedly borrowed from the Sphinx source.
493 required_arguments = 0
494 optional_arguments = 1
495 final_argument_whitespace = False
497 'linenos': directives.flag,
501 # extract langauge from block or commandline
502 # we allow the langauge specification to be optional
504 language = self.arguments[0]
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
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
525 required_arguments = 0
526 optional_arguments = 1
527 final_argument_whitespace = True
529 option_spec = {'width': float}
533 self.assert_has_content()
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)
539 # parse content of columnset
540 dummy = nodes.Element()
541 self.state.nested_parse (self.content, self.content_offset,
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:
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
573 option_spec = {'width': float}
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)):
582 "columnset width '%f' must be between 0.0 and 1.0" % width)
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)
593 child_width = getattr (child, 'width', None)
595 used_width += child_width
597 unsized_cols.append (child)
599 if (1.0 < used_width):
601 "cumulative column width '%f' exceeds 1.0" % used_width)
604 excess_width = width - used_width
605 if (excess_width <= 0.0):
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?
614 ## Postconditions & return:
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
634 option_spec = {'width': float}
638 self.assert_has_content()
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)
646 text = '\n'.join (self.content)
649 # parse content of column
650 self.state.nested_parse (self.content, self.content_offset, col)
652 ## Postconditions & return:
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
672 self.assert_has_content()
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:
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
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
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)
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',
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
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
775 if (shownotes == SHOWNOTES_FALSE):
776 option_str = 'hide notes'
778 elif (shownotes == SHOWNOTES_ONLY):
779 option_str = 'show only notes'
781 if (shownotes == SHOWNOTES_LEFT):
783 elif (shownotes in SHOWNOTES_RIGHT):
785 elif (shownotes == SHOWNOTES_TOP):
787 elif (shownotes == SHOWNOTES_BOTTOM):
788 notes_posn = 'bottom'
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
794 self.head_prefix.append ('\\usepackage{pgfpages}\n')
795 self.head_prefix.append ('\\setbeameroption{%s}\n' % option_str)
796 self.head_prefix.append ('\\usepackage{xmpmulti}\n')
798 if (self.cb_use_pygments):
799 from pygments.formatters import LatexFormatter
800 fmtr = LatexFormatter()
801 self.head_prefix.extend ([
802 LatexFormatter().get_style_defs(),
805 self.overlay_bullets = string_to_bool (document.settings.overlaybullets, False)
806 self.fragile_default = string_to_bool (document.settings.fragile_default, True)
807 #using a False default because
808 #True is the actual default. If you are trying to pass in a value
809 #and I can't determine what you really meant, I am assuming you
810 #want something other than the actual default.
811 self.centerfigs = string_to_bool(document.settings.centerfigs, False)#same reasoning as above
812 self.in_columnset = False
813 self.in_column = False
817 # this fixes the hardcoded section titles in docutils 0.4
818 self.d_class = DocumentClass ('article')
821 def depart_document(self, node):
822 # Complete header with information gained from walkabout
823 # a) conditional requirements (before style sheet)
824 self.requirements = self.requirements.sortedvalues()
825 # b) coditional fallback definitions (after style sheet)
826 self.fallbacks = self.fallbacks.sortedvalues()
828 self.pdfsetup.append(PreambleCmds.linking % (self.colorlinks,
829 self.hyperlink_color,
830 self.hyperlink_color))
832 authors = self.author_separator.join(self.pdfauthor)
833 self.pdfinfo.append(' pdfauthor={%s}' % authors)
835 self.pdfsetup += [r'\hypersetup{'] + self.pdfinfo + ['}']
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)
850 title += [r'\\ % subtitle',
851 r'\large{%s}' % ''.join(self.subtitle)
852 ] + self.subtitle_labels
853 docinfo_list = [shorttitle,
856 ' \\and\n'.join(authors),
857 ', '.join(self.date)]
858 if self.organization is None:
859 docinfo_str = PreambleCmds.documenttitle % tuple(docinfo_list)
861 docinfo_list.append(self.organization)
862 docinfo_str = docinfo_w_institute % tuple(docinfo_list)
863 self.body_pre_docinfo.append(docinfo_str)
865 # TODO insertion point of bibliography should be configurable.
866 if self._use_latex_citations and len(self._bibitems)>0:
869 for bi in self._bibitems:
870 if len(widest_label)<len(bi[0]):
872 self.out.append('\n\\begin{thebibliography}{%s}\n' %
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')
881 self.out.append('\n\\bibliographystyle{%s}\n' %
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):
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
905 text = self.attval(node.astext())
906 self.author_stack[-1].append(text)
909 self.date.append(self.attval(node.astext()))
911 elif name == 'organization':
912 self.organization = node.astext()
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')
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)
931 amount, unit = match.groups()[:2]
933 # LaTeX does not know pixels but points
934 res = "%spt" % amount
936 res = "%.3f\\linewidth" % (float(amount) / 100.0)
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)
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
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,))
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:
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', '}'),}
986 ## pre.append(align_prepost[inline, attrs['align']][0])
987 ## post.append(align_prepost[inline, attrs['align']][1])
989 ## pass # XXX complain here?
994 ## self.out.extend( pre )
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):
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']:
1022 elif 'fragile' in node.attributes['classes']:
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]'
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
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."""
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
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)
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)
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?
1100 lang = node_lang_class (node)
1101 # otherwise, use commandline argument or default
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'))
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):
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' )
1126 begin_str = '\\begin{itemize}'
1127 if self.node_overlay_check(node):
1128 begin_str += '[<+-| alert@+>]'
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']:
1140 elif 'overlay' in node.parent.attributes['classes']:
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' )
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):
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
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' )
1170 begin_str = '\\begin{enumerate}'
1171 if self.node_overlay_check(node):
1172 begin_str += '[<+-| alert@+>]'
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')
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)
1191 ## pdfinfo = '\\hypersetup{\n' + ',\n'.join (self.pdfinfo) + '\n}\n'
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
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):
1208 ## Docinfo is ignored for Beamer documents.
1212 ## def depart_docinfo (self, node):
1213 ## # see visit_docinfo
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"
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)
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)
1267 # currently the LaTeXTranslator does nothing, but just in case
1268 LaTeXTranslator.depart_container (self, node)
1271 # Convert strong from \textbf to \alert
1272 def visit_strong(self, node):
1273 self.out.append('\\alert{')
1275 self.visit_inline(node)
1277 def depart_strong(self, node):
1279 self.depart_inline(node)
1280 self.out.append('}')
1283 class BeamerWriter (Latex2eWriter):
1285 A docutils writer that produces Beamer-flavoured LaTeX.
1287 settings_spec = BEAMER_SPEC
1288 settings_default_overrides = BEAMER_DEFAULT_OVERRIDES
1290 self.settings_defaults.update(BEAMER_DEFAULTS)
1291 Latex2eWriter.__init__(self)
1292 self.translator_class = BeamerTranslator
1295 ### TEST & DEBUG ###
1296 # TODO: should really move to a test file or dir
1298 def test_with_file (fpath, args=[]):
1300 Call rst2beamer on the given file with the given args.
1302 During development, it's handy to be able to easily call the writer from
1303 within Python. This is a convenience function that wraps the docutils
1306 return publish_cmdline (writer=BeamerWriter(), argv=args+[fpath])
1313 "Generates Beamer-flavoured LaTeX for PDF-based presentations." +
1314 default_description)
1315 publish_cmdline (writer=BeamerWriter(), description=description)
1318 if __name__ == '__main__':