#!/usr/bin/env python # Copyright (C) 2002-2005 Christopher Arndt # # 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 2 # 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, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # standard library modules import getopt import math import os import re import shutil import sys import time import ConfigParser from cgi import escape as esc esq = lambda x: esc(x, 1) sys_libdir = '@datadir@' sys.path.insert(0, sys_libdir) # third-party modules try: import em except ImportError: print >>sys.stderr, "pylize now need the tempating module EmPy.\n" + \ "Get it from http://www.alcyone.com/software/empy/" # program specific modules __progname__ = "pylize" __author__ = "Christopher Arndt" __version__ = "1.3b" __revision__ = "001" __date__ = "2005-07-31" __license__ = "GPL" CONFIG_FILE = __progname__ + '.ini' __usage__ = """Usage: %(__progname__)s [OPTIONS] [ACTION] Version: %(__version__)s ACTIONS: create create a new presentation template from scratch and write it to the file 'all.html' (don't do anything else) show view the presentation in your browser Default action is to process the file 'all.html' in the current directory. OPTIONS: -e --expand-slides process each slide with EmPy allowing to embed Python code -l --lang= set language for internationalized strings -L --libdir= add to searchpath for ini and template files -s --symlink symlink auxiliary files instead of copying (only Unix) -t --template= use template for your presentation -v --verbose print internal values used for the generation of slides """ # ----------------------------------------------------------------------------- def warn(*args): sys.stderr.write(" ".join(map(str, args)) + '\n') # ----------------------------------------------------------------------------- def usage(d = vars()): warn(__usage__ % d) # ----------------------------------------------------------------------------- def condense(s): s = s.strip() s = s.replace('\n', ' ') s = s.replace('\r', ' ') return re.sub(' +', ' ', s) # ----------------------------------------------------------------------------- def reverseList(l): return [l[i] for i in xrange(len(l)-1, -1, -1)] # ----------------------------------------------------------------------------- def updateDefaults(a, b): for k in b.keys(): a.setdefault(k, b[k]) # ----------------------------------------------------------------------------- def mkLink(url, text=None, **kw): s = ['%s' % text or url) return "".join(s) # ----------------------------------------------------------------------------- def mkLogoLink(meta): if meta.has_key('logo'): try: import Image if os.path.exists(meta['logo']): i = Image.open(meta['logo']) else: import urllib, cStringIO i = Image.open(cStringIO.StringIO( urllib.urlopen(meta['logo']).read()) ) size = ' width="%s" height="%s"' % i.size except: size = "" logo_link = '%s' % \ (meta['logo'], meta.get('logo_alt', "[logo]"), size) if meta.get('url'): logo_link = mkLink(meta['url'], logo_link) else: logo_link = " " return logo_link # ----------------------------------------------------------------------------- def mkOrgLink(meta): if meta.has_key('url'): return mkLink(meta['url'], meta.get('org') or meta['url']) else: return meta.get('org', '') # ----------------------------------------------------------------------------- def mkAuthorLink(meta): if meta.has_key('email'): return mkLink('mailto:' + meta['email'], meta.get('author', meta['email']), title=meta['email']) else: return meta.get('author', '') # ----------------------------------------------------------------------------- def mkNavBar(links): l = ['\n '] for k in ['start', 'prev', 'toc', 'next', 'end']: if links.get('link_' + k) is not None: l.append(' ' % ('link_' + k, links['link_' + k][1] % links['link_' + k][0])) else: l.append(' ' % ('link_' + k,)) l.append(' \n') return "\n".join(l) # ----------------------------------------------------------------------------- _internalLinkRE = re.compile(r'(?i)(?s)') def _substLink(m): return m.group(0).replace(m.group(1), '%s.html' % m.group(1)[1:]) def fixLinks(s): return re.sub(_internalLinkRE, _substLink, s) # ----------------------------------------------------------------------------- class error(Exception): pass # ----------------------------------------------------------------------------- class Template: def __init__(self, file, **kwds): self.file = file self.source = self._read() self.substitutions = kwds def _read(self): fp = open(self.file) t = fp.read() fp.close() return t def expand(self, subst=None): env = self.substitutions.copy() if subst: env.update(subst) return em.expand(self.source, env) def write(self, file, subst=None): new = self.expand(subst) if os.path.exists(file): fp = open(file) old = fp.read() fp.close() if new == old: raise UserWarning, "File already up to date: %s" % file fp = open(file, 'wb') fp.write(new) fp.close() # ----------------------------------------------------------------------------- class Presentation: def __init__(self, master=""): if master: self.master = master else: self.master = "all.html" self.template = "default" self.searchpath = "." self.lang = None self.progress_bar = 1 self.linklibfiles = 0 self.expand_slides = 0 self.meta = {} def parseMaster(self, fn=""): try: f = open(fn or self.master) except (IOError, OSError): raise error, "Could not open master file: %s" % fn or self.master else: doc = f.read() f.close() rx = re.compile(r''' (?P.*)\s # header (?P.*) # body ''', re.I | re.S | re.X) m = rx.search(doc) if not m: return self._head, self._body = m.groups(('head', 'body')) m = re.search(r'(?s)(?i)(.*?)', self._head) d = {} if m: d['title'] = condense(m.group(1)) try: d['title'], d['subtitle'] = d['title'].split(' -- ', 1) except: d['subtitle'] = '' if not self.meta: self._getDefaults() self._getMeta() self.meta.update(d) self._getSections() def createMaster(self): if not self.meta: self._getDefaults() template = self._findLibFile('all.tmpl') if not template: raise error, "Template file '%s' not found" % template t = Template(template) subst = {} subst.update(self.meta) subst['lang'] = self.lang subst['date'] = time.strftime('%x', time.localtime(time.time())) if subst.has_key('charset'): subst['charset'] = \ '' % \ self.meta['charset'] else: subst['charset'] = \ '' try: t.write(self.master, subst) except UserWarning: pass def write(self): if not hasattr(self, '_head'): raise error, "You must first parse the master document." if self.meta.get('date') == 'today': self.meta['date'] = time.strftime('%x', time.localtime(time.time())) elif self.meta.get('date') == 'last-modified': self.meta['date'] = time.strftime('%x', time.localtime(os.stat(self.master)[8])) self.copyAux() self.meta['logo_link'] = mkLogoLink(self.meta) self.meta['org_link'] = mkOrgLink(self.meta) self.meta['author_link'] = mkAuthorLink(self.meta) self.writeKeyMap() self.writeIndex() self.writeTOC() self.writePages() def writeKeyMap(self): s = ['var keymap = Array();'] for k in options: if k.startswith('key_'): s.append("keymap[%i] = '%s';" % (ord(options[k]), k[4:])) fp = open('keymap.js', 'wb') fp.write("\n".join(s)) fp.close() def writeIndex(self): template = self._findLibFile('index.tmpl') if not template: raise error, "Template file '%' not found" % template t = Template(template) subst = {} subst.update(self.meta) subst['header_title'] = self.meta['title'] subst['title'] = '

%s

' % self.meta['title'] if self.meta.get('subtitle'): subst['subtitle'] = '

%s

' % \ self.meta['subtitle'] subst['header_title'] += ' - ' + self.meta['subtitle'] else: subst['subtitle'] = "" try: t.write('index.html', subst) except UserWarning: pass def writeTOC(self): from roman import dec2roman roman = lambda x: dec2roman(x).lower() template = self._findLibFile(self.template + '.tmpl') if not template: raise error, "Template file '%s' not found" % template t = Template(template) subst = {} subst.update(self.meta) numTOCs = int(math.ceil( len(self.sections) / float(self.meta['toc_num_sects']))) for i in range(numTOCs): subst['progress_bar'] = "" subst['header_title'] = "%s: %s" % (self.meta['title'], self.meta['toc']) content = "" heading = self.meta['toc'] nb = {} if i > 0: if i == 1: # the first toc page has always the same name prev = 'toc.html' title = self.meta['toc'] else: prev = 'toc_%s.html' % roman(i-1) title = "%s (%s)" % (self.meta['toc'], roman(i-1)) subst['rel_previous'] = \ '' % (prev, title) nb['link_prev'] = ('<<', mkLink(prev, '%s', title=title, accesskey=options['key_prev'], id="link_prev")) subst['header_title'] += ' (%s)' % roman(i) filename = 'toc_%s.html' % roman(i) heading += ' (%s)' % roman(i) else: # the first toc page if self.abstract: content += \ '
\n

%s

\n%s
\n' % \ (self.meta['abstract'], self.abstract) subst['rel_previous'] = "" nb['link_prev'] = None filename = 'toc.html' nb['link_toc'] = (self.meta['start_pres'], mkLink( self._getSlideFilename(0), '%s', title=self.sections[0][0], accesskey="s")) if i < numTOCs-1: next = 'toc_%s.html' % roman(i+1) subst['rel_next'] = \ '' % \ (next, self.meta['next']) nb['link_next'] = ('>>', mkLink( next, '%s', title="%s (%s)" % (self.meta['toc'], roman(i+1)), accesskey=options['key_next'], id="link_next")) else: # the last page of the TOC subst['rel_next'] = \ '' % \ (self._getSlideFilename(0), self.meta['next']) nb['link_next'] = None subst.update(nb) subst['nav_buttons'] = mkNavBar(nb) subst['pagetitle'] = "

%s

" % (self.meta['title']) if self.meta.has_key('subtitle'): subst['pagesubtitle'] = "

%s

" % (self.meta['subtitle']) else: subst['pagesubtitle'] = "" content += '\n
\n' content += '

%s

\n
    \n' % heading start = i * int(self.meta['toc_num_sects']) end = min(len(self.sections), start + int(self.meta['toc_num_sects'])) # do the listing of section titles for j in range(start, end): pos = j+1-start if j < len(self.sections): if pos <= 10: accesskey = str(pos % 10) else: accesskey = None # do we have a section with consecutive equal titles? if j > 0 and self.sections[j][0] == self.sections[j-1][0]: # start of new toc page -> repeat section title if j == start: content += '
  • %s (cont.)
  • \n' % \ self.sections[j][0] # start new sub-list if j in [1, start] or self.sections[j][0] != \ self.sections[j-2][0]: content += '
      \n' content += '
    • %s
    • \n' % mkLink( self._getSlideFilename(j), self.sections[j][1], accesskey=accesskey, tabindex=pos ) else: content += '
    • %s
    • \n' % mkLink( self._getSlideFilename(j), self.sections[j][0], accesskey=accesskey, title=self.sections[j][1], tabindex=pos ) if j > 0 and self.sections[j][0] == self.sections[j-1][0]: # end sub list if j == end - 1 or self.sections[j][0] != \ self.sections[j+1][0]: content += '
    \n' content += '
\n
\n' subst['content'] = content try: t.write(filename, subst) except UserWarning: pass def writePages(self): template = self._findLibFile(self.template + '.tmpl') if not template: raise error, "Template file '%s' not found" % template for i in range(len(self.sections)): t = Template(template) subst = {} subst.update(self.meta) subst['progress_bar'] = self._mkProgressBar(i+1) nb = self._mkNavLinks(i+1) subst.update(nb) subst['nav_buttons'] = mkNavBar(nb) subst['header_title'] = self.meta['title'] + ': ' + \ self.sections[i][0] if self.sections[i][2]: subst['pagetitle'] = '

%s

' % self.sections[i][0] subst['content'] = self._expandPage(fixLinks(self.sections[i][2])) else: # no text on this slide, put title centered on page subst['pagetitle'] = "" subst['content'] = self._expandPage( '
\n

%s

\n
' % \ self.sections[i][0] ) # header tags if i > 0: subst['rel_previous'] = \ '' % \ (self._getSlideFilename(i), self.meta['previous']) else: # the first page subst['rel_previous'] = \ '' % \ (self.meta['previous']) if i < len(self.sections)-1: subst['rel_next'] = \ '' % \ (self._getSlideFilename(i+1), self.meta['next']) else: # the last page subst['rel_next'] = "" if self.sections[i][1]: subst['pagesubtitle'] = "

%s

" % (self.sections[i][1]) else: subst['pagesubtitle'] = "" try: t.write(self._getSlideFilename(i), subst) except UserWarning: pass def setLang(self, lang): d = readINI('lang_' + lang, self.searchpath) self.meta.update(d) self.lang = lang def copyAux(self): for file in ["css", self.meta.get('logo', 'logo.png'), 'icons', 'pylize.js']: for d in self.searchpath: src = os.path.join(d, file) dst = file if os.path.exists(src): if os.path.isdir(src): if not os.path.exists(dst): if self.linklibfiles and hasattr(os, 'symlink'): os.symlink(src,dst) else: os.mkdir(dst) if not os.path.islink(dst): for f in os.listdir(src): if not os.path.isdir(os.path.join(src, f)) and \ not os.path.exists(os.path.join(dst, f)): shutil.copy(os.path.join(src, f), os.path.join(dst, f)) elif not os.path.exists(dst): if self.linklibfiles and hasattr(os, 'symlink'): os.symlink(src,dst) else: shutil.copy(src, dst) break else: warn("Could not find auxiliary file '%s'. " "Things might not work as expected!" % file) def _findLibFile(self, base): for d in self.searchpath: libfile = os.path.join(d, base) if os.path.exists(libfile): return libfile return None def _getDefaults(self): self.meta = readINI('defaults', self.searchpath) self.setLang(getattr(self, 'lang') or 'en') _attrRE = re.compile(r'(?i)(?s)\s+([\w-]+)="(.*?)"') def _getMeta(self): """Collects the name, content pairs from meta tags.""" meta = {} meta_tags = re.findall(r'(?s)(?i)', self._head) for tag in meta_tags: d = {} for attr, attr_val in self._attrRE.findall(tag): d[attr.lower()] = attr_val if d.has_key('name'): if d['name'][:7] == 'pylize.': meta[d['name'][7:]] = d['content'] else: meta[d['name']] = d['content'] if d.get('http-equiv') == 'Content-Type': m = re.search(r'(?i)charset=(.*?)(;|$)', d['content']) if m: meta['charset'] = m.group(1) self.meta.update(meta) if meta.get('lang'): self.setLang(meta['lang']) _h1RE = re.compile(r'(?is)(.*?)') _h2RE = re.compile(r'(?is)((.*?))') _commentRE = \ re.compile(r'(?is)(\s*)?()(\s*)?') _anchorRE = re.compile(r'(?is)\s*()(.*?)') def _getSections(self): """Splits the master document in sections at

tags.""" # strip comments but keep JavaScript def stripComments(match): if match.group(0).startswith(' 3 and self.sections[i][3]: return self.sections[i][3] + '.html' return 'slide_%02i.html' % (i+1,) raise IndexError, "Section number '%i' out of range." % i def _mkProgressBar(self, page): pcnt_done = page * 100 / len(self.sections) pcnt_left = 100 - pcnt_done if self.progress_bar: l = r = " " t = "%s %s %s" % (page, self.meta['page_of'], len(self.sections)) if pcnt_done > 50: l = t else: r = t if pcnt_done == 100: r = "" s = """
%s %s
""" % (pcnt_done, l, pcnt_left, r) else: s = "%s %s %s %s" % (self.meta['page'], page, self.meta['page_of'], len(self.sections)) return s def _mkNavLinks(self, page): """Build text navigation links for the footer.""" d = {} # link to first slide if page > 2: d['link_start'] = ("|<<", mkLink( self._getSlideFilename(0), text='%s', title="%s: %s" % (self.meta['start'], self.sections[0][0]), accesskey=options['key_start'], id="link_start")) else: d['link_start'] = None # link to previous slide if page > 1: d['link_prev'] = ("<<", mkLink( self._getSlideFilename(page-2), '%s', title="%s: %s" % (self.meta['previous'], self.sections[page-2][0]), accesskey=options['key_prev'], id="link_prev")) else: d['link_prev'] = None # link to the TOC d['link_toc'] = (self.meta['toc_link'], mkLink( 'toc.html', '%s', title=self.meta['toc'], accesskey="t", id="link_toc")) # link to following slide if page < len(self.sections): d['link_next'] = (">>", mkLink( self._getSlideFilename(page), "%s", title="%s: %s" % (self.meta['next'], self.sections[page][0]), accesskey=options['key_next'], id="link_next")) else: d['link_next'] = None # link to last slide if page < len(self.sections) - 1: d['link_end'] = (">>|", mkLink( self._getSlideFilename(len(self.sections)-1), "%s", title="%s: %s" % (self.meta['end'], self.sections[-1][0]), accesskey=options['key_end'], id="link_end")) else: d['link_end'] = None return d def _expandPage(self, text, env=None): if self.expand_slides: try: subst = getattr(self, '_substitutions').copy() except AttributeError: subst = {} if env: subst.update(env) text = em.expand(text, subst) self._substitutions = subst return text # ----------------------------------------------------------------------------- def readINI(section, searchpath=["."]): """Look for config file in every dir on searchpath and read it.""" d = {} c = ConfigParser.ConfigParser() c.read(map(os.path.join, reverseList(searchpath), [CONFIG_FILE] * len(searchpath))) if c.has_section(section): for o in c.options(section): d[o] = c.get(section, o) return d # ----------------------------------------------------------------------------- def printMeta(obj): """Print meta data of obj to standard out for debugging purposes.""" keys = obj.meta.keys() keys.sort() for key in keys: print "%s: %s" % (key, repr(obj.meta[key])) # ----------------------------------------------------------------------------- def get_options(args): """Parse command line options.""" try: opts, args = getopt.getopt(args, 'vsel:L:t:', [ 'verbose', '--symlink', 'lang=', 'libdir=', 'template=', 'expand-slides'] ) except getopt.error, exc: warn(exc) usage() sys.exit(1) cmdlOpts = {} for o, a in opts: if o in ['-l', '--lang']: cmdlOpts['lang'] = a.lower() elif o in ['-v', '--verbose']: cmdlOpts['verbose'] = 1 elif o in ['-L', '--libdir']: options['searchpath'].insert(0, a) sys.path.insert(0, a) elif o in ['-t', '--template']: cmdlOpts['template'] = a elif o in ['-s', '--symlink']: cmdlOpts['linklibfiles'] = 1 elif o in ['-e', '--expand-slides']: cmdlOpts['expand_slides'] = 1 return cmdlOpts, args # ----------------------------------------------------------------------------- def create_master(pres): """Create a new presentation master (temlate) in the current directory.""" if os.path.exists(pres.master): warn("Presentation file '%s' already exists. Will not overwrite!\n" % \ pres.master) return 1 else: if not pres.lang and options.has_key('lang'): pres.lang = options['lang'] if int(options['verbose']): printMeta(pres) pres.createMaster() # ----------------------------------------------------------------------------- def show_presentation(): """Open the presentation (index.html) in the default web browser.""" if os.path.exists('index.html'): import webbrowser url = 'file://%s/index.html' % \ os.path.abspath(os.curdir) warn("Opening '%s' ...\n" % url) webbrowser.open(url) else: warn("Starting page 'index.html' not found.\n" "Run 'pylize' at least once without arguments\n" "(You must create a master file before that).\n") usage() return 1 # ----------------------------------------------------------------------------- def process_presentation(pres): """Parse presentation master and create slides.""" try: pres.parseMaster() for opt in ('template', 'expand_slides', 'symlink', 'progress_bar'): try: setattr(pres, opt, int(options[opt])) except ValueError: setattr(pres, opt, options[opt]) except KeyError: pass if not pres.lang and options.has_key('lang'): pres.setLang(options['lang']) if int(options['verbose']): printMeta(pres) pres.write() except error: warn("Could not process the presentation file '%s'.\n" % pres.master + "Run 'pylize create' to create a template.\n") usage() return 1 # ----------------------------------------------------------------------------- def main(args): """Parse options & configuration files and carry out requested action.""" global options home = os.environ.get('HOME') if not home: try: import pwd home = pwd.getpwuid(os.geteuid())[5] except: home = os.getcwd() # built-in defaults options = { 'searchpath': [os.path.join(home, '.pylize'), sys_libdir], 'key_prev': 'p', 'key_next': 'n', 'key_toc': 't', 'key_start': 's', 'key_end': 'e', 'expand_slides': 1 } cmdlOpts, args = get_options(args) # overwritten by config file options confOpts = readINI('options', options['searchpath']) options.update(confOpts) # overwritten by command line options options.update(cmdlOpts) try: import locale options['lang'] = locale.setlocale(locale.LC_ALL, options.get('lang', ''))[:2] except: pass pres = Presentation() pres.searchpath = options['searchpath'] if args: action = args.pop(0) if action == "create": ret = create_master(pres) elif action == "show": ret = show_presentation() else: usage() ret = 1 else: ret = process_presentation(pres) return ret # ----------------------------------------------------------------------------- if __name__ == '__main__': sys.exit(main(sys.argv[1:]))