#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2008-2009 Matt Harrison # Licensed under Apache License, Version 2.0 (current) import sys import os import docutils from docutils import io, writers, nodes from docutils.readers import standalone from docutils.core import Publisher, default_description, \ default_usage from docutils.parsers import rst import odplib.preso as preso S5_COLORS = dict( black='#000000', gray='#545454', silver='#c0c0c0', white='#ffffff', maroon='#b03060', red='#ff0000', magenta='#ff00ff', fuchsia='#ff00ff', # FIX pink='#ff1493', orange='#ffa500', yellow='#ffff00', lime='#32cd32', green='#00ff00', olive='#6b8e23', teal='#008080', cyan='#00ffff', aqua='#00ffff', # FIX blue='#0000ff', navy='#000080', purple='#a020f0' ) S5_SIZES = dict( huge='66pt', big='44pt', normal='28pt', small='22pt', tiny='18pt' ) class SyntaxHighlightCodeBlock(rst.Directive): required_arguments = 1 optional_arguments = 0 has_content = True # # See visit_literal_block for code that processes the node # created here. def run(self): language = self.arguments[0] code_block = nodes.literal_block(classes=["code-block", language], language=language) lines = self.content content = '\n'.join(lines) text_node = nodes.Text(content) code_block.append(text_node) # Mark this node for high-lighting so that visit_literal_block # will be able to hight-light those produced here and # *not* high-light regular literal blocks (:: in reST). code_block['hilight'] = True return [code_block] rst.directives.register_directive('code-block', SyntaxHighlightCodeBlock) class ImportNode(nodes.General, nodes.Inline, nodes.Element): pass class ImportSlideBlock(rst.Directive): required_arguments = 2 optional_arguments = 0 has_content = False node_class = ImportNode # # See visit_literal_block for code that processes the node # created here. def run(self): odp_path = self.arguments[0] page_num = self.arguments[1] node = ImportNode(odp_path=odp_path, page_num=page_num) return [node] rst.directives.register_directive('importslide', ImportSlideBlock) class Writer(writers.Writer): settings_spec = ( 'ODP Specific Options', # option group title None, # Description ( # options (help string, list of options, dictions of OptionParser.add_option dicts) ('Specify a template (.otp) to use for styling', ['--template-file'], {'action': 'store', 'dest': 'template_file'}), ('Specify a monospace font to use ("Courier New" default)', ['--mono-font'], {'action': 'store', 'dest': 'mono_font'}), ('Specify a normal font to use ("Arial" default)', ['--font'], {'action': 'store', 'dest': 'font'}), ('Specify pages to export (2,3,9-10)', ['--pages-to-output'], {'action': 'store', 'dest': 'pages_to_output'}) ) ) def __init__(self): writers.Writer.__init__(self) self.translator_class = ODPTranslator def translate(self): self.visitor = self.translator_class(self.document) self.document.walkabout(self.visitor) self.parts['whole'] = self.visitor.get_whole() self.output = self.parts['whole'] self.parts['encoding'] = self.document.settings.output_encoding self.parts['version'] = docutils.__version__ class ODPTranslator(nodes.GenericNodeVisitor): def __init__(self, document): nodes.GenericNodeVisitor.__init__(self, document) self.settings = document.settings self.preso = preso.Preso() if self.settings.pages_to_output: self.preso.limit_pages = num_string_to_list(self.settings.pages_to_output) if self.settings.mono_font: preso.MONO_FONT = self.settings.mono_font if self.settings.font: preso.NORMAL_FONT = self.settings.font self.in_node = {} # map of tagname to True if we are in/under this self._reset() def _reset(self): # state we keep track of self.cur_slide = None self.bullet_list = None self.bullet_depth = 0 self.footer = None def _init_slide(self, force=False): if force or self.cur_slide is None: self._reset() self.cur_slide = self.preso.add_slide() def at(self, nodename): """ shortcut for at/under this node """ return self.in_node.get(nodename, False) def get_whole(self): return self.preso.get_data(self.settings.template_file) def dispatch_visit(self, node): # Easier just to throw nodes I'm in in a dict, than keeping # state for each one count = self.in_node.setdefault(node.tagname, 0) self.in_node[node.tagname] += 1 nodes.GenericNodeVisitor.dispatch_visit(self, node) def dispatch_departure(self, node): self.in_node[node.tagname] -= 1 nodes.GenericNodeVisitor.dispatch_departure(self, node) def default_visit(self, node): if self.settings.report_level >= 3: print "ERR! NODE", node, node.tagname raise NotImplementedError('node is %r, tag is %s' % (node, node.tagname)) def default_departure(self, node): if self.settings.report_level >= 3: print "NODE", node, node.tagname raise NotImplementedError def _dumb_visit(self, node): pass _dumb_depart = _dumb_visit def visit_document(self, node): if self.settings.report_level >= 3: print "DOC", node depart_document = _dumb_depart def visit_title(self, node): if self.at('section') < 2: self._init_slide() if self.at('topic'): return elif self.at('sidebar'): return elif self.at('section') < 2: self.cur_slide.add_title_frame() def depart_title(self, node): if self.at('topic'): return elif self.at('sidebar'): return elif self.at('section') < 2: # not in a title element anymore self.cur_slide.cur_element = None else: pass def visit_Text(self, node): if self.bullet_list and not self.at('handout'): # !!!need to deal w/ bullets in handout self.bullet_list.write(node.astext()) elif self.at('footer'): self.footer.write(node.astext()) elif self.at('comment'): pass elif self.at('topic'): pass elif self.at('literal_block'): pass elif self.at('reference'): # FirstClown - if we have link text, we need to make sure the text # doesn't get any styles applied or it sometimes doesn't show. self.cur_slide.write(node.astext(), add_p_style=False, add_t_style=False) elif self.at('doctest_block'): pass elif self.at('field_name'): pass else: self.cur_slide.write(node.astext()) depart_Text = _dumb_depart def _push_handout(self, classes): if 'handout' in classes: self.in_node['handout'] = True self.cur_slide.push_element() if not self.cur_slide.notes_frame: self.cur_slide.add_notes_frame() else: self.cur_slide.cur_element = self.cur_slide.notes_frame self.cur_slide.insert_line_break += 1 self.cur_slide.insert_line_breaks() def visit_paragraph(self, node): classes = node.attributes.get('classes', []) self._push_handout(classes) ## if 'center' in classes: ## attribs = {'fo:text-align':'center', ## 'fo:margin-left':'1.2cm', ## 'fo:margin-right':'-.9cm', ## } ## style = preso.ParagraphStyle(**attribs) ## self.cur_slide.push_style(style) ## elif 'left' in classes: ## pass # default ## elif 'right' in classes: ## attribs = {'fo:text-align':'end', ## } ## style = preso.ParagraphStyle(**attribs) ## self.cur_slide.push_style(style) p_attribs = self._get_para_attribs(node) if p_attribs: self.cur_slide.push_style(preso.ParagraphStyle(**p_attribs)) # text styles attribs = self._get_text_attribs(node) if attribs: style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) if self.bullet_list: pass elif self.at('topic'): return elif self.at('block_quote'): return # block quote adds paragraph style elif self.at('doctest_block'): pass def depart_paragraph(self, node): # add newline if not self.at('list_item'): self.depart_line(node) classes = node.attributes.get('classes', []) if 'center' in classes or 'right' in classes: self.cur_slide.pop_node() if 'handout' in classes: self.in_node['handout'] = False self.cur_slide.pop_element() if self._get_text_attribs(node): # pop off text:span self.cur_slide.pop_node() self.cur_slide.pop_style() elif self.at('topic'): return elif self.at('block_quote'): return # block quote adds paragraph style else: self.cur_slide.parent_of('text:p') visit_definition = _dumb_visit depart_definition = _dumb_depart def visit_bullet_list(self, node): if self.at('topic'): return p_attribs = self._get_para_attribs(node) if p_attribs: self.cur_slide.push_style(preso.ParagraphStyle(**p_attribs)) attribs = self._get_text_attribs(node) if attribs: style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) classes = node.attributes.get('classes', []) if 'handout' in classes: self._push_handout(classes) self.bullet_depth += 1 if not self.bullet_list: # start a new list self.bullet_list = preso.OutlineList(self.cur_slide) self.cur_slide.add_list(self.bullet_list) else: # indent one more self.bullet_list.indent() if 'incremental' in node.attributes.get('classes', []): self.in_node['incremental'] = True def depart_bullet_list(self, node): if self.at('topic'): return classes = node.attributes.get('classes', []) if 'handout' in classes: self.in_node['handout'] = False self.cur_slide.pop_element() self.bullet_depth -= 1 if self.bullet_depth == 0: # done with list self.bullet_list = None self.cur_slide.pop_element() self.cur_slide.insert_line_break += 1 else: self.bullet_list.dedent() if 'incremental' in node.attributes.get('classes', []): self.in_node['incremental'] = False visit_definition_list = visit_bullet_list depart_definition_list = depart_bullet_list def visit_list_item(self, node): if self.at('topic'): return if self.at('incremental'): self.cur_slide.start_animation(preso.Animation()) self.bullet_list.new_item() def depart_list_item(self, node): if self.at('topic'): return if self.at('incremental'): self.cur_slide.end_animation() visit_definition_list_item = visit_list_item depart_definition_list_item = depart_list_item visit_decoration = _dumb_visit depart_decoration = _dumb_depart def visit_footer(self, node): self.footer = preso.Footer(self.cur_slide) def depart_footer(self, node): self.preso.add_footer(self.footer) self.footer = None visit_docinfo = _dumb_visit depart_docinfo = _dumb_depart # bibliographic elements def visit_author(self, node): self.visit_attribution(node) def depart_author(self, node): self.depart_line(node) # add new-line self.depart_attribution(node) visit_copyright = visit_author depart_copyright = depart_author def visit_date(self, node): self.visit_attribution(node) def depart_date(self, node): self.depart_line(node) # add new-line self.depart_attribution(node) def visit_field(self, node): pass def depart_field(self, node): pass def visit_field_name(self, node): pass # maybe put this somewhere def depart_field_name(self, node): pass def visit_field_body(self, node): self.visit_attribution(node) def depart_field_body(self, node): self.depart_attribution(node) def visit_comment(self, node): pass def depart_comment(self, node): pass visit_topic = _dumb_visit depart_topic = _dumb_depart def visit_reference(self, node): """ Yahoo corp """ if node.has_key('refid'): return elif self.at('topic'): return elif self.at('field_body'): self.visit_attribution(node) self.cur_slide.push_pending_node('text:a', {'xlink:href': '%s' % node['refuri'], 'xlink:type': 'simple'}) self.cur_slide.write(node.astext()) else: # needs to be in a p # need to hack a bit, since .write usually inserts text:p and text:span if not self.cur_slide.cur_element or not self.cur_slide.cur_element._in_p(): #self.cur_slide.add_node('text:p', {}) # pyohio code # we write an empty string since write() creates the paragraph # we need, with the style needed to reset from a possible Title # P0 style. This was most apparent when a link was first word # in a section after a title. self.cur_slide.write("") self.cur_slide.add_node('text:a', attrib={'xlink:href': '%s' % node['refuri'], 'xlink:type': 'simple'}) def depart_reference(self, node): if node.has_key('refid'): return elif self.at('topic'): return elif self.at('field_body'): self.depart_attribution(node) self.cur_slide.parent_of('text:a') else: self.cur_slide.parent_of('text:a') def visit_target(self,node): # Skip the target element since the of the target is # responsible for writing out the content pass def depart_target(self, node): pass def visit_container(self, node): classes = node.attributes.get('classes', []) self._push_handout(classes) def depart_container(self, node): if self.in_node.get('handout', False): self.cur_slide.pop_element() self.in_node['handout'] = False visit_substitution_definition = _dumb_visit depart_substitution_definition = _dumb_depart def visit_section(self, node): # first page has no section if self.at('section') < 2: # don't create slide for subsections self._init_slide(force=True) def depart_section(self, node): if self.at('section') < 1: self._reset() def visit_transition(self, node): # hack to have titleless slides (transitions come in between sections) self._reset() self._init_slide(force=True) depart_transition = _dumb_depart def visit_literal(self, node): style = preso.TextStyle(**{ 'fo:font-family':preso.MONO_FONT, 'style:font-family-generic':"swiss", 'style:font-pitch':"fixed"}) self.cur_slide.push_style(style) def depart_literal(self, node): # pop off the text:span self.cur_slide.pop_style() self.cur_slide.pop_node() def visit_inline(self,node): attribs = self._get_text_attribs(node) if attribs: style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) if 'incremental' in node.attributes.get('classes', []): self.cur_slide.start_animation(preso.Animation()) def depart_inline(self, node): # pop off the text:span attribs = self._get_text_attribs(node) if attribs: self.cur_slide.pop_style() self.cur_slide.pop_node() if 'incremental' in node.attributes.get('classes', []): self.cur_slide.end_animation() def visit_emphasis(self, node): attribs = {'fo:font-style':'italic'} style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) def depart_emphasis(self, node): # pop off the text:span self.cur_slide.pop_style() self.cur_slide.pop_node() visit_title_reference = visit_emphasis depart_title_reference = depart_emphasis def visit_strong(self, node): attribs = {'fo:font-weight':'bold'} style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) def depart_strong(self, node): # pop off the text:span self.cur_slide.pop_style() self.cur_slide.pop_node() visit_term = visit_strong depart_term = depart_strong def visit_superscript(self, node): attribs = {'style:text-position':'super 58%'} style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) def depart_superscript(self, node): # pop off the text:span self.cur_slide.pop_style() self.cur_slide.pop_node() def visit_subscript(self, node): attribs = {'style:text-position':'sub 58%'} style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) def depart_subscript(self, node): # pop off the text:span self.cur_slide.pop_style() self.cur_slide.pop_node() def visit_block_quote(self, node): attribs = {'fo:text-align':'start', 'fo:margin-left':'1.2cm', 'fo:margin-right':'-.9cm', } style = preso.ParagraphStyle(**attribs) self.cur_slide.push_style(style) def depart_block_quote(self, node): # pop off the text:p self.cur_slide.pop_style() self.cur_slide.pop_node() def visit_attribution(self, node): # right justify attribs = {'fo:text-align':'end', 'fo:margin-right':'.9cm' } style = preso.ParagraphStyle(**attribs) self.cur_slide.push_style(style) # italics style = preso.TextStyle(**{'fo:font-size':S5_SIZES['small'], 'fo:font-style':'italic'}) self.cur_slide.push_style(style) def depart_attribution(self, node): # pop off the text:p and text:span self.cur_slide.pop_style() self.cur_slide.pop_node() self.cur_slide.pop_style() self.cur_slide.pop_node() def visit_line_block(self, node): #jump out of current paragraph self.cur_slide.parent_of('text:p') p_attribs = self._get_para_attribs(node) if p_attribs: self.cur_slide.push_style(preso.ParagraphStyle(**p_attribs)) else: # right justify P_attribs = {'fo:text-align':'end', 'fo:margin-right':'.9cm' } style = preso.ParagraphStyle(**p_attribs) self.cur_slide.push_style(preso.ParagraphStyle(**p_attribs)) #self.cur_slide.push_style(style) attribs = self._get_text_attribs(node) if attribs: style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) def depart_line_block(self, node): attribs = self._get_text_attribs(node) if attribs: #self.cur_slide.pop_style() self.cur_slide.pop_node() # pop off text:p self.cur_slide.pop_style() self.cur_slide.pop_node() def visit_line(self, node): pass def depart_line(self, node): self.cur_slide.insert_line_break += 1 self.cur_slide.insert_line_breaks() @preso.cwd_decorator def visit_image(self, node): classes = node.attributes.get('classes', []) source = node.attributes['uri'] p = preso.Picture(os.path.abspath(source), **node.attributes) self.cur_slide.add_picture(p) def depart_image(self, node): pass visit_figure = _dumb_visit depart_figure = _dumb_depart def visit_caption(self, node): #!!! fix pass def depart_caption(self, node): pass def visit_literal_block(self, node): attributes = node.attributes # left align style = preso.ParagraphStyle(**{'fo:text-align':'start'}) self.cur_slide.push_style(style) # text styles attribs = self._get_text_attribs(node) if attribs: style = preso.TextStyle(**attribs) self.cur_slide.push_style(style) if attributes['classes'] and 'code-block' in attributes['classes']: node_input = node.astext() language = node.attributes['language'] self.cur_slide.add_code(node_input, language) # insert a new line after self.cur_slide.insert_line_break += 1 else: style = preso.TextStyle(**{ 'fo:font-family':preso.MONO_FONT, 'style:font-family-generic':"swiss", 'style:font-pitch':"fixed"}) self.cur_slide.push_style(style) node_input = node.astext() chunks = node_input.split('\n') for chunk in chunks: self.cur_slide.write(chunk) self.cur_slide.insert_line_break += 1 self.cur_slide.insert_line_breaks() def depart_literal_block(self, node): # pop text-align self.cur_slide.pop_style() self.cur_slide.pop_node() attributes = node.attributes if attributes['classes'] and 'code-block' in attributes['classes']: pass else: self.cur_slide.pop_style() self.cur_slide.pop_node() def visit_footnote(self, node): # shift to bottom of page? pass def depart_footnote(self, node): pass def visit_footnote_reference(self, node): self.visit_superscript(node) self.cur_slide.write('[') def depart_footnote_reference(self, node): self.cur_slide.write(']') self.depart_superscript(node) def visit_label(self, node): # part of footnote if self.at('footnote'): self.cur_slide.write('[') def depart_label(self, node): if self.at('footnote'): self.cur_slide.write('] ') def visit_enumerated_list(self, node): if self.at('topic'): return self.bullet_depth += 1 if not self.bullet_list: self.bullet_list = preso.NumberList(self.cur_slide) self.cur_slide.add_list(self.bullet_list) else: # indent one more self.bullet_list.indent() if 'incremental' in node.attributes.get('classes', []): self.in_node['incremental'] = True def depart_enumerated_list(self, node): self.depart_bullet_list(node) def visit_doctest_block(self, node): node_input = node.astext() language = 'pycon' self.cur_slide.add_code(node_input, language) # insert a new line after self.cur_slide.insert_line_break += 1 def depart_doctest_block(self, node): pass def visit_importslide(self, node): pass def visit_table(self, node): table = preso.TableFrame(self.cur_slide) self.cur_slide.add_table(table) def depart_table(self, node): self.cur_slide.pop_element() def visit_row(self, node): self.cur_slide.cur_element.add_row() def depart_row(self, node): pass def visit_entry(self, node): self.cur_slide.cur_element.add_cell() def depart_entry(self, node): pass visit_tgroup = _dumb_visit depart_tgroup = _dumb_depart visit_colspec = _dumb_visit depart_colspec = _dumb_depart visit_thead = _dumb_visit depart_thead = _dumb_depart visit_tbody = _dumb_visit depart_tbody = _dumb_depart def visit_hint(self, node): return self._visit_hint(node, 'Hint') def _visit_hint(self, node, name): if self.cur_slide.text_frames: # should adjust width of other frame node = self.cur_slide.text_frames[-1].get_node() node.attrib['svg:width'] = '12.296cm' else: self.cur_slide.add_text_frame() self.cur_slide.push_element() #put the hint on the right side attrib = { 'presentation:style-name':'pr2', 'draw:layer':'layout', 'svg:width':'12.296cm', 'svg:height':'13.86cm', 'svg:x':'14.311cm', 'svg:y':'4.577cm', 'presentation:class':'subtitle' } self.cur_slide.add_text_frame(attrib) self.cur_slide.write(name) self.cur_slide.insert_line_break = 2 self.cur_slide.insert_line_breaks() def depart_hint(self, node): self.cur_slide.pop_element() def visit_sidebar(self, node): return self._visit_hint(node, 'Sidebar') depart_sidebar = depart_hint def _get_para_attribs(self, node): classes = node.attributes.get('classes', []) attribs = {} for c in classes: if c == 'center': attribs.update({'fo:text-align':'center', 'fo:margin-left':'1.2cm', 'fo:margin-right':'.9cm', }) elif c == 'right': attribs.update({'fo:text-align':'end', 'fo:margin-right':'.9cm' }) elif c == 'left': attribs.update({'fo:text-align':'start', 'fo:margin-left':'1.2cm', 'fo:margin-right':'5.9cm', }) #pass # default return attribs def _get_text_attribs(self, node): classes = node.attributes.get('classes', []) attribs = {} for c in classes: if c in S5_COLORS: attribs['fo:color'] = S5_COLORS[c] elif c in S5_SIZES: attribs['fo:font-size'] = S5_SIZES[c] return attribs def num_string_to_list(numstr): """ >>> num_string_to_list('2,5-7') [2, 5, 6, 7] >>> num_string_to_list('1') [1] """ nums = [] if ',' in numstr: comma_delim = numstr.split(',') for part in comma_delim: if '-' in part: start, end = [int(x) for x in part.split('-')] for num in range(start, end+1): nums.append(num) else: nums.append(int(part)) elif '-' in numstr: start, end = [int(x) for x in numstr.split('-')] for num in range(start, end+1): nums.append(num) else: nums.append(int(numstr)) return nums class BinaryFileOutput(io.FileOutput): """ A version of docutils.io.FileOutput which writes to a binary file. """ def open(self): try: self.destination = open(self.destination_path, 'wb') except IOError, error: if not self.handle_io_errors: raise print >>sys.stderr, '%s: %s' % (error.__class__.__name__, error) print >>sys.stderr, ('Unable to open destination file for writing ' '(%r). Exiting.' % self.destination_path) sys.exit(1) self.opened = 1 def main(prog_args): argv = None reader = standalone.Reader() reader_name = 'standalone' writer = Writer() writer_name = 'pseudoxml' parser = None parser_name = 'restructuredtext' settings = None settings_spec = None settings_overrides = None config_section = None enable_exit_status = 1 usage = default_usage publisher = Publisher(reader, parser, writer, settings, destination_class=BinaryFileOutput) publisher.set_components(reader_name, parser_name, writer_name) description = ('Generates OpenDocument/OpenOffice/ODF slides from ' 'standalone reStructuredText sources. ' + default_description) output = publisher.publish(argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) def _test(): import doctest doctest.testmod() if __name__ == "__main__": if '--doctest' in sys.argv: _test() else: sys.exit(main(sys.argv) or 0)