#!/usr/bin/python -tt ''' NAME otl2beamer.py - convert Vimoutliner to LaTeX Beamer slideshow SYNOPSIS ./otl2beamer.py INPUT.otl > OUTPUT.tex DESCRIPTION Converts Vimoutliner-formatted text into LaTeX Beamer source. See outline.otl for an example. Tabs are significant in the input file. See the Vimoutliner help for details. This program ignores everything before the "Metadata" section. Metadata must be in a specific format--see the example for details. Metadata ends with to newlines, then "Intro" should be used to begin parsing data that will become slides. [pause] becomes a Beamer-specific pause command, which causes text after the pause to appear on a subsequent slide. More info on Beamer: http://latex-beamer.sf.net/ On Ubuntu, install the latex-beamer package. This installs all necessary dependencies as well as the Beamer user guide. AUTHOR Adam Monsen COPYRIGHT (C)2008-2010 Adam Monsen LICENSE 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 . CONTRIBUTIONS Patches are welcome! Please send patches in unified diff format to the author along with the following boilerplate text: This work is the sole contribution of myself, and I gift it free of charge to Adam Monsen under the same license terms as otl2beamer.py. DONATIONS If you've found otl2beamer useful and wish to give a gift, please consider donating to the Free Software Foundation or ICCF Holland. ''' from optparse import OptionParser import fileinput import os import re import sys HEADER_STATIC = r'''% generated with otl2beamer.py % $Id: otl2beamer.py 3651 2010-04-14 06:34:47Z adam $ % requires latex-beamer package to be installed (on Ubuntu) \documentclass{beamer} \usepackage{beamerthemesplit} % suppress all navigation symbols \setbeamertemplate{navigation symbols}{} ''' PRESENTER_NOTES = r''' % presenter notes only \setbeameroption{show only notes} ''' # This behaves suboptimally with pauses (step-wise revealing of bullet points). # Since pauses in the source result in separate pages in the PDF, three pauses # increases the total presentation length by three pages. PAGE_NUMERS = r''' % use footline with page numbers \setbeamertemplate{footline}[page number] ''' FOOTER = r'''\end{document}''' # command-line options options = None # return some number of space characters times 4 def spaces(repeat): return chr(0x20) * repeat * 4 # stolen from otl2html.py and modified def linkOrImage(line): line = re.sub(r'\[img:([^],]+),([^],]+)\]', r'\includegraphics[\2]{\1}', line) line = re.sub(r'\[(\S+)\s([^]]+)\]', r'\href{\1}{\2}', line) line = re.sub(r'\[(\S+)\s\]', r'\url{\1}', line) return line def printnotes(line): line = re.sub(r'^(\s+):(.*)$', r'\1\\note{\2}', line) print line def pauses(line): replacement = r'\pause' if options.notes: replacement = '' line = re.sub(r'\[pause\]', replacement, line) return line def latexSpecialChar(line): line = re.sub(r'([&$%#_{}])', r'\\\1', line) return line # currently, double quotes must be on the same line def latexFancyQuotes(s, line): while re.search('"', line): line = re.sub('"', r"``", line, 1) s['inDoubleQuotes'] = True s['doubleQuoteStartLine'] = fileinput.filelineno() if s['inDoubleQuotes']: if (re.search('"', line)): line = re.sub('"', "''", line, 1) s['inDoubleQuotes'] = False return line def emphasisItalicEtc(line): line = re.sub(r'\*\*(.*?)\*\*', r'\\textbf{\1}', line) line = re.sub(r'//(.*?)//', r'\emph{\1}', line) return line def latexFancyFormatting(line): line = re.sub(r'LaTeX', r'\LaTeX', line) return line # count tabs def getLineLevel(line): strstart = line.lstrip() x = line.find(strstart) n = line.count("\t", 0, x) return n def printLine(s, line): lineformat = '' if s['bullets']: lineformat = r'\item' print '%s%s %s' % (spaces(s['outdent']), lineformat, line.strip()) def startNewFrame(s, line): s['inFrame'] = True s['outdent'] = s['indent'] + 1 if re.search('\s+\[nobullets\]', line): line = re.sub('\s+\[nobullets\]', '', line) s['bullets'] = False print r'\begin{frame}' print r'%s\frametitle{%s}' % (spaces(s['outdent']), line.strip()) def backOut(s, to): timesIndentDecreased = (s['lastIndent'] - to) for i in range(timesIndentDecreased): s['outdent'] -= 1 if s['bullets']: print r'%s\end{itemize}' % spaces(s['outdent']) def endFrame(s): print r'\end{frame}' print s['outdent'] = 0 s['inFrame'] = False s['bullets'] = True def handleMetadata(s, line): if not s['inMetaCopyrightDone'] and not s['inMetaCopyright']: if re.match(r'\s+Copyright:', line): s['inMetaCopyright'] = True s['metadata']['copyright'] = '' return if s['inMetaCopyright']: if s['indentDecreased']: s['inMetaCopyrightDone'] = True s['inMetaCopyright'] = False s['metadata']['simpleValues'] = {} else: copyline = re.sub(r'\s+:\s(.*)', r'\1', line) s['metadata']['copyright'] += '%% %s\n' % copyline if s['inMetaCopyrightDone']: key, value = re.match(r'\s+(\w+):\s(.*)', line).groups() s['metadata']['simpleValues'][key] = value def printHeader(s): header = s['metadata']['copyright'] header += HEADER_STATIC for key, value in s['metadata']['simpleValues'].items(): header += r'\%s{%s}' % (key.lower(), value) header += '\n' if options.notes: header += PRESENTER_NOTES header += '\n' header += r'\begin{document}' header += '\n' if not options.easy: header += r'\maketitle' header += '\n' print header def startBody(s): s['inBody'] = True printHeader(s) errorInMain = False def main(): global errorInMain # state machine s = { 'bullets': True, 'inBody': False, 'inDoubleQuotes': False, 'doubleQuoteStartLine': -1, 'inFrame': False, 'inMetaCopyright': False, 'inMetaCopyrightDone': False, 'inMetadata': False, 'metadata': { 'copyright': '', 'simpleValues': {}, }, # number of indent levels (tabs) in Vimoutliner input 'indent': 0, # number of indent levels (4 x spaces) written to LaTeX output 'outdent': 0, 'lastIndent': 0 } for line in fileinput.input(options.inputfile): line = line.rstrip() # count tabs s['indent'] = getLineLevel(line) s['indentDecreased'] = (s['indent'] < s['lastIndent']) if not s['inBody'] and options.easy: # allow eschew of metadata block startBody(s) elif not s['inBody']: if not s['inMetadata']: if re.match('Metadata', line): s['inMetadata'] = True if s['inMetadata']: if re.match('^$', line): s['inMetadata'] = False else: # parse document metadata. End metadata with \n\n. handleMetadata(s, line) s['lastIndent'] = s['indent'] continue if not line.startswith('Intro'): # don't start filtering until we see 'Intro' at the beginning of a line s['lastIndent'] = s['indent'] continue startBody(s) if re.match('\s+:', line): line = printnotes(line) continue # skip empty lines if len(line.strip()) < 1: # ignore indent change, too continue # format markup, escape special chars, etc. line = latexSpecialChar(line) # must be prior to linkOrImage() line = latexFancyQuotes(s, line) # must be prior to linkOrImage() line = linkOrImage(line) line = emphasisItalicEtc(line) line = latexFancyFormatting(line) line = pauses(line) if s['inFrame'] and s['indentDecreased']: backOut(s, s['indent']) if s['indent'] == 0: # time to end this slide endFrame(s) else: printLine(s, line) s['lastIndent'] = s['indent'] continue # time to start a new slide if not s['inFrame'] and (s['indent'] == 0): startNewFrame(s, line) s['lastIndent'] = s['indent'] continue # last slide had no bullets if s['inFrame'] and (s['indent'] == 0): endFrame(s) startNewFrame(s, line) s['lastIndent'] = s['indent'] continue # indent increased if s['inFrame'] and (s['indent'] > s['lastIndent']): if s['bullets']: print r'%s\begin{itemize}' % spaces(s['outdent']) s['outdent'] += 1 printLine(s, line) s['lastIndent'] = s['indent'] continue # indent stayed the same if s['inFrame'] and (s['indent'] == s['lastIndent']): printLine(s, line) s['lastIndent'] = s['indent'] continue errtemplate = "%s:%d:Error, line not processed. %s" print >> sys.stderr, errtemplate % \ (fileinput.filename() ,fileinput.filelineno() ,s) errorInMain = True backOut(s, 0) endFrame(s) print FOOTER if s['inDoubleQuotes']: errtemplate = "%s:%d:Error, Unmatched double quotes. %s" print >> sys.stderr, errtemplate % \ (fileinput.filename() ,s['doubleQuoteStartLine'] ,s) errorInMain = True def parseOptions(): global options parser = OptionParser() parser.add_option('-e', '--easy', help='easy mode--minimal metadata allowed', action='store_true', default=False) parser.add_option('-n', '--notes', help='only print presenter notes', action='store_true', default=False) (options, args) = parser.parse_args() if len(args) > 0: options.inputfile = args[0] else: options.inputfile = '-' if __name__ == '__main__': parseOptions() main() if errorInMain: sys.exit(1)