changeset 21:b4d17897d053 trunk

* Include paths are now interpreted relative to the path of the including template. Closes #3. * The filename is now included as first item in the `pos` tuple of stream events. * Simplified the "basic" example so that it actually ''is'' basic. * Added a more complex example using nested relative includes in [source:/trunk/examples/includes/ examples/includes].
author cmlenz
date Tue, 20 Jun 2006 13:05:37 +0000
parents cc92d74ce9e5
children 2483fe549959
files examples/basic/common/default_header.html examples/basic/common/default_header.kid examples/basic/common/macros.html examples/basic/common/macros.kid examples/basic/kidrun.py examples/basic/layout.html examples/basic/layout.kid examples/basic/module/test.html examples/basic/module/test.kid examples/basic/run.py examples/basic/test.html examples/basic/test.kid examples/includes/common/macros.html examples/includes/module/test.html examples/includes/run.py examples/includes/skins/default/footer.html examples/includes/skins/default/header.html examples/includes/skins/default/layout.html markup/__init__.py markup/filters.py markup/input.py markup/plugin.py markup/template.py markup/tests/core.py markup/tests/template.py
diffstat 21 files changed, 270 insertions(+), 126 deletions(-) [+]
line wrap: on
line diff
deleted file mode 100644
--- a/examples/basic/common/default_header.html
+++ /dev/null
@@ -1,4 +0,0 @@
-<head>
- <title>Hello ${hello}</title>
- <style type="text/css">@import(style.css)</style>
-</head>
deleted file mode 100644
--- a/examples/basic/common/default_header.kid
+++ /dev/null
@@ -1,3 +0,0 @@
-<head>
- <title>Hello ${hello}</title>
-</head>
--- a/examples/basic/kidrun.py
+++ b/examples/basic/kidrun.py
@@ -6,8 +6,7 @@
 
 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,
                 items=['Number %d' % num for num in range(1, 15)],
rename from examples/basic/common/macros.html
rename to examples/basic/layout.html
--- a/examples/basic/common/macros.html
+++ b/examples/basic/layout.html
@@ -1,8 +1,11 @@
-<div xmlns:py="http://purl.org/kid/ns#"
-     py:strip="">
+<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()}
+    Hello ${name.title()}
   </div>
   <span py:match="greeting" class="greeting">
     Hello ${select('@name')}
rename from examples/basic/common/macros.kid
rename to examples/basic/layout.kid
--- a/examples/basic/common/macros.kid
+++ b/examples/basic/layout.kid
@@ -1,13 +1,15 @@
-<div xmlns:py="http://purl.org/kid/ns#"
-     py:extends="'default_header.kid'" py:strip="">
+<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()}
+    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'" style="text-decoration: underline">
-    ${item.findtext('')}
-  </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>
--- a/examples/basic/run.py
+++ b/examples/basic/run.py
@@ -9,9 +9,7 @@
 
 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()
     tmpl = loader.load('test.html')
rename from examples/basic/module/test.html
rename to examples/basic/test.html
--- a/examples/basic/module/test.html
+++ b/examples/basic/test.html
@@ -3,9 +3,9 @@
     "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}_header.html" />
- <xi:include href="macros.html" />
+      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">
rename from examples/basic/module/test.kid
rename to examples/basic/test.kid
--- a/examples/basic/module/test.kid
+++ b/examples/basic/test.kid
@@ -1,8 +1,10 @@
 <!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="'macros.kid'" lang="en">
+<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>
new file mode 100644
--- /dev/null
+++ b/examples/includes/common/macros.html
@@ -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>
new file mode 100644
--- /dev/null
+++ b/examples/includes/module/test.html
@@ -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>
new file mode 100755
--- /dev/null
+++ b/examples/includes/run.py
@@ -0,0 +1,50 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from datetime import datetime, timedelta
+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()
+        sys.stdout.write('.')
+        sys.stdout.flush()
+        times.append(timing.milli())
+    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()
new file mode 100644
--- /dev/null
+++ b/examples/includes/skins/default/footer.html
@@ -0,0 +1,4 @@
+<div id="footer">
+  <hr />
+  <h1>And goodbye</h1>
+</div>
new file mode 100644
--- /dev/null
+++ b/examples/includes/skins/default/header.html
@@ -0,0 +1,3 @@
+<div id="masthead">
+  <h1>Welcome</h1>
+</div>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/includes/skins/default/layout.html
@@ -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>
--- a/markup/__init__.py
+++ b/markup/__init__.py
@@ -54,4 +54,4 @@
 """
 
 from markup.core import *
-from markup.input import XML, HTML
+from markup.input import ParseError, XML, HTML
--- a/markup/filters.py
+++ b/markup/filters.py
@@ -53,7 +53,6 @@
             ns_prefixes = []
         in_fallback = False
         include_href, fallback_stream = None, None
-        indent = 0
 
         for kind, data, pos in stream:
 
@@ -62,7 +61,6 @@
                 tag, attrib = data
                 if tag.localname == 'include':
                     include_href = attrib.get('href')
-                    indent = pos[1]
                 elif tag.localname == 'fallback':
                     in_fallback = True
                     fallback_stream = []
@@ -73,7 +71,8 @@
                         if not include_href:
                             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
 
@@ -85,7 +84,6 @@
 
                     include_href = None
                     fallback_stream = None
-                    indent = 0
 
                 elif data.localname == 'fallback':
                     in_fallback = False
--- a/markup/input.py
+++ b/markup/input.py
@@ -24,12 +24,24 @@
 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):
+    def __init__(self, source, filename=None):
         self.source = source
+        self.filename = filename
 
         # Setup the Expat parser
         parser = expat.ParserCreate('utf-8', '}')
@@ -48,73 +60,80 @@
 
         # 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 _getpos_unknown(self):
-        return (-1, -1)
+        return (self.filename or '<string>', -1, -1)
 
-    def getpos(self):
-        return self.expat.CurrentLineNumber, self.expat.CurrentColumnNumber
+    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._queue.append((Stream.START, (QName(tag), Attributes(attrib.items())),
+                           self._getpos()))
 
     def _handle_end(self, tag):
-        self.queue.append((Stream.END, QName(tag), self.getpos()))
+        self._queue.append((Stream.END, QName(tag), self._getpos()))
 
     def _handle_data(self, text):
-        self.queue.append((Stream.TEXT, text, self.getpos()))
+        self._queue.append((Stream.TEXT, text, self._getpos()))
 
     def _handle_prolog(self, version, encoding, standalone):
-        self.queue.append((Stream.PROLOG, (version, encoding, standalone),
-                           self.getpos()))
+        self._queue.append((Stream.PROLOG, (version, encoding, standalone),
+                           self._getpos()))
 
     def _handle_doctype(self, name, sysid, pubid, has_internal_subset):
-        self.queue.append((Stream.DOCTYPE, (name, pubid, sysid), self.getpos()))
+        self._queue.append((Stream.DOCTYPE, (name, pubid, sysid), self._getpos()))
 
     def _handle_start_ns(self, prefix, uri):
-        self.queue.append((Stream.START_NS, (prefix or '', uri), self.getpos()))
+        self._queue.append((Stream.START_NS, (prefix or '', uri), self._getpos()))
 
     def _handle_end_ns(self, prefix):
-        self.queue.append((Stream.END_NS, prefix or '', self.getpos()))
+        self._queue.append((Stream.END_NS, prefix or '', self._getpos()))
 
     def _handle_pi(self, target, data):
-        self.queue.append((Stream.PI, (target, data), self.getpos()))
+        self._queue.append((Stream.PI, (target, data), self._getpos()))
 
     def _handle_comment(self, text):
-        self.queue.append((Stream.COMMENT, text, self.getpos()))
+        self._queue.append((Stream.COMMENT, text, self._getpos()))
 
     def _handle_other(self, text):
         if text.startswith('&'):
             # deal with undefined entities
             try:
                 text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
-                self.queue.append((Stream.TEXT, text, self.getpos()))
+                self._queue.append((Stream.TEXT, text, self._getpos()))
             except KeyError:
-                lineno, offset = self.getpos()
+                lineno, offset = self._getpos()
                 raise expat.error("undefined entity %s: line %d, column %d" %
                                   (text, lineno, offset))
 
@@ -123,7 +142,7 @@
     return Stream(list(XMLParser(StringIO(text))))
 
 
-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
@@ -134,68 +153,79 @@
                               'hr', 'img', 'input', 'isindex', 'link', 'meta',
                               '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 _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))
+        pos = self._getpos()
+        self._queue.append((Stream.START, (QName(tag), Attributes(attrib)), pos))
         if tag in self._EMPTY_ELEMS:
-            self.queue.append((Stream.END, QName(tag), pos))
+            self._queue.append((Stream.END, QName(tag), pos))
         else:
             self._open_tags.append(tag)
 
     def handle_endtag(self, tag):
         if tag not in self._EMPTY_ELEMS:
-            pos = self.getpos()
+            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._queue.append((Stream.END, QName(open_tag), pos))
+            self._queue.append((Stream.END, QName(tag), pos))
 
     def handle_data(self, text):
-        self.queue.append((Stream.TEXT, text, self.getpos()))
+        self._queue.append((Stream.TEXT, text, self._getpos()))
 
     def handle_charref(self, name):
-        self.queue.append((Stream.TEXT, Markup('&#%s;' % name), self.getpos()))
+        self._queue.append((Stream.TEXT, Markup('&#%s;' % name), self._getpos()))
 
     def handle_entityref(self, name):
-        self.queue.append((Stream.TEXT, Markup('&%s;' % name), self.getpos()))
+        self._queue.append((Stream.TEXT, Markup('&%s;' % name), self._getpos()))
 
     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._queue.append((Stream.PI, (target.strip(), data.strip()),
+                           self._getpos()))
 
     def handle_comment(self, text):
-        self.queue.append((Stream.COMMENT, text, self.getpos()))
+        self._queue.append((Stream.COMMENT, text, self._getpos()))
 
 
 def HTML(text):
--- a/markup/plugin.py
+++ b/markup/plugin.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Mattew Good
+# Copyright (C) 2006 Matthew Good
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -34,18 +34,16 @@
         if divider >= 0:
             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)
 
--- a/markup/template.py
+++ b/markup/template.py
@@ -43,6 +43,7 @@
 
 import compiler
 import os
+import posixpath
 import re
 from StringIO import StringIO
 
@@ -574,20 +575,24 @@
     _dir_by_name = dict(directives)
     _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):
             self.source = StringIO(source)
         else:
             self.source = source
+        self.basedir = basedir
         self.filename = filename or '<string>'
+        if basedir and filename:
+            self.filepath = os.path.join(basedir, filename)
+        else:
+            self.filepath = '<string>'
 
         self.filters = [self._eval, self._match]
         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):
         """Parse the template.
@@ -603,7 +608,7 @@
         ns_prefix = {}
         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:
                 # Strip out the namespace declaration for template directives
@@ -628,7 +633,7 @@
                     if name in self.NAMESPACE:
                         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))
                     else:
@@ -666,7 +671,7 @@
     _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}')
     _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.
         
         This method returns a list containing both literal text and `Expression`
@@ -768,8 +773,8 @@
                 else:
                     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):
         for kind, data, pos in stream:
@@ -783,7 +788,7 @@
             for idx, (test, path, template) in enumerate(ctxt._match_templates):
                 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
 
                 result = test(kind, data, pos)
@@ -804,7 +809,7 @@
                         # enable the path to keep track of the stream state
                         test(*event)
 
-                    content = list(self._flatten(content, ctxt, apply_filters=False))
+                    content = list(self._flatten(content, ctxt, False))
 
                     def _apply(stream, ctxt):
                         stream = list(stream)
@@ -865,7 +870,7 @@
         self._cache = {}
         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
@@ -877,13 +882,21 @@
         file more than once. Thus, subsequent calls of this method with the
         same template file name will return the same `Template` object.
         
+        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)
         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
@@ -892,7 +905,7 @@
             try:
                 fileobj = file(filepath, 'rt')
                 try:
-                    tmpl = Template(fileobj, filename=filepath)
+                    tmpl = Template(fileobj, basedir=dirname, filename=filename)
                     tmpl.filters.append(IncludeFilter(self))
                 finally:
                     fileobj.close()
--- a/markup/tests/core.py
+++ b/markup/tests/core.py
@@ -12,10 +12,10 @@
 # history and logs, available at http://projects.edgewall.com/trac/.
 
 import doctest
-from HTMLParser import HTMLParseError
 import unittest
 
 from markup.core import *
+from markup.input import ParseError
 
 
 class MarkupTestCase(unittest.TestCase):
@@ -123,9 +123,9 @@
         markup = Markup('<SCRIPT SRC="http://example.com/"></SCRIPT>')
         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):
         markup = Markup('<div onclick=\'alert("foo")\' />')
@@ -156,7 +156,7 @@
         self.assertEquals('<img/>', str(markup.sanitize()))
         # 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;'
                         '&#112;&#116;&#58;alert("foo")\'>')
--- a/markup/tests/template.py
+++ b/markup/tests/template.py
@@ -69,7 +69,7 @@
     def test_bad_directive_error(self):
         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)
             if sys.version_info[:2] >= (2, 4):
@@ -77,7 +77,7 @@
 
     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()))
             self.fail('Expected SyntaxError')
Copyright (C) 2012-2017 Edgewall Software