#!/usr/bin/env python g_version = '1.00' g_date = '2000-01-09' # Take the files in a "content directory". # Turn them into presentable files in one or more "presentation # directories", according to "presentation models". # Those high-falutin' terms are really very simple. # A presentation model does two things. # # 1. Maps special pseudo-tags to replacement text, # optionally [UNIMPLEMENTED] computed as a lump of Python. # # 2. Selects which pages should actually appear in # the presentation, by specifying a number of keys. # Pages can contain rules saying "Don't show this # if such-and-such a key is set" or "Do show this # if such-and-such a key isn't set". These keys # can be tested in your pseudo-tag replacement text. # # So, for instance, you could have a presentation # intended for people with flashy browsers and high-bandwidth # links, and another for people using Lynx across a slow # serial connection. Or, one to produce pages formatted using # style sheets, and another to produce ones formatted with # old-style physical markup. Or, one for your local pages # and one for the ones you upload to your ISP-provided web # space. # Special stuff in the content files # ---------------------------------- # # Special tags look like <:foo:> (simplest case, with no # parameters) or <:foo: {param1=blah} {param2=wibble}> # (hairier case, with parameters). # You can include or exclude a section of page according to # keys with the special tags <[if foo]> or <[unless bar]> # ... <[endif]>. There's <[else]> too. # You can include or exclude a whole page according to keys # with the special tags <[include page if foo]> and # <[exclude page if foo]>. # These special "flow-control" tags must appear alone # on lines, except that whitespace may precede or # follow them. # Another pair of very special tags which also must # come alone on lines: # <[presentation]> # <[end-presentation]> # Stuff between these is processed as if it were part # of the presentation model. It applies to the whole file. # Presentation models # ------------------- # # A presentation model is contained in a single text file. # The best way to describe one is with a sample. # # ------ sample begins ------ # key foo # keys bar baz # define author # Gareth McCaughan # end-define # define begin # # <:title:> # # <[if creation-date]> # # <[endif]> # # # #

<:mainhead:>

#
# end-define # exclude "**.gif" # exclude "private-stuff" # preserve "foo/bar.html" # preserve "foo" # ------- sample ends ------- # # A few remarks. # - Keys are actually degenerate cases of macros. # <[if]> is really "ifdef". # - There are some system-defined macros. Their # names begin with "%". # - Filespecs in exclude directives are ordinary # Unix globs, except that a leading ** means # "allow any prefix, even including other directories". # In the present implementation, non-** things must be # leaf-names; this will change. # # A directory may contain a file called "_model". # If so, it will be read in as a presentation model, # active while stuff below that directory is being # considered. (It augments, rather than replacing, # the existing presentation model.) # Bugs and infelicities: # # - The program is ill-structured; lots of code is in # PresentationModel methods that should either be in # PresentationData methods or in global functions, # for instance. There are lots of other related problems. # # - We take a very unprincipled approach to errors. # Some are ignored; some will crash the program. # OK, on to the code. #============================================================================= import fnmatch import os import posixpath import re import stat import string import sys import time #============================================================================= vtable = {} g_macro_re = re.compile(r'<:([^ :<>{}]+):((\s*\{[^ :<>{}=]+=?[^<>{}]+\})*)>') g_arg_re = re.compile(r'\{([^ :<>{}=]+)=?([^<>{}]*)\}') def file_last_change(name): return os.stat(name)[stat.ST_MTIME] class PresentationData: "Guts of a PresentationModel. At present, very stupid." def __init__(self, last_change): self.macros = {} self.exclude_patterns = [] self.preserve_patterns = [] self.last_change = last_change class PresentationModel: "A repository for keyword definitions etc." def __init__(self, lines, last_change): self.data = [] self.push_presentation(lines, last_change) def last_change(self): return self.data[-1].last_change def push_presentation(self, lines, last_change): if self.data and last_change < self.data[-1].last_change: last_change = self.data[-1].last_change lines = map(string.rstrip, lines) self.data.append(PresentationData(last_change)) self.apply_presentation_elements(lines, last_change) def apply_presentation_elements(self, lines, last_change): i=0 while i0 and line[0]<>'#': break i = i+1 if i>=len(lines): return i line = lines[i] if words[0]in ('key','keys'): for name in words[1:]: self.data[-1].macros[name] = ['yes'] elif words[0]=='exclude': self.data[-1].exclude_patterns.extend(words[1:]) elif words[0]=='preserve': self.data[-1].preserve_patterns.extend(words[1:]) elif words[0]=='define': name = words[1] text = [] i=i+1 while i0 and name[0]=='%': return self.system_macroexpand(name[1:]) for i in range(len(self.data)-1,-1,-1): t = self.data[i].macros.get(name,None) if t: ###print "EXPAND %s => %s" % (name, t) return t ###print "EXPAND %s fails." % name return [''] def system_macroexpand(self, name): t = vtable.get(name,None) if t: return [t] return [''] def macro_exists(self, name): return self.macroexpand_1(name)<>[''] def macroexpand(self, name, args): "Perform a complete macroexpansion on a name with normalised args." self.push_definitions(args) try: lines = self.macroexpand_1(name) lines = self.expand(lines, self.data[-1].last_change) finally: self.pop_presentation() return lines def expand(self, lines, last_change): "Take care of macros and <[if]> in a region of text." # If we find that we should exclude this file, # we return None instead. lines,pres_p = self.expand_presentations(lines, last_change) try: lines = self.expand_conditionals(lines) lines = self.expand_macros(lines) lines = self.check_exclude(lines) finally: if pres_p: self.pop_presentation() return lines def check_exclude(self, lines): "Remove include/exclude lines. Return None if should exclude." result = [] known = 0 for line in lines: sline = string.strip(line) if len(sline)>=2 and sline[-2:]==']>': if sline[:18]=='<[include page if ': if not known and self.macro_exists(sline[18:-2]): known = 1 continue if sline[:18]=='<[exclude page if ': if not known and not self.macro_exists(sline[18:-2]): return None continue result.append(line) return result def expand_presentations(self, lines, last_change): "Process <[presentation]> blocks." result = [] presentation_lines = None self.push_presentation([], last_change) any = 0 for line in lines: sline = string.strip(line) if sline=='<[presentation]>': presentation_lines = [] continue if sline=='<[end-presentation]>': if presentation_lines is not None: self.apply_presentation_elements(presentation_lines, last_change) any = 1 presentation_lines = None continue if presentation_lines is not None: presentation_lines.append(line) else: result.append(line) if not any: self.pop_presentation() return result,any def expand_conditionals(self, lines): "Take care of <[if]> in a region of text." result = [] conditionals = [1] on=1 for line in lines: sline = string.strip(line) if len(sline)>=5 and sline[:5]=='<[if ' and sline[-2:]==']>': var = sline[5:-2] conditionals.append(self.macro_exists(var)) if on: on=conditionals[-1] continue elif len(sline)>=9 and sline[:9]=='<[unless ' and sline[-2:]==']>': var = sline[9:-2] conditionals.append(not self.macro_exists(var)) if on: on=conditionals[-1] continue elif sline=='<[else]>': conditionals[-1] = not conditionals[-1] if on: on=0 else: on=1 for t in conditionals: if not t: on=0; break continue elif sline=='<[endif]>': conditionals = conditionals[:-1] if not on: on=1 for t in conditionals: if not t: on=0; break continue if on: result.append(line) return result def expand_macros(self, lines): result = [] for line in lines: matches = g_macro_re.findall(line) interstices = g_macro_re.split(line) if not matches: result.append(line) continue # matches is list of (var, argstr) for i in range(len(matches)): j=i*4 # aargh. To get at the *real* interstices. replacement = self.expand_macro(matches[i][0],matches[i][1]) if replacement: replacement[0] = interstices[j]+replacement[0] else: replacement = [interstices[j]] if i>0 and result: result[-1] = result[-1]+replacement[0] else: result.append(replacement[0]) result.extend(replacement[1:]) if matches and result: result[-1] = result[-1] + interstices[-1] else: result.append(interstices[-1]) return result def expand_macro(self, name, arg_string): "Expand a macro, with args given only as string" return self.macroexpand(name, self.grok_args(name, arg_string)) def grok_args(self, name, arg_string): args = [] matches = g_arg_re.findall(arg_string) # list of (name,value) return map(lambda (x,y): (x,[y]), matches) def push_definitions(self, bindings): self.data.append(PresentationData(self.data[-1].last_change)) for (key,value) in bindings: ###print "%s -> %s" % (key,value) self.data[-1].macros[key]=value def should_exclude(self, name): for d in self.data: for pattern in d.exclude_patterns: if self.pattern_matches(pattern, name): return 1 return 0 def should_preserve(self, name): for d in self.data: for pattern in d.preserve_patterns: if self.pattern_matches(pattern, name): return 1 return 0 def pattern_matches(self, pattern, name): # xxx if len(pattern)>=2 and pattern[:2]=='*': return fnmatch.fnmatch(name, pattern[1:]) i = string.rfind(name, '/') return fnmatch.fnmatch(name[i+1:], pattern) def process_file(self, name, in_prefix, out_prefix): if self.should_preserve(name): print "[Copy %s]" % name self.copy_file(name, in_prefix, out_prefix) return self.update_vtable(name, in_prefix, out_prefix) s = os.stat(in_prefix+name) t = s[stat.ST_MTIME] f = open(in_prefix+name) lines = map(string.rstrip,f.readlines()) f.close() lines = self.expand(lines, t) if lines is None: print "[Skip %s]" % name return print "[Proc %s]" % name f = open(out_prefix+name, 'w') f.write(string.join(lines,'\n')) f.close() times = (s[stat.ST_ATIME], max(t, self.data[-1].last_change)) os.utime(out_prefix+name, times) def update_vtable(self, name, in_prefix, out_prefix): vtable['filename'] = name s = os.stat(in_prefix+name) vtable['last-change-date'] = self.date_format(s[stat.ST_CTIME]) def date_format(self, t): return time.strftime("%A, %Y-%m-%d", time.localtime(t)) def process_directory(self, name, in_prefix, out_prefix): if self.should_preserve(name): self.copy_tree(name, in_prefix, out_prefix) return try: os.mkdir(out_prefix+name, 0755) except OSError: # xxx pass files = os.listdir(in_prefix+name) if name: name = name+os.sep if '_model' in files: do_model=1 del files[files.index('_model')] else: do_model=0 if do_model: t = file_last_change(in_prefix+name+'_model') f = open(in_prefix+name+'_model') print "[Read %s_model]" % name lines = f.readlines() f.close() self.push_presentation(lines, t) try: for file in files: self.process(name+file, in_prefix, out_prefix) finally: if do_model: self.pop_presentation() def process(self, name, in_prefix, out_prefix): if self.should_exclude(name): print "[Skip %s]" % name return s = os.stat(in_prefix+name) if stat.S_ISDIR(s[stat.ST_MODE]): print "[Dir %s]" % name self.process_directory(name, in_prefix, out_prefix) else: if len(name)>=5 and name[-5:]=='.html': self.process_file(name, in_prefix, out_prefix) else: print "[Copy %s]" % name self.copy_file(name, in_prefix, out_prefix) def copy_file(self, name, in_prefix, out_prefix): os.system("cp -fp '%s' '%s'" % (in_prefix+name, out_prefix+name)) def copy_tree(self, name, in_prefix, out_prefix): os.system("cp -fpR '%s' '%s'" % (in_prefix+name, out_prefix+name)) #----------------------------------------------------------------------------- def read_presentation_model(filename): print "[Read %s]" % filename f = open(g_model_dir+filename) lines = f.readlines() f.close() return PresentationModel(lines, file_last_change(g_model_dir+filename)) #----------------------------------------------------------------------------- # Main program. # Usage: munge-web # Other models will be looked for in the same directory # as the starting one. args = sys.argv[1:] if args[0]=='-v': print "This is munge-web, version %s (%s)." % (g_version, g_date) args = args[1:] if len(args)<>3: print "Usage: munge-web " sys.exit(1) content_dir, output_dir, model_file = args g_model_dir, model_leaf = posixpath.split(model_file) if g_model_dir: g_model_dir = g_model_dir+os.sep os.umask(0022) model = read_presentation_model(model_leaf) model.process('', content_dir+os.sep, output_dir+os.sep) print "Done."