fix incorrect merge resolution from ab052d32c5 see: #5
[obnox/wiki2beamer.git] / code / wiki2beamer
1 #!/usr/bin/env python
2
3 # wiki2beamer
4 #
5 # (c) 2007-2008 Michael Rentzsch (http://www.repc.de)
6 # (c) 2009-2011 Michael Rentzsch (http://www.repc.de)
7 #               Kai Dietrich (mail@cleeus.de)
8 #
9 # Create latex beamer sources for multiple frames from a wiki-like code.
10 #
11 #
12 #     This file is part of wiki2beamer.
13 # wiki2beamer is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 2 of the License, or
16 # (at your option) any later version.
17 #
18 # wiki2beamer is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with wiki2beamer.  If not, see <http://www.gnu.org/licenses/>.
25 #
26 # Additional commits by:
27 #     Valentin Haenel <valentin.haenel@gmx.de>
28 #     Julius Plenz <julius@plenz.com>
29
30
31 import sys
32 import os
33 import re
34 import random
35 import optparse
36
37
38 VERSIONTAG = "0.9.5"
39 __version__= VERSIONTAG
40 __author__= "Michael Rentzsch, Kai Dietrich and others"
41
42 #python 2.4 compatability
43 if sys.version_info >= (2, 5):
44     import hashlib
45 else:
46     import md5
47 #python 2.4 compatability
48 def md5hex(string):
49     if sys.version_info >= (2, 5):
50         return hashlib.md5(string).hexdigest()
51     else:
52         dg = md5.md5()
53         dg.update(string)
54         return dg.hexdigest()
55
56 _redirected_stdout = None
57 _redirected_stderr = None
58 def pprint(string, file=sys.stdout, eol=True):
59     ''' portable version of print which directly writes into the given stream '''
60     if file == sys.stdout and _redirected_stdout is not None:
61         file = _redirected_stdout
62     if file == sys.stderr and _redirected_stderr is not None:
63         file = _redirected_stderr
64
65     file.write(string)
66     if eol:
67         file.write(os.linesep)
68     file.flush()
69
70 def mydebug(message):
71     """ print debug message to stderr """
72     pprint(message, file=sys.stderr)
73
74 def syntax_error(message, code):
75     pprint('syntax error: %s' % message, file=sys.stderr)
76     pprint('\tcode:\n%s' % code, file=sys.stderr)
77     sys.exit(-3)
78
79 class IncludeLoopException(Exception):
80     pass
81
82 lstbasicstyle=\
83 r"""{basic}{
84     captionpos=t,%
85     basicstyle=\footnotesize\ttfamily,%
86     numberstyle=\tiny,%
87     numbers=left,%
88     stepnumber=1,%
89     frame=single,%
90     showspaces=false,%
91     showstringspaces=false,%
92     showtabs=false,%
93     %
94     keywordstyle=\color{blue},%
95     identifierstyle=,%
96     commentstyle=\color{gray},%
97     stringstyle=\color{magenta}%
98 }"""
99
100 autotemplate = [
101     ('documentclass', '{beamer}'),
102     ('usepackage', '{listings}'),
103     ('usepackage', '{wasysym}'),
104     ('usepackage', '{graphicx}'),
105     ('date', '{\\today}'),
106     ('lstdefinestyle', lstbasicstyle),
107     ('titleframe', 'True'),
108 ]
109
110 nowikistartre = re.compile(r'^<\[\s*nowiki\s*\]')
111 nowikiendre = re.compile(r'^\[\s*nowiki\s*\]>')
112 codestartre = re.compile(r'^<\[\s*code\s*\]')
113 codeendre = re.compile(r'^\[\s*code\s*\]>')
114
115 # lazy initialisation cache for file content
116 _file_cache = {}
117
118 def add_lines_to_cache(filename, lines):
119     if not filename in _file_cache:
120         _file_cache[filename] = lines
121
122 def get_lines_from_cache(filename):
123     if filename in _file_cache:
124         return _file_cache[filename]
125     else:
126         lines = read_file_to_lines(filename)
127         _file_cache[filename] = lines
128         return lines
129
130 def clear_file_cache():
131     _file_cache.clear()
132
133 try:
134     from collections import OrderedDict as maybe_odict
135 except ImportError:
136     maybe_odict = dict
137
138 class w2bstate:
139     def __init__(self):
140         self.frame_opened = False
141         self.enum_item_level = ''
142         self.frame_header = ''
143         self.frame_footer = ''
144         self.next_frame_footer = ''
145         self.next_frame_header = ''
146         self.current_line = 0
147         self.autotemplate_opened = False
148         self.defverbs = maybe_odict()
149         self.code_pos = 0
150         return
151
152     def switch_to_next_frame(self):
153         self.frame_header = self.next_frame_header
154         self.frame_footer = self.next_frame_footer
155         return
156
157 def escape_resub(string):
158     p = re.compile(r"\\")
159     return p.sub(r"\\\\", string)
160
161
162 def transform_itemenums(string, state):
163     """handle itemizations/enumerations"""
164     preamble = ""   # for enumeration/itemize environment commands
165
166     # handle itemizing/enumerations
167     p = re.compile("^([\*\#]+).*$")
168     m = p.match(string)
169     if m is None:
170         my_enum_item_level = ""
171     else:
172         my_enum_item_level = m.group(1)
173
174     # trivial: old level = new level
175     if my_enum_item_level == state.enum_item_level:
176         pass
177     else:
178         # find common part
179         common = -1
180         while (len(state.enum_item_level) > common + 1) and \
181                 (len(my_enum_item_level) > common + 1) and \
182                 (state.enum_item_level[common+1] == my_enum_item_level[common+1]):
183             common = common + 1
184
185         # close enum_item_level environments from back to front
186         for i in range(len(state.enum_item_level)-1, common, -1):
187             if state.enum_item_level[i] == "*":
188                 preamble = preamble + "\\end{itemize}\n"
189             elif state.enum_item_level[i] == "#":
190                 preamble = preamble + "\\end{enumerate}\n"
191
192         # open my_enum_item_level environments from front to back
193         for i in range(common+1, len(my_enum_item_level)):
194             if my_enum_item_level[i] == "*":
195                 preamble = preamble + "\\begin{itemize}\n"
196             elif my_enum_item_level[i] == "#":
197                 preamble = preamble + "\\begin{enumerate}\n"
198     state.enum_item_level = my_enum_item_level
199
200     # now, substitute item markers
201     p = re.compile("^([\*\#]+)\s*(.*)$")
202     _string = p.sub(r"  \\item \2", string)
203     string = preamble + _string
204
205     return string
206
207 def transform_define_foothead(string, state):
208     """ header and footer definitions"""
209     p = re.compile("^@FRAMEHEADER=(.*)$", re.VERBOSE)
210     m = p.match(string)
211     if m is not None:
212         state.next_frame_header = m.group(1)
213         string = ""
214     p = re.compile("^@FRAMEFOOTER=(.*)$", re.VERBOSE)
215     m = p.match(string)
216     if m is not None:
217         state.next_frame_footer = m.group(1)
218         string = ""
219     return string
220
221 def transform_detect_manual_frameclose(string, state):
222     """ detect manual closing of frames """
223     p = re.compile(r"\[\s*frame\s*\]>")
224     if state.frame_opened:
225         if p.match(string) is not None:
226             state.frame_opened = False
227     return string
228
229 def get_frame_closing(state):
230     return " %s \n\\end{frame}\n" % state.frame_footer
231
232 def transform_h4_to_frame(string, state):
233     """headings (3) to frames"""
234     frame_opening = r"\\begin{frame}\2\n \\frametitle{\1}\n %s \n" % escape_resub(state.next_frame_header)
235     frame_closing = escape_resub(get_frame_closing(state))
236
237     p = re.compile("^!?====\s*(.*?)\s*====(.*)", re.VERBOSE)
238     if not state.frame_opened:
239         _string = p.sub(frame_opening, string)
240     else:
241         _string = p.sub(frame_closing + frame_opening, string)
242
243     if string != _string:
244         state.frame_opened = True
245         state.switch_to_next_frame()
246
247     return _string
248
249 def transform_h3_to_subsec(string, state):
250     """ headings (2) to subsections """
251     frame_closing = escape_resub(get_frame_closing(state))
252     subsec_opening = r"\n\\subsection\2{\1}\n\n"
253
254     p = re.compile("^===\s*(.*?)\s*===(.*)", re.VERBOSE)
255     if state.frame_opened:
256         _string = p.sub(frame_closing + subsec_opening, string)
257     else:
258         _string = p.sub(subsec_opening, string)
259     if string != _string:
260         state.frame_opened = False
261
262     return _string
263
264 def transform_h2_to_sec(string, state):
265     """ headings (1) to sections """
266     frame_closing = escape_resub(get_frame_closing(state))
267     sec_opening = r"\n\\section\2{\1}\n\n"
268     p = re.compile("^==\s*(.*?)\s*==(.*)", re.VERBOSE)
269     if state.frame_opened:
270         _string = p.sub(frame_closing + sec_opening, string)
271     else:
272         _string = p.sub(sec_opening, string)
273     if string != _string:
274         state.frame_opened = False
275
276     return _string
277
278 def transform_replace_headfoot(string, state):
279     string = string.replace("<---FRAMEHEADER--->", state.frame_header)
280     string = string.replace("<---FRAMEFOOTER--->", state.frame_footer)
281     return string
282
283 def transform_environments(string):
284     """
285     latex environments, the users takes full responsibility
286     for closing ALL opened environments
287     exampe:
288     <[block]{block title}
289     message
290     [block]>
291     """
292     # -> open
293     p = re.compile("^<\[([^{}]*?)\]", re.VERBOSE)
294     string = p.sub(r"\\begin{\1}", string)
295     # -> close
296     p = re.compile("^\[([^{}]*?)\]>", re.VERBOSE)
297     string = p.sub(r"\\end{\1}", string)
298
299     return string
300
301 def transform_columns(string):
302     """ columns """
303     p = re.compile("^\[\[\[(.*?)\]\]\]", re.VERBOSE)
304     string = p.sub(r"\\column{\1}", string)
305     return string
306
307 def transform_boldfont(string):
308     """ bold font """
309     p = re.compile("'''(.*?)'''", re.VERBOSE)
310     string = p.sub(r"\\textbf{\1}", string)
311     return string
312
313 def transform_italicfont(string):
314     """ italic font """
315     p = re.compile("''(.*?)''", re.VERBOSE)
316     string = p.sub(r"\\emph{\1}", string)
317     return string
318
319 def _transform_mini_parser(character, replacement, string):
320     # implemented as a state-machine
321     output, typewriter = [], []
322     seen_at, seen_escape = False, False
323     for char in string:
324         if seen_escape:
325             if char == character:
326                 output.append(character)
327             else:
328                 output.append('\\' + char)
329             seen_escape = False
330         elif char == "\\":
331             seen_escape = True
332         elif char == character:
333             if seen_at:
334                 seen_at = False
335                 output, typewriter = typewriter, output
336                 output.append('\\'+replacement+'{')
337                 output += typewriter
338                 output.append('}')
339                 typewriter = []
340             else:
341                 seen_at = True
342                 output, typewriter = typewriter, output
343         else:
344             output.append(char)
345     if seen_at:
346         output, typewriter = typewriter, output
347         output.append(character)
348         output += typewriter
349     return "".join(output)
350
351 def transform_typewriterfont(string):
352     """ typewriter font """
353     return _transform_mini_parser('@', 'texttt', string)
354
355 def transform_alerts(string):
356     """ alerts """
357     return _transform_mini_parser('!', 'alert', string)
358
359 def transform_colors(string):
360     """ colors """
361     def maybe_replace(m):
362         """ only replace if we are not within <<< >>> """
363         for g in graphics:
364             # found color is within a graphics token
365             if m.start() >= g.start() and m.end() <= g.end():
366                 return m.string[m.start():m.end()]
367
368         return "\\textcolor{" + m.group(1) + "}{" + m.group(2) + "}"
369
370     p = re.compile("(\<\<\<)(.*?)\>\>\>", re.VERBOSE)
371     graphics = list(p.finditer(string))
372     p = re.compile("_([^_\\\\{}]*?)_([^_]*?[^_\\\\{}])_", re.VERBOSE)
373     string = p.sub(maybe_replace, string)
374     return string
375
376 def transform_footnotes(string):
377     """ footnotes """
378     p = re.compile("\(\(\((.*?)\)\)\)", re.VERBOSE)
379     string = p.sub(r"\\footnote{\1}", string)
380     return string
381
382 def transform_graphics(string):
383     """ figures/images """
384     p = re.compile("\<\<\<(.*?),(.*?)\>\>\>", re.VERBOSE)
385     string = p.sub(r"\\includegraphics[\2]{\1}", string)
386     p = re.compile("\<\<\<(.*?)\>\>\>", re.VERBOSE)
387     string = p.sub(r"\\includegraphics{\1}", string)
388     return string
389
390 def transform_substitutions(string):
391     """ substitutions """
392     p = re.compile("(\s)-->(\s)", re.VERBOSE)
393     string = p.sub(r"\1$\\rightarrow$\2", string)
394     p = re.compile("(\s)<--(\s)", re.VERBOSE)
395     string = p.sub(r"\1$\\leftarrow$\2", string)
396     p = re.compile("(\s)==>(\s)", re.VERBOSE)
397     string = p.sub(r"\1$\\Rightarrow$\2", string)
398     p = re.compile("(\s)<==(\s)", re.VERBOSE)
399     string = p.sub(r"\1$\\Leftarrow$\2", string)
400     p = re.compile("(\s):-\)(\s)", re.VERBOSE)
401     string = p.sub(r"\1\\smiley\2", string)
402     p = re.compile("(\s):-\((\s)", re.VERBOSE)
403     string = p.sub(r"\1\\frownie\2", string)
404     return string
405
406 def transform_vspace(string):
407     """vspace"""
408     p = re.compile("^\s*--(.*)--\s*$")
409     string = p.sub(r"\n\\vspace{\1}\n", string)
410     return string
411
412 def transform_vspacestar(string):
413     """vspace*"""
414     p = re.compile("^\s*--\*(.*)--\s*$")
415     string = p.sub(r"\n\\vspace*{\1}\n", string)
416     return string
417
418 def transform_uncover(string):
419     """uncover"""
420     p = re.compile("\+<(.*)>\s*{(.*)") # +<1-2>{.... -> \uncover<1-2>{....
421     string = p.sub(r"\uncover<\1>{\2", string)
422     return string
423
424 def transform_only(string):
425     """only"""
426     p = re.compile("-<(.*)>\s*{(.*)") # -<1-2>{.... -> \only<1-2>{....
427     string = p.sub(r"\only<\1>{\2", string)
428     return string
429
430 def transform(string, state):
431     """ convert/transform one line in context of state"""
432
433     #string = transform_itemenums(string, state)
434     string = transform_define_foothead(string, state)
435     string = transform_detect_manual_frameclose(string, state)
436     string = transform_h4_to_frame(string, state)
437     string = transform_h3_to_subsec(string, state)
438     string = transform_h2_to_sec(string, state)
439     string = transform_replace_headfoot(string, state)
440
441     string = transform_environments(string)
442     string = transform_columns(string)
443     string = transform_boldfont(string)
444     string = transform_italicfont(string)
445     string = transform_typewriterfont(string)
446     string = transform_alerts(string)
447     string = transform_colors(string)
448     string = transform_footnotes(string)
449     string = transform_graphics(string)
450     string = transform_substitutions(string)
451     string = transform_vspacestar(string)
452     string = transform_vspace(string)
453     string = transform_uncover(string)
454     string = transform_only(string)
455
456     string = transform_itemenums(string, state)
457
458     return string
459
460 def expand_code_make_defverb(content, name):
461     return "\\defverbatim[colored]\\%s{\n%s\n}" % (name, content)
462
463 def expand_code_make_lstlisting(content, options):
464     return "\\begin{lstlisting}%s%s\\end{lstlisting}" % (options, content)
465
466 def expand_code_search_escape_sequences(code):
467     open = '1'
468     close = '2'
469     while code.find(open) != -1 or code.find(close) != -1:
470         open = open + chr(random.randint(48,57))
471         close = close + chr(random.randint(48,57))
472
473     return (open,close)
474
475 def expand_code_tokenize_anims(code):
476     #escape
477     (esc_open, esc_close) = expand_code_search_escape_sequences(code)
478     code = code.replace('\\[', esc_open)
479     code = code.replace('\\]', esc_close)
480
481     p = re.compile(r'\[\[(?:.|\s)*?\]\]|\[(?:.|\s)*?\]')
482     non_anim = p.split(code)
483     anim = p.findall(code)
484
485     #unescape
486     anim = [s.replace(esc_open, '\\[').replace(esc_close, '\\]') for s in anim]
487     non_anim = [s.replace(esc_open, '[').replace(esc_close, ']') for s in non_anim]
488
489     return (anim, non_anim)
490
491 def make_unique(seq):
492     '''remove duplicate elements in a list, does not preserve order'''
493     keys = {}
494     for elem in seq:
495         keys[elem] = 1
496     return list(keys.keys())
497
498 def expand_code_parse_overlayspec(overlayspec):
499     overlays = []
500
501     groups = overlayspec.split(',')
502     for group in groups:
503         group = group.strip()
504         if group.find('-')!=-1:
505             nums = group.split('-')
506             if len(nums)<2:
507                 syntax_error('overlay specs must be of the form <(%d-%d)|(%d), ...>', overlayspec)
508             else:
509                 try:
510                     start = int(nums[0])
511                     stop = int(nums[1])
512                 except ValueError:
513                     syntax_error('not an int, overlay specs must be of the form <(%d-%d)|(%d), ...>', overlayspec)
514
515                 overlays.extend(list(range(start,stop+1)))
516         else:
517             try:
518                 num = int(group)
519             except ValueError:
520                 syntax_error('not an int, overlay specs must be of the form <(%d-%d)|(%d), ...>', overlayspec)
521             overlays.append(num)
522
523     #make unique
524     overlays = make_unique(overlays)
525     return overlays
526
527 def expand_code_parse_simpleanimspec(animspec):
528     #escape
529     (esc_open, esc_close) = expand_code_search_escape_sequences(animspec)
530     animspec = animspec.replace('\\[', esc_open)
531     animspec = animspec.replace('\\]', esc_close)
532
533     p = re.compile(r'^\[<([0-9,\-]+)>((?:.|\s)*)\]$')
534     m = p.match(animspec)
535     if m is not None:
536         overlays = expand_code_parse_overlayspec(m.group(1))
537         code = m.group(2)
538     else:
539         syntax_error('specification does not match [<%d>%s]', animspec)
540
541     #unescape code
542     code = code.replace(esc_open, '[').replace(esc_close, ']')
543
544     return [(overlay, code) for overlay in overlays]
545
546
547 def expand_code_parse_animspec(animspec):
548     if len(animspec)<4 or not animspec.startswith('[['):
549         return ('simple', expand_code_parse_simpleanimspec(animspec))
550
551     #escape
552     (esc_open, esc_close) = expand_code_search_escape_sequences(animspec)
553     animspec = animspec.replace('\\[', esc_open)
554     animspec = animspec.replace('\\]', esc_close)
555
556     p = re.compile(r'\[|\]\[|\]')
557     simple_specs = ['[%s]'%s for s in [s for s in p.split(animspec) if len(s.strip())>0]]
558
559     #unescape
560     simple_specs = [s.replace(esc_open, '\\[').replace(esc_close, '\\]') for s in simple_specs]
561     parsed_simple_specs = list(map(expand_code_parse_simpleanimspec, simple_specs))
562     unified_pss = []
563     for pss in parsed_simple_specs:
564         unified_pss.extend(pss)
565     return ('double', unified_pss)
566
567
568 def expand_code_getmaxoverlay(parsed_anims):
569     max_overlay = 0
570     for anim in parsed_anims:
571         for spec in anim:
572             if spec[0] > max_overlay:
573                 max_overlay = spec[0]
574     return max_overlay
575
576 def expand_code_getminoverlay(parsed_anims):
577     min_overlay = sys.maxsize
578     for anim in parsed_anims:
579         for spec in anim:
580             if spec[0] < min_overlay:
581                 min_overlay = spec[0]
582     if min_overlay == sys.maxsize:
583         min_overlay = 0
584     return min_overlay
585
586
587 def expand_code_genanims(parsed_animspec, minoverlay, maxoverlay, type):
588     #get maximum length of code
589     maxlen=0
590     if type=='double':
591         for simple_animspec in parsed_animspec:
592             if maxlen < len(simple_animspec[1]):
593                 maxlen = len(simple_animspec[1])
594
595     out = []
596     fill = ''.join([' ' for i in range(0, maxlen)])
597     for x in range(minoverlay,maxoverlay+1):
598         out.append(fill[:])
599
600     for simple_animspec in parsed_animspec:
601         out[simple_animspec[0]-minoverlay] = simple_animspec[1]
602
603     return out
604
605 def expand_code_getname(code):
606     hex2alpha_table = { '0':'a', '1':'b', '2':'c', '3':'d', \
607         '4':'e', '5':'f', '6':'g', '7':'h', '8':'i', '9':'j', \
608         'a':'k', 'b':'l', 'c':'m', 'd':'n', 'e':'o', 'f':'p' \
609     }
610     hexhash = md5hex(code)
611     alphahash = ''.join(hex2alpha_table[x] for x in hexhash)
612     return alphahash
613
614 def expand_code_makeoverprint(names, minoverlay):
615     out = ['\\begin{overprint}\n']
616     for (index, name) in enumerate(names):
617         out.append('  \\onslide<%d>\\%s\n' % (index+minoverlay, name))
618     out.append('\\end{overprint}\n')
619
620     return ''.join(out)
621
622 def expand_code_get_unique_name(defverbs, code, lstparams):
623     """generate a collision free entry in the defverbs-map and names-list"""
624     name = expand_code_getname(code)
625     expanded_code = expand_code_make_defverb(expand_code_make_lstlisting(code, lstparams), name)
626     rehash = ''
627     while name in defverbs and defverbs[name] != expanded_code:
628         rehash += chr(random.randint(65,90)) #append a character from A-Z to rehash value
629         name = expand_code_getname(code + rehash)
630         expanded_code = expand_code_make_defverb(expand_code_make_lstlisting(code, lstparams), name)
631
632     return (name, expanded_code)
633
634 def make_sorted(seq):
635     '''replacement for sorted built-in'''
636     l = list(seq)
637     l.sort()
638     return l
639
640 def expand_code_segment(result, codebuffer, state):
641     #treat first line as params for lstlistings
642     lstparams = codebuffer[0]
643     codebuffer[0] = ''
644
645     #join lines into one string
646     code = ''.join(codebuffer)
647
648     #tokenize code into anim and non_anim parts
649     (anim, non_anim) = expand_code_tokenize_anims(code)
650     if len(anim)>0:
651         #generate multiple versions of the anim parts
652         parsed_anims = list(map(expand_code_parse_animspec, anim))
653         max_overlay = expand_code_getmaxoverlay(x[1] for x in parsed_anims)
654         #if there is unanimated code, use 0 as the starting overlay
655         if len(list(non_anim))>0:
656             min_overlay = 1
657         else:
658             min_overlay = expand_code_getminoverlay(x[1] for x in parsed_anims)
659         gen_anims = [expand_code_genanims(x[1], min_overlay, max_overlay, x[0]) for x in parsed_anims]
660         anim_map = {}
661         for i in range(0,max_overlay-min_overlay+1):
662             anim_map[i+min_overlay] = [x[i] for x in gen_anims]
663
664         names = []
665         for overlay in make_sorted(anim_map.keys()):
666             #combine non_anim and anim parts
667             anim_map[overlay].append('')
668             zipped = zip(non_anim, anim_map[overlay])
669             code = ''.join(x[0] + x[1] for x in zipped)
670
671             #generate a collision free entry in the defverbs-map and names-list
672             (name, expanded_code) = expand_code_get_unique_name(state.defverbs, code, lstparams)
673
674             #now we have a collision free entry, append it
675             names.append(name)
676             state.defverbs[name] = expanded_code
677
678         #append overprint area to result
679         overprint = expand_code_makeoverprint(names, min_overlay)
680         result.append(overprint)
681     else:
682         #we have no animations and can just put the defverbatim in
683         #remove escapings
684         code = code.replace('\\[', '[').replace('\\]', ']')
685         (name, expanded_code) = expand_code_get_unique_name(state.defverbs, code, lstparams)
686         state.defverbs[name] = expanded_code
687         result.append('\n\\%s\n' % name)
688
689     return
690
691 def expand_code_defverbs(result, state):
692     result[state.code_pos] = result[state.code_pos] + '\n'.join(list(state.defverbs.values())) + '\n'
693     state.defverbs.clear()
694
695 def get_autotemplate_closing():
696     return '\n\end{document}\n'
697
698 def parse_bool(string):
699     boolean = False
700
701     if string == 'True' or string == 'true' or string == '1':
702         boolean = True
703     elif string == 'False' or string == 'false' or string =='0':
704         boolean = False
705     else:
706         syntax_error('Boolean expected (True/true/1 or False/false/0)', string)
707
708     return boolean
709
710 def parse_autotemplate(autotemplatebuffer):
711     """
712     @param autotemplatebuffer (list)
713         a list of lines found in the autotemplate section
714     @return (list)
715         a list of tuples of the form (string, string) with \command.parameters pairs
716     """
717     autotemplate = []
718
719     for line in autotemplatebuffer:
720         if len(line.lstrip())==0: #ignore empty lines
721             continue
722         if len(line.lstrip())>0 and line.lstrip().startswith('%'): #ignore lines starting with % as comments
723             continue
724
725         tokens = line.split('=', 1)
726         if len(tokens)<2:
727             syntax_error('lines in the autotemplate section have to be of the form key=value', line)
728
729         autotemplate.append((tokens[0], tokens[1]))
730
731     return autotemplate
732
733 def parse_usepackage(usepackage):
734     """
735     @param usepackage (str)
736         the unparsed usepackage string in the form [options]{name}
737     @return (tuple)
738         (name(str), options(str))
739     """
740
741     p = re.compile(r'^\s*(\[.*\])?\s*\{(.*)\}\s*$')
742     m = p.match(usepackage)
743     g = m.groups()
744     if len(g)<2 or len(g)>2:
745         syntax_error('usepackage specifications have to be of the form [%s]{%s}', usepackage)
746     elif g[1]==None and g[1].strip()!='':
747         syntax_error('usepackage specifications have to be of the form [%s]{%s}', usepackage)
748     else:
749         options = g[0]
750         name = g[1].strip()
751         return (name, options)
752
753
754 def unify_autotemplates(autotemplates):
755     usepackages = {} #packagename : options
756     documentclass = ''
757     titleframe = False
758
759     merged = []
760     for template in autotemplates:
761         for command in template:
762             if command[0] == 'usepackage':
763                 (name, options) = parse_usepackage(command[1])
764                 usepackages[name] = options
765             elif command[0] == 'titleframe':
766                 titleframe = command[1]
767             elif command[0] == 'documentclass':
768                 documentclass = command[1]
769             else:
770                 merged.append(command)
771
772     autotemplate = []
773     autotemplate.append(('documentclass', documentclass))
774     for (name, options) in usepackages.items():
775         if options is not None and options.strip() != '':
776             string = '%s{%s}' % (options, name)
777         else:
778             string = '{%s}' % name
779         autotemplate.append(('usepackage', string))
780     autotemplate.append(('titleframe', titleframe))
781
782     autotemplate.extend(merged)
783
784     return autotemplate
785
786 def expand_autotemplate_gen_opening(autotemplate):
787     """
788     @param autotemplate (list)
789         the specification of the autotemplate in the form of a list of tuples
790     @return (string)
791         the string the with generated latex code
792     """
793     titleframe = False
794     out = []
795     for item in autotemplate:
796         if item[0]!='titleframe':
797             out.append('\\%s%s' % item)
798         else:
799             titleframe = parse_bool(item[1])
800
801     out.append('\n\\begin{document}\n')
802     if titleframe:
803         out.append('\n\\frame{\\titlepage}\n')
804
805     return '\n'.join(out)
806
807
808 def expand_autotemplate_opening(result, templatebuffer, state):
809     my_autotemplate = parse_autotemplate(templatebuffer)
810     the_autotemplate = unify_autotemplates([autotemplate, my_autotemplate])
811
812     opening = expand_autotemplate_gen_opening(the_autotemplate)
813
814     result.append(opening)
815     result.append('')
816     state.code_pos = len(result)
817     state.autotemplate_opened = True
818     return
819
820 def get_autotemplatemode(line, autotemplatemode):
821     autotemplatestart = re.compile(r'^<\[\s*autotemplate\s*\]')
822     autotemplateend = re.compile(r'^\[\s*autotemplate\s*\]>')
823     if not autotemplatemode and autotemplatestart.match(line)!=None:
824         line = autotemplatestart.sub('', line)
825         return (line, True)
826     elif autotemplatemode and autotemplateend.match(line)!=None:
827         line = autotemplateend.sub('', line)
828         return (line, False)
829     else:
830         return (line, autotemplatemode)
831
832 def get_nowikimode(line, nowikimode):
833
834     if not nowikimode and nowikistartre.match(line)!=None:
835         line = nowikistartre.sub('', line)
836         return (line, True)
837     elif nowikimode and nowikiendre.match(line)!=None:
838         line = nowikiendre.sub('', line)
839         return (line, False)
840     else:
841         return (line, nowikimode)
842
843 def get_codemode(line, codemode):
844     if not codemode and codestartre.match(line)!=None:
845         line = codestartre.sub('', line)
846         return (line, True)
847     elif codemode and codeendre.match(line)!=None:
848         line = codeendre.sub('', line)
849         return (line, False)
850     else:
851         return (line, codemode)
852
853 def joinLines(lines):
854     """ join lines ending with unescaped percent signs, unless inside codemode or nowiki mode """
855     nowikimode = False
856     codemode = False
857     r = []  # result array
858     s = ''  # new line
859     for _l in lines:
860         (_,nowikimode) = get_nowikimode(_l, nowikimode)
861         if not nowikimode:
862             (_,codemode) = get_codemode(_l, codemode)
863
864         if not codemode:
865             l = _l.rstrip()
866         else:
867             l = _l
868
869         if not (nowikimode or codemode) and (len(l) > 1) and (l[-1] == "%") and (l[-2] != "\\"):
870             s = s + l[:-1]
871         elif not (nowikimode or codemode) and (len(l) == 1) and (l[-1] == "%"):
872             s = s + l[:-1]
873         else:
874             s = s + l
875             r.append(s)
876             s = ''
877
878     return r
879
880 def read_file_to_lines(filename):
881     """ read file """
882     try:
883         f = open(filename, "r")
884         lines = joinLines(f.readlines())
885         f.close()
886     except:
887         pprint("Cannot read file: %s" % filename, sys.stderr)
888         sys.exit(-2)
889
890     return lines
891
892
893 def scan_for_selected_frames(lines):
894     """scans for frames that should be rendered exclusively, returns true if such frames have been found"""
895     p = re.compile("^!====\s*(.*?)\s*====(.*)", re.VERBOSE)
896     for line in lines:
897         mo = p.match(line)
898         if mo is not None:
899             return True
900     return False
901
902 def line_opens_unselected_frame(line):
903     p = re.compile("^====\s*(.*?)\s*====(.*)", re.VERBOSE)
904     if p.match(line) is not None:
905         return True
906     return False
907
908 def line_opens_selected_frame(line):
909     p = re.compile("^!====\s*(.*?)\s*====(.*)", re.VERBOSE)
910     if p.match(line) is not None:
911         return True
912     return False
913
914 def line_closes_frame(line):
915     p = re.compile("^\s*\[\s*frame\s*\]>", re.VERBOSE)
916     if p.match(line) is not None:
917         return True
918     return False
919
920 def filter_selected_lines(lines):
921     selected_lines = []
922
923     selected_frame_opened = False
924     frame_closed = True
925     frame_manually_closed = False
926     for line in lines:
927         if line_opens_selected_frame(line):
928             selected_frame_opened = True
929             frame_closed = False
930
931         if line_opens_unselected_frame(line):
932             selected_frame_opened = False
933             frame_closed = False
934
935         if line_closes_frame(line):
936             selected_frame_opened = False
937             frame_closed = True
938             frame_manually_closed = True
939
940         if selected_frame_opened or (frame_closed and not frame_manually_closed):
941             selected_lines.append(line)
942
943     return selected_lines
944
945 def convert2beamer(lines):
946     out = ""
947     selectedframemode = scan_for_selected_frames(lines)
948     if selectedframemode:
949         out = convert2beamer_selected(lines)
950     else:
951         out = convert2beamer_full(lines)
952
953     return out
954
955 def convert2beamer_selected(lines):
956     selected_lines = filter_selected_lines(lines)
957     out = convert2beamer_full(selected_lines)
958     return out
959
960 def include_file(line):
961     """ Extract filename to include.
962
963     @param line string
964         a line that might include an inclusion
965     @return string or None
966         if the line contains an inclusion, return the filename,
967         otherwise return None
968     """
969     p = re.compile("\>\>\>(.*?)\<\<\<", re.VERBOSE)
970     if p.match(line):
971         filename = p.sub(r"\1", line)
972         return filename
973     else:
974         return None
975
976 def include_file_recursive(base):
977     stack = []
978     output = []
979     def recurse(file_):
980         stack.append(file_)
981         nowikimode = False
982         codemode = False
983         for line in get_lines_from_cache(file_):
984             if nowikimode or codemode:
985                 if nowikiendre.match(line):
986                     nowikimode = False
987                 elif codeendre.match(line):
988                     codemode = False
989                 output.append(line)
990             elif nowikistartre.match(line):
991                 output.append(line)
992                 nowikimode = True
993             elif codestartre.match(line):
994                 output.append(line)
995                 codemode = True
996             else:
997                 include = include_file(line)
998                 if include is not None:
999                     if include in stack:
1000                         raise IncludeLoopException('Loop detected while trying '
1001                                 "to include: '%s'.\n" % include +
1002                                 'Stack: '+ "->".join(stack))
1003                     else:
1004                         recurse(include)
1005                 else:
1006                     output.append(line)
1007         stack.pop()
1008     recurse(base)
1009     return output
1010
1011 def convert2beamer_full(lines):
1012     """ convert to LaTeX beamer"""
1013     state = w2bstate()
1014     result = [''] #start with one empty line as line 0
1015     codebuffer = []
1016     autotemplatebuffer = []
1017
1018     nowikimode = False
1019     codemode = False
1020     autotemplatemode = False
1021
1022     for line in lines:
1023         (line, nowikimode) = get_nowikimode(line, nowikimode)
1024         if nowikimode:
1025             result.append(line)
1026         else:
1027             (line, _codemode) = get_codemode(line, codemode)
1028             if _codemode and not codemode: #code mode was turned on
1029                 codebuffer = []
1030             elif not _codemode and codemode: #code mode was turned off
1031                 expand_code_segment(result, codebuffer, state)
1032             codemode = _codemode
1033
1034             if codemode:
1035                 codebuffer.append(line)
1036             else:
1037                 (line, _autotemplatemode) = get_autotemplatemode(line, autotemplatemode)
1038                 if _autotemplatemode and not autotemplatemode: #autotemplate mode was turned on
1039                     autotemplatebuffer = []
1040                 elif not _autotemplatemode and autotemplatemode: #autotemplate mode was turned off
1041                     expand_autotemplate_opening(result, autotemplatebuffer, state)
1042                 autotemplatemode = _autotemplatemode
1043
1044                 if autotemplatemode:
1045                     autotemplatebuffer.append(line)
1046                 else:
1047                     state.current_line = len(result)
1048                     result.append(transform(line, state))
1049
1050     result.append(transform("", state))   # close open environments
1051
1052     if state.frame_opened:
1053         result.append(get_frame_closing(state))
1054     if state.autotemplate_opened:
1055         result.append(get_autotemplate_closing())
1056
1057     #insert defverbs somewhere at the beginning
1058     expand_code_defverbs(result, state)
1059
1060     return result
1061
1062 def print_result(lines):
1063     """ print result to stdout """
1064     for l in lines:
1065         pprint(l, file=sys.stdout)
1066     return
1067
1068 def redirect_stdout(outfilename):
1069     global _redirected_stdout
1070     outfile = open(outfilename, "wt")
1071     _redirected_stdout = outfile
1072
1073 def main(argv):
1074     """ check parameters, start file processing """
1075     usage = "%prog [options] [input1.txt [input2.txt ...]] > output.tex"
1076     version = "%prog (http://wiki2beamer.sf.net), version: " + VERSIONTAG
1077
1078     parser = optparse.OptionParser(usage="\n  " + usage, version=version)
1079     parser.add_option("-o", "--output", dest="output", metavar="FILE", help="write output to FILE instead of stdout")
1080     opts, args = parser.parse_args()
1081
1082     if opts.output is not None:
1083         redirect_stdout(opts.output)
1084
1085     input_files = []
1086     if not sys.stdin.isatty():
1087         _file_cache['stdin'] = joinLines(sys.stdin.readlines())
1088         input_files.append('stdin')
1089     elif len(args) == 0:
1090         parser.error("You supplied no files to convert!")
1091
1092     input_files += args
1093     lines = []
1094     for file_ in input_files:
1095         lines += include_file_recursive(file_)
1096
1097     lines = convert2beamer(lines)
1098     print_result(lines)
1099
1100
1101 if __name__ == "__main__":
1102     main(sys.argv)