microcms
Microcms shows how easy it is to perform simple content management tasks using custom python scripts. Microcms searches for and reads files ending in .chtml in and under the current directory and processes them to output a file with a .html extension. Microcms uses plugins (dymanically loaded python scripts) to accomplish specific tasks.
usage is (run from the root directory of the site):
python microcms.py
microcms invokes built-in processing when it encounters the following markup.
-
<microcms function="include" path="..." />
microcms will replace this <microcms> tag with the contents of the file identified by the path attribute
-
...$root...
microcms will expand this placeholder with the relative path from the page being processed, to the root directory.
-
<microcms function="plugin-name" [attribute="value"]... />
Microcms invokes the process function in the named plugin script to replace the tag with a string returned by the plugin. See the description in the section
Microcms plugins below for more details.
Microcms plugins
Plugin scripts are located in the plugins sub-directory relative to microcms.py. Scripts may implement either or both of the following hook functions:
- consume(cfile,content)
During the scanning phase, this function will be invoked on any .chtml file. Parameter cfile is an object representing the file in microcms. Parameter content is a string containing the file contents. This function is implemented by plugins which collect information on the file. and may attach this information to the cfile object. cfile is described in the code listing for microcms provided at the end of this page.
- process(cfile,attrs)
During the processing phase, this function will be invoked on a .chtml file when a <microcms> tag is encountered. The cfile parameter is an object represents the file being processed. The attrs parameter provides name/value pairs for attributes in the tag being processed.
The following plugins have been implemented to provide functionality which generates this web site:
Code for microcms is shown below:
microcms.py# 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 ./.. in HTML files to the relative path to the top level directory
# transform HTML content using plugins
# usage:
# cd <root-directory-of-site>
# 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'(.*)<microcms([^>]*)>(.*)')
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)