# microcms.py - tiny file based CMS for static web sites # Copyright (c) 2009 Niall McCarroll # Distributed under the MIT/X11 License (http://www.mccarroll.net/snippets/license.txt) # functionality: # expand $root in HTML files to the relative path to the top level directory # transform HTML content using plugins # usage: # cd # python microcms.py import sys import os import re import copy import codecs from tinplate import tinplate rootdir = None plugins = {} plugin_consumers = {} plugin_processors = {} def loadplugins(): global plugins global plugin_processors global plugin_consumers pluginsdir = os.path.join(os.path.split(sys.argv[0])[0],'plugins') for f in os.listdir(pluginsdir): if f.endswith(".py"): mname = os.path.splitext(f)[0] try: plugin = __import__("plugins",globals(),locals(),[mname],-1) plugins[mname] = plugin except Exception, ex: print f+":"+str(ex) pass for plugin in plugins: try: processfn = getattr(getattr(plugins[plugin],plugin),'process') plugin_processors[plugin] = processfn print "Loaded processor function from module:"+plugin except Exception, ex: pass try: consumefn = getattr(getattr(plugins[plugin],plugin),'consume') plugin_consumers[plugin] = consumefn print "Loaded consumer function from module:"+plugin except Exception, ex: pass filter = ['.chtml'] pat1 = re.compile(r'(.*)]*)>(.*)') pat2 = re.compile(r'\s*([a-zA-Z_]*)\s*=\s*\"([^\"]*)\"(.*)') def parse_microcms(line): mat = pat1.match(line) if mat: attrs = {} attrtxt = mat.group(2) mat2 = pat2.match(attrtxt) while mat2: aname = mat2.group(1) avalue = mat2.group(2) attrtxt = mat2.group(3) mat2 = pat2.match(attrtxt) attrs[aname] = avalue return (mat.group(1),attrs,mat.group(3)) else: return None class cmsdirectory: def __init__(self,name,files,directories): self.name = name self.files = files self.directories = directories self.indexfile = None for d in self.directories: d.parentdir = self for f in self.files: f.parentdir = self if os.path.split(f.path)[1] == "index.chtml": self.indexfile = f self.parentdir = None def depth(self): depth = 0 d = self.parentdir while d != None: d = d.parentdir depth += 1 return depth def getName(self): return self.name class cmsfile: def __init__(self,path,outpath): self.path = path self.outpath = outpath self.metadata = None self.parentdir = None def depth(self): if self.parentdir: return self.parentdir.depth() else: return 0 def expand_env(line,env): for k in env: pos = 0 while True: pos = line.find("$"+k,pos) if pos == -1: break if pos == 0 or line[pos-1] != "$": line = line[:pos]+env[k]+line[pos+len(k)+1:] pos += len(env[k]) else: line = line[:pos]+line[pos+1:] pos += len(k) return line def rewritecontents(content,cfile,original_env,extpath=None): lines = content.split("\n") result = '' for line in lines: env = copy.copy(original_env) rootpath = "." for x in xrange(0,cfile.parentdir.depth()): rootpath = os.path.join(rootpath,"..") env["root"] = rootpath line = expand_env(line,env) microcms_cmd = parse_microcms(line) if microcms_cmd: pre = microcms_cmd[0] attrs = microcms_cmd[1] if "path" in attrs and extpath: attrs["path"] = os.path.join(extpath,attrs["path"]) post = microcms_cmd[2] func = attrs["function"] inc = '' # search for a matching plugin and invoke it global plugin_processors if func in plugin_processors: inc = plugin_processors[func](cfile,attrs) else: inc += "microcms error: could not load plugin: "+func epath = None if "path" in attrs: epath = os.path.split(attrs["path"])[0] # rewrite the content returned from the plugin for attr in attrs: env[attr] = attrs[attr] result += pre+rewritecontents(inc,cfile,env,epath)+post else: result += line result += "\n" return result def rewritefile(path,cfile,original_env): print path fin = codecs.open(path, encoding='utf-8', mode='r') content = fin.read() fin.close() return rewritecontents(content,cfile,original_env) def rewrite(cfile): fout = codecs.open(cfile.outpath, encoding='utf-8', mode='w') fout.write(rewritefile(cfile.path,cfile,{})) fout.close() def scanfiles(path,norecurse=False): directories = [] files = [] dname = "" pathsplit = os.path.split(path) if len(pathsplit)>1: dname = pathsplit[1] if not norecurse: for f in os.listdir(path): fpath = os.path.join(path,f) if os.path.isdir(fpath): subdir = scanfiles(fpath) if subdir != None: directories.append(subdir) for f in os.listdir(path): fpath = os.path.join(path,f) if fpath.startswith("./"): fpath = fpath[2:] if os.path.isfile(fpath): (root,ext) = os.path.splitext(fpath) outpath = root+".html" if ext in filter: relpath = os.path.join(os.path.split(path)[1],os.path.split(outpath)[1]) print "processing:"+fpath cfile = cmsfile(fpath,outpath) files.append(cfile) fin = open(fpath,"r") contents = fin.read() for func in sorted(plugin_consumers): plugin_consumers[func](cfile,contents) if len(files)>0 or len(directories)>0: return cmsdirectory(dname,files,directories) else: return None def processfiles(directory): for f in directory.files: rewrite(f) for d in directory.directories: processfiles(d) if __name__ == '__main__': norecurse = False if len(sys.argv)>1 and sys.argv[1] == "-norecurse": norecurse = True loadplugins() rootdir = scanfiles(".",norecurse) processfiles(rootdir)