diff options
Diffstat (limited to 'presentty/rst.py')
-rw-r--r-- | presentty/rst.py | 493 |
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 | |||
16 | import os | ||
17 | import re | ||
18 | import docutils | ||
19 | import docutils.frontend | ||
20 | import docutils.parsers.rst | ||
21 | import docutils.nodes | ||
22 | import cStringIO as StringIO | ||
23 | |||
24 | import urwid | ||
25 | |||
26 | import slide | ||
27 | import transition as transition_mod | ||
28 | import image | ||
29 | import ansiparser | ||
30 | import text | ||
31 | |||
32 | try: | ||
33 | import PIL | ||
34 | import PIL.Image | ||
35 | except ImportError: | ||
36 | PIL = None | ||
37 | |||
38 | DEFAULT_TRANSITION = 'dissolve' | ||
39 | DEFAULT_TRANSITION_DURATION = 0.4 | ||
40 | |||
41 | class 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 | |||
64 | class 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 | |||
352 | class 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 | |||
365 | class 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 | |||
378 | class 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 | |||
388 | class 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 | |||
398 | class HideTitleDirective(docutils.parsers.rst.Directive): | ||
399 | has_content = False | ||
400 | |||
401 | def run(self): | ||
402 | node = hidetitle() | ||
403 | return [node] | ||
404 | |||
405 | class transition(docutils.nodes.Special, docutils.nodes.Invisible, | ||
406 | docutils.nodes.Element): | ||
407 | pass | ||
408 | |||
409 | class ansi(docutils.nodes.General, docutils.nodes.Inline, | ||
410 | docutils.nodes.Element): | ||
411 | pass | ||
412 | |||
413 | class figlet(docutils.nodes.General, docutils.nodes.Inline, | ||
414 | docutils.nodes.Element): | ||
415 | pass | ||
416 | |||
417 | class cowsay(docutils.nodes.General, docutils.nodes.Inline, | ||
418 | docutils.nodes.Element): | ||
419 | pass | ||
420 | |||
421 | class hidetitle(docutils.nodes.Special, docutils.nodes.Invisible, | ||
422 | docutils.nodes.Element): | ||
423 | pass | ||
424 | |||
425 | class 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 | |||
457 | def 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 | |||
492 | if __name__ == '__main__': | ||
493 | main() | ||