Index: /trunk/COPYING
===================================================================
--- /trunk/COPYING	(revision 30)
+++ /trunk/COPYING	(revision 30)
@@ -0,0 +1,28 @@
+Copyright (C) 2006 Christopher Lenz
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in
+    the documentation and/or other materials provided with the
+    distribution.
+ 3. The name of the author may not be used to endorse or promote
+    products derived from this software without specific prior
+    written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Index: /trunk/README.txt
===================================================================
--- /trunk/README.txt	(revision 30)
+++ /trunk/README.txt	(revision 30)
@@ -0,0 +1,11 @@
+About Markup
+============
+
+Markup is a Python library that provides a integrated set of components
+for parsing, generating, and processing HTML or XML content in a uniform
+manner. The major feature is a template language, which is heavily
+inspired by Kid.
+
+For more information visit the Markup web site:
+
+  <http://markup.cmlenz.net/>
Index: /trunk/examples/basic/kidrun.py
===================================================================
--- /trunk/examples/basic/kidrun.py	(revision 20)
+++ /trunk/examples/basic/kidrun.py	(revision 30)
@@ -7,6 +7,5 @@
 def test():
     base_path = os.path.dirname(os.path.abspath(__file__))
-    kid.path = kid.TemplatePath([os.path.join(base_path, 'common'),
-                                 os.path.join(base_path, 'module')])
+    kid.path = kid.TemplatePath([base_path])
 
     ctxt = dict(hello='<world>', hey='ZYX', bozz=None,
Index: /trunk/examples/basic/layout.html
===================================================================
--- /trunk/examples/basic/layout.html	(revision 30)
+++ /trunk/examples/basic/layout.html	(revision 30)
@@ -0,0 +1,15 @@
+<div xmlns:py="http://purl.org/kid/ns#" py:strip="">
+  <head>
+    <title>Hello ${hello}</title>
+    <style type="text/css">@import(style.css)</style>
+  </head>
+  <div py:def="macro1">reference me, please</div>
+  <div py:def="macro2(name, classname='expanded')" class="${classname}">
+    Hello ${name.title()}
+  </div>
+  <span py:match="greeting" class="greeting">
+    Hello ${select('@name')}
+  </span>
+  <span py:match="span[@class='greeting']" style="text-decoration: underline" 
+        py:content="select('text()')"/>
+</div>
Index: /trunk/examples/basic/layout.kid
===================================================================
--- /trunk/examples/basic/layout.kid	(revision 30)
+++ /trunk/examples/basic/layout.kid	(revision 30)
@@ -0,0 +1,15 @@
+<div xmlns:py="http://purl.org/kid/ns#" py:strip="">
+  <head>
+    <title>Hello ${hello}</title>
+    <style type="text/css">@import(style.css)</style>
+  </head>
+  <div py:def="macro1">reference me, please</div>
+  <div py:def="macro2(name, classname='expanded')" class="${classname}">
+    Hello ${name.title()}
+  </div>
+  <span py:match="item.tag == '{http://www.w3.org/1999/xhtml}greeting'" class="greeting">
+    Hello ${item.get('name')}
+  </span>
+  <span py:match="item.tag == '{http://www.w3.org/1999/xhtml}span' and item.get('class') == 'greeting'"
+        py:content="item.findtext('')" style="text-decoration: underline" />
+</div>
Index: /trunk/examples/basic/run.py
===================================================================
--- /trunk/examples/basic/run.py	(revision 20)
+++ /trunk/examples/basic/run.py	(revision 30)
@@ -10,7 +10,5 @@
 def test():
     base_path = os.path.dirname(os.path.abspath(__file__))
-    loader = TemplateLoader([os.path.join(base_path, 'common'),
-                             os.path.join(base_path, 'module')],
-                            auto_reload=True)
+    loader = TemplateLoader([base_path], auto_reload=True)
 
     start = datetime.now()
Index: /trunk/examples/basic/test.html
===================================================================
--- /trunk/examples/basic/test.html	(revision 30)
+++ /trunk/examples/basic/test.html	(revision 30)
@@ -0,0 +1,23 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://purl.org/kid/ns#"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      lang="en">
+ <xi:include href="layout.html" />
+ <xi:include href="custom_stuff.html"><xi:fallback/></xi:include>
+ <body class="$bozz">
+  <ul py:attrs="{'id': 'second', 'class': None}" py:if="len(items) > 0">
+   <li py:for="item in items">Item $prefix${item.split()[-1]}</li>
+   XYZ ${hey}
+  </ul>
+  ${macro1()} ${macro1()} ${macro1()}
+  ${macro2('john')}
+  ${macro2('kate', classname='collapsed')}
+  <div py:content="macro2('helmut')" py:strip="">Replace me</div>
+  <greeting name="Dude" />
+  <greeting name="King" />
+  <span class="greeting">Hello Silicon</span>
+ </body>
+</html>
Index: /trunk/examples/basic/test.kid
===================================================================
--- /trunk/examples/basic/test.kid	(revision 30)
+++ /trunk/examples/basic/test.kid	(revision 30)
@@ -0,0 +1,21 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://purl.org/kid/ns#"
+      py:extends="'layout.kid'"
+      lang="en">
+ <body class="${bozz}">
+  <ul py:attrs="{'id': 'second', 'class': None}" py:if="len(items) > 0">
+   <li py:for="item in items">Item $prefix${item.split()[-1]}</li>
+   XYZ ${hey}
+  </ul>
+  ${macro1()} ${macro1()} ${macro1()}
+  ${macro2('john')}
+  ${macro2('kate', classname='collapsed')}
+  <div py:content="macro2('helmut')" py:strip="">Replace me</div>
+  <greeting name="Dude" />
+  <greeting name="King" />
+  <span class="greeting">Hello Silicon</span>
+ </body>
+</html>
Index: /trunk/examples/includes/common/macros.html
===================================================================
--- /trunk/examples/includes/common/macros.html	(revision 30)
+++ /trunk/examples/includes/common/macros.html	(revision 30)
@@ -0,0 +1,12 @@
+<div xmlns:py="http://purl.org/kid/ns#"
+     py:strip="">
+  <div py:def="macro1">reference me, please</div>
+  <div py:def="macro2(name, classname='expanded')" class="${classname}">
+   Hello ${name.title()}
+  </div>
+  <span py:match="greeting" class="greeting">
+    Hello ${select('@name')}
+  </span>
+  <span py:match="span[@class='greeting']" style="text-decoration: underline" 
+        py:content="select('text()')"/>
+</div>
Index: /trunk/examples/includes/module/test.html
===================================================================
--- /trunk/examples/includes/module/test.html	(revision 30)
+++ /trunk/examples/includes/module/test.html	(revision 30)
@@ -0,0 +1,22 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://purl.org/kid/ns#"
+      xmlns:xi="http://www.w3.org/2001/XInclude">
+ <xi:include href="${skin}/layout.html" />
+ <xi:include href="custom_stuff.html"><xi:fallback/></xi:include>
+ <body class="$bozz">
+  <ul py:attrs="{'id': 'second', 'class': None}" py:if="len(items) > 0">
+   <li py:for="item in items">Item ${item.split()[-1]}</li>
+   XYZ ${hey}
+  </ul>
+  ${macro1()} ${macro1()} ${macro1()}
+  ${macro2('john')}
+  ${macro2('kate', classname='collapsed')}
+  <div py:content="macro2('helmut')" py:strip="">Replace me</div>
+  <greeting name="Dude" />
+  <greeting name="King" />
+  <span class="greeting">Hello Silicon</span>
+ </body>
+</html>
Index: /trunk/examples/includes/run.py
===================================================================
--- /trunk/examples/includes/run.py	(revision 30)
+++ /trunk/examples/includes/run.py	(revision 30)
@@ -0,0 +1,49 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import timing
+
+from markup.template import Context, TemplateLoader
+
+def test():
+    base_path = os.path.dirname(os.path.abspath(__file__))
+    loader = TemplateLoader([os.path.join(base_path, 'skins'),
+                             os.path.join(base_path, 'module'),
+                             os.path.join(base_path, 'common')])
+
+    timing.start()
+    tmpl = loader.load('test.html')
+    timing.finish()
+    print ' --> parse stage: %dms' % timing.milli()
+
+    data = dict(hello='<world>', skin='default', hey='ZYX', bozz=None,
+                items=['Number %d' % num for num in range(1, 15)])
+
+    print tmpl.generate(Context(**data)).render(method='html')
+
+    times = []
+    for i in range(100):
+        timing.start()
+        list(tmpl.generate(Context(**data)))
+        timing.finish()
+        times.append(timing.milli())
+        sys.stdout.write('.')
+        sys.stdout.flush()
+    print
+
+    print ' --> render stage: %dms (avg), %dms (min), %dms (max)' % (
+          sum(times) / len(times), min(times), max(times))
+
+if __name__ == '__main__':
+    if '-p' in sys.argv:
+        import hotshot, hotshot.stats
+        prof = hotshot.Profile("template.prof")
+        benchtime = prof.runcall(test)
+        stats = hotshot.stats.load("template.prof")
+        stats.strip_dirs()
+        stats.sort_stats('time', 'calls')
+        stats.print_stats()
+    else:
+        test()
Index: /trunk/examples/includes/skins/default/footer.html
===================================================================
--- /trunk/examples/includes/skins/default/footer.html	(revision 30)
+++ /trunk/examples/includes/skins/default/footer.html	(revision 30)
@@ -0,0 +1,4 @@
+<div id="footer">
+  <hr />
+  <h1>And goodbye</h1>
+</div>
Index: /trunk/examples/includes/skins/default/header.html
===================================================================
--- /trunk/examples/includes/skins/default/header.html	(revision 30)
+++ /trunk/examples/includes/skins/default/header.html	(revision 30)
@@ -0,0 +1,3 @@
+<div id="masthead">
+  <h1>Welcome</h1>
+</div>
Index: /trunk/examples/includes/skins/default/layout.html
===================================================================
--- /trunk/examples/includes/skins/default/layout.html	(revision 30)
+++ /trunk/examples/includes/skins/default/layout.html	(revision 30)
@@ -0,0 +1,17 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://purl.org/kid/ns#"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      py:strip="">
+ <xi:include href="../macros.html" />
+ <head>
+  <title>Hello ${hello}</title>
+  <style type="text/css">@import(style.css)</style>
+ </head>
+ <body py:match="body">
+   <xi:include href="header.html" />
+   <div id="content">
+     ${select('body/*')}
+   </div>
+   <xi:include href="footer.html" />
+ </body>
+</html>
Index: /trunk/markup/__init__.py
===================================================================
--- /trunk/markup/__init__.py	(revision 20)
+++ /trunk/markup/__init__.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 """This package provides various means for generating and processing web markup
@@ -48,10 +48,9 @@
 
 >>> from markup.builder import tag
->>> doc = tag.DOC(tag.TITLE('My document'), lang='en')
+>>> doc = tag.doc(tag.title('My document'), lang='en')
 >>> doc.generate().render(method='html')
 '<doc lang="en"><title>My document</title></doc>'
-
 """
 
 from markup.core import *
-from markup.input import XML, HTML
+from markup.input import ParseError, XML, HTML
Index: /trunk/markup/builder.py
===================================================================
--- /trunk/markup/builder.py	(revision 20)
+++ /trunk/markup/builder.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 from markup.core import Attributes, Namespace, QName, Stream
@@ -18,4 +18,7 @@
 
 class Fragment(object):
+    """Represents a markup fragment, which is basically just a list of element
+    or text nodes.
+    """
     __slots__ = ['children']
 
@@ -49,5 +52,5 @@
 
     def generate(self):
-        """Generator that yield tags and text nodes as strings."""
+        """Return a markup event stream for the fragment."""
         def _generate():
             for child in self.children:
@@ -161,5 +164,5 @@
 
     def generate(self):
-        """Generator that yield tags and text nodes as strings."""
+        """Return a markup event stream for the fragment."""
         def _generate():
             yield Stream.START, (self.tag, self.attrib), (-1, -1)
@@ -171,14 +174,48 @@
 
 class ElementFactory(object):
+    """Factory for `Element` objects.
+    
+    A new element is created simply by accessing a correspondingly named
+    attribute of the factory object:
+    
+    >>> factory = ElementFactory()
+    >>> print factory.foo
+    <foo/>
+    >>> print factory.foo(id=2)
+    <foo id="2"/>
+    
+    A factory can also be bound to a specific namespace:
+    
+    >>> factory = ElementFactory('http://www.w3.org/1999/xhtml')
+    >>> print factory.html(lang="en")
+    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"/>
+    
+    The namespace for a specific element can be altered on an existing factory
+    by specifying the new namespace using item access:
+    
+    >>> factory = ElementFactory()
+    >>> print factory.html(factory['http://www.w3.org/2000/svg'].g(id=3))
+    <html><g id="3" xmlns="http://www.w3.org/2000/svg"/></html>
+    
+    Usually, the `ElementFactory` class is not be used directly. Rather, the
+    `tag` instance should be used to create elements.
+    """
 
     def __init__(self, namespace=None):
-        if not isinstance(namespace, Namespace):
+        """Create the factory, optionally bound to the given namespace.
+        
+        @param namespace: the namespace URI for any created elements, or `None`
+            for no namespace
+        """
+        if namespace and not isinstance(namespace, Namespace):
             namespace = Namespace(namespace)
         self.namespace = namespace
 
     def __getitem__(self, namespace):
+        """Return a new factory that is bound to the specified namespace."""
         return ElementFactory(namespace)
 
     def __getattr__(self, name):
+        """Create an `Element` with the given name."""
         return Element(self.namespace and self.namespace[name] or name)
 
Index: /trunk/markup/core.py
===================================================================
--- /trunk/markup/core.py	(revision 20)
+++ /trunk/markup/core.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 """Core classes for markup processing."""
@@ -58,5 +58,5 @@
         """Initialize the stream with a sequence of markup events.
         
-        @oaram events: a sequence or iterable providing the events
+        @param events: a sequence or iterable providing the events
         """
         self.events = events
@@ -112,5 +112,5 @@
                    'html': output.HTMLSerializer}[method]
         else:
-            assert issubclass(cls, serializers.Serializer)
+            assert issubclass(cls, output.Serializer)
         serializer = cls(**kwargs)
 
@@ -169,5 +169,7 @@
         of `(name, value)` tuples.
         """
-        list.__init__(self, map(lambda (k, v): (QName(k), v), attrib or []))
+        if attrib is None:
+            attrib = []
+        list.__init__(self, [(QName(name), value) for name, value in attrib])
 
     def __contains__(self, name):
@@ -175,5 +177,5 @@
         name.
         """
-        return name in [attr for attr, value in self]
+        return name in [attr for attr, _ in self]
 
     def get(self, name, default=None):
@@ -217,8 +219,8 @@
     __slots__ = []
 
-    def __new__(self, text='', *args):
+    def __new__(cls, text='', *args):
         if args:
             text %= tuple([escape(arg) for arg in args])
-        return unicode.__new__(self, text)
+        return unicode.__new__(cls, text)
 
     def __add__(self, other):
@@ -258,5 +260,6 @@
             else: # character entity
                 ref = match.group(2)
-                if keepxmlentities and ref in ('amp', 'apos', 'gt', 'lt', 'quot'):
+                if keepxmlentities and ref in ('amp', 'apos', 'gt', 'lt',
+                                               'quot'):
                     return '&%s;' % ref
                 try:
Index: /trunk/markup/eval.py
===================================================================
--- /trunk/markup/eval.py	(revision 20)
+++ /trunk/markup/eval.py	(revision 30)
@@ -6,9 +6,11 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
+
+"""Support for "safe" evaluation of Python expressions."""
 
 import __builtin__
@@ -102,8 +104,17 @@
 
     def __init__(self, source):
+        """Create the expression.
+        
+        @param source: the expression as string
+        """
         self.source = source
         self.ast = None
 
     def evaluate(self, data):
+        """Evaluate the expression against the given data dictionary.
+        
+        @param data: a mapping containing the data to evaluate against
+        @return: the result of the evaluation
+        """
         if not self.ast:
             self.ast = compiler.parse(self.source, 'eval')
Index: /trunk/markup/filters.py
===================================================================
--- /trunk/markup/filters.py	(revision 20)
+++ /trunk/markup/filters.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 """Implementation of a number of stream filters."""
@@ -54,5 +54,4 @@
         in_fallback = False
         include_href, fallback_stream = None, None
-        indent = 0
 
         for kind, data, pos in stream:
@@ -63,5 +62,4 @@
                 if tag.localname == 'include':
                     include_href = attrib.get('href')
-                    indent = pos[1]
                 elif tag.localname == 'fallback':
                     in_fallback = True
@@ -74,5 +72,6 @@
                             raise TemplateError('Include misses required '
                                                 'attribute "href"')
-                        template = self.loader.load(include_href)
+                        template = self.loader.load(include_href,
+                                                    relative_to=pos[0])
                         for event in template.generate(ctxt):
                             yield event
@@ -86,5 +85,4 @@
                     include_href = None
                     fallback_stream = None
-                    indent = 0
 
                 elif data.localname == 'fallback':
Index: /trunk/markup/input.py
===================================================================
--- /trunk/markup/input.py	(revision 20)
+++ /trunk/markup/input.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 from xml.parsers import expat
@@ -19,16 +19,44 @@
 import HTMLParser as html
 import htmlentitydefs
-import re
 from StringIO import StringIO
 
 from markup.core import Attributes, Markup, QName, Stream
+
+
+class ParseError(Exception):
+    """Exception raised when fatal syntax errors are found in the input being
+    parsed."""
+
+    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
+        Exception.__init__(self, message)
+        self.filename = filename
+        self.lineno = lineno
+        self.offset = offset
 
 
 class XMLParser(object):
     """Generator-based XML parser based on roughly equivalent code in
-    Kid/ElementTree."""
-
-    def __init__(self, source):
+    Kid/ElementTree.
+    
+    The parsing is initiated by iterating over the parser object:
+    
+    >>> parser = XMLParser(StringIO('<root id="2"><child>Foo</child></root>'))
+    >>> for kind, data, pos in parser:
+    ...     print kind, data
+    START (u'root', [(u'id', u'2')])
+    START (u'child', [])
+    TEXT Foo
+    END child
+    END root
+    """
+
+    def __init__(self, source, filename=None):
+        """Initialize the parser for the given XML text.
+        
+        @param source: the XML text as a file-like object
+        @param filename: the name of the file, if appropriate
+        """
         self.source = source
+        self.filename = filename
 
         # Setup the Expat parser
@@ -49,62 +77,72 @@
         # Location reporting is only support in Python >= 2.4
         if not hasattr(parser, 'CurrentLineNumber'):
-            self.getpos = self._getpos_unknown
+            self._getpos = self._getpos_unknown
 
         self.expat = parser
-        self.queue = []
+        self._queue = []
 
     def __iter__(self):
-        bufsize = 4 * 1024 # 4K
-        done = False
-        while True:
-            while not done and len(self.queue) == 0:
-                data = self.source.read(bufsize)
-                if data == '': # end of data
-                    if hasattr(self, 'expat'):
-                        self.expat.Parse('', True)
-                        del self.expat # get rid of circular references
-                    done = True
-                else:
-                    self.expat.Parse(data, False)
-            for event in self.queue:
-                yield event
-            self.queue = []
-            if done:
-                break
+        try:
+            bufsize = 4 * 1024 # 4K
+            done = False
+            while True:
+                while not done and len(self._queue) == 0:
+                    data = self.source.read(bufsize)
+                    if data == '': # end of data
+                        if hasattr(self, 'expat'):
+                            self.expat.Parse('', True)
+                            del self.expat # get rid of circular references
+                        done = True
+                    else:
+                        self.expat.Parse(data, False)
+                for event in self._queue:
+                    yield event
+                self._queue = []
+                if done:
+                    break
+        except expat.ExpatError, e:
+            msg = str(e)
+            if self.filename:
+                msg += ', in ' + self.filename
+            raise ParseError(msg, self.filename, e.lineno, e.offset)
+
+    def _enqueue(self, kind, data, pos=None):
+        if pos is None:
+            pos = self._getpos()
+        self._queue.append((kind, data, pos))
 
     def _getpos_unknown(self):
-        return (-1, -1)
-
-    def getpos(self):
-        return self.expat.CurrentLineNumber, self.expat.CurrentColumnNumber
+        return (self.filename or '<string>', -1, -1)
+
+    def _getpos(self):
+        return (self.filename or '<string>', self.expat.CurrentLineNumber,
+                self.expat.CurrentColumnNumber)
 
     def _handle_start(self, tag, attrib):
-        self.queue.append((Stream.START, (QName(tag), Attributes(attrib.items())),
-                           self.getpos()))
+        self._enqueue(Stream.START, (QName(tag), Attributes(attrib.items())))
 
     def _handle_end(self, tag):
-        self.queue.append((Stream.END, QName(tag), self.getpos()))
+        self._enqueue(Stream.END, QName(tag))
 
     def _handle_data(self, text):
-        self.queue.append((Stream.TEXT, text, self.getpos()))
+        self._enqueue(Stream.TEXT, text)
 
     def _handle_prolog(self, version, encoding, standalone):
-        self.queue.append((Stream.PROLOG, (version, encoding, standalone),
-                           self.getpos()))
+        self._enqueue(Stream.PROLOG, (version, encoding, standalone))
 
     def _handle_doctype(self, name, sysid, pubid, has_internal_subset):
-        self.queue.append((Stream.DOCTYPE, (name, pubid, sysid), self.getpos()))
+        self._enqueue(Stream.DOCTYPE, (name, pubid, sysid))
 
     def _handle_start_ns(self, prefix, uri):
-        self.queue.append((Stream.START_NS, (prefix or '', uri), self.getpos()))
+        self._enqueue(Stream.START_NS, (prefix or '', uri))
 
     def _handle_end_ns(self, prefix):
-        self.queue.append((Stream.END_NS, prefix or '', self.getpos()))
+        self._enqueue(Stream.END_NS, prefix or '')
 
     def _handle_pi(self, target, data):
-        self.queue.append((Stream.PI, (target, data), self.getpos()))
+        self._enqueue(Stream.PI, (target, data))
 
     def _handle_comment(self, text):
-        self.queue.append((Stream.COMMENT, text, self.getpos()))
+        self._enqueue(Stream.COMMENT, text)
 
     def _handle_other(self, text):
@@ -113,7 +151,7 @@
             try:
                 text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
-                self.queue.append((Stream.TEXT, text, self.getpos()))
+                self._enqueue(Stream.TEXT, text)
             except KeyError:
-                lineno, offset = self.getpos()
+                lineno, offset = self._getpos()
                 raise expat.error("undefined entity %s: line %d, column %d" %
                                   (text, lineno, offset))
@@ -124,9 +162,20 @@
 
 
-class HTMLParser(html.HTMLParser):
+class HTMLParser(html.HTMLParser, object):
     """Parser for HTML input based on the Python `HTMLParser` module.
     
     This class provides the same interface for generating stream events as
     `XMLParser`, and attempts to automatically balance tags.
+    
+    The parsing is initiated by iterating over the parser object:
+    
+    >>> parser = HTMLParser(StringIO('<UL compact><LI>Foo</UL>'))
+    >>> for kind, data, pos in parser:
+    ...     print kind, data
+    START (u'ul', [(u'compact', u'compact')])
+    START (u'li', [])
+    TEXT Foo
+    END li
+    END ul
     """
 
@@ -135,36 +184,57 @@
                               'param'])
 
-    def __init__(self, source):
+    def __init__(self, source, filename=None):
         html.HTMLParser.__init__(self)
         self.source = source
-        self.queue = []
+        self.filename = filename
+        self._queue = []
         self._open_tags = []
 
     def __iter__(self):
-        bufsize = 4 * 1024 # 4K
-        done = False
-        while True:
-            while not done and len(self.queue) == 0:
-                data = self.source.read(bufsize)
-                if data == '': # end of data
-                    self.close()
-                    done = True
-                else:
-                    self.feed(data)
-            for kind, data, pos in self.queue:
-                yield kind, data, pos
-            self.queue = []
-            if done:
-                open_tags = self._open_tags
-                open_tags.reverse()
-                for tag in open_tags:
-                    yield Stream.END, QName(tag), pos
-                break
+        try:
+            bufsize = 4 * 1024 # 4K
+            done = False
+            while True:
+                while not done and len(self._queue) == 0:
+                    data = self.source.read(bufsize)
+                    if data == '': # end of data
+                        self.close()
+                        done = True
+                    else:
+                        self.feed(data)
+                for kind, data, pos in self._queue:
+                    yield kind, data, pos
+                self._queue = []
+                if done:
+                    open_tags = self._open_tags
+                    open_tags.reverse()
+                    for tag in open_tags:
+                        yield Stream.END, QName(tag), pos
+                    break
+        except html.HTMLParseError, e:
+            msg = '%s: line %d, column %d' % (e.msg, e.lineno, e.offset)
+            if self.filename:
+                msg += ', in %s' % self.filename
+            raise ParseError(msg, self.filename, e.lineno, e.offset)
+
+    def _enqueue(self, kind, data, pos=None):
+        if pos is None:
+            pos = self._getpos()
+        self._queue.append((kind, data, pos))
+
+    def _getpos(self):
+        lineno, column = self.getpos()
+        return (self.filename, lineno, column)
 
     def handle_starttag(self, tag, attrib):
-        pos = self.getpos()
-        self.queue.append((Stream.START, (QName(tag), Attributes(attrib)), pos))
+        fixed_attrib = []
+        for name, value in attrib: # Fixup minimized attributes
+            if value is None:
+                value = name
+            fixed_attrib.append((name, unicode(value)))
+
+        self._enqueue(Stream.START, (QName(tag), Attributes(fixed_attrib)))
         if tag in self._EMPTY_ELEMS:
-            self.queue.append((Stream.END, QName(tag), pos))
+            self._enqueue(Stream.END, QName(tag))
         else:
             self._open_tags.append(tag)
@@ -172,29 +242,27 @@
     def handle_endtag(self, tag):
         if tag not in self._EMPTY_ELEMS:
-            pos = self.getpos()
             while self._open_tags:
                 open_tag = self._open_tags.pop()
                 if open_tag.lower() == tag.lower():
                     break
-                self.queue.append((Stream.END, QName(open_tag), pos))
-            self.queue.append((Stream.END, QName(tag), pos))
+                self._enqueue(Stream.END, QName(open_tag))
+            self._enqueue(Stream.END, QName(tag))
 
     def handle_data(self, text):
-        self.queue.append((Stream.TEXT, text, self.getpos()))
+        self._enqueue(Stream.TEXT, text)
 
     def handle_charref(self, name):
-        self.queue.append((Stream.TEXT, Markup('&#%s;' % name), self.getpos()))
+        self._enqueue(Stream.TEXT, Markup('&#%s;' % name))
 
     def handle_entityref(self, name):
-        self.queue.append((Stream.TEXT, Markup('&%s;' % name), self.getpos()))
+        self._enqueue(Stream.TEXT, Markup('&%s;' % name))
 
     def handle_pi(self, data):
         target, data = data.split(maxsplit=1)
         data = data.rstrip('?')
-        self.queue.append((Stream.PI, (target.strip(), data.strip()),
-                           self.getpos()))
+        self._enqueue(Stream.PI, (target.strip(), data.strip()))
 
     def handle_comment(self, text):
-        self.queue.append((Stream.COMMENT, text, self.getpos()))
+        self._enqueue(Stream.COMMENT, text)
 
 
Index: /trunk/markup/output.py
===================================================================
--- /trunk/markup/output.py	(revision 20)
+++ /trunk/markup/output.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 """This module provides different kinds of serialization methods for XML event
@@ -22,5 +22,4 @@
 
 from markup.core import Markup, Namespace, QName, Stream
-from markup.filters import WhitespaceFilter
 
 __all__ = ['Serializer', 'XMLSerializer', 'HTMLSerializer']
@@ -31,4 +30,10 @@
 
     def serialize(self, stream):
+        """Must be implemented by concrete subclasses to serialize the given
+        stream.
+        
+        This method must be implemented as a generator, producing the
+        serialized output incrementally as unicode strings.
+        """
         raise NotImplementedError
 
@@ -38,5 +43,5 @@
     
     >>> from markup.builder import tag
-    >>> elem = tag.DIV(tag.A(href='foo'), tag.BR, tag.HR(noshade=True))
+    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
     >>> print ''.join(XMLSerializer().serialize(elem.generate()))
     <div><a href="foo"/><br/><hr noshade="True"/></div>
@@ -47,5 +52,5 @@
         ns_mapping = {}
 
-        stream = PushbackIterator(stream)
+        stream = _PushbackIterator(stream)
         for kind, data, pos in stream:
 
@@ -82,9 +87,5 @@
                     attrname = attr.localname
                     if attr.namespace:
-                        try:
-                            prefix = ns_mapping[attr.namespace]
-                        except KeyError:
-                            # FIXME: synthesize a prefix for the attribute?
-                            prefix = ''
+                        prefix = ns_mapping.get(attr.namespace)
                         if prefix:
                             attrname = prefix + ':' + attrname
@@ -104,10 +105,7 @@
                 tagname = tag.localname
                 if tag.namespace:
-                    try:
-                        prefix = ns_mapping[tag.namespace]
-                        if prefix:
-                            tagname = prefix + ':' + tag.localname
-                    except KeyError:
-                        pass
+                    prefix = ns_mapping.get(tag.namespace)
+                    if prefix:
+                        tagname = prefix + ':' + tag.localname
                 yield Markup('</%s>' % tagname)
 
@@ -120,5 +118,5 @@
     
     >>> from markup.builder import tag
-    >>> elem = tag.DIV(tag.A(href='foo'), tag.BR, tag.HR(noshade=True))
+    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
     >>> print ''.join(HTMLSerializer().serialize(elem.generate()))
     <div><a href="foo"></a><br><hr noshade></div>
@@ -137,5 +135,5 @@
         ns_mapping = {}
 
-        stream = PushbackIterator(stream)
+        stream = _PushbackIterator(stream)
         for kind, data, pos in stream:
 
@@ -180,5 +178,5 @@
 
 
-class PushbackIterator(object):
+class _PushbackIterator(object):
     """A simple wrapper for iterators that allows pushing items back on the
     queue via the `pushback()` method.
Index: /trunk/markup/path.py
===================================================================
--- /trunk/markup/path.py	(revision 20)
+++ /trunk/markup/path.py	(revision 30)
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006 Christopher Lenz
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 """Basic support for evaluating XPath expressions against streams."""
@@ -20,83 +20,22 @@
 __all__ = ['Path']
 
-_QUOTES = (("'", "'"), ('"', '"'))
 
 class Path(object):
-    """Basic XPath support on markup event streams.
+    """Implements basic XPath support on streams.
     
-    >>> from markup.input import XML
-    
-    Selecting specific tags:
-    
-    >>> Path('root').select(XML('<root/>')).render()
-    '<root/>'
-    >>> Path('//root').select(XML('<root/>')).render()
-    '<root/>'
-    
-    Using wildcards for tag names:
-    
-    >>> Path('*').select(XML('<root/>')).render()
-    '<root/>'
-    >>> Path('//*').select(XML('<root/>')).render()
-    '<root/>'
-    
-    Selecting attribute values:
-    
-    >>> Path('@foo').select(XML('<root/>')).render()
-    ''
-    >>> Path('@foo').select(XML('<root foo="bar"/>')).render()
-    'bar'
-    
-    Selecting descendants:
-    
-    >>> Path("root/*").select(XML('<root><foo/><bar/></root>')).render()
-    '<foo/><bar/>'
-    >>> Path("root/bar").select(XML('<root><foo/><bar/></root>')).render()
-    '<bar/>'
-    >>> Path("root/baz").select(XML('<root><foo/><bar/></root>')).render()
-    ''
-    >>> Path("root/foo/*").select(XML('<root><foo><bar/></foo></root>')).render()
-    '<bar/>'
-    
-    Selecting text nodes:
-    >>> Path("item/text()").select(XML('<root><item>Foo</item></root>')).render()
-    'Foo'
-    >>> Path("item/text()").select(XML('<root><item>Foo</item><item>Bar</item></root>')).render()
-    'FooBar'
-    
-    Skipping ancestors:
-    
-    >>> Path("foo/bar").select(XML('<root><foo><bar/></foo></root>')).render()
-    '<bar/>'
-    >>> Path("foo/*").select(XML('<root><foo><bar/></foo></root>')).render()
-    '<bar/>'
-    >>> Path("root/bar").select(XML('<root><foo><bar/></foo></root>')).render()
-    ''
-    >>> Path("root/bar").select(XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>')).render()
-    '<bar id="2"/>'
-    >>> Path("root/*/bar").select(XML('<root><foo><bar/></foo></root>')).render()
-    '<bar/>'
-    >>> Path("root//bar").select(XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>')).render()
-    '<bar id="1"/><bar id="2"/>'
-    >>> Path("root//bar").select(XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>')).render()
-    '<bar id="1"/><bar id="2"/>'
-    
-    Using simple attribute predicates:
-    >>> Path("root/item[@important]").select(XML('<root><item/><item important="very"/></root>')).render()
-    '<item important="very"/>'
-    >>> Path('root/item[@important="very"]').select(XML('<root><item/><item important="very"/></root>')).render()
-    '<item important="very"/>'
-    >>> Path("root/item[@important='very']").select(XML('<root><item/><item important="notso"/></root>')).render()
-    ''
-    >>> Path("root/item[@important!='very']").select(
-    ...     XML('<root><item/><item important="notso"/></root>')).render()
-    '<item/><item important="notso"/>'
+    Instances of this class represent a "compiled" XPath expression, and provide
+    methods for testing the path against a stream, as well as extracting a
+    substream matching that path.
     """
-
     _TOKEN_RE = re.compile('(::|\.\.|\(\)|[/.:\[\]\(\)@=!])|'
                            '([^/:\[\]\(\)@=!\s]+)|'
                            '\s+')
+    _QUOTES = (("'", "'"), ('"', '"'))
 
     def __init__(self, text):
+        """Create the path object from a string.
+        
+        @param text: the path expression
+        """
         self.source = text
 
@@ -113,5 +52,5 @@
                 elif op.startswith('('):
                     if cur_tag == 'text':
-                        steps[-1] = (False, self.fn_text(), [])
+                        steps[-1] = (False, self._FunctionText(), [])
                     else:
                         raise NotImplementedError('XPath function "%s" not '
@@ -124,21 +63,23 @@
                 if cur_op == '@':
                     if tag == '*':
-                        node_test = self.any_attribute()
+                        node_test = self._AnyAttribute()
                     else:
-                        node_test = self.attribute_by_name(tag)
+                        node_test = self._AttributeByName(tag)
                 else:
                     if tag == '*':
-                        node_test = self.any_element()
+                        node_test = self._AnyElement()
                     elif in_predicate:
-                        if len(tag) > 1 and (tag[0], tag[-1]) in _QUOTES:
-                            node_test = self.literal_string(tag[1:-1])
+                        if len(tag) > 1 and (tag[0], tag[-1]) in self._QUOTES:
+                            node_test = self._LiteralString(tag[1:-1])
                         if cur_op == '=':
-                            node_test = self.op_eq(steps[-1][2][-1], node_test)
+                            node_test = self._OperatorEq(steps[-1][2][-1],
+                                                         node_test)
                             steps[-1][2].pop()
                         elif cur_op == '!=':
-                            node_test = self.op_neq(steps[-1][2][-1], node_test)
+                            node_test = self._OperatorNeq(steps[-1][2][-1],
+                                                          node_test)
                             steps[-1][2].pop()
                     else:
-                        node_test = self.element_by_name(tag)
+                        node_test = self._ElementByName(tag)
                 if in_predicate:
                     steps[-1][2].append(node_test)
@@ -153,6 +94,13 @@
 
     def select(self, stream):
+        """Returns a substream of the given stream that matches the path.
+        
+        If there are no matches, this method returns an empty stream.
+        
+        @param stream: the stream to select from
+        @return: the substream matching the path, or an empty stream
+        """
         stream = iter(stream)
-        def _generate(tests):
+        def _generate():
             test = self.test()
             for kind, data, pos in stream:
@@ -171,12 +119,18 @@
                 elif result:
                     yield result
-        return Stream(_generate(self.steps))
+        return Stream(_generate())
 
     def test(self):
+        """Returns a function that can be used to track whether the path matches
+        a specific stream event.
+        
+        The function returned expects the positional arguments `kind`, `data`,
+        and `pos`, i.e. basically an unpacked stream event. If the path matches
+        the event, the function returns the match (for example, a `START` or
+        `TEXT` event.) Otherwise, it returns `None` or `False`.
+        """
         stack = [0] # stack of cursors into the location path
 
         def _test(kind, data, pos):
-            #print '\nTracker %r test [%s] %r' % (self, kind, data)
-
             if not stack:
                 return False
@@ -192,5 +146,4 @@
             closure, node_test, predicates = self.steps[stack[-1]]
 
-            #print '  Testing against %r' % node_test
             matched = node_test(kind, data, pos)
             if matched and predicates:
@@ -202,15 +155,13 @@
             if matched:
                 if stack[-1] == len(self.steps) - 1:
-                    #print '  Last step %r... returned %r' % (node_test, matched)
                     return matched
 
-                #print '  Matched intermediate step %r... proceed to next step %r' % (node_test, self.steps[stack[-1] + 1])
                 stack[-1] += 1
 
             elif kind is Stream.START and not closure:
-                # FIXME: If this step is not a closure, it cannot be matched
-                #        until the current element is closed... so we need to
-                #        move the cursor back to the last closure and retest
-                #        that against the current element
+                # If this step is not a closure, it cannot be matched until the
+                # current element is closed... so we need to move the cursor
+                # back to the last closure and retest that against the current
+                # element
                 closures = [step for step in self.steps[:stack[-1]] if step[0]]
                 closures.reverse()
@@ -227,6 +178,7 @@
         return _test
 
-    class any_element(object):
-        def __call__(self, kind, data, pos):
+    class _AnyElement(object):
+        """Node test that matches any element."""
+        def __call__(self, kind, *_):
             if kind is Stream.START:
                 return True
@@ -235,8 +187,9 @@
             return '<%s>' % self.__class__.__name__
 
-    class element_by_name(object):
+    class _ElementByName(object):
+        """Node test that matches an element with a specific tag name."""
         def __init__(self, name):
             self.name = QName(name)
-        def __call__(self, kind, data, pos):
+        def __call__(self, kind, data, _):
             if kind is Stream.START:
                 return data[0].localname == self.name
@@ -245,8 +198,9 @@
             return '<%s "%s">' % (self.__class__.__name__, self.name)
 
-    class any_attribute(object):
-        def __call__(self, kind, data, pos):
-            if kind is Stream.START:
-                text = ''.join([val for name, val in data[1]])
+    class _AnyAttribute(object):
+        """Node test that matches any attribute."""
+        def __call__(self, kind, data, pos):
+            if kind is Stream.START:
+                text = ''.join([val for _, val in data[1]])
                 if text:
                     return Stream.TEXT, text, pos
@@ -256,5 +210,6 @@
             return '<%s>' % (self.__class__.__name__)
 
-    class attribute_by_name(object):
+    class _AttributeByName(object):
+        """Node test that matches an attribute with a specific name."""
         def __init__(self, name):
             self.name = QName(name)
@@ -268,5 +223,6 @@
             return '<%s "%s">' % (self.__class__.__name__, self.name)
 
-    class fn_text(object):
+    class _FunctionText(object):
+        """Function that returns text content."""
         def __call__(self, kind, data, pos):
             if kind is Stream.TEXT:
@@ -276,13 +232,15 @@
             return '<%s>' % (self.__class__.__name__)
 
-    class literal_string(object):
+    class _LiteralString(object):
+        """Always returns a literal string."""
         def __init__(self, value):
             self.value = value
-        def __call__(self, kind, data, pos):
+        def __call__(self, *_):
             return Stream.TEXT, self.value, (-1, -1)
         def __repr__(self):
             return '<%s>' % (self.__class__.__name__)
 
-    class op_eq(object):
+    class _OperatorEq(object):
+        """Equality comparison operator."""
         def __init__(self, lval, rval):
             self.lval = lval
@@ -296,5 +254,6 @@
                                      self.rval)
 
-    class op_neq(object):
+    class _OperatorNeq(object):
+        """Inequality comparison operator."""
         def __init__(self, lval, rval):
             self.lval = lval
Index: /trunk/markup/plugin.py
===================================================================
--- /trunk/markup/plugin.py	(revision 20)
+++ /trunk/markup/plugin.py	(revision 30)
@@ -1,14 +1,19 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Mattew Good
+# Copyright (C) 2006 Matthew Good
+# Copyright (C) 2006 Christopher Lenz
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
+
+"""Basic support for the template engine plugin API used by TurboGears and
+CherryPy/Buffet.
+"""
 
 import os
@@ -19,4 +24,5 @@
 
 class TemplateEnginePlugin(object):
+    """Implementation of the plugin API."""
 
     def __init__(self, extra_vars_func=None, options=None):
@@ -35,16 +41,14 @@
             package = templatename[:divider]
             basename = templatename[divider + 1:] + '.html'
-            fullpath = resource_filename(package, basename)
-            dirname, templatename = os.path.split(fullpath)
-            self.loader.search_path.append(dirname) # Kludge
+            templatename = resource_filename(package, basename)
 
         return self.loader.load(templatename)
 
     def render(self, info, format='html', fragment=False, template=None):
-        """Renders the template to a string using the provided info."""
+        """Render the template to a string using the provided info."""
         return self.transform(info, template).render(method=format)
 
     def transform(self, info, template):
-        "Render the output to Elements"
+        """Render the output to an event stream."""
         if not isinstance(template, Template):
             template = self.load_template(template)
Index: /trunk/markup/template.py
===================================================================
--- /trunk/markup/template.py	(revision 20)
+++ /trunk/markup/template.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 """Template engine that is compatible with Kid (http://kid.lesscode.org) to a
@@ -44,4 +44,5 @@
 import compiler
 import os
+import posixpath
 import re
 from StringIO import StringIO
@@ -49,5 +50,4 @@
 from markup.core import Attributes, Namespace, Stream, StreamEventKind
 from markup.eval import Expression
-from markup.filters import IncludeFilter
 from markup.input import HTML, XMLParser, XML
 from markup.path import Path
@@ -123,6 +123,6 @@
 
     def __getitem__(self, key):
-        """Get a variable's value, starting at the current context and going
-        upward.
+        """Get a variable's value, starting at the current context frame and
+        going upward.
         """
         return self.get(key)
@@ -136,4 +136,7 @@
 
     def get(self, key):
+        """Get a variable's value, starting at the current context frame and
+        going upward.
+        """
         for frame in self.stack:
             if key in frame:
@@ -141,7 +144,12 @@
 
     def push(self, **data):
+        """Push a new context frame on the stack."""
         self.stack.insert(0, data)
 
     def pop(self):
+        """Pop the top-most context frame from the stack.
+        
+        If the stack is empty, an `AssertionError` is raised.
+        """
         assert self.stack, 'Pop from empty context stack'
         self.stack.pop(0)
@@ -168,5 +176,5 @@
     __slots__ = ['expr']
 
-    def __init__(self, template, value, pos):
+    def __init__(self, value):
         self.expr = value and Expression(value) or None
 
@@ -290,6 +298,6 @@
     __slots__ = ['name', 'args', 'defaults', 'stream']
 
-    def __init__(self, template, args, pos):
-        Directive.__init__(self, template, None, pos)
+    def __init__(self, args):
+        Directive.__init__(self, None)
         ast = compiler.parse(args, 'eval').node
         self.args = []
@@ -341,8 +349,8 @@
     __slots__ = ['targets']
 
-    def __init__(self, template, value, pos):
-        targets, expr_source = value.split(' in ', 1)
+    def __init__(self, value):
+        targets, value = value.split(' in ', 1)
         self.targets = [str(name.strip()) for name in targets.split(',')]
-        Directive.__init__(self, template, expr_source, pos)
+        Directive.__init__(self, value)
 
     def __call__(self, stream, ctxt):
@@ -447,6 +455,6 @@
     __slots__ = ['path', 'stream']
 
-    def __init__(self, template, value, pos):
-        Directive.__init__(self, template, None, pos)
+    def __init__(self, value):
+        Directive.__init__(self, None)
         self.path = Path(value)
         self.stream = []
@@ -575,5 +583,5 @@
     _dir_order = [directive[1] for directive in directives]
 
-    def __init__(self, source, filename=None):
+    def __init__(self, source, basedir=None, filename=None):
         """Initialize a template from either a string or a file-like object."""
         if isinstance(source, basestring):
@@ -581,12 +589,16 @@
         else:
             self.source = source
+        self.basedir = basedir
         self.filename = filename or '<string>'
-
-        self.filters = [self._eval, self._match]
+        if basedir and filename:
+            self.filepath = os.path.join(basedir, filename)
+        else:
+            self.filepath = '<string>'
+
+        self.filters = []
         self.parse()
 
     def __repr__(self):
-        return '<%s "%s">' % (self.__class__.__name__,
-                              os.path.basename(self.filename))
+        return '<%s "%s">' % (self.__class__.__name__, self.filename)
 
     def parse(self):
@@ -604,5 +616,5 @@
         depth = 0
 
-        for kind, data, pos in XMLParser(self.source):
+        for kind, data, pos in XMLParser(self.source, filename=self.filename):
 
             if kind is Stream.START_NS:
@@ -629,7 +641,7 @@
                         cls = self._dir_by_name.get(name.localname)
                         if cls is None:
-                            raise BadDirectiveError(name, self.filename, pos[0])
+                            raise BadDirectiveError(name, self.filename, pos[1])
                         else:
-                            directives.append(cls(self, value, pos))
+                            directives.append(cls(value))
                     else:
                         value = list(self._interpolate(value, *pos))
@@ -667,5 +679,5 @@
     _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)')
 
-    def _interpolate(cls, text, lineno=-1, offset=-1):
+    def _interpolate(cls, text, filename=None, lineno=-1, offset=-1):
         """Parse the given string and extract expressions.
         
@@ -689,10 +701,15 @@
                     else:
                         yield Stream.TEXT, group.replace('$$', '$'), \
-                              (lineno, offset)
+                              (filename, lineno, offset)
         return _interpolate(text)
     _interpolate = classmethod(_interpolate)
 
     def generate(self, ctxt=None):
-        """Transform the template based on the given context data."""
+        """Apply the template to the given context data.
+        
+        @param ctxt: a `Context` instance containing the data for the template
+        @return: a markup event stream representing the result of applying
+            the template to the context data.
+        """
         if ctxt is None:
             ctxt = Context()
@@ -700,7 +717,11 @@
             ctxt._match_templates = []
 
-        return Stream(self._flatten(self.stream, ctxt))
+        stream = self._match(self._eval(self.stream, ctxt), ctxt)
+        return Stream(self._flatten(stream, ctxt))
 
     def _eval(self, stream, ctxt=None):
+        """Internal stream filter that evaluates any expressions in `START` and
+        `TEXT` events.
+        """
         for kind, data, pos in stream:
 
@@ -723,5 +744,5 @@
                         if not value:
                             continue
-                    new_attrib.append((name, ''.join(value)))
+                    new_attrib.append((name, u''.join(value)))
                 yield kind, (tag, Attributes(new_attrib)), pos
 
@@ -735,5 +756,5 @@
                 # characters
                 if isinstance(result, basestring):
-                    yield Stream.TEXT, result, pos
+                    yield Stream.TEXT, unicode(result), pos
                 else:
                     # Test if the expression evaluated to an iterable, in which
@@ -749,8 +770,8 @@
                 yield kind, data, pos
 
-    def _flatten(self, stream, ctxt=None, apply_filters=True):
-        if apply_filters:
-            for filter_ in self.filters:
-                stream = filter_(iter(stream), ctxt)
+    def _flatten(self, stream, ctxt=None):
+        """Internal stream filter that expands `SUB` events in the stream."""
+        for filter_ in self.filters:
+            stream = filter_(iter(stream), ctxt)
         try:
             for kind, data, pos in stream:
@@ -763,4 +784,5 @@
                     for directive in directives:
                         substream = directive(iter(substream), ctxt)
+                    substream = self._match(self._eval(substream, ctxt), ctxt)
                     for event in self._flatten(substream, ctxt):
                         yield event
@@ -769,8 +791,11 @@
                     yield kind, data, pos
         except SyntaxError, err:
-            raise TemplateSyntaxError(err, self.filename, pos[0],
-                                      pos[1] + (err.offset or 0))
+            raise TemplateSyntaxError(err, self.filename, pos[1],
+                                      pos[2] + (err.offset or 0))
 
     def _match(self, stream, ctxt=None):
+        """Internal stream filter that applies any defined match templates
+        to the stream.
+        """
         for kind, data, pos in stream:
 
@@ -784,5 +809,5 @@
                 if (kind, data, pos) in template[::len(template)]:
                     # This is the event this match template produced itself, so
-                    # matching it  again would result in an infinite loop 
+                    # matching it again would result in an infinite loop 
                     continue
 
@@ -805,8 +830,7 @@
                         test(*event)
 
-                    content = list(self._flatten(content, ctxt, apply_filters=False))
+                    content = list(self._flatten(content, ctxt))
 
                     def _apply(stream, ctxt):
-                        stream = list(stream)
                         ctxt.push(select=lambda path: Stream(stream).select(path))
                         for event in template:
@@ -866,32 +890,54 @@
         self._mtime = {}
 
-    def load(self, filename):
+    def load(self, filename, relative_to=None):
         """Load the template with the given name.
         
-        This method searches the search path trying to locate a template
-        matching the given name. If no such template is found, a
-        `TemplateNotFound` exception is raised. Otherwise, a `Template` object
-        representing the requested template is returned.
-        
-        Template searches are cached to avoid having to parse the same template
-        file more than once. Thus, subsequent calls of this method with the
-        same template file name will return the same `Template` object.
+        If the `filename` parameter is relative, this method searches the search
+        path trying to locate a template matching the given name. If the file
+        name is an absolute path, the search path is not bypassed.
+        
+        If requested template is not found, a `TemplateNotFound` exception is
+        raised. Otherwise, a `Template` object is returned that represents the
+        parsed template.
+        
+        Template instances are cached to avoid having to parse the same
+        template file more than once. Thus, subsequent calls of this method
+        with the same template file name will return the same `Template`
+        object (unless the `auto_reload` option is enabled and the file was
+        changed since the last parse.)
+        
+        If the `relative_to` parameter is provided, the `filename` is
+        interpreted as being relative to that path.
         
         @param filename: the relative path of the template file to load
+        @param relative_to: the filename of the template from which the new
+            template is being loaded, or `None` if the template is being loaded
+            directly
         """
+        if relative_to:
+            filename = posixpath.join(posixpath.dirname(relative_to), filename)
         filename = os.path.normpath(filename)
+
+        # First check the cache to avoid reparsing the same file
         try:
             tmpl = self._cache[filename]
             if not self.auto_reload or \
-                    os.path.getmtime(tmpl.filename) == self._mtime[filename]:
+                    os.path.getmtime(tmpl.filepath) == self._mtime[filename]:
                 return tmpl
         except KeyError:
             pass
-        for dirname in self.search_path:
+
+        # Bypass the search path if the filename is absolute
+        search_path = self.search_path
+        if os.path.isabs(filename):
+            search_path = [os.path.dirname(filename)]
+
+        for dirname in search_path:
             filepath = os.path.join(dirname, filename)
             try:
                 fileobj = file(filepath, 'rt')
                 try:
-                    tmpl = Template(fileobj, filename=filepath)
+                    from markup.filters import IncludeFilter
+                    tmpl = Template(fileobj, basedir=dirname, filename=filename)
                     tmpl.filters.append(IncludeFilter(self))
                 finally:
Index: /trunk/markup/tests/__init__.py
===================================================================
--- /trunk/markup/tests/__init__.py	(revision 20)
+++ /trunk/markup/tests/__init__.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 import doctest
Index: /trunk/markup/tests/builder.py
===================================================================
--- /trunk/markup/tests/builder.py	(revision 20)
+++ /trunk/markup/tests/builder.py	(revision 30)
@@ -6,9 +6,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 import doctest
@@ -23,5 +23,5 @@
 
     def test_link(self):
-        link = tag.A(href='#', title='Foo', accesskey=None)('Bar')
+        link = tag.a(href='#', title='Foo', accesskey=None)('Bar')
         bits = iter(link.generate())
         self.assertEqual((Stream.START, ('a', [('href', "#"), ('title', "Foo")]),
Index: /trunk/markup/tests/core.py
===================================================================
--- /trunk/markup/tests/core.py	(revision 20)
+++ /trunk/markup/tests/core.py	(revision 30)
@@ -6,15 +6,15 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 import doctest
-from HTMLParser import HTMLParseError
 import unittest
 
 from markup.core import *
+from markup.input import ParseError
 
 
@@ -124,7 +124,7 @@
         self.assertEquals('', str(markup.sanitize()))
         markup = Markup('<SCR\0IPT>alert("foo")</SCR\0IPT>')
-        self.assertRaises(HTMLParseError, markup.sanitize().render)
+        self.assertRaises(ParseError, markup.sanitize().render)
         markup = Markup('<SCRIPT&XYZ SRC="http://example.com/"></SCRIPT>')
-        self.assertRaises(HTMLParseError, markup.sanitize().render)
+        self.assertRaises(ParseError, markup.sanitize().render)
 
     def test_sanitize_remove_onclick_attr(self):
@@ -157,5 +157,5 @@
         # Grave accents (not parsed)
         markup = Markup('<IMG SRC=`javascript:alert("RSnake says, \'foo\'")`>')
-        self.assertRaises(HTMLParseError, markup.sanitize().render)
+        self.assertRaises(ParseError, markup.sanitize().render)
         # Protocol encoded using UTF-8 numeric entities
         markup = Markup('<IMG SRC=\'&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;'
Index: /trunk/markup/tests/eval.py
===================================================================
--- /trunk/markup/tests/eval.py	(revision 20)
+++ /trunk/markup/tests/eval.py	(revision 30)
@@ -6,19 +6,18 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at hhttp://markup.cmlenz.net/log/.
 
 import doctest
 import unittest
 
-from markup import eval
-
+from markup.eval import Expression
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(eval))
+    suite.addTest(doctest.DocTestSuite(Expression.__module__))
     return suite
 
Index: /trunk/markup/tests/input.py
===================================================================
--- /trunk/markup/tests/input.py	(revision 20)
+++ /trunk/markup/tests/input.py	(revision 30)
@@ -1,15 +1,16 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006 Christopher Lenz
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
+import doctest
 import unittest
 
@@ -18,12 +19,7 @@
 
 
-class XMLParserTestCase(unittest.TestCase):
-    pass
-
-
-
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(XMLParserTestCase, 'test'))
+    suite.addTest(doctest.DocTestSuite(XMLParser.__module__))
     return suite
 
Index: /trunk/markup/tests/output.py
===================================================================
--- /trunk/markup/tests/output.py	(revision 20)
+++ /trunk/markup/tests/output.py	(revision 30)
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006 Christopher Lenz
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 import doctest
Index: /trunk/markup/tests/path.py
===================================================================
--- /trunk/markup/tests/path.py	(revision 20)
+++ /trunk/markup/tests/path.py	(revision 30)
@@ -6,19 +6,89 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 import doctest
 import unittest
 
-from markup import path
+from markup.input import XML
+from markup.path import Path
+
+
+class PathTestCase(unittest.TestCase):
+
+    def test_1step(self):
+        xml = XML('<root/>')
+        self.assertEqual('<root/>', Path('root').select(xml).render())
+        self.assertEqual('<root/>', Path('//root').select(xml).render())
+
+    def test_1step_wildcard(self):
+        xml = XML('<root/>')
+        self.assertEqual('<root/>', Path('*').select(xml).render())
+        self.assertEqual('<root/>', Path('//*').select(xml).render())
+
+    def test_1step_attribute(self):
+        path = Path('@foo')
+        self.assertEqual('', path.select(XML('<root/>')).render())
+        self.assertEqual('bar', path.select(XML('<root foo="bar"/>')).render())
+
+    def test_1step_attribute(self):
+        path = Path('@foo')
+        self.assertEqual('', path.select(XML('<root/>')).render())
+        self.assertEqual('bar', path.select(XML('<root foo="bar"/>')).render())
+
+    def test_2step(self):
+        xml = XML('<root><foo/><bar/></root>')
+        self.assertEqual('<foo/><bar/>', Path('root/*').select(xml).render())
+        self.assertEqual('<bar/>', Path('root/bar').select(xml).render())
+        self.assertEqual('', Path('root/baz').select(xml).render())
+
+    def test_2step_complex(self):
+        xml = XML('<root><foo><bar/></foo></root>')
+        self.assertEqual('<bar/>', Path('foo/bar').select(xml).render())
+        self.assertEqual('<bar/>', Path('foo/*').select(xml).render())
+        self.assertEqual('', Path('root/bar').select(xml).render())
+
+        xml = XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>')
+        self.assertEqual('<bar id="2"/>', Path('root/bar').select(xml).render())
+
+    def test_2step_text(self):
+        xml = XML('<root><item>Foo</item></root>')
+        self.assertEqual('Foo', Path('item/text()').select(xml).render())
+        xml = XML('<root><item>Foo</item><item>Bar</item></root>')
+        self.assertEqual('FooBar', Path('item/text()').select(xml).render())
+
+    def test_3step(self):
+        xml = XML('<root><foo><bar/></foo></root>')
+        self.assertEqual('<bar/>', Path('root/foo/*').select(xml).render())
+
+    def test_3step_complex(self):
+        xml = XML('<root><foo><bar/></foo></root>')
+        self.assertEqual('<bar/>', Path('root/*/bar').select(xml).render())
+        xml = XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>')
+        self.assertEqual('<bar id="1"/><bar id="2"/>',
+                         Path('root//bar').select(xml).render())
+
+    def test_predicate_attr(self):
+        xml = XML('<root><item/><item important="very"/></root>')
+        self.assertEqual('<item important="very"/>',
+                         Path('root/item[@important]').select(xml).render())
+        self.assertEqual('<item important="very"/>',
+                         Path('root/item[@important="very"]').select(xml).render())
+
+        xml = XML('<root><item/><item important="notso"/></root>')
+        self.assertEqual('',
+                         Path('root/item[@important="very"]').select(xml).render())
+        self.assertEqual('<item/><item important="notso"/>',
+                         Path('root/item[@important!="very"]').select(xml).render())
 
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(path))
+    suite.addTest(doctest.DocTestSuite(Path.__module__))
+    suite.addTest(unittest.makeSuite(PathTestCase, 'test'))
     return suite
 
Index: /trunk/markup/tests/template.py
===================================================================
--- /trunk/markup/tests/template.py	(revision 20)
+++ /trunk/markup/tests/template.py	(revision 30)
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006 Christopher Lenz
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 import doctest
@@ -70,5 +70,5 @@
         xml = '<p xmlns:py="http://purl.org/kid/ns#" py:do="nothing" />'
         try:
-            tmpl = Template(xml, 'test.html')
+            tmpl = Template(xml, filename='test.html')
         except BadDirectiveError, e:
             self.assertEqual('test.html', e.filename)
@@ -78,5 +78,5 @@
     def test_directive_value_syntax_error(self):
         xml = '<p xmlns:py="http://purl.org/kid/ns#" py:if="bar\'" />'
-        tmpl = Template(xml, 'test.html')
+        tmpl = Template(xml, filename='test.html')
         try:
             list(tmpl.generate(Context()))
Index: /trunk/setup.py
===================================================================
--- /trunk/setup.py	(revision 20)
+++ /trunk/setup.py	(revision 30)
@@ -7,9 +7,9 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.com/license.html.
+# are also available at http://markup.cmlenz.net/wiki/License.
 #
 # This software consists of voluntary contributions made by many
 # individuals. For the exact contribution history, see the revision
-# history and logs, available at http://projects.edgewall.com/trac/.
+# history and logs, available at http://markup.cmlenz.net/log/.
 
 from setuptools import setup, find_packages
