summaryrefslogtreecommitdiffstats
path: root/presentty/rst.py
diff options
context:
space:
mode:
Diffstat (limited to 'presentty/rst.py')
-rw-r--r--presentty/rst.py493
1 files changed, 493 insertions, 0 deletions
diff --git a/presentty/rst.py b/presentty/rst.py
new file mode 100644
index 0000000..41b3f97
--- /dev/null
+++ b/presentty/rst.py
@@ -0,0 +1,493 @@
+# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import docutils
+import docutils.frontend
+import docutils.parsers.rst
+import docutils.nodes
+import cStringIO as StringIO
+
+import urwid
+
+import slide
+import transition as transition_mod
+import image
+import ansiparser
+import text
+
+try:
+ import PIL
+ import PIL.Image
+except ImportError:
+ PIL = None
+
+DEFAULT_TRANSITION = 'dissolve'
+DEFAULT_TRANSITION_DURATION = 0.4
+
+class TextAccumulator(object):
+ def __init__(self):
+ self.text = []
+
+ def append(self, text):
+ self.text.append(text)
+
+ def getFormattedText(self):
+ return self.text
+
+ wsre = re.compile('\s+')
+
+ def getFlowedText(self):
+ ret = []
+ for part in self.text:
+ if isinstance(part, tuple):
+ ret.append((part[0], self.wsre.sub(u' ', part[1])))
+ else:
+ ret.append(self.wsre.sub(u' ', part))
+ if not ret:
+ return u''
+ return ret
+
+class UrwidTranslator(docutils.nodes.GenericNodeVisitor):
+ transition_map = {'dissolve': transition_mod.DissolveTransition,
+ 'cut': transition_mod.CutTransition,
+ 'pan': transition_mod.PanTransition,
+ }
+
+ def __init__(self, document, palette, hinter=None, basedir='.'):
+ docutils.nodes.GenericNodeVisitor.__init__(self, document)
+ self.program = []
+ self.stack = []
+ self.default_transition = self._make_transition(
+ DEFAULT_TRANSITION,
+ DEFAULT_TRANSITION_DURATION)
+ self.transition = self.default_transition
+ self.attr = []
+ self.table_columns = []
+ self.table_column = []
+ self.progressives = []
+ self.palette = palette
+ self.hinter = hinter
+ self.basedir = basedir
+ self.slide = None
+ self.default_hide_title = False
+ self.hide_title = self.default_hide_title
+
+ def _make_transition(self, name, duration):
+ tr = self.transition_map[name]
+ return tr(duration)
+
+ def default_visit(self, node):
+ """Override for generic, uniform traversals."""
+ pass
+
+ def default_departure(self, node):
+ """Override for generic, uniform traversals."""
+ pass
+
+ def _append(self, node, widget, *args, **kw):
+ if self.stack:
+ if 'handout' in node.get('classes'):
+ if self.handout_pile not in self.stack:
+ container = self.handout_pile
+ else:
+ # If the handout pile is in the stack, then ignore
+ # this class -- it has probably needlessly been
+ # applied to something deeper in the stack. The
+ # thing further up will end up in the handout.
+ container = self.stack[-1]
+ else:
+ container = self.stack[-1]
+ container.contents.append((widget, container.options(*args, **kw)))
+
+ def styled(self, style, text):
+ if style in self.palette:
+ return (self.palette[style], text)
+ return text
+
+ def visit_transition(self, node):
+ name = node['name']
+ duration = node.get('duration', DEFAULT_TRANSITION_DURATION)
+ self.transition = self._make_transition(name, duration)
+
+ def depart_transition(self, node):
+ pass
+
+ def visit_hidetitle(self, node):
+ if self.slide:
+ self.hide_title = True
+ else:
+ self.default_hide_title = True
+
+ def depart_hidetitle(self, node):
+ pass
+
+ def visit_system_message(self, node):
+ #print node.astext()
+ raise docutils.nodes.SkipNode()
+
+ def visit_section(self, node):
+ self.hide_title = self.default_hide_title
+ self.transition = self.default_transition
+ title_pile = slide.SlidePile([])
+ title_pad = slide.SlidePadding(title_pile,
+ align='center', width='pack')
+
+ main_pile = slide.SlidePile([])
+ main_pad = slide.SlidePadding(main_pile, align='center', width='pack')
+ outer_pile = slide.SlidePile([
+ ('pack', title_pad),
+ ('pack', main_pad),
+ ])
+ s = slide.UrwidSlide(u'', self.transition, outer_pile,
+ self.palette['_default'])
+ self.slide = s
+ self.stack.append(main_pile)
+ self.title_pile = title_pile
+
+ pile = slide.SlidePile([])
+ s = slide.Handout(pile, self.palette['_default'])
+ self.handout = s
+ self.handout_pile = pile
+ self.slide.handout = s
+
+ def depart_section(self, node):
+ self.slide.transition = self.transition
+ if self.hide_title:
+ self.title_pile.contents[:] = []
+ self.program.append(self.slide)
+ self.stack.pop()
+
+ def visit_block_quote(self, node):
+ self.stack.append(slide.SlidePile([]))
+
+ def depart_block_quote(self, node):
+ pile = self.stack.pop()
+ pad = slide.SlidePadding(pile, left=2)
+ self._append(node, pad, 'pack')
+
+ def visit_list_item(self, node):
+ self.stack.append(slide.SlidePile([]))
+
+ def depart_list_item(self, node):
+ pile = self.stack.pop()
+ bullet = urwid.Text(u'* ')
+ cols = slide.SlideColumns([])
+ cols.contents.append((bullet, cols.options('pack')))
+ cols.contents.append((pile, cols.options('weight', 1)))
+ if self.progressives:
+ cols = urwid.AttrMap(cols, self.palette['progressive'])
+ self.progressives[-1].append(cols)
+ self._append(node, cols, 'pack')
+
+ def visit_tgroup(self, node):
+ self.table_columns.append([])
+ self.stack.append(slide.SlidePile([]))
+
+ def visit_colspec(self, node):
+ self.table_columns[-1].append(node['colwidth'])
+
+ def visit_row(self, node):
+ self.stack.append(slide.SlideColumns([], dividechars=1))
+ self.table_column.append(0)
+
+ def depart_row(self, node):
+ self.table_column.pop()
+ cols = self.stack.pop()
+ self._append(node, cols, 'pack')
+
+ def visit_thead(self, node):
+ pass
+
+ def depart_thead(self, node):
+ cols = slide.SlideColumns([], dividechars=1)
+ for width in self.table_columns[-1]:
+ cols.contents.append((urwid.Text(u'='*width),
+ cols.options('given', width)))
+ self._append(node, cols, 'pack')
+
+ def visit_entry(self, node):
+ self.stack.append(slide.SlidePile([]))
+
+ def depart_entry(self, node):
+ colindex = self.table_column[-1]
+ self.table_column[-1] = colindex + 1
+ pile = self.stack.pop()
+ self._append(node, pile, 'given', self.table_columns[-1][colindex])
+
+ def depart_tgroup(self, node):
+ self.table_columns.pop()
+ pile = self.stack.pop()
+ self._append(node, pile, 'pack')
+
+ def visit_textelement(self, node):
+ self.stack.append(TextAccumulator())
+
+ visit_paragraph = visit_textelement
+
+ def depart_paragraph(self, node):
+ text = self.stack.pop()
+ self._append(node, urwid.Text(text.getFlowedText()), 'pack')
+
+ visit_literal_block = visit_textelement
+
+ def depart_literal_block(self, node):
+ text = self.stack.pop()
+ text = urwid.Text(text.getFormattedText(), wrap='clip')
+ pad = slide.SlidePadding(text, width='pack')
+ self._append(node, pad, 'pack')
+
+ visit_line = visit_textelement
+
+ def depart_line(self, node):
+ text = self.stack.pop()
+ self._append(node, urwid.Text(text.getFormattedText(), wrap='clip'),
+ 'pack')
+
+ visit_title = visit_textelement
+
+ def depart_title(self, node):
+ text = self.stack.pop()
+ self.slide.title = node.astext()
+ widget = urwid.Text(self.styled('title', text.getFlowedText()),
+ align='center')
+ self.title_pile.contents.append(
+ (widget, self.title_pile.options('pack')))
+
+ def visit_Text(self, node):
+ pass
+
+ def depart_Text(self, node):
+ if self.stack and isinstance(self.stack[-1], TextAccumulator):
+ if self.attr:
+ t = (self.attr[-1], node.astext())
+ else:
+ t = node.astext()
+ self.stack[-1].append(t)
+
+ def visit_emphasis(self, node):
+ self.attr.append(self.palette['emphasis'])
+
+ def depart_emphasis(self, node):
+ self.attr.pop()
+
+ def visit_inline(self, node):
+ cls = node.get('classes')
+ if not cls:
+ raise docutils.nodes.SkipDeparture()
+ cls = [x for x in cls if x != 'literal']
+ for length in range(len(cls), 0, -1):
+ clsname = '-'.join(cls[:length])
+ if clsname in self.palette:
+ self.attr.append(self.palette[clsname])
+ return
+ raise docutils.nodes.SkipDeparture()
+
+ def depart_inline(self, node):
+ self.attr.pop()
+
+ def visit_image(self, node):
+ if not PIL:
+ return
+ uri = node['uri']
+ fn = os.path.join(self.basedir, uri)
+ w = image.ANSIImage(fn, self.hinter)
+ self._append(node, w, 'pack')
+
+ def visit_ansi(self, node):
+ interval = node.get('interval', 0.5)
+ oneshot = node.get('oneshot', False)
+ animation = slide.AnimatedText(interval, oneshot)
+ for name in node['names']:
+ p = ansiparser.ANSIParser()
+ fn = os.path.join(self.basedir, name)
+ data = unicode(open(fn).read(), 'utf8')
+ text = p.parse(data)
+ animation.addFrame(text)
+ self.slide.animations.append(animation)
+ self._append(node, animation, 'pack')
+
+ def depart_ansi(self, node):
+ pass
+
+ def visit_figlet(self, node):
+ figlet = text.FigletText(node['text'])
+ self._append(node, figlet, 'pack')
+
+ def depart_figlet(self, node):
+ pass
+
+ def visit_cowsay(self, node):
+ cowsay = text.CowsayText(node['text'])
+ self._append(node, cowsay, 'pack')
+
+ def depart_cowsay(self, node):
+ pass
+
+ def visit_container(self, node):
+ self.stack.append(slide.SlidePile([]))
+ if 'progressive' in node.get('classes'):
+ self.progressives.append(self.slide.progressives)
+ self.slide.progressive_attr = self.palette['progressive']
+
+ def depart_container(self, node):
+ pile = self.stack.pop()
+ self._append(node, pile, 'pack')
+ if 'progressive' in node.get('classes'):
+ self.progressives.pop()
+
+class TransitionDirective(docutils.parsers.rst.Directive):
+ required_arguments = 1
+ option_spec = {'duration': float}
+ has_content = False
+
+ def run(self):
+ args = {'name': self.arguments[0]}
+ duration = self.options.get('duration')
+ if duration:
+ args['duration'] = duration
+ node = transition(**args)
+ return [node]
+
+class ANSIDirective(docutils.parsers.rst.Directive):
+ required_arguments = 1
+ final_argument_whitespace = True
+ option_spec = {'interval': float,
+ 'oneshot': bool}
+ has_content = False
+
+ def run(self):
+ args = {'names': self.arguments[0].split()}
+ args.update(self.options)
+ node = ansi(**args)
+ return [node]
+
+class FigletDirective(docutils.parsers.rst.Directive):
+ required_arguments = 1
+ has_content = False
+ final_argument_whitespace = True
+
+ def run(self):
+ args = {'text': self.arguments[0]}
+ node = figlet(**args)
+ return [node]
+
+class CowsayDirective(docutils.parsers.rst.Directive):
+ required_arguments = 1
+ has_content = False
+ final_argument_whitespace = True
+
+ def run(self):
+ args = {'text': self.arguments[0]}
+ node = cowsay(**args)
+ return [node]
+
+class HideTitleDirective(docutils.parsers.rst.Directive):
+ has_content = False
+
+ def run(self):
+ node = hidetitle()
+ return [node]
+
+class transition(docutils.nodes.Special, docutils.nodes.Invisible,
+ docutils.nodes.Element):
+ pass
+
+class ansi(docutils.nodes.General, docutils.nodes.Inline,
+ docutils.nodes.Element):
+ pass
+
+class figlet(docutils.nodes.General, docutils.nodes.Inline,
+ docutils.nodes.Element):
+ pass
+
+class cowsay(docutils.nodes.General, docutils.nodes.Inline,
+ docutils.nodes.Element):
+ pass
+
+class hidetitle(docutils.nodes.Special, docutils.nodes.Invisible,
+ docutils.nodes.Element):
+ pass
+
+class PresentationParser(object):
+ def __init__(self, palette, hinter=None):
+ docutils.parsers.rst.directives.register_directive(
+ 'transition', TransitionDirective)
+ docutils.parsers.rst.directives.register_directive(
+ 'ansi', ANSIDirective)
+ docutils.parsers.rst.directives.register_directive(
+ 'figlet', FigletDirective)
+ docutils.parsers.rst.directives.register_directive(
+ 'cowsay', CowsayDirective)
+ docutils.parsers.rst.directives.register_directive(
+ 'hidetitle', HideTitleDirective)
+ self.warnings = StringIO.StringIO()
+ self.settings = docutils.frontend.OptionParser(
+ components=(docutils.parsers.rst.Parser,),
+ defaults=dict(warning_stream=self.warnings)).get_default_values()
+ self.parser = docutils.parsers.rst.Parser()
+ self.palette = palette
+ self.hinter = hinter
+
+ def _parse(self, input, filename):
+ document = docutils.utils.new_document(filename, self.settings)
+ self.parser.parse(input, document)
+ visitor = UrwidTranslator(document, self.palette, self.hinter,
+ os.path.dirname(filename))
+ document.walkabout(visitor)
+ return document, visitor
+
+ def parse(self, input, filename='program'):
+ document, visitor = self._parse(input, filename)
+ return visitor.program
+
+def main():
+ import argparse
+ import palette
+
+ argp = argparse.ArgumentParser(description='Test RST parser')
+ argp.add_argument('file', help='presentation file (RST)')
+ argp.add_argument('slides', nargs='?', default=[],
+ help='slides to render')
+ argp.add_argument('--render', action='store_true',
+ help='Fully render a slide')
+ args = argp.parse_args()
+
+ parser = PresentationParser(palette.DARK_PALETTE)
+ document, visitor = parser._parse(open(args.file).read(), args.file)
+
+ slides = args.slides
+ if not slides:
+ slides = range(len(visitor.program))
+ slides = [int(x) for x in slides]
+
+ if not args.render:
+ print document.pformat()
+ for i in slides:
+ print '-'*80
+ s = visitor.program[i]
+ for line in s.render((80,25)).text:
+ print line
+ else:
+ screen = urwid.raw_display.Screen()
+ with screen.start():
+ for i in slides:
+ s = visitor.program[i]
+ screen.draw_screen((80,25), s.render((80,25)))
+ raw_input()
+
+if __name__ == '__main__':
+ main()