#!/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."