summaryrefslogtreecommitdiff
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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import os
17import re
18import docutils
19import docutils.frontend
20import docutils.parsers.rst
21import docutils.nodes
22import cStringIO as StringIO
23
24import urwid
25
26import slide
27import transition as transition_mod
28import image
29import ansiparser
30import text
31
32try:
33 import PIL
34 import PIL.Image
35except ImportError:
36 PIL = None
37
38DEFAULT_TRANSITION = 'dissolve'
39DEFAULT_TRANSITION_DURATION = 0.4
40
41class TextAccumulator(object):
42 def __init__(self):
43 self.text = []
44
45 def append(self, text):
46 self.text.append(text)
47
48 def getFormattedText(self):
49 return self.text
50
51 wsre = re.compile('\s+')
52
53 def getFlowedText(self):
54 ret = []
55 for part in self.text:
56 if isinstance(part, tuple):
57 ret.append((part[0], self.wsre.sub(u' ', part[1])))
58 else:
59 ret.append(self.wsre.sub(u' ', part))
60 if not ret:
61 return u''
62 return ret
63
64class UrwidTranslator(docutils.nodes.GenericNodeVisitor):
65 transition_map = {'dissolve': transition_mod.DissolveTransition,
66 'cut': transition_mod.CutTransition,
67 'pan': transition_mod.PanTransition,
68 }
69
70 def __init__(self, document, palette, hinter=None, basedir='.'):
71 docutils.nodes.GenericNodeVisitor.__init__(self, document)
72 self.program = []
73 self.stack = []
74 self.default_transition = self._make_transition(
75 DEFAULT_TRANSITION,
76 DEFAULT_TRANSITION_DURATION)
77 self.transition = self.default_transition
78 self.attr = []
79 self.table_columns = []
80 self.table_column = []
81 self.progressives = []
82 self.palette = palette
83 self.hinter = hinter
84 self.basedir = basedir
85 self.slide = None
86 self.default_hide_title = False
87 self.hide_title = self.default_hide_title
88
89 def _make_transition(self, name, duration):
90 tr = self.transition_map[name]
91 return tr(duration)
92
93 def default_visit(self, node):
94 """Override for generic, uniform traversals."""
95 pass
96
97 def default_departure(self, node):
98 """Override for generic, uniform traversals."""
99 pass
100
101 def _append(self, node, widget, *args, **kw):
102 if self.stack:
103 if 'handout' in node.get('classes'):
104 if self.handout_pile not in self.stack:
105 container = self.handout_pile
106 else:
107 # If the handout pile is in the stack, then ignore
108 # this class -- it has probably needlessly been
109 # applied to something deeper in the stack. The
110 # thing further up will end up in the handout.
111 container = self.stack[-1]
112 else:
113 container = self.stack[-1]
114 container.contents.append((widget, container.options(*args, **kw)))
115
116 def styled(self, style, text):
117 if style in self.palette:
118 return (self.palette[style], text)
119 return text
120
121 def visit_transition(self, node):
122 name = node['name']
123 duration = node.get('duration', DEFAULT_TRANSITION_DURATION)
124 self.transition = self._make_transition(name, duration)
125
126 def depart_transition(self, node):
127 pass
128
129 def visit_hidetitle(self, node):
130 if self.slide:
131 self.hide_title = True
132 else:
133 self.default_hide_title = True
134
135 def depart_hidetitle(self, node):
136 pass
137
138 def visit_system_message(self, node):
139 #print node.astext()
140 raise docutils.nodes.SkipNode()
141
142 def visit_section(self, node):
143 self.hide_title = self.default_hide_title
144 self.transition = self.default_transition
145 title_pile = slide.SlidePile([])
146 title_pad = slide.SlidePadding(title_pile,
147 align='center', width='pack')
148
149 main_pile = slide.SlidePile([])
150 main_pad = slide.SlidePadding(main_pile, align='center', width='pack')
151 outer_pile = slide.SlidePile([
152 ('pack', title_pad),
153 ('pack', main_pad),
154 ])
155 s = slide.UrwidSlide(u'', self.transition, outer_pile,
156 self.palette['_default'])
157 self.slide = s
158 self.stack.append(main_pile)
159 self.title_pile = title_pile
160
161 pile = slide.SlidePile([])
162 s = slide.Handout(pile, self.palette['_default'])
163 self.handout = s
164 self.handout_pile = pile
165 self.slide.handout = s
166
167 def depart_section(self, node):
168 self.slide.transition = self.transition
169 if self.hide_title:
170 self.title_pile.contents[:] = []
171 self.program.append(self.slide)
172 self.stack.pop()
173
174 def visit_block_quote(self, node):
175 self.stack.append(slide.SlidePile([]))
176
177 def depart_block_quote(self, node):
178 pile = self.stack.pop()
179 pad = slide.SlidePadding(pile, left=2)
180 self._append(node, pad, 'pack')
181
182 def visit_list_item(self, node):
183 self.stack.append(slide.SlidePile([]))
184
185 def depart_list_item(self, node):
186 pile = self.stack.pop()
187 bullet = urwid.Text(u'* ')
188 cols = slide.SlideColumns([])
189 cols.contents.append((bullet, cols.options('pack')))
190 cols.contents.append((pile, cols.options('weight', 1)))
191 if self.progressives:
192 cols = urwid.AttrMap(cols, self.palette['progressive'])
193 self.progressives[-1].append(cols)
194 self._append(node, cols, 'pack')
195
196 def visit_tgroup(self, node):
197 self.table_columns.append([])
198 self.stack.append(slide.SlidePile([]))
199
200 def visit_colspec(self, node):
201 self.table_columns[-1].append(node['colwidth'])
202
203 def visit_row(self, node):
204 self.stack.append(slide.SlideColumns([], dividechars=1))
205 self.table_column.append(0)
206
207 def depart_row(self, node):
208 self.table_column.pop()
209 cols = self.stack.pop()
210 self._append(node, cols, 'pack')
211
212 def visit_thead(self, node):
213 pass
214
215 def depart_thead(self, node):
216 cols = slide.SlideColumns([], dividechars=1)
217 for width in self.table_columns[-1]:
218 cols.contents.append((urwid.Text(u'='*width),
219 cols.options('given', width)))
220 self._append(node, cols, 'pack')
221
222 def visit_entry(self, node):
223 self.stack.append(slide.SlidePile([]))
224
225 def depart_entry(self, node):
226 colindex = self.table_column[-1]
227 self.table_column[-1] = colindex + 1
228 pile = self.stack.pop()
229 self._append(node, pile, 'given', self.table_columns[-1][colindex])
230
231 def depart_tgroup(self, node):
232 self.table_columns.pop()
233 pile = self.stack.pop()
234 self._append(node, pile, 'pack')
235
236 def visit_textelement(self, node):
237 self.stack.append(TextAccumulator())
238
239 visit_paragraph = visit_textelement
240
241 def depart_paragraph(self, node):
242 text = self.stack.pop()
243 self._append(node, urwid.Text(text.getFlowedText()), 'pack')
244
245 visit_literal_block = visit_textelement
246
247 def depart_literal_block(self, node):
248 text = self.stack.pop()
249 text = urwid.Text(text.getFormattedText(), wrap='clip')
250 pad = slide.SlidePadding(text, width='pack')
251 self._append(node, pad, 'pack')
252
253 visit_line = visit_textelement
254
255 def depart_line(self, node):
256 text = self.stack.pop()
257 self._append(node, urwid.Text(text.getFormattedText(), wrap='clip'),
258 'pack')
259
260 visit_title = visit_textelement
261
262 def depart_title(self, node):
263 text = self.stack.pop()
264 self.slide.title = node.astext()
265 widget = urwid.Text(self.styled('title', text.getFlowedText()),
266 align='center')
267 self.title_pile.contents.append(
268 (widget, self.title_pile.options('pack')))
269
270 def visit_Text(self, node):
271 pass
272
273 def depart_Text(self, node):
274 if self.stack and isinstance(self.stack[-1], TextAccumulator):
275 if self.attr:
276 t = (self.attr[-1], node.astext())
277 else:
278 t = node.astext()
279 self.stack[-1].append(t)
280
281 def visit_emphasis(self, node):
282 self.attr.append(self.palette['emphasis'])
283
284 def depart_emphasis(self, node):
285 self.attr.pop()
286
287 def visit_inline(self, node):
288 cls = node.get('classes')
289 if not cls:
290 raise docutils.nodes.SkipDeparture()
291 cls = [x for x in cls if x != 'literal']
292 for length in range(len(cls), 0, -1):
293 clsname = '-'.join(cls[:length])
294 if clsname in self.palette:
295 self.attr.append(self.palette[clsname])
296 return
297 raise docutils.nodes.SkipDeparture()
298
299 def depart_inline(self, node):
300 self.attr.pop()
301
302 def visit_image(self, node):
303 if not PIL:
304 return
305 uri = node['uri']
306 fn = os.path.join(self.basedir, uri)
307 w = image.ANSIImage(fn, self.hinter)
308 self._append(node, w, 'pack')
309
310 def visit_ansi(self, node):
311 interval = node.get('interval', 0.5)
312 oneshot = node.get('oneshot', False)
313 animation = slide.AnimatedText(interval, oneshot)
314 for name in node['names']:
315 p = ansiparser.ANSIParser()
316 fn = os.path.join(self.basedir, name)
317 data = unicode(open(fn).read(), 'utf8')
318 text = p.parse(data)
319 animation.addFrame(text)
320 self.slide.animations.append(animation)
321 self._append(node, animation, 'pack')
322
323 def depart_ansi(self, node):
324 pass
325
326 def visit_figlet(self, node):
327 figlet = text.FigletText(node['text'])
328 self._append(node, figlet, 'pack')
329
330 def depart_figlet(self, node):
331 pass
332
333 def visit_cowsay(self, node):
334 cowsay = text.CowsayText(node['text'])
335 self._append(node, cowsay, 'pack')
336
337 def depart_cowsay(self, node):
338 pass
339
340 def visit_container(self, node):
341 self.stack.append(slide.SlidePile([]))
342 if 'progressive' in node.get('classes'):
343 self.progressives.append(self.slide.progressives)
344 self.slide.progressive_attr = self.palette['progressive']
345
346 def depart_container(self, node):
347 pile = self.stack.pop()
348 self._append(node, pile, 'pack')
349 if 'progressive' in node.get('classes'):
350 self.progressives.pop()
351
352class TransitionDirective(docutils.parsers.rst.Directive):
353 required_arguments = 1
354 option_spec = {'duration': float}
355 has_content = False
356
357 def run(self):
358 args = {'name': self.arguments[0]}
359 duration = self.options.get('duration')
360 if duration:
361 args['duration'] = duration
362 node = transition(**args)
363 return [node]
364
365class ANSIDirective(docutils.parsers.rst.Directive):
366 required_arguments = 1
367 final_argument_whitespace = True
368 option_spec = {'interval': float,
369 'oneshot': bool}
370 has_content = False
371
372 def run(self):
373 args = {'names': self.arguments[0].split()}
374 args.update(self.options)
375 node = ansi(**args)
376 return [node]
377
378class FigletDirective(docutils.parsers.rst.Directive):
379 required_arguments = 1
380 has_content = False
381 final_argument_whitespace = True
382
383 def run(self):
384 args = {'text': self.arguments[0]}
385 node = figlet(**args)
386 return [node]
387
388class CowsayDirective(docutils.parsers.rst.Directive):
389 required_arguments = 1
390 has_content = False
391 final_argument_whitespace = True
392
393 def run(self):
394 args = {'text': self.arguments[0]}
395 node = cowsay(**args)
396 return [node]
397
398class HideTitleDirective(docutils.parsers.rst.Directive):
399 has_content = False
400
401 def run(self):
402 node = hidetitle()
403 return [node]
404
405class transition(docutils.nodes.Special, docutils.nodes.Invisible,
406 docutils.nodes.Element):
407 pass
408
409class ansi(docutils.nodes.General, docutils.nodes.Inline,
410 docutils.nodes.Element):
411 pass
412
413class figlet(docutils.nodes.General, docutils.nodes.Inline,
414 docutils.nodes.Element):
415 pass
416
417class cowsay(docutils.nodes.General, docutils.nodes.Inline,
418 docutils.nodes.Element):
419 pass
420
421class hidetitle(docutils.nodes.Special, docutils.nodes.Invisible,
422 docutils.nodes.Element):
423 pass
424
425class PresentationParser(object):
426 def __init__(self, palette, hinter=None):
427 docutils.parsers.rst.directives.register_directive(
428 'transition', TransitionDirective)
429 docutils.parsers.rst.directives.register_directive(
430 'ansi', ANSIDirective)
431 docutils.parsers.rst.directives.register_directive(
432 'figlet', FigletDirective)
433 docutils.parsers.rst.directives.register_directive(
434 'cowsay', CowsayDirective)
435 docutils.parsers.rst.directives.register_directive(
436 'hidetitle', HideTitleDirective)
437 self.warnings = StringIO.StringIO()
438 self.settings = docutils.frontend.OptionParser(
439 components=(docutils.parsers.rst.Parser,),
440 defaults=dict(warning_stream=self.warnings)).get_default_values()
441 self.parser = docutils.parsers.rst.Parser()
442 self.palette = palette
443 self.hinter = hinter
444
445 def _parse(self, input, filename):
446 document = docutils.utils.new_document(filename, self.settings)
447 self.parser.parse(input, document)
448 visitor = UrwidTranslator(document, self.palette, self.hinter,
449 os.path.dirname(filename))
450 document.walkabout(visitor)
451 return document, visitor
452
453 def parse(self, input, filename='program'):
454 document, visitor = self._parse(input, filename)
455 return visitor.program
456
457def main():
458 import argparse
459 import palette
460
461 argp = argparse.ArgumentParser(description='Test RST parser')
462 argp.add_argument('file', help='presentation file (RST)')
463 argp.add_argument('slides', nargs='?', default=[],
464 help='slides to render')
465 argp.add_argument('--render', action='store_true',
466 help='Fully render a slide')
467 args = argp.parse_args()
468
469 parser = PresentationParser(palette.DARK_PALETTE)
470 document, visitor = parser._parse(open(args.file).read(), args.file)
471
472 slides = args.slides
473 if not slides:
474 slides = range(len(visitor.program))
475 slides = [int(x) for x in slides]
476
477 if not args.render:
478 print document.pformat()
479 for i in slides:
480 print '-'*80
481 s = visitor.program[i]
482 for line in s.render((80,25)).text:
483 print line
484 else:
485 screen = urwid.raw_display.Screen()
486 with screen.start():
487 for i in slides:
488 s = visitor.program[i]
489 screen.draw_screen((80,25), s.render((80,25)))
490 raw_input()
491
492if __name__ == '__main__':
493 main()