]> git.llucax.com Git - z.facultad/75.00/presentacion.git/blob - rst2beamer.py
Usar "y" en vez de "+" en autores de trabajos relacionados
[z.facultad/75.00/presentacion.git] / rst2beamer.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 """
4 A docutils script converting restructured text into Beamer-flavoured LaTeX.
5
6 Beamer is a LaTeX document class for presentations. Via this script, ReST can
7 be used to prepare slides. It can be called::
8
9         rst2beamer.py infile.txt > outfile.tex
10
11 where ``infile.txt`` contains the rst and ``outfile.tex`` contains the
12 Beamer-flavoured LaTeX.
13
14 See <http:www.agapow.net/software/rst2beamer> for more details.
15
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?
23
24
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
27
28 __docformat__ = 'restructuredtext en'
29 __author__ = "Ryan Krauss <ryanwkrauss@gmail.com> & Paul-Michael Agapow <agapow@bbsrc.ac.uk>"
30 __version__ = "0.6.6"
31
32
33 ### IMPORTS ###
34
35 import re
36 import pdb
37
38 try:
39     locale.setlocale (locale.LC_ALL, '')
40 except:
41     pass
42
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 ###
52
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'
60
61 SHOWNOTES_OPTIONS = [
62     SHOWNOTES_FALSE,
63     SHOWNOTES_TRUE,
64     SHOWNOTES_ONLY,
65     SHOWNOTES_LEFT,
66     SHOWNOTES_RIGHT,
67     SHOWNOTES_TOP,
68     SHOWNOTES_BOTTOM,
69 ]
70
71 HILITE_OPTIONS = {
72     'python':   'python',
73     'guess':    'guess',
74     'c++':      'cpp',
75 }
76
77 BEAMER_SPEC =   (
78     'Beamer options',
79     'These are derived almost entirely from the LaTeX2e options',
80     tuple (
81         [
82             (
83                 'Specify theme.',
84                 ['--theme'],
85                 {'default': 'Warsaw', }
86             ),
87             (
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, }
93             ),
94             (
95                 'Default for whether or not to pass the fragile option to '
96                 'the beamber frames (slides).',
97                 ['--fragile-default'],
98                 {'default': True, }
99             ),
100             
101             (
102                 'Center figures.  All includegraphics statements will be put '
103                 'inside center environments.',
104                 ['--centerfigs'],
105                 {'default': True, }
106             ),
107             (
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': '', }
113             ),
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 ##             ),
120             (
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'],
126                 {
127                     'action':    "store",
128                     'type':      'choice',
129                     'dest':      'shownotes',
130                     'choices':   SHOWNOTES_OPTIONS,
131                     'default':   SHOWNOTES_FALSE,
132                 }
133             ),
134             # should the pygments highlighter be used for codeblocks?
135             (
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'],
141                 {
142                     'action':    "store_true",
143                     'dest':      'cb_use_pygments',
144                     'default':   False,
145                 }
146             ),
147             # replace tabs inside codeblocks?
148             (
149                 "Replace the leading tabs in codeblocks with spaces.",
150                 ['--codeblocks-replace-tabs'],
151                 {
152                     'action':    'store',
153                     'type':      int,
154                     'dest':      'cb_replace_tabs',
155                     'default':   0,
156                 }
157             ),
158             # what language the codeblock is if not specified
159             (
160                 "The default language to hilight code blocks as. ",
161                 ['--codeblocks-default-language'],
162                 {
163                     'action':    'store',
164                     'type':      'choice',
165                     'dest':      'cb_default_lang',
166                     'choices':   HILITE_OPTIONS.values(),
167                     'default':   'guess',
168                 }
169             ),
170         ] + list (Latex2eWriter.settings_spec[2][2:])
171     ),
172 )
173
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)
179 }
180
181 BEAMER_DEFAULT_OVERRIDES = {'use_latex_docinfo': 1}
182
183
184 bool_strs = ['false','true','0','1']
185 bool_vals = [False, True, False, True]
186 bool_dict = dict (zip (bool_strs, bool_vals))
187
188 PreambleCmds.documenttitle = r"""
189 %% Document title
190 \title[%s]{%s}
191 \author[%s]{%s}
192 \date{%s}
193 \maketitle
194 """
195
196 docinfo_w_institute = r"""
197 %% Document title
198 \title[%s]{%s}
199 \author[%s]{%s}
200 \date{%s}
201 \institute{%s}
202 \maketitle
203 """
204
205 ### IMPLEMENTATION ###
206
207 ### UTILS
208
209 LEADING_SPACE_RE = re.compile ('^ +')
210
211 def adjust_indent_spaces (strn, orig_width=8, new_width=3):
212     """
213     Adjust the leading space on a string so as to change the indent width.
214
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.
222
223     :Returns:
224         The original string re-indented.
225
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.
230
231     For example::
232
233         >>> adjust_indent_spaces ('        foo')
234         '   foo'
235         >>> adjust_indent_spaces ('      foo', orig_width=2, new_width=1)
236         '   foo'
237
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.
243
244     .. note::
245
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.
249
250     """
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
264
265
266 def index (seq, f, fail=None):
267     """
268     Return the index of the first item in seq where f(item) is True.
269
270     :Parameters:
271         seq
272             A sequence or iterable
273         f
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.
277
278     While this could be written in a neater fashion in Python 2.6, this method
279     maintains compatiability with earlier version.
280     """
281     for index in (i for i in xrange (len (seq)) if f (seq[i])):
282         return index
283     return fail
284
285
286 def node_has_class (node, classes):
287     """
288     Does the node have one of these classes?
289
290     :Parameters:
291         node
292             A docutils node
293         class
294             A class name or list of class names.
295
296     :Returns:
297         A boolean indicating membership.
298
299     A convenience function, largely for testing for the special class names
300     in containers.
301     """
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
311
312
313 def node_lang_class (node):
314     """
315     Extract a language specification from a node class names.
316
317     :Parameters:
318         node
319             A docutils node
320
321     :Returns:
322         A string giving a language abbreviation (e.g. 'py') or None if no
323         langauge is found.
324
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.
330     """
331     ## Main:
332     for cname in node['classes']:
333         if (cname.startswith ('lang-')):
334             return cname[5:]
335     return None
336
337
338 def wrap_children_in_columns (par_node, children, width=None):
339     """
340     Replace this node's children with columns containing the passed children.
341
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.
350
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.
354     """
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
370
371
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
382
383
384 def string_to_bool (stringin, default=True):
385     """
386     Turn a commandline arguement string into a boolean value.
387     """
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]
395
396
397 def highlight_code (text, lang):
398     """
399     Syntax-highlight source code using Pygments.
400
401     :Parameters:
402         text
403             The code to be formatted.
404         lang
405             The language of the source code.
406
407     :Returns:
408         A LaTeX formatted representation of the source code.
409
410     """
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))
418
419
420 def get_lexer (text, lang):
421     """
422     Return the Pygments lexer for parsing this sourcecode.
423
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).
432
433     :Returns:
434         A Pygments lexer.
435
436     """
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)
450
451
452
453 ### NODES ###
454 # Special nodes for marking up beamer layout
455
456 class columnset (nodes.container):
457     """
458     A group of columns to display on one slide.
459
460     Named as per docutils standards.
461     """
462     # NOTE: a simple container, has no attributes.
463
464
465 class column (nodes.container):
466     """
467     A single column, grouping content.
468
469     Named as per docutils standards.
470     """
471     # TODO: should really init width in a c'tor
472
473 class beamer_note (nodes.container):
474     """
475     Annotations for a beamer presentation.
476
477     Named as per docutils standards and to distinguish it from core docutils
478     node type.
479     """
480     pass
481
482
483 ### DIRECTIVES
484
485 class CodeBlockDirective (Directive):
486     """
487     Directive for a code block with special highlighting or line numbering
488     settings.
489
490     Unabashedly borrowed from the Sphinx source.
491     """
492     has_content = True
493     required_arguments = 0
494     optional_arguments = 1
495     final_argument_whitespace = False
496     option_spec = {
497         'linenos': directives.flag,
498     }
499
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]
513
514 for name in ['code-block', 'sourcecode']:
515     directives.register_directive (name, CodeBlockDirective)
516
517
518 class SimpleColsDirective (Directive):
519     """
520     A directive that wraps all contained nodes in beamer columns.
521
522     Accept 'width' as an optional argument for total width of contained
523     columns.
524     """
525     required_arguments = 0
526     optional_arguments = 1
527     final_argument_whitespace = True
528     has_content = True
529     option_spec = {'width': float}
530
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]
550
551 for name in ['r2b-simplecolumns', 'r2b_simplecolumns']:
552     directives.register_directive (name, SimpleColsDirective)
553
554
555 class ColumnSetDirective (Directive):
556     """
557     A directive that encloses explicit columns in a 'columns' environment.
558
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.
563
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.
567
568     """
569     required_arguments = 0
570     optional_arguments = 1
571     final_argument_whitespace = True
572     has_content = True
573     option_spec = {'width': float}
574
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)
598
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]
616
617 for name in ['r2b-columnset', 'r2b_columnset']:
618     directives.register_directive (name, ColumnSetDirective)
619
620
621 class ColumnDirective (Directive):
622     """
623     A directive to explicitly create an individual column.
624
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.
629     """
630     required_arguments = 0
631     optional_arguments = 1
632     final_argument_whitespace = True
633     has_content = True
634     option_spec = {'width': float}
635
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]
654
655 for name in ['r2b-column', 'r2b_column']:
656     directives.register_directive (name, ColumnDirective)
657
658
659 class NoteDirective (Directive):
660     """
661     A directive to include notes within a beamer presentation.
662
663     """
664     required_arguments = 0
665     optional_arguments = 0
666     final_argument_whitespace = True
667     has_content = True
668     option_spec = {}
669
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]
682
683 for name in ['r2b-note', 'r2b_note']:
684     directives.register_directive (name, NoteDirective)
685
686
687 class beamer_section (Directive):
688
689     required_arguments = 1
690     optional_arguments = 0
691     final_argument_whitespace = True
692     has_content = True
693
694     def run (self):
695         title = self.arguments[0]
696
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())
702
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]
712
713 for name in ['beamer_section', 'r2b-section', 'r2b_section']:
714     directives.register_directive (name, beamer_section)
715
716
717 ### WRITER
718
719 class BeamerTranslator (LaTeXTranslator):
720     """
721     A converter for docutils elements to beamer-flavoured latex.
722     """
723
724     def __init__ (self, document):
725         LaTeXTranslator.__init__ (self, document)
726
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
733
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'
743             ])
744
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',
754         ])
755
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(),
763             ])
764
765         # set appropriate header options for theming
766         theme = document.settings.theme
767         if theme:
768             self.head_prefix.append ('\\usetheme{%s}\n' % theme)
769
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)
796         self.head_prefix.append ('\\usepackage{xmpmulti}\n')
797
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(),
803             ])
804
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
814         self.in_note = False
815         self.frame_level = 0
816
817         # this fixes the hardcoded section titles in docutils 0.4
818         self.d_class = DocumentClass ('article')
819
820
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()
827         # c) PDF properties
828         self.pdfsetup.append(PreambleCmds.linking % (self.colorlinks,
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)
848
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')
887
888
889
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
914                 
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)
923         
924
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
938
939
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)
947
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')
972
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 )
1001
1002
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')
1007
1008
1009     ## def visit_Text (self, node):
1010     ##     self.out.append(self.encode(node.astext()))
1011
1012     def depart_Text(self, node):
1013         pass
1014
1015
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
1026
1027
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
1034         
1035
1036     def end_frametag (self):
1037         return '\\end{frame}\n'
1038
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)
1047
1048
1049     def bookmark (self, node):
1050         """I think beamer alread handles bookmarks well, so I
1051         don't want duplicates."""
1052         return ''
1053
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())
1059
1060
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)
1070
1071     def depart_title (self, node):
1072         if (self.section_level != self.frame_level+1):#1
1073             LaTeXTranslator.depart_title (self, node)
1074
1075
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)
1086
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' )
1094
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
1113
1114     def depart_codeblock (self, node):
1115         pass
1116
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)
1131
1132
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
1144
1145
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' )
1153
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)
1159
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))
1178             
1179
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' )
1185
1186
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)
1204
1205
1206 ##     def visit_docinfo (self, node):
1207 ##         """
1208 ##         Docinfo is ignored for Beamer documents.
1209 ##         """
1210 ##         pass
1211
1212 ##     def depart_docinfo (self, node):
1213 ##         # see visit_docinfo
1214 ##         pass
1215
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')
1221
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')
1226
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)
1231
1232     def depart_column (self, node):
1233         self.in_column = False
1234         self.out.append ('\n')
1235
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')
1240
1241     def depart_beamer_note (self, node):
1242         self.in_note = False
1243         self.out.append ('}\n')
1244
1245     def visit_container (self, node):
1246         """
1247         Handle containers with 'special' names, ignore the rest.
1248         """
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)
1260
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)
1269
1270
1271     # Convert strong from \textbf to \alert
1272     def visit_strong(self, node):
1273         self.out.append('\\alert{')
1274         if node['classes']:
1275             self.visit_inline(node)
1276
1277     def depart_strong(self, node):
1278         if node['classes']:
1279             self.depart_inline(node)
1280         self.out.append('}')
1281
1282
1283 class BeamerWriter (Latex2eWriter):
1284         """
1285         A docutils writer that produces Beamer-flavoured LaTeX.
1286         """
1287         settings_spec = BEAMER_SPEC
1288         settings_default_overrides = BEAMER_DEFAULT_OVERRIDES
1289         def __init__(self):
1290             self.settings_defaults.update(BEAMER_DEFAULTS)
1291             Latex2eWriter.__init__(self)
1292             self.translator_class = BeamerTranslator
1293
1294
1295 ### TEST & DEBUG ###
1296 # TODO: should really move to a test file or dir
1297
1298 def test_with_file (fpath, args=[]):
1299     """
1300     Call rst2beamer on the given file with the given args.
1301
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
1304     functions to do so.
1305     """
1306     return publish_cmdline (writer=BeamerWriter(), argv=args+[fpath])
1307
1308
1309 ### MAIN ###
1310
1311 def main ():
1312     description = (
1313         "Generates Beamer-flavoured LaTeX for PDF-based presentations." +
1314          default_description)
1315     publish_cmdline (writer=BeamerWriter(), description=description)
1316
1317
1318 if __name__ == '__main__':
1319     main()
1320
1321
1322 ### END ###
1323