# Copyright (c) 2005-2009 Guido Wesdorp. All rights reserved.
# This software is distributed under the terms of the Templess
# License. See LICENSE.txt for license text.
# E-mail: johnny@johnnydebris.net
""" A very compact, XML based templating system.
It has only 5 different directives, and doesn't allow any logic inside the
templates, instead it expects the application to provide a dict with all
the data (as strings or XML nodes) completely prepared. This makes it quite
restrictive, and harder to seperately work on design and logic (the
template has to very strictly adhere to the format of the dict), but makes
it an environment extremely suitable for developers (since all development
is done from code rather than from the template, and they don't have to
learn a new, quirky templating language) and for projects where HTML and
code development are strictly seperated.
Main features/advantages compared to other templating languages:
* templates are very clean
* it's easy to learn, and relatively simple code
* templates and results are well-formed XML, so processable
* proper unicode support
* proper generative rendering (so suitable for async environments)
Quick example:
>>> from templess.templess import template
>>> t = template('''
...
...
...
... ''')
>>> print t.unicode({'bar': 'bar content'})
bar content
"""
import nanosax
# this here for backward compatibility - will be removed some day...
from util import xmlstring, objectcontext
__appname__ = 'templess'
__version__ = '1.0 unreleased'
__author__ = 'Guido Wesdorp '
__last_modified_date__ = \
'$Date$'
__last_author__ = '$Author$'
__revision__ = '$Revision$'
__footer__ = '%s v%s, (c) %s 2005-2009' % (
__appname__, __version__, __author__)
XMLNS = 'http://johnnydebris.net/xmlns/templess'
# tree stuff
class nodebase(object):
""" node base
very specific to Templess, the nodes contain as little functionality
as possible
"""
def find(self, name):
return []
class elnode(list, nodebase):
""" XML element
"""
def __init__(self, name, attrs, parent, charset='UTF-8'):
self.name = name
self.attrs = dict(attrs)
self.parent = parent
self.charset = charset
if parent is not None:
parent.append(self)
def __repr__(self):
return '<%s "%s">' % (self.__class__.__name__, self.name)
def find(self, name):
if self.name == name:
yield self
for child in self:
for found in child.find(name):
yield found
@property
def children(self):
return self.__iter__()
class templessnode(elnode):
""" templess element node
has a special method 'convert' that makes it convert itself to a
normal elnode by processing all templess directives (returns a new
node, doesn't process in-place)
"""
def __init__(self, name, attrs, parent, t_prefix, charset='UTF-8'):
self.name = name
self.attrs = dict(attrs)
self.parent = parent
self.t_prefix = t_prefix
self.charset = charset
if parent is not None:
parent.append(self)
class textnode(nodebase):
""" text element
"""
def __init__(self, text, parent, charset='UTF-8'):
if not isinstance(text, unicode):
text = unicode(text, charset)
self.text = text
self.parent = parent
self.charset = charset
if parent is not None:
parent.append(self)
def __repr__(self):
reprtext = self.text[10:]
if len(reprtext) < len(self.text):
reprtext += '...'
return '<%s "%s">' % (self.__class__.__name__, reprtext)
class cdatanode(textnode):
""" cdata node
"""
class commentnode(textnode):
""" comment node
"""
# nanosax handler
class treebuilderhandler(nanosax.nshandler):
""" nanosax handler to convert XML to a node tree
"""
def __init__(self, charset):
self.charset = charset
def startdoc(self):
self.current = None
self.root = None
self.templess_prefix = None
def enddoc(self):
pass
def startel(self, name, attrs):
for key, value in attrs.items():
if key.startswith('xmlns') and value == XMLNS:
if not ':' in key:
self.templess_prefix = ''
else:
self.templess_prefix = key.split(':')[1]
self.current = templessnode(name, attrs,
self.current, self.templess_prefix,
self.charset)
if self.root is None:
self.root = self.current
def endel(self, name):
self.current = self.current.parent
def text(self, text):
textnode(text, self.current, self.charset)
def comment(self, text):
commentnode(text, self.current, self.charset)
def cdata(self, text):
cdatanode(text, self.current, self.charset)
class template(object):
""" a Templess template
call 'unicode()' to get a unicode string, 'generate()' to get a
generator that yields bits of string, and 'render()' to get a node
"""
def __init__(self, data, charset='UTF-8'):
self.charset = charset
if hasattr(data, 'read'):
data = data.read()
if charset != 'UTF-8':
data = unicode(data, charset)
if isinstance(data, unicode):
data = data.encode('UTF-8')
handler = treebuilderhandler(charset)
parser = nanosax.nsparser(handler)
parser.parse(data)
self.tree = handler.root
def render_to_string(self, context, charset='UTF-8', html=False):
""" returns a complete string with the rendered template
"""
# not preferred anymore... better use template.unicode()
return self.unicode(context, html=html).encode(charset)
def unicode(self, context, html=False):
""" returns a unicode rendering of the template
"""
from convert import xmlserializer
s = xmlserializer(self.tree)
s.convert(context)
return s.unicode(html=html)
def generate(self, context, html=False):
""" returns a generator that generates bits of string on demand
"""
from convert import xmlgenerator
s = xmlgenerator(self.tree)
return s.generate(context, html=html)
def convert(self, context):
""" convert the tree to a 'normal' node
processes all templess directives and returns the resulting tree
(a copy, doesn't process in-place)
"""
from convert import convertor
s = convertor(self.tree)
node = s.convert(context)
return node
# old name, I guess it should be removed
render = convert
class cgitemplate:
""" simple cgi engine for serving templess templates
"""
def __init__(self, template, content_type='text/html; charset=UTF-8',
error_template='templates/cgierror.html', headers={}):
self.template = template
self.content_type = content_type
self.error_template = error_template
self.headers = headers
def render(self, context):
""" render and output the document
this also prints the Content-Type header, if you want it to print
other headers use the headers arg of the constructor
"""
if not context.has_key('footer'):
context['footer'] = __footer__
try:
fp = open(self.template)
try:
t = template(fp)
ret = t.render_to_string(context, html=True)
finally:
fp.close()
assert type(ret) == str
self.print_output(self.content_type, ret)
except:
import sys
exc, e, tb = sys.exc_info()
ret = self.handle_exception(exc, e, tb)
def handle_exception(self, exc, e, tb):
""" internal method that generates and prints an exception page
"""
from traceback import format_tb
tblist = format_tb(tb)
fp = open(self.error_template)
try:
t = template(fp)
context = {
'exception': str(exc),
'message': str(e),
'traceback': '\n'.join(tblist),
'tblist': [{'line': str(l)} for l in tblist],
'footer': __footer__,
}
ret = t.render_to_string(context, html=True)
finally:
fp.close()
self.print_output('text/html; charset=UTF-8', ret)
def print_output(self, content_type, body):
""" actually print the output
"""
print 'Content-Type: %s' % content_type
for kv in self.headers.items():
print '%s: %s' % kv
print
print body